Files
fvtt-celestopol/module/documents/roll.mjs
LeRatierBretonnier 54eacf6afc Fix formule de dés : 2d8 de base (règles Célestopol)
Correction majeure de la mécanique de jet selon les règles :

- Formule : 2d8 + Spécialisation + modificateurs (blessures/aspect/manual)
  (vs. ancienne formule erronée : Nd6 pool variable)
- Dé de la Lune : 1d8 narratif optionnel (résultat 1-8 → Triomphe /
  Brio / Contrecoup / Catastrophe) — pas un bonus numérique
- Destin : disponible uniquement jauge pleine (lvl=8), donne 3d8,
  vide la jauge entière après usage
- system.mjs : MOON_DIE_FACES (tableau 1-8) + MOON_RESULT_TYPES
- roll.mjs : logique complète réécrite (2d8, lune séparée, destin reset)
- character/npc.mjs : prefs.rollMoonDie + destGaugeFull
- roll-dialog.hbs : sans grille lune, checkbox dé lune, destin conditionnel
- chat-message.hbs : résultat dé lune narratif (phase + type + desc),
  dés .d8, suppression moonSymbol/moonBonus header
- roll.less : .form-moon-row, .moon-die-result avec couleurs Triomphe/
  Brio/Contrecoup/Catastrophe
- lang/fr.json : Moon.triomphe/brio/contrecoup/catastrophe + Full descs,
  Roll.rollMoonDie/destGaugeFull/destGaugeEmpty/baseDice

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-29 16:19:04 +02:00

267 lines
10 KiB
JavaScript
Raw 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.
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<CelestopolRoll|null>}
*/
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 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,
}
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
? `<strong>${statLocalized} ${skillLocalized}</strong>`
: `<strong>${skillLocalized}</strong>`
return super.toMessage({ flavor, ...messageData }, { rollMode })
}
}