Files
fvtt-celestopol/module/documents/roll.mjs
LeRatierBretonnier 3cb31dfdef feat: ajoute la règle Fortune dans les jets
Règle : si Fortune > 0, le joueur peut cocher 'Fortune' pour lancer
1d8 + 8 au lieu de 2d8 (contraint les probabilités vers le haut).

- character.mjs : passe fortuneValue dans roll.prompt()
- roll.mjs : checkbox Fortune, formule 1d8 + 8 + totalModifier si cochée
- roll-dialog.hbs : bloc Fortune (visible si fortuneValue > 0), preview mis à jour
- chat-message.hbs : affiche '+8' dans la zone dés, ligne formule avec badge Fortune
- roll.less : styles .form-fortune-row, .fl-mod.fortune, .fortune-fixed-badge
- lang/fr.json : Roll.fortune/fortuneBonus/fortuneFixed/usedFortune

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-29 20:08:06 +02:00

284 lines
11 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 fortuneValue = options.fortuneValue ?? 0
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,
fortuneValue,
}
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 useFortune = fortuneValue > 0 && (rollContext.useFortune === true || rollContext.useFortune === "true")
const rollMoonDie = rollContext.rollMoonDie === true || rollContext.rollMoonDie === "true"
// Fortune : 1d8 + 8 ; Destin : 3d8 ; sinon : 2d8
const nbDice = useDestin ? 3 : 2
const totalModifier = skillValue + woundMalus + aspectMod + modifier
const formula = useFortune
? buildFormula(1, totalModifier + 8)
: 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,
useFortune,
nbDice: useFortune ? 1 : 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 skillValue = this.options.skillValue ?? 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 ?? 0,
aspectMod: this.options.aspectMod ?? 0,
skillValue,
useDestin: this.options.useDestin ?? false,
useFortune: this.options.useFortune ?? 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 })
}
}