diff --git a/lang/fr.json b/lang/fr.json index 65373fe..dbcde01 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -108,6 +108,17 @@ "grave": "Grave", "dramatique": "Dramatique (hors combat)" }, + "Combat": { + "attack": "Attaquer", + "corpsPnj": "Corps du PNJ", + "tie": "ÉGALITÉ", + "tieDesc": "Personne n'est blessé", + "successHit": "PNJ touché — 1 blessure", + "failureHit": "Joueur touché — 1 blessure (mêlée)", + "distanceNoWound": "Raté — pas de riposte", + "weaponDamage": "dégâts supplémentaires", + "playerWounded": "Blessure infligée au joueur (mêlée)" + }, "Tab": { "main": "Principal", "competences": "Domaines", diff --git a/module/applications/sheets/base-actor-sheet.mjs b/module/applications/sheets/base-actor-sheet.mjs index e1fdd39..79594bf 100644 --- a/module/applications/sheets/base-actor-sheet.mjs +++ b/module/applications/sheets/base-actor-sheet.mjs @@ -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 diff --git a/module/documents/roll.mjs b/module/documents/roll.mjs index 158b6ca..cfe465c 100644 --- a/module/documents/roll.mjs +++ b/module/documents/roll.mjs @@ -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, diff --git a/module/models/character.mjs b/module/models/character.mjs index 290dcd9..4873e42 100644 --- a/module/models/character.mjs +++ b/module/models/character.mjs @@ -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, }) } } diff --git a/styles/roll.less b/styles/roll.less index f937532..ab2721a 100644 --- a/styles/roll.less +++ b/styles/roll.less @@ -800,3 +800,61 @@ .wound-icon { font-size: 1em; } } } + +// Bandeau "Égalité" en combat +.celestopol.chat-roll { + .roll-result-banner.tie { + background: #3a2e1a; + color: #d4b870; + border-top: 2px solid #7a6230; + border-bottom: 2px solid #7a6230; + text-shadow: 0 1px 2px rgba(0,0,0,0.6); + } + + .weapon-info-header { + display: flex; + align-items: center; + gap: 0.3em; + margin-bottom: 1px; + font-size: 0.85em; + color: var(--cel-orange, #e07b00); + .weapon-icon-small { font-size: 0.9em; } + .weapon-degats-small { + font-weight: bold; + color: #f0c060; + } + } +} + +// Zone arme dans le dialog (combat) +.fvtt-celestopol.roll-dialog { + .roll-weapon-line { + display: flex; + align-items: center; + gap: 0.4em; + margin-bottom: 2px; + font-size: 0.9em; + color: var(--cel-orange, #e07b00); + .weapon-icon { font-size: 1em; } + .weapon-degats { + font-weight: bold; + color: #f0c060; + font-size: 0.85em; + } + } + + .form-corps-pnj { + .corps-pnj-input { + width: 70px; + font-size: 1.1em; + font-weight: bold; + text-align: center; + font-family: var(--cel-font-title, "CopaseticNF", serif); + background: rgba(0,0,0,0.3); + color: var(--cel-orange, #e07b00); + border: 1px solid var(--cel-orange, #e07b00); + border-radius: 4px; + padding: 2px 4px; + } + } +} diff --git a/templates/character-equipement.hbs b/templates/character-equipement.hbs index 3f39439..95bbc9c 100644 --- a/templates/character-equipement.hbs +++ b/templates/character-equipement.hbs @@ -16,6 +16,9 @@ {{#if (eq item.system.type "melee")}}{{localize "CELESTOPOL.Weapon.typeMelee"}}{{else}}{{localize "CELESTOPOL.Weapon.typeDistance"}}{{/if}} {{localize "CELESTOPOL.Weapon.degats"}} {{item.system.degats}}
diff --git a/templates/chat-message.hbs b/templates/chat-message.hbs index 85b12b6..9d6d720 100644 --- a/templates/chat-message.hbs +++ b/templates/chat-message.hbs @@ -7,6 +7,14 @@ {{/if}}