- Nouveau layout roll-dialog.hbs : header, grille lune 3×3, sélecteur visibility, ligne Destin avec compteur disponible, preview formule améliorée - Fix JS : data-moon-bonus sur chaque option, formule − si négatif - Fix roll.mjs : passe destActuel au dialog, déduit automatiquement 1 point de Destin (system.destin.lvl++) après chaque jet avec Destin - character.mjs : passe destActuel = 8 - destin.lvl au prompt() - chat-message.hbs : icônes résultat (✦✦/✖✖ pour critiques), label Formule, margin-badge right-aligned vs difficulté left-aligned - roll.less : refonte complète couleurs (#0c4c0c/#e07b00), grille lune, destin row, visibility subtil, bandeau critiques améliorés - lang/fr.json : clés Roll.visibility*, destinAvailable, destinNone, destinBonus, formula Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
271 lines
10 KiB
JavaScript
271 lines
10 KiB
JavaScript
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<CelestopolRoll|null>}
|
||
*/
|
||
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",
|
||
destActuel: options.destActuel ?? null,
|
||
}
|
||
|
||
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 })
|
||
|
||
// Déduire automatiquement 1 point de Destin si utilisé
|
||
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)
|
||
await actor.update({
|
||
"system.destin.lvl": newLvl,
|
||
[`system.destin.d${newLvl}.checked`]: true,
|
||
})
|
||
}
|
||
|
||
// Mémoriser les préférences sur l'acteur
|
||
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
|
||
? `<strong>${statLocalized} › ${skillLocalized}</strong>`
|
||
: `<strong>${skillLocalized}</strong>`
|
||
return super.toMessage({ flavor, ...messageData }, { rollMode })
|
||
}
|
||
}
|