Files

648 lines
27 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Célestopol 1922 — Système FoundryVTT
*
* Célestopol 1922 est un jeu de rôle édité par Antre-Monde Éditions.
* Ce système FoundryVTT est une implémentation indépendante et n'est pas
* affilié à Antre-Monde Éditions,
* mais a été réalisé avec l'autorisation d'Antre-Monde Éditions.
*
* @author LeRatierBretonnien
* @copyright 20252026 LeRatierBretonnien
* @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
*/
import { SYSTEM } from "../config/system.mjs"
/** Construit la formule de jet à partir du nombre de dés et du modificateur total. */
function buildFormula(nbDice, totalModifier) {
if (totalModifier > 0) return `${nbDice}d8 + ${totalModifier}`
if (totalModifier < 0) return `${nbDice}d8 - ${Math.abs(totalModifier)}`
return `${nbDice}d8`
}
/**
* Système de dés de Célestopol 1922.
*
* Formule de base : 2d8 + Spécialisation + modificateurs (aspect, blessures, manual)
* Destin (jauge pleine à 8) : 3d8 + modificateurs, puis reset de la jauge à 0
* Dé de la Lune (optionnel) : 1d8 séparé → résultat narratif (Triomphe/Brio/Contrecoup/Catastrophe)
*/
export class CelestopolRoll extends Roll {
static CHAT_TEMPLATE = "systems/fvtt-celestopol/templates/chat-message.hbs"
get resultType() { return this.options.resultType }
get isSuccess() { return this.resultType === "success" || this.resultType === "critical-success" }
get isFailure() { return this.resultType === "failure" || this.resultType === "critical-failure" }
get isTie() { return this.resultType === "tie" }
get isCriticalSuccess(){ return this.resultType === "critical-success" }
get isCriticalFailure(){ return this.resultType === "critical-failure" }
get actorId() { return this.options.actorId }
get actorName() { return this.options.actorName }
get actorImage() { return this.options.actorImage }
get skillLabel() { return this.options.skillLabel }
get difficulty() { return this.options.difficulty }
/**
* Convertit le niveau de dégâts d'une arme en nombre de blessures de base.
* Règle : une attaque réussie inflige toujours 1 blessure, plus le bonus de dégâts.
* @param {string|number|null} weaponDegats
* @returns {number|null}
*/
static getIncomingWounds(weaponDegats) {
const raw = `${weaponDegats ?? "0"}`
const bonus = Number.parseInt(raw, 10)
if (!Number.isFinite(bonus)) return null
return Math.max(0, 1 + bonus)
}
/**
* Retourne la protection totale de l'armure équipée pour un acteur.
* @param {Actor|null} actor
* @returns {number}
*/
static getActorArmorProtection(actor) {
if (!actor) return 0
if (typeof actor.system?.getArmorMalus === "function") {
return Math.abs(actor.system.getArmorMalus())
}
const derivedArmorMalus = actor.system?.armorMalus
if (Number.isFinite(derivedArmorMalus)) {
return Math.abs(derivedArmorMalus)
}
const armures = actor.itemTypes?.armure ?? []
return armures
.filter(a => a.system.equipped)
.reduce((sum, a) => sum + Math.abs(a.system.protection ?? a.system.malus ?? 0), 0)
}
/**
* Ouvre le dialogue de configuration du jet via DialogV2 et exécute le jet.
* @param {object} options
* @returns {Promise<CelestopolRoll|null>}
*/
static async prompt(options = {}) {
const woundMalus = options.woundMalus ?? 0
const armorMalus = options.armorMalus ?? 0
const skillValue = options.skillValue ?? 0
const woundLevelId = options.woundLevel ?? 0
const destGaugeFull = options.destGaugeFull ?? false
const fortuneValue = options.fortuneValue ?? 0
const isResistance = options.isResistance ?? false
const isCombat = options.isCombat ?? false
const isRangedDefense = options.isRangedDefense ?? false
const weaponType = options.weaponType ?? "melee"
const weaponName = options.weaponName ?? null
const weaponDegats = options.weaponDegats ?? "0"
const availableTargets = options.availableTargets ?? []
const isRangedAttack = isCombat && !isRangedDefense && weaponType === "distance"
const woundLabel = woundLevelId > 0
? game.i18n.localize(SYSTEM.WOUND_LEVELS[woundLevelId]?.label ?? "")
: null
const modifierChoices = SYSTEM.CONTEXT_MODIFIER_CHOICES.map(c => ({
id: c.id,
value: c.value,
label: game.i18n.localize(c.label),
}))
const aspectChoices = [-4, -3, -2, -1, 0, 1, 2, 3, 4].map(v => ({
value: v,
label: v > 0 ? `+${v}` : `${v}`,
}))
const situationChoices = Array.from({ length: 17 }, (_, i) => {
const v = i - 8
return { value: v, label: v > 0 ? `+${v}` : `${v}` }
})
const rangedModChoices = SYSTEM.RANGED_MODIFIERS.map(m => ({
id: m.id,
value: m.value,
label: game.i18n.localize(m.label),
}))
const factionAspectChoices = game.celestopol?.getFactionAspectSummary(options.actorId ? game.actors.get(options.actorId) : null)
?.availableAspectChoices ?? []
const dialogContext = {
actorName: options.actorName,
statLabel: options.statLabel,
skillLabel: options.skillLabel,
skillValue,
woundMalus,
woundLabel,
isCombat,
isRangedDefense,
isRangedAttack,
weaponType,
weaponName,
weaponDegats,
modifierChoices,
aspectChoices,
situationChoices,
rangedModChoices,
factionAspectChoices,
availableTargets,
fortuneValue,
armorMalus,
destGaugeFull,
defaultRollMoonDie: options.rollMoonDie ?? false,
isResistance,
}
const content = await foundry.applications.handlebars.renderTemplate(
"systems/fvtt-celestopol/templates/roll-dialog.hbs",
dialogContext
)
const skillLocalized = game.i18n.localize(options.skillLabel ?? "")
const statLocalized = options.statLabel ? game.i18n.localize(options.statLabel) : null
const title = statLocalized ? `${statLocalized} ${skillLocalized}` : skillLocalized
const rollContext = await foundry.applications.api.DialogV2.wait({
window: { title },
classes: ["fvtt-celestopol", "roll-dialog"],
content,
render: (_event, dialog) => {
const wrap = dialog.element.querySelector('.roll-dialog-content')
if (!wrap) return
// Sélection de cible PNJ : masque le champ Corps PNJ (valeur cachée)
const targetSelect = wrap.querySelector('#targetSelect')
const corpsPnjRow = wrap.querySelector('#corps-pnj-row')
const targetConfirmedRow = wrap.querySelector('#target-confirmed-row')
const targetConfirmedName = wrap.querySelector('#target-confirmed-name')
function applyTargetSelection() {
if (!targetSelect) return
const selectedOption = targetSelect.options[targetSelect.selectedIndex]
const val = parseFloat(selectedOption?.dataset.corps ?? "")
const corpsPnjInput = wrap.querySelector('#corpsPnj')
if (targetSelect.value && !isNaN(val)) {
// Cible sélectionnée : masquer la valeur, afficher le nom
if (corpsPnjRow) corpsPnjRow.style.display = 'none'
if (targetConfirmedRow) targetConfirmedRow.style.display = ''
if (targetConfirmedName) targetConfirmedName.textContent = selectedOption?.text ?? ''
if (corpsPnjInput) {
corpsPnjInput.value = val
corpsPnjInput.dispatchEvent(new Event('input'))
}
} else {
// Saisie manuelle
if (corpsPnjRow) corpsPnjRow.style.display = ''
if (targetConfirmedRow) targetConfirmedRow.style.display = 'none'
}
}
if (targetSelect) {
targetSelect.addEventListener('change', applyTargetSelection)
applyTargetSelection()
}
function hasMalus(mod, asp, sit, ranged) {
return woundMalus < 0 || armorMalus < 0 || mod < 0 || asp < 0 || sit < 0 || ranged < 0
}
function update() {
const rawMod = wrap.querySelector('#modifier')?.value
const autoSucc = rawMod === "auto"
const modifier = autoSucc ? 0 : (parseInt(rawMod ?? 0) || 0)
const aspectMod = parseInt(wrap.querySelector('#aspectModifier')?.value ?? 0) || 0
const selectedFactionAspect = wrap.querySelector('#factionAspectId')?.selectedOptions?.[0]
const factionAspectBonus = parseInt(selectedFactionAspect?.dataset.value ?? 0) || 0
const situMod = parseInt(wrap.querySelector('#situationMod')?.value ?? 0) || 0
const rangedMod = parseInt(wrap.querySelector('#rangedMod')?.value ?? 0) || 0
const useDestin = wrap.querySelector('#useDestin')?.checked
const useFort = wrap.querySelector('#useFortune')?.checked
const puiser = wrap.querySelector('#puiserRessources')?.checked
const ndice = useDestin ? 3 : 2
// En résistance : pas de "Puiser" possible
const puiserRow = wrap.querySelector('#puiser-row')
if (puiserRow) {
if (!isResistance && hasMalus(modifier, aspectMod, situMod, rangedMod)) {
puiserRow.style.display = ''
} else {
puiserRow.style.display = 'none'
const cb = wrap.querySelector('#puiserRessources')
if (cb) cb.checked = false
}
}
const effWound = puiser ? 0 : woundMalus
const effMod = puiser ? Math.max(0, modifier) : modifier
const effAspect = puiser ? Math.max(0, aspectMod) : aspectMod
const effSit = puiser ? Math.max(0, situMod) : situMod
const effArmor = puiser ? 0 : armorMalus
const effRanged = puiser ? Math.max(0, rangedMod) : rangedMod
const totalMod = skillValue + effWound + effMod + effAspect + factionAspectBonus + effSit + effArmor + effRanged
let formula
if (autoSucc) {
formula = game.i18n.localize("CELESTOPOL.Roll.autoSuccess")
} else if (useFort) {
const fm = totalMod + 8
formula = `1d8` + (fm > 0 ? ` + ${fm}` : fm < 0 ? ` ${Math.abs(fm)}` : ``)
} else {
formula = `${ndice}d8`
if (totalMod > 0) formula += ` + ${totalMod}`
if (totalMod < 0) formula += ` ${Math.abs(totalMod)}`
}
const previewEl = wrap.querySelector('#preview-formula')
if (previewEl) previewEl.textContent = formula
}
wrap.querySelectorAll('#modifier, #aspectModifier, #factionAspectId, #situationMod, #rangedMod, #useDestin, #useFortune, #puiserRessources, #corpsPnj')
.forEach(el => {
el.addEventListener('change', update)
el.addEventListener('input', update)
})
update()
},
buttons: [
{
label: game.i18n.localize("CELESTOPOL.Roll.roll"),
icon: "fa-solid fa-dice",
callback: (event, button) => {
return Array.from(button.form.elements).reduce((obj, input) => {
if (input.name) {
obj[input.name] = input.type === "checkbox" ? input.checked : input.value
}
return obj
}, {})
},
},
],
rejectClose: false,
})
if (!rollContext) return null
// En combat : Corps PNJ = seuil direct ; sinon seuil fixe = 11
const corpsPnj = isCombat ? (parseInt(rollContext.corpsPnj ?? 7) || 7) : null
const difficulty = isCombat ? "combat" : "standard"
const diffConfig = isCombat
? { value: corpsPnj, label: "CELESTOPOL.Combat.corpsPnj" }
: { value: 11, label: "CELESTOPOL.Roll.threshold" }
const autoSuccess = rollContext.modifier === "auto"
const modifier = autoSuccess ? 0 : (parseInt(rollContext.modifier ?? 0) || 0)
const aspectMod = parseInt(rollContext.aspectModifier ?? 0) || 0
const factionAspectId = typeof rollContext.factionAspectId === "string" ? rollContext.factionAspectId : ""
const selectedFactionAspect = factionAspectChoices.find(choice => choice.id === factionAspectId) ?? null
const factionAspectBonus = selectedFactionAspect?.value ?? 0
const factionAspectLabel = selectedFactionAspect?.label ?? ""
const situationMod = parseInt(rollContext.situationMod ?? 0) || 0
const rangedMod = isRangedAttack ? (parseInt(rollContext.rangedMod ?? 0) || 0) : 0
const isOpposition = !isCombat && (rollContext.isOpposition === true || rollContext.isOpposition === "true")
const useDestin = destGaugeFull && (rollContext.useDestin === true || rollContext.useDestin === "true")
const useFortune = fortuneValue > 0 && (rollContext.useFortune === true || rollContext.useFortune === "true")
const puiserRessources = rollContext.puiserRessources === true || rollContext.puiserRessources === "true"
const rollMoonDie = rollContext.rollMoonDie === true || rollContext.rollMoonDie === "true"
const selectedCombatTargetId = typeof rollContext.targetSelect === "string" ? rollContext.targetSelect : ""
const selectedCombatTarget = selectedCombatTargetId
? availableTargets.find(t => t.id === selectedCombatTargetId) ?? null
: null
const targetActorId = selectedCombatTarget?.id || ""
const targetActorName = selectedCombatTarget?.name || ""
// En résistance : forcer puiser=false, lune=false, fortune=false, destin=false
const effectivePuiser = isResistance ? false : puiserRessources
const effectiveMoon = isResistance ? false : rollMoonDie
// Puiser dans ses ressources → ignorer tous les malus
const effectiveWoundMalus = effectivePuiser ? 0 : woundMalus
const effectiveArmorMalus = effectivePuiser ? 0 : armorMalus
const effectiveModifier = effectivePuiser ? Math.max(0, modifier) : modifier
const effectiveAspectMod = effectivePuiser ? Math.max(0, aspectMod) : aspectMod
const effectiveSituationMod = effectivePuiser ? Math.max(0, situationMod) : situationMod
const effectiveRangedMod = effectivePuiser ? Math.max(0, rangedMod) : rangedMod
// Fortune : 1d8 + 8 ; Destin : 3d8 ; sinon : 2d8
const nbDice = (!isResistance && useDestin) ? 3 : 2
const totalModifier = skillValue + effectiveWoundMalus + effectiveAspectMod + effectiveModifier + factionAspectBonus + effectiveSituationMod + effectiveArmorMalus + effectiveRangedMod
const formula = (!isResistance && useFortune)
? buildFormula(1, totalModifier + 8)
: buildFormula(nbDice, totalModifier)
// Jet du dé de lune séparé (narratif) — pas en résistance
let moonDieResult = null
let moonFace = null
let moonResultType = null
if (effectiveMoon) {
const moonRoll = await new Roll("1d8").evaluate()
moonDieResult = moonRoll.total
moonFace = SYSTEM.MOON_DIE_FACES[moonDieResult] ?? null
moonResultType = moonFace ? SYSTEM.MOON_RESULT_TYPES[moonFace.result] ?? null : null
}
const rollData = {
...options,
difficulty,
difficultyValue: diffConfig.value,
modifier: effectiveModifier,
aspectMod: effectiveAspectMod,
factionAspectId,
factionAspectLabel,
factionAspectBonus,
situationMod: effectiveSituationMod,
woundMalus: effectiveWoundMalus,
autoSuccess,
isResistance,
isOpposition,
isCombat,
isRangedDefense,
weaponType,
weaponName,
weaponDegats,
targetActorId,
targetActorName,
availableTargets,
rangedMod: effectiveRangedMod,
useDestin: !isResistance && useDestin,
useFortune: !isResistance && useFortune,
puiserRessources: effectivePuiser,
nbDice: (!isResistance && useFortune) ? 1 : nbDice,
formula,
rollMode: rollContext.visibility ?? "publicroll",
rollMoonDie: effectiveMoon,
moonDieResult,
moonFace,
moonResultType,
}
const roll = new this(formula, {}, rollData)
await roll.evaluate()
roll.computeResult()
// Test de résistance échoué → cocher automatiquement la prochaine case de blessure
const actor = game.actors.get(options.actorId)
if (isResistance && actor && roll.options.resultType === "failure") {
const nextLvl = (actor.system.blessures.lvl ?? 0) + 1
if (nextLvl <= 8) {
await actor.update({ "system.blessures.lvl": nextLvl })
roll.options.woundTaken = nextLvl
}
}
// Mêlée échouée OU défense à distance échouée → joueur prend une blessure
if (isCombat && (weaponType === "melee" || isRangedDefense) && actor && roll.options.resultType === "failure") {
const nextLvl = (actor.system.blessures.lvl ?? 0) + 1
if (nextLvl <= 8) {
await actor.update({ "system.blessures.lvl": nextLvl })
roll.options.woundTaken = nextLvl
}
}
await roll.toMessage({}, { rollMode: rollData.rollMode })
// Batching de toutes les mises à jour de l'acteur en un seul appel réseau
if (actor) {
const updateData = {}
if (rollData.useDestin) {
updateData["system.destin.lvl"] = 0
}
if (rollData.useFortune) {
const currentFortune = actor.system.attributs.fortune.value ?? 0
updateData["system.attributs.fortune.value"] = Math.max(0, currentFortune - 1)
}
if (rollData.puiserRessources) {
const currentSpleen = actor.system.spleen.lvl ?? 0
if (currentSpleen < 8) {
updateData["system.spleen.lvl"] = currentSpleen + 1
}
}
// Mémoriser les préférences (protagonistes uniquement — le modèle NPC n'a pas de champ prefs)
if (actor.type === "character") {
updateData["system.prefs.rollMoonDie"] = rollData.rollMoonDie
updateData["system.prefs.difficulty"] = difficulty
}
await actor.update(updateData)
}
return roll
}
/**
* Détermine succès/échec selon la marge (total seuil).
* Seuil : 11 pour les tests normaux, Corps PNJ pour le combat.
* Pas de succès/échec critique — seul le Dé de la Lune produit des résultats exceptionnels.
*/
computeResult() {
if (this.options.autoSuccess) {
this.options.resultType = "success"
this.options.margin = null
return
}
// En test d'opposition : pas de résultat calculé — le MJ décide
if (this.options.isOpposition) {
this.options.resultType = "opposition"
this.options.margin = null
return
}
const threshold = this.options.isCombat
? (this.options.difficultyValue ?? 0)
: 11
if (threshold === 0) {
this.options.resultType = "unknown"
this.options.margin = null
return
}
const margin = this.total - threshold
this.options.margin = margin
if (this.options.isCombat && margin === 0) {
this.options.resultType = "tie"
} else if (margin >= 0) {
this.options.resultType = "success"
} else {
this.options.resultType = "failure"
}
}
/** @override */
async render(chatOptions = {}) {
const data = await this._getChatCardData(chatOptions.isPrivate)
return foundry.applications.handlebars.renderTemplate(this.constructor.CHAT_TEMPLATE, data)
}
async _getChatCardData(isPrivate) {
const statLabel = this.options.statLabel
const skillLabel = this.options.skillLabel
const resultType = this.resultType
const diceResults = this.dice[0]?.results?.map(r => r.result) ?? []
const diceSum = diceResults.reduce((a, b) => a + b, 0)
const threshold = this.options.isCombat
? (this.options.difficultyValue ?? 0)
: 11
const margin = this.options.margin
const woundMalus = this.options.woundMalus ?? 0
const armorMalus = this.options.armorMalus ?? 0
const skillValue = this.options.skillValue ?? 0
const woundLevelId = this.options.woundLevel ?? 0
const weaponDegats = `${this.options.weaponDegats ?? "0"}`
const woundLabel = woundLevelId > 0
? game.i18n.localize(SYSTEM.WOUND_LEVELS[woundLevelId]?.label ?? "")
: null
// Dé de lune
const moonDieResult = this.options.moonDieResult ?? null
const moonFace = this.options.moonFace ?? null
const moonResultType = this.options.moonResultType ?? null
const resultClassMap = {
"success": "success",
"tie": "tie",
"failure": "failure",
"opposition": "opposition",
"unknown": "",
}
const isOpposition = this.options.isOpposition ?? false
const isWeaponHit = (this.options.isCombat ?? false) && !(this.options.isRangedDefense ?? false) && this.isSuccess
const incomingWounds = isWeaponHit ? this.constructor.getIncomingWounds(weaponDegats) : null
const hasVariableDamage = isWeaponHit && incomingWounds === null
const targetActorId = this.options.targetActorId ?? ""
const targetActorName = this.options.targetActorName ?? ""
const availableTargets = (this.options.availableTargets ?? []).map(target => ({
...target,
selected: target.id === targetActorId,
}))
const selectedTargetActor = targetActorId ? game.actors.get(targetActorId) : null
const selectedTargetProtection = selectedTargetActor
? this.constructor.getActorArmorProtection(selectedTargetActor)
: null
const selectedTargetAppliedWounds = (incomingWounds !== null && selectedTargetActor)
? Math.max(0, incomingWounds - selectedTargetProtection)
: null
// Libellé de difficulté : en combat "Corps PNJ : N", en opposition "vs ?", sinon "Seuil : 11"
const difficultyLabel = this.options.isCombat
? `${game.i18n.localize("CELESTOPOL.Combat.corpsPnj")} : ${threshold}`
: isOpposition
? `${game.i18n.localize("CELESTOPOL.Roll.oppositionVs")}`
: `${game.i18n.localize("CELESTOPOL.Roll.threshold")} : 11`
return {
cssClass: [SYSTEM.id, "dice-roll"].join(" "),
actorId: this.actorId,
actorName: this.actorName,
actorImg: this.actorImage,
statLabel: statLabel ? game.i18n.localize(statLabel) : "",
skillLabel: skillLabel ? game.i18n.localize(skillLabel) : "",
formula: this.formula,
total: this.total,
diceSum,
diceResults,
resultType,
resultClass: resultClassMap[resultType] ?? "",
isSuccess: this.isSuccess,
isFailure: this.isFailure,
isTie: this.isTie,
isOpposition,
difficulty: this.options.difficulty,
difficultyLabel,
difficultyValue: isOpposition ? null : threshold,
margin,
marginAbs: margin !== null ? Math.abs(margin) : null,
marginAbove: margin !== null && margin >= 0,
modifier: this.options.modifier ?? 0,
autoSuccess: this.options.autoSuccess ?? false,
aspectMod: this.options.aspectMod ?? 0,
factionAspectLabel: this.options.factionAspectLabel ?? "",
factionAspectBonus: this.options.factionAspectBonus ?? 0,
skillValue,
useDestin: this.options.useDestin ?? false,
useFortune: this.options.useFortune ?? false,
puiserRessources: this.options.puiserRessources ?? false,
nbDice: this.options.nbDice ?? diceResults.length,
woundMalus,
armorMalus,
woundLabel,
isResistance: this.options.isResistance ?? false,
isCombat: this.options.isCombat ?? false,
weaponName: this.options.weaponName ?? null,
weaponDegats,
weaponType: this.options.weaponType ?? null,
isRangedDefense: this.options.isRangedDefense ?? false,
woundTaken: this.options.woundTaken ?? null,
situationMod: this.options.situationMod ?? 0,
rangedMod: this.options.rangedMod ?? 0,
hasDamageSummary: isWeaponHit,
incomingWounds,
incomingWoundsDisplay: incomingWounds ?? "1 + X",
hasVariableDamage,
canApplyWeaponDamage: incomingWounds !== null,
targetActorId,
targetActorName,
selectedTargetProtection,
selectedTargetAppliedWounds,
availableTargets,
// Dé de lune
hasMoonDie: moonDieResult !== null,
moonDieResult,
moonFaceSymbol: moonFace?.symbol ?? "",
moonFaceLabel: moonFace ? game.i18n.localize(moonFace.label) : "",
moonResultClass: moonResultType?.cssClass ?? "",
moonResultLabel: moonResultType ? game.i18n.localize(moonResultType.label) : "",
moonResultDesc: moonResultType ? game.i18n.localize(moonResultType.desc) : "",
isPrivate,
tooltip: isPrivate ? "" : await this.getTooltip(),
}
}
/** @override */
async toMessage(messageData = {}, { rollMode, create = true } = {}) {
const skillLocalized = this.skillLabel ? game.i18n.localize(this.skillLabel) : ""
const statLocalized = this.options.statLabel
? game.i18n.localize(this.options.statLabel) : ""
const flavor = statLocalized
? `<strong>${statLocalized} ${skillLocalized}</strong>`
: `<strong>${skillLocalized}</strong>`
return super.toMessage({ flavor, ...messageData }, { rollMode })
}
/**
* Lance le dé de la Lune de façon autonome (hors test de spécialisation).
* Affiche un carte de chat avec le résultat narratif ET l'interprétation chance.
* @param {Actor|null} actor Acteur initiateur (pour le speaker du message)
*/
static async rollMoonStandalone(actor = null) {
const roll = await new Roll("1d8").evaluate()
const result = roll.total
const face = SYSTEM.MOON_DIE_FACES[result] ?? null
const resultType = face ? SYSTEM.MOON_RESULT_TYPES[face.result] ?? null : null
const isGoodFortune = result <= 4
const templateData = {
result,
moonFaceSymbol: face?.symbol ?? "",
moonFaceLabel: face ? game.i18n.localize(face.label) : "",
moonResultLabel: resultType ? game.i18n.localize(resultType.label) : "",
moonResultDesc: resultType ? game.i18n.localize(resultType.desc) : "",
moonResultClass: resultType?.cssClass ?? "",
isGoodFortune,
actorName: actor?.name ?? null,
}
const content = await foundry.applications.handlebars.renderTemplate(
"systems/fvtt-celestopol/templates/moon-standalone.hbs",
templateData
)
const speaker = actor
? ChatMessage.getSpeaker({ actor })
: { alias: game.i18n.localize("CELESTOPOL.Moon.standalone") }
await ChatMessage.create({
content,
speaker,
rolls: [roll],
style: CONST.CHAT_MESSAGE_STYLES?.ROLL ?? 5,
})
}
}