2024 lines
136 KiB
Markdown
2024 lines
136 KiB
Markdown
# pf1e-simulator
|
||
|
||
Simulateur de combat Monte Carlo déterministe pour Pathfinder 1re édition.
|
||
|
||
Le moteur joue une rencontre définie (carte + camps) un grand nombre de fois —
|
||
chaque combat est reproductible grâce à un générateur aléatoire seedé — puis
|
||
produit un rapport d'équilibrage en français : taux de victoire par camp avec
|
||
bande de confiance 3σ, nuls, rounds moyens et attrition par combattant.
|
||
|
||
Phase 0 : le moteur de combat couvre le socle des règles (initiative, attaques,
|
||
critiques, RD, états de vie, déplacement, mêlée/distance). La couche LLM
|
||
(tactiques et rédaction du rapport) n'est pas encore implémentée.
|
||
|
||
## Fonctionnalités
|
||
|
||
- Carte en grille de cases de 5 ft (1,5 m) au format YAML : couches ASCII
|
||
`terrain`, `elevation`, `zones`, points nommés `markers`, déploiement par camp.
|
||
- Comptage des diagonales selon la règle 5-10-5, coûts de déplacement selon le
|
||
terrain, règle des coins pour les diagonales le long des murs.
|
||
- Monstres définis en JSON (bestiaire) ; combattants déployés dans les zones de
|
||
la carte ; ids dupliqués automatiquement désambiguïsés (`goblin`, `goblin-2`…).
|
||
- Combats déterministes : chaque run utilise un flux `SeededRng(seed + i)` ;
|
||
même seed ⇒ mêmes combats.
|
||
- Rapport d'équilibrage en français : taux de victoire + bande 3σ
|
||
(σ = √(p(1−p)/n)), nuls, rounds moyens, attrition moyenne par combattant
|
||
(touches, critiques, dégâts infligés/subis).
|
||
- Interface en ligne de commande `pf1e-sim`.
|
||
|
||
## Prérequis et installation
|
||
|
||
Prérequis :
|
||
|
||
- Python ≥ 3.12
|
||
- [uv](https://docs.astral.sh/uv/) (gestionnaire de paquets et d'environnements)
|
||
|
||
Installation :
|
||
|
||
```bash
|
||
git clone <url-du-dépôt> pf1e-simulator
|
||
cd pf1e-simulator
|
||
uv sync --dev # inclut pytest, ruff et basedpyright
|
||
```
|
||
|
||
`uv sync` seul suffit pour n'utiliser que la CLI (pas d'outillage de dev).
|
||
Vérification :
|
||
|
||
```bash
|
||
uv run pf1e-sim --help
|
||
```
|
||
|
||
## Démarrage rapide
|
||
|
||
La carte d'exemple `data/maps/sample_arena.yaml` et les monstres
|
||
`data/monsters/goblin.json` / `data/monsters/orc.json` permettent de lancer une
|
||
première simulation immédiatement. 3 gobelins (camp `players`) contre 2 orcs
|
||
(camp `monsters`), 1000 combats, seed 1 :
|
||
|
||
```bash
|
||
uv run pf1e-sim \
|
||
--map data/maps/sample_arena.yaml \
|
||
--side players data/monsters/goblin.json data/monsters/goblin.json data/monsters/goblin.json \
|
||
--side monsters data/monsters/orc.json data/monsters/orc.json \
|
||
--runs 1000 --seed 1
|
||
```
|
||
|
||
Sortie (extrait) :
|
||
|
||
```
|
||
=== Rapport d'équilibrage ===
|
||
Carte : Arène d'essai
|
||
players : 3 combatants
|
||
monsters : 2 combatants
|
||
1000 combats simulés (seed 1, plafond 100 rounds)
|
||
|
||
Victoires players : 96.2% (962) — bande 3σ : [94.4%, 98.0%]
|
||
Victoires monsters : 3.6% (36) — bande 3σ : [1.8%, 5.4%]
|
||
Nuls : 0.2% (2)
|
||
Rounds moyens : 6.6
|
||
```
|
||
|
||
## Utilisation de la CLI
|
||
|
||
```
|
||
usage: pf1e-sim [-h] --map MAP [--side NAME [FILE ...]] [--runs RUNS]
|
||
[--seed SEED] [--round-cap ROUND_CAP] [--transcript]
|
||
[--simple-monster-death]
|
||
```
|
||
|
||
| Option | Rôle | Défaut |
|
||
|---|---|---|
|
||
| `--map MAP` | Fichier YAML de la carte (obligatoire) | — |
|
||
| `--side NAME [FILE ...]` | Camp `NAME` avec un ou plusieurs JSON de monstres ; répétable pour chaque camp | — |
|
||
| `--runs RUNS` | Nombre de combats simulés | `1000` |
|
||
| `--seed SEED` | Graine du générateur aléatoire | `1` |
|
||
| `--round-cap ROUND_CAP` | Plafond de rounds par combat (au-delà, combat nul) | `100` |
|
||
| `--transcript` | Imprimer le déroulé du premier combat **en tableau** avant le rapport (voir ci-dessous) | désactivé |
|
||
| `--simple-monster-death` | Mort simplifiée des monstres : `hp ≤ 0` au lieu de `hp ≤ -CON` (les PJ ne sont jamais affectés) | désactivé |
|
||
|
||
Affichage du transcript (`--transcript`) : les lignes du moteur sont présentées
|
||
dans un tableau à trois colonnes alignées — `Round`, `Combattant`, `Message` —
|
||
précédé d'un en-tête et d'une ligne de séparation. Les largeurs de colonnes
|
||
sont calculées sur le contenu affiché et l'alignement tient compte de la
|
||
largeur d'affichage réelle des caractères (accents, CJK). Les lignes du
|
||
transcript qui ne suivent pas le format `round N id: message` (jets
|
||
d'initiative, fin de combat) restent affichées telles quelles, en ligne brute
|
||
intercalée à leur position d'origine — aucune entrée n'est jamais perdue ni
|
||
tronquée. Exemple :
|
||
|
||
```
|
||
=== Déroulé du premier combat ===
|
||
Round Combattant Message
|
||
----- ---------- ---------------------------------------------------------
|
||
initiative: orc d20=19+0=19
|
||
1 orc move (1,15)->(2,14)->(3,13)->(3,12)->(3,11)->(3,10)
|
||
1 goblin short bow vs orc d20=9+4=13 AC 17 -> MISS
|
||
battle over: players win in 4 rounds
|
||
```
|
||
|
||
Les données du transcript (contenu et ordre des lignes du moteur) ne sont pas
|
||
modifiées par cette présentation.
|
||
|
||
Codes de sortie :
|
||
|
||
- `0` — simulation terminée, rapport imprimé.
|
||
- `2` — erreur d'utilisation ou de données (message sur stderr) : côté
|
||
inconnu, côté de déploiement manquant, zone trop petite, fichier invalide…
|
||
|
||
Règles de construction d'une rencontre :
|
||
|
||
- Les noms de camps (`--side NAME`) doivent correspondre exactement à la
|
||
section `deployment` de la carte, et **tous** les camps du déploiement
|
||
doivent être fournis.
|
||
- Le même fichier monstre peut être répété pour créer plusieurs combattants
|
||
identiques ; les ids sont désambiguïsés globalement dans l'ordre de
|
||
déploiement (`goblin`, `goblin-2`, `goblin-3`).
|
||
- Les combattants d'un camp remplissent les cases de leur zone dans l'ordre
|
||
(ligne, colonne). Une zone trop petite pour le nombre de combattants est une
|
||
erreur.
|
||
- Chaque camp du déploiement doit être lié à une lettre de zone **distincte** :
|
||
deux camps partageant la même lettre se déploieraient l'un sur l'autre, ce
|
||
qui est une erreur de carte (`share zone letters`).
|
||
|
||
## Interpréter le rapport
|
||
|
||
Pour chaque camp, le rapport donne le taux de victoire observé et une bande de
|
||
confiance à 3σ calculée en forme fermée (modèle binomial) :
|
||
|
||
- σ = √(p(1−p)/n) ; la bande est [p − 3σ, p + 3σ], bornée à [0, 1].
|
||
- Un affrontement symétrique (combattants identiques sur des zones en miroir)
|
||
doit rester proche de 50 % ; un écart hors de la bande 3σ signale un
|
||
déséquilibre (avantage d'initiative, géométrie, portées…).
|
||
- Une bande large indique un échantillon trop petit pour conclure — augmentez
|
||
`--runs`.
|
||
- L'attrition moyenne par combattant indique qui participe réellement : un
|
||
combattant à distance qui n'est jamais rejoint affiche des dégâts subis
|
||
proches de zéro, un combattant qui meurt systématiquement affiche des dégâts
|
||
subis supérieurs à ses PV maximum (dégâts du coup fatal inclus).
|
||
- Les nuls proviennent des combats atteignant le plafond de rounds (combattants
|
||
immobiles, camps inaccessibles…).
|
||
|
||
Exemple vérifié — 1 gobelin (players) contre 2 orcs (monsters), 1000 runs,
|
||
seed 1 : players 33,0 %, bande 3σ [28,5 % ; 37,5 %], 6 nuls (0,6 %), 7,1 rounds
|
||
moyens. Sans ligne de visée depuis la zone de départ, l'archer gobelin doit
|
||
contourner le mur central avant de tirer ; mais avec l'économie d'action
|
||
activée, les orcs parcourent toute leur vitesse (30 ft) puis frappent dans le
|
||
même tour — le gobelin solitaire ne peut plus les distancer durablement et
|
||
finit par tomber en mêlée sous les 2d4+4 des falchions. Il parvient parfois à
|
||
en coucher un avant : ces duels inégaux prolongés, et les rares combats de
|
||
kiting menés jusqu'au plafond de rounds (les 6 nuls), expliquent que sa part
|
||
dise atteigne tout de même un tiers des combats. C'est ce déséquilibre de
|
||
cadence qui penche nettement le face-à-face à 1 contre 2 du côté des monstres
|
||
(deux tiers des combats).
|
||
|
||
## Formats de données
|
||
|
||
### Carte (YAML)
|
||
|
||
Chaque couche est un bloc ASCII de mêmes dimensions ; un caractère = une case
|
||
de `square_size_ft` pieds. Seule `terrain` est obligatoire.
|
||
|
||
```yaml
|
||
name: "Arène d'essai"
|
||
square_size_ft: 5
|
||
|
||
terrain: | # couche obligatoire
|
||
####################
|
||
#......######......#
|
||
#..C...######...C..#
|
||
#......~..~........#
|
||
#..TT..~..~...TT...#
|
||
#......~..~........#
|
||
#..C............C..#
|
||
####################
|
||
|
||
legend: # un caractère = un type de terrain
|
||
"#": { type: wall, move_cost: null, blocks_los: true } # bloque déplacement + LoS
|
||
".": { type: floor, move_cost: 1 }
|
||
"T": { type: rubble, move_cost: 2 } # terrain difficile
|
||
"~": { type: water, move_cost: 2 }
|
||
"C": { type: pillar, move_cost: null, cover: true } # bloque le pas, couvert (+4)
|
||
"W": { type: ruin, move_cost: null, cover_bonus: 2 } # couvert partiel (+2) ; 4 = standard, 8 = amélioré
|
||
|
||
elevation: | # optionnel : chiffres = hauteur (appliqué : coût de pas + high ground)
|
||
....................
|
||
..1111..............
|
||
..1111..............
|
||
....................
|
||
|
||
zones: | # optionnel : lettres de zone de déploiement
|
||
....................
|
||
.AAAA..........BBBB.
|
||
.AAAA..........BBBB.
|
||
....................
|
||
|
||
markers: # optionnel : points nommés (validés, pas encore utilisés)
|
||
autel: [1, 16]
|
||
|
||
deployment: # obligatoire si zones : nom de camp -> lettre de zone
|
||
players: A
|
||
monsters: B
|
||
```
|
||
|
||
Valeur du `move_cost` : entier > 0 pour un terrain praticable, `null` pour un
|
||
terrain infranchissable (mur, pilier). `blocks_los` et `cover` sont appliqués
|
||
par la résolution des attaques (module `los.py`, règle des coins) : pas de
|
||
ligne d'effet ⇒ l'attaque est impossible ; couvert ⇒ bonus de CA. Le champ
|
||
optionnel `cover_bonus` (`2`, `4` ou `8`) encode le niveau de couvert PF1e
|
||
(partiel +2, standard +4, amélioré +8) ; `cover: true` sans `cover_bonus`
|
||
vaut couvert standard (+4), idem pour un terrain `blocks_los`.
|
||
|
||
Sémantique de `blocks_los` — **ligne d'effet, non ligne de vision** (SRD
|
||
« Aiming a Spell — Line of Effect ») : « A line of effect is a straight,
|
||
unblocked path that indicates what a spell can affect. A line of effect is
|
||
canceled by a solid barrier. It's like line of sight for ranged weapons,
|
||
except that it's not blocked by fog, darkness, and other factors that limit
|
||
normal sight. » Le drapeau encode donc une **barrière solide** ; un terrain
|
||
qui ne limite que la vision normale (brouillard, obscurité, dissimulation)
|
||
n'est pas modélisé — le moteur n'a qu'un seul drapeau, et la dissimulation
|
||
passe par les conditions (`miss_chance`), pas par la carte.
|
||
|
||
### Monstre (JSON)
|
||
|
||
```json
|
||
{
|
||
"name": "Goblin",
|
||
"level": 1,
|
||
"size": "Small",
|
||
"cr": "1/3",
|
||
"xp": 135,
|
||
"source": "Bestiary > Goblin",
|
||
"abilities": {
|
||
"str_score": 11, "dex_score": 15, "con_score": 12,
|
||
"int_score": 10, "wis_score": 9, "cha_score": 6
|
||
},
|
||
"hp_max": 6,
|
||
"ac": { "total": 16, "touch": 13, "flat_footed": 14 },
|
||
"bab": 1,
|
||
"initiative_mod": 6,
|
||
"speed_land_ft": 30,
|
||
"saves": { "fort": 3, "ref": 2, "will": -1 },
|
||
"attacks": [
|
||
{
|
||
"name": "short sword",
|
||
"kind": "melee",
|
||
"attack_bonus": 2,
|
||
"damage": [{ "formula": "1d4", "types": ["slashing"] }],
|
||
"crit_range": 19,
|
||
"crit_mult": 2
|
||
},
|
||
{
|
||
"name": "short bow",
|
||
"kind": "ranged",
|
||
"attack_bonus": 4,
|
||
"damage": [{ "formula": "1d4", "types": ["piercing"] }],
|
||
"crit_mult": 3,
|
||
"range_increment_ft": 60
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
Champ `range_increment_ft` (optionnel, uniquement pour `kind: ranged` ou
|
||
`kind: touch`) : incrément de portée en pieds. La pénalité de portée vaut −2
|
||
par incrément complet au-delà du premier ; une arme `ranged` reste utilisable
|
||
jusqu'à 10 incréments, une arme `touch` jusqu'à 5 (jet/firearm), au-delà elle
|
||
n'est pas sélectionnable. `damage_bonus` et `dr` sont optionnels.
|
||
Le booléen `two_handed` (optionnel, défaut `false`) marque une arme tenue à
|
||
deux mains : Power Attack donne alors +3X (et non +2X) aux dégâts (conformance
|
||
B16). Le booléen `reach_weapon` (optionnel, défaut `false`) marque une arme
|
||
dotée de la qualité « reach » du CRB (lance, glaive, longspear, etc.) : elle
|
||
double l'allonge naturelle du porteur mais ne frappe pas à cette allonge
|
||
naturelle ou moins — anneau avec dead zone (conformance R1, voir « Mêlée »).
|
||
Le `reach_ft` d'une attaque naturelle (morsure, sabots) n'est **pas** une arme
|
||
à allonge : il encode l'allonge naturelle de la créature, qui reste un disque.
|
||
`spell_resistance` (entier), `resistances` (dict type → montant) et
|
||
`immunities` (liste de types) sont optionnels.
|
||
Deux monstres d'exemple sont fournis : gobelin et orc.
|
||
|
||
### Fiches de personnages (PJ)
|
||
|
||
`fiches_personnages/` contient les 8 fiches de PJ au format Foundry VTT
|
||
(`pf1-sheet/v1`) ; le chargeur `loaders/foundry.py` sait les lire. Elles sont
|
||
transposées en JSON au schéma `Combatant` dans `data/pcs/` (`esha.json`,
|
||
`harvie.json`, `ierlieth.json`, `jeanne.json`, `misty.json`, `nairda.json`,
|
||
`oni.json`, `tammara.json`) et se passent directement à la CLI comme les
|
||
monstres :
|
||
|
||
```bash
|
||
uv run pf1e-sim --map data/maps/sample_arena.yaml \
|
||
--side players data/pcs/oni.json \
|
||
--side monsters data/pcs/tammara.json --runs 200
|
||
```
|
||
|
||
Les armes placeholder des fiches sans dégâts encodés sont omises (notées
|
||
dans `notes` le cas échéant) ; le champ `source` de chaque fichier pointe
|
||
vers la fiche Foundry d'origine.
|
||
|
||
> **`data/pcs/*.json` est la référence canonique des PJ.** Le chargeur
|
||
> `loaders/foundry.py` est un outil de migration **one-shot et incomplet** :
|
||
> il ne reproduit pas les corrections manuelles/SRD appliquées aux JSON
|
||
> (drapeaux `natural`/`secondary`/`firearm`, allonge des sabots de Jeanne
|
||
> (10 ft), vol d'Ierlieth (40 ft), morsure de Misty (1d6), dons au-delà de sa
|
||
> table de 8 entrées, `spell_slots`/`resources`/`casting_ability`). Le
|
||
> relancer et régénérer les JSON de PJ ferait régresser ces corrections : ne
|
||
> pas régénérer depuis `foundry.py` sans réappliquer au préalable les
|
||
> corrections de conformance consignées dans
|
||
> [`docs/conformance-srd.md`](docs/conformance-srd.md).
|
||
|
||
## Règles implémentées et limites de la Phase 0
|
||
|
||
> Le suivi détaillé des écarts avec le SRD — corrigés, hors périmètre et
|
||
> résiduels assumés — est consigné dans
|
||
> [`docs/conformance-srd.md`](docs/conformance-srd.md).
|
||
|
||
Règles modélisées :
|
||
|
||
- 1 naturel = échec automatique ; 20 naturel = touche + menace de critique.
|
||
- Critique : (dés physiques + bonus plat) × multiplicateur ; les dés
|
||
énergétiques des spécificités d'arme (flaming, etc.) et les dés de précision
|
||
ne sont PAS multipliés (exception CRB « Critical Hits ») ; le 20 naturel au
|
||
jet de
|
||
confirmation confirme automatiquement la critique (conformance P2 — le jet
|
||
de confirmation est un jet d'attaque) ; la RD s'applique après
|
||
multiplication, un type de contournement annule la RD — une RD à plusieurs
|
||
composants (« DR X/Y and Z », conformance P2) exige que l'attaque porte
|
||
TOUS les composants (le contournement n'est plus traité comme un OU), les
|
||
dégâts plancher à 0.
|
||
- Critique améliorée (« Improved Critical ») : le don double la plage de
|
||
menace — plage effective = `21 - 2 × (21 - crit_range)` (20 → 19-20,
|
||
19 → 17-20, 18 → 15-20). Il n'affecte que le jet de menace, jamais le jet de
|
||
confirmation. Simplification moteur : le don s'applique à toutes les armes
|
||
de l'attaquant (pas de sélection par type d'arme comme sur table).
|
||
- Mourir et stabilisation : une créature à `hp < 0` mais pas morte est
|
||
mourante — inconsciente (inactive), elle perd 1 pv par tour si elle échoue
|
||
son jet de Constitution DD 10 à son tour d'initiative : `d20 + mod de CON −
|
||
|pv négatifs|` contre 10 ; un 20 naturel réussit automatiquement. En cas d'échec, elle
|
||
perd 1 pv de plus ; en cas de réussite, elle est stabilisée (plus de perte de
|
||
pv, reste inconsciente). Une nouvelle blessure sous le seuil de stabilisation
|
||
(`hp < stabilized_hp`) fait reprendre l'agonie (nouveau jet) ; un soin
|
||
ramenant à `hp ≥ 0` réinitialise la stabilisation pour toute chute future.
|
||
La mort survient quand les pv négatifs atteignent le score de CON
|
||
(`hp ≤ -CON`) — conformance P2 : la règle du −10 de D&D 3.5 n'existe pas
|
||
en PF1e et n'est plus modélisée. Une créature mourante qui subit des dégâts
|
||
continus (saignement) perd ses dégâts de saignement au début de son tour,
|
||
puis **échoue automatiquement** son jet de stabilisation (aucun d20 jeté) et
|
||
perd donc 1 pv de plus par tour en plus des dégâts continus (CRB « Stable
|
||
Characters and Recovery ») — elle meurt ainsi plus vite qu'une créature
|
||
mourante non saignante. Les morts-vivants et les constructeurs n'ont pas de
|
||
score de Constitution et sont **détruits dès `hp ≤ 0`** — ni bande
|
||
d'invalidité à 0 pv, ni bande d'agonie (SRD traits « Undead » /
|
||
« Construct » : « undead do not risk death from massive damage, but are
|
||
immediately destroyed when reduced to 0 hit points or fewer ») ; le seuil
|
||
`hp ≤ -CON` ne s'applique qu'aux créatures vivantes.
|
||
- Mort simplifiée des monstres (option `--simple-monster-death`, défaut
|
||
désactivé) : quand l'option est présente dans la commande, un **monstre**
|
||
meurt dès `hp ≤ 0` — ni bande d'invalidité à 0 pv, ni bande d'agonie entre
|
||
0 et `-CON`. Les **personnages joueurs ne sont jamais affectés**, quel que
|
||
soit le réglage : ils gardent la règle PF1e complète (`hp ≤ -CON` pour
|
||
mourir, invalidité à 0 pv, agonie et stabilisation en dessous). La
|
||
distinction monstre/PJ repose sur une seule marque fiable issue du
|
||
chargeur : le champ `source` des JSON de PJ pointe vers la fiche Foundry
|
||
d'origine sous `fiches_personnages/` (convention documentée plus bas),
|
||
alors que les JSON de monstres portent des sources « Bestiary », « summon
|
||
monster » ou « bestiary » ; le nom de camp (`--side`) ne peut pas servir
|
||
de critère puisqu'il est choisi par l'utilisateur et que la carte d'exemple
|
||
oppose des monstres déployés côté `players`. Sans l'option, le comportement
|
||
du moteur est bit-identique à celui d'aujourd'hui (transcripts dorés
|
||
intacts). Équivalent Python : `run_encounter(..., monsters_die_at_zero=True)`
|
||
et `CombatEngine(..., monsters_die_at_zero=True)`.
|
||
- Invalide (conformance P2, CRB « Effects of Death ») : à `hp = 0`
|
||
exactement, une créature est **invalide** — elle reste active mais
|
||
**staggered** (une action de mouvement OU une action standard par tour, pas
|
||
d'action à round complet, mais les actions swift, libre et immédiate restent
|
||
permises — CRB « Staggered »), se déplace à **moitié vitesse**, et achever une
|
||
action non-mouvement (standard ou pénible) lui coûte **1 pv** à la fin de
|
||
cette action (pv → −1, agonie). Les actions de mouvement — dont
|
||
`retrieve_weapon` (sortir/récupérer une arme) et `repair_weapon` (raccommoder
|
||
une arme brisée), classées mouvement — ne risquent pas de blessure
|
||
supplémentaire et coûtent 0 pv (CRB « Disabled »). Simplification documentée :
|
||
si l'action elle-même soigne la créature au-dessus de 0 pv, ce coût n'est pas
|
||
appliqué.
|
||
- `hp < 0` = inactive (mourante ou morte) ; `hp = 0` = active mais invalide.
|
||
- Initiative : triée sur (total, modificateur, ordre de liste), sans re-jet.
|
||
- Round de surprise (SRD « Surprise ») : le champ booléen `aware` du JSON de
|
||
combattant (défaut `true`) encode la conscience des adversaires au début du
|
||
combat. Si certains combattants actifs sont `aware` et d'autres non, un
|
||
round de surprise (round 0) précède le round 1 : « In initiative order
|
||
(highest to lowest), combatants who started the battle aware of their
|
||
opponents each take a standard or move action during the surprise round.
|
||
You can also take free actions during the surprise round. » Le filtre
|
||
`_surprise_filter` applique cette allowance : une seule action standard
|
||
**ou** de mouvement (la première de la séquence gagne), les actions libres
|
||
conservées, les actions swift et à round complet retirées
|
||
(`downgraded:surprise`) ; un `full_attack` se dégrade en attaque standard
|
||
unique (même convention que le filtre accablé) et `withdraw` survit comme
|
||
retraite restreinte standard (SRD « Full-Round Actions »). « Combatants who
|
||
are unaware at the start of battle don't get to act in the surprise round »
|
||
— les non-`aware` sautent leur tour (journalisé). « If no one or everyone is
|
||
surprised, no surprise round occurs » — avec le défaut `aware: true`, le
|
||
comportement est bit-identique à l'absence de la règle. Simplification
|
||
documentée : les combattants non avertis ne sont pas marqués `flat_footed`
|
||
pendant le round de surprise (le moteur n'injecte jamais cette condition
|
||
lui-même — voir « Actions immédiates »).
|
||
- Économie d'action : un tour normal donne une action standard + une action de
|
||
mouvement (ou une action à round complet), plus swift/free/immediate. La
|
||
politique renvoie une séquence ordonnée d'actions exécutées dans l'ordre ;
|
||
`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 : prochain swift ; pendant son tour : swift du tour courant —
|
||
conformance L10) sont modélisées via une politique
|
||
de réaction opt-in (`reaction_policy`) — voir « Actions immédiates » ci-dessous.
|
||
**Application du budget (conformance X4)** : le moteur applique désormais le
|
||
budget d'action PF1e (CRB « Actions in Combat ») à TOUTE séquence renvoyée par
|
||
une politique, y compris une future politique LLM, et non plus seulement aux
|
||
cas immédiat/accablé/nausée. Chaque kind d'action est classé dans un seul
|
||
compartiment du budget — standard, mouvement, swift, round complet, ou
|
||
libre/conditionnel (hors budget) — et la séquence est vérifiée contre le budget
|
||
(≤ 1 standard **ou** 1 round complet, ≤ 1 mouvement, ≤ 1 swift ; un round
|
||
complet consacre à la fois le standard ET le mouvement). **Conversion
|
||
standard → mouvement** (CRB : une action de mouvement peut se prendre « à la
|
||
place » de l'action standard) : quand le compartiment mouvement est déjà
|
||
occupé et que le standard est encore libre, un second mouvement consomme le
|
||
standard — `(move, stand_up)` et `(move, move)` sont donc légaux. La dégradation est
|
||
**déterministe et stable en ordre** : parcours glouton dans l'ordre, chaque
|
||
action est gardée tant que son compartiment est libre ; le résultat est donc le
|
||
plus préfixe-sous-séquence légal, et toute action excédentaire est retirée et
|
||
rapportée comme `ActionResult(kind=…, ok=False, reason="downgraded:action_economy")`
|
||
(visible dans `last_action_results` et le snapshot `last_turn_results`). Exemples :
|
||
`(attack, attack)` → `(attack,)` ; `(charge, attack)` → `(charge,)` ;
|
||
`(attack, full_attack, move)` → `(attack, move)` ; `(move, attack, move)` →
|
||
`(move, attack)` ; `(full_attack, deed, inspire_courage)` → `(full_attack, deed)`.
|
||
Classification (exhaustive sur le `Literal` des kinds, verrouillée par
|
||
`tests/test_conformance_action_economy.py`) : **standard** = `attack`,
|
||
`maneuver`, `channel_energy`, `hex`, `delay`, `ready`,
|
||
`total_defense`, `aid_another` ; `cast_spell` occupe le compartiment que
|
||
lui donne son **temps d'incantation** (champ `cast_time` de `SpellSpec`,
|
||
voir « Temps d'incantation » plus bas ; standard par défaut) ; exception
|
||
par action (conformance D5 — APG « Cackle ») : `hex` avec
|
||
`hex_name="cackle"` occupe le compartiment **libre** (résolu par
|
||
`_effective_slot`, le kind `hex` restant standard pour evil_eye/slumber) ;
|
||
**mouvement** = `move`, `stand_up`,
|
||
`retrieve_weapon` (sortir une arme est une action de mouvement),
|
||
`repair_weapon` ; exception conformance L7 (SRD « Move Actions ») : « If
|
||
you have a base attack bonus of +1 or higher, you may draw a weapon as a
|
||
free action combined with a regular move » — quand la séquence du tour
|
||
contient une action `move` ET que le combattant a un BAB ≥ +1,
|
||
`retrieve_weapon` occupe le compartiment **libre** (drapeau `free_draw`
|
||
résolu par `_effective_slot`) et n'entame pas le budget mouvement ;
|
||
`repair_weapon` reste mouvement (la règle RAW ne couvre que le dégainement) ;
|
||
exception S5a (Ultimate Combat, deed « Quick Clear ») : `deed` avec
|
||
`deed_name="quick_clear"` occupe le compartiment **standard** (résolu par
|
||
`_effective_slot`, le kind `deed` restant swift pour up_close_and_deadly) ;
|
||
**swift** = `swift`, `deed`,
|
||
`inspire_courage` ; **round complet** = `full_attack`, `charge`, `withdraw`,
|
||
`full_round`, `spell_combat` (sort + attaque à outrance dans le même round),
|
||
`run` (round complet en RAW — il consacre le standard ET le mouvement, tout
|
||
en posant `moved_this_turn` et `running`) ;
|
||
**libre/conditionnel (hors budget)** = `wait`, `free`, `immediate` (hors-tour ;
|
||
il consomme le PROCHAIN swift via `immediate_used` ; pendant son propre tour,
|
||
il consomme le swift du tour courant via `swift_used_this_turn` — conformance
|
||
L10), `5foot_step` (pas libre, exclusif avec tout autre mouvement via
|
||
`moved_this_turn`), `drop_prone`, `activate_power_attack` et `fight_defensively`
|
||
(des déclarations de mode appliquées à l'attaque qui les suit, comme le
|
||
basculement Power Attack). Ordre de composition : le pré-filtre des temps
|
||
d'incantation « longs » est appliqué EN PREMIER (voir « Temps d'incantation »
|
||
ci-dessous), puis le budget général, puis les filtres spécialisés
|
||
(immédiat/accablé/nausée) s'exercent sur le reste déjà légal (leur sortie est
|
||
une action unique, donc jamais re-filtrée par le budget). L'ordre de
|
||
dégradation est celui de la politique : la première action légale d'un
|
||
compartiment gagne, les suivantes sont excédentaires.
|
||
- Temps d'incantation (`SpellSpec.cast_time`, conformance économie d'action) :
|
||
chaque sort porte un temps d'incantation normalisé en codes de budget —
|
||
`standard`, `swift`, `immediate`, `free`, `full_round`, `one_round`, `long`
|
||
(défaut `standard`). Le chargeur JSON accepte aussi les formulations RAW :
|
||
`"1 round"`/`"one round"` → `one_round`, `"1 full-round action"`/
|
||
`"full-round action"`/`"full-round"` → `full_round`, `"1 minute"`/
|
||
`"10 minutes"`/`"1 hour"`/`"long"` → `long`, `"standard action"`,
|
||
`"swift action"`, `"immediate action"`, `"free action"` ; une valeur
|
||
inconnue est une erreur de données (`ValueError`). Mapping vers le budget :
|
||
`standard` → standard ; `swift` → swift (il concurrence donc deed/inspire
|
||
courage et est aussi retiré quand le swift est consommé par une immédiate,
|
||
via `immediate_used`) ; `immediate` et `free` → libre (l'immédiate reste
|
||
hors-tour et consomme le prochain swift) ; `full_round` → round complet ;
|
||
`one_round` → **round complet** — SRD « Casting Time » : « A spell that
|
||
takes 1 round to cast is a full-round action » ; l'emplacement est dépensé
|
||
et les gardes de début d'incantation (AoO, concentration) résolus sur le
|
||
tour courant, mais **l'effet est différé au début du tour suivant** du
|
||
lanceur (voir « Incantation d'1 round » ci-dessous) ;
|
||
`long` (> 1 round : 3 rounds, 10 minutes, 1 heure — cast multi-round non
|
||
planifiable dans un tour) → **jeté avant le filtre de budget**, rapporté comme
|
||
`ActionResult(kind="cast_spell", ok=False, reason="downgraded:cast_time")`
|
||
(seule `default_policy` s'en préserve : `_choose_spell_action` ne sélectionne
|
||
jamais un sort à temps long). Le moteur résout le code via sa registry
|
||
(`_cast_time_of`, repli `standard` pour un nom inconnu) et le passe aux
|
||
filtres purs sous forme de résolveur `CastTimeResolver`. Les 108 sorts
|
||
importés de `data/spells/` déclarent tous leur `cast_time` (codes normalisés,
|
||
backfillés depuis le SRD) : 96 `standard`, 6 `one_round` (Enhance Water,
|
||
Silence, Sleep, Summon Instrument, Summon Monster I/II), 2 `immediate`
|
||
(Grasp, Saving Finale) et 4 `long` (Make Whole, Masterwork Transformation,
|
||
Mending, Restoration Lesser). Hors du périmètre (hook futur, commenté dans
|
||
`_cast_time_of`) : la métamagie d'incanteur spontané qui allonge le temps
|
||
d'incantation à un round complet sans don Quickened — la signature
|
||
`Callable[[Action], CastTime]` du résolveur l'accommodera sans changement.
|
||
- Incantation d'1 round (`cast_time: one_round`) : SRD « Casting Time » —
|
||
« It comes into effect just before the beginning of your turn in the round
|
||
after you began casting the spell. You then act normally after the spell is
|
||
completed. » Sur le tour d'incantation, l'emplacement est dépensé, les
|
||
gardes de début d'incantation jouent (AoO d'incantation + jet de
|
||
concentration sur dégât, incantation défensive, agrippé), puis le sort est
|
||
consigné comme incantation en cours (`PendingCast` sur l'état du lanceur :
|
||
nom du sort, cible, point de centre, métamagie, niveau d'emplacement, pv au
|
||
moment de l'incantation) et **rien n'est appliqué** — les décisions
|
||
d'incantation (attaque de contact, RM, sauvegardes, effets) se prennent à la
|
||
résolution (« You make all pertinent decisions about a spell [...] when the
|
||
spell comes into effect »). Au début du tour suivant du lanceur, avant le
|
||
portail de début de tour et avant l'appel de politique, l'incantation en
|
||
cours se résout (journalisé `finishes casting X`), puis le lanceur agit
|
||
normalement. Interruption : toute perte nette de pv entre l'incantation et
|
||
la résolution impose le jet de concentration « blessé pendant l'incantation »
|
||
(DD `10 + dégâts + niveau du sort`, **sans** le bonus Combat Casting — même
|
||
helper `_check_concentration` que l'AoO d'incantation) ; un échec perd le
|
||
sort (`spell fizzles`, emplacement brûlé). Un lanceur mis hors de combat
|
||
avant sa résolution perd le sort (journalisé `lost (cannot act)`).
|
||
Simplifications documentées : l'interruption est mesurée en **perte nette
|
||
de pv à la résolution** et non par un crochet à chaque site de dégâts — un
|
||
soin intermédiaire peut masquer des dégâts subis ; l'incantation en cours
|
||
est exposée dans l'observation via la clé `pending_cast`
|
||
(`{spell_name, target_id, slot_level}` ou `None`). L'incantation `long`
|
||
(> 1 round) reste hors périmètre (jetée du budget, voir « Temps
|
||
d'incantation »).
|
||
- Retard (`Action(kind="delay")`, action standard) : le combattant renonce à
|
||
agir maintenant et agit plus tard dans le **même round** — en fin d'ordre si
|
||
`target_id` est absent, sinon juste avant le combattant désigné. La nouvelle
|
||
position est encodée dans la clé d'initiative par un total égal à celui de la
|
||
cible − 0,5 (ou au minimum des totaux − 0,5 sans cible) et **persiste aux
|
||
rounds suivants** ; le reste du tour est perdu, un second retard dans le même
|
||
round est rejeté (`already_delayed`), et le drapeau se réinitialise au début
|
||
de chaque round. Le tour réinséré plus tard dans le même round **ne rejoue
|
||
pas le début de tour** : la décrément des durées (buffs/conditions), les
|
||
dégâts continus (saignement), l'effet de terreur et les réinitialisations de
|
||
drapeaux par tour ne s'appliquent qu'une fois par round (le tour retardé garde
|
||
néanmoins son appel de politique, son budget et son exécution normaux).
|
||
`default_policy` ne choisit pas le retard (disponible via
|
||
une politique personnalisée).
|
||
- Préparation (`Action(kind="ready", ready_trigger=...)`, action standard) : le
|
||
combattant se prépare à agir quand un événement déclencheur survient. Le
|
||
payload est encodé dans les champs porteurs de l'action, comme l'immédiate :
|
||
`spell_name` → incantation, `maneuver` → manœuvre (avec `direction` /
|
||
`trick_condition`), sinon attaque simple sur `target_id` (les capacités de
|
||
classe en payload ne sont pas modélisées). Quatre déclencheurs, mappés aux
|
||
événements de réaction existants : `enemy_enters_reach` ← pas de mouvement
|
||
d'un ennemi (simplification documentée : la portée menacée n'est pas
|
||
recalculée, tout pas ennemi déclenche), `enemy_casts` ← incantation d'un
|
||
ennemi, `enemy_attacks` ← attaque résolue d'un ennemi, `enemy_hits_ally` ←
|
||
dégâts subis par un allié du préparant infligés par un ennemi. Quand
|
||
l'événement correspond, le moteur exécute le payload **hors-tour, sans coût
|
||
de swift** (au plus une action préparée par événement, sans cascade), puis
|
||
l'action préparée est **consommée** (one-shot). Une nouvelle préparation
|
||
remplace la précédente ; le reste du tour (action de mouvement) reste
|
||
exécutable. **S4b — l'action préparée change l'initiative** (SRD « Ready —
|
||
Initiative Consequences of Readying ») : « Your initiative result becomes
|
||
the count on which you took the readied action, and you act immediately
|
||
ahead of the character whose action triggered your readied action » — quand
|
||
la préparation se déclenche, `_check_readied` appelle `_ready_shift_initiative`
|
||
qui réécrit la clé d'initiative du préparant dans `_initiative_keys` (même
|
||
forme de tuple que `_execute_delay` : total du déclencheur ± le pas de 0,5,
|
||
en conservant le modificateur d'initiative et l'index de liste du préparant),
|
||
si bien que `_build_round_order` conserve la nouvelle position pour **tous
|
||
les rounds suivants**. Le pas est **ajouté** (le préparant agit juste AVANT
|
||
le déclencheur), à l'inverse du retard qui le **retranche** (agir après) —
|
||
les deux intentions sont opposées. Déterministe : aucun dé consommé, l'ordre
|
||
du round courant (déjà construit) n'est pas modifié, seuls les rounds
|
||
suivants se réordonnent. Conformance L4 (SRD « Ready — Initiative
|
||
Consequences of
|
||
Readying ») : « If you come to your next action and have not yet performed
|
||
your readied action, you don't get to take the readied action (though you
|
||
can ready the same action again) » — la préparation non déclenchée
|
||
**expire au début du tour suivant** du préparant (bloc de début de tour de
|
||
`_take_turn`, journalisé `ready expired (trigger never occurred)`), avant
|
||
l'appel de politique ; elle survit donc à la fin du round courant et peut
|
||
se déclencher au round suivant tant que le tour du préparant n'est pas
|
||
arrivé. Le snapshot expose l'état via la clé `readied`
|
||
(`{kind, trigger}` ou `None`). `default_policy` ne se prépare pas
|
||
(disponible via une politique personnalisée).
|
||
- Défense totale (`Action(kind="total_defense")`, action standard) : le
|
||
combattant se couvre entièrement et gagne **+4 d'esquive à la CA** jusqu'au
|
||
début de son prochain tour (champ runtime `defensive_ac_bonus`, réinitialisé
|
||
avec les autres drapeaux de tour). Le kind ne requiert pas de cible.
|
||
Simplification documentée : la progression d'Acrobatie (+6 avec 3 rangs)
|
||
n'est pas modélisée. `default_policy` ne la choisit pas (disponible via une
|
||
politique personnalisée).
|
||
- Combat défensif (`Action(kind="fight_defensively")`, action standard) : le
|
||
combattant se bat avec prudence — **−4 à ses jets d'attaque et de
|
||
confirmation de critique**, et **+2 d'esquive à la CA** jusqu'au début de son
|
||
prochain tour (drapeaux runtime `fight_defensively` + `defensive_ac_bonus`).
|
||
Simplification documentée : la progression d'Acrobatie (−2 / +3 avec 3
|
||
rangs) n'est pas modélisée. `default_policy` ne le choisit pas (disponible
|
||
via une politique personnalisée).
|
||
- Se relever (`Action(kind="stand_up")`, action de mouvement) : le combattant à
|
||
plat ventre (renversé par trip/overrun ou après s'être jeté à terre) retire la
|
||
condition `prone` des **deux** représentations (`active_conditions` durée,
|
||
`conditions` sans durée) et redevient valide. Le relèvement **provoque une
|
||
attaque d'opportunité** de chaque ennemi menaçant (résolue via la mécanique de
|
||
mouvement `_check_move_aoo`, pendant qu'il est encore à plat ventre, CA −4).
|
||
Sans condition `prone`, l'action est rejetée (`not_prone`). Le kind ne requiert
|
||
pas de cible. `default_policy` ne la choisit pas (disponible via une politique
|
||
personnalisée).
|
||
- Se jeter à terre (`Action(kind="drop_prone")`, action libre) : le combattant
|
||
ajoute `prone` à la liste `conditions` **sans durée**, donc la condition
|
||
persiste jusqu'à `stand_up` (contrairement à trip/overrun qui posent
|
||
`active_conditions` avec 1 round). Les modificateurs de `prone` s'appliquent
|
||
immédiatement : l'attaquant à plat ventre subit −4 à ses attaques (de mêlée
|
||
selon le CRB ; les armes à distance sont traitées comme des arbalètes —
|
||
utilisables à −4, simplification documentée), la cible à plat ventre subit
|
||
−4 CA contre les attaques de mêlée et gagne +4 CA contre les attaques à
|
||
distance. Si la condition est déjà
|
||
présente, l'action est rejetée (`already_prone`). Le kind ne requiert pas de
|
||
cible. `default_policy` ne la choisit pas (disponible via une politique
|
||
personnalisée).
|
||
- Aider autrui (`Action(kind="aid_another", aid_mode=...)`, action standard,
|
||
U5e) : l'aidant **doit avoir une arme de mêlée utilisable** ; il fait un jet
|
||
d'attaque `d20 + bonus d'attaque de sa première arme de mêlée utilisable`
|
||
contre le CA 10 (20 naturel réussit toujours, 1 naturel échoue toujours —
|
||
convention des jets d'attaque du moteur). Sur succès, la cible empile **+2**
|
||
soit à son prochain jet d'attaque (`aid_mode="attack"` ou absent — consommé
|
||
one-shot par `resolve_attack`, attaque et confirmation de critique), soit à
|
||
sa CA contre la prochaine attaque la ciblant (`aid_mode="ac"`). Plusieurs
|
||
aides s'additionnent (le bonus s'ajoute, ne se remplace pas) ; tout bonus
|
||
inutilisé expire au début du prochain tour du bénéficiaire. Simplifications
|
||
documentées : pas de vérification d'adjacence/ligne d'effet vers l'allié,
|
||
toute cible résolue peut être aidée, et la progression de compétence d'aide
|
||
(+5 avec 5 rangs, +8 avec 10) n'est pas modélisée. Sans cible →
|
||
`missing_target` ; sans arme de mêlée utilisable → `no_weapon`.
|
||
`default_policy` ne la choisit pas (disponible via une politique
|
||
personnalisée).
|
||
- Retour d'exécution des actions : chaque action produit un `ActionResult`
|
||
explicite (kind, ok, reason) consigné dans le snapshot (`last_turn_results`
|
||
au tour suivant). Les actions invalides sont rejetées et journalisées au lieu
|
||
d'être des no-op silencieux : cible absente → `missing_target` ; kinds
|
||
déclarés mais non implémentés (`swift`, `free`, `full_round` — leur
|
||
implémentation générique reste à faire) → `unsupported_kind` ; dégradation
|
||
par le budget général d'action (X4 — action excédentaire retirée de la
|
||
séquence de la politique), par le pré-filtre des temps d'incantation longs
|
||
(sort `long` jamais planifiable dans un tour) ou par les filtres
|
||
staggered/nauseated (ou swift consommé par une immédiate) →
|
||
`downgraded:action_economy` / `downgraded:cast_time` /
|
||
`downgraded:staggered` / `downgraded:nauseated` /
|
||
`downgraded:immediate_swift` / `downgraded:surprise` ;
|
||
retard valide → `ok=True, reason="delayed"`, second retard du même round →
|
||
`ok=False, reason="already_delayed"` ; préparation valide →
|
||
`ok=True, reason="ready"` ; relèvement sans `prone` → `ok=False,
|
||
reason="not_prone"` ; se jeter à terre déjà `prone` → `ok=False,
|
||
reason="already_prone"` ; attaque avec un `weapon_id` choisi inutilisable →
|
||
`ok=False, reason="weapon_unavailable"` ; incantation avec un `target_pos`
|
||
hors carte ou sans ligne d'effet → `ok=False, reason="invalid_destination"`
|
||
(U7b) ; incantation sans emplacement disponible au niveau requis — niveau
|
||
de base ou majoré par la métamagie (B7) → `ok=False, reason="no_slot"` ;
|
||
course ou aide sans cible **et** sans destination →
|
||
`missing_target` ; aide sans arme de mêlée utilisable → `no_weapon` ;
|
||
course vers une `destination` hors du budget 4× →
|
||
`unreachable_destination` (U5e).
|
||
- 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
|
||
rafale. Attaque à outrance (action à round complet) : à BAB +6/+11/+16, des
|
||
attaques supplémentaires à -5/-10/-15 pour les armes manufacturées. Les
|
||
armes naturelles (`natural: true` dans le JSON — morsures, griffes, sabots,
|
||
talons…) ne gagnent **jamais** d'itératifs : une attaque à outrance avec une
|
||
arme naturelle résout **un seul** balayage quel que soit le BAB (CRB
|
||
« Natural Attacks » : « You do not receive additional natural attacks for a
|
||
high base attack bonus »). Une attaque naturelle marquée `secondary: true`
|
||
(les attaques secondaires du bestiaire) frappe à **BAB −5** ; le don
|
||
Multiattack réduit cette pénalité à **−2** (CRB « Natural Attacks » /
|
||
« Multiattack »). Plusieurs armes naturelles en une seule attaque à outrance
|
||
ne sont pas modélisées.
|
||
Choix de l'arme : `Action(kind="attack", weapon_id=...)` et
|
||
`Action(kind="full_attack", weapon_id=...)` frappent avec l'arme désignée
|
||
(l'arc plutôt que l'épée, l'arme principale plutôt que la secondaire) au
|
||
lieu de l'arme auto-sélectionnée par `weapon_for` (première arme utilisable
|
||
capable de toucher la cible à cette distance avec ligne d'effet).
|
||
L'`id` doit désigner une arme utilisable (non brisée, non volée) pouvant
|
||
atteindre la cible ; sinon l'action est rejetée (`weapon_unavailable`) et
|
||
aucun jet n'est effectué. Sans `weapon_id`, la sélection automatique est
|
||
inchangée. `default_policy` ne choisit pas d'arme (disponible via une
|
||
politique personnalisée).
|
||
- Mêlée (conformance R1, CRB « Big and Little Creatures in Combat ») :
|
||
l'**allonge naturelle** du gabarit (Tiny/Diminutive/Fine 0 ft, Small/Medium
|
||
5 ft, Large 10 ft, Huge 15 ft, Gargantuan 20 ft, Colossal 30 ft) est un
|
||
**disque** : la créature
|
||
frappe et menace toutes les cases jusqu'à son allonge, l'adjacence
|
||
comprise — une créature Large dont l'attaque naturelle a `reach_ft` 10
|
||
menace donc aussi les cases adjacentes (le `reach_ft` d'une attaque
|
||
naturelle EST son allonge naturelle). Une **arme à allonge**
|
||
(`reach_weapon: true` dans le JSON) est un **anneau** : elle double
|
||
l'allonge naturelle du porteur mais ne frappe pas à l'allonge naturelle ou
|
||
moins — la zone utilisable est `allonge naturelle < distance ≤ 2 ×
|
||
allonge naturelle`, la dead zone vaut donc 5 ft pour un porteur
|
||
Small/Medium et 10 ft pour un porteur Large. Une arme ordinaire sans
|
||
allonge menace les cases adjacentes (disque de 5 ft).
|
||
**S4c — allonge nulle des créatures Tiny et plus petites** (CRB « Big and
|
||
Little Creatures in Combat » : « Tiny, Diminutive, and Fine Creatures [...]
|
||
They must enter an opponent's square to attack in melee ») : l'allonge
|
||
naturelle de Tiny/Diminutive/Fine vaut **0 ft** — une telle créature ne
|
||
**menace aucune case** (aucune AoO) et ne peut pas frapper une case
|
||
adjacente ; son attaque de mêlée n'est utilisable qu'à **distance 0**, la
|
||
case qu'elle partage avec sa cible (l'abstraction « entrer dans la case de
|
||
l'adversaire »). L'allonge effective reste `max(allonge naturelle,
|
||
reach_ft de l'arme)` : une attaque naturelle dont le JSON déclare
|
||
explicitement `reach_ft` (ierlieth : morsure `reach_ft: 5`) conserve cette
|
||
allonge — le `reach_ft` d'une attaque naturelle EST son allonge naturelle,
|
||
une créature Tiny peut donc être encodée avec une allonge non nulle si sa
|
||
fiche l'exige.
|
||
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. Conformance L5 (SRD « Movement, Position and
|
||
Distance ») : « You can move through a square occupied by a friendly
|
||
character, unless you are charging » — `move` et `run` **traversent** les
|
||
cases des alliés, mais ne peuvent jamais **finir** sur une case occupée :
|
||
entrer sur une case alliée
|
||
exige un prolongement libre (règle du « pass-through »), et la queue du
|
||
chemin est tronquée si le budget s'épuise sur une case alliée. SRD
|
||
« Moving Through a Square » : « Any creature can move through a
|
||
square occupied by a creature three size categories larger than itself
|
||
[...] Creatures moving through squares occupied by other creatures
|
||
provoke attacks of opportunity from those creatures » — un ennemi séparé
|
||
du déplaceur par **trois catégories de taille ou plus** est traversé de
|
||
la même façon (jamais fini dessus : « You can't end your movement in the
|
||
same square as another creature unless it is helpless »), et l'entrée dans
|
||
sa case **provoque une AoO de l'occupant** (une seule par entrée fraîche,
|
||
plafonnée par l'allowance du round ; résolu par `_check_enter_aoo` dans la
|
||
marche pas à pas). Un ennemi à 0–2 catégories
|
||
reste un blocage total (« You can't move through a square occupied by an
|
||
opponent unless the opponent is helpless »). La charge
|
||
reste bloquée par les créatures (ligne droite dégagée), comme la retraite
|
||
(limite documentée : le passage à travers les alliés n'est pas modélisé
|
||
pour `withdraw` et le pas de placement auto-dirigé).
|
||
Vitesse de déplacement (V4) : le budget de tout mouvement (déplacement,
|
||
charge 2×, retraite 2×, course 4×) est le **meilleur entre la vitesse au
|
||
sol (`speed_land_ft`) et la vitesse de vol (`speed_fly_ft`)** — une créature
|
||
dotée d'une vitesse de vol (harvie : vol 80 vs sol 30, ierlieth : vol 40)
|
||
se déplace donc à sa vitesse de vol. Simplification documentée : voler
|
||
PAR-DESSUS les obstacles ou le terrain difficile n'est PAS modélisé — la
|
||
praticabilité et les coûts de terrain de la grille s'appliquent à tout le
|
||
budget (pas de vol 3D ni de maniabilité) ; le snapshot expose ce budget
|
||
sous la clé additive `speed_effective_ft`.
|
||
Destination explicite : `Action(kind="move", destination=(r, c))` et
|
||
`Action(kind="5foot_step", destination=(r, c))` déplacent le combattant vers
|
||
la case `(r, c)` désignée (monter au couvert, quitter un défilé, se
|
||
désengager) au lieu de auto-diriger vers `target_id`. Pour `move`, la case
|
||
doit être praticable, libre et atteignable dans le budget de vitesse (sinon
|
||
rejet `unreachable_destination`) ; le trajet réutilise la mécanique pas à pas
|
||
avec provocation d'AoO. Pour `5foot_step`, la case doit être exactement
|
||
adjacente (sinon `unreachable_destination`) et le pas ne provoque jamais
|
||
d'AoO. Sans `destination`, le comportement auto-dirigé est inchangé ; avec
|
||
`target_id` et `destination` absents, rejet `missing_target` (U1).
|
||
`default_policy` ne choisit pas de destination (disponible via une politique
|
||
personnalisée).
|
||
- 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.
|
||
**S4c — co-occupation des créatures Tiny et plus petites** (CRB « Big and
|
||
Little Creatures in Combat » : « Very small creatures take up less than 1
|
||
square of space. This means that more than one such creature can fit into a
|
||
single square ») : une créature Tiny/Diminutive/Fine peut **traverser et
|
||
finir son déplacement** dans une case occupée par une créature plus grande,
|
||
et une créature plus grande peut traverser et finir dans une case occupée
|
||
par une créature Tiny ou plus petite — la case n'est pas « bloquée » pour
|
||
ce couple de tailles (`_can_cooccupy` : tailles différentes ET la plus
|
||
petite est Tiny ou moins). Deux créatures de même taille (ou Small et plus
|
||
grande) ne partagent jamais une case : le blocage reste total pour elles.
|
||
La co-occupation s'applique au mouvement (`_blocked`/`_blocked_hostile`
|
||
sont conscients du déplaceur), à la destination explicite (`_cells_free`)
|
||
et à la charge ; deux créatures partageant une case sont à **distance 0**
|
||
l'une de l'autre (ligne d'effet directe, attaque de mêlée possible — c'est
|
||
la voie d'attaque des créatures à allonge nulle, voir « Mêlée »).
|
||
- Taille et attaques/CA : les valeurs `ac` (total/touch/flat_footed) et
|
||
`attack_bonus` des JSON de créatures sont des valeurs FINALES, déjà majorées
|
||
du modificateur de taille PF1e aux attaques/CA (Small +1, Medium 0, Large −1,
|
||
Tiny +2…) — vérifié sur toutes les fiches (`data/monsters/*.json`,
|
||
`data/pcs/*.json`, verrouillé par `tests/test_size_is_baked_in.py`). Le
|
||
moteur n'applique donc JAMAIS de bonus de taille aux jets d'attaque ni à la
|
||
CA (ce serait un double comptage) ; la taille n'intervient dans la
|
||
résolution que pour les manœuvres de combat (CMB/CMD, table
|
||
`_SIZE_MODIFIERS` Small −1 … Colossal +8 — colonne distincte de la table
|
||
attaques/CA), l'allonge naturelle, le footprint et l'occupation.
|
||
- É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). Conformance vérifiée (SRD Table 8-5
|
||
« Attack Roll Modifiers ») : « On higher ground — Melee +1, Ranged +0 » —
|
||
le bonus est à juste titre réservé aux attaques de mêlée, les attaques à
|
||
distance ne le reçoivent pas. 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 (`immediate_used`).
|
||
Conformance L10 (SRD « Immediate Actions ») : « Using an immediate action
|
||
on your turn is the same as using a swift action and counts as your swift
|
||
action for that turn » — une immédiate déclenchée **pendant le tour** du
|
||
combattant consomme le swift du **tour courant** (`swift_used_this_turn`,
|
||
posé par le moteur sur l'acteur du tour) : tout swift encore présent dans
|
||
la séquence est retiré (`downgraded:immediate_swift`) et aucune seconde
|
||
immédiate ne part ce round ; le swift du tour **suivant** reste disponible
|
||
(contrairement à l'immédiate hors-tour). Simplification documentée :
|
||
l'interdiction RAW d'une seconde immédiate s'étend jusqu'au début du tour
|
||
suivant, le moteur la borne au round en cours. Conformance L5 (SRD
|
||
« Immediate Actions ») : « You also cannot use an immediate action if you
|
||
are flat-footed » — un combattant portant la condition `flat_footed` est
|
||
**sauté** par la porte de réaction (`_check_reactions`) : aucune immédiate
|
||
ne part tant qu'il est à plat (la condition est injectable via
|
||
`active_conditions` ; le moteur ne la pose jamais lui-même — voie
|
||
d'entrée future).
|
||
Les actions préparées (`ready`) sont traitées sur ces mêmes
|
||
événements **avant** et indépendamment de la politique de réaction (voir
|
||
« Préparation » plus haut).
|
||
- 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.
|
||
- Couvert (règle des coins) : la cible gagne le bonus de couvert du terrain
|
||
(+2 partiel, +4 standard, +8 amélioré via `cover_bonus`) sur la touche et la
|
||
confirmation de critique ; couvert mou : une créature active entre
|
||
l'attaquant et la cible octroie aussi +4 CA aux attaques à distance (les
|
||
attaques de mêlée ignorent les créatures). Conformance B20 (CRB « Cover »,
|
||
règle des coins) : c'est **l'attaquant qui choisit son meilleur coin** —
|
||
conformance L9 (`los.py`, `cover_bonus_cells`) : à distance, le score de
|
||
chaque case attaquante est le **minimum sur ses quatre coins** de (maximum
|
||
sur les lignes vers les coins de la cible) — « choose a corner of your
|
||
square » — puis le minimum sur les cases de l'attaquant ; un créneau
|
||
n'est donc jamais compté contre l'attaquant si l'un de ses coins l'évite
|
||
(le bonus reste cohérent avec la porte `has_cover_cells`, qui teste déjà
|
||
« tout coin de l'attaquant »). Une créature multi-cases (Large +)
|
||
ne subit que le couvert de SA meilleure
|
||
case occupée (minimum sur ses cases), pas le pire. Côté cible : à distance,
|
||
toute ligne vers tout coin des cases de la cible compte (maximum sur les
|
||
coins de la cible) ; **en mêlée**, l'attaquant peut choisir n'importe
|
||
quelle case occupée par une créature multi-cases pour déterminer le couvert
|
||
(conformance A5 — CRB « Big Creatures and Cover » : « when making a melee
|
||
attack against such a creature, you can pick any of the squares it occupies
|
||
to determine if it has cover against you ») — le score est donc le minimum
|
||
sur les cases de la cible en mêlée, le maximum à distance. **Mêlée
|
||
adjacente** (conformance — SRD « Cover » : « When making a melee attack
|
||
against an adjacent target, your target has cover if any line from any
|
||
corner of your square to the target's square goes through a wall ») : la
|
||
règle est celle de **toute ligne** — une seule ligne coin-à-coin bloquée
|
||
suffit à donner le couvert, l'attaquant ne choisit plus son meilleur coin
|
||
(score : maximum sur les seize lignes de la paire de cases, minimum sur
|
||
les paires de cases adjacentes). **Mêlée non adjacente** (arme à allonge,
|
||
distance > 1) : le SRD renvoie à la règle à distance (« use the rules for
|
||
determining cover from ranged attacks ») — l'attaquant garde son **meilleur
|
||
coin** (minimum sur ses coins du maximum des lignes), avec le choix de la
|
||
case de cible multi-cases.
|
||
- Flanquement : +2 au jet d'attaque de mêlée (toucher de mêlée inclus, voir
|
||
« Touch AC ») si un allié actif menace réellement la cible depuis la
|
||
bordure ou le coin opposé — conformance P2 : l'allié doit **menacer** une
|
||
case de la cible avec une arme de mêlée utilisable (la « dead zone » de 5 ft
|
||
d'une arme allongée ne menace pas) et ne pas être incapable d'agir
|
||
(étourdi/paralysé ne flanque pas). Les attaques à distance ignorent le
|
||
flanquement ; un allié désarmé ou à distance ne compte pas.
|
||
- Attaques d'opportunité : un combattant actif doté d'une arme de mêlée menace
|
||
les cases de sa zone d'allonge (voir « Mêlée » : le disque de l'allonge
|
||
naturelle — les 8 cases adjacentes pour un Small/Medium sans allonge — ou
|
||
l'anneau d'une arme à allonge, dont la dead zone 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 ; sortir
|
||
de plusieurs cases menacées par le **même** adversaire dans le même round ne
|
||
compte qu'**une seule opportunity** pour lui (piste `_move_aoo_provoked`,
|
||
réinitialisée au début du round — conformance A1, CRB « Attacks of
|
||
Opportunity ») ; le relèvement (stand_up) est un déclencheur distinct et
|
||
provoque toujours ; (2) tirer
|
||
avec une arme à distance depuis une case menacée — chaque ennemi menaçant
|
||
porte une AoO avant chaque tir. Une AoO par combattant par round (piste
|
||
`_aoo_used`, réinitialisée au début du round) ; le budget restant est exposé
|
||
dans l'Observation via la clé `aoo_remaining` par combattant ; les AoO sont
|
||
toujours portées
|
||
(PF1e permet de les décliner, le simulateur ne le fait pas) — **sauf** si la
|
||
cible a un couvert ou une dissimulation totale par rapport à l'attaquant :
|
||
l'AoO est alors supprimée et non consommée (conformance A4, CRB « Cover » :
|
||
« You can't execute an attack of opportunity against an opponent with cover
|
||
relative to you » ; CRB « Concealment » : dissimulation totale). Le pas de
|
||
placement (5 ft) ne provoque jamais d'AoO (voir ci-dessous) ; la retraite ne
|
||
protège que la case de départ (voir ci-dessous).
|
||
- Charge : action à round complet. Le combattant se déplace en ligne droite
|
||
(Bresenham) jusqu'à 2× sa vitesse vers la case la plus proche d'où il peut
|
||
frapper la cible en mêlée, puis porte une seule attaque de mêlée à +2. La
|
||
charge impose −2 CA à l'attaquant jusqu'au début de son prochain tour. La
|
||
ligne droite doit être dégagée (pas de terrain difficile, d'obstacles ni de
|
||
créatures) et ne peut pas couper un coin de mur (règle des coins, SRD
|
||
« Measuring Distance » : « You can't move diagonally past a corner ») ;
|
||
distance minimum 2 cases (10 ft). `default_policy` choisit la
|
||
charge quand l'ennemi est hors de portée mais joignable en ligne droite.
|
||
- Retraite : action à round complet. Le combattant se déplace jusqu'à 2× sa
|
||
vitesse en s'éloignant de l'ennemi le plus proche (ascension gloutonne du
|
||
champ de coût Dijkstra depuis la menace). La case de départ n'est pas
|
||
considérée comme menacée — aucune AoO en la quittant. Les cases
|
||
ultérieures provoquent des AoO normalement (résolues pas à pas, comme pour
|
||
le mouvement). Conformance L6 (SRD « Full-Round Actions — Restricted
|
||
Withdraw ») : « If you are limited to taking only a standard action each
|
||
round you can withdraw as a standard action. In this case, you may move up
|
||
to your speed » — un combattant `staggered`/invalidé qui reçoit `withdraw`
|
||
le conserve comme **action standard** (le filtre staggered ne le jette
|
||
plus) avec un budget de **1× vitesse** au lieu de 2× (`_flee_path` via
|
||
`_is_staggered_limited`). `default_policy` ne choisit pas la retraite (disponible via
|
||
une politique personnalisée).
|
||
- Course (`Action(kind="run")`, action à round complet, U5e) : le combattant
|
||
se déplace jusqu'à **4× sa vitesse** vers `target_id` (auto-dirigé,
|
||
s'arrête adjacent à la cible) ou vers une `destination` explicite (U5c).
|
||
**S4a — la course est une ligne droite** (SRD « Full-Round Actions — Run » :
|
||
« When you run, you can move up to four times your speed **in a straight
|
||
line** ») : contrairement à `move` (descente Dijkstra qui contourne les
|
||
obstacles), la course suit la **ligne de Bresenham** entre la position
|
||
courante et la cible/destination, et **s'arrête au premier obstacle** —
|
||
terrain infranchissable, terrain difficile, ou créature bloquante — en
|
||
miroir de la logique de ligne dégagée de la charge (`_run_line_path`,
|
||
`_charge_path_clear`). Le déplacement reste pas à pas avec **provocation
|
||
d'AoO** (courir ne protège pas des AoO), et conserve le budget 4×, le
|
||
drapeau `moved_this_turn` et l'exclusivité avec les autres mouvements.
|
||
Comme pour `move`, la course **traverse les cases des alliés** sans jamais
|
||
y **finir** (SRD « Movement », conformance L5 — la queue du chemin est
|
||
tronquée) ; une créature hostile (la cible incluse) arrête la course une
|
||
case avant, donc la course auto-dirigée **s'arrête adjacent à la cible**.
|
||
La course **ne traverse pas le terrain difficile** (conformance R2 — CRB
|
||
« Difficult Terrain » : « You can't run or charge across difficult
|
||
terrain ») : elle avance jusqu'à l'obstacle et s'y arrête **quand elle est
|
||
auto-dirigée**. À l'achèvement, le combattant est `running` jusqu'au début
|
||
de son prochain tour : il **perd son bonus de Dextérité à la CA** (résolu
|
||
par le même chemin flat-footed/`denies_dex_to_ac` que les conditions),
|
||
**sauf s'il a le don Run** (SRD « Run » : « You lose any Dexterity bonus to
|
||
AC unless you have the Run feat » — `_running_denies_dex`). **Endurance
|
||
(SRD « Full-Round Actions — Run »)** : « You can run for a number of rounds
|
||
equal to your Constitution score, but after that you must make a DC 10
|
||
Constitution check to continue running. You must check again each round in
|
||
which you continue to run, and the DC of this check increases by 1 for each
|
||
check you have made. When you fail this check, you must stop running. A
|
||
character who has run to his limit must rest for 1 minute (10 rounds) before
|
||
running again. » Le moteur compte les rounds de course **consécutifs** par
|
||
combattant (`run_rounds`, la série se brise quand un tour passe sans
|
||
courir) ; au-delà du score de Constitution, chaque nouvelle round exige un
|
||
jet de Constitution DD `10 + jets déjà faits` (`run_checks`), **sans**
|
||
réussite/échec automatique sur 20/1 naturel (un test de caractéristique
|
||
n'est pas un jet d'attaque) ; le don **Run** donne **+4** à ce jet. Un échec
|
||
stoppe la course (tour perdu, aucun mouvement, journalisé `run stopped`) et
|
||
pose `run_rest_rounds = 10` : pendant le repos, toute action `run` est
|
||
refusée (journalisé `wait (cannot run, resting …)`) — le round de l'échec
|
||
compte comme le premier des dix. Les morts-vivants et les constructeurs n'ont
|
||
pas de score de Constitution et sont exemptés. Simplification assumée : la
|
||
règle du repos borne la course consécutive par un compteur de tours au sens
|
||
du moteur (le « repos » n'exige pas l'inactivité totale, seule la course est
|
||
interdite). **Hors périmètre (documenté, non approximé)** : la clause
|
||
d'armure lourde « or three times your speed if you're in heavy armor » — le
|
||
modèle n'a aucun champ armure (la CA des JSON est une valeur finale). Une
|
||
`destination` que la ligne droite ne peut pas honorer dans le budget 4×
|
||
(hors portée, bloquée par un obstacle/une créature, ou joignable uniquement
|
||
en tournant un coin ou en traversant le terrain difficile) est **rejetée**
|
||
(`unreachable_destination`) — le verdict et le chemin parcouru sont
|
||
cohérents (pas de contournement silencieux) ; sans cible ni destination,
|
||
rejet (`missing_target`). `default_policy` ne choisit pas la course
|
||
(disponible via une politique personnalisée).
|
||
- Pas de placement (5-foot step) : action libre qui déplace le combattant d'une
|
||
case vers l'ennemi le plus proche sans provoquer d'AoO. Exclusivité mutuelle
|
||
avec tout autre mouvement (move, charge, retraite) dans le même tour —
|
||
tracked via `moved_this_turn`, réinitialisé au début du tour. Conformance
|
||
L3 (SRD « Measuring Distance ») : « You can't move diagonally past a corner
|
||
(even by taking a 5-foot step) » — un pas diagonal dont les deux cases
|
||
adjacentes (flancs) ne sont pas toutes deux praticables est rejeté
|
||
(`unreachable_destination`), comme pour tout autre déplacement. Simplification
|
||
documentée (plus strict que le RAW) : le texte « You can also move diagonally
|
||
past other impassable obstacles, such as pits » autorise à passer en diagonale
|
||
au ras d'un obstacle isolé (un seul flanc bloqué) — la règle du « coin » RAW
|
||
ne bloque que les deux flancs à la fois ; le moteur ne distingue pas les
|
||
obstacles en coin (murs) des obstacles isolés (fosses) et exige les deux
|
||
flancs praticables, approximation conservatrice assumée (relâcher la règle
|
||
modifierait les chemins Dijkstra, donc les positions et transcripts dorés).
|
||
`default_policy`
|
||
choisit `(5foot_step, full_attack)` quand un seul pas suffit à entrer en
|
||
portée.
|
||
- Distance : pénalité cumulative de −2 par incrément de portée complet au-delà
|
||
du premier, appliquée au jet d'attaque et à la confirmation de critique ;
|
||
les armes ranged restent utilisables jusqu'à 10 incréments, les armes touch
|
||
avec incrément (armes de jet, firearms) jusqu'à 5.
|
||
- Touch AC et armes à feu : les attaques `kind: touch` résolvent leur touche
|
||
contre la CA de contact de la cible (`ac.touch`) ; si la cible perd son bonus
|
||
de Dextérité à la CA (flat-footed, `denies_dex_to_ac`), la CA de contact est
|
||
amputée du modificateur de Dextérité. Les armes marquées `firearm: true`
|
||
(pistolets) résolvent contre la CA de contact uniquement dans leur premier
|
||
incrément de portée — au-delà, elles frappent la CA totale. Une attaque touch
|
||
avec incrément est une attaque à distance : elle provoque des attaques
|
||
d'opportunité depuis une case menacée, subit −4 si l'attaquant est à plat
|
||
ventre et bénéficie du couvert mou ; un contact de mêlée (sans incrément,
|
||
ex. le contact paralysant du liche) n'en fait rien. Conformance P2 : le
|
||
contact de mêlée (touch sans incrément) est une **attaque de mêlée** — il
|
||
bénéficie du flanquement, de la hauteur (+1), du +4 contre une cible inerme
|
||
et peut porter une attaque sournoise ; Power Attack (conformance A6 — CRB :
|
||
« a –1 penalty on all melee attack rolls… The bonus damage does not apply to
|
||
touch attacks ») s'applique à **la touche** du contact de mêlée (pénalité −X
|
||
au jet d'attaque et de confirmation) mais son bonus de dégâts reste réservé
|
||
aux armes de mêlée (le contact ne gagne pas le +2X/+3X).
|
||
- Jets de sauvegarde : `resolve_save(state, save_type, dc)` — fort/ref/will.
|
||
1 naturel = échec automatique, 20 naturel = succès automatique. Les
|
||
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, ...}`) — convention de données :
|
||
`spell_slots` est une valeur **finale** par rencontre et le moteur ne calcule
|
||
**jamais** les sorts bonus de la caractéristique d'incantation élevée (SRD
|
||
« Abilities and Spellcasters » + Table 1-3 « Ability Modifiers and Bonus
|
||
Spells » : sagesse 18 (+4) → +1 sort par niveau 1-4) — les éventuels sorts
|
||
bonus doivent être **encodés dans le JSON par l'auteur de la fiche**.
|
||
Preuve du choix (constat 2026-09) : les fiches PJ actuelles encodent la
|
||
table de classe de BASE sans bonus (oni, clerc 4, SAG 18 → `{1:4, 2:3}` =
|
||
table de base 4/3, alors que le RAW avec bonus serait 5/4 ; nairda, sorcière
|
||
4, INT 21 → `{1:4, 2:3}` au lieu de 6/4), tandis que le liche (INT 20,
|
||
niveau d'incanteur 11) porte `{1:4, 2:4, 3:4, 4:4, 5:4, 6:3}`, qui ne
|
||
correspond à AUCUNE table quotidienne RAW (sorcier 11 : 7/7/7/6/6/4 + bonus)
|
||
— les slots sont donc un budget abstrait d'encounter authored, pas une
|
||
progression RAW ; un calcul moteur de bonus s'appliquerait sur des données
|
||
non-RAW et gonflerait arbitrairement le budget. Conséquence assumée : les
|
||
PJ incanteurs tiennent moins de sorts par rencontre que le RAW ne le
|
||
permettrait. `_cast_spell` dépense
|
||
l'emplacement du niveau concerné (`spend_spell_slot`) — **sans emplacement
|
||
disponible, l'incantation est refusée** (journalisée `NO SPELL SLOT`, feedback
|
||
`ok=False, reason="no_slot"`, aucun jet consommé) ; les cantrips (niveau 0) s'incantent librement. Le DD vaut `10 + niveau du
|
||
sort (niveau de base du sort — la métamagie ne le majore PAS, sauf
|
||
Heighten Spell, voir ci-dessous) +
|
||
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`). Métamagie (conformance B7, CRB
|
||
« Metamagic Feats ») : la métamagie se choisit **à chaque incantation** via
|
||
`Action(kind="cast_spell", metamagic=("Empower Spell", ...))` — seuls les
|
||
dons demandés ET connus du lanceur (`metamagic_feats`) s'appliquent (un don
|
||
inconnu est ignoré, un don connu non demandé n'est pas appliqué) ; le sort
|
||
occupe alors un emplacement de niveau de base + ajustement (Empower +2,
|
||
Maximize +3, Extend +1) mais **son DD reste calculé au niveau de base du
|
||
sort** (SRD `feats/empower-spell.md` : « Saving throws and opposed rolls
|
||
are not affected ») — seule l'exception SRD Heighten Spell majore le DD au
|
||
niveau effectif majoré (SRD `feats/heighten-spell.md` : « All effects
|
||
dependent on spell level (such as saving throw DCs [...]) are calculated
|
||
according to the heightened level »), modélisée à son ajustement minimal
|
||
+1 (la signature d'action ne porte pas le niveau choisi — simplification
|
||
documentée) ; sans
|
||
emplacement au niveau requis, l'incantation est refusée (`no_slot`). Avec
|
||
`metamagic` vide (défaut), le comportement est inchangé. Incanter dans une
|
||
case menacée provoque des AoO. Incantation défensive (conformance B24, CRB
|
||
« Concentration ») : `Action(kind="cast_spell", cast_defensively=True)`
|
||
évite la provocation d'AoO mais impose un jet de concentration DD
|
||
`15 + 2×niveau` (+4 avec Combat Casting) ; un échec perd le sort
|
||
(emplacement brûlé, journalisé `casting defensively: concentration failed`).
|
||
Agrippé (`grappled_with`) : un jet de concentration DD
|
||
`10 + CMB de l'adversaire + niveau` (+4 Combat Casting) est requis pour
|
||
incanter, défensivement ou non — ce jet remplace la provocation (un
|
||
agrippé ne peut pas porter d'AoO). Tout dégât subi pendant l'incantation
|
||
(AoO) impose un jet DD `10 + dégâts + niveau` **sans** le bonus Combat
|
||
Casting (conformance P1). Jet de concentration = d20 + niveau d'incanteur +
|
||
mod de la caractéristique d'incantation (Int/Wis/Cha — jamais Constitution,
|
||
CRB). Sorts de contact (conformance B14, CRB « Magic — Range: Touch ») :
|
||
délivrer un sort de portée `touch` à une créature **hostile** (camp
|
||
différent) exige une attaque de contact de mêlée — d20 + BAB + mod de Force
|
||
du lanceur contre la CA de contact de la cible (amputée du modificateur de
|
||
Dextérité si la cible perd son bonus de Dextérité, même chemin que
|
||
`resolve_attack`) ; en cas d'échec le sort **n'est pas dépensé** : le
|
||
lanceur **garde la charge** (SRD « Holding the Charge » : « If the attack
|
||
misses, you are still holding the charge [...] You can continue to make
|
||
touch attacks round after round ») — l'état `held_charge` du lanceur
|
||
(sort, cible, métamagie, niveau d'emplacement) est rejoué automatiquement
|
||
au **début de chacun de ses tours suivants** tant que la cible vit et
|
||
reste à portée de contact ; une nouvelle incantation **dissipe** la charge
|
||
(« If you cast another spell, the touch spell dissipates ») et une cible
|
||
morte la dissipe aussi (journalisé `dissipates`). L'emplacement reste
|
||
brûlé (dépensé à l'incantation). Simplifications documentées : Weapon
|
||
Finesse et les autres bonus situationnels de jet de contact ne sont pas
|
||
modélisés ; la re-tentative est pilotée par le moteur au début du tour
|
||
(coût RAW : action standard) ; la décharge sur tout contact (même
|
||
involontaire) et les attaques à l'arme tenue pendant la charge ne sont pas
|
||
modélisés ; la charge n'est pas exposée dans l'Observation. Une cible
|
||
volontaire (soi ou un allié du même camp) ne demande aucun jet. Sorts à
|
||
contact à distance (CRB « Magic — Ranged Touch Attacks ») : un sort dont le
|
||
JSON déclare `requires_ranged_touch: true` (les rayons du SRD — Ray of
|
||
Frost, Ray of Enfeeblement, Scorching Ray, Acid Arrow, Acid Splash, Disrupt
|
||
Undead, Shard of Chaos, Spear of Purity) impose une attaque de contact à
|
||
distance contre une cible **hostile** (hors effet de zone) — d20 + BAB + mod
|
||
de **Dextérité** du lanceur contre la CA de contact de la cible (amputée du
|
||
modificateur de Dextérité si la cible perd son bonus de Dextérité, même
|
||
chemin que `resolve_attack`) ; la portée du sort est une valeur absolue, il
|
||
n'y a donc **pas** de pénalité par incrément de portée ; en cas d'échec le
|
||
sort n'a aucun effet (l'emplacement est brûlé). Simplifications documentées
|
||
(miroir du contact de mêlée) : le modificateur de taille et Weapon Finesse
|
||
ne sont pas modélisés. Conformance L3 (CRB « Cast a Spell — Touch Attacks »,
|
||
« Cover », « Concealment », « Range — Touch ») : le jet de contact (mêlée ou
|
||
distance) est une **attaque** — la CA de contact inclut le **couvert** de la
|
||
cible (terrain +2/+4/+8 et couvert mou +4 à distance, même chemin
|
||
`_attack_cover_bonus` que `resolve_attack` ; mêlée = ligne terrain seule), la
|
||
**dissimulation** peut faire rater l'attaque après la touche (jet de d100,
|
||
convention `max(concealment de la cible, 50)` si l'attaquant est `blinded`,
|
||
journalisé « spell lost » / « spell delivered »), et un sort de contact qui
|
||
**inflige des dégâts** menace le critique sur un 20 naturel et **double ses
|
||
dégâts** sur confirmation réussie (le jet de confirmation est un jet
|
||
d'attaque contre la même CA ; un sort de contact sans dégâts — Grace,
|
||
Ray of Enfeeblement — ne menace pas, aucun dé consommé). Magic Missile
|
||
(touche automatiquement), les sorts de
|
||
zone et les sorts à sauvegarde simple ne sont pas concernés. Un sort ciblé avec sauvegarde fait pleins dégâts
|
||
sur un échec et **aucun dégât sur un succès** (la sauvegarde annule) ; le
|
||
demi-dégât sur sauvegarde réussie ne s'applique que là où l'effet le déclare
|
||
(`half_on_save: true` pour les rayons/lignes/cônes, `AreaEffect.save_for_half`
|
||
pour les explosions). **Sorts « harmless » et cible consentante** (SRD
|
||
« Spell Descriptions — Saving Throw » : « (harmless) : The spell is usually
|
||
beneficial, not harmful, but a targeted creature can attempt a saving throw
|
||
if it desires » ; « Voluntarily Giving up a Saving Throw : A creature can
|
||
voluntarily forego a saving throw and willingly accept a spell's result ») :
|
||
`SpellSpec` porte un booléen `harmless` (JSON `"harmless"`, défaut `false`)
|
||
posés sur les 29 sorts dont la fiche SRD marque la sauvegarde « (harmless) »
|
||
(vérifiés un à un contre le KB — les sorts SRD « save: none » comme Aid et
|
||
Bless n'ont pas la marque et n'en ont pas besoin). Jeté sur une cible
|
||
**consentante** (soi-même ou un même camp), le jet de sauvegarde primaire
|
||
est **totalement sauté** — aucun dé consommé, journalisé `NO SAVE`
|
||
(`_save_forgone`). Deux exceptions fidèles conservent le jet : une cible
|
||
**hostile** n'est pas consentante, et un sort de soin sur un **mort-vivant**
|
||
lui est **nuisible** (SRD cure-light-wounds.md : il « deals damage to them
|
||
instead » avec « Will half » pour demi-dégâts). Simplification documentée :
|
||
la « volonté » est approximée par le camp (le moteur n'a pas de concept
|
||
d'intention de cible) ; un sort harmless jeté sur un ennemi garde donc son
|
||
jet. Ciblage par point (U7b) : `Action(kind="cast_spell",
|
||
target_pos=(r, c))` centre l'`AreaEffect` de niveau supérieur du sort sur la
|
||
case `(r, c)` désignée au lieu de la position de la cible créature — poser un
|
||
Fireball au sol, pas seulement sur une tête. Le point doit être dans la carte
|
||
et avoir une ligne d'effet depuis le jeteur (sinon rejet `invalid_destination`,
|
||
aucun sort lancé). Avec `target_pos` et pas de cible résolue, l'incantation se
|
||
fait sur le point : la sauvegarde primaire est sautée et chaque créature de la
|
||
zone effectue sa propre sauvegarde (SR incluse). `target_pos` n'a de sens que
|
||
pour un sort à zone : pour un sort ciblé sans `AreaEffect`, il est ignoré et
|
||
la cible résolue est utilisée normalement. Sans `target_pos`, le comportement
|
||
centré sur la créature est inchangé. `default_policy` ne choisit pas de point
|
||
(disponible via une politique personnalisée). `default_policy` choisit soin / offensif / buff via
|
||
`_find_heal_spell`, `_find_offensive_spell` et `_find_ally_needing_heal`.
|
||
- Formes de zone (S1a) et scalage par niveau d'incanteur (S1b) :
|
||
`AreaEffect.shape` vaut `burst` (défaut), `cone` ou `line` (CRB « Magic —
|
||
Aiming a Spell »). Le **burst** reste un cercle centré sur la case résolue
|
||
(cible ou `target_pos`, U7b) — **sauf** si l'`AreaEffect` déclare
|
||
`"centered_on_caster": true` : le point d'origine est alors **la case du
|
||
jeteur**, quel que soit la cible résolue ou le `target_pos` (SRD
|
||
spells/bless.md : « The caster and all allies within a 50-ft. burst,
|
||
centered on the caster » ; spells/bane.md : « 50-ft.-radius burst, centered
|
||
on you ») ; faute de cible primaire désignée, la sauvegarde primaire est
|
||
sautée et **chaque** créature de la zone jette la sienne (comme au
|
||
point-cast U7b). `bless.json` et `bane.json` portent le drapeau.. Le **cône** (90° — « a quarter-circle », la
|
||
largeur valant la longueur à chaque distance) et la **ligne** (5 ft de large,
|
||
longueur = `radius_ft`) partent de la **case du jeteur** et sont pointés
|
||
vers la cible résolue (`target_id`) ou le point désigné (`target_pos`) — le
|
||
cône exclut la case d'origine, la ligne part d'un **coin** de la case du
|
||
jeteur et n'affecte **pas** sa propre case (CRB « Aiming a Spell » : « It
|
||
starts from any corner of your square » — SRD Lightning Bolt : « The bolt
|
||
begins at your fingertips », le trait quitte la case du jeteur au lieu de la
|
||
remplir ; un viseur dégénéré origine==cible n'affecte personne). **Ligne
|
||
d'effet des zones** (SRD « Aiming a Spell — Line of Effect ») : la **ligne**
|
||
s'arrête à la première case qui bloque la ligne d'effet (« extends to the
|
||
limit of its range or until it strikes a barrier that blocks line of
|
||
effect ») — la case-barrière est la dernière du gabarit, rien au-delà ;
|
||
le **cône** exclut toute case sans ligne d'effet depuis sa case d'origine
|
||
(« a cone... affects only an area, creature, or object to which it has
|
||
line of effect from its origin ») ; le **burst** n'affecte aucune créature
|
||
en couvert total depuis son centre (« It can't affect creatures with total
|
||
cover from its point of origin (in other words, its effects don't extend
|
||
around corners) »). Toute
|
||
créature dont une case occupée intersecte
|
||
le gabarit subit l'effet, avec sauvegarde et RS par créature comme pour un
|
||
burst (application partagée, géométrie dans `areas.py`). **Couvert et
|
||
sauvegardes de Réflexes** (SRD « Cover and Reflex Saves ») : une créature
|
||
dans la zone qui a du couvert **terrain** entre elle et le point d'origine
|
||
de l'effet (centre du burst, case du jeteur pour cône/ligne) ajoute à son
|
||
jet de Réflexes la moitié de son bonus de couvert CA (+1 partiel / +2
|
||
standard / +4 amélioré) ; le couvert mou des créatures n'accorde **aucun**
|
||
bonus de sauvegarde (SRD « Soft Cover »). Simplification documentée : la
|
||
sauvegarde primaire de la cible résolue, jetée sur le chemin générique des
|
||
sorts avant la résolution de la zone, ne rejoue pas ce bonus (nul pour un
|
||
burst centré sur la cible, l'origine étant sa propre case). Le snapshot
|
||
d'observation expose `{shape, radius_ft, centered_on_caster}` via la clé `area`.
|
||
Un `DamageEffect` peut déclarer `"scaling": {"dice": "1d6", "per_levels": 1,
|
||
"max_dice": 10}` (dés par niveau — Fireball/Lightning Bolt « 1d6 per caster
|
||
level (maximum 10d6) », Burning Hands « 1d4 per level (maximum 5d4) »,
|
||
Shocking Grasp « 1d6 per level (maximum 5d6) ») et/ou
|
||
`"scaling_flat": {"amount": 1, "per_levels": 1, "max": 5}` (bonus plat par
|
||
niveau — Inflict Light Wounds « 1d8 + 1 per caster level (maximum +5) ») ;
|
||
à l'incantation, `effective_formula` résout dés additionnels =
|
||
`min(max_dice, niveau // per_levels)` et bonus plat =
|
||
`min(max, (niveau // per_levels) × amount)` avec la formule de base — le
|
||
total scalé suit ensuite le critique, la résistance énergétique et la
|
||
métamagie comme n'importe quelle formule (le chargeur refuse un dé de
|
||
scalage de type différent du dé de base). Absence de scalage = comportement
|
||
inchangé. Le scalage s'applique aussi aux soins (S5b) : un `HealEffect`
|
||
porte le même `scaling_flat` (bonus plat par niveau) résolu par
|
||
`effective_formula(niveau)` à l'incantation — Cure Light Wounds
|
||
« 1d8 + 1 per caster level (maximum +5) » et Cure Moderate Wounds
|
||
« 2d8 + 1 per caster level (maximum +10) » (SRD spells/cure-light-wounds.md,
|
||
spells/cure-moderate-wounds.md) sont désormais scalés au lieu d'épinger
|
||
une formule figée.
|
||
- Sorts multi-projectiles (S2b) : un `DamageEffect` peut déclarer
|
||
`"projectiles": {"base": 1, "per_levels": 2, "at_level": 1, "max": 5}` — le
|
||
nombre de projectiles tirés vaut `base + (niveau - at_level) // per_levels`
|
||
(delta de niveau planché à 0), plafonné à `max` (SRD Magic Missile : « one
|
||
missile, plus one additional missile per two caster levels beyond 1st [...]
|
||
maximum of five » → 1 au niveau 1, 2 au niveau 4, 5 au niveau 9 ; Scorching
|
||
Ray : `per_levels` 4, `at_level` 3, `max` 3 → 1/2/3 rayons aux niveaux
|
||
1/7/11). À l'incantation, chaque projectile **jetle ses propres dégâts** et,
|
||
si le sort exige une attaque de contact à distance (`requires_ranged_touch`,
|
||
Scorching Ray), **jetle sa propre attaque de contact à distance** (couvert,
|
||
dissimulation et menace de critique incluss — un rayon qui rate ne inflige
|
||
rien, les autres partent quand même) ; les dégâts de sort ne sont pas réduits
|
||
par la RD, et la résistance énergétique s'applique à chaque projectile. La RS
|
||
est opposée une fois par cible (le moteur ne répartit pas les projectiles sur
|
||
plusieurs cibles — toutes touchent la cible résolue). Si la cible tombe en
|
||
cours de séquence (pv < 0 ou morte), les projectiles restants sont **perdus**
|
||
(même convention que les attaques multiples d'une arme `count` > 1). Magic
|
||
Missile (auto-touché, pas d'attaque) et Scorching Ray sont ainsi SRD-conformes
|
||
sur le nombre de projectiles ; la fiche dorée du liche (niveau d'incanteur 11)
|
||
tire désormais ses cinq missiles.
|
||
- Dégâts récurrents de sort (S2c) : un `DamageEffect` peut déclarer
|
||
`"recurring": {"formula": "2d4", "types": ["acid"], "base_rounds": 1,
|
||
"per_levels": 3, "max": 7}` — le nombre de rounds de récurrence vaut
|
||
`base_rounds + niveau // per_levels`, plafonné à `max` **en total** (SRD Acid Arrow :
|
||
« For every three caster levels you possess, the acid [...] lasts for another
|
||
round [...] dealing another 2d4 points of damage in each round » → 1 round au
|
||
niveau 1, 2 au niveau 3, 7 au niveau 18+ : 1 + 6 rounds additionnels). À l'attaque initiale réussie, une
|
||
source de dégâts récurrents (`RecurringDamage` sur le `CombatantState`, liste
|
||
`recurring_damage`) est posée sur la cible vivante ; au **début du tour** de
|
||
la cible (dans `_turn_gate`, après le tick de saignement), chaque source
|
||
jetle ses dés, est **réduite par la résistance/immunité énergétique** (acide)
|
||
mais **jamais par la RD** (c'est un dégât de sort), inflige ce dégât, puis
|
||
perd un round ; arrivée à 0 round elle est retirée. L'observation expose
|
||
chaque source sous `recurring_damage` (`{formula, types, rounds_left}`).
|
||
Simplification documentée : la neutralisation d'acide (immersion/eau) n'est
|
||
pas modélisée — le dégât court jusqu'à l'expiration de sa durée.
|
||
- Résistance aux sorts (SR) et résistances/immunités énergétiques : un combattant
|
||
avec `spell_resistance > 0` oppose sa SR à chaque sort qui le cible — jet
|
||
`d20 + niveau du jeteur` contre la SR ; si le total est inférieur, le sort est
|
||
résisté (journalisé, tous les effets annulés pour cette cible). Pour les sorts à
|
||
zone, la SR est résolue par créature touchée (pas seulement sur la cible
|
||
primaire). **Drapeau `spell_resistance` par sort (S2e)** : `SpellSpec` porte un
|
||
booléen `spell_resistance` (JSON `"spell_resistance"`, défaut `true`). Quand un
|
||
sort le déclare `false` (les sorts dont la fiche SRD indique « Spell Resistance:
|
||
no » — Acid Arrow, Flurry of Snowballs, Web, Darkness, Sanctuary, les
|
||
`Protection from …`, etc. ; 28 sorts importés vérifiés contre le KB), la
|
||
vérification de SR est **totalement sautée**, tanto sur la cible directe que par
|
||
créature dans une zone. Les 28 sorts SRD « RM : non » portent désormais
|
||
`"spell_resistance": false` ; les 79 autres gardent le défaut `true`. Les dégâts énergétiques (feu, froid, électricité, acide, son…) sont
|
||
annulés par l'immunité (`immunities`) ou réduits par la résistance du type
|
||
correspondant, **composante par composante** (conformance B21, CRB « Energy
|
||
Resistance ») : chaque composante de dégâts (`DamageComponent`) est réduite
|
||
uniquement par la résistance correspondant à SES types (maximum en cas de types
|
||
multiples — simplification documentée : une même formule de dés n'est pas
|
||
répartie entre les types listés), plancher 0 par composante ; une arme
|
||
1d6 tranchant + 1d6 feu contre résistance feu 5 ne perd que ses dés de feu, les
|
||
dés tranchants, les bonus plats et les dés de précision (attaque sournoise, non
|
||
typés) restent intacts ; l'immunité à un type annule uniquement les composantes
|
||
de ce type. Les types physiques (contondant/perforant/tranchant) restent sur le
|
||
chemin de la RD, appliquée **uniquement à la part physique** du dégât (dés
|
||
physiques + bonus plats + précision) — conformance A3 (CRB « Damage
|
||
Reduction » : « Damage reduction does not negate … energy damage dealt along
|
||
with an attack ») : la RD ne mange plus la composante énergétique d'une
|
||
attaque mixée (ex. arme à feu infligeant des dégâts de feu contre RD 15) ;
|
||
les composantes énergétiques ne sont réduites que par résistance/immunité
|
||
énergétique. La résistance énergétique
|
||
s'applique à l'étape du dégât final (même étape que la RD), y compris aux dégâts
|
||
de sorts (un `DamageEffect` est une composante unique — comportement inchangé).
|
||
- Concealment (chance de rater) : les conditions `blurred` (20 %, sort flou),
|
||
`displaced` (50 %, concealment total) et `invisible` (50 %, concealment total)
|
||
portent un champ `miss_chance` ; quand la
|
||
cible en porte une, après la touche et la confirmation de critique, le moteur
|
||
tire un d100 : si `d100 ≤ miss_chance`, l'attaque rate complètement (aucun
|
||
dégât, pas de critique, deed non consommé — journalisé « concealment »). Le
|
||
champ est porté par l'**instance** de condition, pas seulement par la
|
||
définition : Mirror Image (SRD spells/mirror-image.md) jette ses images à
|
||
l'incantation (1d4 + 1 par 3 niveaux, max 8) et impose une instance `blurred`
|
||
dont le `miss_chance` vaut `images/(images+1)` en pourcentage. Une
|
||
cible avec **n'importe quelle dissimulation** (`miss_chance > 0` — flou 20 %
|
||
comme dissimulation totale 50 %) ne peut pas être la victime d'une attaque
|
||
sournoise (conformance A7 — SRD rogue « Sneak Attack » :
|
||
« A rogue cannot sneak attack while striking a creature with concealment »).
|
||
Attaquant aveugle (V5a) : un attaquant portant `blinded` considère tous ses
|
||
adversaires comme ayant un concealment total — le même jet unique de d100 est
|
||
tiré avec `miss_chance = max(concealment de la cible, 50)` et journalisé
|
||
« attacker blinded » ; l'attaque peut donc rater même contre une cible sans
|
||
concealment. Conformance L2 (CRB « Conditions — Blinded » : « All opponents
|
||
are considered to have total concealment (50% miss chance) against the
|
||
blinded character ») : la même convention `max(cible, 50)` s'applique aux
|
||
deux autres portes de concealment de l'attaquant — l'attaque sournoise est
|
||
**impossible** pour un aveugle (dissimulation totale) et ses attaques
|
||
d'opportunité sont **supprimées** contre toute cible (`suppressed (total
|
||
concealment)`, budget d'AoO non consommé).
|
||
- Attaquant invisible (V5a) : un attaquant portant `invisible` gagne **+2 à ses
|
||
jets d'attaque et de confirmation de critique** et sa cible **perd son bonus
|
||
de Dextérité à la CA** (même chemin `denies_dex_to_ac` que flat-footed/course).
|
||
L'invisibilité est symétrique : subi comme cible, la condition accorde le
|
||
concealment total de 50 % ci-dessus.
|
||
- Cible inerme (helpless, V5a + conformance P1) : sont traitées comme
|
||
**inertes** les conditions `helpless`, `paralyzed`, `asleep` et
|
||
`unconscious` (CRB : inerme = paralysé, immobilisé, ligoté, endormi,
|
||
inconscient — `stunned` **ne** rend **pas** inerme). Les attaques de
|
||
**mêlée** contre une cible inerme gagnent **+4 au jet d'attaque et à la
|
||
confirmation** (aucun bonus à distance) ; la cible a une **Dextérité
|
||
effective de 0** : son modificateur de Dextérité est entièrement remplacé
|
||
par la pénalité Dex 0 (−5), en plus de la perte du bonus de Dextérité
|
||
(CA flat-footed via `denies_dex_to_ac`).
|
||
Simplification documentée : l'attaque sournoise automatique du roublard sur
|
||
cible inerme n'est pas modélisée (la cible, privée de Dextérité, la déclenche
|
||
déjà via le chemin `denies_dex_to_ac`).
|
||
- Attaque sournoise : un combattant avec la capacité « Sneak Attack » inflige des
|
||
d6 de dégâts de précision ((niveau + 1) // 2 dés : 1d6 au niveau 1, 2d6 au
|
||
niveau 3…) si la cible est flanquée (attaque de mêlée uniquement — toucher de
|
||
mêlée inclus, conformance P2) ou privée de
|
||
son bonus de Dextérité à la CA (condition `denies_dex_to_ac` ; mêlée, ou
|
||
attaque à distance à ≤ 30 ft). Les dés sont jetés avec le RNG du moteur et
|
||
ajoutés APRÈS le multiplicateur de critique (la précision n'est jamais
|
||
multipliée) et AVANT la RD et les résistances énergétiques ; jamais contre une
|
||
cible qui a une dissimulation, même partielle (`miss_chance > 0` ;
|
||
conformance A7 — SRD rogue : « A rogue cannot sneak attack while striking
|
||
a creature with concealment »).
|
||
Conformance P2 : les morts-vivants (`undead`) et les
|
||
constructeurs (`construct`) sont immunisés aux dégâts de précision — la cible
|
||
ne peut pas être la victime d'une attaque sournoise ; les autres immunités RAW
|
||
(vase, plante) ne sont pas modélisées (pas de tels types dans le modèle).
|
||
- Tirer dans une mêlée / Precise Shot : toute attaque à distance (kind `ranged`
|
||
ou `touch` avec incrément) contre une cible engagée dans une mêlée subit −4 au
|
||
jet d'attaque ET au jet de confirmation (la cible est « engagée » quand un
|
||
ennemi actif autre que l'attaquant, doté d'une arme de mêlée, menace ses
|
||
cases) ; la pénalité est **totalement évitée (0)** si la cible ou l'adversaire
|
||
menaçant est à ≥ 10 ft (2 cases) de l'attaquant (SRD « Attack — Shooting or
|
||
Throwing into a Melee » : « you can avoid the −4 penalty »). Simplification
|
||
documentée : la réduction RAW à −2 quand la cible est de deux catégories de
|
||
taille au-dessus des personnages amis (et 0 à trois ou plus) n'est pas
|
||
modélisée. Le don « Precise Shot » annule la pénalité.
|
||
Les attaques de mêlée ne sont pas concernées.
|
||
- Couvert : les trois niveaux PF1e sont modélisés via `cover_bonus` sur le
|
||
terrain — partiel +2, standard +4 (ou `cover: true`), amélioré +8 ; le
|
||
couvert mou des créatures reste +4 (couvert standard). B20 (conformance) :
|
||
pour un attaquant multi-cases, le bonus effectif est le **meilleur** (plus
|
||
faible) couvert parmi ses cases occupées — l'attaquant choisit son meilleur
|
||
coin/case selon le CRB ; les créatures 1×1 sont inchangées.
|
||
- 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). La durée des sorts `summon monster` est
|
||
`1 round/level` (SRD spells/summon-monster-1.md : « Duration 1 round/level (D) ») :
|
||
un `SummonEffect` déclare `duration_rounds_per_level` (défaut 0 = durée fixe,
|
||
comportement pré-conformance) et la durée est résolue au moment de l'incantation
|
||
comme `max(1, niveau du lanceur × duration_rounds_per_level)`, selon la même
|
||
convention de scalage par niveau que `ConditionEffect` (voir « Formes de zone et
|
||
scalage »). Les deux sorts importés (`summon_monster_i/ii.json`) portent
|
||
`duration_rounds_per_level: 1`.
|
||
- Conditions effectives : 29 conditions prédéfinies (`conditions.py`). Leurs drapeaux
|
||
comportementaux sont appliqués par le moteur : `cant_act` (étourdi, paralysé, dazed,
|
||
endormi, inerme, fasciné) annule le tour ; `cant_attack` (nauséeux, épinglé) n'autorise qu'une unique action
|
||
de mouvement — conformance L9 (SRD « Conditions — Nauseated ») : « The only
|
||
action such a character can take is a single move actions per turn » — seuls
|
||
les vraies actions de mouvement (`move`, `stand_up`, `retrieve_weapon`,
|
||
`repair_weapon`) survivent au filtre ; le pas de placement (`5foot_step`) et
|
||
se jeter à terre (`drop_prone`) sont des actions **libres**, non de
|
||
mouvement, et sont donc retirés ; le nauséeux bloque toute
|
||
attaque, manœuvre **et incantation** (CRB : un nauséeux ne peut ni attaquer,
|
||
ni incanter, ni se concentrer) ; `staggered` restreint à une action
|
||
simple (une action de mouvement OU standard, pas d'attaque à outrance ni
|
||
d'action à round complet, mais swift/libre/immédiate conservées) ;
|
||
`cant_move` bloque le déplacement ;
|
||
`denies_dex_to_ac` (flat-footed, paralysé, aveuglé…) fait perdre le bonus de Dextérité à
|
||
la CA (CA flat-footed) ; `miss_chance` (flou 20 %, déplacement 50 %, invisible
|
||
50 %) impose la
|
||
chance de rater du concealment. À plat ventre : l'attaquant à plat ventre
|
||
subit −4 à ses attaques (mêlée selon le CRB ; distance traitée arbalète à
|
||
−4, simplification documentée) ; la cible à plat ventre subit −4 CA contre
|
||
la mêlée et gagne +4 CA contre la distance (le CRB inverse du « −4 CA
|
||
contre tout » précédemment modélisé).
|
||
Agrippé : −4 Dextérité — le modificateur cible `dex_skills` s'applique
|
||
désormais réellement aux compétences de Dexterity (Acrobaties, Escalade,
|
||
Équilibre, Évasion, Vol, Équitation, Manipulation de corde, Discrétion,
|
||
Natation, Désamorçage — conformance P2) —, −2 attaque/CA/Réflexes, sans
|
||
déplacement ni AoO. Épinglé : même −4 `dex_skills`, et `cant_attack` — un
|
||
épinglé ne peut pas attaquer (SRD « Conditions — Pinned » : il « peut
|
||
seulement tenter de se libérer ») ; lacune documentée : le moteur ne pose
|
||
**jamais** `pinned` (pas de boucle de maintien du grapple), la condition
|
||
n'est atteignable que par injection directe.
|
||
Aveuglé (conformance P2) : −2 CA, perte du bonus de Dextérité à la CA, −4
|
||
aux compétences de Dexterity (`dex_skills`) et **aucun malus plat
|
||
d'attaque** — la pénalité RAW est la dissimulation totale (50 % d'échec,
|
||
modélisée) ; le −2 d'attaque non-RAW qui s'y superposait a été supprimé.
|
||
Simplification documentée : la moitié Force du −4 RAW (compétences de
|
||
Force) n'est pas modélisée (pas de cible `str_skills`).
|
||
Inerme (`helpless`, `paralyzed`, `asleep`, `unconscious`) : ne peut pas agir,
|
||
Dextérité effective 0 (−5 CA en plus de la perte du bonus), +4 en mêlée
|
||
contre lui (voir « Cible inerme ») ; `stunned` ne rend pas inerte. Fasciné (`fascinated`) : ne peut pas agir.
|
||
Invisible (`invisible`) : concealment total subi +2 attaquant / perte de Dex
|
||
de la cible (voir « Attaquant invisible »).
|
||
Saignement (`bleeding`, V5b) : une créature qui saigne subit `bleed_damage`
|
||
points de dégâts au **début de son tour** (avant politique et flags) ; tout
|
||
effet restaurant des points de vie (sort de soin, channel energy) stoppe le
|
||
saignement (`bleed_damage = 0`) et retire la condition ; l'expiration de la
|
||
dernière condition `bleeding` inflige son **dernier tick** puis remet
|
||
`bleed_damage = 0` (conformance D2 — SRD « Bleed » : le cantrip, sauvegarde
|
||
de **Volonté** annule, pose la condition `bleeding` avec `bleed_damage = 1`,
|
||
donc 1 point de dégâts par round). Simplification
|
||
documentée : le jet de compétence Soin DD 15 n'est pas modélisé (le moteur
|
||
n'a pas d'action Soin).
|
||
Confus (`confused`, V5b) : au début de chaque tour, jet d% — 01-25 agit
|
||
normalement (le tour se poursuit) ; 26-50 bave incohéremment (tour perdu) ;
|
||
51-75 se inflige 1d8 + modificateur de Force avec l'objet tenu ; 76-100
|
||
attaque d'une seule attaque de mêlée la créature la plus proche (allié OU
|
||
ennemi, une seule si en allonge ; sinon le tour est perdu, journalisé).
|
||
Simplification documentée : la clause « si elle est blessée, riposte
|
||
l'agresseur » est réduite à « attaque la créature la plus proche » ; le coup
|
||
de grâce n'est pas modélisé. Une créature confuse ne fait **aucune**
|
||
attaque d'opportunité (`cant_aoo`) — simplification de la clause SRD « not
|
||
already devoted to attacking », le moteur n'ayant pas ce concept.
|
||
- Manœuvres de combat : trip, disarm, bull_rush, sunder, grapple, escape,
|
||
overrun, dirty trick, steal, reposition et drag sont modélisées, chacune
|
||
comme une tentative unique (1d20 + CMB contre CMD) : le jet de manœuvre est
|
||
un jet d'attaque — 20 naturel réussit toujours, 1 naturel échoue toujours
|
||
(conformance P2). Conformance P2 : tenter une manœuvre (sauf `escape`)
|
||
**provoque une attaque d'opportunité** de la cible, résolue **avant** le jet
|
||
de CMB ; le don « Improved <Maneuver> » correspondant (Improved Trip, …)
|
||
annule cette provocation ; les dégâts infligés par cette AoO sont appliqués
|
||
comme **pénalité au jet de manœuvre** (conformance A2 — CRB « Combat
|
||
Maneuvers » : « apply that amount as a penalty to the attack roll to perform
|
||
the maneuver », total planché à 0, 20 naturel réussit / 1 naturel échoue
|
||
toujours) ; si l'AoO fait passer l'attaquant sous 0 pv, la manœuvre est
|
||
perdue. L'AoO de manœuvre consomme le budget d'AoO du round de
|
||
la cible (Combat Reflexes inclus). Conformance B16 (CRB « Power Attack ») :
|
||
la pénalité −X de Power Attack actif s'applique **au jet de CMB** de la
|
||
manœuvre (X = BAB//4 + 1) — la pénalité couvre « les jets d'attaque de mêlée
|
||
et les jets de manœuvres de combat » ; l'escape par compétence n'est pas
|
||
pénalisé (un test de compétence n'est pas un jet de manœuvre). Conformance
|
||
L1 (SRD « Combat Maneuvers ») : « Creatures that are size Tiny or smaller
|
||
use their Dexterity modifier in place of their Strength modifier to
|
||
determine their CMB » — Tiny/Diminutive/Fine calculent donc leur CMB avec
|
||
le modificateur de Dextérité à la place de celui de Force. Leur allonge
|
||
naturelle nulle (aucune case menacée, attaque dans leur propre case) et la
|
||
co-occupation Tiny-or-smaller avec une créature strictement plus grande
|
||
sont modélisées (conformance S4c) ; une attaque naturelle dotée d'un
|
||
`reach_ft` explicite (ex. morsure d'ierlieth) conserve cette allonge.
|
||
Conformance L2 (SRD « Combat Maneuver Defense ») : le CMD calculé ajoute
|
||
les bonus de CA des huit types autorisés (« A creature can also add any
|
||
circumstance, deflection, dodge, insight, luck, morale, profane, and
|
||
sacred bonuses to AC to its CMD » — bonus issus des buffs/conditions/
|
||
dons, défense totale et combat défensif inclus), **toutes les pénalités de CA
|
||
(« Any penalties to a creature's AC also apply to its CMD » — pénalités sans
|
||
type comme grapplé −2 ou à plat ventre −4 contre la mêlée, la manœuvre
|
||
étant un jet d'attaque de mêlée)** et une créature au
|
||
contact pris de dos ne ajoute pas son bonus de Dextérité (« A flat-footed
|
||
creature does not add its Dexterity bonus to its CMD » — la pénalité de
|
||
Dextérité, qui n'est pas un bonus, reste appliquée).
|
||
(SRD « Trip » / « Bull Rush » / « Overrun » ; APG « Reposition » / « Drag ») :
|
||
« You can only [affect/reposition/drag] an opponent [who is] no more than
|
||
one size category larger than you » — conformance
|
||
L8 : trip, bull_rush, overrun, reposition et drag contre une cible de
|
||
**deux catégories de taille ou plus au-dessus** de l'attaquant sont illégaux
|
||
(tentative refusée,
|
||
journalisée `rejected (target too large)`, aucun jet, pas d'AoO provoquée) ;
|
||
les autres manœuvres (disarm, sunder, grapple…) n'ont pas de limite de taille.
|
||
trip → `prone` ; disarm → l'arme tenue est **au sol** (SRD « Disarm » :
|
||
« your target drops one item it is carrying » — `weapon_dropped`, exclue
|
||
de `usable_attacks()` jusqu'à un `retrieve_weapon` ; un échec de 10 ou
|
||
plus fait tomber **l'arme de l'attaquant**) ; bull_rush → pousse la
|
||
cible (direction sélectionnable via `Action.direction`) ; conformance L6
|
||
(SRD « Bull Rush ») : la poussée vaut 5 ft plus « For every 5 by which
|
||
your attack exceeds your opponent's CMD you can push the target back an
|
||
additional 5 feet » — `1 + marge//5` cases, stoppées au premier obstacle
|
||
(un 20 naturel sous le CMD ne donne que la poussée de base) ;
|
||
grapple → `grappled` uniquement (la boucle de maintien du grapple et
|
||
l'application de `pinned` sont hors périmètre du moteur — la définition
|
||
de la condition `pinned` est conforme au SRD, mais elle n'est jamais
|
||
posée par le moteur lui-même) ; conformance L8 (SRD « Grapple ») : « If
|
||
you successfully grapple a creature that is not adjacent to you, move
|
||
that creature to an adjacent open space (if no space is available, your
|
||
grapple fails) » — une réussite à distance **tire la cible** sur la case
|
||
libre adjacente la plus proche (distance 5-10-5, départage (ligne,
|
||
colonne) ; journalisé `grapple pulls`), et **échoue** sans poser la
|
||
condition si aucune case adjacente libre n'existe (`FAILED (no adjacent
|
||
space)`) ; escape → **une** sauvegarde (bonus statique de la
|
||
compétence `escape_artist` ou CMB, + un d20) contre CMD — une seule tentative,
|
||
sans boucle multi-tour. Conformance L4 (SRD « Combat Maneuvers ») : le 20
|
||
naturel réussit **toujours** à l'escape d'un grapple (« Rolling a natural
|
||
20 while attempting a combat maneuver is always a success (except when
|
||
attempting to escape from bonds) » — un grapple n'est pas un lien), le 1
|
||
naturel échoue toujours. 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()` et réparable via
|
||
`Action(kind="repair_weapon")`. overrun →
|
||
passage au travers si la case au-delà est libre, et `prone` 1 round
|
||
**seulement si le jet dépasse le CMD de 5 ou plus** (conformance R3 — CRB
|
||
« Overrun » : « If your attack exceeds your opponent's CMD by 5 or more,
|
||
you move through the target's space and the target is knocked prone ») ;
|
||
un succès par 0–4 points ne fait que traverser, sans renverser ; conformance
|
||
L7 (SRD « Overrun ») : une tentative **échouée** arrête l'attaquant « in the
|
||
space directly in front of the opponent, or the nearest open space in front
|
||
of the creature » — l'attaquant non adjacent est déplacé sur la case libre
|
||
la plus proche de la cible sur la ligne d'approche (adjacent : immobile) ;
|
||
conformance L7 (SRD « Trip ») : un trip qui « fails by 10 or more » renverse
|
||
l'**attaquant** (`prone` 1 round, `total <= CMD - 10`) ; dirty trick →
|
||
condition choisie par `Action.trick_condition` (défaut `dazzled` ; durée
|
||
= 1 round + 1 round par 5 points dont l'attaque dépasse le CMD — APG
|
||
« Dirty Trick » ;
|
||
`blinded`/`dazzled`/`deafened`/`entangled`/`shaken`/`sickened` possibles
|
||
(liste exacte de l'APG), `nauseated` exclu car hors de
|
||
la liste RAW, et rejeté à l'exécution avec journalisation) ; steal → le modèle ne porte
|
||
aucun objet transporté non tenu : en application de l'APG « Steal » (« Items
|
||
held in the hands (such as wielded weapons or wands) also cannot be taken
|
||
with the steal maneuver—you must use the disarm combat maneuver instead »),
|
||
une réussite ne vole rien (journalisé `nothing stealable — wielded weapons
|
||
require disarm`) ; reposition → déplacement de la cible de 5 ft plus
|
||
« For every 5 by which your attack exceeds your opponent's CMD, you can
|
||
move the target an additional 5 feet » (APG « Reposition ») — `1 +
|
||
marge//5` cases le long de la direction choisie (premier voisin libre par
|
||
défaut, direction sélectionnable via `Action.direction`), stoppées au
|
||
premier obstacle ; drag → traction de la cible de 5 ft plus 5 ft
|
||
supplémentaires par 5 points d'excès sur le CMD (APG « Drag »), l'attaquant
|
||
occupant sa case d'origine, stoppée au premier obstacle. `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) ; `evil_eye` (APG, conformance D3) inflige −2 à
|
||
**une seule option** — l'option choisie est modélisée par les jets d'attaque —
|
||
pendant **3 rounds** ; une sauvegarde de Volonté réussie ne l'annule pas mais
|
||
**réduit la durée à 1 round** (« A Will save reduces this to just 1 round ») ;
|
||
`slumber` (APG, conformance D4) endort la cible pour un **nombre de rounds
|
||
égal au niveau de la sorcière** (Volonté annule ; simplification documentée :
|
||
la fin RAW du sortilège quand la cible subit des dégâts n'est pas modélisée,
|
||
le moteur n'a pas de crochet de réveil) ;
|
||
`cackle` (APG, conformance D5) est une **action libre sans coût de
|
||
ressource** qui prolonge d'un round tous les sortilèges evil-eye (et
|
||
fortune/misfortune — non implémentés, seule extension modélisée : les
|
||
buffs evil_eye) portés par **toutes les créatures à 30 ft** de la
|
||
sorcière, sur une liste instantanée pour ne pas s'étendre à l'infini.
|
||
Simplification documentée : le slot « hex » reste standard pour
|
||
evil_eye/slumber ; les autres hexes à prolonger ne sont pas implémentés.
|
||
Les sorts à
|
||
zone (AoE) utilisent le DD d'incantation du jeteur et chaque créature de la
|
||
zone effectue la sauvegarde listée par le sort (Volonté / Réflexes /
|
||
Constitution — le sort primaire réutilise son jet déjà effectué) ; un succès
|
||
annule ou divise par deux selon ce que l'effet déclare (`save_negates` /
|
||
`half_on_save` / `AreaEffect.save_for_half`) — les zones « sauvegarde
|
||
annule » (Sleep, Color Spray, Web) sauvent donc aussi. Une composante de
|
||
dégâts peut déclarer `save_negates: false` (conformance D1 — SRD
|
||
« Sound Burst » : « Fortitude partial » n'annule que l'étourdissement,
|
||
jamais les dés) : la sauvegarde réussie ne réduit alors pas ces dégâts.
|
||
**Sauvegardes partielles (S2d)** : un `ConditionEffect` peut déclarer
|
||
`"on_success": {"condition_name": "shaken", "duration_rounds": 1}` (champ
|
||
`save_partial`) — sur une sauvegarde **réussie**, au lieu d'annuler
|
||
totalement l'effet, la cible subit la condition affaiblie indiquée (SRD
|
||
Fear : paniqué à l'échec, `shaken` 1 round au succès). Sans `on_success`,
|
||
le comportement reste « sauvegarde annule ». L'Extend Spell double aussi la
|
||
durée de la condition partielle.
|
||
- 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, énergie positive :
|
||
si un allié est < 50 % de ses PV → soigne tous les êtres vivants à 30 ft ; sinon, si au
|
||
moins 2 morts-vivants ennemis sont groupés à 30 ft → les blesse en 2d6 ; le camp de la
|
||
cible n'est pas pris en compte — seul `creature_type` compte ; **Selective
|
||
Channeling** modélisé (SRD `feats/selective-channeling.md` : « you can
|
||
choose a number of targets in the area up to your Charisma modifier. These
|
||
targets are not affected by your channeled energy ») via
|
||
`Action(kind="channel_energy", channel_exclude=(ids…))` — honoré
|
||
uniquement avec le don, plafonné au modificateur de Charisme (troncature
|
||
déterministe de la liste excédentaire), sinon ignoré et journalisé
|
||
(`selective channeling ignored (no feat)` — SRD Normal : « All targets in a
|
||
30-foot burst are affected ») ; `default_policy` n'exclut jamais (opt-in
|
||
via politique personnalisée, comme `weapon_id`/`destination`/métamagie) ;
|
||
effet observable dans les données actuelles : le mode soin soigne aussi les
|
||
**ennemis vivants** dans le burst (positive energy heals every living
|
||
creature), le don permet de leur refuser le soin, et le mode blessure peut
|
||
épargner un allié mort-vivant ; les créatures blessées obtiennent un jet de Volonté DD `10 +
|
||
demi-niveau du canaliseur + modificateur de Charisme` pour demi-dégâts
|
||
— CRB Channel Energy ; le mode soin n'accorde pas de sauvegarde), sortilège `evil_eye` (nairda), deed `up_close_and_deadly`
|
||
(tammara, action bonus +1d6 à ≤ 30 ft — consommé sur la première attaque
|
||
résolue : si elle est une attaque **d'arme à feu** à distance ET à ≤ 30 ft
|
||
(Ultimate Combat « Deeds — Up Close and Deadly » : « a successful attack
|
||
with a firearm against a target within 30 feet »), +1d6 s'ajoute aux dégâts ;
|
||
sinon le bonus est perdu ; le drapeau est réinitialisé dans tous les cas),
|
||
deed `quick_clear` (S5a — conformance implémentée, Ultimate Combat « Deeds —
|
||
Quick Clear » : « As a standard action, you can spend 1 point from your grit
|
||
pool to remove the broken condition from a single firearm you are holding,
|
||
as long as that condition was gained by a firearm misfire ») — le deed
|
||
dépense 1 grit et répare la **première** arme à feu brisée par un raté
|
||
(ordre des attaques, déterministe) ; sans grit, l'action est un no-op
|
||
journalisé ; sans arme à feu brisée, aucun grit n'est dépensé. Le deed
|
||
occupe le compartiment **standard** du budget (exception par action résolue
|
||
par `_effective_slot`, comme cackle pour les hex ; le kind `deed` reste
|
||
swift pour up_close_and_deadly, et le filtre swift-consommé-par-immédiate
|
||
ne jette plus le quick_clear). Simplifications documentées : la variante
|
||
RAW « as part of a move action to clear all firearms » n'est pas modélisée
|
||
(un seul firearm par deed), et Quick Clear ne répare que le broken issu
|
||
d'un misfire — le broken d'un sunder se répare via `repair_weapon`.
|
||
Le misfire lui-même (Ultimate Combat « Firearm Rules — Misfires » : « When
|
||
a firearm misfires, it gains the broken condition ») : une attaque d'arme à
|
||
feu (`firearm: true`) sur un **1 naturel** casse l'arme (`weapon_broken`,
|
||
journalisé « MISFIRE (broken) ») — valeur de misfire minimale 1 ; la
|
||
progression de la valeur de misfire et l'explosion des early firearms
|
||
re-cassées ne sont pas modélisées (une arme brisée ne peut plus tirer ici)),
|
||
`inspire_courage` + `spell_combat` (misty — Spell Combat : action de round
|
||
complet, incante un sort standard et porte toutes ses attaques de mêlée à
|
||
**−2** (CRB Magus, conformance — la pénalité s'applique à chaque jet
|
||
d'attaque et de confirmation ; simplification : le coût de pool arcanique
|
||
n'est pas modélisé) ; Inspire Courage (CRB Bardic Performance) : **+1
|
||
compétence** (competence) aux jets d'attaque et de dégâts — type qui se
|
||
cumule donc avec les sources de moral (Heroism) — et **+1 moral** aux jets
|
||
de sauvegarde contre les effets de charme et de peur, portée par deux
|
||
modificateurs conditionnés (`fear` / `mind_affecting`, dédupliqués par la
|
||
règle d'empilement du moral) ; simplifications documentées : la
|
||
progression (+1 par quatre niveaux) n'est pas modélisée (plat +1) et le
|
||
moteur n'ayant pas de descripteur « charme », `mind_affecting` en tient
|
||
lieu (surcouverture : les effets de compulsion comptent aussi))
|
||
— couvertes par `tests/test_class_abilities.py`.
|
||
- Le Liche (`data/monsters/lich.json`) : `creature_type: "undead"` (sensible à l'énergie
|
||
positive canalisée), 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/contondant **et** magie (conformance P2 :
|
||
les deux composants sont requis — une arme contondante non magique ne
|
||
contourne plus la RD ; lacune documentée : le modèle d'arme n'expose pas de
|
||
propriété « magique », donc aucun équipement ne contourne cette RD) ; son JSON porte un dictionnaire
|
||
`spell_slots` sans clés dupliquées.
|
||
- Snapshot d'observation (contrat LLM, Phase 2) : avant chaque appel de
|
||
politique, le moteur construit une `Observation` sérialisable (`to_dict()` →
|
||
JSON) isolée du combat — le snapshot est la SEULE source d'info de la LLM
|
||
(aucune référence `engine.*` exposée, collections copiées, jamais relues en
|
||
retour : la politique peut les muter sans affecter le combat ; l'objet est un
|
||
dataclass figé au premier niveau, les dictionnaires internes sont des copies
|
||
mutables, non deep-freezés). Une politique LLM
|
||
se branche via `CombatEngine(..., llm_policy=...)` (`LLMPolicy(observation,
|
||
state) -> tuple[Action, ...]`). Schéma enrichi (E1-E5) :
|
||
- Par combattant : id, side, pos, occupied_cells, hp/hp_max, active/dead,
|
||
dying/stabilized, conditions (noms + rounds_left), resources_left (+ `spell_slot_{level}`),
|
||
weapon_broken/weapon_stolen/weapon_dropped, grappled_with, moved_this_turn, power_attack,
|
||
name, level, size, natural_reach_ft (S4c : allonge naturelle du gabarit
|
||
en pieds — 0 pour Tiny/Diminutive/Fine, 5 pour Small/Medium, 10 pour
|
||
Large… ; l'allonge effective d'une attaque reste `max(allonge naturelle,
|
||
reach_ft de l'arme)`), aware (SRD « Surprise » : agit au round de surprise),
|
||
dr {amount, bypass} ou None (C1), creature_type (C3),
|
||
bab (E1), initiative_mod (E2) et initiative_total (U2 : total du jet
|
||
d'initiative courant = d20 + mod, 0 si pas encore de clé — invocation
|
||
fraîche), casting_ability ou None pour non-jeteur
|
||
(E3), abilities {str/dex/con/int/wis/cha: score brut} (Medium), skills
|
||
{nom: {rank, class_skill, armor_penalty}} uniquement rank > 0 (Medium),
|
||
metamagic_feats (tuple trié), weapon_damage {weapon_id: dégâts courants},
|
||
summon_duration (int ou None), immediate_used, deed_up_close_and_deadly
|
||
(flags actuels), active_buffs {source, rounds_left, mods: tuple de
|
||
{target, value, bonus_type}} (E5), speed_land_ft, speed_fly_ft,
|
||
speed_effective_ft (V4 : budget de déplacement effectif = max(land, fly),
|
||
en pieds), ac {total,
|
||
touch, flat_footed}, saves {fort, ref, will}, cmb, cmd, attacks
|
||
(référentiel complet : id, name, kind, attack_bonus, damage, damage_bonus,
|
||
crit_range, crit_mult, reach_ft, range_increment_ft, count, firearm —
|
||
y compris les armes broken/stolen), features (noms des capacités),
|
||
feature_details (C4 : tuple de {name, category, source_detail, effects:
|
||
tuple de {target, value, bonus_type, condition, weapon_filter}}),
|
||
spells (résumés enrichis du registre : name, level, school, range_ft,
|
||
save, dc, summary, half_on_save, save_negates (E4), harmless (sort «
|
||
(harmless) » du SRD — sauvegarde sautée sur cible consentante), area {shape,
|
||
radius_ft, centered_on_caster} | None (U7a : premier AreaEffect de niveau supérieur du sort,
|
||
gabarit de zone structuré, None si le sort n'a pas de zone), cast_time
|
||
(code de temps d'incantation, « standard » pour un sort inconnu du
|
||
registre)),
|
||
spell_resistance (int), resistances {type: montant}, immunities
|
||
(tuple triée des types immunisés), sneak_dice (dés de précision disponibles :
|
||
(niveau + 1) // 2 avec « Sneak Attack », sinon 0), engaged_in_melee (bool —
|
||
une cible engagée dans une mêlée, signal pour le tir dans la mêlée et
|
||
l'attaque sournoise), dying (bool — `hp < 0` et pas morte : agonisante,
|
||
inconsciente), stabilized (bool — stabilisée pendant son agonie, ne perd
|
||
plus de pv tant qu'aucune nouvelle blessure ne la remet en agonie),
|
||
last_turn_results (tuple de {kind, ok, reason} — résultat d'exécution du
|
||
dernier tour, pour que la policy apprenne les actions rejetées/dégradées),
|
||
readied ({kind, trigger} de l'action préparée ou None — préparation en
|
||
cours, consommée quand le déclencheur la fait tirer), pending_cast
|
||
({spell_name, target_id, slot_level} de l'incantation d'1 round en cours
|
||
ou None — résolue au début du prochain tour du lanceur, voir « Incantation
|
||
d'1 round »),
|
||
defensive_ac_bonus (int — bonus d'esquive à la CA de la défense totale
|
||
(+4) ou du combat défensif (+2), réinitialisé au début du tour du
|
||
combattant), fight_defensively (bool — combat défensif actif : −4 aux
|
||
jets d'attaque et de confirmation jusqu'au prochain tour), aoo_remaining
|
||
(int — attaques d'opportunité restantes ce round : `_aoo_allowance`
|
||
(1 par défaut, 1 + mod de Dex avec Combat Reflexes, plancher 1) moins les
|
||
AoO déjà consommées, réinitialisé à chaque nouveau round, jamais négatif),
|
||
aid_attack_bonus (int — bonus d'aide autrui (+2 par aide réussie, cumulatif)
|
||
ajouté au prochain jet d'attaque du combattant puis consommé, expiré au
|
||
début de son tour s'il est inutilisé), aid_ac_bonus (int — bonus d'aide
|
||
autrui (+2 par aide réussie, cumulatif) ajouté à sa CA, expiré au début de
|
||
son tour), running (bool — course en cours jusqu'au prochain tour du
|
||
combattant : il perd son bonus de Dextérité à la CA, sauf don Run),
|
||
run_rounds (int — rounds de course consécutifs déjà courus, série brisée
|
||
quand un tour passe sans courir) et run_rest_rounds (int — rounds de repos
|
||
restants après un échec du jet d'endurance, course interdite tant que > 0)
|
||
— voir « Course », bleed_damage (int —
|
||
dégâts de saignement subis au début du tour du combattant, 0 = ne saigne
|
||
pas ; V5b ; `bleeding`/`confused` sont visibles via `conditions`),
|
||
recurring_damage (S2c — liste des sources de dégâts récurrents de sort
|
||
actives, chacune `{formula, types, rounds_left}` ; tick au début du tour,
|
||
réduit par résistance énergétique, jamais par la RD).
|
||
- Par paire combattant actif × cible : target_index, distance_ft, weapon_for,
|
||
range_penalty, can_charge, can_step5_to_attack, has_line_of_effect,
|
||
flanking, movement_cost_ft, target_threatens_me, enemies_threatening_me,
|
||
cover_bonus (int — bonus de couvert que la cible gagne contre une attaque
|
||
de l'acteur, calculé comme dans `resolve_attack` : terrain +2/+4/+8 via la
|
||
règle des coins, couvert mou +4 des créatures interposées pour les seules
|
||
attaques à distance ; 0 si `weapon_for` est None), weapon_reason (str |
|
||
None — None si `weapon_for` a choisi une arme ; sinon la raison normalisée
|
||
de l'indisponibilité d'attaque, dans le vocabulaire `ACTION_RESULT_REASONS` :
|
||
`no_weapon` (aucune arme utilisable), `no_line_of_effect` (ligne d'effet
|
||
bloquée), `out_of_range` (arme existante mais cible hors de portée)),
|
||
charge_reason (str | None — None si `can_charge` est vrai ; sinon la
|
||
raison de `ChargeResult` verbatim (`no_line_of_effect` / `no_charge_path`)
|
||
ou `no_weapon` si aucune arme de mêlée utilisable).
|
||
- Top-level : round, acting_combatant, combatants, pairs, terrain, elevation,
|
||
threatened (grille bool des cases menacées au contact par les ennemis —
|
||
AoO), ranged_threatened (U6d : grille bool des cases sous tir ennemi à
|
||
distance — champ de tir des armes à distance utilisables des ennemis
|
||
actifs (portée maximale + ligne d'effet, bornée à la carte) ; les armes à
|
||
distance ne menacent pas pour les AoO en PF1e : c'est une carte de champ
|
||
de tir, pas une menace d'AoO ; la grille `threatened` de mêlée reste
|
||
inchangée),
|
||
initiative_order (U2 : tuple d'ids dans l'ordre d'initiative du round
|
||
courant — le premier est le prochain à agir ; les invocations héritent de la
|
||
case de leur conjurateur),
|
||
terrain_semantics (un dict par type de terrain : type, move_cost,
|
||
blocks_los, cover, cover_bonus), condition_definitions (C2 : tuple trié de (nom,
|
||
{mods, flags, speed_mult, miss_chance}) — définitions complètes des 29
|
||
conditions de CONDITIONS_BY_NAME pour que la LLM interprète les drapeaux,
|
||
les modificateurs et les chances de rater (concealment)).
|
||
Les helpers publics du moteur (L5) sont désormais embarqués EN DONNÉES dans
|
||
le snapshot (`threatened`, `terrain_semantics`, `movement_cost_ft`,
|
||
`target_threatens_me`, `enemies_threatening_me`) — une `LLMPolicy` ne peut
|
||
pas appeler `engine.*`, elle lit leurs résultats. Mapping des clés
|
||
`resources_left` (vérifié par grep sur `spend_resource`/`resources_left[` :
|
||
`channel_energy`, `hexes`, `grit`, `arcane_pool`, `bardic_rounds`, plus
|
||
`spell_slot_{level}` ajouté par le snapshot). Limites documentées : pas de
|
||
grille `reachable_ft` budget=speed (délibérément rejetée —
|
||
`movement_cost_ft` par paire + `terrain_semantics` + `speed_effective_ft`
|
||
suffisent). Le couvert par paire est exposé via la clé `cover_bonus`
|
||
(calcul identique à `resolve_attack`, couvert mou des créatures inclus).
|
||
Les API de décision exposées (items L1-L8)
|
||
s'y retrouvent : `direction`/`trick_condition`/`weapon_id` sur `Action`,
|
||
ainsi que `metamagic` (dons de métamagie choisis pour cette incantation,
|
||
B7) et `cast_defensively` (incantation défensive, B24) sur l'action
|
||
`cast_spell`, et `channel_exclude` (ids exclus par Selective Channeling,
|
||
plafonné au mod de Charisme, don requis) sur l'action `channel_energy` ;
|
||
`can_charge` → `ChargeResult`, kinds `retrieve_weapon`/`repair_weapon`.
|
||
Le kind `activate_power_attack` (action libre) active le don Power Attack
|
||
(`state.power_attack = True`) si le combattant possède le don ; sans le don,
|
||
c'est un no-op journalisé. Le snapshot expose l'état courant via la clé
|
||
`power_attack` (bool) par combattant.
|
||
|
||
Non modélisé : la couche `markers` (points nommés de la carte) est validée à
|
||
la lecture mais non appliquée dans la résolution.
|
||
|
||
Limites hors périmètre (arrêtées lors de la conformance L1-L10, documentées
|
||
plutôt que bricolées) :
|
||
|
||
- **Incantation multi-round vraie (`long`) hors périmètre** : le round de
|
||
surprise et l'incantation d'1 round sont modélisés (voir « Round de
|
||
surprise » et « Incantation d'1 round ») ; restent hors périmètre les temps
|
||
d'incantation > 1 round (3 rounds, 10 minutes, 1 heure — SRD « Casting
|
||
Time » : « These actions must be consecutive and uninterrupted, or the
|
||
spell automatically fails ») : non planifiables dans un tour, ils sont
|
||
jetés du budget (`downgraded:cast_time`) plutôt qu'approximés, faute de
|
||
données d'objectif pour ces sorts rituels.
|
||
- **Co-occupation au-delà de Tiny vs plus grand** : la règle Tiny-or-smaller
|
||
est modélisée (S4c) — allonge naturelle 0, aucune case menacée, attaque
|
||
possible dans sa propre case, et partage de case avec une créature
|
||
strictement plus grande (déplacement à travers et fin de mouvement dessus) ;
|
||
en revanche deux créatures de même taille (Small et plus) ne partagent
|
||
jamais une case, et la co-occupation de plusieurs Tiny sur une même case
|
||
n'est pas modélisée.
|
||
- **Squeeze (se serrer dans un passage étroit) hors périmètre** (SRD
|
||
« Moving Around or Through an Open Square » / « Squeezing Through » : une
|
||
créature Large+ peut « squeeze » à travers un espace assez large pour une
|
||
créature d'une catégorie de taille en dessous, à coût de mouvement accru et
|
||
pénalités) : la largeur de corridor n'est pas modélisée par rapport à la
|
||
taille — une créature Huge (3×3) est simplement bloquée par tout endroit où
|
||
son footprint ne rentre pas ; ni le squeeze payant, ni la réduction de
|
||
footprint dans les couloirs ne sont modélisés.
|
||
- **Passage à travers une créature (3 catégories d'écart) — approximations** :
|
||
la règle est modélisée (traversée + AoO d'entrée, voir « Mouvement ») ;
|
||
restent hors périmètre le test d'Acrobatics « Tumbling » (« A trained
|
||
character can attempt to use Acrobatics to move through a square occupied
|
||
by an opponent » — la compétence n'existe pas dans le moteur) et la clause
|
||
des très grosses créatures inertes (« Some creatures, particularly very
|
||
large ones, may present an obstacle even when helpless. In such cases, each
|
||
square you move through counts as 2 squares » — les cases d'une créature
|
||
inactive ne bloquent pas et ne coûtent pas double).
|
||
- **Échec d'incantation pour un personnage sourd hors périmètre** (SRD
|
||
« Conditions — Deafened » : « a 20% chance of spell failure when casting
|
||
spells with verbal components ») : le moteur ne porte pas la composante
|
||
verbale des sorts (`SpellSpec` n'a pas de champ `components` — il
|
||
faudrait l'encoder sur les 108 sorts du registre) et ne modélise pas non
|
||
plus le jet de Perception DD 20 basé sur le son ni l'échec automatique
|
||
associé ; la condition `deafened` reste donc un porteur de modificateurs
|
||
(−4 d'initiative) sans la faillibilité d'incantation. Le soin des
|
||
morts-vivants par les sorts de soin (`cure`) est en revanche modélisé :
|
||
SRD « Cure Light Wounds » — « this spell deals damage to them instead of
|
||
curing their wounds [...] can attempt a Will save to take half damage ».
|
||
- **Échec d'incantation arcanique due à l'armure (ASF) hors périmètre** (SRD
|
||
« Arcane Spells and Armor » : « Armor restricts the complicated gestures
|
||
required while casting any spell that has a somatic component. The armor
|
||
and shield descriptions list the arcane spell failure chance for different
|
||
armors and shields ») : le moteur ne modélise pas l'équipement — le
|
||
`Combatant` n'a aucun champ armure/bouclier (la CA des JSON est une valeur
|
||
finale, voir « Taille et attaques/CA ») et `SpellSpec` n'a aucun champ
|
||
`components` (même constat que la limite « personnage sourd » ci-dessus) ;
|
||
la probabilité d'échec d'incantation n'a donc ni source (montant d'ASF de
|
||
l'armure) ni filtre (sorts à composante somatique) et n'est pas appliquée.
|
||
- **Congé des sorts à durée « (D) » hors périmètre** (SRD « Cast a Spell —
|
||
Dismiss a Spell » : « Dismissing an active spell is a standard action
|
||
that doesn't provoke attacks of opportunity » ; marque « (D) » de la
|
||
durée, ex. SRD spells/summon-monster-1.md « Duration 1 round/level (D) ») :
|
||
le moteur n'a ni action `dismiss` ni registre des sorts actifs par source
|
||
— les effets à durée (`active_buffs`, `active_conditions`, invocations via
|
||
`summon_duration`) expirent uniquement par leur décompte de rounds en fin
|
||
de tour ; le congé volontaire (rappeler une invocation, couper un sort
|
||
soutenu) n'est pas modélisable sans un modèle de suivi des sorts actifs,
|
||
hors périmètre de la Phase 1.
|
||
|
||
Limites de sorts (conformance D6-D9, documentées plutôt que bricolées) :
|
||
|
||
- **Fear — durée figée** (SRD spells/fear.md : « each living creature in the
|
||
area [...] becomes panicked unless it succeeds on a Will save [...]. If the
|
||
Will save succeeds, the creature is shaken for 1 round » ; cône de 30 ft —
|
||
modélisé, S1a/S1c) : la sauvegarde partielle est désormais modélisée (panic
|
||
à l'échec, `shaken` 1 round au succès — voir « Sauvegardes partielles » plus
|
||
bas) ; seule la durée « 1 round/niveau » du `panicked` reste fixée à 4 rounds.
|
||
- **Bladed Dash — approximation** (SRD spells/bladed-dash.md : « When you cast
|
||
this spell, you immediately move up to 30 feet in a straight line any
|
||
direction [...] This movement does not provoke attacks of opportunity. You
|
||
may make a single melee attack at your highest base attack bonus against any
|
||
creature you are adjacent to at any point along this 30 feet ») : le sort est
|
||
modélisé comme un buff de vitesse +30 ft pour 1 round ; l'attaque de mêlée
|
||
gratuite, le déplacement sans AoO, la contrainte de ligne droite et le bonus
|
||
de circonstance à l'attaque (mod d'Int ou de Cha, le plus élevé) ne sont pas
|
||
modélisés.
|
||
|
||
## Architecture
|
||
|
||
- `dice.py` — parseur de notation de dés (`2d4+1`) et lancer.
|
||
- `rng.py` — `SeededRng` (flux reproductible par graine) et `ScriptedRng`
|
||
(séquence de dés scriptée pour les tests de transcripts).
|
||
- `models.py` — schémas Pydantic v2 : `Combatant`, `AbilityScores`,
|
||
`ACProfile`, `AttackSpec` (avec `firearm` pour les armes touch à feu),
|
||
`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` (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 par niveau (+2/+4/+8 terrain via
|
||
`cover_bonus`, +4 couvert mou des créatures) dans `resolve_attack`.
|
||
- `areas.py` — géométrie des gabarits de zone cône et ligne (CRB « Magic —
|
||
Aiming a Spell ») : `cone_cells` (quart de cercle de 90°, test d'angle ±45°
|
||
en arithmétique entière exacte, case d'origine exclue, cases sans ligne
|
||
d'effet depuis l'origine exclues via `los.has_line_of_effect`) et
|
||
`line_cells` (raie de Bresenham de 5 ft de large, longue de `radius_ft`,
|
||
partant de la case du jeteur, tronquée à la première case `blocks_los`).
|
||
Les bursts restent gérés par `_area_creatures` (filtrés par ligne d'effet
|
||
depuis le centre).
|
||
- `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), 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 (avec sélection directionnelle et choix de condition d'état),
|
||
capacités de classe, transcripts, politique par défaut, snapshot
|
||
d'observation sérialisable `Observation`/`LLMPolicy` (frontière LLM). 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,
|
||
sans type) s'additionnent ; types non-cumulables (morale, sacré, profane,
|
||
enhancement…) gardent la valeur la plus élevée. Les pénalités suivent les
|
||
mêmes règles. Fondation pour sorts, conditions, dons et capacités de classe.
|
||
- `conditions.py` — conditions PF1e : `Condition` (dataclass avec
|
||
modificateurs + flags comportementaux + `miss_chance`) et 29 conditions prédéfinies
|
||
(shaken, sickened, fatigued, exhausted, entangled, dazzled, blinded,
|
||
deafened, flat-footed, prone, stunned, paralyzed, nauseated, dazed,
|
||
staggered, frightened, panicked, cowering, helpless, fascinated, invisible,
|
||
bleeding, confused, bane).
|
||
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 ; 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) 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 — la pénalité −X vise toutes les attaques de mêlée
|
||
(contact de mêlée inclus, conformance A6), le bonus de dégâts uniquement les
|
||
armes de mêlée —, activé comme action libre via le
|
||
kind `activate_power_attack` qui pose `state.power_attack`). Conformance
|
||
B16 (CRB « Power Attack ») : le bonus de dégâts passe à **+3X (×1,5)** pour
|
||
une arme marquée `two_handed: true` dans le JSON de l'attaque (arme à deux
|
||
mains, arme à une main tenue à deux mains, ou attaque naturelle primaire à
|
||
×1,5 de Force) ; la pénalité −X s'applique aussi **aux jets de manœuvre de
|
||
combat** (CMB, hors escape par compétence) ; la division par deux (×0,5,
|
||
+1X) pour une arme tenue à la main secondaire ou une attaque naturelle
|
||
secondaire n'est **pas modélisée** (le moteur ne gère pas le combat à deux
|
||
armes). Les modificateurs sont collectés par
|
||
`_stat_modifiers` avec filtrage par arme (`weapon_filter`). Le champ
|
||
`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, exigence de contact à distance `requires_ranged_touch`
|
||
(rayons), marque `harmless` (SRD « (harmless) » — sauvegarde sautée sur
|
||
cible consentante), temps d'incantation `cast_time`) et types d'effets
|
||
(`DamageEffect`, `ConditionEffect`,
|
||
`HealEffect`, `BuffEffect`, `AreaEffect` — burst/cône/ligne, S1a). Un
|
||
`DamageEffect` (et un `HealEffect`, S5b) porte des specs de scalage
|
||
optionnels (`scaling` en dés, `scaling_flat` en bonus plat) résolues par
|
||
`effective_formula(niveau)` au moment de l'incantation (S1b). Portées PF1e
|
||
(personal, touch, close, medium,
|
||
long) calculées par `spell_range_ft`. Chargeur JSON (`load_spell`,
|
||
`load_spell_registry`) pour `data/spells/`, acceptant les codes de budget
|
||
`cast_time` et leurs alias RAW (« 1 round », « 1 minute »…). 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 (aucun dégât sur sauvegarde
|
||
réussie pour un effet ciblé — la sauvegarde annule ; demi-dégâts seulement si
|
||
l'effet déclare `half_on_save` ; 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 +
|
||
désambiguïsation des ids), `run_encounter` (un `SeededRng` par run) et
|
||
agrégation en `EncounterReport` avec attrition par combattant.
|
||
- `cli.py` — front-end argparse `pf1e-sim`, rapport français, code de sortie 2
|
||
en cas d'erreur.
|
||
- `loaders/` — `combatant.py` (chargeur unifié PJ + monstres avec résolution
|
||
des dons → `AbilitySpec` et des sorts → `Combatant.spells`) et
|
||
`foundry.py` (outil de migration one-shot depuis les fiches Foundry —
|
||
incomplet, voir l'encadré « Fiches de personnages (PJ) » : ne pas régénérer
|
||
`data/pcs/*.json`, qui est canonique, sans réappliquer les corrections SRD).
|
||
|
||
## Développement
|
||
|
||
La gate de validation complète (tests + lint + types) :
|
||
|
||
```bash
|
||
uv run pytest -q # 1647 tests
|
||
uv run ruff check src tests
|
||
uv run basedpyright src # mode strict
|
||
```
|
||
|
||
- Ruff est configuré avec `select = ["ALL"]`, longueur de ligne 100.
|
||
- Basedpyright tourne en `typeCheckingMode = "strict"` sur `src` et `tests`.
|
||
- Le moteur est testé par des transcripts épinglés (combats 1v1 et 2v2
|
||
scriptés) et par des assertions statistiques en forme fermée (matchup
|
||
symétrique dans la bande 3σ).
|
||
|
||
## Feuille de route
|
||
|
||
- **Phase 1** — magie et états : largement réalisée. Sorts par emplacements
|
||
(`spells.py` + base `data/spells/`, 108 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 ». 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.
|
||
- **Phase 2** — couche tactique LLM : stratégies en langage naturel traduites
|
||
en politiques, balayage de matrices de positionnement. Le contrat
|
||
d'observation (snapshot sérialisable `Observation` + wrapper `LLMPolicy`)
|
||
est en place ; la couche LLM elle-même reste à implémenter. Les capacités
|
||
désormais débloquées du moteur (ready, delay, défense totale, destination
|
||
de déplacement, choix d'arme, boucle de feedback) sont démontrées par une
|
||
politique lisant uniquement l'`Observation` dans
|
||
`tests/test_llm_engine_capabilities.py` (démo de capacités, pas une
|
||
implémentation de la couche LLM).
|
||
- **Phase 3** — rapporteur LLM local : agrégation des statistiques et
|
||
rédaction du rapport d'équilibrage en français. (Non implémentée.)
|