import { MournbladeUtility } from "../mournblade-utility.js" export default class MournbladeInvocationDialog { /** * 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, "") .replace(/[^a-z0-9]+/g, " ") .trim() } /** * Returns the first competence item whose name contains all of the given keywords. */ static _findCompetence(actor, ...keywords) { const normKeys = keywords.map(k => MournbladeInvocationDialog._normalize(k)) return actor.items.find(item => { if (item.type !== "competence") return false const norm = MournbladeInvocationDialog._normalize(item.name) return normKeys.every(k => norm.includes(k)) }) ?? null } static async create(actor, rollData) { const ameDisponible = Math.max(0, (actor.system.ame.currentmax - actor.system.ame.value)) const maxExtra = Math.max(0, ameDisponible - 15) // Detect elemental pacte bonus — the bonus is always available, // but we display it dynamically based on chosen element. // We pass the pactes to let JS listeners detect the match. const pactes = actor.getPactes().map(p => ({ name: p.name, allegeance: (p.system.allegeance || "").toLowerCase() })) // Robust skill detection: partial keyword matching, diacritic-insensitive const hautParlerComp = MournbladeInvocationDialog._findCompetence(actor, "haut", "parler") const seigneursElemComp = MournbladeInvocationDialog._findCompetence(actor, "seigneurs", "elementaires") const context = { ...rollData, img: actor.img, name: actor.name, ameDisponible, ameExtraOptions: Array.from({ length: maxExtra + 1 }, (_, i) => i), modOptions: Array.from({ length: 21 }, (_, i) => i - 10), hautParlerNiveau: hautParlerComp ? hautParlerComp.system.niveau : null, seigneursElemNiveau: seigneursElemComp ? seigneursElemComp.system.niveau : null, bonusPacte: false, pactes, } const content = await foundry.applications.handlebars.renderTemplate( "systems/fvtt-mournblade/templates/dialog-invocation-elementaire.hbs", context ) Hooks.once("renderDialogV2", (_app, html) => { const form = html.querySelector ? html : html[0] MournbladeInvocationDialog._attachListeners(form, pactes) }) return foundry.applications.api.DialogV2.wait({ window: { title: "Invoquer un Élémentaire", icon: "fa-solid fa-wind" }, classes: ["mournblade-roll-dialog"], position: { width: 480 }, modal: false, content, buttons: [ { action: "invoquer", label: "Invoquer", icon: "fa-solid fa-wind", default: true, callback: (event, button, dialog) => { MournbladeInvocationDialog._updateRollData(rollData, button.form.elements, actor, pactes) MournbladeUtility.rollInvocationElementaire(rollData) } }, ], rejectClose: false, }) } static _elementKeywords = { air: ["air", "vent", "sylphe", "seigneur de l'air"], terre: ["terre", "gnome", "seigneur de la terre"], feu: ["feu", "flamme", "salamandre", "seigneur du feu"], eau: ["eau", "ondine", "seigneur de l'eau"], } static _hasPacteBonus(element, pactes) { const keywords = MournbladeInvocationDialog._elementKeywords[element] || [] return pactes.some(p => keywords.some(kw => p.allegeance.includes(kw))) } static _attachListeners(html, pactes) { const tierSeuils = { mineur: 15, median: 20, majeur: 25 } const tierTemps = { mineur: "1 tour", median: "1 minute", majeur: "1 heure" } const recalculate = () => { const element = html.querySelector('[name="element"]')?.value ?? "air" const tier = html.querySelector('[name="tier"]')?.value ?? "mineur" const ameExtra = parseInt(html.querySelector('[name="ameExtra"]')?.value ?? 0) const seuilBase = tierSeuils[tier] ?? 15 const seuil = seuilBase + ameExtra const hasPacte = MournbladeInvocationDialog._hasPacteBonus(element, pactes) const seuilEl = html.querySelector('#invoc-seuil') const coutEl = html.querySelector('#invoc-cout') const tempsEl = html.querySelector('#invoc-temps') const pacteBanner = html.querySelector('.invoc-bonus-pacte') if (seuilEl) seuilEl.textContent = seuil + (hasPacte ? " (-5 avec bonus Pacte)" : "") if (coutEl) coutEl.textContent = seuil if (tempsEl) tempsEl.textContent = tierTemps[tier] ?? "1 tour" if (pacteBanner) pacteBanner.style.display = hasPacte ? "" : "none" } html.querySelectorAll('[name="element"], [name="tier"], [name="ameExtra"]').forEach(el => { el.addEventListener('change', recalculate) }) recalculate() } static _updateRollData(rollData, formElements, actor, pactes) { const element = formElements['element']?.value ?? "air" const tier = formElements['tier']?.value ?? "mineur" const ameExtra = parseInt(formElements['ameExtra']?.value ?? 0) const modificateur = parseInt(formElements['modificateur']?.value ?? 0) const tierSeuils = { mineur: 15, median: 20, majeur: 25 } const seuilBase = tierSeuils[tier] ?? 15 const seuil = seuilBase + ameExtra rollData.invocationElement = element rollData.invocationTier = tier rollData.invocationSeuil = seuil rollData.invocationAmeExtra = ameExtra rollData.invocationSoulCost = seuil rollData.difficulte = seuil rollData.modificateur = modificateur rollData.bonusPacte = MournbladeInvocationDialog._hasPacteBonus(element, pactes) ? 5 : 0 } }