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:
@@ -22,6 +22,7 @@ export default class CelestopolActorSheet extends HandlebarsApplicationMixin(fou
|
||||
toggleSheet: CelestopolActorSheet.#onToggleSheet,
|
||||
edit: CelestopolActorSheet.#onItemEdit,
|
||||
delete: CelestopolActorSheet.#onItemDelete,
|
||||
attack: CelestopolActorSheet.#onAttack,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -151,6 +152,12 @@ export default class CelestopolActorSheet extends HandlebarsApplicationMixin(fou
|
||||
await item?.deleteDialog()
|
||||
}
|
||||
|
||||
static async #onAttack(_event, target) {
|
||||
const itemId = target.getAttribute("data-item-id")
|
||||
if (!itemId) return
|
||||
await this.document.system.rollAttack(itemId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup sequential checkbox logic for wound/destin/spleen tracks
|
||||
* Only allows checking the next checkbox in sequence
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -239,18 +239,39 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
|
||||
const statData = this.stats[statId]
|
||||
if (!statData) return null
|
||||
|
||||
/**
|
||||
* Lance une attaque avec une arme (test Échauffourée vs Corps PNJ).
|
||||
* Mêlée : échec → blessure joueur auto-cochée.
|
||||
* Distance : échec → simple raté, pas de blessure joueur.
|
||||
* Égalité (marge=0) → personne n'est blessé.
|
||||
* @param {string} itemId - Id de l'item arme
|
||||
*/
|
||||
async rollAttack(itemId) {
|
||||
const { CelestopolRoll } = await import("../documents/roll.mjs")
|
||||
const item = this.parent.items.get(itemId)
|
||||
if (!item || item.type !== "weapon") return null
|
||||
|
||||
const echauffouree = this.stats.corps.echauffouree
|
||||
if (!echauffouree) 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,
|
||||
actorId: this.parent.id,
|
||||
actorName: this.parent.name,
|
||||
actorImage: this.parent.img,
|
||||
statId: "corps",
|
||||
skillId: "echauffouree",
|
||||
statLabel: SYSTEM.STATS.corps.label,
|
||||
skillLabel: SYSTEM.SKILLS.corps.echauffouree.label,
|
||||
skillValue: echauffouree.value,
|
||||
woundMalus: this.getWoundMalus(),
|
||||
woundLevel: this.blessures.lvl,
|
||||
rollMoonDie: this.prefs.rollMoonDie ?? false,
|
||||
destGaugeFull: this.destin.lvl > 0,
|
||||
fortuneValue: this.attributs.fortune.value,
|
||||
isCombat: true,
|
||||
weaponType: item.system.type,
|
||||
weaponName: item.name,
|
||||
weaponDegats: item.system.degats,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user