fix(rules): set the slumber hex duration to the witch level
PF1e (APG Slumber): the target "remains asleep for a number of rounds equal to the witch's level" (Will negates). _hex_slumber hardcoded 1 round; it now uses the caster's level. The RAW clause that the hex ends when the target takes damage is not modeled (no wake-on-damage hook in the engine) — documented in README. Two behavioral tests pin the failed-save duration (= level) and the saved case. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -1098,6 +1098,10 @@ Règles modélisées :
|
|||||||
**une seule option** — l'option choisie est modélisée par les jets d'attaque —
|
**une seule option** — l'option choisie est modélisée par les jets d'attaque —
|
||||||
pendant **3 rounds** ; une sauvegarde de Volonté réussie ne l'annule pas mais
|
pendant **3 rounds** ; une sauvegarde de Volonté réussie ne l'annule pas mais
|
||||||
**réduit la durée à 1 round** (« A Will save reduces this to just 1 round ») ;
|
**réduit la durée à 1 round** (« A Will save reduces this to just 1 round ») ;
|
||||||
|
`slumber` (APG, conformance D4) endort la cible pour un **nombre de rounds
|
||||||
|
égal au niveau de la sorcière** (Volonté annule ; simplification documentée :
|
||||||
|
la fin RAW du sortilège quand la cible subit des dégâts n'est pas modélisée,
|
||||||
|
le moteur n'a pas de crochet de réveil) ;
|
||||||
`cackle` prolonge d'un round les sortilèges déjà
|
`cackle` prolonge d'un round les sortilèges déjà
|
||||||
actifs, sur une liste instantanée pour ne pas s'étendre à l'infini. Les sorts à
|
actifs, sur une liste instantanée pour ne pas s'étendre à l'infini. Les sorts à
|
||||||
zone (AoE) utilisent le DD d'incantation du jeteur et chaque créature de la
|
zone (AoE) utilisent le DD d'incantation du jeteur et chaque créature de la
|
||||||
|
|||||||
@@ -4783,7 +4783,10 @@ class CombatEngine:
|
|||||||
def _hex_slumber(
|
def _hex_slumber(
|
||||||
self, state: CombatantState, target: CombatantState, hex_dc: int
|
self, state: CombatantState, target: CombatantState, hex_dc: int
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Slumber: asleep/disabled if Will fails."""
|
"""Slumber (APG): the target remains asleep for a number of rounds
|
||||||
|
equal to the witch's level; a successful Will save negates.
|
||||||
|
Simplification documented in README: the RAW ending of the hex when
|
||||||
|
the target takes damage is not modeled (no wake-on-damage hook)."""
|
||||||
save_result = self.resolve_save(target, "will", hex_dc)
|
save_result = self.resolve_save(target, "will", hex_dc)
|
||||||
if save_result.success:
|
if save_result.success:
|
||||||
self._log(
|
self._log(
|
||||||
@@ -4791,16 +4794,17 @@ class CombatEngine:
|
|||||||
f"{target.combatant.id} -> SAVED"
|
f"{target.combatant.id} -> SAVED"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
rounds = state.combatant.level
|
||||||
target.active_conditions.append(
|
target.active_conditions.append(
|
||||||
ActiveCondition(
|
ActiveCondition(
|
||||||
condition=CONDITIONS_BY_NAME["asleep"],
|
condition=CONDITIONS_BY_NAME["asleep"],
|
||||||
rounds_left=1,
|
rounds_left=rounds,
|
||||||
source=f"hex: slumber by {state.combatant.id}",
|
source=f"hex: slumber by {state.combatant.id}",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self._log(
|
self._log(
|
||||||
f"round {self._current_round} {state.combatant.id}: slumber on "
|
f"round {self._current_round} {state.combatant.id}: slumber on "
|
||||||
f"{target.combatant.id} -> asleep (1r)"
|
f"{target.combatant.id} -> asleep ({rounds}r)"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _hex_cackle(self, state: CombatantState, target: CombatantState) -> None:
|
def _hex_cackle(self, state: CombatantState, target: CombatantState) -> None:
|
||||||
|
|||||||
@@ -294,6 +294,44 @@ def test_evil_eye_saved_deals_minus_two_attack_one_round() -> None:
|
|||||||
assert hex_buffs[0].modifiers == (StatModifier(target="attack", value=-2),)
|
assert hex_buffs[0].modifiers == (StatModifier(target="attack", value=-2),)
|
||||||
|
|
||||||
|
|
||||||
|
def test_slumber_failed_save_puts_target_asleep_for_witch_level() -> None:
|
||||||
|
"""APG Slumber: the target "remains asleep for a number of rounds equal to
|
||||||
|
the witch's level". Given: a level-5 witch and a target failing the Will
|
||||||
|
save (d20 2 +4 < DC 14)
|
||||||
|
When: the hex is cast
|
||||||
|
Then: the asleep condition carries rounds_left = 5."""
|
||||||
|
nairda = _combatant("nairda", resources={"hexes": 5})
|
||||||
|
goblin = _combatant("goblin", hp_max=6)
|
||||||
|
engine, me, enemy = _engine(nairda, [goblin], rng_queue=[2])
|
||||||
|
|
||||||
|
engine._execute(
|
||||||
|
me, Action(kind="hex", target_id=enemy[0].combatant.id, hex_name="slumber")
|
||||||
|
)
|
||||||
|
|
||||||
|
asleep = [
|
||||||
|
ac for ac in enemy[0].active_conditions if ac.condition.name == "asleep"
|
||||||
|
]
|
||||||
|
assert len(asleep) == 1
|
||||||
|
assert asleep[0].rounds_left == 5
|
||||||
|
|
||||||
|
|
||||||
|
def test_slumber_saved_leaves_target_awake() -> None:
|
||||||
|
"""APG Slumber: Will negates. Given: a target saving (natural 20)
|
||||||
|
When: the hex is cast
|
||||||
|
Then: no asleep condition is applied."""
|
||||||
|
nairda = _combatant("nairda", resources={"hexes": 5})
|
||||||
|
goblin = _combatant("goblin", hp_max=6)
|
||||||
|
engine, me, enemy = _engine(nairda, [goblin], rng_queue=[20])
|
||||||
|
|
||||||
|
engine._execute(
|
||||||
|
me, Action(kind="hex", target_id=enemy[0].combatant.id, hex_name="slumber")
|
||||||
|
)
|
||||||
|
|
||||||
|
assert not any(
|
||||||
|
ac.condition.name == "asleep" for ac in enemy[0].active_conditions
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# --- tammara: deeds ---
|
# --- tammara: deeds ---
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user