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: '', label: "Lancer !", callback: () => this._onRoll() }, cancel: { icon: '', 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} 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} 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() }); } }