import { VermineUtils } from "../roll.mjs"; const { HandlebarsApplicationMixin } = foundry.applications.api; export default class RollDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) { #actor; get title() { return game.i18n.localize("VERMINE.roll"); } static DEFAULT_OPTIONS = { classes: ["vermine-roll"], tag: "form", window: { icon: "fas fa-dice-d10", resizable: false }, position: { width: 520, height: 600 }, actions: { roll: RollDialog.#onRoll, cancel: RollDialog.#onCancel, giveToGroup: RollDialog.#onGiveToGroup } }; static PARTS = { main: { template: "systems/vermine2047/templates/dialogs/roll-dialog.hbs" } }; static async create(data = {}) { if (data.actor instanceof Actor) { return new RollDialog({ actor: data.actor, label: data.label, rolltype: data.rolltype, weapon: data.weapon, locks: data.locks, defense: data.defense }); } const actorId = data.actorId ?? game.user.character?.id ?? canvas.tokens.controlled[0]?.actor?.id; if (!actorId || typeof actorId !== "string") { ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected")); return null; } const actor = await game.actors.get(actorId); if (!actor) { ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected")); return null; } return new RollDialog({ actor, label: data.label, rolltype: data.rolltype, weapon: data.weapon, locks: data.locks, defense: data.defense }); } constructor(options = {}) { super(options); this.#actor = options.actor; this.label = options.label ?? null; this.rolltype = options.rolltype ?? null; this.weapon = options.weapon ?? null; this.locks = options.locks ?? null; this.defense = options.defense ?? null; } async _prepareContext() { const actor = this.#actor; const isCharacter = actor.type === "character"; const group = isCharacter ? RollDialog.#findGroup(actor) : null; const groupPool = group?.system?.experience?.dice || 0; return { actor, system: actor.system, config: CONFIG.VERMINE, label: this.label, rollType: this.rolltype, labelKey: this.label, speakerId: actor.id, ability: null, help: false, specialty: false, availableSpecialties: actor.items.filter(i => i.type === "specialty"), availableItems: actor.items.filter(i => i.type === "item"), locks: this.locks, experience: { hasExperience: isCharacter, individual: isCharacter ? actor.system?.experience?.dice || 0 : 0, groupPool, pullOptions: [0, 1, 2].filter(v => v <= Math.min(2, groupPool)), canPull: Boolean(group) && (game.user.isGM || group.isOwner), canGive: isCharacter && (game.user.isGM || actor.isOwner) } }; } async _onRender(context, options) { this.element.dataset.actorId = this.#actor.id; // Mode verrouillé (défense en échange de coups) : présélectionne et bloque // caractéristique + difficulté, présélectionne la compétence. if (this.locks) { this.#applyLocks(this.locks); } for (const inp of this.element.querySelectorAll("[data-roll]")) { inp.addEventListener("change", this.#onInputChange.bind(this)); } const ability = this.element.querySelector("#ability"); if (ability) { ability.addEventListener("change", this.#onChangeAbility.bind(this)); const selfControl = this.element.querySelector("#self_control"); if (selfControl) selfControl.max = ability.value; } const selfControl = this.element.querySelector("#self_control"); if (selfControl) { selfControl.addEventListener("change", this.#onChangeSelfControl.bind(this)); } this.element.querySelector("#difficulty")?.addEventListener("change", () => this.#updateUI()); this.element.querySelector("#handicap")?.addEventListener("change", () => this.#updateUI()); this.element.querySelector("#human-totem")?.addEventListener("change", () => this.#updateUI()); this.element.querySelector("#adapted-totem")?.addEventListener("change", () => this.#updateUI()); this.#displaySpecialties(); this.#refreshExperienceUI(); this.#updateUI(); if (ability?.value !== "0") { this.element.querySelector("#self_control")?.dispatchEvent(new Event("change")); } } // ── Getters ────────────────────────────────────────────────────────── get #el() { return this.element; } #getAbility() { return this.#el.querySelector("#ability"); } #getSkill() { return this.#el.querySelector("#skill"); } #getDifficulty() { return this.#el.querySelector("#difficulty"); } #getHandicap() { return this.#el.querySelector("#handicap"); } #getSelfCtrl() { return this.#el.querySelector("#self_control"); } #getExperiencePull() { return parseInt(this.#el.querySelector("#experience-pull")?.value, 10) || 0; } /** * Trouve le premier Groupe dont le personnage est membre. * @param {Actor} actor * @returns {Actor|null} */ static #findGroup(actor) { return game.actors.filter(a => a.type === "group" && (a.system?.members ?? []).includes(actor.id))[0] ?? null; } /** Décomposition du pool de dés pour affichage dans le message. */ getPoolBreakdown() { const abil = this.#getAbility(); const ability = parseInt(abil?.options[abil?.selectedIndex]?.value, 10) || 0; const skill = this.#getSkill(); const skillPool = parseInt(skill?.options[skill?.selectedIndex]?.dataset?.pool, 10) || 0; const selfControl = parseInt(this.#getSelfCtrl()?.value, 10) || 0; const specChecked = this.hasSpecialtySelected(); const helped = this.#el.querySelector("#helped")?.checked; const toolsChecked = this.#el.querySelector("input[name='usingTools']:checked"); const tools = toolsChecked && toolsChecked.value !== "0"; const group = parseInt(this.#el.querySelector("#group")?.value, 10) || 0; const experience = this.#getExperiencePull(); const specialty = specChecked ? 1 : 0; const help = helped ? 1 : 0; const tooling = tools ? 1 : 0; const total = ability + skillPool + selfControl + specialty + help + tooling + group + experience; return { ability, skill: skillPool, selfControl, specialty, help, tooling, group, experience, total }; } getDicePool() { return this.getPoolBreakdown().total; } getSpecialtyName() { const checked = this.#el.querySelector("input[name='usingSpecialization']:checked"); return checked && checked.value !== "aucune" ? checked.value : null; } getDifficultySelect() { const sel = this.#getDifficulty(); const idx = sel?.selectedIndex ?? 0; return parseInt(sel?.options[idx]?.value, 10) || 7; } getReroll() { const sel = this.#getSkill(); const idx = sel?.selectedIndex ?? 0; return parseInt(sel?.options[idx]?.dataset?.reroll, 10) || 0; } getHandicapSelect() { const sel = this.#getHandicap(); return Math.max(0, (parseInt(sel?.value, 10) || 1) - 1); } getSkillCategory() { const sel = this.#getSkill(); const idx = sel?.selectedIndex ?? 0; return sel?.options[idx]?.dataset?.category ?? null; } getSkillLevel() { const sel = this.#getSkill(); const idx = sel?.selectedIndex ?? 0; const val = sel?.options[idx]?.value; return val ? parseInt(val, 10) : null; } hasSpecialtySelected() { const checked = this.#el.querySelector("input[name='usingSpecialization']:checked"); return checked && checked.value !== "aucune"; } getRollType() { const sel = this.#getSkill(); return sel?.value ? "skill" : "ability"; } getLabel() { const type = this.getRollType(); if (type === "skill") { const sel = this.#getSkill(); const idx = sel?.selectedIndex ?? 0; return sel?.options[idx]?.dataset?.label ?? ""; } const sel = this.#getAbility(); const idx = sel?.selectedIndex ?? 0; return sel?.options[idx]?.dataset?.label ?? ""; } getSelfControl() { return parseInt(this.#getSelfCtrl()?.value, 10) || 0; } getMaxEffort() { const sel = this.#getAbility(); return parseInt(sel?.value, 10) || 0; } getTotems() { return { human: this.#el.querySelector("#human-totem")?.checked ?? false, adapted: this.#el.querySelector("#adapted-totem")?.checked ?? false }; } getKeepTotem() { return this.#el.querySelector("#keep-totem-select")?.value ?? null; } // ── UI ─────────────────────────────────────────────────────────────── #applyLocks(locks) { const ability = this.#getAbility(); if (ability && locks.ability) { const opt = [...ability.options].find(o => o.dataset.label === locks.ability); if (opt) { opt.selected = true; ability.disabled = true; const scoreEl = this.#el.querySelector("#abilityScore"); if (scoreEl) scoreEl.value = opt.value; const sc = this.#getSelfCtrl(); if (sc) sc.max = opt.value; } } const skill = this.#getSkill(); if (skill && locks.skill) { const opt = [...skill.options].find(o => o.dataset.label === locks.skill); if (opt) opt.selected = true; } const difficulty = this.#getDifficulty(); if (difficulty && locks.difficulty !== undefined && locks.difficulty !== null) { const locked = parseInt(locks.difficulty, 10); let opt = [...difficulty.options].find(o => parseInt(o.value, 10) === locked); if (!opt) { // Difficulté hors des options (ex: 9 + 2) : ajoute une option dédiée. opt = document.createElement("option"); opt.value = String(locked); opt.textContent = `${game.i18n.localize("VERMINE.difficulty")} (${locked})`; difficulty.appendChild(opt); } opt.selected = true; difficulty.disabled = true; } } #displaySpecialties() { for (const el of this.#el.querySelectorAll("[data-spec-skill]")) { el.style.display = "inline"; } } #calculateBonusCount() { const b = this.getPoolBreakdown(); return b.specialty + b.help + b.tooling + b.group + b.selfControl + b.experience; } #refreshExperienceUI() { const actor = this.#actor; const group = RollDialog.#findGroup(actor); const individual = actor.system?.experience?.dice || 0; const groupPool = group?.system?.experience?.dice || 0; const indEl = this.#el.querySelector("#exp-individual"); if (indEl) indEl.textContent = individual; const grpEl = this.#el.querySelector("#exp-group"); if (grpEl) grpEl.textContent = groupPool; const pull = this.#el.querySelector("#experience-pull"); if (pull) { const max = Math.min(2, groupPool); const current = Math.min(parseInt(pull.value, 10) || 0, max); pull.innerHTML = ""; for (let i = 0; i <= max; i++) { const opt = document.createElement("option"); opt.value = String(i); opt.textContent = `${i}D`; if (i === current) opt.selected = true; pull.appendChild(opt); } } } #updateUI() { const total = this.getDicePool(); const totalEl = this.#el.querySelector("#dice-pool-total"); if (totalEl) totalEl.textContent = `${total}D`; const bonusEl = this.#el.querySelector("#total-bonus"); if (bonusEl) bonusEl.textContent = this.#calculateBonusCount(); const diffSel = this.#getDifficulty(); const diffEl = this.#el.querySelector("#current-difficulty"); if (diffEl && diffSel) { const idx = diffSel.selectedIndex; const val = diffSel.options[idx].value; const lbl = diffSel.options[idx].text.split(" ")[0]; diffEl.textContent = `${lbl} (${val})`; } const handSel = this.#getHandicap(); const handEl = this.#el.querySelector("#current-handicap"); if (handEl && handSel) { handEl.textContent = handSel.options[handSel.selectedIndex].text; } const abilSel = this.#getAbility(); const abilValEl = this.#el.querySelector("#abilityScoreValue"); if (abilSel && abilValEl) { const idx = abilSel.selectedIndex; abilValEl.textContent = idx > 0 ? abilSel.options[idx].value : "0"; } const specChecked = this.#el.querySelector("input[name='usingSpecialization']:checked"); const specEl = this.#el.querySelector(".current-specialty"); if (specEl && specChecked) { specEl.textContent = specChecked.value === "aucune" ? game.i18n.localize("VERMINE.none") : specChecked.value; } } // ── Event handlers ─────────────────────────────────────────────────── #onInputChange() { this.#updateUI(); } #onChangeAbility(ev) { const sel = ev.currentTarget; const score = sel.options[sel.selectedIndex]?.value ?? "0"; const scoreEl = this.#el.querySelector("#abilityScore"); if (scoreEl) scoreEl.value = score; const sc = this.#getSelfCtrl(); if (sc) sc.max = score; this.#updateUI(); } #onChangeSelfControl(ev) { const valEl = this.#el.querySelector("#self_control_value"); if (valEl) valEl.textContent = ev.currentTarget.value; } static async #onCancel(event, target) { this.close(); } static async #onGiveToGroup(event, target) { const actor = this.#actor; if (actor.type !== "character") return; const individual = actor.system?.experience?.dice || 0; if (individual < 1) { ui.notifications.warn(game.i18n.localize("VERMINE.error_no_experience_dice")); return; } if (!game.user.isGM && !actor.isOwner) { ui.notifications.warn(game.i18n.localize("VERMINE.error_no_permission")); return; } const group = RollDialog.#findGroup(actor); if (!group) { ui.notifications.warn(game.i18n.localize("VERMINE.error_no_group")); return; } const groupPool = group.system?.experience?.dice || 0; await actor.update({ "system.experience.dice": individual - 1 }); await group.update({ "system.experience.dice": groupPool + 1 }); this.#refreshExperienceUI(); } static async #onRoll(event, target) { const selfCtrl = this.getSelfControl(); if (selfCtrl > 0) { const current = this.#actor?.system?.attributes?.self_control?.value ?? 0; if (current < selfCtrl) { ui.notifications.warn(game.i18n.localize("VERMINE.error_not_enough_self_control")); return; } } const abilityVal = this.#el.querySelector('[name="ability"]')?.value; if (!abilityVal || abilityVal === "0") { ui.notifications.warn(game.i18n.localize("VERMINE.error_select_ability")); return; } if (selfCtrl > 0) { const newVal = this.#actor.system.attributes.self_control.value - selfCtrl; await this.#actor.update({ "system.attributes.self_control.value": newVal }); } // Transfert d'Expérience : puiser dans la Réserve de Groupe (1-2D). const pulled = this.#getExperiencePull(); if (pulled > 0) { const group = RollDialog.#findGroup(this.#actor); if (!group) { ui.notifications.warn(game.i18n.localize("VERMINE.error_no_group")); return; } if (!game.user.isGM && !group.isOwner) { ui.notifications.warn(game.i18n.localize("VERMINE.error_no_permission")); return; } const groupPool = group.system?.experience?.dice || 0; if (groupPool < pulled) { ui.notifications.warn(game.i18n.localize("VERMINE.error_no_group_dice")); return; } await group.update({ "system.experience.dice": groupPool - pulled }); } await VermineUtils.roll({ actor: this.#actor, NoD: this.getDicePool(), Reroll: this.getReroll(), difficulty: this.getDifficultySelect(), handicap: this.getHandicapSelect(), rollLabel: this.getLabel(), totems: this.getTotems(), self_control: selfCtrl, max_effort: this.getMaxEffort(), keepTotem: this.getKeepTotem(), skillCategory: this.getSkillCategory(), skillLevel: this.getSkillLevel(), hasSpecialty: this.hasSpecialtySelected(), poolBreakdown: this.getPoolBreakdown(), specialtyName: this.getSpecialtyName(), weapon: this.weapon?.toObject() ?? null, targets: [...game.user.targets].map(t => t.name), messageFlags: this.defense ? { "vermine-defense": this.defense } : null }); this.close(); } }