From 68699ad6c7cb1ea469d866d8dd60d2e917a268a6 Mon Sep 17 00:00:00 2001 From: ctan Date: Sat, 12 Sep 2026 12:52:52 +0200 Subject: [PATCH] fix(rules): set the slumber hex duration to the witch level MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- README.md | 4 ++++ src/pf1e_simulator/combat.py | 10 ++++++--- tests/test_class_abilities.py | 38 +++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7027e4f..a110712 100644 --- a/README.md +++ b/README.md @@ -1098,6 +1098,10 @@ Règles modélisées : **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 **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à 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 diff --git a/src/pf1e_simulator/combat.py b/src/pf1e_simulator/combat.py index 692399b..6eb4e36 100644 --- a/src/pf1e_simulator/combat.py +++ b/src/pf1e_simulator/combat.py @@ -4783,7 +4783,10 @@ class CombatEngine: def _hex_slumber( self, state: CombatantState, target: CombatantState, hex_dc: int ) -> 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) if save_result.success: self._log( @@ -4791,16 +4794,17 @@ class CombatEngine: f"{target.combatant.id} -> SAVED" ) return + rounds = state.combatant.level target.active_conditions.append( ActiveCondition( condition=CONDITIONS_BY_NAME["asleep"], - rounds_left=1, + rounds_left=rounds, source=f"hex: slumber by {state.combatant.id}", ) ) self._log( 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: diff --git a/tests/test_class_abilities.py b/tests/test_class_abilities.py index eeb2246..4cd7a1f 100644 --- a/tests/test_class_abilities.py +++ b/tests/test_class_abilities.py @@ -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),) +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 ---