import { MournbladeUtility } from "../mournblade-utility.js" /** * Dialogue de jet de dé pour Mournblade - Version DialogV2 */ export class MournbladeRollDialog { /** * Create and display the roll dialog * @param {MournbladeActor} actor - The actor making the roll * @param {Object} rollData - Data for the roll * @returns {Promise} */ static async create(actor, rollData) { // Préparer le contexte pour le template const context = { ...rollData, difficulte: String(rollData.difficulte || 0), img: actor.img, name: actor.name, config: game.system.mournblade.config, } // Si attrKey est "tochoose", préparer la liste des attributs sélectionnables if (rollData.attrKey === "tochoose") { context.selectableAttributes = actor.system.attributs } // Rendre le template en HTML const content = await foundry.applications.handlebars.renderTemplate( "systems/fvtt-mournblade/templates/roll-dialog-v2.hbs", context ) // Utiliser DialogV2.wait avec le HTML rendu return foundry.applications.api.DialogV2.wait({ window: { title: "Test de Capacité", icon: "fa-solid fa-dice-d20" }, classes: ["mournblade-roll-dialog"], position: { width: 500 }, modal: false, content, buttons: [ { action: "rolld10", label: "Lancer 1d10", icon: "fa-solid fa-dice-d10", default: true, callback: async (event, button, dialog) => { this._updateRollDataFromForm(rollData, button.form.elements, actor) rollData.mainDice = "1d10" await MournbladeUtility.rollMournblade(rollData) } }, { action: "rolld20", label: "Lancer 1d20", icon: "fa-solid fa-dice-d20", callback: async (event, button, dialog) => { this._updateRollDataFromForm(rollData, button.form.elements, actor) rollData.mainDice = "1d20" await MournbladeUtility.rollMournblade(rollData) } }, ], rejectClose: false, }) } /** * Create and display the sortilège (multi-rune spell) roll dialog * @param {MournbladeActor} actor * @param {Object} rollData */ static async createSortilege(actor, rollData) { const ameDisponible = Math.max(1, (actor.system.ame.currentmax - actor.system.ame.value)) const context = { ...rollData, img: actor.img, name: actor.name, config: game.system.mournblade.config, runes: actor.getRunes(), runemode: "prononcer", ameDisponible, ameOptions: Array.from({ length: ameDisponible }, (_, i) => i + 1), modOptions: Array.from({ length: 21 }, (_, i) => i - 10), } const content = await foundry.applications.handlebars.renderTemplate( "systems/fvtt-mournblade/templates/dialog-sortilege.hbs", context ) // Attach dynamic recalculation after render Hooks.once("renderDialogV2", (_app, html) => { const form = html.querySelector ? html : html[0] MournbladeRollDialog._attachSortilegeListeners(form) }) return foundry.applications.api.DialogV2.wait({ window: { title: "Lancer un Sortilège", icon: "fa-solid fa-star-of-david" }, classes: ["mournblade-roll-dialog"], position: { width: 520 }, modal: false, content, buttons: [ { action: "rolld10", label: "Lancer 1d10", icon: "fa-solid fa-dice-d10", default: true, callback: async (event, button, dialog) => { this._updateSortilegeRollData(rollData, button.form.elements, actor) rollData.mainDice = "1d10" await MournbladeUtility.rollSortilege(rollData) } }, { action: "rolld20", label: "Lancer 1d20", icon: "fa-solid fa-dice-d20", callback: async (event, button, dialog) => { this._updateSortilegeRollData(rollData, button.form.elements, actor) rollData.mainDice = "1d20" await MournbladeUtility.rollSortilege(rollData) } }, ], rejectClose: false, }) } /** * Open the potion preparation dialog * @param {MournbladeActor} actor * @param {object} rollData */ static async createPotion(actor, rollData) { const ameDisponible = Math.max(1, (actor.system.ame.currentmax - actor.system.ame.value)) const context = { ...rollData, img: actor.img, name: actor.name, config: game.system.mournblade.config, runes: actor.getRunes(), ameDisponible, ameOptions: Array.from({ length: ameDisponible }, (_, i) => i + 1), modOptions: Array.from({ length: 21 }, (_, i) => i - 10), } const content = await foundry.applications.handlebars.renderTemplate( "systems/fvtt-mournblade/templates/dialog-potion.hbs", context ) Hooks.once("renderDialogV2", (_app, html) => { const form = html.querySelector ? html : html[0] MournbladeRollDialog._attachPotionListeners(form) }) return foundry.applications.api.DialogV2.wait({ window: { title: "Préparer une Potion", icon: "fa-solid fa-flask" }, classes: ["mournblade-roll-dialog"], position: { width: 520 }, modal: false, content, buttons: [ { action: "preparer", label: "Préparer", icon: "fa-solid fa-flask", default: true, callback: async (event, button, dialog) => { MournbladeRollDialog._updatePotionRollData(rollData, button.form.elements, actor) await MournbladeUtility.rollPotion(rollData) } }, ], rejectClose: false, }) } /** * Attach dynamic recalculation listeners to the potion dialog * @param {HTMLElement} html */ static _attachPotionListeners(html) { const recalculate = () => { const radioChecked = html.querySelector('.potion-rune-radio:checked') const runeRow = radioChecked?.closest('[data-seuil]') const seuil = parseInt(runeRow?.dataset.seuil ?? 0) const pa = parseInt(html.querySelector('[name="pointsAme"]')?.value ?? 1) const mod = parseInt(html.querySelector('[name="modificateur"]')?.value ?? 0) const difficulteEl = html.querySelector('#potion-difficulte') if (difficulteEl) difficulteEl.textContent = (seuil + pa) + (mod !== 0 ? ` (mod ${mod > 0 ? '+' : ''}${mod})` : '') const tempsEl = html.querySelector('#potion-temps') if (tempsEl) { const heures = Math.max(1, Math.ceil(pa / 3)) tempsEl.textContent = heures === 1 ? '1 heure' : `${heures} heures` } } html.querySelectorAll('.potion-rune-radio, [name="pointsAme"]').forEach(el => { el.addEventListener('change', recalculate) }) recalculate() } /** * Update rollData from the potion dialog form elements * @param {object} rollData * @param {HTMLFormControlsCollection} formElements * @param {MournbladeActor} actor */ static _updatePotionRollData(rollData, formElements, actor) { const runeId = formElements['rune-selected']?.value if (runeId) { const rune = actor.getRunes().find(r => r._id === runeId) if (rune) { rollData.runeId = runeId rollData.runeName = rune.name rollData.runeImg = rune.img rollData.runeSeuil = rune.system.seuil ?? 0 rollData.runeHautParler = rune.system.hautParler ?? "" } } rollData.pointsAme = parseInt(formElements['pointsAme']?.value ?? 1) rollData.forme = formElements['forme']?.value ?? "liquide" rollData.modificateur = parseInt(formElements['modificateur']?.value ?? 0) } /** * Attach dynamic recalculation listeners to the sortilège dialog * @param {HTMLElement} html */ static _attachSortilegeListeners(html) { const recalculate = () => { const checkboxes = html.querySelectorAll('.sortilege-rune-checkbox:checked') const modeSelect = html.querySelector('[name="runemode"]') const mode = modeSelect?.value ?? "prononcer" let maxSeuil = 0 let totalAme = 0 let totalActions = 0 let count = 0 // Enable/disable points inputs based on checkbox state html.querySelectorAll('.sortilege-rune-checkbox').forEach(cb => { const row = cb.closest('.sortilege-rune-row') const pointsInput = row?.querySelector('.sortilege-rune-points') if (pointsInput) pointsInput.disabled = !cb.checked }) checkboxes.forEach(cb => { const row = cb.closest('.sortilege-rune-row') const seuil = Number(row?.dataset.seuil ?? 0) const pts = Number(row?.querySelector('.sortilege-rune-points')?.value ?? 1) if (seuil > maxSeuil) maxSeuil = seuil totalAme += pts totalActions += Math.ceil(pts / 3) * (mode === "inscrire" ? 2 : 1) count++ }) const difficulte = count > 0 ? maxSeuil + (count - 1) : 0 html.querySelector('#sortilege-difficulte').textContent = count > 0 ? difficulte : '—' html.querySelector('#sortilege-total-ame').textContent = totalAme html.querySelector('#sortilege-actions').textContent = totalActions } html.querySelectorAll('.sortilege-rune-checkbox, .sortilege-rune-points, [name="runemode"]') .forEach(el => el.addEventListener('change', recalculate)) } /** * Extract sortilège data from the form * @param {Object} rollData * @param {HTMLFormControlsCollection} formElements * @param {MournbladeActor} actor */ static _updateSortilegeRollData(rollData, formElements, actor) { rollData.runemode = formElements.runemode?.value ?? "prononcer" rollData.modificateur = Number(formElements.modificateur?.value ?? 0) rollData.runeautocible = formElements.runeautocible?.checked ?? false // Collect selected runes with their soul points const runes = actor.getRunes() rollData.sortilegeRunes = [] for (const rune of runes) { const checkbox = formElements[`rune-selected-${rune._id}`] if (checkbox?.checked) { const pts = Math.max(1, Number(formElements[`rune-points-${rune._id}`]?.value ?? 1)) rollData.sortilegeRunes.push({ id: rune._id, name: rune.name, img: rune.img, seuil: rune.system.seuil, formule: rune.system.formule, pts, }) } } if (rollData.sortilegeRunes.length === 0) return const maxSeuil = Math.max(...rollData.sortilegeRunes.map(r => r.seuil)) rollData.difficulte = maxSeuil + (rollData.sortilegeRunes.length - 1) rollData.runeame = rollData.sortilegeRunes.reduce((s, r) => s + r.pts, 0) } /** * @param {Object} rollData - L'objet rollData à mettre à jour * @param {HTMLFormControlsCollection} formElements - Les éléments du formulaire * @param {MournbladeActor} actor - L'acteur pour récupérer les attributs * @private */ static _updateRollDataFromForm(rollData, formElements, actor) { // Attributs if (formElements.attrKey) { rollData.attrKey = formElements.attrKey.value if (rollData.attrKey !== "tochoose" && actor) { rollData.attr = foundry.utils.duplicate(actor.system.attributs[rollData.attrKey]) rollData.actionImg = "systems/fvtt-mournblade/assets/icons/" + actor.system.attributs[rollData.attrKey].labelnorm + ".webp" } } // Modificateurs de base if (formElements.difficulte) { rollData.difficulte = Number(formElements.difficulte.value) } if (formElements.modificateur) { rollData.modificateur = Number(formElements.modificateur.value) } // Runes if (formElements.runemode) { rollData.runemode = String(formElements.runemode.value) } if (formElements.runeame) { rollData.runeame = Number(formElements.runeame.value) } if (formElements.runeautocible !== undefined) { rollData.runeautocible = formElements.runeautocible.checked } // Combat mêlée if (formElements.typeAttaque) { rollData.typeAttaque = String(formElements.typeAttaque.value) } if (formElements.isMonte !== undefined) { rollData.isMonte = formElements.isMonte.checked } // Combat distance if (formElements.visee !== undefined) { rollData.visee = formElements.visee.checked } if (formElements.cibleconsciente !== undefined) { rollData.cibleconsciente = formElements.cibleconsciente.checked } if (formElements.ciblecourt !== undefined) { rollData.ciblecourt = formElements.ciblecourt.checked } if (formElements.typeCouvert) { rollData.typeCouvert = String(formElements.typeCouvert.value) } // Désavantages if (!rollData.desavantages) rollData.desavantages = {} if (formElements.cibleausol !== undefined) { rollData.desavantages.cibleausol = formElements.cibleausol.checked } if (formElements.cibledesarmee !== undefined) { rollData.desavantages.cibledesarmee = formElements.cibledesarmee.checked } if (formElements.ciblerestreint !== undefined) { rollData.desavantages.ciblerestreint = formElements.ciblerestreint.checked } if (formElements.cibleimmobilisée !== undefined) { rollData.desavantages.cibleimmobilisée = formElements.cibleimmobilisée.checked } if (formElements.ciblesurplomb !== undefined) { rollData.desavantages.ciblesurplomb = formElements.ciblesurplomb.checked } // Double D20 if (formElements.doubleD20 !== undefined) { rollData.doubleD20 = formElements.doubleD20.checked } // Modifiers if (rollData.modifiers) { rollData.modifiers.forEach((modifier, idx) => { const checkbox = formElements[`apply-modifier-${idx}`] if (checkbox) { modifier.system.apply = checkbox.checked } }) } } }