fix(rules): apply maneuver failure consequences
PF1e (SRD Trip): "If your attack fails by 10 or more, you are knocked prone instead" - a trip whose total lands at CMD-10 or below now knocks the attacker prone (1 round). PF1e (SRD Overrun): "If your overrun attempt fails, you stop in the space directly in front of the opponent, or the nearest open space in front of the creature" - a failed overrun from beyond adjacency walks the attacker to the free approach cell nearest the target; an already-adjacent attacker stays put. README updated. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -194,6 +194,60 @@ def test_bull_rush_natural_20_below_cmd_pushes_one_cell() -> None:
|
||||
assert b.pos == (1, 3)
|
||||
|
||||
|
||||
def test_trip_fails_by_10_knocks_attacker_prone() -> None:
|
||||
# SRD Trip: "If your attack fails by 10 or more, you are knocked prone
|
||||
# instead." b CMD 30 (bab 20); a total 10+2=12 fails by 18.
|
||||
a = make_state(make_combatant("a", attack_bonus=2), "players", (1, 1))
|
||||
b = make_state(make_combatant("b", attack_bonus=20), "monsters", (1, 2))
|
||||
engine = make_engine([1, 10], [a, b])
|
||||
engine._execute_maneuver(a, b, "trip")
|
||||
assert not any(c.condition.name == "prone" for c in b.active_conditions)
|
||||
prone = next(c for c in a.active_conditions if c.condition.name == "prone")
|
||||
assert prone.source == "maneuver: trip retaliation"
|
||||
|
||||
|
||||
def test_trip_fails_by_less_than_10_attacker_stands() -> None:
|
||||
# a total 19+2=21 vs CMD 30 fails by 9 — no retaliation.
|
||||
a = make_state(make_combatant("a", attack_bonus=2), "players", (1, 1))
|
||||
b = make_state(make_combatant("b", attack_bonus=20), "monsters", (1, 2))
|
||||
engine = make_engine([1, 19], [a, b])
|
||||
engine._execute_maneuver(a, b, "trip")
|
||||
assert not any(c.condition.name == "prone" for c in a.active_conditions)
|
||||
|
||||
|
||||
def test_overrun_fail_stops_in_front_of_target() -> None:
|
||||
# SRD Overrun: "If your overrun attempt fails, you stop in the space
|
||||
# directly in front of the opponent." a at (1,0), b at (1,3): the failed
|
||||
# attempt parks a on (1,2), the cell in front of b. a is not threatened
|
||||
# from (1,0), so no AoO roll is consumed: the single d20 is the maneuver.
|
||||
a = make_state(make_combatant("a", attack_bonus=2), "players", (1, 0))
|
||||
b = make_state(make_combatant("b"), "monsters", (1, 3))
|
||||
engine = make_engine([5], [a, b])
|
||||
engine._execute_maneuver(a, b, "overrun")
|
||||
assert a.pos == (1, 2)
|
||||
|
||||
|
||||
def test_overrun_fail_nearest_open_when_front_occupied() -> None:
|
||||
# "...or the nearest open space in front of the creature if there are
|
||||
# other creatures occupying that space." Blocker on (1,2): a stops (1,1).
|
||||
a = make_state(make_combatant("a", attack_bonus=2), "players", (1, 0))
|
||||
b = make_state(make_combatant("b"), "monsters", (1, 3))
|
||||
c = make_state(make_combatant("c"), "monsters", (1, 2))
|
||||
engine = make_engine([1, 5], [a, b, c])
|
||||
engine._execute_maneuver(a, b, "overrun")
|
||||
assert a.pos == (1, 1)
|
||||
|
||||
|
||||
def test_overrun_fail_adjacent_attacker_does_not_move() -> None:
|
||||
# An already-adjacent attacker is in front of the target: it stays put.
|
||||
a = make_state(make_combatant("a", attack_bonus=2), "players", (1, 1))
|
||||
b = make_state(make_combatant("b"), "monsters", (1, 2))
|
||||
engine = make_engine([1, 5], [a, b])
|
||||
engine._execute_maneuver(a, b, "overrun")
|
||||
assert a.pos == (1, 1)
|
||||
assert not any("overrun blocked" in line for line in engine._transcript)
|
||||
|
||||
|
||||
def test_overrun_knocks_prone_and_moves_through() -> None:
|
||||
# d20=15+2=17 vs CMD 12: margin 5 -> prone AND moved through (CRB
|
||||
# "Overrun": prone only when the attack exceeds CMD by 5 or more).
|
||||
|
||||
Reference in New Issue
Block a user