feat(rules): add ongoing spell damage

Acid Arrow (SRD spells/acid-arrow.md) deals 2d4 acid, then 2d4 again at the start of each of the target's turns for 1 round + 1 per three caster levels (max 6). DamageEffect gains a RecurringDamageSpec (JSON 'recurring'); a landed hit attaches a RecurringDamage source to CombatantState.recurring_damage, ticked in _turn_gate (after bleed), reduced by energy resistance/immunity but never DR (spell damage), cleared at expiry, and exposed in the observation snapshot. Extracted _resolve_one_projectile to keep _apply_damage_effect under the complexity ceiling. Updated acid_arrow.json, added tests/test_recurring_damage.py, and the README.

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 12:25:38 +02:00
parent 88ef9f39be
commit a59ebee37a
5 changed files with 476 additions and 42 deletions
+20 -8
View File
@@ -961,6 +961,22 @@ Règles modélisées :
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": 6}` — le nombre de rounds de récurrence vaut
`base_rounds + niveau // per_levels`, plafonné à `max` (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, 6 au niveau 18+). À 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
@@ -1340,7 +1356,10 @@ Règles modélisées :
son tour), running (bool — course en cours jusqu'au prochain tour du
combattant : il perd son bonus de Dextérité à la CA), 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`).
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,
@@ -1415,13 +1434,6 @@ plutôt que bricolées) :
Limites de sorts (conformance D6-D9, documentées plutôt que bricolées — le
moteur n'a pas encore de sauvegarde partielle) :
- **Acid Arrow — sans dégâts continus** (SRD spells/acid-arrow.md : portée
**longue** — corrigée, conformance D7 — et « For every three caster levels
you possess, the acid, unless neutralized, lasts for another round [...],
dealing another 2d4 points of damage in each round ») : le moteur n'a pas de
subsystem de dégâts continus par sort (ni neutralisation d'acide) ; seuls les
2d4 de dégâts initiaux sont modélisés, les rounds de dégâts persistants sont
hors périmètre.
- **Fear — sauvegarde totale** (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