From 2e49aa7293852528c5fd9d5f2ea971781600fd5e Mon Sep 17 00:00:00 2001 From: Thien An Date: Sat, 29 Aug 2026 13:37:32 +0200 Subject: [PATCH] docs(handoff): add finalize-engine plan for session transfer --- _session_handoff/finalize-engine-plan.md | 261 +++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 _session_handoff/finalize-engine-plan.md diff --git a/_session_handoff/finalize-engine-plan.md b/_session_handoff/finalize-engine-plan.md new file mode 100644 index 0000000..c2d26cc --- /dev/null +++ b/_session_handoff/finalize-engine-plan.md @@ -0,0 +1,261 @@ +# finalize-engine - Work Plan + +## TL;DR (For humans) + + + +**What you'll get:** Un moteur de combat Pathfinder complet pour la reprise de campagne : les sorts sont enfin lancés (avec concentration, portées, effets de zone, invocations et métamagie), les durées des buffs sont respectées, les conditions (étourdi, paralysé, paniqué…) bloquent réellement les actions, les ressources (grit, channel, hexes) sont comptées, les compétences et les manœuvres de combat (y compris la lutte complète) fonctionnent, et les 8 personnages jouables disposent de leurs dons, sorts et capacités de classe. + +**Why this approach:** Fondations correctes d'abord (durées, flags, DC, ressources) car toute la magie et les systèmes de combat s'appuient dessus ; ensuite les systèmes génériques (compétences, manœuvres) ; enfin le contenu des 8 personnages. Le moteur devient générique : n'importe quelle entrée du contenu Pathfinder complet (hors périmètre) fonctionnera sans nouvelle mécanique. + +**What it will NOT do:** Pas d'encodage de la base complète Pathfinder (9500 entrées), pas de couche LLM (tactiques/rapporteur), pas de tailles Large+, pas de 3D/élévation, aucune modification des fiches Foundry d'origine. + +**Effort:** Large +**Risk:** Medium - 20 étapes couplées (durées/flags) avec gate stricte à chaque commit +**Decisions to sanity-check:** (1) DC des sorts = 10 + niveau du sort + modificateur de la stat de casting ; (2) ressources au niveau du personnage (slots/grit/channel/hexes) consommées en combat ; (3) sorts non mappables → `effects=[]` (inertes) plutôt que de les inventer. + +Your next move: approuve le plan, puis lance l'exécution (`$start-work`). Full execution detail follows below. + +--- + +> TL;DR (machine): Large effort, Medium risk - 20 commits gate-verts pour finaliser le moteur PF1e (fondations → magie → systèmes → contenu 8 PJ → validation), ~401 → ~500 tests. + +## Scope +### Must have +- **A. Fondations** : durées d'effets/conditions avec expiration (`rounds_left`), enforcement des flags comportementaux (cant_act, cant_move, cant_attack, cant_charge, cant_aoo, must_flee, denies_dex_to_ac, speed_mult), formule DC correcte (10 + niveau sort + mod. stat de casting), système de ressources (slots, grit, channel, hexes, rounds bardique). +- **B. Magie** : politique de sort dans `default_policy` (les casters lancent enfin des sorts), AoO sur incantation + jets de concentration, AoE/multi-cibles (fireball 20 ft), summoning complet (durée, insertion initiative), métamagie (Empower/Maximize/Extend). +- **C. Systèmes de combat** : compétences complètes (37, checks, rangs, class skills, armor penalty), manœuvres complètes (CMB/CMD : trip, disarm, bull rush, sunder), lutte complète (maintain/pin/escape/release). +- **D. Contenu des 8 PJ** : 55 feats → `AbilitySpec`, 101 sorts → `data/spells/*.json`, capacités de classe (mount, hex, channel, deed, domain, spell combat, performance), monstre complexe exemple (liche). +- **E. Validation** : transcripts épinglés par système, golden tests 8 PJ (`tests/test_pc_combat_golden.py`), gate finale + README à jour. + +### Must NOT have (guardrails, anti-slop, scope boundaries) +- PAS la base de données complète PF1e (3400 feats, 3100 sorts, 2000 traits) — le moteur les supporte, on ne les encode pas. +- PAS de couche LLM (Phases 2/3 du plan initial : tactiques et rapporteur LLM). +- PAS de modifications de `fiches_personnages/`, `.omo/` (hors ce plan), `.codegraph/`. +- PAS de tailles Large+ (2×2) ni de déplacement en 3D/élévation. +- PAS d'AoE en ligne/cone (burst uniquement), PAS de swallow whole, PAS de phylactère. +- PAS de réécriture des règles d'empilement des bonus (`resolve_modifiers`) ni des définitions de conditions existantes. + +## Verification strategy +> Zero human intervention - all verification is agent-executed. +- Test decision: **TDD** (transcript épinglé ou test d'assertion d'abord, implémentation ensuite) + pytest. +- Evidence: `.omo/evidence/task--finalize-engine.md` (un fichier par todo, documentant commande exécutée + résultat). attemptDir = `.omo/evidence/` (hors ulw-loop). +- Gate par commit (obligatoire, verte avant chaque commit) : + `export PATH="$HOME/.local/bin:$PATH" && uv run ruff check src tests && uv run basedpyright src && uv run pytest -q` +- État initial : **401 tests verts**, ruff + basedpyright propres (commit `82148ce`). + +## Execution strategy +### Parallel execution waves +> Target 5-8 todos per wave. Fewer than 3 (except the final) means you under-split. + +- **Wave 1** (fondations indépendantes) : 1, 3, 4, 9 +- **Wave 2** : 2 (←1), 6 (←2), 10 +- **Wave 3** : 5 (←2+6), 7, 11 (←10), 8 +- **Wave 4** : 12 (←11+10), 13 (←2+10), 14 (←7+8+9) +- **Wave 5** : 15, 16 (←4), 17 (←4+7+14), 18 (←14) +- **Wave 6** (finale) : 19 (←tous), 20 (←19) + +### Dependency matrix +| Todo | Depends on | Blocks | Can parallelize with | +| --- | --- | --- | --- | +| 1 Durées | — | 2 | 3, 4, 9 | +| 2 Flags | 1 | 5, 6, 13 | 3, 4, 9 | +| 3 Formule DC | — | 14 | 1, 2, 4, 9 | +| 4 Ressources | — | 16, 17 | 1, 2, 3, 9 | +| 5 Politique de sort | 2, 6 | 19 | 7, 8, 11 | +| 6 AoO + concentration | 2 | 5 | 7, 8, 10, 11 | +| 7 AoE | — | 14, 17 | 5, 8, 11 | +| 8 Summoning | — | 14 | 5, 7, 11 | +| 9 Métamagie | — | 14 | 1, 3, 4 | +| 10 Compétences | — | 11, 12, 13 | 2, 6, 7 | +| 11 Manœuvres | 10 | 12 | 5, 7, 8 | +| 12 Lutte | 11, 10 | 19 | 13, 14 | +| 13 Feats 8 PJ | 2, 10 | 19 | 12, 14 | +| 14 101 sorts | 7, 8, 9 | 17, 18, 19 | 12, 13 | +| 15 Compagnons | — | 19 | 16, 17, 18 | +| 16 Channel/hexes | 4 | 19 | 15, 17, 18 | +| 17 Deeds/spell combat | 4, 7, 14 | 19 | 15, 16, 18 | +| 18 Liche | 14 | 19 | 15, 16, 17 | +| 19 Golden tests | 2, 5, 12, 13, 14, 15, 16, 17, 18 | 20 | — | +| 20 Gate finale | 19 | — | — | + +## Todos +> Implementation + Test = ONE todo. Never separate. + +- [ ] 1. Durées d'effets et de conditions (expiration) + What to do / Must NOT do: Introduire `ActiveBuff` et `ActiveCondition` (dataclasses avec `rounds_left`, source, effet/condition référencés). Remplacer `state.effects: list[StatModifier]` par `state.active_buffs: list[ActiveBuff]` et `state.conditions: list[Condition]` par `state.active_conditions: list[ActiveCondition]`. Au début du tour de la créature (`_take_turn`), DÉCRÉMENTER `rounds_left` et expirer les entrées à 0 (au lieu de `state.effects.clear()`). La pénalité de charge −2 CA devient un `ActiveBuff` de 1 round. `_stat_modifiers` et `resolve_attack`/`resolve_save` doivent lire les buffs actifs via leurs effets. MUST NOT: toucher aux règles d'empilement (`resolve_modifiers`), ni aux définitions de conditions. + Parallelization: Wave 1 | Blocked by: — | Blocks: 2 + References (executor has NO interview context - be exhaustive): `src/pf1e_simulator/combat.py:169-193` (CombatantState), `combat.py:284-299` (`_take_turn` — le `state.effects.clear()` à corriger), `combat.py:380-399` (`_stat_modifiers`), `combat.py:829-866` (`_apply_spell_effects` — application des buffs/conditions), `combat.py:848-865` (log `duration_rounds`), `src/pf1e_simulator/effects.py:49-65` (StatModifier), `src/pf1e_simulator/conditions.py:21-39` (Condition), `tests/test_combat.py` (pattern transcript ScriptedRng), `src/pf1e_simulator/rng.py` (ScriptedRng) + Acceptance criteria (agent-executable): `uv run pytest -q` vert avec les nouveaux transcripts ; test épinglé : un buff `duration_rounds=3` persiste ≥3 tours ; la pénalité de charge disparaît après le tour du chargeur. + QA scenarios (name the exact tool + invocation): happy — `uv run pytest -q tests/test_combat.py -k "duration or charge"` passe ; failure — transcript où un buff expire après 1 tour doit échouer (pin). Evidence `.omo/evidence/task-1-finalize-engine.md` + Commit: Y | feat(combat): effect and condition durations + +- [ ] 2. Enforcement des flags comportementaux + What to do / Must NOT do: Appliquer les flags déclarés dans `conditions.py:31-39` partout où le moteur prend des décisions : `cant_act` → tour sauté (aucune action) ; `cant_move` → aucun déplacement (move/charge/retraite/5ft-step) ; `cant_attack` → aucune attaque ; `cant_charge` ; `cant_aoo` → la créature ne porte jamais d'AoO (`_resolve_aoo`) ; `must_flee` → la politique renvoie `(withdraw,)` en s'éloignant de l'ennemi le plus proche ; `denies_dex_to_ac` → la cible est traitée en flat-footed (mod DEX retiré de la CA) ; `speed_mult` → budget de mouvement multiplié. MUST NOT: modifier les définitions de conditions ni `_stat_modifiers`. + Parallelization: Wave 2 | Blocked by: 1 | Blocks: 5, 6, 13 + References (executor has NO interview context - be exhaustive): `src/pf1e_simulator/conditions.py:31-39` (champs flags), `src/pf1e_simulator/combat.py:284-299` (`_take_turn` — gate d'action), `combat.py:498-518` (`_roll_initiative`), `combat.py:535-544` (`_enemies_threatening`), `combat.py:546-558` (`_resolve_aoo`), `combat.py:1126-1152` (`default_policy`), `combat.py:360-380` (mouvement / budget de déplacement), `src/pf1e_simulator/models.py` (ACProfile — flat-footed) + Acceptance criteria (agent-executable): `uv run pytest -q` vert ; transcripts épinglés : un combattant `stunned` ne fait aucune action ; `panicked` fuit (withdraw) ; une cible `blinded` perd le mod DEX à la CA ; `speed_mult=0.5` parcourt la moitié. + QA scenarios (name the exact tool + invocation): happy — `uv run pytest -q tests/test_combat.py -k "stun or panic or blind or speed"` passe ; failure — tour non sauté pour un stun doit échouer. Evidence `.omo/evidence/task-2-finalize-engine.md` + Commit: Y | feat(combat): enforce behavioral condition flags + +- [ ] 3. Formule DC correcte des sorts + What to do / Must NOT do: Ajouter `casting_ability: str` (nom de la stat : str/dex/con/int/wis/cha) à `Combatant` (modèle) et au JSON des combattants (champ optionnel avec défaut). Remplacer `dc = spell.base_dc + caster.combatant.level` (`combat.py:807`) par `dc = 10 + spell.level + modificateur(casting_ability)`. Enrichir les 8 JSON `data/pcs/*.json` avec la bonne stat (tammara: wis, nairda: int, misty: cha (scion) / int, oni: wis, esha: int). MUST NOT: toucher à `resolve_save` ni aux DC des capacités autres que sorts. + Parallelization: Wave 1 | Blocked by: — | Blocks: 14 + References (executor has NO interview context - be exhaustive): `src/pf1e_simulator/combat.py:781-827` (`_cast_spell` — DC à la ligne ~807), `src/pf1e_simulator/models.py:93-115` (Combatant), `src/pf1e_simulator/spells.py:120-132` (`load_spell`), `tests/test_combat.py` (`test_dc_is_base_plus_level` à mettre à jour), `data/pcs/*.json` (8 fichiers), `src/pf1e_simulator/loaders/combatant.py:107-147` (`load_combatant`) + Acceptance criteria (agent-executable): `uv run pytest -q` vert ; test mis à jour : DC = 10 + niveau du sort + mod. stat ; vérifier un DC concret (ex. sort niv 2, mod wis +3 → DC 15). + QA scenarios (name the exact tool + invocation): happy — `uv run pytest -q tests/test_combat.py -k "dc"` passe ; failure — ancienne formule (base_dc + level) doit échouer. Evidence `.omo/evidence/task-3-finalize-engine.md` + Commit: Y | fix(combat): correct spell DC formula + +- [ ] 4. Système de ressources + What to do / Must NOT do: Ajouter `resources: dict[str, int]` (max par combat : slots de sort, grit, channel_energy, hexes, bardic_rounds) à `Combatant`, et `resources_left: dict[str, int]` à `CombatantState` (initialisé depuis `resources` à `build_states`). Méthode `spend_resource(name, n=1) -> bool` sur l'état. Charger depuis JSON (champ optionnel). Pré-remplir `tammara.json` (grit: 3) et `oni.json` (channel_energy: 6). MUST NOT: implémenter les consommateurs (deeds/channel/hexes = todos 16-17) — uniquement le socle + tests. + Parallelization: Wave 1 | Blocked by: — | Blocks: 16, 17 + References (executor has NO interview context - be exhaustive): `src/pf1e_simulator/models.py:93-115` (Combatant), `src/pf1e_simulator/combat.py:169-193` (CombatantState), `src/pf1e_simulator/runner.py` (`build_states` — initialisation par combat), `src/pf1e_simulator/loaders/combatant.py:107-147`, `data/pcs/tammara.json`, `data/pcs/oni.json` + Acceptance criteria (agent-executable): `uv run pytest -q` vert ; test : `spend_resource` décrémente et refuse en dessous de 0 ; loader remplit `resources` depuis JSON. + QA scenarios (name the exact tool + invocation): happy — `uv run pytest -q tests/test_combatant_loader.py -k "resource"` + test unitaire passent ; failure — dépense au-delà du max doit échouer (pin). Evidence `.omo/evidence/task-4-finalize-engine.md` + Commit: Y | feat(models): combat resources + +- [ ] 5. default_policy lance des sorts + What to do / Must NOT do: Étendre `default_policy` (`combat.py:1126-1152`) : (1) soin si un allié est à <50% PV et un sort de soin est disponible ; (2) offensif si un ennemi est dans la portée du sort ; (3) buff sinon ; (4) repli sur le comportement actuel (full_attack/charge/5ft-step/move+attack) sinon ; (5) JAMAIS de sort si menacé sans concentration (voir todo 6). Vérifier la ressource (slots) avant de lancer. MUST NOT: réécrire les branches non-sort existantes, ni la mécanique de `_cast_spell`. + Parallelization: Wave 3 | Blocked by: 2, 6 | Blocks: 19 + References (executor has NO interview context - be exhaustive): `src/pf1e_simulator/combat.py:1126-1152` (`default_policy`), `combat.py:781-827` (`_cast_spell`), `src/pf1e_simulator/spells.py` (SpellSpec : level, range, effects), `data/pcs/nairda.json` (32 sorts), `tests/test_combat.py` (pattern transcript) + Acceptance criteria (agent-executable): `uv run pytest -q` vert ; transcript épinglé : nairda (ou un caster) lance un sort dans un combat Monte Carlo 1v1 ; un caster à court de slots ne lance plus. + QA scenarios (name the exact tool + invocation): happy — `uv run pytest -q tests/test_combat.py -k "cast"` passe ; failure — caster qui n'a jamais lancé en 5 rounds doit échouer. Evidence `.omo/evidence/task-5-finalize-engine.md` + Commit: Y | feat(combat): spellcasting default policy + +- [ ] 6. AoO sur incantation + concentration + What to do / Must NOT do: Étendre `_check_provoked_aoo` (`combat.py:560`) : lancer un sort depuis une case menacée provoque une AoO de chaque ennemi menaçant (une par round, piste `_aoo_used`). Si touché pendant l'incantation : jet de concentration `DC = 15 + 2 × niveau du sort` (bonus Combat Casting +4 le cas échéant) ; échec → sort perdu (fizzle). MUST NOT: modifier les AoO liées au mouvement ni au tir. + Parallelization: Wave 2 | Blocked by: 2 | Blocks: 5 + References (executor has NO interview context - be exhaustive): `src/pf1e_simulator/combat.py:535-544` (`_enemies_threatening`), `combat.py:546-558` (`_resolve_aoo`), `combat.py:560` (`_check_provoked_aoo`), `combat.py:781-827` (`_cast_spell`), `src/pf1e_simulator/dice.py` (jet), `src/pf1e_simulator/rng.py` (ScriptedRng pour transcripts) + Acceptance criteria (agent-executable): `uv run pytest -q` vert ; transcript : un lanceur menacé subit l'AoO, échoue sa concentration (DC volontairement dépassé via ScriptedRng), et le sort fizzle (aucun effet appliqué). + QA scenarios (name the exact tool + invocation): happy — `uv run pytest -q tests/test_combat.py -k "concentrat"` passe ; failure — sort appliqué malgré concentration ratée doit échouer. Evidence `.omo/evidence/task-6-finalize-engine.md` + Commit: Y | feat(combat): AoO and concentration on casting + +- [ ] 7. AoE / multi-cibles + What to do / Must NOT do: Ajouter `AreaEffect` (shape="burst", `radius_ft`, `save_for_half: bool`) aux types d'effets (`spells.py:32-57`). Dans `_apply_spell_effects` (`combat.py:829-866`), quand l'effet est une zone : cibler une case, collecter toutes les créatures dans le rayon (distance via `grid.distance`), résoudre la sauvegarde par cible (demi-dégâts si réussie). Implémenter fireball (20-ft burst) dans `data/spells/`. MUST NOT: lignes ni cônes. + Parallelization: Wave 3 | Blocked by: — | Blocks: 14, 17 + References (executor has NO interview context - be exhaustive): `src/pf1e_simulator/spells.py:32-57` (types d'effets), `combat.py:829-866` (`_apply_spell_effects`), `src/pf1e_simulator/grid.py` (`distance`), `combat.py:481-496` (`resolve_save`), `tests/test_spells.py:113-124` (test all-10-spells), `data/spells/` (10 JSON existants) + Acceptance criteria (agent-executable): `uv run pytest -q` vert ; transcript : un fireball touche ≥2 créatures dans le rayon, la sauvegarde réussie divise les dégâts par 2. + QA scenarios (name the exact tool + invocation): happy — `uv run pytest -q tests/test_spells.py -k "area or fireball"` passe ; failure — fireball à cible unique doit échouer. Evidence `.omo/evidence/task-7-finalize-engine.md` + Commit: Y | feat(combat): area-of-effect spells + +- [ ] 8. Summoning complet + What to do / Must NOT do: Ajouter `SummonEffect` (`creature: str`, `duration_rounds: int`) aux types d'effets. À l'incantation : charger le statblock (ex. `data/monsters/celestial_wolf.json` à créer), créer un `CombatantState` adjacent au lanceur, l'insérer DANS l'ordre d'initiative juste après le lanceur, même camp, avec `default_policy` ; expirer (retrait) après `duration_rounds` tours de la créature. MUST NOT: summon multiples ni contrôle fin (le summoner ne dirige pas les actions tour par tour). + Parallelization: Wave 3 | Blocked by: — | Blocks: 14 + References (executor has NO interview context - be exhaustive): `src/pf1e_simulator/spells.py:32-57` (types d'effets), `combat.py:829-866` (`_apply_spell_effects`), `combat.py:169-193` (CombatantState), `combat.py:284-299` (`_take_turn` — itération d'initiative), `src/pf1e_simulator/runner.py` (`build_states`), `src/pf1e_simulator/loaders/combatant.py:107-147` (recharger un statblock), `data/monsters/goblin.json` (schéma à copier pour `celestial_wolf.json`) + Acceptance criteria (agent-executable): `uv run pytest -q` vert ; transcript : la créature invoquée agit après le lanceur et disparaît après la durée. + QA scenarios (name the exact tool + invocation): happy — `uv run pytest -q tests/test_combat.py -k "summon"` passe ; failure — créature encore présente après expiration doit échouer. Evidence `.omo/evidence/task-8-finalize-engine.md` + Commit: Y | feat(combat): summoning + +- [ ] 9. Métamagie + What to do / Must NOT do: Support générique feat→modification de `SpellSpec` appliqué à la volée au lancement (aucune duplication de données) : Empower (dégâts ×1.5), Maximize (dés max), Extend (durée ×2). Ajouter `metamagic_feats: list[str]` à `Combatant` + champ JSON. Appliquer dans `_cast_spell`. MUST NOT: Heighten/Persistent, ni nouvelles entrées de sorts par variante. + Parallelization: Wave 1 | Blocked by: — | Blocks: 14 + References (executor has NO interview context - be exhaustive): `src/pf1e_simulator/spells.py` (SpellSpec, DamageEffect, BuffEffect), `combat.py:781-827` (`_cast_spell`), `src/pf1e_simulator/abilities.py:24-36` (AbilitySpec — catégorie metamagic), `src/pf1e_simulator/models.py:93-115` (Combatant), `tests/test_spells.py` + Acceptance criteria (agent-executable): `uv run pytest -q` vert ; transcript : boule de feu Empowered inflige ×1.5 ; buff Extendé dure ×2 rounds. + QA scenarios (name the exact tool + invocation): happy — `uv run pytest -q tests/test_spells.py -k "meta"` passe ; failure — dégâts non multipliés doit échouer. Evidence `.omo/evidence/task-9-finalize-engine.md` + Commit: Y | feat(combat): metamagic + +- [ ] 10. Compétences complètes + What to do / Must NOT do: `SkillSpec` (37 compétences PF1e : nom, stat liée, trained_only) ; `Combatant.skills: dict[str, SkillRank]` (rank, class_skill, armor_penalty) ; étendre `StatTarget` (`effects.py:40-44`) aux 37 noms de compétences ; `resolve_skill(state, skill, dc)` → d20 + rank + mod. stat + armor penalty ; feat Skill Focus (+3) via `StatModifier` avec filtre skill. Loader Foundry : parser les compétences des fiches (rank/total/classSkill/armorPenalty déjà présents). MUST NOT: synergies de compétences ni prises 10/20. + Parallelization: Wave 2 | Blocked by: — | Blocks: 11, 12, 13 + References (executor has NO interview context - be exhaustive): `src/pf1e_simulator/effects.py:40-44` (StatTarget), `effects.py:49-65` (StatModifier — ajouter champ `skill`), `src/pf1e_simulator/models.py` (Combatant), `src/pf1e_simulator/loaders/foundry.py:280-355` (`load_sheet_detailed`), `fiches_personnages/*_sheet.json` (8 fiches — sections skills), `src/pf1e_simulator/combat.py:380-399` (`_stat_modifiers` — filtre skill) + Acceptance criteria (agent-executable): `uv run pytest -q` vert ; test : `resolve_skill` applique rank+mod+penalty ; Skill Focus +3 ; loader remplit `skills` pour les 8 PJ. + QA scenarios (name the exact tool + invocation): happy — `uv run pytest -q tests/test_combatant_loader.py -k "skill"` passe ; failure — skill sans armor penalty sur une fiche à pénalité doit échouer. Evidence `.omo/evidence/task-10-finalize-engine.md` + Commit: Y | feat(combat): skills system + +- [ ] 11. Manœuvres CMB/CMD + What to do / Must NOT do: Ajouter `cmb` (BAB + mod STR + taille) et `cmd` (10 + CMB + mod DEX) à `Combatant` (calculés par le loader depuis BAB/abilities). Nouveau `Action(kind="maneuver", maneuver="trip|disarm|bull_rush|sunder")`. Résolution : `d20 + CMB` vs `CMD` de la cible ; succès → effet (trip: prone ; disarm: arme jetée (non utilisable) ; bull rush: poussée d'une case ; sunder: dégâts sur l'arme). `default_policy` choisit trip si BAB avantageux et cible en mêlée. MUST NOT: grapple (todo 12). + Parallelization: Wave 3 | Blocked by: 10 | Blocks: 12 + References (executor has NO interview context - be exhaustive): `src/pf1e_simulator/models.py` (Combatant, AttackSpec), `combat.py:221-239` (Action), `combat.py:1126-1152` (`default_policy`), `src/pf1e_simulator/conditions.py` (prone), `README.md:260` (« Manœuvres de combat » non modélisées), `src/pf1e_simulator/dice.py` + Acceptance criteria (agent-executable): `uv run pytest -q` vert ; transcripts : trip → cible prone ; disarm → arme inutilisable ; bull rush → poussée d'une case. + QA scenarios (name the exact tool + invocation): happy — `uv run pytest -q tests/test_combat.py -k "maneuver or trip or disarm"` passe ; failure — trip sans mise à terre doit échouer. Evidence `.omo/evidence/task-11-finalize-engine.md` + Commit: Y | feat(combat): combat maneuvers CMB/CMD + +- [ ] 12. Lutte complète (grapple) + What to do / Must NOT do: Condition `GRAPPLED` (ajout à `conditions.py`), suivi `grappled_with` (id de l'adversaire). Actions : grapple (standard, d20+CMB vs CMD, initie), maintain (standard : dégâts ou déplacement), pin (maintain réussi vs CMD+5), escape (d20+CMB **ou** Escape Artist via `resolve_skill` vs CMD), release (libre). Effets grapple : pas de mod DEX à la CA, pas de mouvement, −2 attaques, lancer un sort nécessite concentration. `default_policy` : grapple si pas d'avantage d'arme. MUST NOT: swallow whole, ni grapple multi-adversaires. + Parallelization: Wave 4 | Blocked by: 11, 10 | Blocks: 19 + References (executor has NO interview context - be exhaustive): `src/pf1e_simulator/conditions.py` (18 conditions existantes, registry:212-220), `combat.py:221-239` (Action), `combat.py:169-193` (CombatantState), `combat.py:1126-1152` (`default_policy`), `src/pf1e_simulator/effects.py` (modificateurs), todo 10 (Escape Artist via resolve_skill) + Acceptance criteria (agent-executable): `uv run pytest -q` vert ; transcript complet : initiate → maintain (dégâts) → pin → escape réussi. + QA scenarios (name the exact tool + invocation): happy — `uv run pytest -q tests/test_combat.py -k "grapple"` passe ; failure — grapplé qui se déplace doit échouer. Evidence `.omo/evidence/task-12-finalize-engine.md` + Commit: Y | feat(combat): grapple + +- [ ] 13. Mapping des feats des 8 PJ + What to do / Must NOT do: Résoudre les 55 feats uniques des 8 PJ → `AbilitySpec` : conserver les 8 connus + Weapon Focus/Toughness ; ajouter Combat Reflexes (AoO supplémentaires par round — interagit avec todo 2/`_resolve_aoo`), Skill Focus (+3 sur une compétence — todo 10), Point-Blank Shot déjà présent ; les autres → passifs minimaux ou no-op avec note (jamais inventer de mécanique). Synchroniser `_KNOWN_FEATS` (`loaders/combatant.py:47`) et `_FEAT_MAP` (`loaders/foundry.py:52-61`) avec la liste réelle des fiches. MUST NOT: créer de nouvelles mécaniques de feats au-delà de la liste. + Parallelization: Wave 4 | Blocked by: 2, 10 | Blocks: 19 + References (executor has NO interview context - be exhaustive): `src/pf1e_simulator/loaders/combatant.py:47` (`_KNOWN_FEATS`), `combatant.py:59-75` (`_resolve_feat`), `src/pf1e_simulator/loaders/foundry.py:52-71` (`_FEAT_MAP`, `_match_feat`), `src/pf1e_simulator/abilities.py:24-36` (AbilitySpec + factories), `fiches_personnages/*_sheet.json` (8 fiches — sections feats), `src/pf1e_simulator/combat.py:546-558` (`_resolve_aoo` — Combat Reflexes) + Acceptance criteria (agent-executable): `uv run pytest -q` vert ; le loader résout les 55 feats sans warning ni no-op silencieux (tous tracés) ; Combat Reflexes octroie une AoO supplémentaire par round. + QA scenarios (name the exact tool + invocation): happy — `uv run pytest -q tests/test_combatant_loader.py` passe (24 tests + nouveaux) ; failure — feat inconnu silencieusement ignoré doit échouer (pin). Evidence `.omo/evidence/task-13-finalize-engine.md` + Commit: Y | feat(loaders): map 8-PC feats + +- [ ] 14. 101 sorts → data/spells/*.json + What to do / Must NOT do: Script de génération (dans `loaders/`) : créer les 91 JSON manquants depuis les fiches Foundry (nom, niveau, école, portée, sauvegarde ; effets par heuristique Damage/Condition/Heal/Buff/Area/Summon selon les conventions existantes) ; tout sort non mappable → `effects=[]` (inerte mais présent). Mettre à jour le registre (`load_spell_registry`) et `tests/test_spells.py:113-124` (test all-10-spells → all-101). MUST NOT: encoder à la main des effets au-delà des heuristiques, ni inventer des stats absentes des fiches. + Parallelization: Wave 4 | Blocked by: 7, 8, 9 | Blocks: 17, 18, 19 + References (executor has NO interview context - be exhaustive): `data/spells/` (10 JSON existants — format de référence), `src/pf1e_simulator/spells.py:84-132` (`_parse_effect`, `load_spell`), `src/pf1e_simulator/loaders/foundry.py` (extraction des sorts depuis les fiches), `fiches_personnages/*_sheet.json` (8 fiches — spells), `tests/test_spells.py:113-124`, `data/pcs/*.json` (champ `spells`) + Acceptance criteria (agent-executable): `uv run pytest -q tests/test_spells.py` vert ; le registre charge les 101 sorts ; `uv run pf1e-sim --help` inchangé ; aucun fichier JSON invalide. + QA scenarios (name the exact tool + invocation): happy — `uv run pytest -q tests/test_spells.py` + chargement CLI passe ; failure — un sort connu manquant du registre doit échouer. Evidence `.omo/evidence/task-14-finalize-engine.md` + Commit: Y | feat(data): 101 spell JSONs + +- [ ] 15. Compagnons et montures + What to do / Must NOT do: Créer des JSON `Combatant` séparés pour Harvie (animal companion), Ierlieth (court drake), Jeanne (bodyguard) et la monture d'Esha, depuis les fiches Foundry (compétences/attaques/HP présents dans les fiches). Charger via la CLI comme les autres combattants. Noter dans `notes` les données absentes. MUST NOT: mouvement simultané cavalier/monture ni règles de monte. + Parallelization: Wave 5 | Blocked by: — | Blocks: 19 + References (executor has NO interview context - be exhaustive): `fiches_personnages/harvie_sheet.json`, `fiches_personnages/ierlieth_sheet.json`, `fiches_personnages/jeanne_sheet.json`, `fiches_personnages/esha_sheet.json`, `data/pcs/` (format existant), `src/pf1e_simulator/loaders/combatant.py:107-147`, `data/monsters/goblin.json` (schéma) + Acceptance criteria (agent-executable): les 4 JSON se chargent via `uv run pf1e-sim --side players data/pcs/.json` sans erreur de schéma. + QA scenarios (name the exact tool + invocation): happy — combat 1v1 compagnon vs gobelin, 100 runs seed 1, exit code 0 ; failure — JSON invalide → exit code 2 (pin). Evidence `.omo/evidence/task-15-finalize-engine.md` + Commit: Y | feat(data): companions and mounts + +- [ ] 16. Channel energy, domaines, hexes + What to do / Must NOT do: Pattern d'action de classe : `Action(kind="class_ability", class_ability="channel_energy|hex")`. Oni : channel energy (burst 30 ft, 2d6 soin/dommage, consomme `channel_energy` ressource (6/jour)) ; domaines Animal/Air minimaux. Nairda : hexes Evil Eye (shaken −2 AC ou saves, cible 1 round), Slumber (cible → asleep/disabled si Will échoue), Cackle (prolonge un hex). MUST NOT: listes de sorts de domaine complètes ni autres hexes. + Parallelization: Wave 5 | Blocked by: 4 | Blocks: 19 + References (executor has NO interview context - be exhaustive): `data/pcs/oni.json`, `data/pcs/nairda.json`, `src/pf1e_simulator/combat.py:221-239` (Action), `combat.py:781-827` (pattern `_cast_spell` à réutiliser pour class_ability), `src/pf1e_simulator/conditions.py` (shaken ; ajouter asleep), `src/pf1e_simulator/combat.py:481-496` (`resolve_save`) + Acceptance criteria (agent-executable): `uv run pytest -q` vert ; transcript : channel soigne les alliés dans 30 ft et consomme la ressource ; Slumber endort un gobelin. + QA scenarios (name the exact tool + invocation): happy — `uv run pytest -q tests/test_combat.py -k "channel or hex"` passe ; failure — channel après épuisement de la ressource doit échouer. Evidence `.omo/evidence/task-16-finalize-engine.md` + Commit: Y | feat(combat): channel energy and hexes + +- [ ] 17. Deeds/grit, spell combat, performance + What to do / Must NOT do: Tammara : deed Up Close and Deadly (+1d6 à ≤30 ft, coûte 1 grit), Quick Clear (swift, débloque l'arme) ; ressources depuis todo 4. Misty : Spell Combat (lancer un sort + attaque à outrance dans le même tour), Inspire Courage (buff moral +1 attaque/dégâts, 30 ft, coûte un round bardique). MUST NOT: la liste complète des deeds ni d'autres performances. + Parallelization: Wave 5 | Blocked by: 4, 7, 14 | Blocks: 19 + References (executor has NO interview context - be exhaustive): `data/pcs/tammara.json`, `data/pcs/misty.json`, `src/pf1e_simulator/combat.py:221-239` (Action), `combat.py:781-827` (`_cast_spell`), `combat.py:1126-1152` (`default_policy`), `src/pf1e_simulator/effects.py` (BuffEffect moral), `src/pf1e_simulator/grid.py` (`distance` pour le rayon 30 ft) + Acceptance criteria (agent-executable): `uv run pytest -q` vert ; transcripts : Misty lance + attaque dans le même tour ; le deed de Tammara consomme 1 grit ; Inspire Courage applique +1 attaque/dégâts aux alliés. + QA scenarios (name the exact tool + invocation): happy — `uv run pytest -q tests/test_combat.py -k "spell_combat or deed or inspire"` passe ; failure — deed sans grit disponible doit échouer. Evidence `.omo/evidence/task-17-finalize-engine.md` + Commit: Y | feat(combat): class action patterns + +- [ ] 18. Liche (monstre complexe) + What to do / Must NOT do: `data/monsters/lich.json` : Fear Aura (30 ft, shaken, Will DC 22 negate), Paralyzing Touch (attaque de contact → PARALYZED 1d4+1 rounds), DR 15/bludgeoning+magic (via le système DR existant avec types), liste de sorts (fireball, haste…). MUST NOT: mécanique de phylactère ni de destruction définitive. + Parallelization: Wave 5 | Blocked by: 14 | Blocks: 19 + References (executor has NO interview context - be exhaustive): `data/monsters/goblin.json` (schéma), `src/pf1e_simulator/models.py` (DamageReduction, AttackSpec — touch), `src/pf1e_simulator/conditions.py` (paralyzed), `src/pf1e_simulator/combat.py:781-827` (sorts), `src/pf1e_simulator/spells.py` + Acceptance criteria (agent-executable): la liche se charge via la CLI ; transcript : Fear Aura applique shaken ; Paralyzing Touch applique PARALYZED ; DR 15/bludgeoning+magic absorbe une attaque non-magique. + QA scenarios (name the exact tool + invocation): happy — `uv run pf1e-sim --side players data/pcs/tammara.json --side monsters data/monsters/lich.json --runs 50 --seed 1` exit 0 ; failure — la liche sans Fear Aura au contact doit échouer (pin transcript). Evidence `.omo/evidence/task-18-finalize-engine.md` + Commit: Y | feat(data): lich monster + +- [ ] 19. Golden tests 8 PJ + What to do / Must NOT do: `tests/test_pc_combat_golden.py` : pour chaque PJ, combat scripté (ScriptedRng) contre un gobelin (et tammara vs liche) avec transcript épinglé ; assertions sur l'issue (victoire/défaite) et plages de PV/ressources attendues. Tous les tests déterministes. MUST NOT: assertions statistiques flaky ni seeds aléatoires. + Parallelization: Wave 6 | Blocked by: 2, 5, 12, 13, 14, 15, 16, 17, 18 | Blocks: 20 + References (executor has NO interview context - be exhaustive): `src/pf1e_simulator/rng.py` (ScriptedRng), `tests/test_combat.py` (pattern des transcripts existants), `tests/test_combatant_loader.py` (pattern), `data/pcs/*.json` (8), `data/monsters/goblin.json`, `data/monsters/lich.json`, `data/maps/sample_arena.yaml` + Acceptance criteria (agent-executable): `uv run pytest -q tests/test_pc_combat_golden.py` vert ; chaque PJ a ≥1 combat scripté épinglé. + QA scenarios (name the exact tool + invocation): happy — `uv run pytest -q tests/test_pc_combat_golden.py` passe ; failure — transcript divergent après un changement doit échouer (pin). Evidence `.omo/evidence/task-19-finalize-engine.md` + Commit: Y | test(combat): PC golden transcripts + +- [ ] 20. Gate finale + README + What to do / Must NOT do: Gate complète (`ruff` + `basedpyright` + `pytest -q`), mise à jour du README : nouvelles règles implémentées (durées, flags, DC, ressources, politique de sort, AoO/concentration, AoE, summoning, métamagie, compétences, manœuvres, lutte, capacités de classe), section « Non modélisé » nettoyée, compteur de sorts (101), route de Phase 1 mise à jour. MUST NOT: créer de nouveaux fichiers de doc, ni toucher aux autres sections. + Parallelization: Wave 6 | Blocked by: 19 | Blocks: — + References (executor has NO interview context - be exhaustive): `README.md` (sections « Règles implémentées et limites de la Phase 0 », « Feuille de route »), `pyproject.toml`, `.gitignore`, `tests/` (comptage final) + Acceptance criteria (agent-executable): gate complète verte ; `uv run pytest -q` affiche ~500+ tests ; README sans référence obsolète (ex. « Manœuvres non modélisées », « 377 tests »). + QA scenarios (name the exact tool + invocation): happy — gate complète ; failure — `grep -rn "377 tests\|non modélisé" README.md` doit retourner vide (sauf mention historique justifiée). Evidence `.omo/evidence/task-20-finalize-engine.md` + Commit: Y | docs: finalize engine documentation + +## Final verification wave +> Runs in parallel after ALL todos. ALL must APPROVE. Surface results and wait for the user's explicit okay before declaring complete. +- [ ] F1. Plan compliance audit — les 20 todos sont terminés, evidence `.omo/evidence/task--finalize-engine.md` présente pour chacun, gate verte sur le commit final. +- [ ] F2. Code quality review — `uv run ruff check src tests` et `uv run basedpyright src` propres, aucune suppression de type (`as any`/`@ts-ignore` n/a en Python), pas de `catch` vide, pas de régression des règles existantes (empilement des bonus, AoO mouvement/tir). +- [ ] F3. Real manual QA — CLI : `uv run pf1e-sim --map data/maps/sample_arena.yaml --side players data/pcs/tammara.json --side monsters data/monsters/lich.json --runs 100 --seed 1` et `--side players data/pcs/oni.json --side monsters data/monsters/goblin.json data/monsters/goblin.json` ; vérifier dans les transcripts : sorts lancés, summon, manœuvres, ressources consommées ; aucun crash. +- [ ] F4. Scope fidelity — les 8 PJ jouables via CLI ; AUCUNE modification de `fiches_personnages/`, `.omo/` (hors plans/evidence), `.codegraph/` ; pas de couche LLM ; pas d'entrées de la base complète PF1e ; `git status` ne montre que des fichiers attendus. + +## Commit strategy +- Un commit par todo (20 commits attendus), message conventionnel `type(scope): summary` en anglais, conforme au style du dépôt (`test(loaders): add unit tests for combatant feat resolution…`). +- Gate (`ruff` + `basedpyright` + `pytest -q`) VERTE avant chaque commit. En cas d'échec : corriger, re-vérifier, puis committer. +- Le push reste manuel (fait par l'utilisateur). + +## Success criteria +- Gate finale verte : `uv run ruff check src tests && uv run basedpyright src && uv run pytest -q` (≈500 tests). +- Les 8 PJ jouables via CLI avec sorts lancés (politique de sort), ressources consommées, capacités de classe actives, compétences et manœuvres opérationnelles. +- Scénario doré : tammara vs liche et oni vs gobelins reproductibles et déterministes. +- README exact, sans référence obsolète ; scope respecté (pas de base complète PF1e, pas de LLM, `fiches_personnages/` intactes).