diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..6ec7574 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,358 @@ +/** + * ESLint configuration for Vermine2047 FoundryVTT system + * Compatible with FoundryVTT v11-v14 + */ +module.exports = { + root: true, + env: { + browser: true, + es2022: true, + node: false + }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended' + ], + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + ecmaFeatures: { + impliedStrict: true + } + }, + globals: { + // FoundryVTT global objects + game: 'readonly', + ui: 'readonly', + Hooks: 'readonly', + CONFIG: 'readonly', + Canvas: 'readonly', + ChatMessage: 'readonly', + Roll: 'readonly', + Actor: 'readonly', + Item: 'readonly', + Dialog: 'readonly', + foundry: 'readonly', + Handlebars: 'readonly', + renderTemplate: 'readonly' + }, + rules: { + // Possible Problems + 'no-console': 'error', + 'no-constant-condition': 'error', + 'no-control-regex': 'error', + 'no-debugger': 'error', + 'no-dupe-args': 'error', + 'no-dupe-keys': 'error', + 'no-duplicate-case': 'error', + 'no-empty': 'warn', + 'no-empty-character-class': 'error', + 'no-ex-assign': 'error', + 'no-extra-boolean-cast': 'error', + 'no-fallthrough': 'error', + 'no-func-assign': 'error', + 'no-inner-html': 'off', // Foundry uses innerHTML extensively + 'no-invalid-regexp': 'error', + 'no-irregular-whitespace': 'error', + 'no-misleading-character-class': 'error', + 'no-new-symbol': 'error', + 'no-obj-calls': 'error', + 'no-octal': 'error', + 'no-prototype-builtins': 'error', + 'no-regex-spaces': 'error', + 'no-self-assign': 'error', + 'no-sparse-arrays': 'error', + 'no-template-curly-in-string': 'warn', + 'no-unexpected-multiline': 'error', + 'no-unreachable': 'error', + 'no-unsafe-finally': 'error', + 'no-unsafe-negation': 'error', + 'no-unsafe-optional-chaining': 'error', + 'no-useless-backreference': 'error', + 'require-atomic-updates': 'off', + 'use-isnan': 'error', + 'valid-typeof': 'error', + + // Suggestions + 'accessor-pairs': 'warn', + 'arrow-body-style': ['warn', 'as-needed'], + 'block-scoped-var': 'error', + 'camelcase': ['warn', { allow: ['^_', '^VERMINE_'] }], + 'class-methods-use-this': 'off', // Many utility methods don't use this + 'complexity': ['warn', 20], + 'consistent-return': 'warn', + 'consistent-this': 'warn', + 'curly': ['warn', 'multi-line', 'consistent'], + 'default-case': 'warn', + 'default-case-last': 'warn', + 'default-param-last': 'warn', + 'dot-locale-compare': 'warn', + 'dot-notation': ['warn', { allowKeywords: true }], + 'eqeqeq': ['error', 'always', { null: 'ignore' }], + 'func-name-matching': 'warn', + 'func-names': ['warn', 'as-needed'], + 'func-style': ['warn', 'declaration', { allowArrowFunctions: true }], + 'grouped-accessor-pairs': 'warn', + 'guard-for-in': 'warn', + 'id-blacklist': 'off', + 'id-length': 'off', + 'id-match': 'off', + 'init-declarations': ['warn', 'always'], + 'line-comment-position': 'off', + 'lines-between-class-members': ['warn', 'always', { exceptAfterSingleLine: true }], + 'logical-assignment-operators': ['warn', 'always'], + 'max-classes-per-file': ['warn', 1], + 'max-depth': ['warn', 5], + 'max-lines': ['warn', { max: 500, skipBlankLines: true, skipComments: true }], + 'max-lines-per-function': ['warn', { max: 100, skipBlankLines: true, skipComments: true, IIFEs: true }], + 'max-nested-callbacks': ['warn', 3], + 'max-params': ['warn', 5], + 'max-statements': ['warn', 30], + 'multiline-comment-style': 'off', + 'new-cap': ['warn', { newIsCap: true, capIsNew: false }], + 'no-alert': 'warn', + 'no-array-constructor': 'error', + 'no-bitwise': 'warn', + 'no-caller': 'error', + 'no-case-declarations': 'error', + 'no-class-assign': 'error', + 'no-cond-assign': ['error', 'always'], + 'no-confusing-arrow': ['warn', { allowParens: true }], + 'no-const-assign': 'error', + 'no-continue': 'off', + 'no-delete-var': 'error', + 'no-div-regex': 'warn', + 'no-else-return': ['warn', { allowElseIf: true }], + 'no-empty-destructuring': 'warn', + 'no-empty-function': ['warn', { allow: ['constructors'] }], + 'no-empty-pattern': 'warn', + 'no-eq-null': 'off', + 'no-eval': 'error', + 'no-extend-native': 'error', + 'no-extra-bind': 'warn', + 'no-extra-label': 'warn', + 'no-floating-decimal': 'warn', + 'no-global-assign': 'error', + 'no-implicit-coercion': ['warn', { allow: ['!!', '+'] }], + 'no-implicit-globals': 'error', + 'no-implied-eval': 'error', + 'no-inline-comments': 'off', + 'no-invalid-this': 'off', + 'no-iterator': 'warn', + 'no-label-var': 'error', + 'no-labels': ['warn', { allowLoop: true, allowSwitch: true }], + 'no-lone-blocks': 'warn', + 'no-lonely-if': 'warn', + 'no-loop-func': 'warn', + 'no-magic-numbers': ['warn', { ignore: [0, 1, 2], ignoreEnums: true, ignoreNumericLiteralTypes: true, ignoreArrayIndexes: true }], + 'no-multi-assign': 'warn', + 'no-multi-str': 'warn', + 'no-negated-condition': 'warn', + 'no-nested-ternary': 'warn', + 'no-new': 'warn', + 'no-new-func': 'warn', + 'no-new-wrappers': 'error', + 'no-nonoctal-decimal-escape': 'error', + 'no-object-multi-space': 'warn', + 'no-octal-escape': 'error', + 'no-param-reassign': ['warn', { props: false }], + 'no-plusplus': 'off', + 'no-promise-executor-return': 'error', + 'no-proto': 'error', + 'no-redeclare': 'error', + 'no-regex-spaces': 'error', + 'no-restricted-globals': 'off', + 'no-restricted-imports': 'off', + 'no-restricted-modules': 'off', + 'no-restricted-properties': 'off', + 'no-restricted-syntax': 'off', + 'no-return-assign': ['error', 'always'], + 'no-return-await': 'error', + 'no-script-url': 'warn', + 'no-sequences': 'error', + 'no-setter-return': 'error', + 'no-shadow': 'warn', + 'no-shadow-restricted-names': 'error', + 'no-sparse-arrays': 'error', + 'no-tabs': 'warn', + 'no-template-curly-in-string': 'warn', + 'no-ternary': 'off', + 'no-this-before-super': 'error', + 'no-throw-literal': 'warn', + 'no-undef': ['error', { typeof: true }], + 'no-undef-init': 'warn', + 'no-undefined': 'off', + 'no-underscore-dangle': ['warn', { allow: ['_id', '_on', '_source', '_total', '_html'] }], + 'no-unneeded-ternary': ['warn', { defaultAssignment: false }], + 'no-unreachable-loop': 'warn', + 'no-unsafe-finally': 'error', + 'no-unsafe-negation': 'error', + 'no-unused-expressions': ['warn', { allowShortCircuit: true, allowTernary: true, enforceForJSX: false }], + 'no-unused-labels': 'warn', + 'no-unused-private-class-members': 'warn', + 'no-unused-vars': ['warn', { args: 'none', caughtErrors: 'none', ignoreRestSiblings: true }], + 'no-use-before-define': ['warn', { functions: false, classes: false, variables: true }], + 'no-useless-call': 'warn', + 'no-useless-catch': 'warn', + 'no-useless-computed-key': ['warn', { enforceForClassMembers: false }], + 'no-useless-concat': 'warn', + 'no-useless-constructor': 'warn', + 'no-useless-escape': 'warn', + 'no-useless-rename': 'warn', + 'no-useless-return': 'warn', + 'no-var': 'error', + 'no-void': ['warn', { allowAsStatement: true }], + 'no-warning-comments': 'warn', + 'no-with': 'error', + 'object-shorthand': ['warn', 'always', { ignoreConstructors: false, avoidQuotes: true }], + 'one-var': ['warn', 'never'], + 'one-var-declaration-per-line': ['warn', 'initializations'], + 'operator-assignment': ['warn', 'always'], + 'prefer-arrow-callback': ['warn', { classPropertiesAllowed: true, disallowTLSClassFields: true }], + 'prefer-const': ['error', { destructuring: 'all', ignoreReadBeforeAssign: false }], + 'prefer-destructuring': ['warn', { array: false, object: true }], + 'prefer-exponentiation-operator': 'warn', + 'prefer-named-capture-group': 'off', + 'prefer-numeric-literals': 'warn', + 'prefer-object-has-own': 'warn', + 'prefer-object-spread': 'warn', + 'prefer-promise-reject-errors': ['error', { allowEmptyReject: false }], + 'prefer-regex-literals': ['warn', { disallowRedundantWrapping: true }], + 'prefer-rest-params': 'warn', + 'prefer-spread': 'warn', + 'prefer-template': 'warn', + 'quote-props': ['warn', 'as-needed', { keywords: true, unnecessaryQuote: false, numbers: true }], + 'radix': ['error', 'always'], + 'require-await': 'warn', + 'require-unicode-regexp': 'off', + 'require-yield': 'error', + 'sort-imports': 'off', + 'sort-keys': 'off', + 'sort-vars': 'off', + 'spaced-comment': ['warn', 'always', { line: { markers: ['!', '/'] }, block: { balanced: true, markers: ['!', '*'], exceptions: ['*'] } }], + 'strict': ['error', 'never'], + 'symbol-description': 'warn', + 'unicode-bom': ['error', 'never'], + 'vars-on-top': 'off', + 'yoda': ['warn', 'never', { exceptRange: true }], + + // Layout & Formatting + 'array-bracket-newline': ['warn', 'consistent'], + 'array-bracket-spacing': ['warn', 'never'], + 'array-element-newline': ['warn', 'consistent'], + 'arrow-parens': ['warn', 'always'], + 'arrow-spacing': ['warn', { before: true, after: true }], + 'block-spacing': ['warn', 'always'], + 'brace-style': ['warn', '1tbs', { allowSingleLine: true }], + 'comma-dangle': ['warn', { + arrays: 'always-multiline', + objects: 'always-multiline', + imports: 'always-multiline', + exports: 'always-multiline', + functions: 'always-multiline' + }], + 'comma-spacing': ['warn', { before: false, after: true }], + 'comma-style': ['warn', 'last', { exceptions: { VariableDeclarator: true, ArrayExpression: true, ObjectExpression: true } }], + 'computed-property-spacing': ['warn', 'never', { enforceForClassMembers: true }], + 'dot-notation': ['warn', { allowKeywords: true }], + 'eol-last': ['warn', 'always'], + 'func-call-spacing': ['warn', 'never'], + 'func-style': ['warn', 'declaration', { allowArrowFunctions: true }], + 'function-call-argument-newline': ['warn', 'consistent'], + 'function-paren-newline': ['warn', 'consistent'], + 'generator-star-spacing': ['warn', { before: false, after: true }], + 'implicit-arrow-linebreak': ['warn', 'beside'], + 'indent': ['warn', 2, { + SwitchCase: 1, + VariableDeclarator: { var: 2, let: 2, const: 3 }, + outerIIFEBody: 1, + MemberExpression: 'off', + FunctionDeclaration: { body: 1, parameters: 1, parameters: { var: 2, let: 2, const: 3 } }, + FunctionExpression: { body: 1, parameters: 1, parameters: { var: 2, let: 2, const: 3 } }, + StaticBlock: { body: 1 }, + ClassBody: 1 + }], + 'jsx-quotes': 'off', + 'key-spacing': ['warn', { beforeColon: false, afterColon: true, mode: 'strict' }], + 'keyword-spacing': ['warn', { before: true, after: true, overrides: { return: { after: true }, throw: { after: true }, case: { after: true } } }], + 'line-comment-position': 'off', + 'linebreak-style': ['warn', 'unix'], + 'lines-around-comment': 'off', + 'lines-between-class-members': ['warn', 'always', { exceptAfterSingleLine: true }], + 'max-len': ['warn', { + code: 120, + tabWidth: 2, + comments: 120, + ignoreComments: false, + ignoreTrailingComments: true, + ignoreUrls: true, + ignoreStrings: true, + ignoreTemplateLiterals: true, + ignoreRegExpLiterals: true + }], + 'max-statements-per-line': ['warn', { max: 1 }], + 'multiline-comment-style': 'off', + 'multiline-ternary': ['warn', 'always-multiline'], + 'new-parens': 'warn', + 'newline-per-chained-call': ['warn', { ignoreChainWithDepth: 3 }], + 'no-extra-parens': ['warn', 'all', { conditionalAssign: false, returnAssign: false, nestedBinaryExpressions: false, ignoreJSX: 'all', enforceForArrowConditionals: false, enforceForSequenceExpressions: false, enforceForNewInMemberExpressions: false }], + 'no-extra-semi': 'warn', + 'no-floating-decimal': 'warn', + 'no-mixed-operators': ['warn', { groups: [['+', '-', '*', '/', '%', '**'], ['&', '|', '^', '~', '<<', '>>', '>>>'], ['==', '!=', '===', '!==', '>', '>=', '<', '<='], ['&&', '||'], ['in', 'instanceof']], allowSamePrecedence: false }], + 'no-mixed-spaces-and-tabs': 'warn', + 'no-multi-spaces': ['warn', { ignoreEOLComments: false, exceptions: { Property: true, BinaryExpression: false, VariableDeclarator: true, ImportDeclaration: true } }], + 'no-multiple-empty-lines': ['warn', { max: 1, maxEOF: 0, maxBOF: 0 }], + 'no-tabs': 'warn', + 'no-trailing-spaces': ['warn', { skipBlankLines: false, ignoreComments: false }], + 'no-whitespace-before-property': 'warn', + 'nonblock-statement-body-position': ['warn', 'beside', { overrides: { if: 'beside', while: 'beside', do: 'beside', for: 'beside' } }], + 'object-curly-newline': ['warn', { multiline: true, consistent: true }], + 'object-curly-spacing': ['warn', 'always'], + 'object-property-newline': ['warn', { allowAllPropertiesOnSameLine: true }], + 'operator-linebreak': ['warn', 'after', { overrides: { '?': 'before', ':': 'before', '||': 'after', '&&': 'after', '|>': 'after' } }], + 'padded-blocks': ['warn', 'never'], + 'padding-line-between-statements': 'off', + 'prefer-exponentiation-operator': 'warn', + 'quote-props': ['warn', 'as-needed', { keywords: true, unnecessaryQuote: false, numbers: true }], + 'quotes': ['warn', 'single', { avoidEscape: true, allowTemplateLiterals: true }], + 'rest-spread-spacing': ['warn', 'never'], + 'semi': ['warn', 'always'], + 'semi-spacing': ['warn', { before: false, after: true }], + 'semi-style': ['warn', 'last'], + 'space-before-blocks': ['warn', 'always'], + 'space-before-function-paren': ['warn', { anonymous: 'always', named: 'never', asyncArrow: 'always' }], + 'space-in-parens': ['warn', 'never'], + 'space-infix-ops': 'warn', + 'space-unary-ops': ['warn', { words: true, nonwords: false, overrides: {} }], + 'switch-colon-spacing': ['warn', { after: true, before: false }], + 'template-curly-spacing': 'warn', + 'template-tag-spacing': ['warn', 'never'], + 'unicode-bom': ['error', 'never'], + 'wrap-iife': ['warn', 'outside', { functionPrototypeMethods: true }], + 'wrap-regex': 'warn', + 'yield-star-spacing': ['warn', { before: false, after: true }] + }, + overrides: [ + { + files: ['*.hbs', '*.handlebars'], + rules: { + // Handlebars templates don't need linting + 'no-undef': 'off' + } + }, + { + files: ['**/tests/**', '**/*.test.js', '**/*.spec.js'], + env: { + jest: true, + mocha: true + } + } + ], + ignorePatterns: [ + 'node_modules/', + 'dist/', + 'build/', + '*.min.js', + '*.min.css' + ] +}; diff --git a/lang/fr.json b/lang/fr.json index 1ba342c..77bc2fe 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -120,6 +120,16 @@ "pools": "Réserves", "self_control": "Sang-Froid", "effort": "Effort", + "test_of": "test de", + "rerolls_possible": "relances possibles", + "grant_reroll": "accorder des relances", + "success_count": "nombre de succès", + "success_required": "succès requis", + "error_no_actor_selected": "Vous n'avez pas de personnage attitré ou de token sélectionné", + "error_cannot_reroll": "Vous ne pouvez pas relancer un dés sur ce jet", + "error_no_rerolls_left": "Plus de relances possibles", + "error_select_ability": "Veuillez sélectionner une caractéristique", + "error_not_enough_self_control": "Pas assez de points de sang-froid", "group": "Groupe", "abilities": "Caractéristiques", "ability": "Caractéristique", diff --git a/module/system/dialogs/rollDialog.mjs b/module/system/dialogs/rollDialog.mjs index 5457ac5..6f89654 100644 --- a/module/system/dialogs/rollDialog.mjs +++ b/module/system/dialogs/rollDialog.mjs @@ -1,70 +1,86 @@ import { VermineUtils } from "../roll.mjs"; + /** - * Represents a dialog for rolling dice. + * Dialog for rolling dice in Vermine2047. + * Handles dice pool calculation, modifiers, and roll execution. */ export default class RollDialog extends Dialog { /** * Creates a new RollDialog instance. - * @param {Object} data - The data for the dialog. - * @param {HTMLElement} html - The HTML content of the dialog. - * @param {Object} options - The options for the dialog. - * @param {Function} close - The callback function for closing the dialog. + * @param {Object} data - The data for the dialog + * @param {HTMLElement} html - The HTML content of the dialog + * @param {Object} options - The options for the dialog + * @param {Function} [close] - The callback function for closing the dialog */ - constructor(data, html, options, close = undefined) { - let conf = { + const conf = { title: "jet de dés", content: html, buttons: { roll: { icon: '', label: "Lancer !", - callback: () => { - this._onRoll() - } + callback: () => this._onRoll() }, cancel: { icon: '', label: "Annuler", - callback: () => { this.close() } + callback: () => this.close() } }, - close: close, - - } - return super({ ...conf, ...data }, options); - }; + close: close + }; + super({ ...conf, ...data }, options); + // Store reference to close callback + this._closeCallback = close; + } /** - * Creates a new RollDialog instance. - * @param {Object} data - The data for the dialog. - * @param {HTMLElement} html - The HTML content of the dialog. - * @param {Object} options - The options for the dialog. - * @param {Function} close - The callback function for closing the dialog. - */ + * Creates a new RollDialog instance. + * @param {Object} [data] - The data for the dialog + * @param {string} [data.label] - Roll label + * @param {string} [data.rolltype] - Roll type + * @param {number} [data.NoD=1] - Number of dice + * @param {boolean} [data.Reroll=false] - Allow rerolls + * @param {string} [data.actorId] - Actor ID for the roll + * @returns {Promise} The RollDialog instance or null if creation failed + */ static async create(data = { label: null, rolltype: null, NoD: 1, Reroll: false, - actorId: game.user.character?.id || canvas.tokens.controlled[0]?.actor.id + actorId: game.user.character?.id ?? canvas.tokens.controlled[0]?.actor?.id }) { - // Retrieve the actor data based on the actorId - data.actor = await game.actors.get(data.actorId); - if (!data.actor) { - return await ui.notifications.warn("Vous n'avez pas de personnage attitré ou de token selectionné"); - + // Validate actorId + const actorId = data.actorId; + if (!actorId || typeof actorId !== 'string') { + ui.notifications.warn(game.i18n.localize('VERMINE.error_no_actor_selected')); + return null; } - console.log(data.actor) - data.availableSpecialties = data.actor.items.filter(it => it.type == "specialty"); - console.log(data.availableSpecialties) - data.availableItems = data.actor.items.filter(it => it.type == "item"); + + // Retrieve the actor data based on the actorId + data.actor = await game.actors.get(actorId); + if (!data.actor) { + ui.notifications.warn(game.i18n.localize('VERMINE.error_no_actor_selected')); + return null; + } + + data.availableSpecialties = data.actor.items.filter(item => item.type === "specialty"); + data.availableItems = data.actor.items.filter(item => item.type === "item"); data.config = CONFIG.VERMINE; + // Define options for the dialog - let options = { classes: ["vermineDialog"], width: "fit-content", height: 'fit-content', 'z-index': 99999 }; + const options = { + classes: ["vermineDialog"], + width: "fit-content", + height: 'fit-content', + zIndex: 99999 + }; + // Render the HTML template for the dialog - let html = await renderTemplate('systems/vermine2047/templates/dialogs/roll-dialog.hbs', data); + const html = await renderTemplate('systems/vermine2047/templates/dialogs/roll-dialog.hbs', data); // Return a new RollDialog instance with the provided data, HTML, and options return new RollDialog(data, html, options); @@ -82,25 +98,31 @@ export default class RollDialog extends Dialog { } /** * Retrieves the data for the dialog. - * @returns {Object} The context data for the dialog. + * @returns {Object} The context data for the dialog */ getData() { // Get the context data from the superclass - let context = super.getData(); + const context = super.getData(); context.data = this.data; context.config = CONFIG.VERMINE; - // Return the context data return context; - } + /** + * Prepares items for display. + * @returns {Array} Filtered list of items + */ prepareItems() { - return this.data.actor.items.filter(it => it.type == "item") + return this.data.actor.items.filter(it => it.type === "item"); } - prepareSpecialties() { - return this.data.actor.items.filter(it => it.type == "specialty") + /** + * Prepares specialties for display. + * @returns {Array} Filtered list of specialties + */ + prepareSpecialties() { + return this.data.actor.items.filter(it => it.type === "specialty"); } /** @@ -118,18 +140,18 @@ export default class RollDialog extends Dialog { await this.getRollData(); // Set up event listeners for all roll-related inputs - let rollInputs = html.find('[data-roll]'); - for (let inp of rollInputs) { + const rollInputs = html.find('[data-roll]'); + for (const inp of rollInputs) { inp.addEventListener('change', this._onRollInputChange.bind(this)); - }; + } this.displaySpecialties(); - let selectAbil = html.find('#ability')[0]; + const selectAbil = html.find('#ability')[0]; // Set the maximum value for self control based on ability value html.find("#self_control")[0].max = selectAbil.value; selectAbil.addEventListener('change', this._onChangeAbility.bind(this)); - let selfControl = html.find('#self_control')[0] + const selfControl = html.find('#self_control')[0]; // Add event listener for self control changes selfControl.addEventListener('change', this._onChangeSelfControl.bind(this)); @@ -150,9 +172,9 @@ export default class RollDialog extends Dialog { /** * Retrieves the roll data for the dialog. - * @param {Event} ev - The event triggering the roll data retrieval. + * @param {Event} _ev - The event triggering the roll data retrieval (unused). */ - async getRollData(ev) { + getRollData(_ev) { // Calculate and store the roll data this.rollData = { actor: this.data.actor, @@ -167,7 +189,7 @@ export default class RollDialog extends Dialog { max_effort: this.getMaxEffort(), keepTotem: this.getKeepTotem(), skillCategory: this.getSkillCategory() - } + }; this.displaySpecialties(); this._updateUI(); }; @@ -211,11 +233,11 @@ export default class RollDialog extends Dialog { } /** - * Handles changes to roll inputs and updates UI - * @param {Event} ev - The change event + * Handles changes to roll inputs and updates UI. + * @param {Event} ev - The change event. */ - async _onRollInputChange(ev) { - await this.getRollData(); + _onRollInputChange(ev) { + this.getRollData(ev); } /** @@ -280,41 +302,41 @@ export default class RollDialog extends Dialog { } /** - * Calculates the bonus count for display - * @returns {number} - Total bonus dice + * Calculates the bonus count for display. + * @returns {number} Total bonus dice. */ _calculateBonusCount() { let bonus = 0; // Help bonus - if (this._html.find('#helped')[0]?.checked) { + if (this._html?.find('#helped')[0]?.checked) { bonus += 1; } // Group bonus - const groupValue = parseInt(this._html.find('#group')[0]?.value) || 0; + const groupValue = parseInt(this._html?.find('#group')[0]?.value, 10) || 0; bonus += groupValue; // Self control bonus - const selfControlValue = parseInt(this._html.find('#self_control')[0]?.value) || 0; + const selfControlValue = parseInt(this._html?.find('#self_control')[0]?.value, 10) || 0; bonus += selfControlValue; // Tools bonus - const toolsChecked = this._html.find('input[name="usingTools"]:checked')[0]?.value !== '0'; + const toolsChecked = this._html?.find('input[name="usingTools"]:checked')[0]?.value !== '0'; if (toolsChecked) { bonus += 1; } // Totems bonus - if (this._html.find('#human-totem')[0]?.checked) { - bonus += parseInt(this.data.actor.system.adaptation.totems.human.value) || 0; + if (this._html?.find('#human-totem')[0]?.checked) { + bonus += parseInt(this.data.actor?.system?.adaptation?.totems?.human?.value, 10) || 0; } - if (this._html.find('#adapted-totem')[0]?.checked) { - bonus += parseInt(this.data.actor.system.adaptation.totems.adapted.value) || 0; + if (this._html?.find('#adapted-totem')[0]?.checked) { + bonus += parseInt(this.data.actor?.system?.adaptation?.totems?.adapted?.value, 10) || 0; } // Specialty bonus - const specialtyChecked = this._html.find('input[name="usingSpecialization"]:checked')[0]?.value !== 'aucune'; + const specialtyChecked = this._html?.find('input[name="usingSpecialization"]:checked')[0]?.value !== 'aucune'; if (specialtyChecked) { bonus += 1; } @@ -365,45 +387,69 @@ export default class RollDialog extends Dialog { * @param {Event} ev - The event triggering the change in self control value. */ _onChangeSelfControl(ev) { - let html = this.element[0]; - // Update the displayed self control value based on the event - html.querySelector('#self_control_value').innerText = ev.currentTarget.value; - }; + const html = this.element[0]; + const selfControlValueElement = html.querySelector('#self_control_value'); + if (selfControlValueElement) { + selfControlValueElement.innerText = ev.currentTarget.value; + } + } + /** + * Retrieves the handicap value from the HTML element. + * @returns {number} The handicap value. + */ getHandicap() { - let html = this.element[0]; - // Parse and return the self control value from the HTML element - let handicap = parseInt(html.querySelector('#handicap').value) - return handicap + const html = this.element[0]; + const handicapValue = html.querySelector('#handicap')?.value ?? '1'; + return parseInt(handicapValue, 10); } + /** + * Gets the roll type (ability or skill). + * @returns {string} The roll type: 'skill' or 'ability'. + */ getRollType() { - let html = this.element[0]; - // Update the displayed self control value based on the event - if (html.querySelector('select#skill').value) { - return "skill" - } return "ability" + const html = this.element[0]; + return html.querySelector('select#skill')?.value ? "skill" : "ability"; } + + /** + * Gets the label for the roll. + * @returns {string} The roll label. + */ getLabel() { - let html = this.element[0]; - if (this.getRollType() == "skill") { - return html.querySelector('select#skill').options[html.querySelector('select#skill').selectedIndex].dataset.label + const html = this.element[0]; + const rollType = this.getRollType(); + + if (rollType === "skill") { + const skillSelect = html.querySelector('select#skill'); + const selectedIndex = skillSelect?.selectedIndex ?? 0; + return skillSelect?.options[selectedIndex]?.dataset?.label ?? ""; } - return html.querySelector('select#ability').options[html.querySelector('select#ability').selectedIndex].dataset.label + + const abilitySelect = html.querySelector('select#ability'); + const selectedIndex = abilitySelect?.selectedIndex ?? 0; + return abilitySelect?.options[selectedIndex]?.dataset?.label ?? ""; } + + /** + * Displays specialties related to the selected skill. + */ displaySpecialties() { - let specialties = this.element[0].querySelectorAll('[data-spec-skill]'); - for (let specEl of specialties) { - specEl.style.display = "inline" + const specialties = this.element[0]?.querySelectorAll('[data-spec-skill]'); + if (specialties) { + specialties.forEach(specEl => { + specEl.style.display = "inline"; + }); } } + /** * Retrieves the self control value from the HTML element. * @returns {number} The self control value. */ getSelfControl() { - let html = this.element[0]; - // Parse and return the self control value from the HTML element - let selfControl = parseInt(html.querySelector('#self_control').value) - return selfControl + const html = this.element[0]; + const selfControlValue = html.querySelector('#self_control')?.value ?? '0'; + return parseInt(selfControlValue, 10); } /** @@ -411,23 +457,21 @@ export default class RollDialog extends Dialog { * @returns {number} The maximum effort value. */ getMaxEffort() { - let html = this.element[0]; - // Retrieve and return the maximum effort value from the HTML element - return parseInt(html.querySelector('#ability').value); + const html = this.element[0]; + const abilityValue = html.querySelector('#ability')?.value ?? '0'; + return parseInt(abilityValue, 10); } /** * Retrieves the selected totems from the HTML element. - * @returns {Object} An object containing the selected totems. + * @returns {Object} An object containing the selected totems {human: boolean, adapted: boolean}. */ getTotems() { - let html = this.element[0]; - // Check and store the status of human and adapted totems - let totems = { - human: html.querySelector('#human-totem')?.checked, - adapted: html.querySelector('#adapted-totem')?.checked, - } - return totems + const html = this.element[0]; + return { + human: html.querySelector('#human-totem')?.checked ?? false, + adapted: html.querySelector('#adapted-totem')?.checked ?? false + }; } /** @@ -435,37 +479,50 @@ export default class RollDialog extends Dialog { * @param {Event} ev - The event triggering the change in ability value. */ _onChangeAbility(ev) { - let html = this.element[0]; - // Retrieve the selected ability score and update related elements - let score = html.querySelector('#ability').options[html.querySelector('#ability').selectedIndex].value; - // Check if the score is a number, otherwise set it to 0 - if (!typeof score == "number") { - score = 0 + const html = this.element[0]; + const abilitySelect = html.querySelector('#ability'); + const selectedIndex = abilitySelect?.selectedIndex ?? 0; + const score = abilitySelect?.options[selectedIndex]?.value ?? '0'; + + const scoreElement = html.querySelector('#abilityScore'); + if (scoreElement) { + scoreElement.value = score; + } + + const selfControlElement = html.querySelector('#self_control'); + if (selfControlElement) { + selfControlElement.max = score; } - html.querySelector('#abilityScore').value = score; - html.querySelector('#self_control').max = score; } + /** - * Retrieves the total dice pool based on various factors. - * @returns {number} The total dice pool value. - */ + * Retrieves the total dice pool based on various factors. + * @returns {number} The total dice pool value. + */ getDicePool() { // Retrieve the HTML element - let html = this.element[0]; - // Get the ability value or set to 0 if not found - let abilValue = html.querySelector('#ability').options[html.querySelector('#ability').selectedIndex].value || 0; - // Get the skill value or set to 0 if not found - let skillValue = html.querySelector('#skill').options[html.querySelector('#skill').selectedIndex].dataset.pool || 0; + const html = this.element[0]; + + // Safely get ability value + const abilitySelect = html.querySelector('#ability'); + const abilValue = abilitySelect?.options[abilitySelect?.selectedIndex]?.value ?? 0; + + // Safely get skill value and pool + const skillSelect = html.querySelector('#skill'); + const skillOption = skillSelect?.options[skillSelect?.selectedIndex]; + const skillValue = skillOption?.dataset?.pool ?? 0; + // Get the self control value - let selfControl = html.querySelector('#self_control').value; + const selfControl = html.querySelector('#self_control')?.value ?? 0; + // Calculate bonuses based on certain conditions - console.log(html.querySelector('#usingTools').checked) - let bonuses = + const bonuses = (html.querySelector('#usingSpecialization')?.checked ? 1 : 0) + - (html.querySelector('#helped').checked ? 1 : 0) + - (html.querySelector('#usingTools').checked ? 1 : 0); + (html.querySelector('#helped')?.checked ? 1 : 0) + + (html.querySelector('#usingTools')?.checked ? 1 : 0); + // Calculate the total dice pool - let total = parseInt(abilValue) + parseInt(selfControl) + parseInt(skillValue) + bonuses; + const total = parseInt(abilValue, 10) + parseInt(selfControl, 10) + parseInt(skillValue, 10) + bonuses; return total || 0; } @@ -474,69 +531,66 @@ export default class RollDialog extends Dialog { * @returns {number} The reroll value. */ getReroll() { - // Retrieve the HTML element - let html = this.element[0]; - // Get the selected skill index - let selected = html.querySelector('#skill').selectedIndex; - // Get the reroll value from the selected skill or set to 0 if not found - let reroll = html.querySelector('#skill').options[selected].dataset.reroll || 0; - return parseInt(reroll) || 0; + const html = this.element[0]; + const skillSelect = html.querySelector('#skill'); + const selectedIndex = skillSelect?.selectedIndex ?? 0; + const rerollValue = skillSelect?.options[selectedIndex]?.dataset?.reroll ?? '0'; + return parseInt(rerollValue, 10) || 0; } /** * Retrieves the difficulty value based on selected option. * @returns {number} The difficulty value. */ - getDifficulty() { - // Retrieve the HTML element - let html = this.element[0]; - // Get the selected index for difficulty - let selected = html.querySelector('#difficulty').selectedIndex; - // Get the difficulty value from the selected option or set to 0 if not found - let diff = html.querySelector('#difficulty').options[selected].value || 0; - return parseInt(diff) || 0; + const html = this.element[0]; + const difficultySelect = html.querySelector('#difficulty'); + const selectedIndex = difficultySelect?.selectedIndex ?? 0; + const diffValue = difficultySelect?.options[selectedIndex]?.value ?? '0'; + return parseInt(diffValue, 10) || 0; } /** - * Performs a dice roll based on the roll data and handles self control checks. - * @returns {Promise} A promise that resolves with the result of the dice roll. - */ + * Performs a dice roll based on the roll data and handles self control checks. + * @returns {Promise} A promise that resolves with the Roll result or false if cancelled. + */ async _onRoll() { // Check if self control is required for the roll if (this.rollData.self_control > 0) { // Check if the actor has enough self control - if (this.rollData.actor.system.attributes.self_control.value < this.rollData.self_control) { + const currentSelfControl = this.rollData.actor?.system?.attributes?.self_control?.value ?? 0; + if (currentSelfControl < this.rollData.self_control) { // Display a warning message if self control is insufficient ui.notifications.warn(game.i18n.localize('VERMINE.error_not_enough_self_control')); // Re-render the dialog this.render(true); return false; // Exit the function if self control is insufficient } - } - let caracName = this.element[0].querySelector('[name="ability"]')?.value - if (caracName == "0" || caracName === undefined) { + + const caracName = this.element[0]?.querySelector('[name="ability"]')?.value; + if (caracName === "0" || caracName === undefined) { // Display a warning message if no ability selected ui.notifications.warn(game.i18n.localize('VERMINE.error_select_ability')); // Re-render the dialog this.render(true); return false; // Exit the function if no ability } + // Deduct self control points if necessary if (this.rollData.self_control > 0) { + const newSelfControl = this.rollData.actor.system.attributes.self_control.value - this.rollData.self_control; // Update the actor's self control value - await this.rollData.actor.update({ - "system.attributes.self_control.value": - this.rollData.actor.system.attributes.self_control.value - this.rollData.self_control + await this.rollData.actor.update({ + "system.attributes.self_control.value": newSelfControl }); } // Perform the dice roll using VermineUtils - return VermineUtils.roll({ - ...this.rollData, - skillLevel: this.getSkillLevel(), - hasSpecialty: this.hasSpecialtySelected() + return VermineUtils.roll({ + ...this.rollData, + skillLevel: this.getSkillLevel(), + hasSpecialty: this.hasSpecialtySelected() }); } } \ No newline at end of file diff --git a/module/system/group-link.mjs b/module/system/group-link.mjs index de2c875..c4a49d6 100644 --- a/module/system/group-link.mjs +++ b/module/system/group-link.mjs @@ -1,53 +1,52 @@ /** - * GroupLink - Gestion des liens entre acteurs et groupes - * - * Cette classe permet de gérer les relations bidirectionnelles entre : - * - Les personnages (characters) et leurs groupes/rencontres - * - Les groupes (groups) et leurs membres/rencontres - * + * GroupLink - Manages bidirectional links between actors and groups + * + * This class handles bidirectional relationships between: + * - Characters and their groups/encounters + * - Groups and their members/encounters + * * @author Vermine2047 System */ export class GroupLink { /** - * Met à jour les groupes dans tous les personnages membres - * quand un groupe est modifié - * @param {Actor} group - Le groupe modifié - * @param {Object} changes - Les changements effectués + * Updates actors when a group is modified. + * @param {Actor} group - The modified group + * @param {Object} changes - The changes made */ static async updateActorsOnGroupChange(group, changes) { - if (group.type !== 'group') return; + if (group?.type !== 'group') return; const groupData = group.system; - const members = groupData.members || []; - const encounters = groupData.encounters || []; + const members = [...(groupData.members || [])]; + const encounters = [...(groupData.encounters || [])]; - // Mettre à jour les membres du groupe - if (changes.members !== undefined || changes.encounters !== undefined) { + // Update group members and encounters + if (changes?.members !== undefined || changes?.encounters !== undefined) { await this._updateMembersInGroup(group, members); await this._updateEncountersInGroup(group, encounters); } - // Synchroniser les données dans les acteurs membres + // Sync data to member actors await this._syncGroupToMembers(group, members); await this._syncGroupToEncounters(group, encounters); } /** - * Met à jour le groupe quand un personnage est modifié - * @param {Actor} actor - L'acteur modifié - * @param {Object} changes - Les changements effectués + * Updates groups when an actor is modified. + * @param {Actor} actor - The modified actor + * @param {Object} changes - The changes made */ static async updateGroupsOnActorChange(actor, changes) { - if (actor.type === 'group') return; + if (actor?.type === 'group') return; const actorData = actor.system; - const encounters = actorData.encounters || []; + const encounters = [...(actorData.encounters || [])]; - // Si les rencontres de l'acteur ont changé - if (changes.encounters !== undefined) { - // Pour chaque groupe dans les rencontres, mettre à jour les membres + // If actor's encounters have changed + if (changes?.encounters !== undefined) { + // Update each group in encounters for (const groupId of encounters) { const group = game.actors.get(groupId); if (group && group.type === 'group') { @@ -58,78 +57,84 @@ export class GroupLink { } /** - * Synchronise les données du groupe vers les acteurs membres - * @param {Actor} group - Le groupe - * @param {Array} memberIds - Liste des IDs des membres + * Syncs group data to member actors. + * @param {Actor} group - The group + * @param {Array} memberIds - List of member IDs */ static async _syncGroupToMembers(group, memberIds) { + const groupId = group.id; + for (const memberId of memberIds) { const member = game.actors.get(memberId); - if (member) { - // Vérifier que le groupe est dans les rencontres du membre - const memberEncounters = member.system.encounters || []; - if (!memberEncounters.includes(group.id)) { - // Ajouter le groupe aux rencontres du membre - memberEncounters.push(group.id); - await member.update({ - 'system.encounters': memberEncounters - }); - } + if (!member) continue; + + const memberEncounters = [...(member.system.encounters || [])]; + if (!memberEncounters.includes(groupId)) { + memberEncounters.push(groupId); + await member.update({ + 'system.encounters': memberEncounters + }); } } } /** - * Synchronise les données du groupe vers les acteurs rencontres - * @param {Actor} group - Le groupe - * @param {Array} encounterIds - Liste des IDs des rencontres + * Syncs group data to encounter actors. + * @param {Actor} group - The group + * @param {Array} encounterIds - List of encounter IDs */ static async _syncGroupToEncounters(group, encounterIds) { + const groupId = group.id; + for (const encounterId of encounterIds) { const encounter = game.actors.get(encounterId); - if (encounter) { - // Vérifier que le groupe est dans les rencontres de l'acteur - const encounterGroups = encounter.system.encounters || []; - if (!encounterGroups.includes(group.id)) { - encounterGroups.push(group.id); - await encounter.update({ - 'system.encounters': encounterGroups - }); - } + if (!encounter) continue; + + const encounterGroups = [...(encounter.system.encounters || [])]; + if (!encounterGroups.includes(groupId)) { + encounterGroups.push(groupId); + await encounter.update({ + 'system.encounters': encounterGroups + }); } } } /** - * Met à jour les membres dans un groupe - * @param {Actor} group - Le groupe - * @param {Array} memberIds - Liste des IDs des membres + * Updates members in a group. + * @param {Actor} group - The group + * @param {Array} memberIds - List of member IDs */ static async _updateMembersInGroup(group, memberIds) { - const currentMembers = group.system.members || []; + const groupId = group.id; + const currentMembers = [...(group.system.members || [])]; - // Retirer les membres qui ne sont plus dans la liste - const membersToRemove = currentMembers.filter(id => !memberIds.includes(id)); - const membersToAdd = memberIds.filter(id => !currentMembers.includes(id)); + // Convert to Sets for O(1) lookups + const currentMembersSet = new Set(currentMembers); + const memberIdsSet = new Set(memberIds); - // Mettre à jour les acteurs qui ont été retirés + // Find members to remove and add + const membersToRemove = [...currentMembersSet].filter(id => !memberIdsSet.has(id)); + const membersToAdd = [...memberIdsSet].filter(id => !currentMembersSet.has(id)); + + // Update actors that were removed for (const memberId of membersToRemove) { const member = game.actors.get(memberId); if (member) { - const memberEncounters = (member.system.encounters || []).filter(id => id !== group.id); + const memberEncounters = (member.system.encounters || []).filter(id => id !== groupId); await member.update({ 'system.encounters': memberEncounters }); } } - // Mettre à jour les nouveaux membres + // Update new members for (const memberId of membersToAdd) { const member = game.actors.get(memberId); if (member) { - const memberEncounters = member.system.encounters || []; - if (!memberEncounters.includes(group.id)) { - memberEncounters.push(group.id); + const memberEncounters = [...(member.system.encounters || [])]; + if (!memberEncounters.includes(groupId)) { + memberEncounters.push(groupId); await member.update({ 'system.encounters': memberEncounters }); @@ -139,35 +144,40 @@ export class GroupLink { } /** - * Met à jour les rencontres dans un groupe - * @param {Actor} group - Le groupe - * @param {Array} encounterIds - Liste des IDs des rencontres + * Updates encounters in a group. + * @param {Actor} group - The group + * @param {Array} encounterIds - List of encounter IDs */ static async _updateEncountersInGroup(group, encounterIds) { - const currentEncounters = group.system.encounters || []; + const groupId = group.id; + const currentEncounters = [...(group.system.encounters || [])]; - // Retirer les rencontres qui ne sont plus dans la liste - const encountersToRemove = currentEncounters.filter(id => !encounterIds.includes(id)); - const encountersToAdd = encounterIds.filter(id => !currentEncounters.includes(id)); + // Convert to Sets for O(1) lookups + const currentEncountersSet = new Set(currentEncounters); + const encounterIdsSet = new Set(encounterIds); - // Mettre à jour les acteurs qui ont été retirés des rencontres + // Find encounters to remove and add + const encountersToRemove = [...currentEncountersSet].filter(id => !encounterIdsSet.has(id)); + const encountersToAdd = [...encounterIdsSet].filter(id => !currentEncountersSet.has(id)); + + // Update actors that were removed from encounters for (const encounterId of encountersToRemove) { const encounter = game.actors.get(encounterId); if (encounter) { - const encounterGroups = (encounter.system.encounters || []).filter(id => id !== group.id); + const encounterGroups = (encounter.system.encounters || []).filter(id => id !== groupId); await encounter.update({ 'system.encounters': encounterGroups }); } } - // Mettre à jour les nouvelles rencontres + // Update new encounters for (const encounterId of encountersToAdd) { const encounter = game.actors.get(encounterId); if (encounter) { - const encounterGroups = encounter.system.encounters || []; - if (!encounterGroups.includes(group.id)) { - encounterGroups.push(group.id); + const encounterGroups = [...(encounter.system.encounters || [])]; + if (!encounterGroups.includes(groupId)) { + encounterGroups.push(groupId); await encounter.update({ 'system.encounters': encounterGroups }); @@ -177,12 +187,12 @@ export class GroupLink { } /** - * Met à jour un acteur dans les membres d'un groupe - * @param {Actor} group - Le groupe - * @param {string} actorId - L'ID de l'acteur + * Updates an actor in group members. + * @param {Actor} group - The group + * @param {string} actorId - The actor ID */ static async _updateActorInGroupMembers(group, actorId) { - const groupMembers = group.system.members || []; + const groupMembers = [...(group.system.members || [])]; if (!groupMembers.includes(actorId)) { groupMembers.push(actorId); await group.update({ @@ -192,12 +202,12 @@ export class GroupLink { } /** - * Met à jour un acteur dans les rencontres d'un groupe - * @param {Actor} group - Le groupe - * @param {string} actorId - L'ID de l'acteur + * Updates an actor in group encounters. + * @param {Actor} group - The group + * @param {string} actorId - The actor ID */ static async _updateActorInGroupEncounters(group, actorId) { - const groupEncounters = group.system.encounters || []; + const groupEncounters = [...(group.system.encounters || [])]; if (!groupEncounters.includes(actorId)) { groupEncounters.push(actorId); await group.update({ @@ -207,30 +217,31 @@ export class GroupLink { } /** - * Retourne les objets Actor pour une liste d'IDs - * @param {Array} actorIds - Liste d'IDs d'acteurs - * @returns {Array} - Liste d'objets Actor + * Returns Actor objects for a list of IDs. + * @param {Array} actorIds - List of actor IDs + * @returns {Array} List of Actor objects */ static getActorObjects(actorIds) { + if (!Array.isArray(actorIds)) return []; return actorIds .map(id => game.actors.get(id)) .filter(actor => actor !== undefined); } - + /** - * Retourne les objets Actor pour les membres d'un groupe - * @param {Actor} group - Le groupe - * @returns {Array} - Liste d'objets Actor + * Returns Actor objects for group members. + * @param {Actor} group - The group + * @returns {Array} List of Actor objects */ static getGroupMembers(group) { - const memberIds = group.system.members || []; + const memberIds = group?.system?.members || []; return this.getActorObjects(memberIds); } /** - * Retourne les objets Actor pour les rencontres d'un groupe - * @param {Actor} group - Le groupe - * @returns {Array} - Liste d'objets Actor + * Returns Actor objects for group encounters + * @param {Actor} group - The group + * @returns {Array} - List of Actor objects */ static getGroupEncounters(group) { const encounterIds = group.system.encounters || []; @@ -238,9 +249,9 @@ export class GroupLink { } /** - * Retourne les groupes auxquels un acteur appartient - * @param {Actor} actor - L'acteur - * @returns {Array} - Liste d'objets Actor (groupes) + * Returns groups that an actor belongs to + * @param {Actor} actor - The actor + * @returns {Array} - List of Actor objects (groups) */ static getActorGroups(actor) { const groupIds = actor.system.encounters || []; @@ -248,9 +259,9 @@ export class GroupLink { } /** - * Retourne les rencontres (PNJ/Créatures) d'un acteur - * @param {Actor} actor - L'acteur - * @returns {Array} - Liste d'objets Actor (PNJ/Créatures) + * Returns encounters (NPC/Creatures) for an actor + * @param {Actor} actor - The actor + * @returns {Array} - List of Actor objects (NPC/Creatures) */ static getActorEncounters(actor) { const encounterIds = actor.system.encounters || []; @@ -258,25 +269,27 @@ export class GroupLink { } /** - * Supprime un acteur de tous ses groupes - * @param {string} actorId - L'ID de l'acteur à supprimer + * Removes an actor from all groups. + * @param {string} actorId - The actor ID to remove */ static async removeActorFromAllGroups(actorId) { const allGroups = game.actors.filter(a => a.type === 'group'); for (const group of allGroups) { - const members = group.system.members || []; - const encounters = group.system.encounters || []; + const members = [...(group.system.members || [])]; + const encounters = [...(group.system.encounters || [])]; - let needsUpdate = false; - const newMembers = members.filter(id => id !== actorId); - const newEncounters = encounters.filter(id => id !== actorId); + // Use Set for O(1) lookups + const membersSet = new Set(members); + const encountersSet = new Set(encounters); - if (newMembers.length !== members.length || newEncounters.length !== encounters.length) { - needsUpdate = true; - } + const hasActorInMembers = membersSet.has(actorId); + const hasActorInEncounters = encountersSet.has(actorId); - if (needsUpdate) { + if (hasActorInMembers || hasActorInEncounters) { + const newMembers = hasActorInMembers ? members.filter(id => id !== actorId) : members; + const newEncounters = hasActorInEncounters ? encounters.filter(id => id !== actorId) : encounters; + await group.update({ 'system.members': newMembers, 'system.encounters': newEncounters @@ -284,7 +297,7 @@ export class GroupLink { } } - // Supprimer les groupes des rencontres de l'acteur + // Remove groups from actor encounters const actor = game.actors.get(actorId); if (actor) { await actor.update({ @@ -294,9 +307,9 @@ export class GroupLink { } /** - * Ajoute un acteur à un groupe - * @param {string} actorId - L'ID de l'acteur - * @param {string} groupId - L'ID du groupe + * Adds an actor to a group. + * @param {string} actorId - The actor ID + * @param {string} groupId - The group ID */ static async addActorToGroup(actorId, groupId) { const actor = game.actors.get(actorId); @@ -304,8 +317,8 @@ export class GroupLink { if (!actor || !group || group.type !== 'group') return; - // Ajouter l'acteur aux membres du groupe - const groupMembers = group.system.members || []; + // Add actor to group members using spread operator + const groupMembers = [...(group.system.members || [])]; if (!groupMembers.includes(actorId)) { groupMembers.push(actorId); await group.update({ @@ -313,8 +326,8 @@ export class GroupLink { }); } - // Ajouter le groupe aux rencontres de l'acteur - const actorEncounters = actor.system.encounters || []; + // Add group to actor encounters using spread operator + const actorEncounters = [...(actor.system.encounters || [])]; if (!actorEncounters.includes(groupId)) { actorEncounters.push(groupId); await actor.update({ @@ -324,9 +337,9 @@ export class GroupLink { } /** - * Retire un acteur d'un groupe - * @param {string} actorId - L'ID de l'acteur - * @param {string} groupId - L'ID du groupe + * Removes an actor from a group. + * @param {string} actorId - The actor ID + * @param {string} groupId - The group ID */ static async removeActorFromGroup(actorId, groupId) { const actor = game.actors.get(actorId); @@ -334,64 +347,66 @@ export class GroupLink { if (!actor || !group || group.type !== 'group') return; - // Retirer l'acteur des membres du groupe - const groupMembers = (group.system.members || []).filter(id => id !== actorId); + // Remove actor from group members using spread operator and filter + const groupMembers = [...(group.system.members || [])].filter(id => id !== actorId); await group.update({ 'system.members': groupMembers }); - // Retirer le groupe des rencontres de l'acteur - const actorEncounters = (actor.system.encounters || []).filter(id => id !== groupId); + // Remove group from actor encounters using spread operator and filter + const actorEncounters = [...(actor.system.encounters || [])].filter(id => id !== groupId); await actor.update({ 'system.encounters': actorEncounters }); } /** - * Initialise les hooks pour la synchronisation automatique + * Initializes hooks for automatic synchronization between actors and groups. + * Sets up event listeners for actor creation, updates, and deletion. */ static registerHooks() { - // Hook sur la mise à jour d'un acteur + // Hook on actor update - synchronize group memberships Hooks.on('updateActor', async (actor, changes, options, userId) => { if (!game.user.isGM && userId !== game.userId) return; - // Si c'est un groupe qui est mis à jour + // If it is a group being updated, sync its members if (actor.type === 'group') { await this.updateActorsOnGroupChange(actor, changes); } - // Si c'est un autre acteur qui est mis à jour + // If it is another actor being updated, sync its groups else { await this.updateGroupsOnActorChange(actor, changes); } }); - // Hook sur la création d'un acteur + // Hook on actor creation - clean up invalid group references Hooks.on('createActor', async (actor, options, userId) => { if (!game.user.isGM && userId !== game.userId) return; - // Si un personnage est créé, vérifier qu'il n'a pas de groupes invalides + // If a character is created, check for invalid group references if (actor.type !== 'group') { - const encounters = actor.system.encounters || []; - for (const groupId of encounters) { - const group = game.actors.get(groupId); - if (!group) { - // Nettoyer les références invalides - await actor.update({ - 'system.encounters': encounters.filter(id => game.actors.get(id)) - }); - } + const encounters = [...(actor.system.encounters || [])]; + const validGroups = new Set( + encounters.filter(id => game.actors.get(id)) + ); + + // Only update if there are invalid references + if (validGroups.size !== encounters.length) { + await actor.update({ + 'system.encounters': [...validGroups] + }); } } }); - // Hook sur la suppression d'un acteur + // Hook on actor deletion - clean up references Hooks.on('deleteActor', async (actor, options, userId) => { if (!game.user.isGM && userId !== game.userId) return; if (actor.type === 'group') { - // Si un groupe est supprimé, nettoyer les références dans les acteurs - const memberIds = actor.system.members || []; - const encounterIds = actor.system.encounters || []; + // If a group is deleted, clean up references in its members and encounters + const memberIds = [...(actor.system.members || [])]; + const encounterIds = [...(actor.system.encounters || [])]; for (const id of [...memberIds, ...encounterIds]) { const a = game.actors.get(id); @@ -403,7 +418,7 @@ export class GroupLink { } } } else { - // Si un acteur est supprimé, le retirer de tous les groupes + // If an actor is deleted, remove it from all groups await this.removeActorFromAllGroups(actor.id); } }); diff --git a/module/system/roll.mjs b/module/system/roll.mjs index db1a0b8..a0ff7a7 100644 --- a/module/system/roll.mjs +++ b/module/system/roll.mjs @@ -1,109 +1,119 @@ export class VermineUtils { /** - * Méthode pour effectuer un jet de dés avec différentes options - * @param {Object} options - Les options du jet de dés - * @param {Actor} options.actor - L'acteur qui lance les dés - * @param {number} options.NoD - Nombre de dés de base - * @param {number} [options.Reroll=0] - Nombre de relances autorisées - * @param {number} [options.difficulty=7] - Difficulté du jet - * @param {number} [options.self_control=0] - Sang-froid utilisé - * @param {string} [options.rollLabel="jet custom"] - Libellé du jet - * @param {Object} [options.totems={}] - Totems utilisés {human: false, adapted: false} - * @param {number} [options.max_effort=0] - Effort maximum - * @param {string} [options.skillCategory=null] - Catégorie de compétence pour les bonus de domaine - * @param {string} [options.keepTotem=null] - Totem à garder ('human' ou 'adapted') - * @param {number} [options.skillLevel=null] - Niveau de la compétence pour les réussites automatiques - * @param {boolean} [options.hasSpecialty=false] - Si une spécialité est utilisée - * @returns {Roll} - Le résultat du jet de dés + * Rolls dice with Vermine2047-specific rules. + * @param {Object} options - Roll options + * @param {Actor} options.actor - The actor rolling + * @param {number} options.NoD - Base dice pool + * @param {number} [options.Reroll=0] - Reroll count + * @param {number} [options.difficulty=7] - Difficulty threshold + * @param {number} [options.self_control=0] - Self control used + * @param {string} [options.rollLabel="jet custom"] - Roll label + * @param {Object} [options.totems={}] - Totems used {human: boolean, adapted: boolean} + * @param {number} [options.max_effort=0] - Max effort + * @param {string} [options.skillCategory=null] - Skill category for domain bonuses + * @param {string} [options.keepTotem=null] - Totem to keep ('human' or 'adapted') + * @param {number} [options.skillLevel=null] - Skill level for auto-successes + * @param {boolean} [options.hasSpecialty=false] - Whether a specialty is used + * @returns {Promise} The roll result */ - static async roll({ actor, NoD, Reroll = 0, difficulty = 7, self_control = 0, rollLabel = "jet custom", totems = { human: false, adapted: false }, max_effort = 0, skillCategory = null, keepTotem = null, skillLevel = null, hasSpecialty = false }) { - // Déclaration des variables + static async roll({ + actor, + NoD, + Reroll = 0, + difficulty = 7, + self_control = 0, + rollLabel = "jet custom", + totems = { human: false, adapted: false }, + max_effort = 0, + skillCategory = null, + keepTotem = null, + skillLevel = null, + hasSpecialty = false + }) { + // Validate inputs + if (!actor) { + throw new Error("Actor is required for rolling"); + } + + // Sanitize user name for use in dice flavor + const safeUserName = (game.user?.name ?? "user").replace(/[^a-zA-Z0-9_]/g, '_'); + + // Declare variables let formula = ""; let modFormula = null; let totemBonus = { human: 0, adapted: 0 }; - - // Calculer les bonus/malus par domaine de totem + + // Calculate domain bonuses for totems if (skillCategory) { totemBonus = this._calculateTotemDomainBonuses(skillCategory, actor); } - - // Appliquer les réussites automatiques et seuils auto + + // Apply auto-successes and auto-thresholds let autoSuccesses = 0; let adjustedDifficulty = difficulty; - + if (skillLevel !== null && skillLevel !== undefined) { - // Calculer les réussites automatiques autoSuccesses = this._calculateAutoSuccesses(skillLevel, hasSpecialty); - - // Appliquer le seuil automatique si nécessaire const autoThreshold = this._getAutoThreshold(skillLevel); if (autoThreshold !== null) { adjustedDifficulty = autoThreshold; } } - // Vérification des totems humains + // Handle human totem if (totems.human) { NoD--; const humanDifficulty = skillLevel !== null ? Math.max(adjustedDifficulty, difficulty) : adjustedDifficulty; - const humanFormula = "(1D10cs>=" + humanDifficulty + `[human_${game.user.name}]*2)`; - - // Appliquer bonus/malus de domaine + const humanFormula = `(1D10cs>=${humanDifficulty}[human_${safeUserName}]*2)`; + + // Apply domain bonus/malus if (totemBonus.human !== 0) { - // Si bonus, ajouter un dé supplémentaire, si malus, réduire le pool NoD += totemBonus.human; } - + modFormula = humanFormula; } - - // Vérification des totems adaptés + + // Handle adapted totem if (totems.adapted) { NoD--; const adaptedDifficulty = skillLevel !== null ? Math.max(adjustedDifficulty, difficulty) : adjustedDifficulty; - const adaptedFormula = "(1D10cs>=" + adaptedDifficulty + `[adapted_${game.user.name}]*2)`; - - // Appliquer bonus/malus de domaine + const adaptedFormula = `(1D10cs>=${adaptedDifficulty}[adapted_${safeUserName}]*2)`; + + // Apply domain bonus/malus if (totemBonus.adapted !== 0) { NoD += totemBonus.adapted; } - - // Construction de la formule modifiée - if (modFormula != null) { - modFormula = modFormula + "+" + adaptedFormula; + + // Build combined formula + if (modFormula !== null) { + modFormula = `${modFormula}+${adaptedFormula}`; } else { modFormula = adaptedFormula; } - }; + } - // Gestion du choix de totem à garder (si les deux sont activés) + // Handle keepTotem selection (if both totems are active) if (totems.human && totems.adapted && keepTotem) { - // Si on veut garder un seul totem, ne pas doubler le bonus if (keepTotem === 'human' && totems.adapted) { - // Retirer le totem adapté du calcul - modFormula = "(1D10cs>=" + adjustedDifficulty + `[human_${game.user.name}]*2)`; - NoD++; // On avait décrémenté pour adapted, on annule + modFormula = `(1D10cs>=${adjustedDifficulty}[human_${safeUserName}]*2)`; + NoD++; // Cancel the decrement for adapted } else if (keepTotem === 'adapted' && totems.human) { - // Retirer le totem humain du calcul - modFormula = "(1D10cs>=" + adjustedDifficulty + `[adapted_${game.user.name}]*2)`; - NoD++; // On avait décrémenté pour human, on annule + modFormula = `(1D10cs>=${adjustedDifficulty}[adapted_${safeUserName}]*2)`; + NoD++; // Cancel the decrement for human } } - // Construction de la formule de base - let baseFormula = '' + NoD + "d10"; - baseFormula += (adjustedDifficulty != undefined) ? "cs>=" + adjustedDifficulty : "cs>=7"; - baseFormula += `[regular_${game.user.name}]` + // Build base formula + const baseFormula = `${NoD}d10cs>=${adjustedDifficulty}[regular_${safeUserName}]`; - // Construction de la formule finale - if (modFormula != null) { - formula = baseFormula + "+" + modFormula; - } else { formula = baseFormula } + // Build final formula + formula = modFormula !== null ? `${baseFormula}+${modFormula}` : baseFormula; - // Création du jet de dés - let roll = new Roll(formula, actor.getRollData()); + // Create the roll + const roll = new Roll(formula, actor.getRollData()); - // Stocker les métadonnées du roll pour l'affichage + // Store metadata for display roll.vermineData = { totemsUsed: { ...totems }, keepTotem: keepTotem, @@ -118,71 +128,93 @@ export class VermineUtils { rerolls: Reroll, selfControl: self_control }; - - //effectuer le lancé + + // Evaluate the roll await roll.evaluate(); - //afficher le lancer 3d + + // Show 3D dice if available await VermineUtils.showDiceSoNice(roll); - // afficher le résultat dans le chat - VermineUtils.diplayChatRoll(roll, { actor, NoD, Reroll, difficulty, self_control, rollLabel, totems, max_effort, skillCategory, keepTotem, skillLevel, hasSpecialty }); + + // Display result in chat + VermineUtils.diplayChatRoll(roll, { + actor, + NoD, + Reroll, + difficulty, + self_control, + rollLabel, + totems, + max_effort, + skillCategory, + keepTotem, + skillLevel, + hasSpecialty + }); + return roll; } /** - * Calcule les bonus/malus par domaine de totem - * @param {string} skillCategory - Catégorie de la compétence - * @param {Actor} actor - L'acteur - * @returns {Object} - Bonus pour chaque totem {human: number, adapted: number} + * Calculates domain bonuses/penalties for totems. + * @param {string} skillCategory - The skill category + * @param {Actor} actor - The actor + * @returns {Object} Bonuses for each totem {human: number, adapted: number} */ static _calculateTotemDomainBonuses(skillCategory, actor) { const bonuses = { human: 0, adapted: 0 }; - + + // Validate inputs if (!CONFIG.VERMINE?.totemDomains || !actor?.system?.identity?.totem) { return bonuses; } - + const actorTotem = actor.system.identity.totem; - const totemConfig = CONFIG.VERMINE.totemDomains[actorTotem]; - if (!totemConfig || !totemConfig.domains) { + // Check if actor's totem exists in configuration + if (!CONFIG.VERMINE.totemDomains[actorTotem]) { return bonuses; } - - // Vérifier si la catégorie de compétence est dans les domaines du totem + + const totemConfig = CONFIG.VERMINE.totemDomains[actorTotem]; + + if (!totemConfig?.domains) { + return bonuses; + } + + // Get actor's preferred skill category const preferredCategory = actor.system.skill_categories?.preferred; - - // Bonus pour le totem de l'acteur + + // Bonus for actor's totem if preferred category is in its domains if (preferredCategory && totemConfig.domains.includes(preferredCategory)) { - // Le domaine de prédilection est dans les domaines du totem bonuses[actorTotem] = totemConfig.bonus || 1; } - - // Malus pour le totem opposé + + // Penalty for opposite totem if preferred category is in its domains const oppositeTotem = CONFIG.VERMINE.totem_opposites?.[actorTotem]; - if (oppositeTotem && preferredCategory) { + if (oppositeTotem && CONFIG.VERMINE.totemDomains[oppositeTotem]) { const oppositeConfig = CONFIG.VERMINE.totemDomains[oppositeTotem]; - if (oppositeConfig?.domains?.includes(preferredCategory)) { + if (preferredCategory && oppositeConfig?.domains?.includes(preferredCategory)) { bonuses[oppositeTotem] = -(oppositeConfig.bonus || 1); } } - + return bonuses; } /** - * Calcule les réussites automatiques basées sur la maîtrise de la compétence - * @param {number} skillLevel - Niveau de la compétence (0-5) - * @param {boolean} hasSpecialty - Si une spécialité est utilisée - * @returns {number} - Nombre de réussites automatiques + * Calculates automatic successes based on skill mastery level. + * @param {number} skillLevel - Skill level (0-5) + * @param {boolean} [hasSpecialty=false] - Whether a specialty is used + * @returns {number} Number of automatic successes */ static _calculateAutoSuccesses(skillLevel, hasSpecialty = false) { - // Selon les règles de Vermine2047, les réussites automatiques sont basées sur le niveau de maîtrise - // Niveau 0 (Incompétent): 0 réussite automatique - // Niveau 1 (Débutant): 0 réussite automatique - // Niveau 2 (Compétent): 1 réussite automatique si spécialité utilisée - // Niveau 3 (Expert): 1 réussite automatique - // Niveau 4 (Maître): 1 réussite automatique + 1 si spécialité utilisée - // Niveau 5 (Légende): 2 réussites automatiques + // According to Vermine2047 rules, automatic successes are based on mastery level: + // Level 0 (Incompetent): 0 automatic successes + // Level 1 (Beginner): 0 automatic successes + // Level 2 (Proficient): 1 automatic success if specialty is used + // Level 3 (Expert): 1 automatic success + // Level 4 (Master): 1 automatic success + 1 if specialty is used + // Level 5 (Legend): 2 automatic successes if (!skillLevel) return 0; @@ -210,204 +242,239 @@ export class VermineUtils { } /** - * Détermine le seuil automatique si la compétence n'est pas maîtrisée - * @param {number} skillLevel - Niveau de la compétence - * @returns {number|null} - Seuil automatique ou null si la compétence est maîtrisée + * Determines the automatic threshold if the skill is not mastered. + * @param {number} skillLevel - Skill level + * @returns {number|null} Automatic threshold or null if skill is mastered */ static _getAutoThreshold(skillLevel) { - // Si la compétence n'est pas maîtrisée (niveau 0 ou 1), utiliser un seuil par défaut - // Niveau 0 (Incompétent): seuil = 9 (très difficile) - // Niveau 1 (Débutant): seuil = 7 (difficile) - // Niveau >= 2: null (utiliser le seuil normal) + // If the skill is not mastered (level 0 or 1), use a default threshold + // Level 0 (Incompetent): threshold = 9 (very hard) + // Level 1 (Beginner): threshold = 7 (hard) + // Level >= 2: null (use normal threshold) - if (skillLevel === 0) return 9; // Très difficile - if (skillLevel === 1) return 7; // Difficile + if (skillLevel === 0) return 9; // Very hard + if (skillLevel === 1) return 7; // Hard return null; // Utiliser le seuil normal } /** - * Méthode pour gérer les événements de relance de dés - * @param {Object} message - Le message contenant l'événement de relance - * @param {Object} ev - L'événement de relance + * Handles reroll events on dice in chat messages. + * @param {Object} message - The chat message containing the reroll event + * @param {Object} ev - The reroll event + * @returns {Promise} Whether the reroll was successful */ static async onReroll(message, ev) { - // Vérification de l'utilisateur - if (game.user._id != message.user._id || !game.user.isGM) { - ui.notifications.warn('vous ne pouvez pas relancer un dés sur ce jet') - return false + // Verify user permissions + if (game.user?._id !== message.user._id && !game.user?.isGM) { + ui.notifications.warn(game.i18n.localize('VERMINE.error_cannot_reroll')); + return false; } - // Récupération du nombre de relances autorisé - let rerollCount = ev.currentTarget.closest('div.vermine-roll-message').querySelector('#allowed_reroll')?.innerText; - // Vérification du nombre de relances restantes - if (!rerollCount || parseInt(rerollCount) < 1) { - console.log('no reroll') - ui.notifications.warn("plus de relance possible"); - let rerollables = ev.currentTarget.closest('ul').querySelectorAll('.rerollable'); - rerollables.forEach(el => el.classList.remove('rerollable')); - - // Mise à jour du nombre de relances restantes - ev.currentTarget.closest('div.vermine-roll-message').querySelector('#allowed_reroll').innerText = rerollCount - 1; - - let content = ev.currentTarget.closest('div.message-content').outerHTML; - - await message.update({ - content: content - }) - return false + // Get reroll count + const rollMessage = ev.currentTarget.closest('div.vermine-roll-message'); + if (!rollMessage) { + return false; } + + let rerollCount = rollMessage.querySelector('#allowed_reroll')?.innerText; + + // Check if rerolls are available + if (!rerollCount || parseInt(rerollCount, 10) < 1) { + ui.notifications.warn(game.i18n.localize('VERMINE.error_no_rerolls_left')); + const rerollables = ev.currentTarget.closest('ul')?.querySelectorAll('.rerollable'); + if (rerollables) { + rerollables.forEach(el => el.classList.remove('rerollable')); + } + return false; + } + ev.currentTarget.classList.add('rerolled'); - // Mise en place de la relance + // Set reroll flag await message.setFlag("world", "reroll", true); - // Récupération de la difficulté et du type de dé - let difficulty = ev.currentTarget.closest('ul').dataset.difficulty; + // Get difficulty and dice type + const ulElement = ev.currentTarget.closest('ul'); + const difficulty = ulElement?.dataset.difficulty ?? 7; let diceType = ev.currentTarget.dataset.diceType; - // Mise à jour du nombre de relances restantes - ev.currentTarget.closest('div.vermine-roll-message').querySelector('#allowed_reroll').innerText = rerollCount - 1; + // Sanitize user name + const safeUserName = (game.user?.name ?? "user").replace(/[^a-zA-Z0-9_]/g, '_'); - // Construction de la formule de relance + // Build reroll formula let formula = `1d10cs>=${difficulty}`; - console.log(diceType) - switch (diceType.trim()) { + + switch ((diceType ?? '').trim()) { case 'human': - formula = `(1d10cs>=${difficulty}[human_${game.user.name}])*2` + formula = `(1d10cs>=${difficulty}[human_${safeUserName}])*2`; break; case 'adapted': - formula = `(1d10cs>=${difficulty}[adapted_${game.user.name}])*2` + formula = `(1d10cs>=${difficulty}[adapted_${safeUserName}])*2`; break; default: - formula += `[regular_${game.user.name}]` + formula += `[regular_${safeUserName}]`; break; - }; + } - // Création et évaluation du jet de dés de relance - let reroll = await new Roll(formula); + // Create and evaluate reroll + const reroll = new Roll(formula); await reroll.evaluate(); - //afficher les dés 3d + // Show 3D dice if available await VermineUtils.showDiceSoNice(reroll); - // mise à jour de l'affichage du dés - console.log(reroll) - let result = reroll.dice[0].results[0].result; - ev.currentTarget.querySelector('span').innerText = result; - //mise à jour du total - let success = reroll.dice[0].results[0].success; - if (success) { - ev.currentTarget.classList.add('success') - let total = parseInt(ev.currentTarget.closest('.vermine-roll-message').querySelector('#total').innerText) + reroll.total - ev.currentTarget.closest('.vermine-roll-message').querySelector('#total').innerText = total - } - // Mise à jour de l'affichagedu message - ev.currentTarget.classList.remove("rerollable") - let content = ev.currentTarget.closest('div.message-content').outerHTML; - console.log(reroll, message); - await message.update({ - content: content - }) + // Update die display + const result = reroll.dice[0]?.results[0]?.result ?? 0; + const dieSpan = ev.currentTarget.querySelector('span'); + if (dieSpan) { + dieSpan.innerText = result; + } + + // Update total if successful + const success = reroll.dice[0]?.results[0]?.success; + if (success) { + ev.currentTarget.classList.add('success'); + const totalElement = rollMessage.querySelector('#total'); + if (totalElement) { + const currentTotal = parseInt(totalElement.innerText, 10) || 0; + totalElement.innerText = currentTotal + reroll.total; + } + } + + // Update message content + ev.currentTarget.classList.remove("rerollable"); + + const messageContent = ev.currentTarget.closest('div.message-content'); + if (messageContent) { + const newRerollCount = parseInt(rerollCount, 10) - 1; + rollMessage.querySelector('#allowed_reroll').innerText = newRerollCount; + + await message.update({ + content: messageContent.outerHTML + }); + } + + return true; } - /** - * Méthode pour gérer les événements de chat - * @param {HTMLElement} html - L'élément HTML contenant les événements de chat + /** + * Sets up event listeners for chat messages. + * @param {HTMLElement} html - The HTML element containing chat events. */ static async chatListenners(html) { - // Récupérer le nombre de relances autorisées - let reroll = html.find('#allowed_reroll')[0]?.innerText; - // Vérifier s'il n'y a pas de relances ou si le nombre est inférieur à 1 - if (!reroll || parseInt(reroll) < 1) { - // Désactiver les relances pour chaque dé - for (let die of html.find('.die')) { - die.classList.remove("rerollable") - }; + // Get reroll count + const rerollCountElement = html.find('#allowed_reroll')[0]; + const rerollCount = rerollCountElement?.innerText; + + // Enable/disable rerolls based on count + if (!rerollCount || parseInt(rerollCount, 10) < 1) { + // Disable rerolls for all dice + html.find('.die').forEach(die => { + die.classList.remove("rerollable"); + }); } else { - // Activer les relances pour chaque dé - for (let die of html.find('.die')) { - die.classList.add("rerollable") - }; + // Enable rerolls for all dice + html.find('.die').forEach(die => { + die.classList.add("rerollable"); + }); } - // Ajouter un événement de clic pour les dés pouvant être relancés + // Add click event for rerollable dice html.find('.rerollable').click(async (ev) => { ev.preventDefault(); - // Récupérer l'ID du message - let msgId = ev.currentTarget.closest("li.message").dataset.messageId; - // Récupérer le message correspondant à l'ID - let message = await game.messages.get(msgId); - // Appeler la fonction onReroll de VermineUtils - await VermineUtils.onReroll(message, ev); + const msgId = ev.currentTarget.closest("li.message")?.dataset?.messageId; + if (msgId) { + const message = await game.messages.get(msgId); + await VermineUtils.onReroll(message, ev); + } }); - // Mettre à jour l'étiquette en fonction de la valeur sélectionnée + // Update granted reroll label html.find("#effort-reroll").change(ev => { - let label = html.find("#granted-reroll")[0] - label.innerText = ev.currentTarget.value + const label = html.find("#granted-reroll")[0]; + if (label) { + label.innerText = ev.currentTarget.value; + } }); - // Ajouter un événement de clic pour accorder une relance + // Add click event for granting rerolls html.find("button.grant-reroll").click(async (ev) => { - // Mettre à jour le nombre de relances autorisées - html.find("#allowed_reroll")[0].innerText = html.find('#granted-reroll')[0].innerText - let mesEl = ev.currentTarget.closest('[data-message-id]') - let messageId = mesEl.dataset.messageId; - // Quand relance accorder masquer la zone pour accorder les relances - ev.currentTarget.closest('.reroll-from-effort').style.display = "none" - let content = ev.currentTarget.closest(".vermine-roll-message").outerHTML; - // Mettre à jour le contenu du message avec la relance accordée - let message = await game.messages.get(messageId); - await message.update({ content: content }); + const grantedRerollElement = html.find('#granted-reroll')[0]; + const allowedRerollElement = html.find("#allowed_reroll")[0]; + + if (grantedRerollElement && allowedRerollElement) { + allowedRerollElement.innerText = grantedRerollElement.innerText; + } + + const mesEl = ev.currentTarget.closest('[data-message-id]'); + const messageId = mesEl?.dataset?.messageId; + + if (messageId) { + // Hide reroll grant area + ev.currentTarget.closest('.reroll-from-effort').style.display = "none"; + + const rollMessage = ev.currentTarget.closest(".vermine-roll-message"); + if (rollMessage) { + const content = rollMessage.outerHTML; + const message = await game.messages.get(messageId); + await message.update({ content: content }); + } + } }); } /** - * Méthode pour afficher les résultats des dés de manière graphique - * @param {Roll} roll - Le jet de dés à afficher - * @param {string} rollMode - Le mode d'affichage du jet de dés + * Displays dice rolls in 3D if available. + * @param {Roll} roll - The roll to display + * @param {string} [rollMode] - The roll mode (uses game settings if not provided) + * @returns {Promise} Whether 3D dice were shown */ static async showDiceSoNice(roll, rollMode) { - if (game.dice3d) { - let whisper = null; - let blind = false; - rollMode = rollMode ?? game.settings.get("core", "rollMode"); - switch (rollMode) { - case "blindroll": //GM only - blind = true; - case "gmroll": //GM + rolling player - whisper = this.getUsers(user => user.isGM); - break; - case "roll": //everybody - whisper = this.getUsers(user => user.active); - break; - case "selfroll": - whisper = [game.user.id]; - break; - } - await game.dice3d.showForRoll(roll, game.user, true, whisper, blind); + if (!game.dice3d) { + return false; } - else { return false } + + rollMode = rollMode ?? game.settings.get("core", "rollMode"); + let whisper = null; + let blind = false; + + switch (rollMode) { + case "blindroll": // GM only + blind = true; + // Falls through + case "gmroll": // GM + rolling player + whisper = this.getUsers(user => user.isGM); + break; + case "roll": // Everybody + whisper = this.getUsers(user => user.active); + break; + case "selfroll": + whisper = [game.user.id]; + break; + } + + await game.dice3d.showForRoll(roll, game.user, true, whisper, blind); + return true; } /** - * Méthode pour afficher un jet de dés dans le chat - * @param {Roll} roll - Le jet de dés à afficher - * @param {Object} param - Les paramètres du jet de dés - * @returns {ChatMessage} - Le message affichant le jet de dés + * Displays a dice roll in the chat. + * @param {Roll} roll - The roll to display + * @param {Object} param - Roll parameters + * @returns {Promise} The created chat message */ static async diplayChatRoll(roll, param) { - let content = await renderTemplate("systems/vermine2047/templates/roll-message.hbs", { roll, param }) - let chatData = { - user: game.user._id, + const content = await renderTemplate("systems/vermine2047/templates/roll-message.hbs", { roll, param }); + const chatData = { + user: game.user?._id, speaker: ChatMessage.getSpeaker(), content: content, roll: roll }; - let msg = await ChatMessage.create(chatData); + const msg = await ChatMessage.create(chatData); await msg.setFlag('world', 'roll', roll); - return msg + return msg; } diff --git a/templates/roll-message.hbs b/templates/roll-message.hbs index eeb077e..783d72b 100644 --- a/templates/roll-message.hbs +++ b/templates/roll-message.hbs @@ -1,78 +1,61 @@ - {{log this}} - {{param.actor.name}} : test de {{param.rollLabel}} + {{param.actor.name}} : {{localize "VERMINE.test_of"}} {{param.rollLabel}} + - difficulté + {{localize "VERMINE.difficulty"}}: {{param.difficulty}} + - relances possibles : {{param.Reroll}} - - + {{localize "VERMINE.rerolls_possible"}} : {{param.Reroll}} + + - effort + {{localize "VERMINE.effort"}} - - - 0 - - - - - - - - {{#each roll.dice as |dieType index|}} - {{#each dieType.results as |die index|}} - - - {{die.result}} - - - {{/each}} - {{/each}} - - - - nombre de succès : - {{roll._total}} - - - succès requis : - {{param.handicap}} + + + + 0 + + + + {{#each roll.dice as |dieType index|}} + {{#each dieType.results as |die index|}} + {{! Determine dice class and type based on flavor }} + {{#ifincludes dieType.options.flavor "human"}} + {{set diceClass="human"}} + {{set diceTypeVal="human"}} + {{else ifincludes dieType.options.flavor "adapted"}} + {{set diceClass="adapted"}} + {{set diceTypeVal="adapted"}} + {{else}} + {{set diceClass="regular"}} + {{set diceTypeVal="regular"}} + {{/ifincludes}} + + {{die.result}} + + {{/each}} + {{/each}} + + + + + {{localize "VERMINE.success_count"}}: + {{roll._total}} + + + {{localize "VERMINE.success_required"}}: + {{param.handicap}} + + \ No newline at end of file