fix(rules): distinguish natural reach from reach weapons

PF1e CRB 'Big and Little Creatures in Combat': a creature's natural
reach is a disk (a Large creature still threatens and strikes adjacent
squares), while a reach weapon doubles the wielder's natural reach but
'can't strike at their natural reach or less' (a ring whose dead zone is
the natural reach). The engine keyed the ring off reach_ft > 5, so
reach-10 natural attacks (giant belostomatid bite/claw, mounted
hooves) wrongly got a 5-ft dead zone.

AttackSpec gains reach_weapon (parsed from the combatant JSON); the
disk-vs-ring rule is centralized in _melee_reach_zone_cells and applied
consistently to weapon selection, melee usability, threatened squares
and charge end placement. Esha's Lance is the only reach weapon in the
shipped data; Large natural attackers keep the default (disk).

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-09-11 22:06:46 +02:00
parent 4f6b076a49
commit 4d89305881
11 changed files with 292 additions and 80 deletions
+23 -9
View File
@@ -269,7 +269,13 @@ jusqu'à 10 incréments, une arme `touch` jusqu'à 5 (jet/firearm), au-delà ell
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). `spell_resistance` (entier), `resistances` (dict type → montant) et
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.
@@ -565,11 +571,19 @@ Règles modélisées :
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 : allonge effective = max(allonge naturelle du gabarit (Small/Medium
5 ft, Large 10 ft, Huge 15 ft, Gargantuan 20 ft, Colossal 30 ft), `reach_ft`
de l'arme). Une arme à allonge (`reach_ft` > 5) menace l'anneau de cases à
distance égale à son allonge — les cases à 5 ft sont une « dead zone » où
elle ne frappe pas ; une arme sans allonge menace les cases adjacentes.
- Mêlée (conformance R1, CRB « Big and Little Creatures in Combat ») :
l'**allonge naturelle** du gabarit (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).
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 à
@@ -647,9 +661,9 @@ Règles modélisées :
(é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 à distance de son allonge effective (les 8 cases adjacentes pour
une arme sans allonge ; l'anneau à son allonge pour une arme reach — la
dead zone de 5 ft n'est pas menacée). Deux déclencheurs sont modélisés : (1) quitter une
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 ; (2) tirer
avec une arme à distance depuis une case menacée — chaque ennemi menaçant