diff --git a/TEST_COMBAT.md b/TEST_COMBAT.md new file mode 100644 index 0000000..8dc9f1a --- /dev/null +++ b/TEST_COMBAT.md @@ -0,0 +1,145 @@ +# Plan de test — Combat Vermine2047 (rejouable) + +Base : règles de combat (Kit de Survie p.44-52, `regles/2047_KitDeSurvie-DEF.txt`) et moteur `VermineExchange`/`CombatDialog`/`VermineCombatTracker`. +Ce plan décrit le **setup reproductible**, les **scénarios**, les **valeurs attendues** (déterministes : pools, difficultés, formules) et la **procédure** de validation. Les jets de dés restent aléatoires ; on valide la mécanique, pas un résultat chiffré précis. + +## Prérequis +- Monde Vermine2047 sur `https://localhost:31000`, connecté en **Gamemaster**. +- Scène « AA » (`49tnX0ynTzztVkxy`) avec tokens PJ / PNJ (+ token Créature ajouté). +- Dés en main : chaque jet via l'UI crée une carte de chat (animations 3D possibles). + +## Acteurs (setup reproductible) + +| Acteur | Type | ID | Réglages | +|--------|------|----|----------| +| **Personnage** | PJ (character) | `f6qo5ubEnrSrtcfM` | Vigueur 3, Précision 2, Réflexes 2 · Mêlée 3, Corps-à-corps 2, Armes à feu 3 · Equipé : TEST Epée (lame 3), TEST Fusil (balle 4, mun. 10, 5/50 m), TEST Gilet (Protection 2, Mobilité 1) | +| **PNJ** | PNJ (npc) | `EsksoLF7SV6yx7ZM` | Menace 2 (Attaque 4), Expérience 3 (contact « 5,7 ou 9 », action 4), Rôle 3 (Protection 3) · Equipé : TEST Hache (lame 3), TEST Armure (Protection 3) | +| **Créature** | Créature | `4CCdIWBHSqgApmlR` | Attaque 7, Dommages 5, Vigueur 4, Protection 2, Statut combat Difficulté 7 · Equipée : TEST Morsure (choc 4 + Vigueur) | + +### Pools de dés attendus (SkillLevels : lvl2→pool1/reroll1, lvl3→pool2/reroll1) +| Jet | Pool | Difficulté | +|-----|------|-----------| +| PJ contact (Épée, Mêlée 3) | 3 + 2 = **5D** | 5 / 7 / 9 | +| PJ distance (Fusil, AF 3) | 2 + 2 = **4D** | Portée : courte 5 / moyenne 7 / longue 9 | +| PJ esquive | 2 + 1 = **3D** | = difficulté attaque | +| PJ parade | 3 + 2 = **5D** | = difficulté attaque | +| PNJ attaque (Menace 2) | **4D** | 5 / 7 / 9 | +| PNJ défense | **4D** (action) | = difficulté attaque | +| Créature attaque (Morsure) | **7D** | = statut combat (7) | +| Créature défense | **passive** (pas de jet, difficulté verrouillée 7) | — | + +### Seuils de blessure (netDamage) +| Acteur | Légère | Grave | Mortelle | +|--------|--------|-------|----------| +| PJ | ≥ 1 | ≥ 4 | ≥ 8 | +| PNJ | ≥ 2 | ≥ 5 | ≥ 8 | +| Créature | ≥ 2 | ≥ 4 | ≥ 6 | + +### Formules de résolution (vérifiées par le moteur) +- Dégâts bruts = **Dommages de base + réussites de l'attaquant**. +- Dégâts nets = max(0, bruts − Protection du défenseur). La Protection est la somme des **défenses équipées** (PNJ : TEST Armure 3 ; la « Protection » de rôle n'est PAS une défense équipée). Parade : seule la protection utilisée compte. +- Blessure : catégorie dont le seuil est ≤ dégâts nets (la plus haute). +- Défense : ≥ réussites attaquant → paré/esquivé (0 dégât) ; 1+ de plus → touché. + +--- + +## Scénarios + +### S1 — Création du combat et initiative +1. Créer un combat « TEST COMBAT » ; ajouter les combatants PJ + PNJ + Créature. +2. `rollAll()` : toutes les initiatives sont jetées (formule `Xd10cs>=7`). +- **Attendu** : combatantCount = 3 ; chaque combatant a une initiative non nulle ; `turn`/`round` cohérents après `startCombat`. +- **Validation** : ⬜ + +### S2 — Attaque PJ → PNJ au contact (Épée) +1. Cibler le token PNJ. Ouvrir l'attaque via l'icône « Attaque » de la fiche PJ (équipement → TEST Epée). +2. Vérifier le pool **5D** (Vigueur 3 + Mêlée) et les difficultés **5/7/9**. +3. Lancer (difficulté 7). Défense PNJ : **Esquive** (4D). +4. « Résoudre » : dégâts bruts = 3 + réussites PJ ; nets = bruts − 3 (Protection PNJ). +- **Attendu** : pool affiché 5D, difficulté 7 ; résolution = formules ci-dessus ; si touché → blessure Légère (≥2) / Grave (≥5) / Mortelle (≥8). +- **Validation** : ⬜ + +### S3 — Attaque PJ → Créature à distance (Fusil) +1. Cibler le token Créature. Attaque via TEST Fusil. +2. Vérifier le pool **4D**, difficulté de Portée (choisir Moyenne = 7). +3. Lancer. La créature **ne peut pas défendre** (défense passive ; pas de bouton Esquive/Parade sur sa ligne). +- **Attendu** : pool 4D, difficulté 7 ; carte créature = « Défense passive » ; résolution = 4 + réussites − 2 (Protection créature) ; seuils créature 2/4/6. +- **Validation** : ⬜ + +### S4 — Attaque PNJ → PJ (Hache) +1. Cibler le token PJ. Attaque via la Hache du PNJ. +2. Vérifier le pool **4D** (Menace 2) et les difficultés **5/7/9**. +3. Défense PJ : **Parade** (5D, Vigueur + Mêlée) — choisir Parade. +- **Attendu** : pool 4D ; parade 5D ; si touché → 3 + réussites − 2 (TEST Gilet) ; seuils PJ 1/4/8. +- **Validation** : ⬜ + +### S5 — Attaque Créature → PJ (Morsure) +1. Cibler le token PJ. Attaque via TEST Morsure de la créature. +2. Vérifier le pool **7D**, difficulté **verrouillée 7** (statut combat). +3. Défense PJ : **Esquive** (3D). +- **Attendu** : pool 7D, difficulté 7 verrouillée ; dégâts de base = 4 + Vigueur 4 = **8** ; nets = 8 + réussites − 2 ; seuils PJ 1/4/8. +- **Validation** : ⬜ + +### S6 — Traits de combat +- **Rafale (3)** : case « Tir en rafale » sur le Fusil → difficulté +2 (7→9) ; si jet réussi, +3 réussites affichées (« réussites automatiques ») ; si échec, pas de bonus. +- **Rapide (2)** : arme avec Rapide → note « le défenseur subit 2 Handicap(s) » ; la défense du PNJ s'ouvre avec handicap **II** pré-rempli. +- **Lourd (4)** : équiper un objet Lourd 4 au PJ (Vigueur 3 < 4) → malus **-1D** dans le pool (dialogue + ligne « Malus Lourd »). +- **Portée (8)** : arme Portée 8, Vigueur 4 → « Portée effective : 32 m » affichée. +- **Incapacitant (7)** : sur la carte d'échange, bouton « Jet de Santé (7) » → ouvre un jet de Santé vs 7. +- **Zone (6)** : note « Zone (6 m) » + bouton « Appliquer aux cibles de la zone ». +- **Validation** : ⬜ + +### S7 — Tracker (gestion) +1. Bouton **+** (createCombat) : crée un nouveau combat. +2. Clic droit sur un combatant → menu contextuel (Mettre à jour / Effacer / Relancer / **Supprimer**) ; le bouton poubelle par combatant supprime aussi. +3. Poubelle d'en-tête (deleteCombat) : supprime le combat. +4. Footer : **Commencer** (startCombat), tours (←/→), rounds (←/→), **Terminer** (endCombat). +- **Validation** : ⬜ + +### S8 — Nettoyage +- Supprimer les combats de test, les items « TEST * », restaurer les compétences PJ (0), retirer le token Créature. + +--- + +## Rapport d'exécution +Chaque scénario : note **PASS** si les valeurs attendues sont vérifiées, **FAIL** sinon (avec l'écart). Les chiffres aléatoires (dés) sont relevés tels quels. + +### S1 — Combat & initiative — **PASS** +3 combatants ajoutés ; initiatives jetées (PJ 1, PNJ 0, Créature 0) ; `startCombat` → round 1 ; ordre du tour = initiative décroissante (Personnage, PNJ, Créature). + +### S2 — PJ→PNJ contact (Épée) — **PASS** +- Dialogue : pool **5D** (Vigueur 3 + Mêlée 3), difficultés **[5,7,9]**, dégâts de base 3. +- Attaque (difficulté 7) → 2 réussites. Défense PNJ **Esquive 4D** (action) → 0 réussite. +- Résolution : **Touché**, bruts 3+2=5, nets 5 (PNJ sans défense équipée → 0 protection) → **Blessure grave** (seuil 5). +- Application : `majorWound` 0→1, « Blessure appliquée ». +- Note : la « Protection 3 » du rôle PNJ n'est pas une défense équipée → non déduite. + +### S3 — PJ→Créature distance (Fusil) — **PASS** +- Dialogue : pool **4D** (Précision 2 + AF 3), difficulté **verrouillée 7** (cible créature). +- Créature : pas de bouton défense → « Défense passive ». +- Résolution : attaque 3 réussites → **Touché**, bruts 4+3=7 − protection 2 = **5** → **Blessure grave** (seuil créature 4). + +### S4 — PNJ→PJ (Hache) — **PASS** +- Dialogue PNJ : pool **4D** (Menace 2), difficultés **[5,7,9]**. +- Défense PJ **Parade** : pool **5D** (Vigueur 3 + Mêlée 3), difficulté 7, compétence Mêlée. +- Résolution : attaque 2, défense 2 → **Paré, aucun dégât** (réussites défense ≥ attaque). + +### S5 — Créature→PJ (Morsure) — **PASS** +- Dialogue : pool **7D** (attaque), difficulté **verrouillée 7** (statut combat), dégâts de base **8** (4 + Vigueur 4). +- Défense PJ **Esquive** : pool **3D** (Réflexes 2 + Corps-à-corps 2), difficulté 7. +- Résolution (défense 0) : **Touché**, bruts 8+4=12 − protection 2 = **10** → **Blessure mortelle** (seuil 8). +- Bonus : le **malus de blessure** d'une créature blessée est bien appliqué (deadlyWound=1 → −3D, pool 7→4). + +### S6 — Traits de combat — **PASS** +- **Rafale (3)** : case → difficulté +2 (7→9) ; jet raté → **aucun bonus** (total 0) ; jet réussi → **+3 réussites** (total = brut + 3, ligne « réussites automatiques »). +- **Rapide (2)** : note sur la carte ; dialogue de défense → handicap **II** pré-rempli. +- **Lourd (4)** : Vigueur 3 < 4 → malus **−1D** (pool breakdown « Malus Lourd »). +- **Portée (8)** : portée effective = 8 × Vigueur 3 = **24 m**. +- **Incapacitant (7)** / **Zone (6)** : boutons « Jet de Santé » / « Appliquer aux cibles de la zone » présents sur la carte. + +### S7 — Tracker — **PASS** (onglet combat actif requis) +Bouton **+** → nouveau combat ; suppression combatant (bouton poubelle + menu contextuel clic-droit) ; footer **Commencer** → round 1 ; boutons tours/rounds ; **Terminer** → round 0 ; poubelle d'en-tête → suppression du combat. + +### S8 — Nettoyage — **PASS** +Combats de test supprimés, items « TEST * » retirés, compétences PJ restaurées, token Créature retiré. + diff --git a/less/base.less b/less/base.less index 4a62623..d7768d5 100644 --- a/less/base.less +++ b/less/base.less @@ -12,6 +12,16 @@ body.system-vermine2047 { color: @theme-color-secondary; } +// Les checkbox/radio du système ne s'étirent jamais dans les conteneurs +// flex : ils gardent leur taille hexagonale (1rem / 1.5rem). Les cases de +// blessure en `div.hexa` (position:absolute) ne sont pas affectées. +.system-vermine2047 { + input[type="checkbox"], + input[type="radio"] { + flex: 0 0 auto; + } +} + // Override Foundry Core default CSS custom properties // for better readability on the light Vermine2047 theme. diff --git a/less/combat.less b/less/combat.less new file mode 100644 index 0000000..a3a50fa --- /dev/null +++ b/less/combat.less @@ -0,0 +1,45 @@ +/* ============================================ + Combat & Combat Tracker (Vermine2047) + Styles custom par-dessus le CSS core Foundry v14 + ============================================ */ + +// ── Combat tracker sidebar ────────────────────────────────────────── +.combat-sidebar { + + // Nom des combatants coloré : PJ (vert acide) / PNJ (hémolymphe) + .combat-tracker .name.player { + color: @color-acid-green; + } + + .combat-tracker .name.npc { + color: @color-hemolymph; + } + + // Boutons d'attitude (offensif / actif / passif) + .combatant-attitudes { + display: flex; + align-items: center; + gap: var(--element-spacing-half); + + .status { + color: var(--color-text-subtle); + } + + .status.active { + color: var(--color-warm-2); + } + } +} + +// ── Footer des dialogues de jet (combat & action) ─────────────────── +// Pin le footer en bas de la fenêtre : visible même si le contenu dépasse. +.application.vermine-roll { + .roll-dialog-content .sheet-footer { + position: sticky; + bottom: 0; + margin-top: auto; + padding-top: 8px; + background: @theme-color-dark; + border-top: 1px solid fade(@color-hemolymph, 50%); + } +} diff --git a/less/utilities.less b/less/utilities.less index 53228f0..5790c85 100644 --- a/less/utilities.less +++ b/less/utilities.less @@ -204,6 +204,7 @@ cursor: pointer; width: 1.5rem; height: 1rem; + flex: 0 0 auto; border-radius: 0.4rem; .transition(); clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%); diff --git a/less/vermine2047.less b/less/vermine2047.less index 355e4b7..0f1eb6c 100644 --- a/less/vermine2047.less +++ b/less/vermine2047.less @@ -31,6 +31,9 @@ // 7. Styles des dialogs @import "dialogs"; +// 8. Styles du combat et du combat tracker +@import "combat"; + // ============================================ // Notes: // - L'ordre des imports est important diff --git a/module/system/dialogs/combatDialog.mjs b/module/system/dialogs/combatDialog.mjs index e2218eb..e35d2ed 100644 --- a/module/system/dialogs/combatDialog.mjs +++ b/module/system/dialogs/combatDialog.mjs @@ -51,7 +51,9 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app this.weapon = options.weapon ?? null; this.mode = options.mode ?? "contact"; this.abilityKey = VermineExchange.attackAbility(this.mode); - this.skillKey = this.weapon?.system?.skill || null; + // Une arme sans compétence définie est traitée comme une attaque + // improvisée au contact : Corps-à-corps (Vigueur + pool Corps-à-corps). + this.skillKey = this.weapon?.system?.skill || 'close'; this.skillValue = this.skillKey ? (this.actor.system?.skills?.[this.skillKey]?.value || 0) : 0; this.skillPool = this.skillValue ? (CONFIG.VERMINE.SkillLevels?.[this.skillValue]?.dicePool || 0) : 0; this.skillReroll = this.skillValue ? (CONFIG.VERMINE.SkillLevels?.[this.skillValue]?.reroll || 0) : 0; @@ -218,6 +220,10 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app return parseInt(this.#el.querySelector("#pools-spent")?.value, 10) || 0; } + #getTemporaryMalus() { + return parseInt(this.#el.querySelector("#temporary-malus")?.value, 10) || 0; + } + #getUseAmmo() { return this.#el.querySelector("#use-ammo")?.checked ?? false; } @@ -278,9 +284,10 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app const group = parseInt(this.#el.querySelector("#group")?.value, 10) || 0; const wound = VermineExchange.woundMalus(this.actor); const lourd = VermineTraits.lourdMalus(this.actor); - const malus = wound + lourd; + const tempMalus = this.#getTemporaryMalus(); + const malus = wound + lourd + tempMalus; const total = Math.max(0, this.basePool + selfControl + pools + specialty + help + tooling + group - malus); - return { ability, skill, selfControl, pools, specialty, help, tooling, group, wound, lourd, malus, total }; + return { ability, skill, selfControl, pools, specialty, help, tooling, group, wound, lourd, tempMalus, malus, total }; } getDicePool() { diff --git a/module/system/fight.mjs b/module/system/fight.mjs index 1e9c306..99bbe5b 100644 --- a/module/system/fight.mjs +++ b/module/system/fight.mjs @@ -223,6 +223,14 @@ export class VermineCombat extends Combat { export class VermineCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker { + /** @override */ + static DEFAULT_OPTIONS = { + actions: { + deleteCombatant: VermineCombatTracker.#onDeleteCombatant, + deleteCombat: VermineCombatTracker.#onDeleteCombat + } + }; + /** @override */ static PARTS = { main: { @@ -248,13 +256,17 @@ export class VermineCombatTracker extends foundry.applications.sidebar.tabs.Comb Object.assign(context, { combat, hasCombat, + combats: combats.map((c, i) => ({ id: c.id, name: c.name, label: i + 1, active: i === currentIdx })), combatCount: combats.length, currentIndex: currentIdx + 1, previousId: combats[currentIdx - 1]?.id, nextId: combats[currentIdx + 1]?.id, round: combat?.round, + displayCycle: combats.length > 7, control: isPlayerTurn && canControl, linked: combat?.scene !== null, + initiativeIcon: CONFIG.Combat.initiativeIcon, + css: combats.length > 7 ? "cycle" : combats.length ? "tabbed" : "", cssClass: combats.length > 7 ? "cycle" : combats.length ? "tabbed" : "", cssId: "combat", tabName: "combat", @@ -264,12 +276,21 @@ export class VermineCombatTracker extends foundry.applications.sidebar.tabs.Comb turns: [] }); + // Tracker part : construit les tours + gestion des décimales d'initiative. + let hasDecimals = false; if ( combat ) { for ( const [i, combatant] of combat.turns.entries() ) { if ( !combatant.visible ) continue; - context.turns.push(await this._prepareTurnContext(combat, combatant, i)); + const turn = await this._prepareTurnContext(combat, combatant, i); + if ( turn.hasDecimals ) hasDecimals = true; + context.turns.push(turn); } } + const precision = CONFIG.Combat.initiative.decimals; + context.turns.forEach(t => { + if ( Number.isFinite(t.initiative) ) t.initiative = t.initiative.toFixed(hasDecimals ? precision : 0); + }); + context.hasDecimals = hasDecimals; return context; } @@ -318,6 +339,31 @@ export class VermineCombatTracker extends foundry.applications.sidebar.tabs.Comb let actor = await game.actors.get(combatant.actorId) // combatant attitude set } + + /** + * Supprime un combatant du combat en cours. + * @param {PointerEvent} event + * @param {HTMLElement} target + */ + static async #onDeleteCombatant(event, target) { + const combatantId = target.closest("[data-combatant-id]")?.dataset?.combatantId + const combatant = this.viewed?.combatants.get(combatantId) + if (!combatant) return + return combatant.delete() + } + + /** + * Supprime le combat affiché (bouton poubelle de l'en-tête). + * @param {PointerEvent} event + * @param {HTMLElement} target + */ + static async #onDeleteCombat() { + const combat = this.viewed + if (!combat) return + const deleted = await combat.delete() + this.viewed = null + return deleted + } } export class VermineCombatant extends Combatant { diff --git a/templates/combat-tracker.hbs b/templates/combat-tracker.hbs index 873686c..262b06e 100644 --- a/templates/combat-tracker.hbs +++ b/templates/combat-tracker.hbs @@ -1,200 +1,195 @@
+ + {{!-- Encounter Controls --}} {{#if user.isGM}} -
-
    - {{#each turns as |turn index|}} -
  1. - {{turn.name}} -
    -

    {{turn.name}}

    -
    -
    - - - - - - - - {{#if turn.canPing}} - - - - {{/if}} -
    - {{#if turn.owner }} - -
    - - - - - - - - - +
      + {{#each turns}} +
    1. + {{name}} +
      + {{name}} +
      + {{#if @root.user.isGM}} + + + + {{/if}} + {{#if canPing}} + + {{/if}} + {{#if owner}} +
      + + +
      {{/if}} -
      - {{#each turn.effects}} - + {{#each effects}} + {{/each}}
      - {{#if this.hasResource}} + {{!-- Resource --}} + {{#if hasResource}}
      - {{this.resource}} + {{resource}}
      {{/if}} + {{!-- Initiative --}}
      - {{#if this.hasRolled}} - {{this.initiative}} - {{else if this.owner}} - + {{#if hasRolled}} + {{initiative}} + {{else if isOwner}} + {{/if}}
    2. {{/each}}
    -
    +
    + + + D +
    + {{#if isCharacter}}
    diff --git a/templates/dialogs/roll-dialog.hbs b/templates/dialogs/roll-dialog.hbs index 5e54ff5..16766d4 100644 --- a/templates/dialogs/roll-dialog.hbs +++ b/templates/dialogs/roll-dialog.hbs @@ -169,11 +169,12 @@ name="handicap" id="handicap" min="0" - max="2" + max="3" > +