import { SYSTEM } from "../config/system.mjs" /** Symboles Unicode pour chaque phase de lune. */ const MOON_SYMBOLS = { none: "☽", nouvellelune: "🌑", premiercroissant: "🌒", premierquartier: "🌓", lunegibbeuse: "🌔", lunevoutee: "🌕", derniercroissant: "🌖", dernierquartier: "🌗", pleinelune: "🌕", } /** * Système de dés de Célestopol 1922. * * Le jet de base est : (valeur domaine + malus blessures)d6 + bonus lune + modificateurs * comparé à un seuil de difficulté. * - Succès critique : marge ≥ 5 * - Échec critique : marge ≤ −5 */ 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 } get moonBonus() { return this.options.moonBonus ?? 0 } /** * 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 baseSkillVal = options.skillValue ?? 0 const nbDiceBase = Math.max(1, baseSkillVal + woundMalus) const woundLevelId = options.woundLevel ?? 0 const woundLabel = woundLevelId > 0 ? game.i18n.localize(SYSTEM.WOUND_LEVELS[woundLevelId]?.label ?? "") : null // Construire les phases lune avec symboles const moonPhaseChoices = Object.fromEntries( Object.entries(SYSTEM.MOON_DICE_PHASES).map(([key, val]) => [ key, { ...val, symbol: MOON_SYMBOLS[key] ?? "☽" } ]) ) const dialogContext = { actorName: options.actorName, statLabel: options.statLabel, skillLabel: options.skillLabel, skillValue: baseSkillVal, woundMalus, woundLabel, nbDiceBase, difficultyChoices: SYSTEM.DIFFICULTY_CHOICES, moonPhaseChoices, defaultDifficulty: options.difficulty ?? "normal", defaultMoonPhase: options.moonPhase ?? "none", } 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 moonPhase = rollContext.moonPhase ?? "none" const moonConfig = SYSTEM.MOON_DICE_PHASES[moonPhase] ?? SYSTEM.MOON_DICE_PHASES.none const modifier = parseInt(rollContext.modifier ?? 0) || 0 const aspectMod = parseInt(rollContext.aspectModifier ?? 0) || 0 const useDestin = rollContext.useDestin === true || rollContext.useDestin === "true" const destinDice = useDestin ? 2 : 0 const skillValue = Math.max(0, baseSkillVal + woundMalus) const nbDice = Math.max(1, skillValue) + destinDice const moonBonus = moonConfig.bonus ?? 0 const totalModifier = moonBonus + modifier + aspectMod const formula = totalModifier !== 0 ? `${nbDice}d6 + ${totalModifier}` : `${nbDice}d6` const rollData = { ...options, difficulty, difficultyValue: diffConfig.value, moonPhase, moonBonus, modifier, aspectMod, useDestin, destinDice, nbDice, formula, rollMode: rollContext.visibility ?? "publicroll", } const roll = new this(formula, {}, rollData) await roll.evaluate() roll.computeResult() await roll.toMessage({}, { rollMode: rollData.rollMode }) // Mémoriser les préférences sur l'acteur const actor = game.actors.get(options.actorId) if (actor) { await actor.update({ "system.prefs.moonPhase": moonPhase, "system.prefs.difficulty": difficulty, }) } return roll } /** * Détermine succès/échec et critiques 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 moonPhase = this.options.moonPhase ?? "none" const moonSymbol = MOON_SYMBOLS[moonPhase] ?? "☽" 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 // Classe CSS principale du résultat 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, moonPhase, moonPhaseLabel: game.i18n.localize(SYSTEM.MOON_DICE_PHASES[moonPhase]?.label ?? ""), moonSymbol, moonBonus: this.moonBonus, modifier: this.options.modifier, aspectMod: this.options.aspectMod ?? 0, useDestin: this.options.useDestin ?? false, destinDice: this.options.destinDice ?? 0, nbDice: this.options.nbDice ?? diceResults.length, woundMalus, woundLabel, 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 }) } }