feat: tests de résistance (2d8+TR, auto-blessure sur échec)

- rollResistance(statId) dans character.mjs : formule 2d8 + bonus TR + malus blessures
- Dialog sans Modificateur/Aspect/Lune/Destin/Fortune/Puiser en mode résistance
- Auto-cochage de la prochaine case de blessure sur échec
- Chat message : notification blessure cochée (woundTaken)
- Stat-res cliquable (rollable) en mode jeu dans l'onglet compétences
- base-actor-sheet : routing clic stat-res → rollResistance
- CSS : .resistance-wound-notice
- i18n : resistanceTest, resistanceClickToRoll, woundTaken

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-03-29 23:35:38 +02:00
parent df2ed14f34
commit 89d47ba6ec
8 changed files with 107 additions and 22 deletions

View File

@@ -163,7 +163,10 @@
"puiser": "Puiser dans ses ressources", "puiser": "Puiser dans ses ressources",
"puiserDesc": "Ignore tous les malus — coche 1 case de Spleen", "puiserDesc": "Ignore tous les malus — coche 1 case de Spleen",
"usedPuiser": "Ressources puisées — malus ignorés, +1 Spleen", "usedPuiser": "Ressources puisées — malus ignorés, +1 Spleen",
"autoSuccess": "Réussite automatique" "autoSuccess": "Réussite automatique",
"resistanceTest": "Test de résistance",
"resistanceClickToRoll": "Lancer un test de résistance",
"woundTaken": "Blessure cochée suite à l'échec"
}, },
"Modifier": { "Modifier": {
"evident": "Évident — Réussite automatique", "evident": "Évident — Réussite automatique",

View File

@@ -76,7 +76,12 @@ export default class CelestopolActorSheet extends HandlebarsApplicationMixin(fou
const el = event.currentTarget const el = event.currentTarget
const statId = el.dataset.statId const statId = el.dataset.statId
const skillId = el.dataset.skillId const skillId = el.dataset.skillId
if (!statId || !skillId) return if (!statId) return
if (!skillId) {
// Test de résistance (clic sur la zone TR de la stat)
await this.document.system.rollResistance(statId)
return
}
await this.document.system.roll(statId, skillId) await this.document.system.roll(statId, skillId)
} }

View File

@@ -39,6 +39,7 @@ export class CelestopolRoll extends Roll {
const woundLevelId = options.woundLevel ?? 0 const woundLevelId = options.woundLevel ?? 0
const destGaugeFull = options.destGaugeFull ?? false const destGaugeFull = options.destGaugeFull ?? false
const fortuneValue = options.fortuneValue ?? 0 const fortuneValue = options.fortuneValue ?? 0
const isResistance = options.isResistance ?? false
const woundLabel = woundLevelId > 0 const woundLabel = woundLevelId > 0
? game.i18n.localize(SYSTEM.WOUND_LEVELS[woundLevelId]?.label ?? "") ? game.i18n.localize(SYSTEM.WOUND_LEVELS[woundLevelId]?.label ?? "")
: null : null
@@ -64,6 +65,7 @@ export class CelestopolRoll extends Roll {
defaultDifficulty: options.difficulty ?? "normal", defaultDifficulty: options.difficulty ?? "normal",
destGaugeFull, destGaugeFull,
defaultRollMoonDie: options.rollMoonDie ?? false, defaultRollMoonDie: options.rollMoonDie ?? false,
isResistance,
modifierChoices, modifierChoices,
aspectChoices, aspectChoices,
fortuneValue, fortuneValue,
@@ -100,10 +102,10 @@ export class CelestopolRoll extends Roll {
const puiser = wrap.querySelector('#puiserRessources')?.checked const puiser = wrap.querySelector('#puiserRessources')?.checked
const ndice = useDestin ? 3 : 2 const ndice = useDestin ? 3 : 2
// Afficher/masquer le bloc "Puiser" selon les malus actifs // En résistance : pas de "Puiser" possible
const puiserRow = wrap.querySelector('#puiser-row') const puiserRow = wrap.querySelector('#puiser-row')
if (puiserRow) { if (puiserRow) {
if (hasMalus(modifier, aspectMod)) { if (!isResistance && hasMalus(modifier, aspectMod)) {
puiserRow.style.display = '' puiserRow.style.display = ''
} else { } else {
puiserRow.style.display = 'none' puiserRow.style.display = 'none'
@@ -169,23 +171,27 @@ export class CelestopolRoll extends Roll {
const puiserRessources = rollContext.puiserRessources === true || rollContext.puiserRessources === "true" const puiserRessources = rollContext.puiserRessources === true || rollContext.puiserRessources === "true"
const rollMoonDie = rollContext.rollMoonDie === true || rollContext.rollMoonDie === "true" const rollMoonDie = rollContext.rollMoonDie === true || rollContext.rollMoonDie === "true"
// En résistance : forcer puiser=false, lune=false, fortune=false, destin=false
const effectivePuiser = isResistance ? false : puiserRessources
const effectiveMoon = isResistance ? false : rollMoonDie
// Puiser dans ses ressources → ignorer tous les malus // Puiser dans ses ressources → ignorer tous les malus
const effectiveWoundMalus = puiserRessources ? 0 : woundMalus const effectiveWoundMalus = effectivePuiser ? 0 : woundMalus
const effectiveModifier = puiserRessources ? Math.max(0, modifier) : modifier const effectiveModifier = effectivePuiser ? Math.max(0, modifier) : modifier
const effectiveAspectMod = puiserRessources ? Math.max(0, aspectMod) : aspectMod const effectiveAspectMod = effectivePuiser ? Math.max(0, aspectMod) : aspectMod
// Fortune : 1d8 + 8 ; Destin : 3d8 ; sinon : 2d8 // Fortune : 1d8 + 8 ; Destin : 3d8 ; sinon : 2d8
const nbDice = useDestin ? 3 : 2 const nbDice = (!isResistance && useDestin) ? 3 : 2
const totalModifier = skillValue + effectiveWoundMalus + effectiveAspectMod + effectiveModifier const totalModifier = skillValue + effectiveWoundMalus + effectiveAspectMod + effectiveModifier
const formula = useFortune const formula = (!isResistance && useFortune)
? buildFormula(1, totalModifier + 8) ? buildFormula(1, totalModifier + 8)
: buildFormula(nbDice, totalModifier) : buildFormula(nbDice, totalModifier)
// Jet du dé de lune séparé (narratif) // Jet du dé de lune séparé (narratif) — pas en résistance
let moonDieResult = null let moonDieResult = null
let moonFace = null let moonFace = null
let moonResultType = null let moonResultType = null
if (rollMoonDie) { if (effectiveMoon) {
const moonRoll = await new Roll("1d8").evaluate() const moonRoll = await new Roll("1d8").evaluate()
moonDieResult = moonRoll.total moonDieResult = moonRoll.total
moonFace = SYSTEM.MOON_DIE_FACES[moonDieResult] ?? null moonFace = SYSTEM.MOON_DIE_FACES[moonDieResult] ?? null
@@ -200,13 +206,14 @@ export class CelestopolRoll extends Roll {
aspectMod: effectiveAspectMod, aspectMod: effectiveAspectMod,
woundMalus: effectiveWoundMalus, woundMalus: effectiveWoundMalus,
autoSuccess, autoSuccess,
useDestin, isResistance,
useFortune, useDestin: !isResistance && useDestin,
puiserRessources, useFortune: !isResistance && useFortune,
nbDice: useFortune ? 1 : nbDice, puiserRessources: effectivePuiser,
nbDice: (!isResistance && useFortune) ? 1 : nbDice,
formula, formula,
rollMode: rollContext.visibility ?? "publicroll", rollMode: rollContext.visibility ?? "publicroll",
rollMoonDie, rollMoonDie: effectiveMoon,
moonDieResult, moonDieResult,
moonFace, moonFace,
moonResultType, moonResultType,
@@ -215,11 +222,22 @@ export class CelestopolRoll extends Roll {
const roll = new this(formula, {}, rollData) const roll = new this(formula, {}, rollData)
await roll.evaluate() await roll.evaluate()
roll.computeResult() roll.computeResult()
// Test de résistance échoué → cocher automatiquement la prochaine case de blessure
const actor = game.actors.get(options.actorId)
if (isResistance && actor && roll.options.resultType === "failure") {
const wounds = actor.system.blessures
const nextIdx = [1,2,3,4,5,6,7,8].find(i => !wounds[`b${i}`]?.checked)
if (nextIdx) {
await actor.update({ [`system.blessures.b${nextIdx}.checked`]: true })
roll.options.woundTaken = nextIdx
}
}
await roll.toMessage({}, { rollMode: rollData.rollMode }) await roll.toMessage({}, { rollMode: rollData.rollMode })
// Destin utilisé → vider la jauge (reset à 0) // Destin utilisé → vider la jauge (reset à 0)
const actor = game.actors.get(options.actorId) if (rollData.useDestin && actor) {
if (useDestin && actor) {
await actor.update({ await actor.update({
"system.destin.lvl": 0, "system.destin.lvl": 0,
"system.destin.d1.checked": false, "system.destin.d1.checked": false,
@@ -234,13 +252,13 @@ export class CelestopolRoll extends Roll {
} }
// Fortune utilisée → décrémenter de 1 (min 0) // Fortune utilisée → décrémenter de 1 (min 0)
if (useFortune && actor) { if (rollData.useFortune && actor) {
const currentFortune = actor.system.attributs.fortune.value ?? 0 const currentFortune = actor.system.attributs.fortune.value ?? 0
await actor.update({ "system.attributs.fortune.value": Math.max(0, currentFortune - 1) }) await actor.update({ "system.attributs.fortune.value": Math.max(0, currentFortune - 1) })
} }
// Puiser dans ses ressources → coche une case de spleen // Puiser dans ses ressources → coche une case de spleen
if (puiserRessources && actor) { if (rollData.puiserRessources && actor) {
const currentSpleen = actor.system.spleen.lvl ?? 0 const currentSpleen = actor.system.spleen.lvl ?? 0
if (currentSpleen < 8) { if (currentSpleen < 8) {
const newLvl = currentSpleen + 1 const newLvl = currentSpleen + 1
@@ -255,7 +273,7 @@ export class CelestopolRoll extends Roll {
// Mémoriser les préférences sur l'acteur // Mémoriser les préférences sur l'acteur
if (actor) { if (actor) {
await actor.update({ await actor.update({
"system.prefs.rollMoonDie": rollMoonDie, "system.prefs.rollMoonDie": rollData.rollMoonDie,
"system.prefs.difficulty": difficulty, "system.prefs.difficulty": difficulty,
}) })
} }
@@ -357,6 +375,8 @@ export class CelestopolRoll extends Roll {
nbDice: this.options.nbDice ?? diceResults.length, nbDice: this.options.nbDice ?? diceResults.length,
woundMalus, woundMalus,
woundLabel, woundLabel,
isResistance: this.options.isResistance ?? false,
woundTaken: this.options.woundTaken ?? null,
// Dé de lune // Dé de lune
hasMoonDie: moonDieResult !== null, hasMoonDie: moonDieResult !== null,
moonDieResult, moonDieResult,

View File

@@ -226,4 +226,31 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
fortuneValue: this.attributs.fortune.value, fortuneValue: this.attributs.fortune.value,
}) })
} }
/**
* Lance un test de résistance pour une stat donnée.
* Formule : 2d8 + resBonus + woundMalus
* Pas de lune, Puiser, Fortune ou Destin.
* Échec → blessure automatique.
* @param {string} statId - Id de la stat (ame, corps, coeur, esprit)
*/
async rollResistance(statId) {
const { CelestopolRoll } = await import("../documents/roll.mjs")
const statData = this.stats[statId]
if (!statData) return null
return CelestopolRoll.prompt({
actorId: this.parent.id,
actorName: this.parent.name,
actorImage: this.parent.img,
statId,
statLabel: SYSTEM.STATS[statId]?.label,
skillLabel: "CELESTOPOL.Roll.resistanceTest",
skillValue: statData.res ?? 0,
woundMalus: this.getWoundMalus(),
woundLevel: this.blessures.lvl,
difficulty: this.prefs.difficulty,
isResistance: true,
})
}
} }

View File

@@ -782,3 +782,20 @@
} }
} }
// Notification de blessure cochée lors d'un test de résistance raté
.celestopol.chat-roll {
.resistance-wound-notice {
display: flex;
align-items: center;
gap: 0.4em;
margin-top: 0.4em;
padding: 0.4em 0.8em;
background: #4a1520;
border-left: 3px solid #c0392b;
border-radius: 4px;
color: #f0c0c0;
font-size: 0.85em;
.wound-icon { font-size: 1em; }
}
}

View File

@@ -5,7 +5,8 @@
<div class="stat-block"> <div class="stat-block">
<div class="stat-header"> <div class="stat-header">
<span class="stat-name">{{localize stat.label}}</span> <span class="stat-name">{{localize stat.label}}</span>
<div class="stat-res"> <div class="stat-res {{#unless ../isEditMode}}rollable{{/unless}}" data-stat-id="{{statId}}"
title="{{localize 'CELESTOPOL.Roll.resistanceClickToRoll'}}">
<label>{{localize "CELESTOPOL.Stat.res"}}</label> <label>{{localize "CELESTOPOL.Stat.res"}}</label>
<span class="stat-res-value">{{lookup (lookup ../system.stats statId) 'res'}}</span> <span class="stat-res-value">{{lookup (lookup ../system.stats statId) 'res'}}</span>
</div> </div>

View File

@@ -123,5 +123,12 @@
<span class="result-label">{{localize "CELESTOPOL.Roll.failure"}}</span> <span class="result-label">{{localize "CELESTOPOL.Roll.failure"}}</span>
{{/if}} {{/if}}
</div> </div>
{{!-- Blessure auto-cochée (test de résistance raté) --}}
{{#if woundTaken}}
<div class="resistance-wound-notice">
<span class="wound-icon">🩹</span>
<span>{{localize "CELESTOPOL.Roll.woundTaken"}}</span>
</div>
{{/if}}
</div> </div>

View File

@@ -36,6 +36,9 @@
</select> </select>
</div> </div>
{{!-- Options non disponibles en test de résistance --}}
{{#unless isResistance}}
{{!-- Modificateur & Aspect côte à côte --}} {{!-- Modificateur & Aspect côte à côte --}}
<div class="form-two-col"> <div class="form-two-col">
<div class="form-row-line"> <div class="form-row-line">
@@ -110,6 +113,8 @@
</div> </div>
{{/if}} {{/if}}
{{/unless}}{{!-- /isResistance --}}
{{!-- Visibilité --}} {{!-- Visibilité --}}
<div class="form-row-line form-visibility"> <div class="form-row-line form-visibility">
<label for="visibility">{{localize "CELESTOPOL.Roll.visibility"}}</label> <label for="visibility">{{localize "CELESTOPOL.Roll.visibility"}}</label>