Potions et élémentaires

This commit is contained in:
2026-05-02 08:26:28 +02:00
parent a234ba5d14
commit d6b5891519
248 changed files with 7020 additions and 350 deletions
@@ -0,0 +1,126 @@
import { MournbladeUtility } from "../mournblade-utility.js"
export default class MournbladeInvocationDialog {
static async create(actor, rollData) {
const ameDisponible = Math.max(0, (actor.system.ame.currentmax - actor.system.ame.value))
const maxExtra = Math.max(0, ameDisponible - 15)
// Detect elemental pacte bonus — the bonus is always available,
// but we display it dynamically based on chosen element.
// We pass the pactes to let JS listeners detect the match.
const pactes = actor.getPactes().map(p => ({
name: p.name,
allegeance: (p.system.allegeance || "").toLowerCase()
}))
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")
const context = {
...rollData,
img: actor.img,
name: actor.name,
ameDisponible,
ameExtraOptions: Array.from({ length: maxExtra + 1 }, (_, i) => i),
modOptions: Array.from({ length: 21 }, (_, i) => i - 10),
hautParlerNiveau: hautParlerComp ? hautParlerComp.system.niveau : null,
seigneursElemNiveau: seigneursElemComp ? seigneursElemComp.system.niveau : null,
bonusPacte: false,
pactes,
}
const content = await foundry.applications.handlebars.renderTemplate(
"systems/fvtt-mournblade/templates/dialog-invocation-elementaire.hbs",
context
)
Hooks.once("renderDialogV2", (_app, html) => {
const form = html.querySelector ? html : html[0]
MournbladeInvocationDialog._attachListeners(form, pactes)
})
return foundry.applications.api.DialogV2.wait({
window: { title: "Invoquer un Élémentaire", icon: "fa-solid fa-wind" },
classes: ["mournblade-roll-dialog"],
position: { width: 480 },
modal: false,
content,
buttons: [
{
action: "invoquer",
label: "Invoquer",
icon: "fa-solid fa-wind",
default: true,
callback: (event, button, dialog) => {
MournbladeInvocationDialog._updateRollData(rollData, button.form.elements, actor, pactes)
MournbladeUtility.rollInvocationElementaire(rollData)
}
},
],
rejectClose: false,
})
}
static _elementKeywords = {
air: ["air", "vent", "sylphe", "seigneur de l'air"],
terre: ["terre", "gnome", "seigneur de la terre"],
feu: ["feu", "flamme", "salamandre", "seigneur du feu"],
eau: ["eau", "ondine", "seigneur de l'eau"],
}
static _hasPacteBonus(element, pactes) {
const keywords = MournbladeInvocationDialog._elementKeywords[element] || []
return pactes.some(p => keywords.some(kw => p.allegeance.includes(kw)))
}
static _attachListeners(html, pactes) {
const tierSeuils = { mineur: 15, median: 20, majeur: 25 }
const tierTemps = { mineur: "1 tour", median: "1 minute", majeur: "1 heure" }
const recalculate = () => {
const element = html.querySelector('[name="element"]')?.value ?? "air"
const tier = html.querySelector('[name="tier"]')?.value ?? "mineur"
const ameExtra = parseInt(html.querySelector('[name="ameExtra"]')?.value ?? 0)
const seuilBase = tierSeuils[tier] ?? 15
const seuil = seuilBase + ameExtra
const hasPacte = MournbladeInvocationDialog._hasPacteBonus(element, pactes)
const seuilEl = html.querySelector('#invoc-seuil')
const coutEl = html.querySelector('#invoc-cout')
const tempsEl = html.querySelector('#invoc-temps')
const pacteBanner = html.querySelector('.invoc-bonus-pacte')
if (seuilEl) seuilEl.textContent = seuil + (hasPacte ? " (-5 avec bonus Pacte)" : "")
if (coutEl) coutEl.textContent = seuil
if (tempsEl) tempsEl.textContent = tierTemps[tier] ?? "1 tour"
if (pacteBanner) pacteBanner.style.display = hasPacte ? "" : "none"
}
html.querySelectorAll('[name="element"], [name="tier"], [name="ameExtra"]').forEach(el => {
el.addEventListener('change', recalculate)
})
recalculate()
}
static _updateRollData(rollData, formElements, actor, pactes) {
const element = formElements['element']?.value ?? "air"
const tier = formElements['tier']?.value ?? "mineur"
const ameExtra = parseInt(formElements['ameExtra']?.value ?? 0)
const modificateur = parseInt(formElements['modificateur']?.value ?? 0)
const tierSeuils = { mineur: 15, median: 20, majeur: 25 }
const seuilBase = tierSeuils[tier] ?? 15
const seuil = seuilBase + ameExtra
rollData.invocationElement = element
rollData.invocationTier = tier
rollData.invocationSeuil = seuil
rollData.invocationAmeExtra = ameExtra
rollData.invocationSoulCost = seuil
rollData.difficulte = seuil
rollData.modificateur = modificateur
rollData.bonusPacte = MournbladeInvocationDialog._hasPacteBonus(element, pactes) ? 5 : 0
}
}
+245 -1
View File
@@ -67,7 +67,248 @@ export class MournbladeRollDialog {
}
/**
* Mettre à jour rollData avec les valeurs du formulaire
* Create and display the sortilège (multi-rune spell) roll dialog
* @param {MournbladeActor} actor
* @param {Object} rollData
*/
static async createSortilege(actor, rollData) {
const ameDisponible = Math.max(1, (actor.system.ame.currentmax - actor.system.ame.value))
const context = {
...rollData,
img: actor.img,
name: actor.name,
config: game.system.mournblade.config,
runes: actor.getRunes(),
runemode: "prononcer",
ameDisponible,
ameOptions: Array.from({ length: ameDisponible }, (_, i) => i + 1),
modOptions: Array.from({ length: 21 }, (_, i) => i - 10),
}
const content = await foundry.applications.handlebars.renderTemplate(
"systems/fvtt-mournblade/templates/dialog-sortilege.hbs",
context
)
// Attach dynamic recalculation after render
Hooks.once("renderDialogV2", (_app, html) => {
const form = html.querySelector ? html : html[0]
MournbladeRollDialog._attachSortilegeListeners(form)
})
return foundry.applications.api.DialogV2.wait({
window: { title: "Lancer un Sortilège", icon: "fa-solid fa-star-of-david" },
classes: ["mournblade-roll-dialog"],
position: { width: 520 },
modal: false,
content,
buttons: [
{
action: "rolld10",
label: "Lancer 1d10",
icon: "fa-solid fa-dice-d10",
default: true,
callback: (event, button, dialog) => {
this._updateSortilegeRollData(rollData, button.form.elements, actor)
rollData.mainDice = "1d10"
MournbladeUtility.rollSortilege(rollData)
}
},
{
action: "rolld20",
label: "Lancer 1d20",
icon: "fa-solid fa-dice-d20",
callback: (event, button, dialog) => {
this._updateSortilegeRollData(rollData, button.form.elements, actor)
rollData.mainDice = "1d20"
MournbladeUtility.rollSortilege(rollData)
}
},
],
rejectClose: false,
})
}
/**
* Open the potion preparation dialog
* @param {MournbladeActor} actor
* @param {object} rollData
*/
static async createPotion(actor, rollData) {
const ameDisponible = Math.max(1, (actor.system.ame.currentmax - actor.system.ame.value))
const context = {
...rollData,
img: actor.img,
name: actor.name,
config: game.system.mournblade.config,
runes: actor.getRunes(),
ameDisponible,
ameOptions: Array.from({ length: ameDisponible }, (_, i) => i + 1),
modOptions: Array.from({ length: 21 }, (_, i) => i - 10),
}
const content = await foundry.applications.handlebars.renderTemplate(
"systems/fvtt-mournblade/templates/dialog-potion.hbs",
context
)
Hooks.once("renderDialogV2", (_app, html) => {
const form = html.querySelector ? html : html[0]
MournbladeRollDialog._attachPotionListeners(form)
})
return foundry.applications.api.DialogV2.wait({
window: { title: "Préparer une Potion", icon: "fa-solid fa-flask" },
classes: ["mournblade-roll-dialog"],
position: { width: 520 },
modal: false,
content,
buttons: [
{
action: "preparer",
label: "Préparer",
icon: "fa-solid fa-flask",
default: true,
callback: (event, button, dialog) => {
MournbladeRollDialog._updatePotionRollData(rollData, button.form.elements, actor)
MournbladeUtility.rollPotion(rollData)
}
},
],
rejectClose: false,
})
}
/**
* Attach dynamic recalculation listeners to the potion dialog
* @param {HTMLElement} html
*/
static _attachPotionListeners(html) {
const recalculate = () => {
const radioChecked = html.querySelector('.potion-rune-radio:checked')
const runeRow = radioChecked?.closest('[data-seuil]')
const seuil = parseInt(runeRow?.dataset.seuil ?? 0)
const pa = parseInt(html.querySelector('[name="pointsAme"]')?.value ?? 1)
const mod = parseInt(html.querySelector('[name="modificateur"]')?.value ?? 0)
const difficulteEl = html.querySelector('#potion-difficulte')
if (difficulteEl) difficulteEl.textContent = (seuil + pa) + (mod !== 0 ? ` (mod ${mod > 0 ? '+' : ''}${mod})` : '')
const tempsEl = html.querySelector('#potion-temps')
if (tempsEl) {
const heures = Math.max(1, Math.ceil(pa / 3))
tempsEl.textContent = heures === 1 ? '1 heure' : `${heures} heures`
}
}
html.querySelectorAll('.potion-rune-radio, [name="pointsAme"]').forEach(el => {
el.addEventListener('change', recalculate)
})
recalculate()
}
/**
* Update rollData from the potion dialog form elements
* @param {object} rollData
* @param {HTMLFormControlsCollection} formElements
* @param {MournbladeActor} actor
*/
static _updatePotionRollData(rollData, formElements, actor) {
const runeId = formElements['rune-selected']?.value
if (runeId) {
const rune = actor.getRunes().find(r => r._id === runeId)
if (rune) {
rollData.runeId = runeId
rollData.runeName = rune.name
rollData.runeImg = rune.img
rollData.runeSeuil = rune.system.seuil ?? 0
rollData.runeHautParler = rune.system.hautParler ?? ""
}
}
rollData.pointsAme = parseInt(formElements['pointsAme']?.value ?? 1)
rollData.forme = formElements['forme']?.value ?? "liquide"
rollData.modificateur = parseInt(formElements['modificateur']?.value ?? 0)
}
/**
* Attach dynamic recalculation listeners to the sortilège dialog
* @param {HTMLElement} html
*/
static _attachSortilegeListeners(html) {
const recalculate = () => {
const checkboxes = html.querySelectorAll('.sortilege-rune-checkbox:checked')
const modeSelect = html.querySelector('[name="runemode"]')
const mode = modeSelect?.value ?? "prononcer"
let maxSeuil = 0
let totalAme = 0
let totalActions = 0
let count = 0
// Enable/disable points inputs based on checkbox state
html.querySelectorAll('.sortilege-rune-checkbox').forEach(cb => {
const row = cb.closest('.sortilege-rune-row')
const pointsInput = row?.querySelector('.sortilege-rune-points')
if (pointsInput) pointsInput.disabled = !cb.checked
})
checkboxes.forEach(cb => {
const row = cb.closest('.sortilege-rune-row')
const seuil = Number(row?.dataset.seuil ?? 0)
const pts = Number(row?.querySelector('.sortilege-rune-points')?.value ?? 1)
if (seuil > maxSeuil) maxSeuil = seuil
totalAme += pts
totalActions += Math.ceil(pts / 3) * (mode === "inscrire" ? 2 : 1)
count++
})
const difficulte = count > 0 ? maxSeuil + (count - 1) : 0
html.querySelector('#sortilege-difficulte').textContent = count > 0 ? difficulte : '—'
html.querySelector('#sortilege-total-ame').textContent = totalAme
html.querySelector('#sortilege-actions').textContent = totalActions
}
html.querySelectorAll('.sortilege-rune-checkbox, .sortilege-rune-points, [name="runemode"]')
.forEach(el => el.addEventListener('change', recalculate))
}
/**
* Extract sortilège data from the form
* @param {Object} rollData
* @param {HTMLFormControlsCollection} formElements
* @param {MournbladeActor} actor
*/
static _updateSortilegeRollData(rollData, formElements, actor) {
rollData.runemode = formElements.runemode?.value ?? "prononcer"
rollData.modificateur = Number(formElements.modificateur?.value ?? 0)
rollData.runeautocible = formElements.runeautocible?.checked ?? false
// Collect selected runes with their soul points
const runes = actor.getRunes()
rollData.sortilegeRunes = []
for (const rune of runes) {
const checkbox = formElements[`rune-selected-${rune._id}`]
if (checkbox?.checked) {
const pts = Math.max(1, Number(formElements[`rune-points-${rune._id}`]?.value ?? 1))
rollData.sortilegeRunes.push({
id: rune._id,
name: rune.name,
img: rune.img,
seuil: rune.system.seuil,
formule: rune.system.formule,
pts,
})
}
}
if (rollData.sortilegeRunes.length === 0) return
const maxSeuil = Math.max(...rollData.sortilegeRunes.map(r => r.seuil))
rollData.difficulte = maxSeuil + (rollData.sortilegeRunes.length - 1)
rollData.runeame = rollData.sortilegeRunes.reduce((s, r) => s + r.pts, 0)
}
/**
* @param {Object} rollData - L'objet rollData à mettre à jour
* @param {HTMLFormControlsCollection} formElements - Les éléments du formulaire
* @param {MournbladeActor} actor - L'acteur pour récupérer les attributs
@@ -98,6 +339,9 @@ export class MournbladeRollDialog {
if (formElements.runeame) {
rollData.runeame = Number(formElements.runeame.value)
}
if (formElements.runeautocible !== undefined) {
rollData.runeautocible = formElements.runeautocible.checked
}
// Combat mêlée
if (formElements.typeAttaque) {
+1
View File
@@ -24,6 +24,7 @@ export { default as MournbladePacteSheet } from './mournblade-pacte-sheet.mjs';
export { default as MournbladeProtectionSheet } from './mournblade-protection-sheet.mjs';
export { default as MournbladeRuneSheet } from './mournblade-rune-sheet.mjs';
export { default as MournbladeRuneEffectSheet } from './mournblade-runeeffect-sheet.mjs';
export { default as MournbladePotionSheet } from './mournblade-potion-sheet.mjs';
export { default as MournbladeTendanceSheet } from './mournblade-tendance-sheet.mjs';
export { default as MournbladeTraitChaotiqueSheet } from './mournblade-traitchaotique-sheet.mjs';
export { default as MournbladeTraitEspeceSheet } from './mournblade-traitespece-sheet.mjs';
@@ -45,6 +45,7 @@ export default class MournbladeActorSheet extends HandlebarsApplicationMixin(fou
editItem: MournbladeActorSheet.#onEditItem,
deleteItem: MournbladeActorSheet.#onDeleteItem,
createItem: MournbladeActorSheet.#onCreateItem,
postItem: MournbladeActorSheet.#onPostItem,
equipItem: MournbladeActorSheet.#onEquipItem,
modifyQuantity: MournbladeActorSheet.#onModifyQuantity,
modifySante: MournbladeActorSheet.#onModifySante,
@@ -52,6 +53,10 @@ export default class MournbladeActorSheet extends HandlebarsApplicationMixin(fou
rollAttribut: MournbladeActorSheet.#onRollAttribut,
rollCompetence: MournbladeActorSheet.#onRollCompetence,
rollRune: MournbladeActorSheet.#onRollRune,
rollSortilege: MournbladeActorSheet.#onRollSortilege,
preparePotion: MournbladeActorSheet.#onPreparePotion,
invoquerElementaire: MournbladeActorSheet.#onInvoquerElementaire,
bannirElementaire: MournbladeActorSheet.#onBannirElementaire,
rollArmeOffensif: MournbladeActorSheet.#onRollArmeOffensif,
rollArmeSpecial: MournbladeActorSheet.#onRollArmeSpecial,
rollArmeDegats: MournbladeActorSheet.#onRollArmeDegats,
@@ -238,6 +243,16 @@ export default class MournbladeActorSheet extends HandlebarsApplicationMixin(fou
await this.actor.createEmbeddedDocuments("Item", [{ name: `Nouveau ${itemType}`, type: itemType }], { renderSheet: true })
}
/**
* Handle posting an item to chat
*/
static async #onPostItem(event, target) {
const li = target.closest(".item")
const itemId = li?.dataset.itemId ?? target.dataset.itemId
if (!itemId) return
MournbladeUtility.postItemToChat(this.actor.id, itemId)
}
/**
* Handle equipping an item
* @param {Event} event - The triggering event
@@ -335,6 +350,39 @@ export default class MournbladeActorSheet extends HandlebarsApplicationMixin(fou
await actor.rollRune(runeId)
}
/**
* Handle launching a sortilège (multi-rune spell)
*/
static async #onRollSortilege(event, target) {
event.preventDefault()
await this.document.rollSortilege()
}
/**
* Handle preparing a potion
*/
static async #onPreparePotion(event, target) {
event.preventDefault()
await this.document.preparePotion()
}
/**
* Handle invoking an elemental
*/
static async #onInvoquerElementaire(event, target) {
event.preventDefault()
await this.document.invoquerElementaire()
}
/**
* Handle banishing an elemental invocation
*/
static async #onBannirElementaire(event, target) {
event.preventDefault()
const invocIndex = parseInt(target.dataset.invocIndex ?? "0")
await this.document.bannirElementaire(invocIndex)
}
/**
* Handle rolling an arme offensif
* @param {Event} event - The triggering event
@@ -122,21 +122,37 @@ export default class MournbladeItemSheet extends HandlebarsApplicationMixin(foun
if (this.document.actor) {
chatData.actor = { id: this.document.actor.id }
}
// Don't post any image for the item if the default image is used
if (chatData.img.includes("/blank.png")) {
if (chatData.img?.includes("/blank.png")) {
chatData.img = null
}
// JSON object for easy creation
chatData.jsondata = JSON.stringify({
compendium: "postedItem",
payload: chatData,
})
const html = await foundry.applications.handlebars.renderTemplate('systems/fvtt-mournblade/templates/post-item.hbs', chatData)
const chatOptions = {
user: game.user.id,
content: html,
// Localized type label
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"
}
ChatMessage.create(chatOptions)
chatData.typeLabel = typeLabels[chatData.type] ?? chatData.type
// Type icon for the badge
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"
}
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 })
}
}
@@ -32,6 +32,7 @@ export default class MournbladeCreatureSheet extends MournbladeActorSheet {
context.skills = actor.getSkills()
context.armes = foundry.utils.duplicate(actor.getWeapons())
context.protections = foundry.utils.duplicate(actor.getArmors())
context.capacites = foundry.utils.duplicate(actor.getCapacites())
context.runes = foundry.utils.duplicate(actor.getRunes())
context.combat = actor.getCombatValues()
context.equipements = foundry.utils.duplicate(actor.getEquipments())
@@ -0,0 +1,47 @@
import MournbladeItemSheet from "./base-item-sheet.mjs"
export default class MournbladePotionSheet extends MournbladeItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["potion"],
position: {
width: 640,
},
window: {
contentClasses: ["potion-content"],
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-mournblade/templates/item-potion-sheet.hbs",
},
}
/** @override */
tabGroups = {
primary: "details",
}
#getTabs() {
const tabs = {
details: { id: "details", group: "primary", label: "Détails" },
effets: { id: "effets", group: "primary", label: "Effets" },
description: { id: "description", group: "primary", label: "Description" },
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
context.config = game.system.mournblade.config
return context
}
}
+1
View File
@@ -5,6 +5,7 @@ export default class CapaciteDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
typeCapacite: new fields.StringField({ initial: "creature", blank: false }),
description: new fields.HTMLField({ initial: "" })
};
}
+2
View File
@@ -10,6 +10,8 @@ export default class CreatureDataModel extends foundry.abstract.TypeDataModel {
name: new fields.StringField({ initial: "" }),
age: new fields.NumberField({ initial: 0, integer: true }),
alignement: new fields.StringField({ initial: "" }),
creatureType: new fields.StringField({ initial: "creature" }),
elementType: new fields.StringField({ initial: "" }),
poids: new fields.StringField({ initial: "" }),
taille: new fields.StringField({ initial: "" }),
cheveux: new fields.StringField({ initial: "" }),
+1
View File
@@ -19,6 +19,7 @@ export { default as PacteDataModel } from './pacte.mjs';
export { default as ProtectionDataModel } from './protection.mjs';
export { default as RuneDataModel } from './rune.mjs';
export { default as RuneEffectDataModel } from './runeeffect.mjs';
export { default as PotionDataModel } from './potion.mjs';
export { default as TendanceDataModel } from './tendance.mjs';
export { default as TraitChaotiqueDataModel } from './traitchaotique.mjs';
export { default as TraitEspeceDataModel } from './traitespece.mjs';
+1
View File
@@ -80,6 +80,7 @@ export default class PersonnageDataModel extends foundry.abstract.TypeDataModel
value: new fields.NumberField({ initial: 0, integer: true }),
traumatismes: new fields.StringField({ initial: "" })
}),
invocationsElementaires: 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 }),
+24
View File
@@ -0,0 +1,24 @@
/**
* Data model pour les potions à base de runes
*/
export default class PotionDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
description: new fields.HTMLField({ initial: "" }),
effetCuratif: new fields.HTMLField({ initial: "" }),
effetLetal: new fields.HTMLField({ initial: "" }),
effetSecondaire: new fields.HTMLField({ initial: "" }),
rune: new fields.StringField({ initial: "" }),
runeImg: new fields.StringField({ initial: "" }),
runeSeuil: new fields.NumberField({ initial: 0, integer: true }),
pointsAme: new fields.NumberField({ initial: 1, integer: true, min: 1 }),
forme: new fields.StringField({ initial: "liquide" }),
statut: new fields.StringField({ initial: "inconnue" }),
virulence: new fields.NumberField({ initial: 0, integer: true }),
duree: new fields.StringField({ initial: "" }),
conservation: new fields.StringField({ initial: "" }),
tempsPreparation: new fields.StringField({ initial: "" }),
};
}
}
+90 -1
View File
@@ -1,6 +1,7 @@
/* -------------------------------------------- */
import { MournbladeUtility } from "./mournblade-utility.js";
import { MournbladeRollDialog } from "./applications/mournblade-roll-dialog.mjs";
import MournbladeInvocationDialog from "./applications/mournblade-invocation-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]
@@ -215,6 +216,9 @@ export class MournbladeActor extends Actor {
getArmors() {
return this.getItemSorted(["protection"])
}
getCapacites() {
return this.getItemSorted(["capacite"])
}
getRuneEffects() {
return this.getItemSorted(["runeeffect"])
}
@@ -455,7 +459,7 @@ export class MournbladeActor extends Actor {
} else {
ame.currentmax -= value
}
this.update({ 'system.ame': ame })
return this.update({ 'system.ame': ame })
}
/* -------------------------------------------- */
@@ -650,6 +654,91 @@ export class MournbladeActor extends Actor {
await MournbladeRollDialog.create(this, rollData)
}
/* -------------------------------------------- */
async rollSortilege() {
const runes = this.getRunes()
if (runes.length < 2) {
ui.notifications.warn("Il faut au moins deux Runes pour lancer un Sortilège.")
return
}
const comp = this.items.find(c => c.type == "competence" && c.name.toLowerCase() == "savoir : runes")
if (!comp) {
ui.notifications.warn("La compétence Savoir : Runes n'a pas été trouvée, abandon.")
return
}
let rollData = this.getCommonRollData("cla", undefined, "Savoir : Runes")
rollData.isSortilege = true
rollData.difficulte = 0
rollData.runemode = "prononcer"
rollData.runeame = 0
await MournbladeRollDialog.createSortilege(this, rollData)
}
/* -------------------------------------------- */
async preparePotion() {
const runes = this.getRunes()
if (runes.length === 0) {
ui.notifications.warn("Aucune Rune disponible pour préparer une potion.")
return
}
const comp = this.items.find(c => c.type == "competence" && c.name.toLowerCase() == "savoir : runes")
if (!comp) {
ui.notifications.warn("La compétence Savoir : Runes n'a pas été trouvée, abandon.")
return
}
let rollData = this.getCommonRollData("cla", comp._id)
rollData.isPotion = true
rollData.mainDice = "1d10"
// Optionally cap by Savoir:Haut-Parler and Savoir:Alchimie
const hautParlerComp = this.items.find(c => c.type == "competence" && c.name.toLowerCase() == "savoir : haut-parler")
const alchimieComp = this.items.find(c => c.type == "competence" && c.name.toLowerCase() == "savoir : alchimie")
if (hautParlerComp) rollData.limitHautParlerValue = hautParlerComp.system.niveau
if (alchimieComp) rollData.limitAlchimieValue = alchimieComp.system.niveau
await MournbladeRollDialog.createPotion(this, rollData)
}
/* -------------------------------------------- */
async invoquerElementaire() {
const persuasionComp = this.items.find(c => c.type == "competence" && c.name.toLowerCase() == "persuasion")
if (!persuasionComp) {
ui.notifications.warn("La compétence Persuasion est requise pour invoquer un Élémentaire.")
return
}
let rollData = this.getCommonRollData("pre", persuasionComp._id)
rollData.isInvocationElementaire = true
rollData.mainDice = "1d10"
const hautParlerComp = this.items.find(c => c.type == "competence" && c.name.toLowerCase() == "savoir : haut-parler")
const seigneursElemComp = this.items.find(c => c.type == "competence" && c.name.toLowerCase() == "savoir : seigneurs élémentaires")
if (hautParlerComp) rollData.hautParlerNiveau = hautParlerComp.system.niveau
if (seigneursElemComp) rollData.seigneursElemNiveau = seigneursElemComp.system.niveau
await MournbladeInvocationDialog.create(this, rollData)
}
/* -------------------------------------------- */
async bannirElementaire(invocIndex) {
const invocations = foundry.utils.duplicate(this.system.invocationsElementaires || [])
const invoc = invocations[invocIndex]
if (!invoc) return
// Free the blocked soul
await this.subPointsAme("prononcer", -invoc.soulCost)
// Delete the created actor if it still exists
if (invoc.actorId) {
const createdActor = game.actors.get(invoc.actorId)
if (createdActor) await createdActor.delete()
}
// Remove from the list
invocations.splice(invocIndex, 1)
await this.update({ "system.invocationsElementaires": invocations })
ui.notifications.info(`L'Élémentaire ${invoc.actorName} a été banni. ${invoc.soulCost} points d'Âme libérés.`)
}
/* -------------------------------------------- */
async rollArmeOffensif(armeId) {
let arme = this.items.get(armeId)
+30
View File
@@ -72,6 +72,36 @@ export class MournbladeConfig {
loi: game.i18n.localize("MNBL.law"),
betes: game.i18n.localize("MNBL.beastslords"),
elementaires: game.i18n.localize("MNBL.elementslords")
},
potionFormeOptions: {
liquide: game.i18n.localize("MNBL.potionLiquide"),
onguent: game.i18n.localize("MNBL.potionOnguent"),
cachets: game.i18n.localize("MNBL.potionCachets"),
pilules: game.i18n.localize("MNBL.potionPilules"),
},
potionStatutOptions: {
inconnue: game.i18n.localize("MNBL.potionInconnue"),
efficace: game.i18n.localize("MNBL.potionEfficace"),
heroique: game.i18n.localize("MNBL.potionHeroique"),
inefficace: game.i18n.localize("MNBL.potionInnefficace"),
poison: game.i18n.localize("MNBL.potionPoison"),
},
typeCapaciteOptions: {
elue: game.i18n.localize("MNBL.typeCapaciteElue"),
elementaire: game.i18n.localize("MNBL.typeCapaciteElementaire"),
demoniaque: game.i18n.localize("MNBL.typeCapaciteDemoniaque"),
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"),
},
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"),
}
}
+16 -14
View File
@@ -1,20 +1,22 @@
import { MournbladeUtility } from "./mournblade-utility.js";
export const defaultItemImg = {
competence: "systems/fvtt-mournblade/assets/icons/competence.webp",
arme: "systems/fvtt-mournblade/assets/icons/arme.webp",
capacite: "systems/fvtt-mournblade/assets/icons/capacite.webp",
don: "systems/fvtt-mournblade/assets/icons/don.webp",
equipement: "systems/fvtt-mournblade/assets/icons/equipement.webp",
monnaie: "systems/fvtt-mournblade/assets/icons/monnaie.webp",
pacte: "systems/fvtt-mournblade/assets/icons/pacte.webp",
predilection: "systems/fvtt-mournblade/assets/icons/predilection.webp",
protection: "systems/fvtt-mournblade/assets/icons/protection.webp",
rune: "systems/fvtt-mournblade/assets/icons/rune.webp",
runeeffect: "systems/fvtt-mournblade/assets/icons/rune.webp",
tendance: "systems/fvtt-mournblade/assets/icons/tendance.webp",
traitchaotique: "systems/fvtt-mournblade/assets/icons/traitchaotique.webp",
traitespece: "systems/fvtt-mournblade/assets/icons/capacite.webp"
competence: "systems/fvtt-mournblade/assets/icons/competence.webp",
arme: "systems/fvtt-mournblade/assets/icons/arme.webp",
capacite: "systems/fvtt-mournblade/assets/icons/capacite.webp",
don: "systems/fvtt-mournblade/assets/icons/don.webp",
equipement: "systems/fvtt-mournblade/assets/icons/equipement.webp",
monnaie: "systems/fvtt-mournblade/assets/icons/monnaie.webp",
pacte: "systems/fvtt-mournblade/assets/icons/pacte.webp",
predilection: "systems/fvtt-mournblade/assets/icons/predilection.webp",
protection: "systems/fvtt-mournblade/assets/icons/protection.webp",
rune: "systems/fvtt-mournblade/assets/icons/rune.webp",
runeeffect: "systems/fvtt-mournblade/assets/icons/rune.webp",
potion: "systems/fvtt-mournblade/assets/icons/potion.webp",
tendance: "systems/fvtt-mournblade/assets/icons/tendance.webp",
traitchaotique: "systems/fvtt-mournblade/assets/icons/traitchaotique.webp",
traitespece: "systems/fvtt-mournblade/assets/icons/capacite.webp",
potion: "systems/fvtt-mournblade/assets/icons/potion.svg",
}
/**
+2
View File
@@ -74,6 +74,7 @@ Hooks.once("init", async function () {
protection: models.ProtectionDataModel,
rune: models.RuneDataModel,
runeeffect: models.RuneEffectDataModel,
potion: models.PotionDataModel,
tendance: models.TendanceDataModel,
traitchaotique: models.TraitChaotiqueDataModel,
traitespece: models.TraitEspeceDataModel
@@ -108,6 +109,7 @@ Hooks.once("init", async function () {
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeProtectionSheet, { types: ["protection"], makeDefault: true });
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeRuneSheet, { types: ["rune"], makeDefault: true });
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeRuneEffectSheet, { types: ["runeeffect"], makeDefault: true });
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladePotionSheet, { types: ["potion"], makeDefault: true });
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeTendanceSheet, { types: ["tendance"], makeDefault: true });
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeTraitChaotiqueSheet, { types: ["traitchaotique"], makeDefault: true });
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeTraitEspeceSheet, { types: ["traitespece"], makeDefault: true });
+394 -12
View File
@@ -127,6 +127,12 @@ export class MournbladeUtility {
/* -------------------------------------------- */
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)
@@ -157,6 +163,52 @@ export class MournbladeUtility {
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 })
}
/* -------------------------------------------- */
@@ -166,7 +218,9 @@ export class MournbladeUtility {
'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-nav.hbs',
'systems/fvtt-mournblade/templates/dialog-invocation-elementaire.hbs',
'systems/fvtt-mournblade/templates/chat-invocation-result.hbs',
]
return foundry.applications.handlebars.loadTemplates(templatePaths);
}
@@ -391,12 +445,16 @@ export class MournbladeUtility {
}
if (rollData.rune) {
rollData.runeduree = Math.ceil((rollData.runeame + 3) / 3)
const actionsBase = Math.ceil(rollData.runeame / 3)
rollData.runeActionsComplexes = (rollData.runemode == "inscrire") ? actionsBase * 2 : actionsBase
if (rollData.runemode == "inscrire") {
rollData.runeduree *= 2
}
if (rollData.runemode == "prononcer") {
rollData.runeduree = 1
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`
}
}
@@ -412,17 +470,40 @@ export class MournbladeUtility {
// Application immédiate selon type de jet
if (rollData.rune) {
let subAme = rollData.runeame
if (rollData.isEchec && !rollData.isDramatique) {
// 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"
const dureeLabel = rollData.runeduree === 1
? `${rollData.runeduree} tour`
: `${rollData.runeduree} tours`
await actor.createEmbeddedDocuments("Item", [{
name: rollData.rune.name,
type: "runeeffect",
@@ -430,7 +511,7 @@ export class MournbladeUtility {
system: {
rune: rollData.rune.name,
mode: effetMode,
duree: dureeLabel,
duree: rollData.dureeLabel,
pointame: rollData.runeame
}
}])
@@ -455,6 +536,99 @@ export class MournbladeUtility {
}
/* -------------------------------------------- */
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
@@ -894,4 +1068,212 @@ export class MournbladeUtility {
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 = rollData.tokenId
? game.canvas.tokens.get(rollData.tokenId)?.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
this.createChatWithRollMode(rollData.alias, {
content: await foundry.applications.handlebars.renderTemplate(
`systems/fvtt-mournblade/templates/chat-invocation-result.hbs`, rollData)
}, { ...rollData, rollMode: "blindroll" })
}
}