feat(rules): support multi-projectile spells

SRD Magic Missile fires one missile +1 per two caster levels beyond 1st (max 5, 1d4+1 force, auto-hit); Scorching Ray fires one ray +1 per four levels beyond 3rd (max 3, 4d6 fire, ranged touch each). DamageEffect gains a ProjectileScaling spec (JSON 'projectiles'); _apply_damage_effect loops count_for(caster_level) projectiles, each rolling its own damage and (for a declared-projectile ray) its own ranged touch attack, with the target-drops-mid-sequence convention losing the rest. Updated magic_missile.json/scorching_ray.json, the golden lich transcript (CL 11 now fires 5 missiles, Tammara dies round 1: 43 damage), the scripted MM queues in test_spells/test_conformance_spellflow, added tests/test_multi_projectile.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:05:56 +02:00
parent 642f449b9e
commit 88ef9f39be
9 changed files with 482 additions and 74 deletions
+22 -9
View File
@@ -940,7 +940,27 @@ Règles modélisées :
inchangé. Simplification documentée : le scalage ne s'applique qu'aux
dégâts (`DamageEffect`) — les soins par niveau (Cure Light Wounds
« +1 per caster level (maximum +5) ») ne sont pas scalés (la fiche dorée
épingle `1d8+1`).
épingle `1d8+1`).
- 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.
- 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
@@ -1393,7 +1413,7 @@ plutôt que bricolées) :
Dextérité/Force du CMB (conformance L1) tient compte de leur taille.
Limites de sorts (conformance D6-D9, documentées plutôt que bricolées — le
moteur n'a ni projectiles multiples, ni sauvegarde partielle) :
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
@@ -1409,13 +1429,6 @@ moteur n'a ni projectiles multiples, ni sauvegarde partielle) :
bien `panicked` (conformance D8), mais le moteur n'a pas de sauvegarde
partielle pour les conditions — un succès annule totalement l'effet au lieu
de poser `shaken` 1 round ; la durée « 1 round/niveau » est fixée à 4 rounds.
- **Magic Missile — un seul projectile** (SRD spells/magic-missile.md :
« You strike [...] with one missile, plus one additional missile per two
caster levels beyond 1st [...] maximum of five missiles ») : le moteur
n'a pas de scalage multi-projectiles ni de répartition sur plusieurs
cibles ; le sort tire une unique missile (1d4+1 force, touche
automatiquement) sur la cible résolue (conformance D9 — documenté, la
fiche dorée est inchangée).
- **Résistance aux sorts appliquée à tout sort ciblé** : le moteur oppose la
SR du combattant à chaque sort qui le cible, sans drapeau par sort ; les
rares sorts dont la fiche SRD déclare « Spell Resistance: no » (ex. Acid