From f01e061a4e0655f1192cf37565ebeca44a0d8bab Mon Sep 17 00:00:00 2001 From: ctan Date: Tue, 1 Sep 2026 17:21:58 +0200 Subject: [PATCH] docs: align README with fixed behavior --- README.md | 123 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 94 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 8390ff6..ac2c065 100644 --- a/README.md +++ b/README.md @@ -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.)