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
+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" })
}
}