feat(combat): Combat Reflexes extra AoOs
This commit is contained in:
@@ -8,6 +8,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from pf1e_simulator.abilities import AbilitySpec
|
||||
from pf1e_simulator.combat import (
|
||||
Action,
|
||||
CombatantState,
|
||||
@@ -1331,3 +1332,91 @@ def test_5ft_step_blocks_move() -> None:
|
||||
"round 1 orc: wait",
|
||||
"battle over: draw after 1 rounds",
|
||||
)
|
||||
|
||||
|
||||
def _make_guard_cr(dex: int, *, combat_reflexes: bool) -> Combatant:
|
||||
"""Guard with given DEX and optional Combat Reflexes feat."""
|
||||
return Combatant(
|
||||
id="guard",
|
||||
name="guard",
|
||||
level=3,
|
||||
size="Medium",
|
||||
abilities=AbilityScores(
|
||||
str_score=14,
|
||||
dex_score=dex,
|
||||
con_score=12,
|
||||
int_score=10,
|
||||
wis_score=10,
|
||||
cha_score=10,
|
||||
),
|
||||
hp_max=20,
|
||||
ac=ACProfile(total=15, touch=15, flat_footed=15),
|
||||
bab=3,
|
||||
initiative_mod=(dex - 10) // 2,
|
||||
speed_land_ft=0,
|
||||
saves=Saves(fort=4, ref=3, will=1),
|
||||
attacks=[
|
||||
AttackSpec(
|
||||
id="guard-w",
|
||||
name="short sword",
|
||||
kind="melee",
|
||||
attack_bonus=5,
|
||||
damage=[DamageComponent(formula=parse_dice("1d4"), types=["slashing"])],
|
||||
crit_range=20,
|
||||
crit_mult=2,
|
||||
)
|
||||
],
|
||||
features=[AbilitySpec(name="Combat Reflexes", category="feat")] if combat_reflexes else [],
|
||||
)
|
||||
|
||||
|
||||
def test_combat_reflexes_multiple_aoos() -> None:
|
||||
"""Guard with Combat Reflexes + DEX 16 takes multiple AoOs when enemies move."""
|
||||
runner1 = make_combatant("runner1", hp=20, ac=12, speed=30)
|
||||
runner2 = make_combatant("runner2", hp=20, ac=12, speed=30)
|
||||
runner3 = make_combatant("runner3", hp=20, ac=12, speed=30)
|
||||
guard = _make_guard_cr(dex=16, combat_reflexes=True)
|
||||
dest = make_combatant("dest", hp=20, ac=15, attack_bonus=0, speed=0)
|
||||
states = [
|
||||
make_state(runner1, "players", (2, 3)),
|
||||
make_state(runner2, "players", (4, 3)),
|
||||
make_state(runner3, "players", (3, 2)),
|
||||
make_state(guard, "monsters", (3, 5)),
|
||||
make_state(dest, "monsters", (6, 5)),
|
||||
]
|
||||
engine = CombatEngine(
|
||||
ScriptedRng([15, 14, 13, 5, 4, 15, 3, 14, 2, 13, 4, 12, 1]),
|
||||
make_grid(),
|
||||
states,
|
||||
round_cap=1,
|
||||
policy=_move_to_dest,
|
||||
)
|
||||
result = engine.run()
|
||||
aoo_lines = [line for line in result.transcript if "AoO" in line]
|
||||
assert len(aoo_lines) >= 3
|
||||
|
||||
|
||||
def test_no_combat_reflexes_single_aoo() -> None:
|
||||
"""Without Combat Reflexes, the guard still takes exactly1 AoO per round."""
|
||||
runner1 = make_combatant("runner1", hp=20, ac=12, speed=30)
|
||||
runner2 = make_combatant("runner2", hp=20, ac=12, speed=30)
|
||||
runner3 = make_combatant("runner3", hp=20, ac=12, speed=30)
|
||||
guard = _make_guard_cr(dex=16, combat_reflexes=False)
|
||||
dest = make_combatant("dest", hp=20, ac=15, attack_bonus=0, speed=0)
|
||||
states = [
|
||||
make_state(runner1, "players", (2, 3)),
|
||||
make_state(runner2, "players", (4, 3)),
|
||||
make_state(runner3, "players", (3, 2)),
|
||||
make_state(guard, "monsters", (3, 5)),
|
||||
make_state(dest, "monsters", (6, 5)),
|
||||
]
|
||||
engine = CombatEngine(
|
||||
ScriptedRng([15, 14, 13, 5, 4, 15, 3]),
|
||||
make_grid(),
|
||||
states,
|
||||
round_cap=1,
|
||||
policy=_move_to_dest,
|
||||
)
|
||||
result = engine.run()
|
||||
aoo_lines = [line for line in result.transcript if "AoO" in line]
|
||||
assert len(aoo_lines) == 1
|
||||
|
||||
Reference in New Issue
Block a user