feat(combat): grant soft cover (+4 AC) to ranged attacks through creatures
This commit is contained in:
@@ -664,3 +664,58 @@ def test_ranged_weapon_unusable_beyond_maximum_range() -> None:
|
||||
assert engine.weapon_for(states[0], states[1]) is None
|
||||
result = engine.run()
|
||||
assert result.transcript[2] == "round 1 archer: move (0,0)->(0,1)"
|
||||
|
||||
|
||||
def test_ranged_soft_cover_from_creature_between() -> None:
|
||||
"""A creature between shooter and target grants soft cover (+4 AC)."""
|
||||
archer = make_combatant("archer", attack_bonus=4, kind="ranged", range_increment_ft=60)
|
||||
blocker = make_combatant("blocker", speed=0)
|
||||
target = make_combatant("target", ac=13)
|
||||
states = [
|
||||
make_state(archer, "players", (1, 1)),
|
||||
make_state(blocker, "players", (1, 2)),
|
||||
make_state(target, "monsters", (1, 4)),
|
||||
]
|
||||
engine = CombatEngine(ScriptedRng([12, 3]), make_grid(), states)
|
||||
result = engine.resolve_attack(states[0], states[2], archer.attacks[0])
|
||||
assert result.ac == 17 # 13 + 4 soft cover
|
||||
assert result.hit is False # 12 + 4 = 16 < 17
|
||||
|
||||
|
||||
def test_melee_soft_cover_ignored_in_resolution() -> None:
|
||||
"""Melee attacks ignore creatures for soft cover (CRB: ranged only)."""
|
||||
attacker = make_combatant("attacker", attack_bonus=2)
|
||||
blocker = make_combatant("blocker", speed=0)
|
||||
target = make_combatant("target", ac=13)
|
||||
states = [
|
||||
make_state(attacker, "players", (1, 1)),
|
||||
make_state(blocker, "players", (1, 2)),
|
||||
make_state(target, "monsters", (1, 4)),
|
||||
]
|
||||
engine = CombatEngine(ScriptedRng([12, 3]), make_grid(), states)
|
||||
result = engine.resolve_attack(states[0], states[2], attacker.attacks[0])
|
||||
assert result.ac == 13 # melee ignores soft cover
|
||||
assert result.hit is True # 12 + 2 = 14 >= 13
|
||||
|
||||
|
||||
def test_ranged_soft_cover_shown_in_transcript() -> None:
|
||||
"""Soft cover raises the AC displayed in the attack log line."""
|
||||
archer = make_combatant("archer", attack_bonus=4, kind="ranged", range_increment_ft=60)
|
||||
blocker = make_combatant("blocker", speed=0)
|
||||
target = make_combatant("target", speed=0)
|
||||
states = [
|
||||
make_state(archer, "players", (0, 0)),
|
||||
make_state(blocker, "players", (0, 2)),
|
||||
make_state(target, "monsters", (0, 4)),
|
||||
]
|
||||
engine = CombatEngine(ScriptedRng([10, 9, 8, 12]), make_grid(), states, round_cap=1)
|
||||
result = engine.run()
|
||||
assert result.transcript == (
|
||||
"initiative: archer d20=10+0=10",
|
||||
"initiative: blocker d20=9+0=9",
|
||||
"initiative: target d20=8+0=8",
|
||||
"round 1 archer: short bow vs target d20=12+4=16 AC 17 -> MISS",
|
||||
"round 1 blocker: wait",
|
||||
"round 1 target: wait",
|
||||
"battle over: draw after 1 rounds",
|
||||
)
|
||||
|
||||
@@ -88,3 +88,31 @@ def test_cover_melee_uses_same_corner_rule_in_phase0() -> None:
|
||||
def test_cover_same_square_is_false() -> None:
|
||||
grid = make_grid(["."])
|
||||
assert has_cover(grid, (0, 0), (0, 0), ranged=True) is False
|
||||
|
||||
|
||||
def test_soft_cover_creature_between_grants_cover() -> None:
|
||||
"""A creature between shooter and target grants cover on ranged attacks."""
|
||||
grid = make_grid([".....", ".....", "....."])
|
||||
occupied = frozenset({(1, 2)})
|
||||
assert has_cover(grid, (1, 0), (1, 4), ranged=True, occupied=occupied) is True
|
||||
|
||||
|
||||
def test_soft_cover_melee_ignores_creatures() -> None:
|
||||
"""Melee attacks ignore creatures for soft cover (CRB: ranged only)."""
|
||||
grid = make_grid([".....", ".....", "....."])
|
||||
occupied = frozenset({(1, 2)})
|
||||
assert has_cover(grid, (1, 0), (1, 4), ranged=False, occupied=occupied) is False
|
||||
|
||||
|
||||
def test_soft_cover_creature_behind_target_ignored() -> None:
|
||||
"""A creature behind the target does not grant cover."""
|
||||
grid = make_grid(["......", "......"])
|
||||
occupied = frozenset({(1, 5)})
|
||||
assert has_cover(grid, (1, 0), (1, 4), ranged=True, occupied=occupied) is False
|
||||
|
||||
|
||||
def test_soft_cover_creature_off_axis_ignored() -> None:
|
||||
"""A creature off the attack line does not grant cover."""
|
||||
grid = make_grid([".....", "....."])
|
||||
occupied = frozenset({(0, 2)})
|
||||
assert has_cover(grid, (1, 0), (1, 4), ranged=True, occupied=occupied) is False
|
||||
|
||||
Reference in New Issue
Block a user