import { VermineUtils } from "../roll.mjs" const { HandlebarsApplicationMixin } = foundry.applications.api /** * Dialogue de jet simplifié pour les PNJ. * * Un PNJ n'a ni caractéristiques ni compétences : toutes ses actions sont * résolues par une valeur unique dérivée de son Profil (Menace / Expérience / * Rôle), exposée dans system.computed. Ce dialogue fixe le pool selon le type * de jet (Action, Spécialité, Attaque ou Réaction) et n'offre que la difficulté, * le handicap et les bonus simples. * * Utilisé par : * - la fiche PNJ (boutons de jet rapide Action / Spécialité / Réaction) ; * - la défense des PNJ dans les échanges de coups (mode verrouillé). */ export default class NpcRollDialog 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: NpcRollDialog.#onRoll, cancel: NpcRollDialog.#onCancel } } static PARTS = { main: { template: "systems/vermine2047/templates/dialogs/npc-roll-dialog.hbs" } } static async create(data = {}) { const actor = data.actor instanceof Actor ? data.actor : await game.actors.get(data.actorId) if (!actor) { ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected")) return null } return new NpcRollDialog({ actor, rolltype: data.rolltype ?? "action", locks: data.locks ?? null, defense: data.defense ?? null }) } constructor(options = {}) { super(options) this.#actor = options.actor this.rolltype = options.rolltype ?? "action" this.locks = options.locks ?? null this.defense = options.defense ?? null } // ── Getters ────────────────────────────────────────────────────────── get computed() { return this.#actor.system?.computed || {} } /** Pool de dés selon le type de jet. */ get pool() { const c = this.computed switch (this.rolltype) { case "specialty": return c.specialties || 0 case "attack": return c.attack || 0 case "reaction": return c.reaction || 0 default: return c.action || 3 } } /** Réussites automatiques (bonus de Réaction du rôle). */ get freeSuccesses() { return this.rolltype === "reaction" ? (this.computed.reactionBonus || 0) : 0 } /** Relances : utilisables uniquement sur les attaques ou les Spécialités. */ get rerolls() { if (this.rolltype !== "specialty" && this.rolltype !== "attack") return 0 return this.computed.rerolls || 0 } get label() { const keys = { action: "ADVERSITY.action", specialty: "ADVERSITY.specialties", attack: "ADVERSITY.attack", reaction: "ADVERSITY.reaction" } return game.i18n.localize(keys[this.rolltype] || "VERMINE.roll") } // ── Rendu ──────────────────────────────────────────────────────────── async _prepareContext() { const difficultyOptions = [] for (let d = 3; d <= 10; d++) { difficultyOptions.push({ difficulty: d, label: String(d) }) } let lockedDifficulty = null let defaultDifficulty = 7 if (this.locks?.difficulty !== undefined && this.locks?.difficulty !== null) { lockedDifficulty = parseInt(this.locks.difficulty, 10) defaultDifficulty = lockedDifficulty } return { actor: this.#actor, system: this.#actor.system, config: CONFIG.VERMINE, rollLabel: this.label, rolltype: this.rolltype, pool: this.pool, freeSuccesses: this.freeSuccesses, rerolls: this.rerolls, isDefense: Boolean(this.defense), lockedDifficulty, difficultyOptions, defaultDifficulty, speakerId: this.#actor.id, availableItems: this.#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)) } this.element.querySelector("#handicap")?.addEventListener("change", () => this.#updateUI()) this.#updateUI() } get #el() { return this.element } #getHandicap() { const sel = this.#el.querySelector("#handicap") return Math.max(0, (parseInt(sel?.value, 10) || 1) - 1) } #getDifficulty() { const sel = this.#el.querySelector("#difficulty") return parseInt(sel?.value, 10) || 7 } #getHelped() { return this.#el.querySelector("#helped")?.checked ? 1 : 0 } #getGroup() { return parseInt(this.#el.querySelector("#group")?.value, 10) || 0 } getPoolBreakdown() { const help = this.#getHelped() const group = this.#getGroup() return { pool: this.pool, help, group, total: this.pool + help + group } } #updateUI() { const b = this.getPoolBreakdown() const totalEl = this.#el.querySelector("#dice-pool-total") if (totalEl) totalEl.textContent = `${b.total}D` const bonusEl = this.#el.querySelector("#total-bonus") if (bonusEl) bonusEl.textContent = b.help + b.group const freeEl = this.#el.querySelector("#free-successes") if (freeEl) freeEl.textContent = String(this.freeSuccesses) } #onInputChange() { this.#updateUI() } // ── Actions ────────────────────────────────────────────────────────── static async #onCancel(event, target) { this.close() } static async #onRoll(event, target) { const label = this.defense ? (this.defense.type === "parade" ? game.i18n.localize("VERMINE.defense_parade") : game.i18n.localize("VERMINE.defense_esquive")) : this.label await VermineUtils.roll({ actor: this.#actor, NoD: this.getPoolBreakdown().total, Reroll: this.rerolls, difficulty: this.#getDifficulty(), handicap: this.#getHandicap(), rollLabel: label, bonusSuccesses: this.freeSuccesses, poolBreakdown: this.getPoolBreakdown(), messageFlags: this.defense ? { "vermine-defense": this.defense } : null }) this.close() } }