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 } }; static PARTS = { main: { template: "systems/vermine2047/templates/dialogs/roll-dialog.hbs" } }; static async create(data = {}) { 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 }); } constructor(options = {}) { super(options); this.#actor = options.actor; this.label = options.label ?? null; this.rolltype = options.rolltype ?? null; } async _prepareContext() { const actor = this.#actor; 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") }; } async _onRender(context, options) { this.element.dataset.actorId = this.#actor.id; 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.#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"); } getDicePool() { const abil = this.#getAbility(); const abilVal = parseInt(abil?.options[abil?.selectedIndex]?.value, 10) || 0; const skill = this.#getSkill(); const skillPool = parseInt(skill?.options[skill?.selectedIndex]?.dataset?.pool, 10) || 0; const sc = parseInt(this.#getSelfCtrl()?.value, 10) || 0; const specChecked = this.#el.querySelector("#usingSpecialization")?.checked; const helped = this.#el.querySelector("#helped")?.checked; const tools = this.#el.querySelector("input[name='usingTools']:checked")?.value !== "0"; const bonuses = (specChecked ? 1 : 0) + (helped ? 1 : 0) + (tools ? 1 : 0); return (abilVal + sc + skillPool + bonuses) || 0; } 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 parseInt(sel?.value, 10) || 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 ─────────────────────────────────────────────────────────────── #displaySpecialties() { for (const el of this.#el.querySelectorAll("[data-spec-skill]")) { el.style.display = "inline"; } } #calculateBonusCount() { let b = 0; if (this.#el.querySelector("#helped")?.checked) b += 1; b += parseInt(this.#el.querySelector("#group")?.value, 10) || 0; b += parseInt(this.#getSelfCtrl()?.value, 10) || 0; const tools = this.#el.querySelector("input[name='usingTools']:checked"); if (tools && tools.value !== "0") b += 1; const human = this.#el.querySelector("#human-totem"); if (human?.checked) b += parseInt(this.#actor?.system?.adaptation?.totems?.human?.value, 10) || 0; const adapted = this.#el.querySelector("#adapted-totem"); if (adapted?.checked) b += parseInt(this.#actor?.system?.adaptation?.totems?.adapted?.value, 10) || 0; if (this.hasSpecialtySelected()) b += 1; return b; } #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 #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 }); } 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() }); this.close(); } }