fix(rules): correct metamagic slots, touch-spell delivery, and defensive/grapple casting

This commit is contained in:
2026-09-11 00:06:06 +02:00
parent 9aee4f6f6b
commit 890b7959d4
4 changed files with 691 additions and 52 deletions
+40 -12
View File
@@ -402,7 +402,9 @@ Règles modélisées :
reason="already_prone"` ; attaque avec un `weapon_id` choisi inutilisable →
`ok=False, reason="weapon_unavailable"` ; incantation avec un `target_pos`
hors carte ou sans ligne d'effet → `ok=False, reason="invalid_destination"`
(U7b) ; course ou aide sans cible **et** sans destination →
(U7b) ; incantation sans emplacement disponible au niveau requis — niveau
de base ou majoré par la métamagie (B7) → `ok=False, reason="no_slot"` ;
course ou aide sans cible **et** sans destination →
`missing_target` ; aide sans arme de mêlée utilisable → `no_weapon` ;
course vers une `destination` hors du budget 4× →
`unreachable_destination` (U5e).
@@ -579,18 +581,41 @@ Règles modélisées :
- Incantation : chaque `CombatantState` porte `spell_slots_left` (initialisé depuis
le champ `spell_slots` du JSON, ex. `{"1": 4, ...}`) ; `_cast_spell` dépense
l'emplacement du niveau concerné (`spend_spell_slot`) — **sans emplacement
disponible, l'incantation est refusée** (journalisée `NO SPELL SLOT`, aucun
jet consommé) ; les cantrips (niveau 0) s'incantent librement. Le DD vaut `10 + niveau du
sort + mod de la caractéristique d'incantation` (`casting_ability`), la sauvegarde
disponible, l'incantation est refusée** (journalisée `NO SPELL SLOT`, feedback
`ok=False, reason="no_slot"`, aucun jet consommé) ; les cantrips (niveau 0) s'incantent librement. Le DD vaut `10 + niveau du
sort (niveau d'emplacement effectif en cas de métamagie, voir ci-dessous) +
mod de la caractéristique d'incantation` (`casting_ability`), la sauvegarde
est résolue par `resolve_save`, et la RD ne réduit pas les dégâts des sorts
(`test_dr_does_not_reduce_spell_damage`). Incanter dans une
case menacée provoque des AoO ; tout dégât subi pendant l'incantation impose un
jet de concentration dont le DD dépend de la situation (`_concentration_dc`) :
`10 + dégâts + niveau` si blessé, `10 + CMB de l'adversaire + niveau` si agrippé,
sinon `15 + 2×niveau` (incantation défensive), jet = d20 + niveau + mod de
la caractéristique d'incantation (Int/Wis/Cha — jamais Constitution, CRB) ;
le don Combat Casting n'accorde son +4 qu'en incantation défensive ou quand
on est agrippé, jamais pour les dégâts subis pendant l'incantation. Un sort ciblé avec sauvegarde fait pleins dégâts
(`test_dr_does_not_reduce_spell_damage`). Métamagie (conformance B7, CRB
« Metamagic Feats ») : la métamagie se choisit **à chaque incantation** via
`Action(kind="cast_spell", metamagic=("Empower Spell", ...))` — seuls les
dons demandés ET connus du lanceur (`metamagic_feats`) s'appliquent (un don
inconnu est ignoré, un don connu non demandé n'est pas appliqué) ; le sort
occupe alors un emplacement de niveau de base + ajustement (Empower +2,
Maximize +3, Extend +1) et son DD utilise ce niveau d'emplacement ; sans
emplacement au niveau requis, l'incantation est refusée (`no_slot`). Avec
`metamagic` vide (défaut), le comportement est inchangé. Incanter dans une
case menacée provoque des AoO. Incantation défensive (conformance B24, CRB
« Concentration ») : `Action(kind="cast_spell", cast_defensively=True)`
évite la provocation d'AoO mais impose un jet de concentration DD
`15 + 2×niveau` (+4 avec Combat Casting) ; un échec perd le sort
(emplacement brûlé, journalisé `casting defensively: concentration failed`).
Agrippé (`grappled_with`) : un jet de concentration DD
`10 + CMB de l'adversaire + niveau` (+4 Combat Casting) est requis pour
incanter, défensivement ou non — ce jet remplace la provocation (un
agrippé ne peut pas porter d'AoO). Tout dégât subi pendant l'incantation
(AoO) impose un jet DD `10 + dégâts + niveau` **sans** le bonus Combat
Casting (conformance P1). Jet de concentration = d20 + niveau d'incanteur +
mod de la caractéristique d'incantation (Int/Wis/Cha — jamais Constitution,
CRB). Sorts de contact (conformance B14, CRB « Magic — Range: Touch ») :
délivrer un sort de portée `touch` à une créature **hostile** (camp
différent) exige une attaque de contact de mêlée — d20 + BAB + mod de Force
du lanceur contre la CA de contact de la cible (amputée du modificateur de
Dextérité si la cible perd son bonus de Dextérité, même chemin que
`resolve_attack`) ; en cas d'échec le sort n'a aucun effet (l'emplacement
est brûlé). Simplifications documentées : Weapon Finesse et les autres
bonus situationnels de jet de contact ne sont pas modélisés. Une cible
volontaire (soi ou un allié du même camp) ne demande aucun jet. Un sort ciblé avec sauvegarde fait pleins dégâts
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`
@@ -881,6 +906,9 @@ Règles modélisées :
(calcul identique à `resolve_attack`, couvert mou des créatures inclus).
Les API de décision exposées (items L1-L8)
s'y retrouvent : `direction`/`trick_condition`/`weapon_id` sur `Action`,
ainsi que `metamagic` (dons de métamagie choisis pour cette incantation,
B7) et `cast_defensively` (incantation défensive, B24) sur l'action
`cast_spell` ;
`can_charge` → `ChargeResult`, kinds `retrieve_weapon`/`repair_weapon`.
Le kind `activate_power_attack` (action libre) active le don Power Attack
(`state.power_attack = True`) si le combattant possède le don ; sans le don,