fix(rules): give Tiny-or-smaller creatures zero reach and square sharing
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
"""Tests for Tiny-or-smaller reach zero and square sharing (PF1e S4c).
|
||||
|
||||
SRD "Big and Little Creatures in Combat": Tiny, Diminutive and Fine creatures
|
||||
"take up less than 1 square of space", have "a natural reach of 0 feet", do not
|
||||
threaten the squares around them, and "must enter an opponent's square to
|
||||
attack" — so a Tiny-or-smaller creature and a strictly larger one may share a
|
||||
square (move through and end on it), while two creatures of the same (Small+)
|
||||
size never share.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pf1e_simulator.combat import _effective_reach_ft
|
||||
from tests.test_creature_sizes import make_combatant, make_engine, make_state
|
||||
|
||||
|
||||
class TestTinyReachZero:
|
||||
"""A Tiny creature whose melee attack has reach_ft 0 reaches only its cell."""
|
||||
|
||||
def test_threatens_no_squares(self) -> None:
|
||||
tiny = make_combatant("tiny", size="Tiny", reach_ft=0)
|
||||
foe = make_combatant("foe", size="Medium")
|
||||
s_tiny = make_state(tiny, "a", (3, 3))
|
||||
s_foe = make_state(foe, "b", (3, 4))
|
||||
eng = make_engine([s_tiny, s_foe])
|
||||
assert eng.threatened_squares(s_tiny) == frozenset()
|
||||
|
||||
def test_attacks_a_creature_in_its_own_cell(self) -> None:
|
||||
tiny = make_combatant("tiny", size="Tiny", reach_ft=0)
|
||||
foe = make_combatant("foe", size="Medium")
|
||||
s_tiny = make_state(tiny, "a", (3, 3))
|
||||
s_foe = make_state(foe, "b", (3, 3)) # sharing the square
|
||||
eng = make_engine([s_tiny, s_foe])
|
||||
assert eng.weapon_for(s_tiny, s_foe) is not None
|
||||
|
||||
def test_cannot_reach_an_adjacent_creature(self) -> None:
|
||||
tiny = make_combatant("tiny", size="Tiny", reach_ft=0)
|
||||
foe = make_combatant("foe", size="Medium")
|
||||
s_tiny = make_state(tiny, "a", (3, 3))
|
||||
s_foe = make_state(foe, "b", (3, 4))
|
||||
eng = make_engine([s_tiny, s_foe])
|
||||
assert eng.weapon_for(s_tiny, s_foe) is None
|
||||
|
||||
def test_authored_reach_is_kept(self) -> None:
|
||||
# A natural attack authored with reach_ft 5 (e.g. ierlieth's bite)
|
||||
# overrides the size default: the creature reaches and threatens 5 ft.
|
||||
tiny = make_combatant("tiny", size="Tiny", reach_ft=5)
|
||||
foe = make_combatant("foe", size="Medium")
|
||||
s_tiny = make_state(tiny, "a", (3, 3))
|
||||
s_foe = make_state(foe, "b", (3, 4))
|
||||
eng = make_engine([s_tiny, s_foe])
|
||||
assert _effective_reach_ft(s_tiny, tiny.attacks[0]) == 5
|
||||
assert (3, 4) in eng.threatened_squares(s_tiny)
|
||||
assert eng.weapon_for(s_tiny, s_foe) is not None
|
||||
|
||||
|
||||
class TestCoOccupancy:
|
||||
"""A Tiny-or-smaller creature shares a square with a strictly larger one."""
|
||||
|
||||
def test_tiny_may_end_on_a_larger_creature_cell(self) -> None:
|
||||
tiny = make_combatant("tiny", size="Tiny", reach_ft=0)
|
||||
foe = make_combatant("foe", size="Medium")
|
||||
s_tiny = make_state(tiny, "a", (3, 3))
|
||||
s_foe = make_state(foe, "b", (3, 4))
|
||||
eng = make_engine([s_tiny, s_foe])
|
||||
assert eng._destination_reachable(s_tiny, (3, 4)) is True
|
||||
|
||||
def test_larger_may_end_on_a_tiny_cell(self) -> None:
|
||||
tiny = make_combatant("tiny", size="Tiny", reach_ft=0)
|
||||
foe = make_combatant("foe", size="Medium")
|
||||
s_tiny = make_state(tiny, "a", (3, 4))
|
||||
s_foe = make_state(foe, "b", (3, 3))
|
||||
eng = make_engine([s_tiny, s_foe])
|
||||
assert eng._destination_reachable(s_foe, (3, 4)) is True
|
||||
|
||||
def test_two_medium_never_share(self) -> None:
|
||||
m1 = make_combatant("m1", size="Medium")
|
||||
m2 = make_combatant("m2", size="Medium")
|
||||
s1 = make_state(m1, "a", (3, 3))
|
||||
s2 = make_state(m2, "b", (3, 4))
|
||||
eng = make_engine([s1, s2])
|
||||
assert eng._destination_reachable(s1, (3, 4)) is False
|
||||
assert eng._cells_free(s2.occupied_cells(), s1) is False
|
||||
|
||||
def test_tiny_may_cross_a_larger_hostile_cell(self) -> None:
|
||||
tiny = make_combatant("tiny", size="Tiny", reach_ft=0)
|
||||
foe = make_combatant("foe", size="Medium")
|
||||
s_tiny = make_state(tiny, "a", (3, 3))
|
||||
s_foe = make_state(foe, "b", (3, 4))
|
||||
eng = make_engine([s_tiny, s_foe])
|
||||
assert eng._cells_walkable(s_foe.occupied_cells(), s_tiny) is True
|
||||
|
||||
def test_medium_cannot_cross_a_medium_hostile_cell(self) -> None:
|
||||
m1 = make_combatant("m1", size="Medium")
|
||||
m2 = make_combatant("m2", size="Medium")
|
||||
s1 = make_state(m1, "a", (3, 3))
|
||||
s2 = make_state(m2, "b", (3, 4))
|
||||
eng = make_engine([s1, s2])
|
||||
assert eng._cells_walkable(s2.occupied_cells(), s1) is False
|
||||
Reference in New Issue
Block a user