fix(rules): correct the evil eye hex

PF1e (APG Evil Eye): the target takes -2 on ONE chosen option (AC, ability checks, attack rolls, saving throws or skill checks) for 3 rounds, and "A Will save reduces this to just 1 round" — it never negates. _hex_evil_eye applied shaken (-2 attack AND saves) for 1 round with the save negating entirely. It now adds a targeted -2 attack ActiveBuff for 3 rounds, or 1 round on a successful Will save. Simplification documented in README: the option choice is not modeled (attack rolls are the engine's target). The hex-DC behavioral test now reads the -2 attack (3r) transcript line.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-09-12 12:49:48 +02:00
parent 910ed1dc7d
commit f9c369380d
4 changed files with 61 additions and 16 deletions
+39
View File
@@ -255,6 +255,45 @@ def test_nairda_hex_spends_resource() -> None:
assert me.resources_left["hexes"] == 4
def test_evil_eye_failed_save_deals_minus_two_attack_three_rounds() -> None:
"""APG Evil Eye: -2 on one chosen option, 3 rounds; Will does not negate.
Given: a target that fails the Will save (d20 2 +4 < DC 14)
When: the witch hexes it
Then: a -2 attack modifier is applied for 3 rounds (no shaken condition)."""
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="evil_eye")
)
hex_buffs = [b for b in enemy[0].active_buffs if b.source.startswith("hex: evil_eye")]
assert len(hex_buffs) == 1
assert hex_buffs[0].rounds_left == 3
assert hex_buffs[0].modifiers == (StatModifier(target="attack", value=-2),)
assert not any(ac.condition.name == "shaken" for ac in enemy[0].active_conditions)
def test_evil_eye_saved_deals_minus_two_attack_one_round() -> None:
"""APG Evil Eye: "A Will save reduces this to just 1 round" — the penalty
still lands. Given: a target that saves (natural 20)
When: the witch hexes it
Then: the -2 attack modifier is applied for exactly 1 round."""
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="evil_eye")
)
hex_buffs = [b for b in enemy[0].active_buffs if b.source.startswith("hex: evil_eye")]
assert len(hex_buffs) == 1
assert hex_buffs[0].rounds_left == 1
assert hex_buffs[0].modifiers == (StatModifier(target="attack", value=-2),)
# --- tammara: deeds ---
+4 -3
View File
@@ -110,8 +110,9 @@ class TestHexDcUsesInt:
Then: DC = 10 + 4//2 + (21-10)//2 = 17.
Behavioral test: target has Will +2. d20=13 -> total 15.
DC 17: 15 < 17 -> fail -> shaken applied.
DC 12 (wrong, uses CHA): 15 >= 12 -> success -> no effect.
DC 17: 15 < 17 -> fail -> the -2 attack hex lands for its full 3 rounds
(APG Evil Eye: a save only reduces the duration to 1 round).
DC 12 (wrong, uses CHA): 15 >= 12 -> "saved" -> 1 round.
"""
def test_hex_dc_uses_int_not_cha(self) -> None:
@@ -119,7 +120,7 @@ class TestHexDcUsesInt:
target = _make_target(hp_max=100)
engine, c, t = _make_engine(caster, target, [13])
engine._execute_hex(c, t, "evil_eye")
assert any("shaken" in line for line in engine._transcript)
assert any("-2 attack (3r)" in line for line in engine._transcript)
class TestAoEDcEqualsCasterSpellDc: