docs: align README with fixed behavior

This commit is contained in:
2026-09-04 22:56:04 +02:00
parent 0dcb967033
commit f4fc66a6ab
+94 -29
View File
@@ -335,14 +335,75 @@ 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).
- 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
sort + mod de la caractéristique d'incantation` (`casting_ability`), la sauvegarde
est résolue par `resolve_save`, et la RD ne réduit pas les dégâts des sorts
(`test_dr_does_not_reduce_spell_damage`). Incanter dans une
case menacée provoque des AoO ; tout dégât subi pendant l'incantation impose un
jet de concentration dont le DD dépend de la situation (`_concentration_dc`) :
`10 + dégâts + niveau` si blessé, `10 + CMB de l'adversaire + niveau` si agrippé,
sinon `15 + 2×niveau` (incantation défensive), jet = d20 + niveau + mod de CON
(+4 avec le don Combat Casting). `default_policy` choisit soin / offensif / buff via
`_find_heal_spell`, `_find_offensive_spell` et `_find_ally_needing_heal`.
- Invocation : les créatures invoquées agissent (calées sur la case d'initiative de
leur conjurateur via `_build_round_order`) puis expirent après leur `summon_duration`
(`_expire_summons` décrémente puis retire).
- Conditions effectives : 18 conditions prédéfinies (`conditions.py`). Leurs drapeaux
comportementaux sont appliqués par le moteur : `cant_act` (étourdi, paralysé, dazed,
endormi) annule le tour ; `cant_attack` (nauséeux) n'autorise qu'une unique action
simple et bloque toute attaque / manœuvre ; `staggered` restreint à une action
simple (pas d'attaque à outrance) ; `cant_move` bloque le déplacement ;
`denies_dex_to_ac` (à plat ventre, paralysé…) fait perdre le bonus de Dextérité à
la CA (CA flat-footed). À plat ventre : −4 CA et −4 aux attaques à distance.
Agrippé : −4 Dextérité, −2 attaque/CA/Réflexes, sans déplacement ni AoO.
- Manœuvres de combat : trip, disarm, bull_rush, sunder, grapple et escape sont
modélisées, chacune comme une tentative unique (1d20 + CMB contre CMD) :
trip → `prone` ; disarm → `disarmed` (−4 attaque) ; bull_rush → pousse la cible ;
grapple → `grappled`/`pinned` ; escape → **une** sauvegarde (bonus statique de la
compétence `escape_artist` ou CMB, + un d20) contre CMD — une seule tentative,
sans boucle multi-tour. Un sunder endommage l'**arme**, pas les PV : `str_mod + 2`
de dégâts à l'arme, qui se brise à 10 d'usure cumulée (`_WEAPON_DURABILITY`) →
`disarmed` ; une arme brisée est exclue de `usable_attacks()`. `default_policy` ne
choisit pas ces manœuvres (utilisables via `Action(kind="maneuver")` ou une
politique personnalisée).
- Combat Reflexes : 1 AoO par combattant par round par défaut ; avec le don Combat
Reflexes, `1 + mod de Dextérité` AoO par round (`_aoo_allowance`, plancher 1).
- Sortilèges du sorcier (witch hex) : le DD vaut `10 + niveau//2 + mod d'Intelligence`
(evil_eye, slumber, cackle) ; `cackle` prolonge d'un round les sortilèges déjà
actifs, sur une liste instantanée pour ne pas s'étendre à l'infini. Les sorts à
zone (AoE) utilisent le DD d'incantation du jeteur et, sur une sauvegarde réussie,
ne font que la moitié des dégâts.
- Dons : les 8 PJ portent 55 noms de dons uniques ; le chargeur résout chacun d'eux
soit vers un effet réel (Power Attack actif, Weapon Focus ciblé par arme, Toughness
lié au niveau, dons passifs via `_stat_modifiers`), soit vers un « no-op » tracé en
toute transparence (noms décoratifs / de race) — aucun n'est silencieusement ignoré
(`tests/test_feats.py`).
- Capacités de classe émises par `default_policy`, chacune conditionnée à une ressource
`> 0` dépensée dans la méthode d'exécution : `channel_energy` (oni, allié < 50 % ou
ennemis groupés à 30 ft), sortilège `evil_eye` (nairda), deed `up_close_and_deadly`
(tammara, action bonus +1d6 à ≤ 30 ft), `inspire_courage` + `spell_combat` (misty)
— couvertes par `tests/test_class_abilities.py`.
- Le Liche (`data/monsters/lich.json`) : aura de terreur (30 ft, Volonté DD 22 ou
`shaken` 1 round, au début de son tour) et contact paralysant (au toucher,
`paralysé` 1d4+1 rounds), RD 15/magie+contondant ; son JSON porte un dictionnaire
`spell_slots` sans clés dupliquées.
Non modélisé (couches `elevation`/`markers` présentes mais non appliquées
dans la résolution) :
- Manœuvres de combat.
- Effets mécaniques de hauteur/élévation.
- Tailles Large+ (2×2), allonge > 5 ft
et actions immédiates hors-tour.
- Effets mécaniques de hauteur/élévation (les hauteurs sont validées mais non
appliquées au déplacement, à la portée ou à la ligne d'effet).
- Tailles Large+ (2×2), allonge > 5 ft (la mêlée reste limitée à une case) et
actions immédiates hors-tour (consommeraient le prochain swift).
- 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).
## Architecture
@@ -360,7 +421,9 @@ dans la résolution) :
des créatures) dans `resolve_attack`.
- `combat.py` — `CombatEngine` déterministe : initiative, actions, résolution
des attaques (couvert, ligne d'effet, pénalités de portée), états de vie,
transcripts, politique par défaut.
incantation (emplacements, concentration par situation, AoO d'incantation),
invocation, manœuvres, capacités de classe, transcripts, politique par
défaut.
- `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,
@@ -373,25 +436,28 @@ dans la résolution) :
deafened, flat-footed, prone, stunned, paralyzed, nauseated, dazed,
staggered, frightened, panicked, cowering). Les modificateurs sont intégrés
à `resolve_attack` (attaque, dégâts, CA) et `resolve_save` (jets de
sauvegarde) via `_stat_modifiers` qui collecte effets + conditions.
sauvegarde) via `_stat_modifiers` qui collecte effets + conditions ; les
drapeaux comportementaux (`cant_act`, `cant_attack`, `cant_move`, staggered,
etc.) sont appliqués par le moteur pendant le tour.
- `abilities.py` — dons et capacités : `AbilitySpec` (dataclass avec
catégorie feat/class/racial/trait), dons passifs prédéfinis (Weapon Focus,
Toughness, Iron Will, Great Fortitude, Lightning Reflexes, Alertness,
Point-Blank Shot, Dodge) et don actif Power Attack (−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
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
`_stat_modifiers` avec filtrage par arme (`weapon_filter`). Le champ
`features` de `Combatant` contient les aptitudes permanentes.
`features` de `Combatant` contient les aptitudes permanentes ; le chargeur
(`loaders/combatant.py`) résout les noms de dons des fiches vers des
`AbilitySpec` réels ou des « no-op » tracés.
- `spells.py` — sorts : `SpellSpec` (dataclass avec niveau, école, portée,
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 (avec RD et demi-dégâts sur sauvegarde réussie),
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`.
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 +
@@ -408,7 +474,7 @@ dans la résolution) :
La gate de validation complète (tests + lint + types) :
```bash
uv run pytest -q # 377 tests
uv run pytest -q # 472 tests
uv run ruff check src tests
uv run basedpyright src # mode strict
```
@@ -421,17 +487,16 @@ uv run basedpyright src # mode strict
## Feuille de route
- **Phase 1** — magie et états : sorts modélisés comme effets paramétrés,
conditions, manœuvres de combat, dons et capacités de classe. Flanquement,
attaques à outrance, attaques d'opportunité, charge, retraite et pas de
placement sont déjà modélisés. Le système d'effets (`effects.py`), les
règles d'empilement des bonus, les jets de sauvegarde (`resolve_save`), le
système de conditions (`conditions.py`), les dons passifs et actifs
(`abilities.py`) et les sorts (`spells.py` + base JSON `data/spells/`)
sont en place. Le chargeur Foundry extrait automatiquement les dons
(feats, traits, racial) vers des `AbilitySpec` pré-définis et les noms
de sorts vers `Combatant.spells`.
- **Phase 1** — magie et états : largement réalisée. Sorts par emplacements
(`spells.py` + base `data/spells/`, 11 sorts), DD et sauvegardes, RD non
appliquée aux dégâts de sorts, concentration par situation, invocation,
conditions effectives (`conditions.py`), manœuvres de combat, dons (55 noms
résolus, `abilities.py` + chargeur) et capacités de classe (channel energy,
hex, deeds, inspire courage / spell combat) sont en place et documentés dans
« Règles implémentées ». Restent hors du socle (voir « Non modélisé ») : les
hauteurs/élévations, tailles Large+, allonges > 5 ft, actions immédiates
hors-tour, et le reste des manœuvres.
- **Phase 2** — couche tactique LLM : stratégies en langage naturel traduites
en politiques, balayage de matrices de positionnement.
en politiques, balayage de matrices de positionnement. (Non implémentée.)
- **Phase 3** — rapporteur LLM local : agrégation des statistiques et
rédaction du rapport d'équilibrage en français.
rédaction du rapport d'équilibrage en français. (Non implémentée.)