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:
@@ -355,7 +355,9 @@ class TestAreaSavesPerSpellSaveType:
|
||||
save-free, and secondary creatures do not always roll Reflex."""
|
||||
|
||||
def test_sleep_secondary_creature_rolls_will_and_negates(self) -> None:
|
||||
# Sleep (Will negates, save_for_half=False): primary target saves on
|
||||
# Sleep (one-round cast, Will negates, save_for_half=False): casting
|
||||
# records the pending cast; resolution (start of the caster's next
|
||||
# turn, no damage taken) delivers it — primary target saves on
|
||||
# natural 20 (first roll), the caster — second creature in the area —
|
||||
# rolls its own Will and fails on natural 1 (second roll).
|
||||
caster = _make_caster()
|
||||
@@ -365,6 +367,8 @@ class TestAreaSavesPerSpellSaveType:
|
||||
)
|
||||
engine._cast_spell(c, t, "Sleep")
|
||||
assert not any(ac.condition.name == "asleep" for ac in t.active_conditions)
|
||||
engine._resolve_pending_cast(c)
|
||||
assert not any(ac.condition.name == "asleep" for ac in t.active_conditions)
|
||||
assert any(ac.condition.name == "asleep" for ac in c.active_conditions)
|
||||
|
||||
def test_color_spray_primary_save_is_not_discarded(self) -> None:
|
||||
@@ -709,8 +713,9 @@ class TestSpellDamageWithDr:
|
||||
|
||||
|
||||
class TestSummonMonster:
|
||||
"""Given: a caster casting Summon Monster II
|
||||
When: _cast_spell is called
|
||||
"""Given: a caster casting Summon Monster II (one-round cast)
|
||||
When: _cast_spell is called and the pending cast resolves on the
|
||||
caster's next turn (SRD "Casting Time")
|
||||
Then: a celestial wolf is summoned adjacent to the caster."""
|
||||
|
||||
def test_summon_creature(self) -> None:
|
||||
@@ -719,6 +724,7 @@ class TestSummonMonster:
|
||||
engine, c, t = _make_engine(caster, target, [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3])
|
||||
hp_before = len(engine._states)
|
||||
engine._cast_spell(c, t, "Summon Monster II")
|
||||
engine._resolve_pending_cast(c)
|
||||
# Caster + target + summoned = 3 combatants
|
||||
assert len(engine._states) == hp_before + 1
|
||||
# Find the summoned creature
|
||||
|
||||
Reference in New Issue
Block a user