feat(rules): resolve one-round casts on the following turn
SRD Casting Time: a spell that takes 1 round is a full-round action that comes into effect just before the caster's turn in the next round; the caster then acts normally. _cast_spell now spends the slot and runs the start-of-casting gates, then records a PendingCast (spell, target, center, metamagic, slot level, hp at cast); delivery (touch attack, SR, saves, effects) resolves at the start of the caster's next turn, before the policy runs. Net hp lost in between forces the injured-while-casting concentration check (DC 10 + damage + spell level, no Combat Casting) or the spell fizzles; a caster knocked out of the fight loses it. Exposed in the observation via pending_cast. Bounded model documented: net-hp measurement instead of a per-damage-site hook; long (1 minute+) casting stays out of scope. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -461,9 +461,11 @@ Règles modélisées :
|
||||
courage et est aussi retiré quand le swift est consommé par une immédiate,
|
||||
via `immediate_used`) ; `immediate` et `free` → libre (l'immédiate reste
|
||||
hors-tour et consomme le prochain swift) ; `full_round` → round complet ;
|
||||
`one_round` → **round complet par clamp** — approximation documentée : le
|
||||
moteur ne tient pas d'incantation en cours entre les tours, le sort
|
||||
d'1 round se résout au bout du tour courant au lieu du suivant ;
|
||||
`one_round` → **round complet** — SRD « Casting Time » : « A spell that
|
||||
takes 1 round to cast is a full-round action » ; l'emplacement est dépensé
|
||||
et les gardes de début d'incantation (AoO, concentration) résolus sur le
|
||||
tour courant, mais **l'effet est différé au début du tour suivant** du
|
||||
lanceur (voir « Incantation d'1 round » ci-dessous) ;
|
||||
`long` (> 1 round : 3 rounds, 10 minutes, 1 heure — cast multi-round non
|
||||
planifiable dans un tour) → **jeté avant le filtre de budget**, rapporté comme
|
||||
`ActionResult(kind="cast_spell", ok=False, reason="downgraded:cast_time")`
|
||||
@@ -479,6 +481,33 @@ Règles modélisées :
|
||||
`_cast_time_of`) : la métamagie d'incanteur spontané qui allonge le temps
|
||||
d'incantation à un round complet sans don Quickened — la signature
|
||||
`Callable[[Action], CastTime]` du résolveur l'accommodera sans changement.
|
||||
- Incantation d'1 round (`cast_time: one_round`) : SRD « Casting Time » —
|
||||
« It comes into effect just before the beginning of your turn in the round
|
||||
after you began casting the spell. You then act normally after the spell is
|
||||
completed. » Sur le tour d'incantation, l'emplacement est dépensé, les
|
||||
gardes de début d'incantation jouent (AoO d'incantation + jet de
|
||||
concentration sur dégât, incantation défensive, agrippé), puis le sort est
|
||||
consigné comme incantation en cours (`PendingCast` sur l'état du lanceur :
|
||||
nom du sort, cible, point de centre, métamagie, niveau d'emplacement, pv au
|
||||
moment de l'incantation) et **rien n'est appliqué** — les décisions
|
||||
d'incantation (attaque de contact, RM, sauvegardes, effets) se prennent à la
|
||||
résolution (« You make all pertinent decisions about a spell [...] when the
|
||||
spell comes into effect »). Au début du tour suivant du lanceur, avant le
|
||||
portail de début de tour et avant l'appel de politique, l'incantation en
|
||||
cours se résout (journalisé `finishes casting X`), puis le lanceur agit
|
||||
normalement. Interruption : toute perte nette de pv entre l'incantation et
|
||||
la résolution impose le jet de concentration « blessé pendant l'incantation »
|
||||
(DD `10 + dégâts + niveau du sort`, **sans** le bonus Combat Casting — même
|
||||
helper `_check_concentration` que l'AoO d'incantation) ; un échec perd le
|
||||
sort (`spell fizzles`, emplacement brûlé). Un lanceur mis hors de combat
|
||||
avant sa résolution perd le sort (journalisé `lost (cannot act)`).
|
||||
Simplifications documentées : l'interruption est mesurée en **perte nette
|
||||
de pv à la résolution** et non par un crochet à chaque site de dégâts — un
|
||||
soin intermédiaire peut masquer des dégâts subis ; l'incantation en cours
|
||||
est exposée dans l'observation via la clé `pending_cast`
|
||||
(`{spell_name, target_id, slot_level}` ou `None`). L'incantation `long`
|
||||
(> 1 round) reste hors périmètre (jetée du budget, voir « Temps
|
||||
d'incantation »).
|
||||
- Retard (`Action(kind="delay")`, action standard) : le combattant renonce à
|
||||
agir maintenant et agit plus tard dans le **même round** — en fin d'ordre si
|
||||
`target_id` est absent, sinon juste avant le combattant désigné. La nouvelle
|
||||
@@ -582,7 +611,7 @@ Règles modélisées :
|
||||
staggered/nauseated (ou swift consommé par une immédiate) →
|
||||
`downgraded:action_economy` / `downgraded:cast_time` /
|
||||
`downgraded:staggered` / `downgraded:nauseated` /
|
||||
`downgraded:immediate_swift` ;
|
||||
`downgraded:immediate_swift` / `downgraded:surprise` ;
|
||||
retard valide → `ok=True, reason="delayed"`, second retard du même round →
|
||||
`ok=False, reason="already_delayed"` ; préparation valide →
|
||||
`ok=True, reason="ready"` ; relèvement sans `prone` → `ok=False,
|
||||
@@ -1376,7 +1405,10 @@ Règles modélisées :
|
||||
last_turn_results (tuple de {kind, ok, reason} — résultat d'exécution du
|
||||
dernier tour, pour que la policy apprenne les actions rejetées/dégradées),
|
||||
readied ({kind, trigger} de l'action préparée ou None — préparation en
|
||||
cours, consommée quand le déclencheur la fait tirer),
|
||||
cours, consommée quand le déclencheur la fait tirer), pending_cast
|
||||
({spell_name, target_id, slot_level} de l'incantation d'1 round en cours
|
||||
ou None — résolue au début du prochain tour du lanceur, voir « Incantation
|
||||
d'1 round »),
|
||||
defensive_ac_bonus (int — bonus d'esquive à la CA de la défense totale
|
||||
(+4) ou du combat défensif (+2), réinitialisé au début du tour du
|
||||
combattant), fight_defensively (bool — combat défensif actif : −4 aux
|
||||
@@ -1453,11 +1485,14 @@ la lecture mais non appliquée dans la résolution.
|
||||
Limites hors périmètre (arrêtées lors de la conformance L1-L10, documentées
|
||||
plutôt que bricolées) :
|
||||
|
||||
- **Incantation `one_round` bornée à un round complet** : le moteur ne tient
|
||||
pas d'incantation en cours entre les tours ; un sort d'1 round se résout à
|
||||
la fin du tour courant au lieu du suivant (approximation documentée dans
|
||||
« Temps d'incantation »). L'incantation multi-round vraie (> 1 round) est
|
||||
jetée du budget (`long`).
|
||||
- **Incantation multi-round vraie (`long`) hors périmètre** : le round de
|
||||
surprise et l'incantation d'1 round sont modélisés (voir « Round de
|
||||
surprise » et « Incantation d'1 round ») ; restent hors périmètre les temps
|
||||
d'incantation > 1 round (3 rounds, 10 minutes, 1 heure — SRD « Casting
|
||||
Time » : « These actions must be consecutive and uninterrupted, or the
|
||||
spell automatically fails ») : non planifiables dans un tour, ils sont
|
||||
jetés du budget (`downgraded:cast_time`) plutôt qu'approximés, faute de
|
||||
données d'objectif pour ces sorts rituels.
|
||||
- **Gabarits Tiny/Diminutive/Fine abstraits à la grille de 5 ft** : leur
|
||||
allonge nulle et la co-occupation de leurs cases (plusieurs créatures
|
||||
minuscules sur une même case) ne sont pas modélisées ; seule la substitution
|
||||
@@ -1579,7 +1614,7 @@ Limites de sorts (conformance D6-D9, documentées plutôt que bricolées) :
|
||||
La gate de validation complète (tests + lint + types) :
|
||||
|
||||
```bash
|
||||
uv run pytest -q # 1402 tests
|
||||
uv run pytest -q # 1408 tests
|
||||
uv run ruff check src tests
|
||||
uv run basedpyright src # mode strict
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user