From febd408f29b0d22630b48087c142fda78c25e160 Mon Sep 17 00:00:00 2001 From: ctan Date: Sat, 5 Sep 2026 18:04:35 +0200 Subject: [PATCH] docs: document B5 engine hardening - reach, sizes, elevation, immediate actions --- README.md | 60 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 3c2fbf9..cf4af96 100644 --- a/README.md +++ b/README.md @@ -166,7 +166,7 @@ legend: # un caractère = un type de terrain "~": { type: water, move_cost: 2 } "C": { type: pillar, move_cost: null, cover: true } # bloque le pas, couvert -elevation: | # optionnel : chiffres = hauteur (validé, pas encore appliqué) +elevation: | # optionnel : chiffres = hauteur (appliqué : coût de pas + high ground) .................... ..1111.............. ..1111.............. @@ -273,7 +273,8 @@ Règles modélisées : `default_policy` renvoie `(full_attack,)` si l'ennemi est en portée, `(charge,)` si une ligne droite existe, `(5foot_step, full_attack)` si un seul pas suffit à entrer en portée, ou `(move, attack)` sinon. Les actions immédiates - (hors-tour, consomment le prochain swift) ne sont pas encore modélisées. + (hors-tour, consomment le prochain swift) sont modélisées via une politique + de réaction opt-in (`reaction_policy`) — voir « Actions immédiates » ci-dessous. - Attaques multiples par action : une arme avec `count` > 1 (« 2x Talons ») résout `count` balayages indépendants dans la même action ; les balayages restants sont perdus si la cible tombe (inconsciente ou morte) en cours de @@ -281,11 +282,34 @@ Règles modélisées : attaques supplémentaires à -5/-10/-15 ; les armes naturelles (`count` > 1) ne gagnent pas d'itératifs. Plusieurs armes naturelles en une seule attaque à outrance ne sont pas modélisées. -- Mêlée : allonge d'une case ; mouvement : un déplacement parcourt jusqu'à la +- Mêlée : allonge effective = max(allonge naturelle du gabarit (Small/Medium + 5 ft, Large 10 ft, Huge 15 ft, Gargantuan 20 ft, Colossal 30 ft), `reach_ft` + de l'arme). Une arme à allonge (`reach_ft` > 5) menace l'anneau de cases à + distance égale à son allonge — les cases à 5 ft sont une « dead zone » où + elle ne frappe pas ; une arme sans allonge menace les cases adjacentes. + Mouvement : un déplacement parcourt jusqu'à la vitesse du combattant en cases le long du plus court chemin réel (champ de coût Dijkstra depuis la cible, diagonales 5-10-5), en s'arrêtant adjacent à l'ennemi le plus proche — les combattants contournent les murs au lieu d'osciller contre eux. +- Tailles et occupation multi-cases : une créature occupe un footprint carré à + son ancre (`pos`) selon sa taille — Small/Medium 1×1, Large 2×2, Huge 3×3, + Gargantuan 4×4, Colossal 6×6 (taille inconnue → 1×1). Distance, ligne + d'effet/couvert, flanquement, zones menacées, AoO, charge, mouvement, + manœuvres et invocation sont généralisés aux cellules occupées (distance = + minimum entre cellules, LoS/couvert = meilleure paire) ; le comportement 1×1 + reste bit-identique. +- Élévation : monter d'un niveau de hauteur coûte +1 case de mouvement par + niveau (la descente est gratuite) ; une créature située plus haut que sa + cible gagne +1 à ses jets d'attaque de mêlée et à la confirmation de + critique (règle du high ground). La ligne d'effet reste 2D (pas de LoS 3D). +- Actions immédiates hors-tour : politique de réaction optionnelle + `reaction_policy(engine, state, event)` (défaut : aucune) ; sur événement + (`attack_resolved`, `movement_step`, `spell_cast`, `damage_taken`) une + réaction `Action(kind="immediate")` peut être exécutée hors tour — une par + événement — et consomme le prochain swift de l'état (immédiate pendant le + tour : exécutée, consomme aussi le prochain swift — simplification + documentée). - Ligne d'effet : une cible entièrement derrière un terrain `blocks_los` ne peut pas être attaquée — la politique se déplace jusqu'à gagner une ligne de visée. @@ -298,7 +322,9 @@ Règles modélisées : distance ignorent le flanquement ; un allié désarmé ou à distance ne compte pas pour le flanquement. - Attaques d'opportunité : un combattant actif doté d'une arme de mêlée menace - les 8 cases adjacentes. Deux déclencheurs sont modélisés : (1) quitter une + les cases à distance de son allonge effective (les 8 cases adjacentes pour + une arme sans allonge ; l'anneau à son allonge pour une arme reach — la + dead zone de 5 ft n'est pas menacée). Deux déclencheurs sont modélisés : (1) quitter une case menacée — le mouvement est résolu pas à pas, et chaque ennemi qui menace la case actuelle porte une AoO avant que le mouvant ne la quitte ; (2) tirer avec une arme à distance depuis une case menacée — chaque ennemi menaçant @@ -423,13 +449,8 @@ Règles modélisées : `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) : - -- 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). +Non modélisé (couche `markers` présente mais non appliquée dans la +résolution) : - Manœuvres : dirty trick applique toujours `dazzled` (le choix aveuglé/assourdi/entravé/malade n'est pas modélisé) ; reposition et drag utilisent une direction déterministe (premier voisin libre / au-delà de la @@ -446,14 +467,17 @@ dans la résolution) : `DamageComponent`, `Saves`, `DamageReduction`. - `map.py` — `MapSpec`/`TerrainType`, chargement YAML et validation (dimensions, caractères connus, zones, déploiement, markers). -- `grid.py` — grille 5-10-5 : `distance`, `step_cost`, règle des coins - (`diagonal_allowed`), Dijkstra `reachable` avec ou sans budget. +- `grid.py` — grille 5-10-5 : `distance`, `step_cost` (terrain + élévation), + règle des coins (`diagonal_allowed`), Dijkstra `reachable` avec ou sans + budget. - `los.py` — ligne d'effet et couvert par la règle des coins : gate de ligne d'effet dans `weapon_for`, bonus de couvert +4 CA (terrain et couvert mou 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, CA de contact - pour les attaques touch et firearms), états de vie, incantation + pour les attaques touch et firearms), tailles et allonges (occupation + multi-cases, zones menacées en anneau), high ground, actions immédiates + opt-in (réactions hors-tour), états de vie, incantation (emplacements, concentration par situation, AoO d'incantation), invocation, manœuvres, capacités de classe, transcripts, politique par défaut. Les modificateurs conditionnés (`StatModifier.condition`) sont évalués via @@ -533,9 +557,11 @@ uv run basedpyright src # mode strict 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 et actions immédiates - hors-tour. + « Règles implémentées ». Le socle B5 est en place : hauteurs/élévations + (coût de déplacement + high ground), tailles Large+ (occupation multi-cases), + allonges > 5 ft (allonge naturelle + `reach_ft`, zones menacées en anneau) et + actions immédiates hors-tour (politique de réaction opt-in). Restent hors du + socle (voir « Non modélisé ») : les markers et les manœuvres avancées. - **Phase 2** — couche tactique LLM : stratégies en langage naturel traduites en politiques, balayage de matrices de positionnement. (Non implémentée.) - **Phase 3** — rapporteur LLM local : agrégation des statistiques et