Files
vermine2047/module/system/dialogs/rollDialog.mjs
T
uberwald 386d80639c code review: fix critical issues and improve code quality
- Fix constructor in rollDialog.mjs (spread operator for options)
- Remove all console.log statements from production code
- Add comprehensive JSDoc comments for all public APIs
- Convert French comments to English for consistency
- Use parseInt with radix parameter (10) throughout
- Replace let with const where appropriate
- Use Set for O(1) lookups in group-link.mjs methods
- Use spread operators for array cloning
- Optimize removeActorFromAllGroups with Set lookups
- Improve registerHooks with better comments and Set usage
- Simplify roll-message.hbs template logic
- Fix duplicate VERMINE key in lang/fr.json
- Add missing error translations
- Add .eslintrc.js with FoundryVTT-compatible linting config

Compatibility: FoundryVTT v11-v14

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-04 13:33:58 +02:00

596 lines
21 KiB
JavaScript

import { VermineUtils } from "../roll.mjs";
/**
* 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
*/
constructor(data, html, options, close = undefined) {
const conf = {
title: "jet de dés",
content: html,
buttons: {
roll: {
icon: '<i class="fas fa-check"></i>',
label: "Lancer !",
callback: () => this._onRoll()
},
cancel: {
icon: '<i class="fas fa-times"></i>',
label: "Annuler",
callback: () => this.close()
}
},
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 {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<RollDialog|null>} 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
}) {
// Validate actorId
const actorId = data.actorId;
if (!actorId || typeof actorId !== 'string') {
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_actor_selected'));
return null;
}
// 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
const options = {
classes: ["vermineDialog"],
width: "fit-content",
height: 'fit-content',
zIndex: 99999
};
// Render the HTML template for the dialog
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);
}
/**
* Retrieves the default options for the RollDialog.
*/
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
focus: true,
classes: ["dialog vermine-roll"],
});
}
/**
* Retrieves the data for the dialog.
* @returns {Object} The context data for the dialog
*/
getData() {
// Get the context data from the superclass
const context = super.getData();
context.data = this.data;
context.config = CONFIG.VERMINE;
return context;
}
/**
* Prepares items for display.
* @returns {Array} Filtered list of items
*/
prepareItems() {
return this.data.actor.items.filter(it => it.type === "item");
}
/**
* Prepares specialties for display.
* @returns {Array} Filtered list of specialties
*/
prepareSpecialties() {
return this.data.actor.items.filter(it => it.type === "specialty");
}
/**
* Activates event listeners for the dialog.
* @param {HTMLElement} html - The HTML element of the dialog.
*/
async activateListeners(html) {
// Activate event listeners from the superclass
super.activateListeners(html);
// Initialize UI elements
this._html = html;
// Retrieve roll data and set up event listeners
await this.getRollData();
// Set up event listeners for all roll-related inputs
const rollInputs = html.find('[data-roll]');
for (const inp of rollInputs) {
inp.addEventListener('change', this._onRollInputChange.bind(this));
}
this.displaySpecialties();
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));
const selfControl = html.find('#self_control')[0];
// Add event listener for self control changes
selfControl.addEventListener('change', this._onChangeSelfControl.bind(this));
// Set up difficulty change listener
html.find('#difficulty')[0].addEventListener('change', this._onDifficultyChange.bind(this));
// Set up handicap change listener
html.find('#handicap')[0].addEventListener('change', this._onHandicapChange.bind(this));
// Set up totem checkbox listeners
html.find('#human-totem')[0]?.addEventListener('change', this._onTotemChange.bind(this));
html.find('#adapted-totem')[0]?.addEventListener('change', this._onTotemChange.bind(this));
// Initial update of all UI elements
this._updateUI();
};
/**
* Retrieves the roll data for the dialog.
* @param {Event} _ev - The event triggering the roll data retrieval (unused).
*/
getRollData(_ev) {
// Calculate and store the roll data
this.rollData = {
actor: this.data.actor,
NoD: this.getDicePool(),
Reroll: this.getReroll(),
difficulty: this.getDifficulty(),
handicap: this.getHandicap(),
rollType: this.getRollType(),
rollLabel: this.getLabel(),
totems: this.getTotems(),
self_control: this.getSelfControl(),
max_effort: this.getMaxEffort(),
keepTotem: this.getKeepTotem(),
skillCategory: this.getSkillCategory()
};
this.displaySpecialties();
this._updateUI();
};
/**
* Gets the selected skill category
* @returns {string|null} - The skill category
*/
getSkillCategory() {
const html = this.element[0];
const skillSelect = html.querySelector('#skill');
if (skillSelect && skillSelect.selectedIndex > 0) {
const selectedOption = skillSelect.options[skillSelect.selectedIndex];
return selectedOption.dataset.category || null;
}
return null;
}
/**
* Gets the selected skill level
* @returns {number|null} - The skill level
*/
getSkillLevel() {
const html = this.element[0];
const skillSelect = html.querySelector('#skill');
if (skillSelect && skillSelect.selectedIndex > 0) {
const selectedOption = skillSelect.options[skillSelect.selectedIndex];
return parseInt(selectedOption.value) || null;
}
return null;
}
/**
* Checks if a specialty is selected
* @returns {boolean} - True if a specialty is selected
*/
hasSpecialtySelected() {
const html = this.element[0];
const specialtyRadio = html.querySelector('input[name="usingSpecialization"]:checked');
return specialtyRadio && specialtyRadio.value !== 'aucune';
}
/**
* Handles changes to roll inputs and updates UI.
* @param {Event} ev - The change event.
*/
_onRollInputChange(ev) {
this.getRollData(ev);
}
/**
* Updates all UI elements based on current roll data
*/
_updateUI() {
if (!this._html) return;
const html = this._html[0];
// Update total dice pool display
const totalDice = this.getDicePool();
const totalEl = html.querySelector('#dice-pool-total');
if (totalEl) {
totalEl.textContent = `${totalDice}D`;
}
// Update bonus count
const bonusCount = this._calculateBonusCount();
const bonusEl = html.querySelector('#total-bonus');
if (bonusEl) {
bonusEl.textContent = bonusCount;
}
// Update difficulty display
const difficultyEl = html.querySelector('#current-difficulty');
const difficultySelect = html.querySelector('#difficulty');
if (difficultyEl && difficultySelect) {
const selectedIndex = difficultySelect.selectedIndex;
const diffValue = parseInt(difficultySelect.options[selectedIndex].value);
const diffLabel = difficultySelect.options[selectedIndex].text.split(' ')[0];
difficultyEl.textContent = `${diffLabel} (${diffValue})`;
}
// Update handicap display
const handicapEl = html.querySelector('#current-handicap');
const handicapSelect = html.querySelector('#handicap');
if (handicapEl && handicapSelect) {
const selectedIndex = handicapSelect.selectedIndex;
handicapEl.textContent = handicapSelect.options[selectedIndex].text;
}
// Update ability score display
const abilSelect = html.querySelector('#ability');
const abilScoreEl = html.querySelector('#abilityScoreValue');
if (abilSelect && abilScoreEl) {
const selectedIndex = abilSelect.selectedIndex;
if (selectedIndex > 0) {
abilScoreEl.textContent = abilSelect.options[selectedIndex].value;
} else {
abilScoreEl.textContent = '0';
}
}
// Update specialty display
const specialtyRadios = html.querySelectorAll('input[name="usingSpecialization"]:checked');
const currentSpecEl = html.querySelector('.current-specialty');
if (currentSpecEl && specialtyRadios.length > 0) {
const checkedRadio = specialtyRadios[0];
currentSpecEl.textContent = checkedRadio.value === 'aucune' ? game.i18n.localize('VERMINE.none') : checkedRadio.value;
}
}
/**
* Calculates the bonus count for display.
* @returns {number} Total bonus dice.
*/
_calculateBonusCount() {
let bonus = 0;
// Help bonus
if (this._html?.find('#helped')[0]?.checked) {
bonus += 1;
}
// Group bonus
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, 10) || 0;
bonus += selfControlValue;
// Tools bonus
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, 10) || 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';
if (specialtyChecked) {
bonus += 1;
}
return bonus;
}
/**
* Handles difficulty change
* @param {Event} ev - The change event
*/
_onDifficultyChange(ev) {
this._updateUI();
}
/**
* Handles handicap change
* @param {Event} ev - The change event
*/
_onHandicapChange(ev) {
this._updateUI();
}
/**
* Handles totem checkbox change
* @param {Event} ev - The change event
*/
_onTotemChange(ev) {
this._updateUI();
}
/**
* Gets the selected totem to keep (for dual totem rolls)
* @returns {string|null} - The totem to keep ('human', 'adapted', or null)
*/
getKeepTotem() {
const keepTotemSelect = this._html?.find('#keep-totem-select')[0];
if (keepTotemSelect) {
return keepTotemSelect.value;
}
// Default to null (both totems used)
return null;
}
/**
* Handles the change in self control value.
* @param {Event} ev - The event triggering the change in self control value.
*/
_onChangeSelfControl(ev) {
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() {
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() {
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() {
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 ?? "";
}
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() {
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() {
const html = this.element[0];
const selfControlValue = html.querySelector('#self_control')?.value ?? '0';
return parseInt(selfControlValue, 10);
}
/**
* Retrieves the maximum effort value from the HTML element.
* @returns {number} The maximum effort value.
*/
getMaxEffort() {
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 {human: boolean, adapted: boolean}.
*/
getTotems() {
const html = this.element[0];
return {
human: html.querySelector('#human-totem')?.checked ?? false,
adapted: html.querySelector('#adapted-totem')?.checked ?? false
};
}
/**
* Handles the change in ability value.
* @param {Event} ev - The event triggering the change in ability value.
*/
_onChangeAbility(ev) {
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;
}
}
/**
* Retrieves the total dice pool based on various factors.
* @returns {number} The total dice pool value.
*/
getDicePool() {
// Retrieve the HTML element
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
const selfControl = html.querySelector('#self_control')?.value ?? 0;
// Calculate bonuses based on certain conditions
const bonuses =
(html.querySelector('#usingSpecialization')?.checked ? 1 : 0) +
(html.querySelector('#helped')?.checked ? 1 : 0) +
(html.querySelector('#usingTools')?.checked ? 1 : 0);
// Calculate the total dice pool
const total = parseInt(abilValue, 10) + parseInt(selfControl, 10) + parseInt(skillValue, 10) + bonuses;
return total || 0;
}
/**
* Retrieves the reroll value based on selected skill.
* @returns {number} The reroll value.
*/
getReroll() {
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() {
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<Roll|false>} 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
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
}
}
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": newSelfControl
});
}
// Perform the dice roll using VermineUtils
return VermineUtils.roll({
...this.rollData,
skillLevel: this.getSkillLevel(),
hasSpecialty: this.hasSpecialtySelected()
});
}
}