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>
This commit is contained in:
2026-03-29 16:19:04 +02:00
parent ad85ecf4bf
commit 54eacf6afc
8 changed files with 308 additions and 258 deletions

View File

@@ -1,25 +1,18 @@
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: "🌕",
/** 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.
*
* 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
* 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"
@@ -34,7 +27,6 @@ export class CelestopolRoll extends Roll {
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.
@@ -43,33 +35,24 @@ export class CelestopolRoll extends Roll {
*/
static async prompt(options = {}) {
const woundMalus = options.woundMalus ?? 0
const baseSkillVal = options.skillValue ?? 0
const nbDiceBase = Math.max(1, baseSkillVal + woundMalus)
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
// 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,
actorName: options.actorName,
statLabel: options.statLabel,
skillLabel: options.skillLabel,
skillValue,
woundMalus,
woundLabel,
nbDiceBase,
difficultyChoices: SYSTEM.DIFFICULTY_CHOICES,
moonPhaseChoices,
defaultDifficulty: options.difficulty ?? "normal",
defaultMoonPhase: options.moonPhase ?? "none",
destActuel: options.destActuel ?? null,
difficultyChoices: SYSTEM.DIFFICULTY_CHOICES,
defaultDifficulty: options.difficulty ?? "normal",
destGaugeFull,
defaultRollMoonDie: options.rollMoonDie ?? false,
}
const content = await foundry.applications.handlebars.renderTemplate(
@@ -79,9 +62,7 @@ export class CelestopolRoll extends Roll {
const skillLocalized = game.i18n.localize(options.skillLabel ?? "")
const statLocalized = options.statLabel ? game.i18n.localize(options.statLabel) : null
const title = statLocalized
? `${statLocalized} ${skillLocalized}`
: skillLocalized
const title = statLocalized ? `${statLocalized} ${skillLocalized}` : skillLocalized
const rollContext = await foundry.applications.api.DialogV2.wait({
window: { title },
@@ -108,34 +89,39 @@ export class CelestopolRoll extends Roll {
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 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)
const formula = totalModifier !== 0
? `${nbDice}d6 + ${totalModifier}`
: `${nbDice}d6`
// 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,
moonPhase,
moonBonus,
modifier,
aspectMod,
useDestin,
destinDice,
nbDice,
formula,
rollMode: rollContext.visibility ?? "publicroll",
rollMode: rollContext.visibility ?? "publicroll",
rollMoonDie,
moonDieResult,
moonFace,
moonResultType,
}
const roll = new this(formula, {}, rollData)
@@ -143,22 +129,27 @@ export class CelestopolRoll extends Roll {
roll.computeResult()
await roll.toMessage({}, { rollMode: rollData.rollMode })
// Déduire automatiquement 1 point de Destin si utilisé
// Destin utilisé → vider la jauge (reset à 0)
const actor = game.actors.get(options.actorId)
if (useDestin && actor && (options.destActuel ?? 1) > 0) {
const currentLvl = actor.system.destin?.lvl ?? 0
const newLvl = Math.min(8, currentLvl + 1)
if (useDestin && actor) {
await actor.update({
"system.destin.lvl": newLvl,
[`system.destin.d${newLvl}.checked`]: true,
"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.moonPhase": moonPhase,
"system.prefs.difficulty": difficulty,
"system.prefs.rollMoonDie": rollMoonDie,
"system.prefs.difficulty": difficulty,
})
}
@@ -166,7 +157,7 @@ export class CelestopolRoll extends Roll {
}
/**
* Détermine succès/échec et critiques selon la marge (total seuil).
* Détermine succès/échec selon la marge (total seuil).
* - Marge ≥ 5 → succès critique
* - Marge ≥ 0 → succès
* - Marge ≤ 5 → échec critique
@@ -194,22 +185,24 @@ export class CelestopolRoll extends Roll {
}
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
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
// Classe CSS principale du résultat
// 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",
@@ -219,41 +212,44 @@ export class CelestopolRoll extends Roll {
}
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,
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,
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,
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(),
tooltip: isPrivate ? "" : await this.getTooltip(),
}
}