Fix roll dialog CSS + JS: template <div> wrapper, moon-section, selectors

- Remplace <form class='roll-dialog celestopol'> par <div class='roll-dialog-content'>
  pour éviter les formulaires HTML imbriqués invalides (DialogV2 a son propre <form>)
- Corrige le sélecteur CSS de .roll-dialog.celestopol vers .application.roll-dialog .roll-dialog-content
- Remplace .form-group.form-moon par .moon-section (classe custom) pour éviter
  les conflits avec le CSS grid de FoundryVTT standard-form (label 130px de hauteur)
- Met à jour le script JS inline pour utiliser document.querySelector('.roll-dialog-content')
- Ajoute white-space: nowrap sur le label Destin pour éviter le wrapping sur 3 lignes
- Supprime .application.roll-dialog .window-content padding override (remplacé par dialog-content)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-03-28 17:21:18 +01:00
parent a581853f95
commit cff700bd3d
24 changed files with 1133 additions and 283 deletions

View File

@@ -1,24 +1,40 @@
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 compétence)d6 comparé à un seuil de difficulté.
* Le dé de lune ajoute un bonus selon la phase actuelle.
* Destin et Spleen modifient le nombre de dés.
* 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" }
get isFailure() { return this.resultType === "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 }
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.
@@ -26,26 +42,33 @@ export class CelestopolRoll extends Roll {
* @returns {Promise<CelestopolRoll|null>}
*/
static async prompt(options = {}) {
const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes)
const fieldRollMode = new foundry.data.fields.StringField({
choices: rollModes,
blank: false,
default: "publicroll",
})
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: options.skillValue,
woundMalus: options.woundMalus ?? 0,
nbDice: Math.max(1, options.skillValue ?? 1),
difficultyChoices:SYSTEM.DIFFICULTY_CHOICES,
moonPhaseChoices: SYSTEM.MOON_DICE_PHASES,
defaultDifficulty:options.difficulty ?? "normal",
defaultMoonPhase: options.moonPhase ?? "none",
rollModes,
fieldRollMode,
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(
@@ -53,7 +76,11 @@ export class CelestopolRoll extends Roll {
dialogContext
)
const title = `${game.i18n.localize("CELESTOPOL.Roll.title")}${game.i18n.localize(options.skillLabel ?? "")}`
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 },
@@ -62,9 +89,12 @@ export class CelestopolRoll extends Roll {
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.value
if (input.name) {
obj[input.name] = input.type === "checkbox" ? input.checked : input.value
}
return obj
}, {})
},
@@ -80,11 +110,13 @@ export class CelestopolRoll extends Roll {
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 woundMalus = options.woundMalus ?? 0
const skillValue = Math.max(0, (options.skillValue ?? 0) + woundMalus)
const nbDice = Math.max(1, skillValue)
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
const totalModifier = moonBonus + modifier + aspectMod
const formula = totalModifier !== 0
? `${nbDice}d6 + ${totalModifier}`
@@ -97,6 +129,10 @@ export class CelestopolRoll extends Roll {
moonPhase,
moonBonus,
modifier,
aspectMod,
useDestin,
destinDice,
nbDice,
formula,
rollMode: rollContext.visibility ?? "publicroll",
}
@@ -118,16 +154,26 @@ export class CelestopolRoll extends Roll {
return roll
}
/** Détermine succès/échec selon le total vs le seuil. */
/**
* 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"
} else if (this.total >= threshold) {
this.options.resultType = "success"
} else {
this.options.resultType = "failure"
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 */
@@ -137,44 +183,77 @@ 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 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,
resultType,
resultClass: resultType === "success" ? "success" : resultType === "failure" ? "failure" : "",
success: this.isSuccess,
failure: this.isFailure,
difficulty: this.options.difficulty,
difficultyLabel:game.i18n.localize(SYSTEM.DIFFICULTY_CHOICES[this.options.difficulty]?.label ?? ""),
moonPhase: this.options.moonPhase,
moonPhaseLabel: game.i18n.localize(SYSTEM.MOON_DICE_PHASES[this.options.moonPhase]?.label ?? ""),
moonBonus: this.moonBonus,
modifier: this.options.modifier,
isPrivate,
tooltip: isPrivate ? "" : await this.getTooltip(),
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 } = {}) {
return super.toMessage(
{
flavor: `<strong>${game.i18n.localize(this.skillLabel ?? "")}</strong>`,
...messageData,
},
{ rollMode }
)
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 })
}
}