import { MournbladeUtility } from "../mournblade-utility.js" export default class MournbladeInvocationDemonDialog { /** * Normalize a string for fuzzy matching: lowercase, remove diacritics, * replace œ/æ ligatures, collapse whitespace and punctuation. */ static _normalize(str) { return (str ?? "") .toLowerCase() .replace(/œ/g, "oe") .replace(/æ/g, "ae") .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") // strip combining diacritics .replace(/[^a-z0-9]+/g, " ") // replace any non-alnum with space .trim() } /** * Returns the first competence item whose name contains all of the given keywords. * Keywords are normalized before comparison. */ static _findCompetence(actor, ...keywords) { const normKeys = keywords.map(k => MournbladeInvocationDemonDialog._normalize(k)) return actor.items.find(item => { if (item.type !== "competence") return false const norm = MournbladeInvocationDemonDialog._normalize(item.name) return normKeys.every(k => norm.includes(k)) }) ?? null } /** * Returns true if the actor has a capacite or don whose name contains all given keywords. */ static _hasCapacite(actor, ...keywords) { const normKeys = keywords.map(k => MournbladeInvocationDemonDialog._normalize(k)) return actor.items.some(item => { if (item.type !== "capacite" && item.type !== "don") return false const norm = MournbladeInvocationDemonDialog._normalize(item.name) return normKeys.every(k => norm.includes(k)) }) } static async create(actor, rollData) { const ameDisponible = Math.max(0, (actor.system.ame.currentmax - actor.system.ame.value)) // Robust skill detection: partial keyword matching, diacritic-insensitive const coercitionComp = MournbladeInvocationDemonDialog._findCompetence(actor, "coercition") const hautParlerComp = MournbladeInvocationDemonDialog._findCompetence(actor, "haut", "parler") const loiChaosComp = MournbladeInvocationDemonDialog._findCompetence(actor, "loi", "chaos") // Check prerequisites — robust capacite/rune detection const isChaotique = actor.system.balance.chaos > actor.system.balance.loi const hasOeilSorcier = MournbladeInvocationDemonDialog._hasCapacite(actor, "oeil", "sorcier") const hasRuneChaos = actor.items.some(i => { if (i.type !== "rune") return false return MournbladeInvocationDemonDialog._normalize(i.name).includes("chaos") }) const prerequisOk = isChaotique && hasOeilSorcier && hasRuneChaos // Auto-detect chaos link bonuses const aspectGe8 = (actor.system.balance.aspect ?? 0) >= 8 // hasPacte: true if actor has a Pacte item whose associated deity matches a demon // (simplified: just expose it as a selectable option — can't auto-detect) const hasPacte = false const context = { ...rollData, img: actor.img, name: actor.name, ameDisponible, modOptions: Array.from({ length: 21 }, (_, i) => i - 10), coercitionNiveau: coercitionComp ? coercitionComp.system.niveau : null, hautParlerNiveau: hautParlerComp ? hautParlerComp.system.niveau : null, loiChaosNiveau: loiChaosComp ? loiChaosComp.system.niveau : null, isChaotique, hasOeilSorcier, hasRuneChaos, prerequisOk, aspectGe8, hasPacte, } const content = await foundry.applications.handlebars.renderTemplate( "systems/fvtt-mournblade/templates/dialog-invocation-demon.hbs", context ) Hooks.once("renderDialogV2", (_app, html) => { const form = html.querySelector ? html : html[0] MournbladeInvocationDemonDialog._attachListeners(form, ameDisponible, prerequisOk) }) return foundry.applications.api.DialogV2.wait({ window: { title: "Invoquer un Démon", icon: "fa-solid fa-skull" }, classes: ["mournblade-roll-dialog"], position: { width: 560 }, modal: false, content, buttons: [ { action: "invoquer", label: "Invoquer", icon: "fa-solid fa-skull", default: true, callback: async (event, button, dialog) => { MournbladeInvocationDemonDialog._updateRollData(rollData, button.form.elements, actor, { coercitionComp, hautParlerComp, loiChaosComp }) await MournbladeUtility.rollInvocationDemon(rollData) } }, ], rejectClose: false, }) } static _calculateSeuil(html) { const get = (name) => parseInt(html.querySelector(`[name="${name}"]`)?.value ?? 0) || 0 const seuil = get("seuil_nature") + get("seuil_traits") + get("seuil_augmentation") + get("seuil_service") + get("seuil_duree") + get("seuil_marche") + get("seuil_chaos") + get("seuil_sacrifice") return Math.max(1, seuil) } static _attachListeners(html, ameDisponible, prerequisOk = true) { const invoquerBtn = html.querySelector('button[data-action="invoquer"]') if (invoquerBtn) invoquerBtn.disabled = !prerequisOk const coutEl = html.querySelector('#invoc-demon-cout') const totalEl = html.querySelector('#invoc-demon-seuil-total') const hiddenEl = html.querySelector('#invoc-demon-seuil-hidden') const warnEl = html.querySelector('#invoc-demon-ame-warn') const criteriaNames = [ "seuil_nature", "seuil_traits", "seuil_augmentation", "seuil_service", "seuil_duree", "seuil_marche", "seuil_chaos", "seuil_sacrifice" ] const recalculate = () => { const seuil = MournbladeInvocationDemonDialog._calculateSeuil(html) if (totalEl) totalEl.textContent = seuil if (hiddenEl) hiddenEl.value = seuil if (coutEl) coutEl.textContent = seuil if (warnEl) warnEl.style.display = seuil > ameDisponible ? "" : "none" } for (const name of criteriaNames) { const el = html.querySelector(`[name="${name}"]`) if (el) el.addEventListener('change', recalculate) if (el && el.type === 'number') el.addEventListener('input', recalculate) } recalculate() } static _updateRollData(rollData, formElements, actor, { coercitionComp }) { const seuil = parseInt(formElements['seuil']?.value ?? 5) const modificateur = parseInt(formElements['modificateur']?.value ?? 0) rollData.invocationSeuil = seuil rollData.invocationSoulCost = seuil rollData.difficulte = seuil rollData.modificateur = modificateur rollData.competence = coercitionComp ?? null } }