feat(combat): add activate_power_attack action and consume Up Close and Deadly deed

- New Action kind activate_power_attack: free action that sets
  state.power_attack = True when the combatant has the Power Attack feat;
  logged no-op otherwise. Routed via _execute_movement (free-action branch)
  to keep _execute under the C901 complexity ceiling.
- Consume deed_up_close_and_deadly in resolve_attack: the first attack
  resolved while the flag is set gains +1d6 if it is a ranged attack within
  30 ft; the flag is reset in all cases (benefit lost if out of range or
  not ranged). Extracted into _consume_up_close_and_deadly helper.
- Document activate_power_attack, corrected deed semantics, and the
  power_attack snapshot key in README.
- Add tests/test_engine_fixes.py (5 tests): PA activation, no-feat no-op,
  deed+ranged within 30ft, deed out-of-range, deed melee.
This commit is contained in:
2026-09-06 18:37:58 +02:00
parent e29c472a67
commit a9ccdd3a56
3 changed files with 215 additions and 3 deletions
+11 -3
View File
@@ -446,7 +446,10 @@ Règles modélisées :
moins 2 morts-vivants ennemis sont groupés à 30 ft → les blesse en 2d6 ; le camp de la
cible n'est pas pris en compte — seul `creature_type` compte, `selective_channeling` non
modélisé), sortilège `evil_eye` (nairda), deed `up_close_and_deadly`
(tammara, action bonus +1d6 à ≤ 30 ft), `inspire_courage` + `spell_combat` (misty)
(tammara, action bonus +1d6 à ≤ 30 ft — consommé sur la première attaque
résolue : si elle est à distance ET à ≤ 30 ft, +1d6 s'ajoute aux dégâts ;
sinon le bonus est perdu ; le drapeau est réinitialisé dans tous les cas),
`inspire_courage` + `spell_combat` (misty)
— couvertes par `tests/test_class_abilities.py`.
- Le Liche (`data/monsters/lich.json`) : `creature_type: "undead"` (sensible à l'énergie
positive canalisée), aura de terreur (30 ft, Volonté DD 22 ou
@@ -501,6 +504,10 @@ Règles modélisées :
`terrain_semantics` + positions). Les API de décision exposées (items L1-L8)
s'y retrouvent : `direction`/`trick_condition`/`weapon_id` sur `Action`,
`can_charge` → `ChargeResult`, kinds `retrieve_weapon`/`repair_weapon`.
Le kind `activate_power_attack` (action libre) active le don Power Attack
(`state.power_attack = True`) si le combattant possède le don ; sans le don,
c'est un no-op journalisé. Le snapshot expose l'état courant via la clé
`power_attack` (bool) par combattant.
Non modélisé : la couche `markers` (points nommés de la carte) est validée à
la lecture mais non appliquée dans la résolution.
@@ -551,8 +558,9 @@ la lecture mais non appliquée dans la résolution.
- `abilities.py` — dons et capacités : `AbilitySpec` (dataclass avec
catégorie feat/class/racial/trait) et fabriques de dons (Weapon Focus ciblé
par arme, Toughness lié au niveau, Power Attack actif −X attaque / +2X
dégâts, X = BAB//4 + 1, mêlée uniquement, declared as free action at turn
start via `state.power_attack`). Les modificateurs sont collectés par
dégâts, X = BAB//4 + 1, mêlée uniquement, activé comme action libre via le
kind `activate_power_attack` qui pose `state.power_attack`). Les
modificateurs sont collectés par
`_stat_modifiers` avec filtrage par arme (`weapon_filter`). Le champ
`features` de `Combatant` contient les aptitudes permanentes ; le chargeur
(`loaders/combatant.py`) résout les noms de dons des fiches vers des