fix(combat): evaluate conditional stat modifiers with action context

This commit is contained in:
2026-09-05 12:17:58 +02:00
parent 31d970dd5f
commit 2f2acd992f
4 changed files with 597 additions and 29 deletions
+23 -11
View File
@@ -346,6 +346,14 @@ Règles modélisées :
modificateurs d'effets (`StatModifier` avec `target` correspondant) sont
appliqués via `resolve_modifiers` (mêmes règles d'empilement que les
attaques). Renvoie un `SaveResult` (success, roll, total, dc).
- Modificateurs conditionnés : un `StatModifier` avec `condition=` n'est appliqué
que si la condition tient au moment de la résolution (`_modifier_condition_holds`) :
`target_within_30ft_ranged` (arme à distance à ≤ 30 ft), `axe` (arme dont le nom
contient « axe »), et les descripteurs de sauvegarde (`fear`, `cold`,
`electricity`, `fey`, `divination`, `poison`, `disease`, `mind_affecting`) — les
descripteurs sont portés par l'aura de terreur, les sorts (types de dégâts,
conditions de peur, écoles enchantement/divination) et les effets de zone. Toute
condition inconnue ou non supportée reste exclue (False).
- Incantation : chaque `CombatantState` porte `spell_slots_left` (initialisé depuis
le champ `spell_slots` du JSON, ex. `{"1": 4, ...}`) ; `_cast_spell` dépense
l'emplacement du niveau concerné (`spend_spell_slot`). Le DD vaut `10 + niveau du
@@ -415,10 +423,8 @@ dans la résolution) :
- Parmi les manœuvres, seules trip / disarm / bull_rush / sunder / grapple / escape
sont câblées ; les autres (overrun, dirty trick, steal, reposition, drag) ne le
sont pas. Une arme brisée ne peut pas être réparée en cours de combat.
- Les modificateurs d'effets conditionnés par un `condition=` au runtime ne sont pas
évalués (`_modifier_condition_holds` renvoie toujours False, le modificateur est
alors exclu) ; la distribution des dégâts par type sur un sort n'envoie que des
dégâts simples (sans plage « half on save » pour les effets ciblés).
- La distribution des dégâts par type sur un sort n'envoie que des dégâts simples
(sans plage « half on save » pour les effets ciblés).
## Architecture
@@ -439,7 +445,10 @@ dans la résolution) :
des attaques (couvert, ligne d'effet, pénalités de portée, CA de contact
pour les attaques touch et firearms), états de vie, incantation
(emplacements, concentration par situation, AoO d'incantation), invocation,
manœuvres, capacités de classe, transcripts, politique par défaut.
manœuvres, capacités de classe, transcripts, politique par défaut. Les
modificateurs conditionnés (`StatModifier.condition`) sont évalués via
`_modifier_condition_holds` avec le contexte d'action (arme, portée,
descripteurs de sauvegarde) ; les conditions inconnues restent exclues.
- `effects.py` — système d'effets : `StatModifier` (modificateur de
caractéristique avec type de bonus) et `resolve_modifiers` (application des
règles d'empilement PF1e). Types de bonus cumulables (dodge, racial, trait,
@@ -468,12 +477,15 @@ dans la résolution) :
sauvegarde, DC de base) et types d'effets (`DamageEffect`, `ConditionEffect`,
`HealEffect`, `BuffEffect`). Portées PF1e (personal, touch, close, medium,
long) calculées par `spell_range_ft`. Chargeur JSON (`load_spell`,
`load_spell_registry`) pour `data/spells/`. Le moteur intègre les sorts via
`Action(kind="cast_spell")` et `_cast_spell` : résolution de sauvegarde,
application des dégâts (demi-dégâts sur sauvegarde réussie ; la RD ne
s'applique pas aux dégâts de sorts), conditions, soins (plafonnés à hp_max)
et buffs. Le champ `spells` de `Combatant` contient les noms de sorts connus ;
le `spell_registry` est passé au `CombatEngine`.
`load_spell_registry`) pour `data/spells/`. Des descripteurs de sauvegarde
(`spell_save_descriptors` / `save_descriptors_for_effects` : types de dégâts,
conditions de peur, écoles enchantement/divination) sont dérivés des effets
pour évaluer les modificateurs conditionnés lors d'une sauvegarde. Le moteur
intègre les sorts via `Action(kind="cast_spell")` et `_cast_spell` :
résolution de sauvegarde, application des dégâts (demi-dégâts sur sauvegarde
réussie ; la RD ne s'applique pas aux dégâts de sorts), conditions, soins
(plafonnés à hp_max) et buffs. Le champ `spells` de `Combatant` contient les
noms de sorts connus ; le `spell_registry` est passé au `CombatEngine`.
- `metrics.py` — statistiques en forme fermée : `win_rate`, `win_rate_sigma`,
`win_rate_band` (bande 3σ bornée à [0, 1]).
- `runner.py` — `EncounterSpec`/`Side`, `build_states` (placement en zone +