diff --git a/module.json b/module.json new file mode 100644 index 0000000..eee39d9 --- /dev/null +++ b/module.json @@ -0,0 +1,52 @@ +{ + "id": "pf2e-roll-manager", + "title": "Pathfinder 2E Roll Manager", + "description": "This module makes setting up DC checks easier for groups of players and busy GMs.", + "version": "#{VERSION}#", + "library": "false", + "manifestPlusVersion": "1.2.0", + "compatibility": { + "minimum": 11, + "verified": 12, + "maximum": 12 + }, + "authors": [ + { + "name": "Ingram Blakelock (Mythica Machina)", + "url": "https://mythicamachina.com/", + "discord": "garsondee" + } + ], + "relationships": { + "systems": [], + "requires": [] + }, + "esmodules": [ + "scripts/module.js" + ], + "scripts": [ + "scripts/lib/lib.js" + ], + "styles": [ + "styles/module.css" + ], + "languages": [ + { + "lang": "en", + "name": "English", + "path": "languages/en.json" + } + ], + "url": "#{URL}#", + "manifest": "#{MANIFEST}#", + "download": "#{DOWNLOAD}#", + "license": "LICENSE", + "readme": "README.md", + "media": [ + { + "type": "icon", + "url": "https://avatars2.githubusercontent.com/u/71292812?s=400&u=ccdb4eeb7abf551ca8f314e5a9bfd0479a4d3d41&v=4" + } + ], + "socket": true +} diff --git a/package.json b/package.json deleted file mode 100644 index fd2a7ca..0000000 --- a/package.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "private": true, - "name": "pf2e-roll-manager", - "description": "This module makes setting up DC checks easier for groups of players and busy GMs.", - "license": "MIT", - "homepage": "https://github.com/garsondee/pf2e-roll-manager", - "repository": { - "type": "git", - "url": "https://github.com/garsondee/pf2e-roll-manager.git" - }, - "bugs": { - "url": "https://github.com/garsondee/pf2e-roll-manager/issues" - }, - "contributors": [ - { - "name": "Ingram Blakelock (Mythica Machina)", - "url": "https://mythicamachina.com/" - } - ], - "type": "module", - "scripts": { - "build": "gulp build", - "build:watch": "gulp watch", - "link-project": "gulp link", - "clean": "gulp clean", - "clean:link": "gulp link --clean", - "typecheck": "tsc --noEmit", - "lint": "eslint --ext .ts,.js,.cjs,.mjs .", - "lint:fix": "eslint --ext .ts,.js,.cjs,.mjs --fix .", - "format": "prettier --write \"./**/*.(ts|js|cjs|mjs|json|yml|scss)\"", - "test": "jest", - "test:watch": "jest --watch", - "test:ci": "jest --ci --reporters=default --reporters=jest-junit", - "postinstall": "husky install" - }, - "devDependencies": { - "@league-of-foundry-developers/foundry-vtt-types": "^9.280.0", - "@rollup/plugin-node-resolve": "^15.2.3", - "@rollup/plugin-typescript": "^11.1.6", - "@rollup/stream": "^3.0.1", - "@typescript-eslint/eslint-plugin": "^7.13.0", - "@typescript-eslint/parser": "^7.13.0", - "eslint": "^8.57.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-jest": "^28.6.0", - "eslint-plugin-prettier": "^5.1.3", - "fs-extra": "^11.2.0", - "gulp": "^5.0.0", - "gulp-dart-sass": "^1.1.0", - "gulp-sourcemaps": "^3.0.0", - "husky": "^9.0.11", - "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0", - "jest-junit": "^16.0.0", - "lint-staged": "^15.2.5", - "prettier": "^3.3.2", - "rollup": "^2.79.1", - "ts-jest": "^29.1.4", - "tslib": "^2.6.3", - "typescript": "^5.4.5", - "vinyl-buffer": "^1.0.1", - "vinyl-source-stream": "^2.0.0", - "yargs": "^17.7.2" - }, - "lint-staged": { - "*.(ts|js|cjs|mjs)": "eslint --fix", - "*.(json|yml|scss)": "prettier --write" - } -} diff --git a/scripts/module.js b/scripts/module.js index 4c18ac9..c0b16b9 100644 --- a/scripts/module.js +++ b/scripts/module.js @@ -1,3 +1,74 @@ +function handleDiceButtonClick(preSelectedSkills, dc) { + createActionDropdown({ + defaultDC: dc, + excludeActions: [], + gameSystem: game.pf2e, + defaultRollMode: "publicroll", + defaultCreateMessage: true, + defaultSkipDialog: false, + }).then(dialog => { + // Use the render hook to ensure the dialog is fully rendered + Hooks.once('renderDialog', (app, html) => { + // Pre-select the skills in the dialog + preSelectedSkills.forEach(skill => { + const skillButton = html.find(`.skill-button[data-slug="${skill.toLowerCase()}"]`); + if (skillButton.length) { + skillButton.addClass('selected'); + } else { + console.error('Skill button not found in dialog for skill:', skill); + } + }); + + // Set the DC value in the dialog + const dcInput = html.find('#dc-input'); + if (dcInput.length) { + dcInput.val(dc); + dcInput.trigger('change'); // Trigger change event to ensure any listeners are updated + } else { + console.error('DC input field not found in dialog.'); + } + }); + }).catch(error => { + console.error('Error opening GM dialog:', error); + }); +} + +// Hook into the rendering process of applications +Hooks.on("renderApplication", (app, html, data) => { + // Check if the current user is a GM + if (!game.user.isGM) return; + + // Find all inline check elements with the 'with-repost' class + html.find("a.inline-check.with-repost").each(function () { + const skillCheckElement = $(this); + + // Create the button element with additional CSS styles + const button = $(``); + const icon = $('').css({ + fontSize: '14px', + position: 'relative', + top: '-6px', + right: '-1px' + }); + + // Append the icon to the button + button.append(icon); + + // Append the button to the skill check element + button.insertAfter(skillCheckElement); + + // Extract the skill type and DC from the inline check button + const skillType = skillCheckElement.attr('data-pf2-check'); + const dc = parseInt(skillCheckElement.attr('data-pf2-dc'), 10); + + // Add click event listener to the button + button.on("click", function () { + const preSelectedSkills = skillType ? [skillType.charAt(0).toUpperCase() + skillType.slice(1)] : []; + handleDiceButtonClick(preSelectedSkills, dc); + }); + }); +}); + const multiStatisticActions = { 'arrest-a-fall': ['acrobatics', 'reflex'], 'decipher-writing': ['arcana', 'occultism', 'society', 'religion'], @@ -213,7 +284,7 @@ let selectedCharacterUUIDs = new Set(); function attachCharacterSelectionListeners(container) { console.log("Attaching character selection listeners..."); container.querySelectorAll('.character-select-button').forEach(button => { - console.log("Attaching listener to button:", button); + // console.log("Attaching listener to button:", button); button.addEventListener('click', (event) => { const button = event.currentTarget; const actorUuid = button.dataset.actorUuid; @@ -765,21 +836,23 @@ async function executeInstantRoll(selectedActors, selectedActions, dc, createMes statistic = null; } console.log(`Processing action: ${actionSlug}, statistic: ${statistic}`); - // Check if the action is listed under 'Multiple' and if it was selected from the initial dialog - if (multiStatisticActions[actionSlug] && fromDialog) { - ui.notifications.warn(`The action "${actionSlug}" cannot be used with Instant Roll. Please select a specific skill.`); - return; - } - const tokens = actor.getActiveTokens(true); - if (tokens.length > 0) { - const token = tokens[0]; - console.log(`Token found for actor ${actor.name}:`, token); - token.control({releaseOthers: true}); - } else { - console.error(`No active token found for actor ${actor.name} with UUID ${actor.uuid}`); - ui.notifications.error(`No active token found for actor ${actor.name} with UUID ${actor.uuid}.`); - continue; + + // Check if the action requires a token + const requiresToken = !['perception', 'acrobatics', 'arcana', 'athletics', 'crafting', 'deception', 'diplomacy', 'intimidation', 'medicine', 'nature', 'occultism', 'performance', 'religion', 'society', 'stealth', 'survival', 'thievery', 'fortitude', 'reflex', 'will'].includes(actionSlug); + + if (requiresToken) { + const tokens = actor.getActiveTokens(true); + if (tokens.length > 0) { + const token = tokens[0]; + console.log(`Token found for actor ${actor.name}:`, token); + token.control({releaseOthers: true}); + } else { + console.error(`No active token found for actor ${actor.name} with UUID ${actor.uuid}`); + ui.notifications.error(`No active token found for actor ${actor.name} with UUID ${actor.uuid}.`); + continue; + } } + let result; try { const rollOptions = {event: new Event('click'), rollMode, createMessage, secret: isBlindRoll}; @@ -945,14 +1018,22 @@ async function executeActionRoll(actor, actionSlug, variantSlug, dc, rollOptions if (!action) { throw new Error(`Action ${actionSlug} not found.`); } - // Temporarily select the token that the actor owns in the scene - const token = canvas.tokens.placeables.find(t => t.actor?.id === actor.id); - if (token) { - token.control({releaseOthers: true}); - } else { - throw new Error(`No token found for actor ${actor.name}`); + + // Check if the action requires a token + const requiresToken = true; // Actions always require a token + + if (requiresToken) { + // Temporarily select the token that the actor owns in the scene + const token = canvas.tokens.placeables.find(t => t.actor?.id === actor.id); + if (token) { + token.control({releaseOthers: true}); + } else { + throw new Error(`No token found for actor ${actor.name}`); + } } + const useOptions = {...rollOptions, difficultyClass: dc, skipDialog: true, actor}; + // Apply overrides if they exist if (actionOverrides[actionSlug]) { const override = actionOverrides[actionSlug]; @@ -962,6 +1043,7 @@ async function executeActionRoll(actor, actionSlug, variantSlug, dc, rollOptions variantSlug = override.variants[0]; } } + // Handle specific actions that should not use variants if (actionSlug === "identify-magic" || actionSlug === "recall-knowledge") { if (!selectedStatistic) { @@ -970,6 +1052,7 @@ async function executeActionRoll(actor, actionSlug, variantSlug, dc, rollOptions useOptions.statistic = selectedStatistic; variantSlug = null; // Ensure no variant is used } + let result; try { // Attempt to use the action without any variant first @@ -1275,38 +1358,52 @@ async function generateCharacterRollBoxes(selectedCharacters, skillsToRoll, dc, async function addRollButtonEventListener(rollButton, character, skillSelect, box, dc, characterBoxes, resultsSummary) { rollButton.addEventListener('click', async () => { - console.log(`Attempting to find token for actor ${character.name} with UUID ${character.uuid}`); - const token = canvas.tokens.placeables.find(t => t.actor?.uuid === character.uuid); // Use UUID instead of ID - if (token) { - console.log(`Token found for actor ${character.name}:`, token); - token.control({releaseOthers: true}); - } else { - console.error(`No active token found for actor ${character.name} with UUID ${character.uuid}`); - notifyTokenRequired(character.name); - return; - } const selectedSlug = skillSelect.value; const selectedActions = [selectedSlug]; const selectedActors = [character]; + + // Check if the action requires a token + const requiresToken = !['perception', 'acrobatics', 'arcana', 'athletics', 'crafting', 'deception', 'diplomacy', 'intimidation', 'medicine', 'nature', 'occultism', 'performance', 'religion', 'society', 'stealth', 'survival', 'thievery', 'fortitude', 'reflex', 'will'].includes(selectedSlug.split(':')[0]); + + if (requiresToken) { + console.log(`Attempting to find token for actor ${character.name} with UUID ${character.uuid}`); + const token = canvas.tokens.placeables.find(t => t.actor?.uuid === character.uuid); // Use UUID instead of ID + if (token) { + console.log(`Token found for actor ${character.name}:`, token); + token.control({releaseOthers: true}); + } else { + console.error(`No active token found for actor ${character.name} with UUID ${character.uuid}`); + notifyTokenRequired(character.name); + return; + } + } + await executeInstantRoll(selectedActors, selectedActions, dc, true, true, 'publicroll', null, false); // Pass false for fromDialog }); } function addRollBlindButtonEventListener(rollButton, character, skillSelect, box, dc, characterBoxes, resultsSummary) { rollButton.addEventListener('click', async () => { - console.log(`Attempting to find token for actor ${character.name} with UUID ${character.uuid}`); - const token = canvas.tokens.placeables.find(t => t.actor?.uuid === character.uuid); // Use UUID instead of ID - if (token) { - console.log(`Token found for actor ${character.name}:`, token); - token.control({releaseOthers: true}); - } else { - console.error(`No active token found for actor ${character.name} with UUID ${character.uuid}`); - notifyTokenRequired(character.name); - return; - } const selectedSlug = skillSelect.value; const selectedActions = [selectedSlug]; const selectedActors = [character]; + + // Check if the action requires a token + const requiresToken = !['perception', 'acrobatics', 'arcana', 'athletics', 'crafting', 'deception', 'diplomacy', 'intimidation', 'medicine', 'nature', 'occultism', 'performance', 'religion', 'society', 'stealth', 'survival', 'thievery', 'fortitude', 'reflex', 'will'].includes(selectedSlug.split(':')[0]); + + if (requiresToken) { + console.log(`Attempting to find token for actor ${character.name} with UUID ${character.uuid}`); + const token = canvas.tokens.placeables.find(t => t.actor?.uuid === character.uuid); // Use UUID instead of ID + if (token) { + console.log(`Token found for actor ${character.name}:`, token); + token.control({releaseOthers: true}); + } else { + console.error(`No active token found for actor ${character.name} with UUID ${character.uuid}`); + notifyTokenRequired(character.name); + return; + } + } + await executeInstantRoll(selectedActors, selectedActions, dc, true, true, 'blindroll', null, false, {secret: true}); // Pass false for fromDialog and add secret option }); }