fix(rules): harmless spells on willing targets forgo the save roll - SpellSpec gains a data-driven 'harmless' flag set on the 29 SRD '(harmless)' spells (verified one by one against the KB); a same-side/self target consumes NO save die (SRD saving-throw.md: '(harmless) ... can attempt a saving throw if it desires' + 'Voluntarily Giving up a Saving Throw'), while hostile targets and undead receiving a cure (harmful to them, Will half for undead damage) keep the roll; snapshot spell summaries expose 'harmless'; golden transcript unchanged (no harmless-save cast in it); +8 tests, 3 fixtures re-pinned for the removed die

This commit is contained in:
2026-09-19 22:27:46 +02:00
parent ea835293a0
commit 4665a1c245
37 changed files with 266 additions and 12 deletions
+22 -3
View File
@@ -1137,7 +1137,24 @@ Règles modélisées :
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). Ciblage par point (U7b) : `Action(kind="cast_spell",
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
@@ -1653,7 +1670,8 @@ Règles modélisées :
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), area {shape,
save, dc, summary, half_on_save, save_negates (E4), harmless (sort «
(harmless) » du SRD — sauvegarde sautée sur cible consentante), area {shape,
radius_ft} | 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
@@ -1908,7 +1926,8 @@ Limites de sorts (conformance D6-D9, documentées plutôt que bricolées) :
`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), temps d'incantation `cast_time`) et types d'effets
(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