fix(rules): constrain a run to a straight line

SRD Full-Round Actions - Run: 'you can move up to four times your speed in a straight line'. _execute_run reused the Dijkstra move machinery, so it turned corners around walls. It now walks the straight Bresenham line (_run_line_path) and stops at the first obstacle (impassable terrain, difficult terrain, or a blocking creature), mirroring the charge clear-path logic. Keeps the 4x budget, step-by-step AoO provocation, moved_this_turn and the running/Dex-loss flag; a destination the straight line cannot honour is rejected (unreachable_destination).

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-13 14:48:25 +02:00
parent f5dc9da0c4
commit 6702efc652
3 changed files with 194 additions and 50 deletions
+28 -21
View File
@@ -814,27 +814,34 @@ Règles modélisées :
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), en
réutilisant exactement la mécanique de `move` (descente du champ de coût
Dijkstra, pas à pas avec provocation d'AoO — courir ne protège pas des
AoO), **sauf que la course ne traverse pas le terrain difficile**
(conformance R2 — CRB « Movement, Position and Distance — Difficult
Terrain » : « You can't run or charge across difficult terrain ») : la
descente refuse tout pas dont le footprint toucherait une case de coût > 1
(mécanisme `avoid_difficult` de `_descend_path`, symétrique du refus de la
charge dans `_charge_path_clear`) ; le combattant **avance jusqu'à
l'obstacle et s'y arrête** (comportement déterministe, retenu plutôt que
le rejet de l'action) **quand la course 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). Une `destination` hors budget 4× (ou invalide) est rejetée
(`unreachable_destination`) ; une `destination` joignable uniquement en
traversant le terrain difficile est **aussi rejetée** — le test de
portée (`_destination_reachable`) applique la même contrainte
`avoid_difficult` que la descente, donc le verdict et le chemin parcouru
sont cohérents (pas d'arrêt silencieux avant la destination) ; sans cible ni destination, rejet
(`missing_target`). `default_policy` ne choisit pas la course (disponible
via une politique personnalisée).
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). 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 —