feat(combat): implement overrun, dirty trick, steal, reposition and drag maneuvers

This commit is contained in:
2026-09-05 12:37:06 +02:00
parent 2f2acd992f
commit cd75a4f2bf
3 changed files with 297 additions and 12 deletions
+130
View File
@@ -116,3 +116,133 @@ def test_escape_from_grapple_consumes_single_d20() -> None:
assert not engine._rng._queue
assert a.grappled_with is None
assert not any(c.condition.name == "grappled" for c in a.active_conditions)
def test_overrun_knocks_prone_and_moves_through() -> None:
a_spec = make_combatant("a", attack_bonus=2)
b_spec = make_combatant("b")
a = make_state(a_spec, "players", (1, 1))
b = make_state(b_spec, "monsters", (1, 2))
engine = make_engine([12], [a, b])
engine._execute_maneuver(a, b, "overrun")
prone = next(c for c in b.active_conditions if c.condition.name == "prone")
assert prone.rounds_left == 1
assert prone.source == "maneuver: overrun"
assert a.pos == (1, 3)
def test_overrun_prone_but_no_space() -> None:
a_spec = make_combatant("a", attack_bonus=2)
b_spec = make_combatant("b")
c_spec = make_combatant("c")
a = make_state(a_spec, "players", (1, 1))
b = make_state(b_spec, "monsters", (1, 2))
c = make_state(c_spec, "monsters", (1, 3))
engine = make_engine([12], [a, b, c])
engine._execute_maneuver(a, b, "overrun")
assert any(cond.condition.name == "prone" for cond in b.active_conditions)
assert a.pos == (1, 1)
assert "SUCCESS (prone)" in engine._transcript[-1]
assert "moved through" not in engine._transcript[-1]
def test_dirty_trick_dazzles() -> None:
a_spec = make_combatant("a", attack_bonus=2)
b_spec = make_combatant("b")
a = make_state(a_spec, "players", (1, 1))
b = make_state(b_spec, "monsters", (1, 2))
engine = make_engine([12], [a, b])
engine._execute_maneuver(a, b, "dirty_trick")
dazzled = next(c for c in b.active_conditions if c.condition.name == "dazzled")
assert dazzled.rounds_left == 1
assert dazzled.source == "maneuver: dirty trick"
def test_dirty_trick_fails_on_natural_1() -> None:
a_spec = make_combatant("a", attack_bonus=2)
b_spec = make_combatant("b")
a = make_state(a_spec, "players", (1, 1))
b = make_state(b_spec, "monsters", (1, 2))
engine = make_engine([1], [a, b])
engine._execute_maneuver(a, b, "dirty_trick")
assert not any(c.condition.name == "dazzled" for c in b.active_conditions)
assert not any(c.condition.name == "prone" for c in b.active_conditions)
assert engine._transcript[-1].endswith("-> FAILED")
def test_steal_takes_melee_weapon() -> None:
a_spec = make_combatant("a", attack_bonus=2)
b_spec = make_combatant("b")
a = make_state(a_spec, "players", (1, 1))
b = make_state(b_spec, "monsters", (1, 2))
hp_before = b.hp
engine = make_engine([12], [a, b])
engine._execute_maneuver(a, b, "steal")
assert "b-w" in b.weapon_stolen
assert engine.weapon_for(b, a) is None
assert b.hp == hp_before
def test_steal_nothing_to_steal() -> None:
a_spec = make_combatant("a", attack_bonus=2)
b_spec = make_combatant("b").model_copy(update={"attacks": []})
a = make_state(a_spec, "players", (1, 1))
b = make_state(b_spec, "monsters", (1, 2))
engine = make_engine([12], [a, b])
engine._execute_maneuver(a, b, "steal")
assert not b.weapon_stolen
assert "nothing to steal" in engine._transcript[-1]
def test_reposition_moves_to_first_free_neighbor() -> None:
a_spec = make_combatant("a", attack_bonus=2)
b_spec = make_combatant("b")
c_spec = make_combatant("c")
a = make_state(a_spec, "players", (1, 1))
b = make_state(b_spec, "monsters", (1, 2))
c = make_state(c_spec, "monsters", (1, 3))
engine = make_engine([12], [a, b, c])
engine._execute_maneuver(a, b, "reposition")
assert b.pos == (0, 1)
assert c.pos == (1, 3)
def test_reposition_no_space() -> None:
a_spec = make_combatant("a", attack_bonus=2)
b_spec = make_combatant("b")
blocker_positions = [(0, 0), (0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 2)]
blockers = [
make_state(make_combatant(f"c{idx}"), "monsters", pos)
for idx, pos in enumerate(blocker_positions)
]
a = make_state(a_spec, "players", (2, 1))
b = make_state(b_spec, "monsters", (1, 1))
engine = make_engine([12], [a, b, *blockers])
engine._execute_maneuver(a, b, "reposition")
assert b.pos == (1, 1)
assert "no space" in engine._transcript[-1]
def test_drag_moves_pair_together() -> None:
a_spec = make_combatant("a", attack_bonus=2)
b_spec = make_combatant("b")
a = make_state(a_spec, "players", (1, 1))
b = make_state(b_spec, "monsters", (1, 2))
engine = make_engine([12], [a, b])
engine._execute_maneuver(a, b, "drag")
assert b.pos == (1, 3)
assert a.pos == (1, 2)
def test_drag_no_space() -> None:
a_spec = make_combatant("a", attack_bonus=2)
b_spec = make_combatant("b")
c_spec = make_combatant("c")
a = make_state(a_spec, "players", (1, 1))
b = make_state(b_spec, "monsters", (1, 2))
c = make_state(c_spec, "monsters", (1, 3))
engine = make_engine([12], [a, b, c])
engine._execute_maneuver(a, b, "drag")
assert b.pos == (1, 2)
assert a.pos == (1, 1)
assert "no space" in engine._transcript[-1]