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 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 } /** * Ouvre le dialogue de configuration du jet via DialogV2 et exécute le jet. * @param {object} options * @returns {Promise} */ static async prompt(options = {}) { const woundMalus = options.woundMalus ?? 0 const skillValue = options.skillValue ?? 0 const woundLevelId = options.woundLevel ?? 0 const destGaugeFull = options.destGaugeFull ?? false const woundLabel = woundLevelId > 0 ? game.i18n.localize(SYSTEM.WOUND_LEVELS[woundLevelId]?.label ?? "") : null const modifierChoices = [-4, -3, -2, -1, 0, 1, 2, 3, 4].map(v => ({ value: v, label: v > 0 ? `+${v}` : `${v}`, })) const dialogContext = { actorName: options.actorName, statLabel: options.statLabel, skillLabel: options.skillLabel, skillValue, woundMalus, woundLabel, difficultyChoices: SYSTEM.DIFFICULTY_CHOICES, defaultDifficulty: options.difficulty ?? "normal", destGaugeFull, defaultRollMoonDie: options.rollMoonDie ?? false, modifierChoices, } 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, 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 const difficulty = rollContext.difficulty ?? "normal" const diffConfig = SYSTEM.DIFFICULTY_CHOICES[difficulty] ?? SYSTEM.DIFFICULTY_CHOICES.normal const modifier = parseInt(rollContext.modifier ?? 0) || 0 const aspectMod = parseInt(rollContext.aspectModifier ?? 0) || 0 const useDestin = destGaugeFull && (rollContext.useDestin === true || rollContext.useDestin === "true") const rollMoonDie = rollContext.rollMoonDie === true || rollContext.rollMoonDie === "true" const nbDice = useDestin ? 3 : 2 const totalModifier = skillValue + woundMalus + aspectMod + modifier const formula = buildFormula(nbDice, totalModifier) // Jet du dé de lune séparé (narratif) let moonDieResult = null let moonFace = null let moonResultType = null if (rollMoonDie) { 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, aspectMod, useDestin, nbDice, formula, rollMode: rollContext.visibility ?? "publicroll", rollMoonDie, moonDieResult, moonFace, moonResultType, } const roll = new this(formula, {}, rollData) await roll.evaluate() roll.computeResult() await roll.toMessage({}, { rollMode: rollData.rollMode }) // Destin utilisé → vider la jauge (reset à 0) const actor = game.actors.get(options.actorId) if (useDestin && actor) { await actor.update({ "system.destin.lvl": 0, "system.destin.d1.checked": false, "system.destin.d2.checked": false, "system.destin.d3.checked": false, "system.destin.d4.checked": false, "system.destin.d5.checked": false, "system.destin.d6.checked": false, "system.destin.d7.checked": false, "system.destin.d8.checked": false, }) } // Mémoriser les préférences sur l'acteur if (actor) { await actor.update({ "system.prefs.rollMoonDie": rollMoonDie, "system.prefs.difficulty": difficulty, }) } return roll } /** * Détermine succès/échec selon la marge (total − seuil). * - Marge ≥ 5 → succès critique * - Marge ≥ 0 → succès * - Marge ≤ −5 → échec critique * - Marge < 0 → échec */ computeResult() { const threshold = SYSTEM.DIFFICULTY_CHOICES[this.options.difficulty]?.value ?? 0 if (threshold === 0) { this.options.resultType = "unknown" this.options.margin = null return } const margin = this.total - threshold this.options.margin = margin if (margin >= 5) this.options.resultType = "critical-success" else if (margin >= 0) this.options.resultType = "success" else if (margin <= -5) this.options.resultType = "critical-failure" 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 = SYSTEM.DIFFICULTY_CHOICES[this.options.difficulty]?.value ?? 0 const margin = this.options.margin const woundMalus = this.options.woundMalus ?? 0 const woundLevelId = this.options.woundLevel ?? 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 = { "critical-success": "critical-success", "success": "success", "failure": "failure", "critical-failure": "critical-failure", "unknown": "", } 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, isCriticalSuccess: this.isCriticalSuccess, isCriticalFailure: this.isCriticalFailure, difficulty: this.options.difficulty, difficultyLabel: game.i18n.localize(SYSTEM.DIFFICULTY_CHOICES[this.options.difficulty]?.label ?? ""), difficultyValue: threshold, margin, marginAbs: margin !== null ? Math.abs(margin) : null, marginAbove: margin !== null && margin >= 0, modifier: this.options.modifier, aspectMod: this.options.aspectMod ?? 0, useDestin: this.options.useDestin ?? false, nbDice: this.options.nbDice ?? diceResults.length, woundMalus, woundLabel, // 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 ? `${statLocalized} › ${skillLocalized}` : `${skillLocalized}` return super.toMessage({ flavor, ...messageData }, { rollMode }) } }