Esprit de la Loi + Automaton
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
import { MournbladeUtility } from "../mournblade-utility.js"
|
||||
|
||||
export default class MournbladeEnchantementDialog {
|
||||
|
||||
static _normalize(str) {
|
||||
return (str ?? "")
|
||||
.toLowerCase()
|
||||
.replace(/œ/g, "oe")
|
||||
.replace(/æ/g, "ae")
|
||||
.normalize("NFD")
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/[^a-z0-9]+/g, " ")
|
||||
.trim()
|
||||
}
|
||||
|
||||
static _findCompetence(actor, ...keywords) {
|
||||
const normKeys = keywords.map(k => MournbladeEnchantementDialog._normalize(k))
|
||||
return actor.items.find(item => {
|
||||
if (item.type !== "competence") return false
|
||||
const norm = MournbladeEnchantementDialog._normalize(item.name)
|
||||
return normKeys.every(k => norm.includes(k))
|
||||
}) ?? null
|
||||
}
|
||||
|
||||
static async create(actor, item) {
|
||||
const normalize = MournbladeEnchantementDialog._normalize.bind(MournbladeEnchantementDialog)
|
||||
const findComp = (...kw) => MournbladeEnchantementDialog._findCompetence(actor, ...kw)
|
||||
|
||||
const ameDisponible = Math.max(0, (actor.system.ame.currentmax - actor.system.ame.value))
|
||||
const aspect = actor.system.balance.aspect ?? 0
|
||||
|
||||
// Skill lookups
|
||||
const savoirRunesComp = findComp("rune")
|
||||
const hautParlerComp = findComp("haut", "parler")
|
||||
const artisanatComp = findComp("savoir", "artisanat")
|
||||
const claAttr = actor.system.attributs?.clairvoyance
|
||||
|
||||
// Prerequisite: Rune de la Loi in inventory
|
||||
const hasRuneLoi = actor.items.some(i => {
|
||||
if (i.type !== "rune") return false
|
||||
return normalize(i.name).includes("loi")
|
||||
})
|
||||
|
||||
const savoirRunesNiveau = savoirRunesComp ? (savoirRunesComp.system.niveau ?? 0) : null
|
||||
const hautParlerNiveau = hautParlerComp ? (hautParlerComp.system.niveau ?? 0) : null
|
||||
const artisanatNiveau = artisanatComp ? (artisanatComp.system.niveau ?? 0) : null
|
||||
const claValeur = claAttr ? (claAttr.value ?? 0) : 0
|
||||
|
||||
// Limit: CLA + Savoir:Runes is capped by min(Haut-Parler, Artisanat) if those skills exist
|
||||
const limiteur = (hautParlerNiveau !== null && artisanatNiveau !== null)
|
||||
? Math.min(hautParlerNiveau, artisanatNiveau)
|
||||
: (hautParlerNiveau ?? artisanatNiveau ?? null)
|
||||
|
||||
const context = {
|
||||
actorImg: actor.img,
|
||||
actorName: actor.name,
|
||||
itemImg: item.img,
|
||||
itemName: item.name,
|
||||
itemType: item.type,
|
||||
itemId: item.id,
|
||||
ameDisponible,
|
||||
aspect,
|
||||
hasRuneLoi,
|
||||
savoirRunesNiveau,
|
||||
hautParlerNiveau,
|
||||
artisanatNiveau,
|
||||
claValeur,
|
||||
limiteur,
|
||||
modOptions: Array.from({ length: 21 }, (_, i) => i - 10),
|
||||
enchantementActif: item.system.enchantementLoi?.actif ?? false,
|
||||
enchantementBonus: item.system.enchantementLoi?.bonus ?? 0,
|
||||
enchantementAntiChaos: item.system.enchantementLoi?.antiChaos ?? false,
|
||||
}
|
||||
|
||||
const prerequisOk = hasRuneLoi
|
||||
|
||||
const content = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-mournblade/templates/dialog-enchantement.hbs",
|
||||
context
|
||||
)
|
||||
|
||||
Hooks.once("renderDialogV2", (_app, html) => {
|
||||
const form = html.querySelector ? html : html[0]
|
||||
MournbladeEnchantementDialog._attachListeners(form, ameDisponible, claValeur, savoirRunesNiveau, limiteur, prerequisOk)
|
||||
})
|
||||
|
||||
return foundry.applications.api.DialogV2.wait({
|
||||
window: { title: `Enchanter : ${item.name}`, icon: "fa-solid fa-star" },
|
||||
classes: ["mournblade-roll-dialog"],
|
||||
position: { width: 520 },
|
||||
modal: false,
|
||||
content,
|
||||
buttons: [
|
||||
{
|
||||
action: "enchanter",
|
||||
label: "Enchanter",
|
||||
icon: "fa-solid fa-star",
|
||||
default: true,
|
||||
callback: (event, button, dialog) => {
|
||||
const elems = button.form.elements
|
||||
const ptsAme = parseInt(elems["ptsAme"]?.value ?? 5) || 5
|
||||
const antiChaos = elems["antiChaos"]?.value === "true"
|
||||
const modificateur = parseInt(elems["modificateur"]?.value ?? 0) || 0
|
||||
MournbladeUtility.rollEnchantement({
|
||||
actor,
|
||||
item,
|
||||
ptsAme,
|
||||
antiChaos,
|
||||
modificateur,
|
||||
savoirRunesComp,
|
||||
hautParlerComp,
|
||||
artisanatComp,
|
||||
claValeur,
|
||||
limiteur,
|
||||
})
|
||||
}
|
||||
},
|
||||
],
|
||||
rejectClose: false,
|
||||
})
|
||||
}
|
||||
|
||||
static _attachListeners(html, ameDisponible, claValeur, savoirRunesNiveau, limiteur, prerequisOk = true) {
|
||||
const enchanterBtn = html.querySelector('button[data-action="enchanter"]')
|
||||
if (enchanterBtn) enchanterBtn.disabled = !prerequisOk
|
||||
|
||||
const diffEl = html.querySelector('#enchant-difficulte')
|
||||
const bonusEl = html.querySelector('#enchant-bonus-preview')
|
||||
const warnAmeEl = html.querySelector('#enchant-ame-warn')
|
||||
const warnLimitEl = html.querySelector('#enchant-limit-warn')
|
||||
const totalEl = html.querySelector('#enchant-total-dice')
|
||||
|
||||
const recalculate = () => {
|
||||
const ptsAme = parseInt(html.querySelector('[name="ptsAme"]')?.value ?? 5) || 0
|
||||
const difficulte = ptsAme
|
||||
const bonus = Math.floor(ptsAme / 5)
|
||||
const savoir = savoirRunesNiveau ?? 0
|
||||
const basePool = claValeur + savoir
|
||||
const effectivePool = limiteur !== null ? Math.min(basePool, limiteur) : basePool
|
||||
|
||||
if (diffEl) diffEl.textContent = difficulte
|
||||
if (bonusEl) bonusEl.textContent = `+${bonus}`
|
||||
if (totalEl) totalEl.textContent = effectivePool
|
||||
|
||||
if (warnAmeEl) warnAmeEl.style.display = ptsAme > ameDisponible ? "" : "none"
|
||||
if (warnLimitEl && limiteur !== null)
|
||||
warnLimitEl.style.display = basePool > limiteur ? "" : "none"
|
||||
}
|
||||
|
||||
const ptsAmeEl = html.querySelector('[name="ptsAme"]')
|
||||
if (ptsAmeEl) {
|
||||
ptsAmeEl.addEventListener('input', recalculate)
|
||||
ptsAmeEl.addEventListener('change', recalculate)
|
||||
}
|
||||
recalculate()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
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: (event, button, dialog) => {
|
||||
MournbladeInvocationDemonDialog._updateRollData(rollData, button.form.elements, actor, {
|
||||
coercitionComp, hautParlerComp, loiChaosComp
|
||||
})
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,33 @@ 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)
|
||||
@@ -14,8 +41,9 @@ export default class MournbladeInvocationDialog {
|
||||
allegeance: (p.system.allegeance || "").toLowerCase()
|
||||
}))
|
||||
|
||||
const hautParlerComp = actor.items.find(c => c.type == "competence" && c.name.toLowerCase() == "savoir : haut-parler")
|
||||
const seigneursElemComp = actor.items.find(c => c.type == "competence" && c.name.toLowerCase() == "savoir : seigneurs élémentaires")
|
||||
// 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,
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { MournbladeUtility } from "../mournblade-utility.js"
|
||||
|
||||
export default class MournbladeInvocationEspritDialog {
|
||||
|
||||
static _normalize(str) {
|
||||
return (str ?? "")
|
||||
.toLowerCase()
|
||||
.replace(/œ/g, "oe").replace(/æ/g, "ae")
|
||||
.normalize("NFD").replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/[^a-z0-9]+/g, " ").trim()
|
||||
}
|
||||
|
||||
static _findCompetence(actor, ...keywords) {
|
||||
const normKeys = keywords.map(k => MournbladeInvocationEspritDialog._normalize(k))
|
||||
return actor.items.find(item => {
|
||||
if (item.type !== "competence") return false
|
||||
const norm = MournbladeInvocationEspritDialog._normalize(item.name)
|
||||
return normKeys.every(k => norm.includes(k))
|
||||
}) ?? null
|
||||
}
|
||||
|
||||
/** Seuil and soul cost per puissance */
|
||||
static SEUILS = { mineur: 15, median: 20, majeur: 25 }
|
||||
|
||||
static async create(actor, rollData) {
|
||||
const ameDisponible = Math.max(0, actor.system.ame.currentmax - actor.system.ame.value)
|
||||
|
||||
const persuasionComp = MournbladeInvocationEspritDialog._findCompetence(actor, "persuasion")
|
||||
const hautParlerComp = MournbladeInvocationEspritDialog._findCompetence(actor, "haut", "parler")
|
||||
const loiChaosComp = MournbladeInvocationEspritDialog._findCompetence(actor, "loi", "chaos")
|
||||
|
||||
const isLoyal = actor.system.balance.loi > actor.system.balance.chaos
|
||||
const hasRuneLoi = actor.items.some(i =>
|
||||
i.type === "rune" && MournbladeInvocationEspritDialog._normalize(i.name).includes("loi")
|
||||
)
|
||||
const prerequisOk = isLoyal && hasRuneLoi
|
||||
|
||||
const automatonTypes = {
|
||||
combat: "Combat",
|
||||
voyage: "Voyage",
|
||||
perception: "Perception",
|
||||
restauration:"Restauration",
|
||||
reparateur: "Réparateur",
|
||||
}
|
||||
|
||||
const context = {
|
||||
...rollData,
|
||||
img: actor.img,
|
||||
name: actor.name,
|
||||
ameDisponible,
|
||||
treValeur: actor.system.attributs?.tre?.value ?? 0,
|
||||
persuasionNiveau: persuasionComp?.system?.niveau ?? null,
|
||||
hautParlerNiveau: hautParlerComp?.system?.niveau ?? null,
|
||||
loiChaosNiveau: loiChaosComp?.system?.niveau ?? null,
|
||||
isLoyal,
|
||||
hasRuneLoi,
|
||||
prerequisOk,
|
||||
automatonTypes,
|
||||
seuils: MournbladeInvocationEspritDialog.SEUILS,
|
||||
modOptions: Array.from({ length: 21 }, (_, i) => i - 10),
|
||||
}
|
||||
|
||||
const content = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-mournblade/templates/dialog-invocation-esprit.hbs",
|
||||
context
|
||||
)
|
||||
|
||||
Hooks.once("renderDialogV2", (_app, html) => {
|
||||
const form = html.querySelector ? html : html[0]
|
||||
MournbladeInvocationEspritDialog._attachListeners(form, ameDisponible, prerequisOk)
|
||||
})
|
||||
|
||||
return foundry.applications.api.DialogV2.wait({
|
||||
window: { title: "Invoquer un Esprit de la Loi", icon: "fa-solid fa-star" },
|
||||
classes: ["mournblade-roll-dialog"],
|
||||
position: { width: 520 },
|
||||
modal: false,
|
||||
content,
|
||||
buttons: [
|
||||
{
|
||||
action: "invoquer",
|
||||
label: "Invoquer",
|
||||
icon: "fa-solid fa-star",
|
||||
default: true,
|
||||
callback: (event, button, dialog) => {
|
||||
MournbladeInvocationEspritDialog._updateRollData(rollData, button.form.elements, actor, {
|
||||
persuasionComp, hautParlerComp, loiChaosComp
|
||||
})
|
||||
MournbladeUtility.rollInvocationEsprit(rollData)
|
||||
}
|
||||
},
|
||||
],
|
||||
rejectClose: false,
|
||||
})
|
||||
}
|
||||
|
||||
static _attachListeners(html, ameDisponible, prerequisOk = true) {
|
||||
const invoquerBtn = html.querySelector('button[data-action="invoquer"]')
|
||||
if (invoquerBtn) invoquerBtn.disabled = !prerequisOk
|
||||
|
||||
const puissanceEl = html.querySelector('[name="puissance"]')
|
||||
const seuilEl = html.querySelector('#esprit-seuil-total')
|
||||
const coutEl = html.querySelector('#esprit-cout-ame')
|
||||
const hiddenEl = html.querySelector('#esprit-seuil-hidden')
|
||||
const warnEl = html.querySelector('#esprit-ame-warn')
|
||||
const dureeEl = html.querySelector('#esprit-duree')
|
||||
|
||||
const DUREE = { mineur: "1 heure", median: "1 jour", majeur: "1 semaine" }
|
||||
|
||||
const recalculate = () => {
|
||||
const puissance = puissanceEl?.value ?? "mineur"
|
||||
const seuil = MournbladeInvocationEspritDialog.SEUILS[puissance] ?? 15
|
||||
if (seuilEl) seuilEl.textContent = seuil
|
||||
if (coutEl) coutEl.textContent = seuil
|
||||
if (hiddenEl) hiddenEl.value = seuil
|
||||
if (dureeEl) dureeEl.textContent = DUREE[puissance] ?? "1 heure"
|
||||
if (warnEl) warnEl.style.display = seuil > ameDisponible ? "" : "none"
|
||||
}
|
||||
|
||||
if (puissanceEl) puissanceEl.addEventListener("change", recalculate)
|
||||
recalculate()
|
||||
}
|
||||
|
||||
static _updateRollData(rollData, formElements, actor, { persuasionComp }) {
|
||||
const seuil = parseInt(formElements["seuil"]?.value ?? 15)
|
||||
const modificateur = parseInt(formElements["modificateur"]?.value ?? 0)
|
||||
const automatonType = formElements["automatonType"]?.value ?? "combat"
|
||||
const puissance = formElements["puissance"]?.value ?? "mineur"
|
||||
|
||||
rollData.invocationSeuil = seuil
|
||||
rollData.invocationSoulCost = seuil
|
||||
rollData.difficulte = seuil
|
||||
rollData.modificateur = modificateur
|
||||
rollData.competence = persuasionComp ?? null
|
||||
rollData.automatonType = automatonType
|
||||
rollData.automatonPuissance = puissance
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,10 @@ export default class MournbladeActorSheet extends HandlebarsApplicationMixin(fou
|
||||
preparePotion: MournbladeActorSheet.#onPreparePotion,
|
||||
invoquerElementaire: MournbladeActorSheet.#onInvoquerElementaire,
|
||||
bannirElementaire: MournbladeActorSheet.#onBannirElementaire,
|
||||
invoquerDemon: MournbladeActorSheet.#onInvoquerDemon,
|
||||
libererDemon: MournbladeActorSheet.#onLibererDemon,
|
||||
invoquerEsprit: MournbladeActorSheet.#onInvoquerEsprit,
|
||||
enchanter: MournbladeActorSheet.#onEnchanter,
|
||||
rollArmeOffensif: MournbladeActorSheet.#onRollArmeOffensif,
|
||||
rollArmeSpecial: MournbladeActorSheet.#onRollArmeSpecial,
|
||||
rollArmeDegats: MournbladeActorSheet.#onRollArmeDegats,
|
||||
@@ -110,6 +114,21 @@ export default class MournbladeActorSheet extends HandlebarsApplicationMixin(fou
|
||||
return context
|
||||
}
|
||||
|
||||
/** @override */
|
||||
_preSyncPartState(partId, newElement, priorElement, state) {
|
||||
super._preSyncPartState(partId, newElement, priorElement, state)
|
||||
// Save scrollable tab positions for deferred restoration in _onRender.
|
||||
// Tabs are hidden (display:none) at _syncPartState time, so scrollTop
|
||||
// assignments have no effect. We re-apply them after making tabs visible.
|
||||
const part = this.constructor.PARTS?.[partId]
|
||||
if (part?.scrollable) {
|
||||
this._pendingScrollRestores = part.scrollable.map(selector => {
|
||||
const el = selector ? priorElement.querySelector(selector) : priorElement
|
||||
return el ? { selector, scrollTop: el.scrollTop } : null
|
||||
}).filter(Boolean)
|
||||
}
|
||||
}
|
||||
|
||||
/** @override */
|
||||
_onRender(context, options) {
|
||||
super._onRender(context, options)
|
||||
@@ -136,22 +155,37 @@ export default class MournbladeActorSheet extends HandlebarsApplicationMixin(fou
|
||||
const nav = this.element.querySelector('nav.tabs[data-group]')
|
||||
if (nav) {
|
||||
const group = nav.dataset.group
|
||||
// Activate the current tab
|
||||
const activeTab = this.tabGroups[group] || "stats"
|
||||
|
||||
const switchTab = (tab) => {
|
||||
this.tabGroups[group] = tab
|
||||
nav.querySelectorAll('[data-tab]').forEach(link => {
|
||||
link.classList.toggle('active', link.dataset.tab === tab)
|
||||
})
|
||||
this.element.querySelectorAll('[data-group="' + group + '"][data-tab]').forEach(content => {
|
||||
content.classList.toggle('active', content.dataset.tab === tab)
|
||||
})
|
||||
}
|
||||
|
||||
// Set initial state (makes active tab visible)
|
||||
switchTab(activeTab)
|
||||
|
||||
// Restore scroll positions now that the active tab is visible
|
||||
if (this._pendingScrollRestores?.length) {
|
||||
for (const { selector, scrollTop } of this._pendingScrollRestores) {
|
||||
const el = selector ? this.element.querySelector(selector) : this.element
|
||||
if (el) el.scrollTop = scrollTop
|
||||
}
|
||||
this._pendingScrollRestores = null
|
||||
}
|
||||
|
||||
// Tab clicks: DOM-only, no re-render (preserves scroll positions)
|
||||
nav.querySelectorAll('[data-tab]').forEach(link => {
|
||||
const tab = link.dataset.tab
|
||||
link.classList.toggle('active', tab === activeTab)
|
||||
link.addEventListener('click', (event) => {
|
||||
event.preventDefault()
|
||||
this.tabGroups[group] = tab
|
||||
this.render()
|
||||
switchTab(link.dataset.tab)
|
||||
})
|
||||
})
|
||||
|
||||
// Show/hide tab content
|
||||
this.element.querySelectorAll('[data-group="' + group + '"][data-tab]').forEach(content => {
|
||||
content.classList.toggle('active', content.dataset.tab === activeTab)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,6 +417,40 @@ export default class MournbladeActorSheet extends HandlebarsApplicationMixin(fou
|
||||
await this.document.bannirElementaire(invocIndex)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle invoking a demon
|
||||
*/
|
||||
static async #onInvoquerDemon(event, target) {
|
||||
event.preventDefault()
|
||||
await this.document.invoquerDemon()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle releasing a demon invocation
|
||||
*/
|
||||
static async #onLibererDemon(event, target) {
|
||||
event.preventDefault()
|
||||
const invocIndex = parseInt(target.dataset.invocIndex ?? "0")
|
||||
await this.document.libererDemon(invocIndex)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle invoking a Law Spirit into an Automaton
|
||||
*/
|
||||
static async #onInvoquerEsprit(event, target) {
|
||||
event.preventDefault()
|
||||
await this.document.invoquerEspritLoi()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle enchanting an item with Loi power
|
||||
*/
|
||||
static async #onEnchanter(event, target) {
|
||||
event.preventDefault()
|
||||
const itemId = target.dataset.itemId
|
||||
await this.document.enchanter(itemId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle rolling an arme offensif
|
||||
* @param {Event} event - The triggering event
|
||||
|
||||
@@ -32,6 +32,7 @@ export default class MournbladeItemSheet extends HandlebarsApplicationMixin(foun
|
||||
actions: {
|
||||
editImage: MournbladeItemSheet.#onEditImage,
|
||||
postItem: MournbladeItemSheet.#onPostItem,
|
||||
enchanter: MournbladeItemSheet.#onEnchanter,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -43,17 +44,24 @@ export default class MournbladeItemSheet extends HandlebarsApplicationMixin(foun
|
||||
|
||||
/** @override */
|
||||
async _prepareContext() {
|
||||
const item = this.document
|
||||
const enchantableTypes = ["arme", "equipement", "protection", "bouclier"]
|
||||
const actorIsLoyal = item.actor?.getAlignement?.() === "loyal"
|
||||
const alreadyEnchanted = enchantableTypes.includes(item.type) && (item.system.enchantementLoi?.actif ?? false)
|
||||
const canEnchant = enchantableTypes.includes(item.type) && !!item.actor && actorIsLoyal && !alreadyEnchanted
|
||||
const context = {
|
||||
fields: this.document.schema.fields,
|
||||
systemFields: this.document.system.schema.fields,
|
||||
item: this.document,
|
||||
system: this.document.system,
|
||||
source: this.document.toObject(),
|
||||
enrichedDescription: await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.document.system.description, { async: true }),
|
||||
fields: item.schema.fields,
|
||||
systemFields: item.system.schema.fields,
|
||||
item,
|
||||
system: item.system,
|
||||
source: item.toObject(),
|
||||
enrichedDescription: await foundry.applications.ux.TextEditor.implementation.enrichHTML(item.system.description, { async: true }),
|
||||
isEditMode: true,
|
||||
isEditable: this.isEditable,
|
||||
isGM: game.user.isGM,
|
||||
config: game.system.mournblade.config,
|
||||
canEnchant,
|
||||
enchantementActif: alreadyEnchanted,
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -96,6 +104,19 @@ export default class MournbladeItemSheet extends HandlebarsApplicationMixin(foun
|
||||
|
||||
// #region Actions
|
||||
|
||||
/**
|
||||
* Handle enchanting this item with Loi power
|
||||
*/
|
||||
static async #onEnchanter(event) {
|
||||
event.preventDefault()
|
||||
const item = this.document
|
||||
if (!item.actor) {
|
||||
ui.notifications.warn("Cet objet doit être sur un personnage pour être enchanté.")
|
||||
return
|
||||
}
|
||||
await item.actor.enchanter(item.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle editing the item image
|
||||
* @param {Event} event - The triggering event
|
||||
@@ -137,7 +158,8 @@ export default class MournbladeItemSheet extends HandlebarsApplicationMixin(foun
|
||||
protection: "Protection", equipement: "Équipement", heritage: "Héritage",
|
||||
metier: "Métier", capacite: "Capacité", tendance: "Tendance",
|
||||
traitchaotique: "Trait Chaotique", traitespece: "Trait d'Espèce",
|
||||
origine: "Origine", modifier: "Modificateur", monnaie: "Monnaie"
|
||||
origine: "Origine", modifier: "Modificateur", monnaie: "Monnaie",
|
||||
potion: "Potion"
|
||||
}
|
||||
chatData.typeLabel = typeLabels[chatData.type] ?? chatData.type
|
||||
|
||||
@@ -148,10 +170,30 @@ export default class MournbladeItemSheet extends HandlebarsApplicationMixin(foun
|
||||
pacte: "fa-scroll", protection: "fa-shield", equipement: "fa-box",
|
||||
heritage: "fa-dna", metier: "fa-hammer", capacite: "fa-bolt",
|
||||
tendance: "fa-yin-yang", traitchaotique: "fa-skull", traitespece: "fa-paw",
|
||||
origine: "fa-compass", modifier: "fa-sliders", monnaie: "fa-coins"
|
||||
origine: "fa-compass", modifier: "fa-sliders", monnaie: "fa-coins",
|
||||
potion: "fa-flask"
|
||||
}
|
||||
chatData.typeIcon = typeIcons[chatData.type] ?? "fa-cube"
|
||||
|
||||
// Potion: add localized labels for statut and forme
|
||||
if (chatData.type === "potion") {
|
||||
const statutLabels = {
|
||||
inconnue: game.i18n.localize("MNBL.potionInconnue"),
|
||||
efficace: game.i18n.localize("MNBL.potionEfficace"),
|
||||
heroique: game.i18n.localize("MNBL.potionHeroique"),
|
||||
inefficace: game.i18n.localize("MNBL.potionInefficace"),
|
||||
poison: game.i18n.localize("MNBL.potionPoison"),
|
||||
}
|
||||
const formeLabels = {
|
||||
liquide: game.i18n.localize("MNBL.potionLiquide"),
|
||||
onguent: game.i18n.localize("MNBL.potionOnguent"),
|
||||
cachets: game.i18n.localize("MNBL.potionCachets"),
|
||||
pilules: game.i18n.localize("MNBL.potionPilules"),
|
||||
}
|
||||
chatData.system.statutLabel = statutLabels[chatData.system.statut] ?? chatData.system.statut
|
||||
chatData.system.formeLabel = formeLabels[chatData.system.forme] ?? chatData.system.forme
|
||||
}
|
||||
|
||||
const html = await foundry.applications.handlebars.renderTemplate('systems/fvtt-mournblade/templates/post-item.hbs', chatData)
|
||||
ChatMessage.create({ user: game.user.id, content: html })
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export default class MournbladeCreatureSheet extends MournbladeActorSheet {
|
||||
static PARTS = {
|
||||
sheet: {
|
||||
template: "systems/fvtt-mournblade/templates/creature-sheet.hbs",
|
||||
scrollable: [".tab.competences", ".tab.equipement", ".tab.biodata"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ export default class MournbladePersonnageSheet extends MournbladeActorSheet {
|
||||
static PARTS = {
|
||||
sheet: {
|
||||
template: "systems/fvtt-mournblade/templates/actor-sheet.hbs",
|
||||
scrollable: [".tab.principal", ".tab.competences", ".tab.dons", ".tab.equipement"],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -35,6 +36,8 @@ export default class MournbladePersonnageSheet extends MournbladeActorSheet {
|
||||
context.dons = foundry.utils.duplicate(actor.getDons())
|
||||
context.pactes = foundry.utils.duplicate(actor.getPactes())
|
||||
context.alignement = actor.getAlignement()
|
||||
context.isChaotique = context.alignement === "chaotique"
|
||||
context.isLoyal = context.alignement === "loyal"
|
||||
context.aspect = actor.getAspect()
|
||||
context.marge = actor.getMarge()
|
||||
context.tendances = foundry.utils.duplicate(actor.getTendances())
|
||||
@@ -46,6 +49,7 @@ export default class MournbladePersonnageSheet extends MournbladeActorSheet {
|
||||
context.metier = foundry.utils.duplicate(actor.getMetier() || {})
|
||||
context.combat = actor.getCombatValues()
|
||||
context.equipements = foundry.utils.duplicate(actor.getEquipments())
|
||||
context.potions = foundry.utils.duplicate(actor.getPotions())
|
||||
context.modifiers = foundry.utils.duplicate(actor.getModifiers())
|
||||
context.monnaies = foundry.utils.duplicate(actor.getMonnaies())
|
||||
context.runeEffects = foundry.utils.duplicate(actor.getRuneEffects())
|
||||
|
||||
@@ -21,7 +21,12 @@ export default class ArmeDataModel extends foundry.abstract.TypeDataModel {
|
||||
tr: new fields.NumberField({ initial: 0, integer: true }),
|
||||
rarete: new fields.NumberField({ initial: 0, integer: true }),
|
||||
prix: new fields.NumberField({ initial: 0, integer: true }),
|
||||
equipped: new fields.BooleanField({ initial: false })
|
||||
equipped: new fields.BooleanField({ initial: false }),
|
||||
enchantementLoi: new fields.SchemaField({
|
||||
actif: new fields.BooleanField({ initial: false }),
|
||||
bonus: new fields.NumberField({ initial: 0, integer: true }),
|
||||
antiChaos: new fields.BooleanField({ initial: false }),
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,12 @@ export default class BouclierDataModel extends foundry.abstract.TypeDataModel {
|
||||
nonletaux: new fields.BooleanField({ initial: false }),
|
||||
rarete: new fields.NumberField({ initial: 0, integer: true }),
|
||||
prix: new fields.NumberField({ initial: 0, integer: true }),
|
||||
equipped: new fields.BooleanField({ initial: false })
|
||||
equipped: new fields.BooleanField({ initial: false }),
|
||||
enchantementLoi: new fields.SchemaField({
|
||||
actif: new fields.BooleanField({ initial: false }),
|
||||
bonus: new fields.NumberField({ initial: 0, integer: true }),
|
||||
antiChaos: new fields.BooleanField({ initial: false }),
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,11 @@ export default class CreatureDataModel extends foundry.abstract.TypeDataModel {
|
||||
alignement: new fields.StringField({ initial: "" }),
|
||||
creatureType: new fields.StringField({ initial: "creature" }),
|
||||
elementType: new fields.StringField({ initial: "" }),
|
||||
demonType: new fields.StringField({ initial: "" }),
|
||||
demonPuissance: new fields.StringField({ initial: "" }),
|
||||
automatonType: new fields.StringField({ initial: "" }),
|
||||
automatonPuissance: new fields.StringField({ initial: "" }),
|
||||
automatonVoyageType: new fields.StringField({ initial: "" }),
|
||||
poids: new fields.StringField({ initial: "" }),
|
||||
taille: new fields.StringField({ initial: "" }),
|
||||
cheveux: new fields.StringField({ initial: "" }),
|
||||
|
||||
@@ -7,7 +7,12 @@ export default class EquipementDataModel extends foundry.abstract.TypeDataModel
|
||||
return {
|
||||
description: new fields.HTMLField({ initial: "" }),
|
||||
rarete: new fields.NumberField({ initial: 0, integer: true }),
|
||||
prix: new fields.NumberField({ initial: 0, integer: true })
|
||||
prix: new fields.NumberField({ initial: 0, integer: true }),
|
||||
enchantementLoi: new fields.SchemaField({
|
||||
actif: new fields.BooleanField({ initial: false }),
|
||||
bonus: new fields.NumberField({ initial: 0, integer: true }),
|
||||
antiChaos: new fields.BooleanField({ initial: false }),
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -81,6 +81,7 @@ export default class PersonnageDataModel extends foundry.abstract.TypeDataModel
|
||||
traumatismes: new fields.StringField({ initial: "" })
|
||||
}),
|
||||
invocationsElementaires: new fields.ArrayField(new fields.ObjectField(), { initial: [] }),
|
||||
invocationsDemons: new fields.ArrayField(new fields.ObjectField(), { initial: [] }),
|
||||
combat: new fields.SchemaField({
|
||||
initbonus: new fields.NumberField({ initial: 0, integer: true }),
|
||||
vitessebonus: new fields.NumberField({ initial: 0, integer: true }),
|
||||
|
||||
@@ -11,7 +11,12 @@ export default class ProtectionDataModel extends foundry.abstract.TypeDataModel
|
||||
degats: new fields.StringField({ initial: "" }),
|
||||
rarete: new fields.NumberField({ initial: 0, integer: true }),
|
||||
prix: new fields.NumberField({ initial: 0, integer: true }),
|
||||
equipped: new fields.BooleanField({ initial: false })
|
||||
equipped: new fields.BooleanField({ initial: false }),
|
||||
enchantementLoi: new fields.SchemaField({
|
||||
actif: new fields.BooleanField({ initial: false }),
|
||||
bonus: new fields.NumberField({ initial: 0, integer: true }),
|
||||
antiChaos: new fields.BooleanField({ initial: false }),
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,9 @@
|
||||
import { MournbladeUtility } from "./mournblade-utility.js";
|
||||
import { MournbladeRollDialog } from "./applications/mournblade-roll-dialog.mjs";
|
||||
import MournbladeInvocationDialog from "./applications/mournblade-invocation-dialog.mjs";
|
||||
import MournbladeInvocationDemonDialog from "./applications/mournblade-invocation-demon-dialog.mjs";
|
||||
import MournbladeInvocationEspritDialog from "./applications/mournblade-invocation-esprit-dialog.mjs";
|
||||
import MournbladeEnchantementDialog from "./applications/mournblade-enchantement-dialog.mjs";
|
||||
|
||||
/* -------------------------------------------- */
|
||||
const __degatsBonus = [-2, -2, -1, -1, 0, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 8, 8, 9, 9, 10, 10]
|
||||
@@ -91,6 +94,8 @@ export class MournbladeActor extends Actor {
|
||||
prepareArme(arme) {
|
||||
arme = foundry.utils.duplicate(arme)
|
||||
let combat = this.getCombatValues()
|
||||
const enchBonus = (arme.system.enchantementLoi?.actif && arme.system.enchantementLoi?.bonus > 0)
|
||||
? arme.system.enchantementLoi.bonus : 0
|
||||
if (arme.system.typearme == "contact" || arme.system.typearme == "contactjet") {
|
||||
arme.system.isMelee = true
|
||||
let competence = this.items.find(item => item.type == "competence" && item.name.toLowerCase() == "mêlée")
|
||||
@@ -98,7 +103,7 @@ export class MournbladeActor extends Actor {
|
||||
arme.system.competence = foundry.utils.duplicate(competence)
|
||||
arme.system.attrKey = "pui"
|
||||
arme.system.totalDegats = arme.system.degats + "+" + combat.bonusDegatsTotal
|
||||
arme.system.totalOffensif = this.system.attributs.pui.value + arme.system.competence.system.niveau + arme.system.bonusmaniementoff + combat.attaqueModifier
|
||||
arme.system.totalOffensif = this.system.attributs.pui.value + arme.system.competence.system.niveau + arme.system.bonusmaniementoff + combat.attaqueModifier + enchBonus
|
||||
if (arme.system.isdefense) {
|
||||
arme.system.totalDefensif = combat.defenseTotal + arme.system.competence.system.niveau + arme.system.bonusmaniementdef
|
||||
}
|
||||
@@ -115,7 +120,7 @@ export class MournbladeActor extends Actor {
|
||||
if (competence) {
|
||||
arme.system.competence = foundry.utils.duplicate(competence)
|
||||
arme.system.attrKey = "adr"
|
||||
arme.system.totalOffensif = this.system.attributs.adr.value + arme.system.competence.system.niveau + arme.system.bonusmaniementoff + combat.attaqueModifier
|
||||
arme.system.totalOffensif = this.system.attributs.adr.value + arme.system.competence.system.niveau + arme.system.bonusmaniementoff + combat.attaqueModifier + enchBonus
|
||||
arme.system.totalDegats = arme.system.degats
|
||||
if (arme.system.isdefense) {
|
||||
arme.system.totalDefensif = combat.defenseTotal + arme.system.competence.system.niveau + arme.system.bonusmaniementdef
|
||||
@@ -133,12 +138,14 @@ export class MournbladeActor extends Actor {
|
||||
prepareBouclier(bouclier) {
|
||||
bouclier = foundry.utils.duplicate(bouclier)
|
||||
let combat = this.getCombatValues()
|
||||
const enchBonus = (bouclier.system.enchantementLoi?.actif && bouclier.system.enchantementLoi?.bonus > 0)
|
||||
? bouclier.system.enchantementLoi.bonus : 0
|
||||
let competence = this.items.find(item => item.type == "competence" && item.name.toLowerCase() == "mêlée")
|
||||
if (competence) {
|
||||
bouclier.system.competence = foundry.utils.duplicate(competence)
|
||||
bouclier.system.attrKey = "pui"
|
||||
bouclier.system.totalDegats = bouclier.system.degats + "+" + combat.bonusDegatsTotal
|
||||
bouclier.system.totalOffensif = this.system.attributs.pui.value + bouclier.system.competence.system.niveau
|
||||
bouclier.system.totalOffensif = this.system.attributs.pui.value + bouclier.system.competence.system.niveau + enchBonus
|
||||
bouclier.system.isdefense = true
|
||||
bouclier.system.bonusmaniementoff = 0
|
||||
bouclier.system.totalDefensif = combat.defenseTotal + bouclier.system.competence.system.niveau + bouclier.system.bonusdefense
|
||||
@@ -213,6 +220,9 @@ export class MournbladeActor extends Actor {
|
||||
getMonnaies() {
|
||||
return this.getItemSorted(["monnaie"])
|
||||
}
|
||||
getPotions() {
|
||||
return this.getItemSorted(["potion"])
|
||||
}
|
||||
getArmors() {
|
||||
return this.getItemSorted(["protection"])
|
||||
}
|
||||
@@ -739,6 +749,90 @@ export class MournbladeActor extends Actor {
|
||||
ui.notifications.info(`L'Élémentaire ${invoc.actorName} a été banni. ${invoc.soulCost} points d'Âme libérés.`)
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
async invoquerDemon() {
|
||||
const normalize = str => str.toLowerCase()
|
||||
.replace(/œ/g, "oe").replace(/æ/g, "ae")
|
||||
.normalize("NFD").replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/[^a-z0-9]+/g, " ").trim()
|
||||
const hasKeywords = (item, ...words) => {
|
||||
const n = normalize(item.name)
|
||||
return words.every(w => n.includes(normalize(w)))
|
||||
}
|
||||
const coercitionComp = this.items.find(c => c.type === "competence" && hasKeywords(c, "coercition"))
|
||||
if (!coercitionComp) {
|
||||
ui.notifications.warn("La compétence Coercition est requise pour invoquer un Démon.")
|
||||
return
|
||||
}
|
||||
const isChaotique = this.system.balance.chaos > this.system.balance.loi
|
||||
const hasOeilSorcier = this.items.some(c => (c.type === "capacite" || c.type === "don") && hasKeywords(c, "oeil", "sorcier"))
|
||||
const hasRuneChaos = this.items.some(i => i.type === "rune" && hasKeywords(i, "chaos"))
|
||||
if (!isChaotique || !hasOeilSorcier || !hasRuneChaos) {
|
||||
const missing = []
|
||||
if (!isChaotique) missing.push("alignement chaotique (Chaos > Loi)")
|
||||
if (!hasOeilSorcier) missing.push("Capacité/Don Œil du Sorcier")
|
||||
if (!hasRuneChaos) missing.push("Rune du Chaos")
|
||||
ui.notifications.warn(`Prérequis manquants pour l'invocation démoniaque : ${missing.join(", ")}.`)
|
||||
}
|
||||
|
||||
let rollData = this.getCommonRollData("tre", coercitionComp._id)
|
||||
rollData.isInvocationDemon = true
|
||||
rollData.mainDice = "1d10"
|
||||
|
||||
await MournbladeInvocationDemonDialog.create(this, rollData)
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
async libererDemon(invocIndex) {
|
||||
const invocations = foundry.utils.duplicate(this.system.invocationsDemons || [])
|
||||
const invoc = invocations[invocIndex]
|
||||
if (!invoc) return
|
||||
|
||||
invocations.splice(invocIndex, 1)
|
||||
await this.update({ "system.invocationsDemons": invocations })
|
||||
ui.notifications.info(`Le Démon ${invoc.demonName ?? "invoqué"} a été libéré.`)
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
async invoquerEspritLoi() {
|
||||
const normalize = str => str.toLowerCase()
|
||||
.replace(/œ/g, "oe").replace(/æ/g, "ae")
|
||||
.normalize("NFD").replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/[^a-z0-9]+/g, " ").trim()
|
||||
const hasKeywords = (item, ...words) => {
|
||||
const n = normalize(item.name)
|
||||
return words.every(w => n.includes(normalize(w)))
|
||||
}
|
||||
|
||||
const persuasionComp = this.items.find(c => c.type === "competence" && hasKeywords(c, "persuasion"))
|
||||
const isLoyal = this.system.balance.loi > this.system.balance.chaos
|
||||
const hasRuneLoi = this.items.some(i => i.type === "rune" && hasKeywords(i, "loi"))
|
||||
|
||||
if (!isLoyal || !hasRuneLoi) {
|
||||
const missing = []
|
||||
if (!isLoyal) missing.push("alignement loyal (Loi > Chaos)")
|
||||
if (!hasRuneLoi) missing.push("Rune de la Loi")
|
||||
ui.notifications.warn(`Prérequis manquants : ${missing.join(", ")}.`)
|
||||
}
|
||||
|
||||
const rollData = this.getCommonRollData("tre", persuasionComp?._id ?? null)
|
||||
rollData.isInvocationEsprit = true
|
||||
rollData.mainDice = "1d10"
|
||||
|
||||
await MournbladeInvocationEspritDialog.create(this, rollData)
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
async enchanter(itemId) {
|
||||
const item = this.items.get(itemId)
|
||||
if (!item) return
|
||||
if (!["arme", "equipement", "protection", "bouclier"].includes(item.type)) {
|
||||
ui.notifications.warn("Seules les armes, équipements, protections et boucliers peuvent être enchantés.")
|
||||
return
|
||||
}
|
||||
await MournbladeEnchantementDialog.create(this, item)
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
async rollArmeOffensif(armeId) {
|
||||
let arme = this.items.get(armeId)
|
||||
|
||||
@@ -90,18 +90,55 @@ export class MournbladeConfig {
|
||||
elue: game.i18n.localize("MNBL.typeCapaciteElue"),
|
||||
elementaire: game.i18n.localize("MNBL.typeCapaciteElementaire"),
|
||||
demoniaque: game.i18n.localize("MNBL.typeCapaciteDemoniaque"),
|
||||
automaton: game.i18n.localize("MNBL.typeCapaciteAutomaton"),
|
||||
creature: game.i18n.localize("MNBL.typeCapaciteCreature"),
|
||||
},
|
||||
creatureTypeOptions: {
|
||||
creature: game.i18n.localize("MNBL.creatureTypeCreature"),
|
||||
demon: game.i18n.localize("MNBL.creatureTypeDemon"),
|
||||
elementaire: game.i18n.localize("MNBL.creatureTypeElementaire"),
|
||||
automaton: game.i18n.localize("MNBL.creatureTypeAutomaton"),
|
||||
},
|
||||
demonTypeOptions: {
|
||||
"": game.i18n.localize("MNBL.demonTypeNone"),
|
||||
combat: game.i18n.localize("MNBL.demonTypeCombat"),
|
||||
desir: game.i18n.localize("MNBL.demonTypeDesir"),
|
||||
savoir: game.i18n.localize("MNBL.demonTypeSavoir"),
|
||||
protection: game.i18n.localize("MNBL.demonTypeProtection"),
|
||||
voyage: game.i18n.localize("MNBL.demonTypeVoyage"),
|
||||
},
|
||||
demonPuissanceOptions: {
|
||||
"": game.i18n.localize("MNBL.demonPuissanceNone"),
|
||||
mineur: game.i18n.localize("MNBL.demonPuissanceMineur"),
|
||||
median: game.i18n.localize("MNBL.demonPuissanceMedian"),
|
||||
majeur: game.i18n.localize("MNBL.demonPuissanceMajeur"),
|
||||
},
|
||||
elementTypeOptions: {
|
||||
air: game.i18n.localize("MNBL.elementTypeAir"),
|
||||
terre: game.i18n.localize("MNBL.elementTypeTerre"),
|
||||
feu: game.i18n.localize("MNBL.elementTypeFeu"),
|
||||
eau: game.i18n.localize("MNBL.elementTypeEau"),
|
||||
},
|
||||
automatonTypeOptions: {
|
||||
"": game.i18n.localize("MNBL.automatonTypeNone"),
|
||||
combat: game.i18n.localize("MNBL.automatonTypeCombat"),
|
||||
voyage: game.i18n.localize("MNBL.automatonTypeVoyage"),
|
||||
perception: game.i18n.localize("MNBL.automatonTypePerception"),
|
||||
restauration: game.i18n.localize("MNBL.automatonTypeRestauration"),
|
||||
reparateur: game.i18n.localize("MNBL.automatonTypeReparateur"),
|
||||
},
|
||||
automatonPuissanceOptions: {
|
||||
"": game.i18n.localize("MNBL.automatonPuissanceNone"),
|
||||
mineur: game.i18n.localize("MNBL.automatonPuissanceMineur"),
|
||||
median: game.i18n.localize("MNBL.automatonPuissanceMedian"),
|
||||
majeur: game.i18n.localize("MNBL.automatonPuissanceMajeur"),
|
||||
},
|
||||
automatonVoyageTypeOptions: {
|
||||
"": game.i18n.localize("MNBL.automatonVoyageTypeNone"),
|
||||
terrestre: game.i18n.localize("MNBL.automatonVoyageTypeTerrestre"),
|
||||
aquatique: game.i18n.localize("MNBL.automatonVoyageTypeAquatique"),
|
||||
aerien: game.i18n.localize("MNBL.automatonVoyageTypeAerien"),
|
||||
extradimensionnel: game.i18n.localize("MNBL.automatonVoyageTypeExtradimensionnel"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -219,8 +219,15 @@ export class MournbladeUtility {
|
||||
'systems/fvtt-mournblade/templates/partial-item-description.hbs',
|
||||
'systems/fvtt-mournblade/templates/partial-item-header.hbs',
|
||||
'systems/fvtt-mournblade/templates/partial-item-nav.hbs',
|
||||
'systems/fvtt-mournblade/templates/partial-item-enchantement.hbs',
|
||||
'systems/fvtt-mournblade/templates/dialog-invocation-elementaire.hbs',
|
||||
'systems/fvtt-mournblade/templates/chat-invocation-result.hbs',
|
||||
'systems/fvtt-mournblade/templates/dialog-invocation-demon.hbs',
|
||||
'systems/fvtt-mournblade/templates/chat-invocation-demon-result.hbs',
|
||||
'systems/fvtt-mournblade/templates/dialog-enchantement.hbs',
|
||||
'systems/fvtt-mournblade/templates/chat-enchantement-result.hbs',
|
||||
'systems/fvtt-mournblade/templates/dialog-invocation-esprit.hbs',
|
||||
'systems/fvtt-mournblade/templates/chat-invocation-esprit-result.hbs',
|
||||
]
|
||||
return foundry.applications.handlebars.loadTemplates(templatePaths);
|
||||
}
|
||||
@@ -1079,9 +1086,7 @@ export class MournbladeUtility {
|
||||
return
|
||||
}
|
||||
|
||||
const actor = rollData.tokenId
|
||||
? game.canvas.tokens.get(rollData.tokenId)?.actor
|
||||
: game.actors.get(rollData.actorId)
|
||||
const actor = game.actors.get(rollData.actorId)
|
||||
|
||||
const pa = rollData.pointsAme ?? 1
|
||||
const seuil = rollData.runeSeuil ?? 0
|
||||
@@ -1269,6 +1274,8 @@ export class MournbladeUtility {
|
||||
rollData.createdActorName = createdActorName
|
||||
rollData.bonusPacte = bonusPacte
|
||||
rollData.isGM = game.user.isGM
|
||||
const powersByTier = { mineur: 2, median: 3, majeur: 4 }
|
||||
rollData.invocationPowerCount = powersByTier[rollData.invocationTier] ?? 2
|
||||
|
||||
this.createChatWithRollMode(rollData.alias, {
|
||||
content: await foundry.applications.handlebars.renderTemplate(
|
||||
@@ -1276,4 +1283,235 @@ export class MournbladeUtility {
|
||||
}, { ...rollData, rollMode: "blindroll" })
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
static async rollInvocationDemon(rollData) {
|
||||
const actor = rollData.tokenId
|
||||
? game.canvas.tokens.get(rollData.tokenId)?.actor
|
||||
: game.actors.get(rollData.actorId)
|
||||
|
||||
if (!actor) {
|
||||
ui.notifications.error("Acteur introuvable pour l'invocation démoniaque.")
|
||||
return
|
||||
}
|
||||
|
||||
const soulCost = rollData.invocationSoulCost ?? rollData.invocationSeuil ?? 20
|
||||
const compNiveau = rollData.competence?.system?.niveau ?? 0
|
||||
const compMod = compNiveau === 0 ? -3 : 0
|
||||
const modificateur = rollData.modificateur ?? 0
|
||||
|
||||
// Validate soul
|
||||
const ameDisponible = Math.max(0, actor.system.ame.currentmax - actor.system.ame.value)
|
||||
if (ameDisponible < soulCost) {
|
||||
ui.notifications.warn(`Âme insuffisante pour cette invocation (requis : ${soulCost}, disponible : ${ameDisponible}).`)
|
||||
return
|
||||
}
|
||||
|
||||
rollData.difficulte = rollData.invocationSeuil
|
||||
rollData.diceFormula = `${rollData.mainDice ?? "1d10"}+${rollData.attr.value}+${compNiveau}+${modificateur}+${compMod}+${rollData.malusSante}+${rollData.malusAme}`
|
||||
|
||||
const myRoll = await new Roll(rollData.diceFormula).evaluate()
|
||||
await this.showDiceSoNice(myRoll, "blindroll")
|
||||
rollData.roll = foundry.utils.duplicate(myRoll)
|
||||
rollData.diceResult = myRoll.terms[0].results[0].result
|
||||
rollData.finalResult = myRoll.total
|
||||
this.computeResult(rollData)
|
||||
|
||||
// Soul cost handling
|
||||
let ameDeduct = soulCost
|
||||
let d20Result = null
|
||||
let isDemonAttaque = false
|
||||
let isDisastreDramatique = false
|
||||
let isTraitChaotique = false
|
||||
|
||||
if (rollData.isSuccess || rollData.isHeroique) {
|
||||
// Soul spent immediately (not blocked)
|
||||
await actor.subPointsAme("prononcer", soulCost)
|
||||
|
||||
// Track active invocation
|
||||
const invocations = foundry.utils.duplicate(actor.system.invocationsDemons || [])
|
||||
invocations.push({ demonName: "Démon invoqué", soulCost, date: Date.now() })
|
||||
await actor.update({ "system.invocationsDemons": invocations })
|
||||
} else if (rollData.isDramatique) {
|
||||
// All soul lost
|
||||
await actor.subPointsAme("prononcer", soulCost)
|
||||
|
||||
// Roll d20 for dramatic failure consequences
|
||||
const d20Roll = await new Roll("1d20").evaluate()
|
||||
await this.showDiceSoNice(d20Roll, "blindroll")
|
||||
d20Result = d20Roll.total
|
||||
if (d20Result === 1 || d20Result === 11) {
|
||||
isDisastreDramatique = true
|
||||
} else if (d20Result % 2 !== 0) {
|
||||
// Odd (not 1 or 11) → demon attacks
|
||||
isDemonAttaque = true
|
||||
} else {
|
||||
// Even → chaotic trait
|
||||
isTraitChaotique = true
|
||||
}
|
||||
} else {
|
||||
// Simple failure: half soul lost (round up)
|
||||
ameDeduct = Math.ceil(soulCost / 2)
|
||||
await actor.subPointsAme("prononcer", ameDeduct)
|
||||
}
|
||||
|
||||
rollData.invocationSoulDeducted = (rollData.isSuccess || rollData.isHeroique) ? soulCost : ameDeduct
|
||||
rollData.d20Result = d20Result
|
||||
rollData.isDemonAttaque = isDemonAttaque
|
||||
rollData.isDisastreDramatique = isDisastreDramatique
|
||||
rollData.isTraitChaotique = isTraitChaotique
|
||||
rollData.isGM = game.user.isGM
|
||||
rollData.claValue = actor.system.attributs?.cla?.value ?? 0
|
||||
|
||||
this.createChatWithRollMode(rollData.alias, {
|
||||
content: await foundry.applications.handlebars.renderTemplate(
|
||||
`systems/fvtt-mournblade/templates/chat-invocation-demon-result.hbs`, rollData)
|
||||
}, { ...rollData, rollMode: "blindroll" })
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
static async rollInvocationEsprit(rollData) {
|
||||
const actor = rollData.tokenId
|
||||
? game.canvas.tokens.get(rollData.tokenId)?.actor
|
||||
: game.actors.get(rollData.actorId)
|
||||
|
||||
if (!actor) {
|
||||
ui.notifications.error("Acteur introuvable pour l'invocation d'un Esprit de la Loi.")
|
||||
return
|
||||
}
|
||||
|
||||
const soulCost = rollData.invocationSoulCost ?? 15
|
||||
const compNiveau = rollData.competence?.system?.niveau ?? 0
|
||||
const compMod = compNiveau === 0 ? -3 : 0
|
||||
const modificateur = rollData.modificateur ?? 0
|
||||
|
||||
const ameDisponible = Math.max(0, actor.system.ame.currentmax - actor.system.ame.value)
|
||||
if (ameDisponible < soulCost) {
|
||||
ui.notifications.warn(`Âme insuffisante (requis : ${soulCost}, disponible : ${ameDisponible}).`)
|
||||
return
|
||||
}
|
||||
|
||||
rollData.difficulte = soulCost
|
||||
rollData.diceFormula = `${rollData.mainDice ?? "1d10"}+${rollData.attr.value}+${compNiveau}+${modificateur}+${compMod}+${rollData.malusSante}+${rollData.malusAme}`
|
||||
|
||||
const myRoll = await new Roll(rollData.diceFormula).evaluate()
|
||||
await this.showDiceSoNice(myRoll, "roll")
|
||||
rollData.roll = foundry.utils.duplicate(myRoll)
|
||||
rollData.diceResult = myRoll.terms[0].results[0].result
|
||||
rollData.finalResult = myRoll.total
|
||||
this.computeResult(rollData)
|
||||
|
||||
let ameDeduct = soulCost
|
||||
|
||||
if (rollData.isSuccess || rollData.isHeroique) {
|
||||
await actor.subPointsAme("prononcer", soulCost)
|
||||
} else if (rollData.isDramatique) {
|
||||
// All soul lost, Réceptacle destroyed
|
||||
await actor.subPointsAme("prononcer", soulCost)
|
||||
} else {
|
||||
// Simple failure: half soul lost (round up)
|
||||
ameDeduct = Math.ceil(soulCost / 2)
|
||||
await actor.subPointsAme("prononcer", ameDeduct)
|
||||
}
|
||||
|
||||
rollData.invocationSoulDeducted = (rollData.isSuccess || rollData.isHeroique) ? soulCost : ameDeduct
|
||||
rollData.isGM = game.user.isGM
|
||||
|
||||
const typeLabels = {
|
||||
combat: "Combat", voyage: "Voyage", perception: "Perception",
|
||||
restauration: "Restauration", reparateur: "Réparateur",
|
||||
}
|
||||
const puissanceLabels = { mineur: "Mineur", median: "Médian", majeur: "Majeur" }
|
||||
rollData.automatonTypeLabel = typeLabels[rollData.automatonType] ?? rollData.automatonType
|
||||
rollData.automatonPuissanceLabel = puissanceLabels[rollData.automatonPuissance] ?? rollData.automatonPuissance
|
||||
|
||||
this.createChatWithRollMode(rollData.alias, {
|
||||
content: await foundry.applications.handlebars.renderTemplate(
|
||||
`systems/fvtt-mournblade/templates/chat-invocation-esprit-result.hbs`, rollData)
|
||||
}, { ...rollData, rollMode: "roll" })
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
static async rollEnchantement({ actor, item, ptsAme, antiChaos, modificateur,
|
||||
savoirRunesComp, hautParlerComp, artisanatComp, claValeur, limiteur }) {
|
||||
|
||||
// Validate soul
|
||||
const ameDisponible = Math.max(0, actor.system.ame.currentmax - actor.system.ame.value)
|
||||
if (ameDisponible < ptsAme) {
|
||||
ui.notifications.warn(`Âme insuffisante (requis : ${ptsAme}, disponible : ${ameDisponible}).`)
|
||||
return
|
||||
}
|
||||
|
||||
const savoirNiveau = savoirRunesComp?.system?.niveau ?? 0
|
||||
const compMod = savoirNiveau === 0 ? -3 : 0
|
||||
const basePool = claValeur + savoirNiveau + compMod + (modificateur ?? 0)
|
||||
const effectivePool = limiteur !== null ? Math.min(basePool, limiteur) : basePool
|
||||
const difficulte = ptsAme
|
||||
|
||||
const formula = `1d10+${effectivePool}`
|
||||
const myRoll = await new Roll(formula).evaluate()
|
||||
await this.showDiceSoNice(myRoll, game.settings.get("core", "rollMode"))
|
||||
|
||||
const rollData = this.getBasicRollData()
|
||||
rollData.alias = actor.name
|
||||
rollData.actorImg = actor.img
|
||||
rollData.diceResult = myRoll.terms[0].results[0].result
|
||||
rollData.finalResult = myRoll.total
|
||||
rollData.difficulte = difficulte
|
||||
rollData.roll = foundry.utils.duplicate(myRoll)
|
||||
this.computeResult(rollData)
|
||||
|
||||
// Compute bonus
|
||||
let bonusBase = Math.floor(ptsAme / 5)
|
||||
let bonusFinal = bonusBase
|
||||
let ameDeduct = ptsAme
|
||||
let itemDestroyed = false
|
||||
let message = ""
|
||||
|
||||
if (rollData.isHeroique) {
|
||||
bonusFinal = bonusBase + 1
|
||||
await actor.subPointsAme("prononcer", ptsAme)
|
||||
await item.update({
|
||||
"system.enchantementLoi.actif": true,
|
||||
"system.enchantementLoi.bonus": bonusFinal,
|
||||
"system.enchantementLoi.antiChaos": antiChaos,
|
||||
})
|
||||
message = `Réussite héroïque ! L'objet est enchanté avec un bonus de +${bonusFinal}.`
|
||||
} else if (rollData.isSuccess) {
|
||||
await actor.subPointsAme("prononcer", ptsAme)
|
||||
await item.update({
|
||||
"system.enchantementLoi.actif": true,
|
||||
"system.enchantementLoi.bonus": bonusFinal,
|
||||
"system.enchantementLoi.antiChaos": antiChaos,
|
||||
})
|
||||
message = `Succès ! L'objet est enchanté avec un bonus de +${bonusFinal}.`
|
||||
} else if (rollData.isDramatique) {
|
||||
ameDeduct = ptsAme
|
||||
await actor.subPointsAme("prononcer", ameDeduct)
|
||||
await item.delete()
|
||||
itemDestroyed = true
|
||||
message = `Échec dramatique ! Tous les points d'Âme sont perdus et l'objet est détruit !`
|
||||
} else {
|
||||
// Failure: half soul lost
|
||||
ameDeduct = Math.ceil(ptsAme / 2)
|
||||
await actor.subPointsAme("prononcer", ameDeduct)
|
||||
message = `Échec ! ${ameDeduct} points d'Âme perdus. L'objet n'est pas enchanté.`
|
||||
}
|
||||
|
||||
rollData.itemName = item.name
|
||||
rollData.itemImg = item.img
|
||||
rollData.ptsAme = ptsAme
|
||||
rollData.antiChaos = antiChaos
|
||||
rollData.bonusFinal = bonusFinal
|
||||
rollData.ameDeduct = ameDeduct
|
||||
rollData.itemDestroyed = itemDestroyed
|
||||
rollData.enchantMessage = message
|
||||
rollData.effectivePool = effectivePool
|
||||
rollData.savoirNiveau = savoirNiveau
|
||||
|
||||
this.createChatWithRollMode(actor.name, {
|
||||
content: await foundry.applications.handlebars.renderTemplate(
|
||||
`systems/fvtt-mournblade/templates/chat-enchantement-result.hbs`, rollData)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user