feat: jets d'attaque depuis les armes (combat en opposition)

- Bouton ⚔ Attaquer sur chaque arme (onglet Équipement, mode Jeu)
- rollAttack(itemId) dans character.mjs : jet Échauffourée vs Corps PNJ
- Dialog combat : input numérique 'Corps du PNJ' à la place du sélect difficulté
- computeResult() : margin===0 → résultat 'tie' (égalité) en combat
- Mêlée échec → blessure joueur auto-cochée (comme résistance)
- Distance échec → simple raté, pas de blessure joueur
- Chat message : infos arme, bandeau égalité, desc succès/échec combat
- CSS : bandeau 'tie' (brun doré), zone arme dans dialog
- i18n : CELESTOPOL.Combat.* (attack, corpsPnj, tie, successHit, etc.)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-03-30 00:29:29 +02:00
parent 149d55dfa0
commit 79a68ee9ab
8 changed files with 210 additions and 23 deletions

View File

@@ -20,6 +20,7 @@ export class CelestopolRoll extends Roll {
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 isTie() { return this.resultType === "tie" }
get isCriticalSuccess(){ return this.resultType === "critical-success" }
get isCriticalFailure(){ return this.resultType === "critical-failure" }
get actorId() { return this.options.actorId }
@@ -40,6 +41,10 @@ export class CelestopolRoll extends Roll {
const destGaugeFull = options.destGaugeFull ?? false
const fortuneValue = options.fortuneValue ?? 0
const isResistance = options.isResistance ?? false
const isCombat = options.isCombat ?? false
const weaponType = options.weaponType ?? "melee"
const weaponName = options.weaponName ?? null
const weaponDegats = options.weaponDegats ?? "0"
const woundLabel = woundLevelId > 0
? game.i18n.localize(SYSTEM.WOUND_LEVELS[woundLevelId]?.label ?? "")
: null
@@ -66,6 +71,10 @@ export class CelestopolRoll extends Roll {
destGaugeFull,
defaultRollMoonDie: options.rollMoonDie ?? false,
isResistance,
isCombat,
weaponType,
weaponName,
weaponDegats,
modifierChoices,
aspectChoices,
fortuneValue,
@@ -135,7 +144,7 @@ export class CelestopolRoll extends Roll {
if (previewEl) previewEl.textContent = formula
}
wrap.querySelectorAll('#modifier, #aspectModifier, #useDestin, #useFortune, #puiserRessources')
wrap.querySelectorAll('#modifier, #aspectModifier, #useDestin, #useFortune, #puiserRessources, #corpsPnj')
.forEach(el => {
el.addEventListener('change', update)
el.addEventListener('input', update)
@@ -161,8 +170,12 @@ export class CelestopolRoll extends Roll {
if (!rollContext) return null
const difficulty = rollContext.difficulty ?? "normal"
const diffConfig = SYSTEM.DIFFICULTY_CHOICES[difficulty] ?? SYSTEM.DIFFICULTY_CHOICES.normal
// En combat : Corps PNJ = seuil direct (pas le sélect difficulté)
const corpsPnj = isCombat ? (parseInt(rollContext.corpsPnj ?? 7) || 7) : null
const difficulty = isCombat ? "combat" : (rollContext.difficulty ?? "normal")
const diffConfig = isCombat
? { value: corpsPnj, label: "CELESTOPOL.Combat.corpsPnj" }
: (SYSTEM.DIFFICULTY_CHOICES[difficulty] ?? SYSTEM.DIFFICULTY_CHOICES.normal)
const autoSuccess = rollContext.modifier === "auto"
const modifier = autoSuccess ? 0 : (parseInt(rollContext.modifier ?? 0) || 0)
const aspectMod = parseInt(rollContext.aspectModifier ?? 0) || 0
@@ -207,6 +220,10 @@ export class CelestopolRoll extends Roll {
woundMalus: effectiveWoundMalus,
autoSuccess,
isResistance,
isCombat,
weaponType,
weaponName,
weaponDegats,
useDestin: !isResistance && useDestin,
useFortune: !isResistance && useFortune,
puiserRessources: effectivePuiser,
@@ -234,6 +251,16 @@ export class CelestopolRoll extends Roll {
}
}
// Combat mêlée échoué → joueur prend une blessure
if (isCombat && weaponType === "melee" && 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 })
// Destin utilisé → vider la jauge (reset à 0)
@@ -284,7 +311,8 @@ export class CelestopolRoll extends Roll {
/**
* Détermine succès/échec selon la marge (total seuil).
* - Marge ≥ 5 → succès critique
* - Marge 0 → succès
* - Marge > 0 → succès
* - Marge = 0 → succès (ou égalité en combat)
* - Marge ≤ 5 → échec critique
* - Marge < 0 → échec
*/
@@ -294,7 +322,9 @@ export class CelestopolRoll extends Roll {
this.options.margin = null
return
}
const threshold = SYSTEM.DIFFICULTY_CHOICES[this.options.difficulty]?.value ?? 0
const threshold = this.options.isCombat
? (this.options.difficultyValue ?? 0)
: (SYSTEM.DIFFICULTY_CHOICES[this.options.difficulty]?.value ?? 0)
if (threshold === 0) {
this.options.resultType = "unknown"
this.options.margin = null
@@ -302,7 +332,9 @@ export class CelestopolRoll extends Roll {
}
const margin = this.total - threshold
this.options.margin = margin
if (margin >= 5) this.options.resultType = "critical-success"
if (this.options.isCombat && margin === 0) {
this.options.resultType = "tie"
} else 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"
@@ -320,7 +352,9 @@ export class CelestopolRoll extends Roll {
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 threshold = this.options.isCombat
? (this.options.difficultyValue ?? 0)
: (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
@@ -337,11 +371,17 @@ export class CelestopolRoll extends Roll {
const resultClassMap = {
"critical-success": "critical-success",
"success": "success",
"tie": "tie",
"failure": "failure",
"critical-failure": "critical-failure",
"unknown": "",
}
// Libellé de difficulté : en combat, afficher "Corps PNJ : N"
const difficultyLabel = this.options.isCombat
? `${game.i18n.localize("CELESTOPOL.Combat.corpsPnj")} : ${threshold}`
: game.i18n.localize(SYSTEM.DIFFICULTY_CHOICES[this.options.difficulty]?.label ?? "")
return {
cssClass: [SYSTEM.id, "dice-roll"].join(" "),
actorId: this.actorId,
@@ -357,10 +397,11 @@ export class CelestopolRoll extends Roll {
resultClass: resultClassMap[resultType] ?? "",
isSuccess: this.isSuccess,
isFailure: this.isFailure,
isTie: this.isTie,
isCriticalSuccess: this.isCriticalSuccess,
isCriticalFailure: this.isCriticalFailure,
difficulty: this.options.difficulty,
difficultyLabel: game.i18n.localize(SYSTEM.DIFFICULTY_CHOICES[this.options.difficulty]?.label ?? ""),
difficultyLabel,
difficultyValue: threshold,
margin,
marginAbs: margin !== null ? Math.abs(margin) : null,
@@ -376,6 +417,10 @@ export class CelestopolRoll extends Roll {
woundMalus,
woundLabel,
isResistance: this.options.isResistance ?? false,
isCombat: this.options.isCombat ?? false,
weaponName: this.options.weaponName ?? null,
weaponDegats: this.options.weaponDegats ?? null,
weaponType: this.options.weaponType ?? null,
woundTaken: this.options.woundTaken ?? null,
// Dé de lune
hasMoonDie: moonDieResult !== null,