Esprit de la Loi + Automaton

This commit is contained in:
2026-05-02 23:16:10 +02:00
parent d6b5891519
commit 0df4a5a9fb
280 changed files with 10668 additions and 419 deletions
@@ -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())