Files
fvtt-mournblade/modules/mournblade-utility.js
T
2026-05-02 23:16:10 +02:00

1518 lines
59 KiB
JavaScript

/* -------------------------------------------- */
import { MournbladeCombat } from "./mournblade-combat.js";
import { MournbladeCommands } from "./mournblade-commands.js";
/* -------------------------------------------- */
export class MournbladeUtility {
/* -------------------------------------------- */
static async init() {
Hooks.on('renderChatMessageHTML', (log, html, data) => MournbladeUtility.chatListeners(html))
Hooks.on("getChatMessageContextOptions", (html, options) => MournbladeUtility.chatRollMenu(html, options))
Hooks.on('renderChatMessageHTML', (message, html, data) => MournbladeUtility.chatMessageHandler(message, html, data))
Hooks.on("getCombatTrackerEntryContext", (html, options) => {
MournbladeUtility.pushInitiativeOptions(html, options);
})
this.rollDataStore = {}
this.defenderStore = {}
MournbladeCommands.init();
Handlebars.registerHelper('count', function (list) {
return list.length;
})
Handlebars.registerHelper('includes', function (array, val) {
return array.includes(val);
})
Handlebars.registerHelper('upper', function (text) {
return text.toUpperCase();
})
Handlebars.registerHelper('lower', function (text) {
return text.toLowerCase()
})
Handlebars.registerHelper('upperFirst', function (text) {
if (typeof text !== 'string') return text
return text.charAt(0).toUpperCase() + text.slice(1)
})
Handlebars.registerHelper('notEmpty', function (list) {
return list.length > 0;
})
Handlebars.registerHelper('mul', function (a, b) {
return parseInt(a) * parseInt(b);
})
}
/* -------------------------------------------- */
static getActorFromRollData(rollData) {
let actor = game.actors.get(rollData.actorId)
if (rollData.tokenId) {
let token = canvas.tokens.placeables.find(t => t.id == rollData.tokenId)
if (token) {
actor = token.actor
}
}
return actor
}
/* -------------------------------------------- */
static sortArrayObjectsByName(myArray) {
myArray.sort((a, b) => {
return a.name.localeCompare(b.name);
})
}
/* -------------------------------------------- */
static getAttributs() {
return game.system.mournblade.config.attributs
}
/* -------------------------------------------- */
static pushInitiativeOptions(html, options) {
}
/* -------------------------------------------- */
static getSkills() {
return this.skills
}
/* -------------------------------------------- */
static async ready() {
const skills = await MournbladeUtility.loadCompendium("fvtt-mournblade.skills")
this.skills = skills.map(i => i.toObject())
game.system.mournblade.config.listeNiveauSkill = MournbladeUtility.createDirectOptionList(0, 10)
game.system.mournblade.config.listeNiveauCreature = MournbladeUtility.createDirectOptionList(0, 35)
game.system.mournblade.config.modificateurOptions = MournbladeUtility.createArrayOptionList(-15, 15)
game.system.mournblade.config.pointsAmeOptions = MournbladeUtility.createDirectOptionList(0, 20)
}
/* -------------------------------------------- */
static async loadCompendiumData(compendium) {
const pack = game.packs.get(compendium);
return await pack?.getDocuments() ?? [];
}
/* -------------------------------------------- */
static async loadCompendium(compendium, filter = item => true) {
let compendiumData = await MournbladeUtility.loadCompendiumData(compendium);
return compendiumData.filter(filter);
}
/* -------------------------------------------- */
static getOptionsStatusList() {
return this.optionsStatusList;
}
/* -------------------------------------------- */
static getPredilection(comp, predIdx) {
let pred = foundry.utils.duplicate(comp.system.predilections)
return foundry.utils.duplicate(pred[predIdx] || { name: "Error!" })
}
/* -------------------------------------------- */
static async chatMessageHandler(message, html, data) {
const chatCard = $(html).find('.action-section')
if (chatCard.length > 0) {
// If the user is the message author or the actor owner, proceed
const actor = game.actors.get(data.message.speaker.actor)
// DEBUG : console.log("FOUND 1!!! ", actor, data.message)
if (actor?.isOwner || game.user.isGM) {
return
}
chatCard.hide()
}
}
/* -------------------------------------------- */
static async chatListeners(html) {
$(html).on("click", '.mournblade-open-guide', async event => {
event.preventDefault()
const doc = await fromUuid("Compendium.fvtt-mournblade.journal-aide.JournalEntry.JurnlHelpGuide01")
if (doc) doc.sheet.render(true)
})
$(html).on("click", '.predilection-reroll', async event => {
let predIdx = $(event.currentTarget).data("predilection-index")
let messageId = MournbladeUtility.findChatMessageId(event.currentTarget)
let message = game.messages.get(messageId)
let rollData = message.getFlag("world", "mournblade-roll")
let actor = MournbladeUtility.getActorFromRollData(rollData)
await actor.setPredilectionUsed(rollData.competence._id, predIdx)
rollData.competence = foundry.utils.duplicate(actor.getCompetence(rollData.competence._id))
rollData.predilectionUsed = MournbladeUtility.getPredilection(rollData.competence, predIdx)
await MournbladeUtility.rollMournblade(rollData)
})
$(html).on("click", '.arme-roll-degats', async event => {
let messageId = MournbladeUtility.findChatMessageId(event.currentTarget)
let message = game.messages.get(messageId)
let rollData = message.getFlag("world", "mournblade-roll")
MournbladeUtility.rollDegatsFromAttaque(rollData)
})
$(html).on("click", '.arme-apply-degats', async event => {
let messageId = MournbladeUtility.findChatMessageId(event.currentTarget)
let message = game.messages.get(messageId)
let rollData = message.getFlag("world", "mournblade-roll")
if (game.user.isGM) {
MournbladeUtility.applyDegatsFromAttaque(rollData)
} else {
game.socket.emit("system.fvtt-mournblade", { name: "msg_apply_damage", data: { rollData: rollData } })
}
})
$(html).on("click", '.rune-post-chat', async event => {
event.preventDefault()
const btn = event.currentTarget
const actorId = btn.dataset.actorId
const itemId = btn.dataset.itemId
await MournbladeUtility.postItemToChat(actorId, itemId)
})
}
/* -------------------------------------------- */
static async postItemToChat(actorId, itemId) {
const actor = game.actors.get(actorId)
const item = actor?.items.get(itemId)
if (!item) { ui.notifications.warn("Item introuvable."); return }
let chatData = foundry.utils.duplicate(item)
if (actor) chatData.actor = { id: actor.id }
if (chatData.img?.includes("/blank.png")) chatData.img = null
chatData.jsondata = JSON.stringify({ compendium: "postedItem", payload: chatData })
const typeLabels = {
arme: "Arme", bouclier: "Bouclier", competence: "Compétence",
rune: "Rune", runeeffect: "Rune Active", don: "Don", pacte: "Pacte",
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"
}
chatData.typeLabel = typeLabels[chatData.type] ?? chatData.type
const typeIcons = {
arme: "fa-sword", bouclier: "fa-shield-halved", competence: "fa-graduation-cap",
rune: "fa-star-of-david", runeeffect: "fa-star-of-david", don: "fa-hand-sparkles",
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",
potion: "fa-flask"
}
chatData.typeIcon = typeIcons[chatData.type] ?? "fa-cube"
const html = await foundry.applications.handlebars.renderTemplate(
'systems/fvtt-mournblade/templates/post-item.hbs', chatData)
ChatMessage.create({ user: game.user.id, content: html })
}
/* -------------------------------------------- */
static async preloadHandlebarsTemplates() {
const templatePaths = [
'systems/fvtt-mournblade/templates/editor-notes-gm.hbs',
'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);
}
/* -------------------------------------------- */
static removeChatMessageId(messageId) {
if (messageId) {
game.messages.get(messageId)?.delete();
}
}
static findChatMessageId(current) {
return MournbladeUtility.getChatMessageId(MournbladeUtility.findChatMessage(current));
}
static getChatMessageId(node) {
return node?.attributes.getNamedItem('data-message-id')?.value;
}
static findChatMessage(current) {
return MournbladeUtility.findNodeMatching(current, it => it.classList.contains('chat-message') && it.attributes.getNamedItem('data-message-id'))
}
static findNodeMatching(current, predicate) {
if (current) {
if (predicate(current)) {
return current;
}
return MournbladeUtility.findNodeMatching(current.parentElement, predicate);
}
return undefined;
}
/* -------------------------------------------- */
static createDirectOptionList(min, max) {
let options = {};
for (let i = min; i <= max; i++) {
options[`${i}`] = `${i}`;
}
return options;
}
static createArrayOptionList(min, max) {
let options = [];
for (let i = min; i <= max; i++) {
options.push({ key: `${i}`, label: `${i}` });
}
return options;
}
/* -------------------------------------------- */
static getTarget() {
if (game.user.targets && game.user.targets.size == 1) {
for (let target of game.user.targets) {
return target;
}
}
return undefined;
}
/* -------------------------------------------- */
static getRollData(id) {
return this.rollDataStore[id];
}
/* -------------------------------------------- */
static onSocketMesssage(msg) {
if (msg.name == "msg_apply_damage") {
if (game.user.isGM) {
this.applyDegatsFromAttaque(msg.data.rollData);
}
}
}
/* -------------------------------------------- */
static chatDataSetup(content, modeOverride, isRoll = false, forceWhisper) {
let chatData = {
user: game.user.id,
rollMode: modeOverride || game.settings.get("core", "rollMode"),
content: content
};
if (["gmroll", "blindroll"].includes(chatData.rollMode)) chatData["whisper"] = ChatMessage.getWhisperRecipients("GM").map(u => u.id);
if (chatData.rollMode === "blindroll") chatData["blind"] = true;
else if (chatData.rollMode === "selfroll") chatData["whisper"] = [game.user];
if (forceWhisper) { // Final force !
chatData["speaker"] = ChatMessage.getSpeaker();
chatData["whisper"] = ChatMessage.getWhisperRecipients(forceWhisper);
}
return chatData;
}
/* -------------------------------------------- */
static async showDiceSoNice(roll, rollMode) {
if (game.modules.get("dice-so-nice")?.active) {
if (game.dice3d) {
let whisper = null;
let blind = false;
rollMode = rollMode ?? game.settings.get("core", "rollMode");
switch (rollMode) {
case "blindroll": //GM only
blind = true;
case "gmroll": //GM + rolling player
whisper = this.getUsers(user => user.isGM);
break;
case "roll": //everybody
whisper = this.getUsers(user => user.active);
break;
case "selfroll":
whisper = [game.user.id];
break;
}
await game.dice3d.showForRoll(roll, game.user, true, whisper, blind);
}
}
}
/* -------------------------------------------- */
static computeResult(rollData) {
if (rollData.mainDice == "1d20") {
let diceValue = rollData.roll.terms[0].results[0].result
diceValue *= (rollData.doubleD20) ? 2 : 1
//console.log("PAIR/IMP", diceValue)
if (diceValue % 2 == 1) {
//console.log("PAIR/IMP2", diceValue)
rollData.finalResult -= rollData.roll.terms[0].results[0].result // Substract value
rollData.isImpair = true
if (diceValue == 1 || diceValue == 11) {
rollData.isDramatique = true
rollData.isSuccess = false
}
}
}
this.computeQualityResult(rollData)
}
/* -------------------------------------------- */
static computeQualityResult(rollData) {
//console.log("Result : ", rollData)
if (rollData.difficulte > 0 && !rollData.isDramatique) {
rollData.isSuccess = (rollData.finalResult >= rollData.difficulte)
rollData.isHeroique = ((rollData.finalResult - rollData.difficulte) >= 10)
rollData.isDramatique = ((rollData.finalResult - rollData.difficulte) <= -10)
rollData.isPureSuccess = (rollData.isSuccess && !rollData.isHeroique)
}
rollData.isEchec = !rollData.isSuccess
}
/* -------------------------------------------- */
static async rollMournblade(rollData) {
let actor = MournbladeUtility.getActorFromRollData(rollData)
if (rollData.attrKey == "tochoose") { // No attr selected, force address
rollData.attrKey = "adr"
}
if (!rollData.attr) {
rollData.actionImg = "systems/fvtt-mournblade/assets/icons/" + actor.system.attributs[rollData.attrKey].labelnorm + ".webp"
rollData.attr = foundry.utils.duplicate(actor.system.attributs[rollData.attrKey])
}
rollData.diceFormula = rollData.mainDice
if (rollData.doubleD20) { // Multiply result !
rollData.diceFormula += "*2"
if (!rollData.isReroll) {
actor.changeEclat(-1)
}
}
//console.log("BEFORE COMP", rollData)
if (rollData.competence) {
rollData.predilections = foundry.utils.duplicate(rollData.competence.system.predilections)
let compmod = (rollData.competence.system.niveau == 0) ? -3 : 0
rollData.diceFormula += `+${rollData.attr.value}+${rollData.competence.system.niveau}+${rollData.modificateur}+${compmod}`
} else {
rollData.diceFormula += `+${rollData.attr.value}*2+${rollData.modificateur}`
}
rollData.diceFormula += `+${rollData.malusSante}+${rollData.malusAme}`
if (rollData.arme?.type == "arme") {
rollData.diceFormula += `+${rollData.arme.system.bonusmaniementoff}`
}
// Apply modifiers
for (let modifier of rollData.modifiers) {
if (modifier.system.modifiertype == "roll" && modifier.system.apply) {
rollData.diceFormula += `+${modifier.system.value}`
}
}
// Apply desavantages
let desavantagesBonus = 0
for (let desavantage in rollData.desavantages) {
if (rollData.desavantages[desavantage]) {
desavantagesBonus += 5
}
}
desavantagesBonus = Math.min(15, desavantagesBonus)
rollData.diceFormula += `+${desavantagesBonus}`
// Monté ?
if (rollData.isMonte) {
rollData.diceFormula += "+5"
}
// Specific modifier for distance
if (rollData.arme?.system?.isDistance) {
if (rollData.visee) {
rollData.diceFormula += "+5"
}
if (rollData.cibleconsciente && rollData.defender) {
rollData.diceFormula += `-${rollData.defender.system.attributs.adr.value}`
}
if (rollData.ciblecourt) {
if (rollData.difficulte <= 15) { // Portée courte ou moins
rollData.diceFormula += `-5`
} else {
rollData.diceFormula += `-10`
}
}
if (rollData.typeCouvert && rollData.typeCouvert != "aucun" && rollData.config.couverts[rollData.typeCouvert]) {
rollData.diceFormula += `+${rollData.config.couverts[rollData.typeCouvert].value}`
}
}
if (rollData.rune) {
const actionsBase = Math.ceil(rollData.runeame / 3)
rollData.runeActionsComplexes = (rollData.runemode == "inscrire") ? actionsBase * 2 : actionsBase
if (rollData.runemode == "inscrire") {
rollData.runeduree = null // durée infinie
rollData.dureeLabel = "infinie"
} else {
// prononcer : 1 heure de base + 1 heure par tranche de 2 points d'âme
rollData.runeduree = 1 + Math.floor(rollData.runeame / 2)
rollData.dureeLabel = rollData.runeduree === 1 ? "1 heure" : `${rollData.runeduree} heures`
}
}
let myRoll = await new Roll(rollData.diceFormula).evaluate();
await this.showDiceSoNice(myRoll, game.settings.get("core", "rollMode"))
rollData.roll = foundry.utils.duplicate(myRoll)
rollData.diceResult = myRoll.terms[0].results[0].result
console.log(">>>> ", myRoll)
rollData.finalResult = myRoll.total
this.computeResult(rollData)
// Application immédiate selon type de jet
if (rollData.rune) {
let subAme = rollData.runeame
// Réussite héroïque + rune uniquement sur soi : coût d'âme divisé par 2 (arrondi sup.)
if (rollData.isHeroique && rollData.runeautocible) {
subAme = Math.ceil(subAme / 2)
rollData.runeameCostReduit = true
rollData.runeameCostFinal = subAme
} else if (rollData.isEchec && !rollData.isDramatique) {
// Échec simple : perd la moitié (arrondie sup.)
subAme = Math.ceil((subAme + 1) / 2)
}
actor.subPointsAme(rollData.runemode, subAme)
// Échec dramatique : dé du Chaos (d20)
if (rollData.isDramatique) {
const chaosRoll = await new Roll("1d20").evaluate()
await this.showDiceSoNice(chaosRoll, game.settings.get("core", "rollMode"))
const cr = chaosRoll.terms[0].results[0].result
rollData.chaosDieResult = cr
const claValue = rollData.attr?.value ?? 0
if (cr === 1 || cr === 11) {
rollData.chaosEffet = "desastre"
rollData.chaosEffetTexte = `Désastre extraordinaire ! La Rune se déclenche à des kilomètres de là sur des cibles inconnues. La Loi se manifeste : le sorcier ne peut plus utiliser l'Œil pendant ${claValue} semaine${claValue > 1 ? "s" : ""}.`
} else if (cr % 2 === 1) {
rollData.chaosEffet = "echec_absolu"
rollData.chaosEffetTexte = "Échec absolu. Le MJ décide si la Rune se manifeste sur des cibles autres, dans des proportions désavantageuses ou en un lieu très lointain."
} else {
rollData.chaosEffet = "rien"
rollData.chaosEffetTexte = "Rien de particulier ne se produit en plus de la perte des points d'Âme."
}
}
// Créer l'effet de rune sur l'acteur si le jet est réussi
if (rollData.isSuccess) {
const effetMode = (rollData.runemode == "prononcer") ? "prononcee" : "inscrite"
await actor.createEmbeddedDocuments("Item", [{
name: rollData.rune.name,
type: "runeeffect",
img: rollData.rune.img || "systems/fvtt-mournblade/assets/icons/rune.webp",
system: {
rune: rollData.rune.name,
mode: effetMode,
duree: rollData.dureeLabel,
pointame: rollData.runeame
}
}])
}
}
if (rollData.typeAttaque == "assomer" && rollData.defenderTokenId && rollData.isPureSuccess) {
let defender = game.canvas.tokens.get(rollData?.defenderTokenId)?.actor
defender.setModifier("Assomer : Prochaine action", "roll", -5)
}
if (rollData.typeAttaque == "fuir" && rollData.difficulte > 0 && !rollData.isSuccess) {
actor.setModifier("Fuite échouée : -5 en défense ce round et suivant", "defense", -5)
}
if (rollData.typeAttaque == "immobiliser" && rollData.difficulte > 0 && rollData.isPureSuccess) {
actor.setModifier("Immobilisation en cours : -5 pour prochaine action", "roll", -5)
}
if (rollData.typeAttaque == "chargecavalerie") {
actor.setModifier("Charge de Cavalerie : -5 défense pour le tour", "defense", -5)
}
this.createChatWithRollMode(rollData.alias, {
content: await foundry.applications.handlebars.renderTemplate(`systems/fvtt-mournblade/templates/chat-generic-result-v2.hbs`, rollData)
}, rollData)
}
/* -------------------------------------------- */
static async rollSortilege(rollData) {
if (!rollData.sortilegeRunes || rollData.sortilegeRunes.length === 0) {
ui.notifications.warn("Aucune Rune sélectionnée pour le Sortilège.")
return
}
const actor = rollData.tokenId
? game.canvas.tokens.get(rollData.tokenId)?.actor
: game.actors.get(rollData.actorId)
// Pré-calcul des infos du sortilège
const isInscrire = rollData.runemode === "inscrire"
rollData.sortilegeRunes.forEach(r => {
r.actionsComplexes = Math.ceil(r.pts / 3) * (isInscrire ? 2 : 1)
if (isInscrire) {
r.dureeLabel = "infinie"
} else {
const h = 1 + Math.floor(r.pts / 2)
r.dureeLabel = h === 1 ? "1 heure" : `${h} heures`
}
})
rollData.runeActionsComplexes = rollData.sortilegeRunes.reduce((s, r) => s + r.actionsComplexes, 0)
// Construction de la formule de jet : mainDice + CLA + Savoir:Runes + malus + modificateur
const compNiveau = rollData.competence?.system?.niveau ?? 0
const compMod = compNiveau === 0 ? -3 : 0
rollData.diceFormula = `${rollData.mainDice}+${rollData.attr.value}+${compNiveau}+${rollData.modificateur}+${compMod}+${rollData.malusSante}+${rollData.malusAme}`
const myRoll = await new Roll(rollData.diceFormula).evaluate()
await this.showDiceSoNice(myRoll, game.settings.get("core", "rollMode"))
rollData.roll = foundry.utils.duplicate(myRoll)
rollData.diceResult = myRoll.terms[0].results[0].result
rollData.finalResult = myRoll.total
this.computeResult(rollData)
// Déduction des points d'âme
let totalCost = rollData.sortilegeRunes.reduce((s, r) => s + r.pts, 0)
if (rollData.isHeroique && rollData.runeautocible) {
totalCost = Math.ceil(totalCost / 2)
rollData.runeameCostReduit = true
rollData.runeameCostFinal = totalCost
} else if (rollData.isEchec && !rollData.isDramatique) {
totalCost = Math.ceil((totalCost + 1) / 2)
}
actor.subPointsAme(rollData.runemode, totalCost)
// Échec dramatique : dé du Chaos
if (rollData.isDramatique) {
const chaosRoll = await new Roll("1d20").evaluate()
await this.showDiceSoNice(chaosRoll, game.settings.get("core", "rollMode"))
const cr = chaosRoll.terms[0].results[0].result
rollData.chaosDieResult = cr
const claValue = rollData.attr?.value ?? 0
if (cr === 1 || cr === 11) {
rollData.chaosEffet = "desastre"
rollData.chaosEffetTexte = `Désastre extraordinaire ! Les Runes se déclenchent à des kilomètres de là sur des cibles inconnues. La Loi se manifeste : le sorcier ne peut plus utiliser l'Œil pendant ${claValue} semaine${claValue > 1 ? "s" : ""}.`
} else if (cr % 2 === 1) {
rollData.chaosEffet = "echec_absolu"
rollData.chaosEffetTexte = "Échec absolu. Le MJ décide si les Runes se manifestent sur des cibles autres, dans des proportions désavantageuses ou en un lieu très lointain."
} else {
rollData.chaosEffet = "rien"
rollData.chaosEffetTexte = "Rien de particulier ne se produit en plus de la perte des points d'Âme."
}
}
// Succès : créer un runeeffect par rune
if (rollData.isSuccess) {
const effetMode = isInscrire ? "inscrite" : "prononcee"
const items = rollData.sortilegeRunes.map(r => ({
name: r.name,
type: "runeeffect",
img: r.img || "systems/fvtt-mournblade/assets/icons/rune.webp",
system: {
rune: r.name,
mode: effetMode,
duree: r.dureeLabel,
pointame: r.pts
}
}))
await actor.createEmbeddedDocuments("Item", items)
}
rollData.runeame = rollData.sortilegeRunes.reduce((s, r) => s + r.pts, 0)
this.createChatWithRollMode(rollData.alias, {
content: await foundry.applications.handlebars.renderTemplate(
`systems/fvtt-mournblade/templates/chat-sortilege-result.hbs`, rollData)
}, rollData)
}
/* -------------------------------------------- */
static async rollDegatsFromAttaque(rollData) {
let maximize = false
let degatsMessage = "Degats normaux"
if (rollData.arme?.system?.isMelee) {
rollData.degatsFormula = rollData.arme.system.totalDegats
if (rollData.isHeroique) { // Deux fois les dés de dégats
degatsMessage = "Dégats doublés"
}
if (rollData.typeAttaque == "assomer") {
rollData.degatsFormula = false
}
if (rollData.typeAttaque == "charger") {
rollData.degatsFormula += "+2"
}
if (rollData.typeAttaque == "chargecavalerie") {
rollData.degatsFormula += "+5"
}
if (rollData.typeAttaque == "precise") {
degatsMessage = "Degats normaux"
if (rollData.isHeroique) { // Degats max
maximize = true
degatsMessage = "Dégats maximaux, ignore l'armure du défenseur";
rollData.ignoreDefenseArmor = true
}
}
if (rollData.typeAttaque == "feinte") {
degatsMessage = "Pas de dégats, mais bonus pour prochaine attaque"
rollData.degatsFormula = false
rollData.nextBonus = 5
if (rollData.isHeroique) { // Bonus pour prochaine action
rollData.nextBonus = 10
rollData.nextBonusDegats = 10
}
}
if (rollData.typeAttaque == "coupbas") {
degatsMessage = "Pas de dégats, mais malus pour prochaine action complexe du défenseur"
rollData.degatsFormula = false
rollData.nextMalus = 5
if (rollData.isHeroique) { // Malus pour prochaine action
rollData.nextMalus = 15
}
}
if (rollData.typeAttaque == "contenir") {
degatsMessage = "Pas de dégats, mais l'adversaire ne peut pas vous attaquer pour le reste du tour"
rollData.degatsFormula = false
if (rollData.isHeroique) { // Malus pour prochaine action
degatsMessage = "Pas de dégats, mais tout les adversaires avec une défense inférieure ou égale à " + rollData.finalResult - 10 +
" ne peuvent pas vous attaquer pour le reste du tour"
}
}
if (rollData.typeAttaque == "desarmer") {
degatsMessage = "Pas de dégats, mais l'adversaire reçoit un malus de -5 pour sa prochaine action"
rollData.degatsFormula = false
if (rollData.isHeroique) { // Malus pour prochaine action
rollData.defenderDesarme = true
degatsMessage = "Pas de dégats, mais l'arme de votre adversaire est arrachée de ses mains"
}
}
} else { // Armes à distance
rollData.degatsFormula = rollData.arme.system.totalDegats
}
// Perform the roll, show the dice
rollData.finalResult = 0
rollData.degatsMessage = degatsMessage
if (rollData.degatsFormula) {
console.log("Degats formula", rollData.degatsFormula)
// Twice!maximize
if (rollData.isHeroique && !maximize) {
rollData.degatsFormula += "+" + rollData.degatsFormula
}
// Latest modifiers
for (let mod of rollData.modifiers) {
if (mod.system.modifiertype == "degats") {
rollData.degatsFormula += `+${mod.system.value}`
}
}
let degatsRoll = await new Roll(rollData.degatsFormula).evaluate({ maximize: maximize })
await this.showDiceSoNice(degatsRoll, game.settings.get("core", "rollMode"))
rollData.degatsRoll = foundry.utils.duplicate(degatsRoll)
rollData.finalResult = degatsRoll.total
}
this.createChatWithRollMode(rollData.alias, {
content: await foundry.applications.handlebars.renderTemplate(`systems/fvtt-mournblade/templates/chat-degats-result-v2.hbs`, rollData)
}, rollData)
}
/* -------------------------------------------- */
static applyDegatsFromAttaque(rollData) {
let defender = game.canvas.tokens.get(rollData?.defenderTokenId)?.actor
if (defender && rollData.arme) {
let actor = MournbladeUtility.getActorFromRollData(rollData)
if (rollData.typeAttaque == "desarmer" && !rollData.isHeroique) {
defender.setModifier("Malus suite à désarmement", "roll", -5)
}
if (rollData.typeAttaque == "charger") {
actor.setModifier("Défense suite à charge", "roll", -5)
}
if (rollData.nextBonus) {
actor.setModifier("Prochaine attaque", "roll", rollData.nextBonus)
if (rollData.nextDegatsBonus) {
actor.setModifier("Prochaine attaque", "degats", rollData.nextDegatsBonus)
}
}
if (rollData.nextMalus) {
defender.setModifier("Prochaine action complexe", "roll", -rollData.nextMalus)
}
if (rollData.defenderDesarme) {
ui.notifications.info("L'arme de " + defender.name + " est arrachée de ses mains (à gérer manuellement)")
}
let degats = rollData.finalResult
let type = (rollData.arme.system.nonletaux) ? "nonletaux" : "letaux"
if (rollData.arme.system.ignorearmure) {
rollData.ignoreDefenseArmor = true
}
defender.incDecSante(type, +degats, rollData.ignoreDefenseArmor)
ui.notifications.info(defender.name + "a subi " + degats + " points de santé " + type + ".")
} else {
ui.notifications.warn("Pas de cible sélectionnée ou pas d'arme de défense équipée.")
}
}
/* -------------------------------------------- */
static async bonusRollMournblade(rollData) {
rollData.bonusFormula = rollData.addedBonus
let bonusRoll = await new Roll(rollData.bonusFormula).evaluate()
await this.showDiceSoNice(bonusRoll, game.settings.get("core", "rollMode"));
rollData.bonusRoll = foundry.utils.duplicate(bonusRoll)
rollData.finalResult += rollData.bonusRoll.total
this.computeQualityResult(rollData)
this.createChatWithRollMode(rollData.alias, {
content: await foundry.applications.handlebars.renderTemplate(`systems/fvtt-mournblade/templates/chat-generic-result-v2.hbs`, rollData)
}, rollData)
}
/* -------------------------------------------- */
static getUsers(filter) {
return game.users.filter(filter).map(user => user._id);
}
/* -------------------------------------------- */
static getWhisperRecipients(rollMode, name) {
switch (rollMode) {
case "blindroll": return this.getUsers(user => user.isGM);
case "gmroll": return this.getWhisperRecipientsAndGMs(name);
case "selfroll": return [game.user.id];
}
return undefined;
}
/* -------------------------------------------- */
static getWhisperRecipientsAndGMs(name) {
let recep1 = ChatMessage.getWhisperRecipients(name) || [];
return recep1.concat(ChatMessage.getWhisperRecipients('GM'));
}
/* -------------------------------------------- */
static blindMessageToGM(chatOptions) {
let chatGM = foundry.utils.duplicate(chatOptions);
chatGM.whisper = this.getUsers(user => user.isGM);
chatGM.content = "Blinde message of " + game.user.name + "<br>" + chatOptions.content;
console.log("blindMessageToGM", chatGM);
game.socket.emit("system.fvtt-mournblade", { msg: "msg_gm_chat_message", data: chatGM });
}
/* -------------------------------------------- */
static async searchItem(dataItem) {
let item;
if (dataItem.pack) {
item = await fromUuid("Compendium." + dataItem.pack + "." + dataItem.id);
} else {
item = game.items.get(dataItem.id)
}
return item
}
/* -------------------------------------------- */
static split3Columns(data) {
let array = [[], [], []];
if (data == undefined) return array;
let col = 0;
for (let key in data) {
let keyword = data[key];
keyword.key = key; // Self-reference
array[col].push(keyword);
col++;
if (col == 3) col = 0;
}
return array;
}
/* -------------------------------------------- */
static async createChatMessage(name, rollMode, chatOptions, rollData = undefined) {
switch (rollMode) {
case "blindroll": // GM only
if (!game.user.isGM) {
this.blindMessageToGM(chatOptions);
chatOptions.whisper = [game.user.id];
chatOptions.content = "Message only to the GM";
}
else {
chatOptions.whisper = this.getUsers(user => user.isGM);
}
break;
default:
chatOptions.whisper = this.getWhisperRecipients(rollMode, name);
break;
}
chatOptions.alias = chatOptions.alias || name;
chatOptions.speaker = ChatMessage.getSpeaker();
let msg = await ChatMessage.create(chatOptions)
console.log("=======>", rollData)
msg.setFlag("world", "mournblade-roll", rollData)
}
/* -------------------------------------------- */
static getBasicRollData() {
let rollData = {
rollId: foundry.utils.randomID(16),
rollMode: game.settings.get("core", "rollMode"),
difficulte: 0,
modificateur: 0,
config: foundry.utils.duplicate(game.system.mournblade.config),
}
MournbladeUtility.updateWithTarget(rollData)
return rollData
}
/* -------------------------------------------- */
static updateWithTarget(rollData) {
let target = MournbladeUtility.getTarget()
if (target) {
rollData.defenderTokenId = target.id
let defender = game.canvas.tokens.get(rollData.defenderTokenId)?.actor
rollData.defenderCombatValues = defender.getCombatValues()
rollData.defender = defender.toObject() // Simpler
rollData.defenderDefense = defender.getBestDefenseValue()
rollData.armeDefense = defender.getBestDefenseValue()
if (rollData.armeDefense) {
rollData.difficulte = rollData.armeDefense.system.totalDefensif
} else {
ui.notifications.warn("Aucune arme de défense équipée, difficulté manuelle à positionner.")
}
}
}
/* -------------------------------------------- */
static createChatWithRollMode(name, chatOptions, rollData = undefined) {
this.createChatMessage(name, game.settings.get("core", "rollMode"), chatOptions, rollData)
}
/* -------------------------------------------- */
static applyBonneAventureRoll(li, changed, addedBonus) {
let msgId = $(li).data("message-id")
let msg = game.messages.get(msgId)
if (msg) {
let rollData = msg.getFlag("world", "mournblade-roll")
let actor = MournbladeUtility.getActorFromRollData(rollData)
actor.changeBonneAventure(changed)
rollData.isReroll = true
rollData.textBonus = "Bonus de Points d'Aventure"
if (addedBonus == "reroll") {
MournbladeUtility.rollMournblade(rollData)
} else {
rollData.addedBonus = addedBonus
MournbladeUtility.bonusRollMournblade(rollData)
}
}
}
/* -------------------------------------------- */
static applyEclatRoll(li, changed, addedBonus) {
let msgId = $(li).data("message-id")
let msg = game.messages.get(msgId)
if (msg) {
let rollData = msg.getFlag("world", "mournblade-roll")
let actor = MournbladeUtility.getActorFromRollData(rollData)
actor.changeEclat(changed)
rollData.isReroll = true
rollData.textBonus = "Bonus d'Eclat"
rollData.addedBonus = addedBonus
MournbladeUtility.bonusRollMournblade(rollData)
}
}
/* -------------------------------------------- */
static chatRollMenu(html, options) {
let canApply = li => canvas.tokens.controlled.length && li.find(".mournblade-roll").length
let canApplyBALoyal = function (li) {
let message = game.messages.get($(li).attr("data-message-id"))
let rollData = message.getFlag("world", "mournblade-roll")
let actor = MournbladeUtility.getActorFromRollData(rollData)
return (!rollData.isReroll && actor.getBonneAventure() > 0 && actor.getAlignement() == "loyal")
}
let canApplyPELoyal = function (li) {
let message = game.messages.get($(li).attr("data-message-id"))
let rollData = message.getFlag("world", "mournblade-roll")
let actor = MournbladeUtility.getActorFromRollData(rollData)
return (!rollData.isReroll && actor.getEclat() > 0 && actor.getAlignement() == "loyal")
}
let canApplyBAChaotique = function (li) {
let message = game.messages.get($(li).attr("data-message-id"))
let rollData = message.getFlag("world", "mournblade-roll")
let actor = MournbladeUtility.getActorFromRollData(rollData)
return (!rollData.isReroll && actor.getBonneAventure() > 0 && actor.getAlignement() == "chaotique")
}
let canApplyBAChaotique3 = function (li) {
let message = game.messages.get($(li).attr("data-message-id"))
let rollData = message.getFlag("world", "mournblade-roll")
let actor = MournbladeUtility.getActorFromRollData(rollData)
return (!rollData.isReroll && actor.getBonneAventure() > 2 && actor.getAlignement() == "chaotique")
}
let canApplyPEChaotique = function (li) {
let message = game.messages.get($(li).attr("data-message-id"))
let rollData = message.getFlag("world", "mournblade-roll")
let actor = MournbladeUtility.getActorFromRollData(rollData)
return (!rollData.isReroll && actor.getEclat() > 0 && actor.getAlignement() == "chaotique")
}
let hasPredilection = function (li) {
let message = game.messages.get($(li).attr("data-message-id"))
let rollData = message.getFlag("world", "mournblade-roll")
let actor = MournbladeUtility.getActorFromRollData(rollData)
if (rollData.competence) {
let nbPred = rollData.competence.system.predilections.filter(pred => !pred.used).length
return (!rollData.isReroll && rollData.competence && nbPred > 0)
}
return false
}
let canCompetenceDouble = function (li) {
let message = game.messages.get($(li).attr("data-message-id"))
let rollData = message.getFlag("world", "mournblade-roll")
let actor = MournbladeUtility.getActorFromRollData(rollData)
if (rollData.competence) {
return rollData.competence.system.doublebonus
}
return false
}
options.push(
{
name: "Ajouer +3 (1 point de Bonne Aventure)",
icon: "<i class='fas fa-user-plus'></i>",
condition: canApply && canApplyBALoyal,
callback: li => MournbladeUtility.applyBonneAventureRoll(li, -1, "+3")
}
)
options.push(
{
name: "Ajouer +6 (1 point de Bonne Aventure)",
icon: "<i class='fas fa-user-plus'></i>",
condition: canApply && canApplyBALoyal && canCompetenceDouble,
callback: li => MournbladeUtility.applyBonneAventureRoll(li, -1, "+6")
}
)
options.push(
{
name: "Ajouer +1d6 (1 point de Bonne Aventure)",
icon: "<i class='fas fa-user-plus'></i>",
condition: canApply && canApplyBAChaotique,
callback: li => MournbladeUtility.applyBonneAventureRoll(li, -1, "+1d6")
}
)
options.push(
{
name: "Ajouer +2d6 (1 point de Bonne Aventure)",
icon: "<i class='fas fa-user-plus'></i>",
condition: canApply && canApplyBAChaotique && canCompetenceDouble,
callback: li => MournbladeUtility.applyBonneAventureRoll(li, -1, "+2d6")
}
)
options.push(
{
name: "Relancer le dé (3 points de Bonne Aventure)",
icon: "<i class='fas fa-user-plus'></i>",
condition: canApply && canApplyBAChaotique3,
callback: li => MournbladeUtility.applyBonneAventureRoll(li, -3, "reroll")
}
)
options.push(
{
name: "Ajouter +10 (1 Point d'Eclat)",
icon: "<i class='fas fa-user-plus'></i>",
condition: canApply && canApplyPELoyal,
callback: li => MournbladeUtility.applyEclatRoll(li, -1, "+10")
}
)
options.push(
{
name: "Ajouter +20 (1 Point d'Eclat)",
icon: "<i class='fas fa-user-plus'></i>",
condition: canApply && canApplyPELoyal && canCompetenceDouble,
callback: li => MournbladeUtility.applyEclatRoll(li, -1, "+20")
}
)
return options
}
/* -------------------------------------------- */
static async confirmDelete(actorSheet, li) {
let itemId = li.dataset?.itemId || li.data("item-id");
let msgTxt = "<p>Voulez vous supprimer cet item ?";
let buttons = {
delete: {
icon: '<i class="fas fa-check"></i>',
label: "Oui !",
callback: () => {
actorSheet.actor.deleteEmbeddedDocuments("Item", [itemId]);
actorSheet.render(false);
}
},
cancel: {
icon: '<i class="fas fa-times"></i>',
label: "Non !"
}
}
msgTxt += "</p>";
let d = new Dialog({
title: "Confirmer la suppression",
content: msgTxt,
buttons: buttons,
default: "cancel"
});
d.render(true);
}
/* -------------------------------------------- */
/**
* Roll for potion preparation (blind mode — GM only sees result)
* @param {object} rollData
*/
static async rollPotion(rollData) {
if (!rollData.runeId) {
ui.notifications.warn("Aucune Rune sélectionnée pour la préparation de la potion.")
return
}
const actor = game.actors.get(rollData.actorId)
const pa = rollData.pointsAme ?? 1
const seuil = rollData.runeSeuil ?? 0
const difficulte = seuil + pa
const modificateur = rollData.modificateur ?? 0
rollData.difficulte = difficulte
const compNiveau = rollData.competence?.system?.niveau ?? 0
const compMod = compNiveau === 0 ? -3 : 0
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)
// Determine potion status
let potionStatut
let virulence = 0
let ameDeduct = pa
let potionCreated = false
if (rollData.isHeroique) {
potionStatut = "heroique"
} else if (rollData.isSuccess) {
potionStatut = "efficace"
} else if (rollData.isDramatique) {
potionStatut = "inconnue"
virulence = pa * 3
} else {
potionStatut = "inefficace"
ameDeduct = Math.ceil(pa / 2)
}
rollData.virulence = virulence
actor.subPointsAme("prononcer", ameDeduct)
// Calculate durations and prep time
const forme = rollData.forme ?? "liquide"
const isSolide = ["onguent", "cachets", "pilules"].includes(forme)
const dureeHeures = pa
const conservationMois = isSolide ? pa * 6 : pa
const tempsPrep = Math.max(1, Math.ceil(pa / 3))
rollData.dureePotion = dureeHeures === 1 ? "1 heure" : `${dureeHeures} heures`
rollData.conservationPotion = `${conservationMois} mois`
rollData.tempsPreparation = tempsPrep === 1 ? "1 heure" : `${tempsPrep} heures`
const formeLabels = { liquide: "Liquide", onguent: "Onguent", cachets: "Cachets", pilules: "Pilules" }
rollData.formeLabel = formeLabels[forme] ?? forme
if (potionStatut !== "inefficace") {
const potionItem = {
name: `Potion de ${rollData.runeName}`,
type: "potion",
img: rollData.runeImg || "systems/fvtt-mournblade/assets/icons/potion.webp",
system: {
rune: rollData.runeName,
runeImg: rollData.runeImg ?? "",
runeSeuil: seuil,
pointsAme: pa,
forme: forme,
statut: potionStatut,
virulence: virulence,
duree: rollData.dureePotion,
conservation: rollData.conservationPotion,
tempsPreparation: rollData.tempsPreparation,
}
}
await actor.createEmbeddedDocuments("Item", [potionItem])
potionCreated = true
}
rollData.potionCreated = potionCreated
rollData.isGM = game.user.isGM
this.createChatWithRollMode(rollData.alias, {
content: await foundry.applications.handlebars.renderTemplate(
`systems/fvtt-mournblade/templates/chat-potion-result.hbs`, rollData)
}, { ...rollData, rollMode: "blindroll" })
}
/* -------------------------------------------- */
static async rollInvocationElementaire(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.")
return
}
const soulCost = rollData.invocationSoulCost ?? rollData.invocationSeuil ?? 15
const bonusPacte = rollData.bonusPacte ?? 0
const compNiveau = rollData.competence?.system?.niveau ?? 0
const compMod = compNiveau === 0 ? -3 : 0
const modificateur = rollData.modificateur ?? 0
// Validate that the actor has enough soul to invoke
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}+${bonusPacte}+${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)
let ameDeduct = soulCost
let elementaireCreated = false
let createdActorId = null
let createdActorName = null
if (rollData.isSuccess || rollData.isHeroique) {
// Build elemental name for compendium lookup
const elementNames = { air: "d'Air", terre: "de Terre", feu: "de Feu", eau: "de l'Eau" }
const tierNames = { mineur: "Mineur", median: "Médian", majeur: "Majeur" }
const elemLabel = elementNames[rollData.invocationElement] ?? rollData.invocationElement
const tierLabel = tierNames[rollData.invocationTier] ?? rollData.invocationTier
const searchName = `Élémentaire ${elemLabel} ${tierLabel}`
// Import from compendium
const pack = game.packs.get("fvtt-mournblade.creatures-elementaires")
if (pack) {
const packIndex = await pack.getIndex()
const entry = packIndex.find(e => e.name === searchName)
if (entry) {
const doc = await pack.getDocument(entry._id)
if (doc) {
const createdActors = await Actor.createDocuments([doc.toObject()], { renderSheet: false })
const createdActor = createdActors[0]
if (createdActor) {
// Set elemental soul = soulCost invested by invoker
await createdActor.update({
"system.ame.fullmax": soulCost,
"system.ame.currentmax": soulCost,
"system.ame.value": 0,
})
createdActorId = createdActor.id
createdActorName = createdActor.name
elementaireCreated = true
// Soul blocked only on confirmed elemental creation
await actor.subPointsAme("prononcer", soulCost)
// Track invocation on personnage
const invocations = foundry.utils.duplicate(actor.system.invocationsElementaires || [])
invocations.push({
element: rollData.invocationElement,
tier: rollData.invocationTier,
soulCost,
actorId: createdActorId,
actorName: createdActorName,
})
await actor.update({ "system.invocationsElementaires": invocations })
}
}
} else {
ui.notifications.warn(`Élémentaire "${searchName}" introuvable dans le compendium creatures-elementaires.`)
}
} else {
ui.notifications.warn("Compendium creatures-elementaires introuvable.")
}
} else if (rollData.isDramatique) {
// All soul lost
actor.subPointsAme("prononcer", soulCost)
} else {
// Simple failure: half soul lost (round up)
ameDeduct = Math.ceil(soulCost / 2)
actor.subPointsAme("prononcer", ameDeduct)
}
rollData.invocationSoulDeducted = rollData.isSuccess || rollData.isHeroique ? soulCost : ameDeduct
rollData.elementaireCreated = elementaireCreated
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(
`systems/fvtt-mournblade/templates/chat-invocation-result.hbs`, rollData)
}, { ...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)
})
}
}