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
});
}