175 lines
6.4 KiB
JavaScript
175 lines
6.4 KiB
JavaScript
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
|
|
}
|
|
}
|