159 lines
6.3 KiB
Python
159 lines
6.3 KiB
Python
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from engine.character import Character
|
|
from engine.defs import DefRegistry
|
|
from engine.entity import EntityPosition
|
|
from engine.map import Map
|
|
from engine.tile import Tile, TileLayerInstance
|
|
from engine.world import World
|
|
from resources.logic.combat import find_ranged_target, perform_shoot
|
|
|
|
DEFS_DIR = Path(__file__).resolve().parent.parent / "resources" / "defs"
|
|
|
|
|
|
@pytest.fixture()
|
|
def registry():
|
|
return DefRegistry.load(DEFS_DIR)
|
|
|
|
|
|
def make_human(registry, **kwargs) -> Character:
|
|
return Character(species_id="human", default_body_parts=registry.new_default_body_parts("human"), **kwargs)
|
|
|
|
|
|
def make_world(registry):
|
|
field = Map("field", 10, 10, 1)
|
|
for x in range(10):
|
|
for y in range(10):
|
|
field.set_tile(x, y, 0, Tile(flooring=TileLayerInstance("grass")))
|
|
world = World(registry=registry)
|
|
world.add_map(field)
|
|
return world, field
|
|
|
|
|
|
class ScriptedRng:
|
|
"""Deterministic stand-in: independent, ordered queues for randint() and choice() calls."""
|
|
|
|
def __init__(self, randints=None, choices=None):
|
|
self._randints = list(randints or [])
|
|
self._choices = list(choices or [])
|
|
|
|
def randint(self, _lo, _hi):
|
|
return self._randints.pop(0)
|
|
|
|
def choice(self, seq):
|
|
if self._choices:
|
|
return self._choices.pop(0)
|
|
return seq[0]
|
|
|
|
|
|
def test_find_ranged_target_hits_entity_in_line(registry):
|
|
world, field = make_world(registry)
|
|
shooter = make_human(registry, position=EntityPosition("field", 0, 5, 0))
|
|
target = make_human(registry, position=EntityPosition("field", 3, 5, 0))
|
|
world.add_entity(shooter)
|
|
world.add_entity(target)
|
|
|
|
found, impact, steps = find_ranged_target(world, "field", 0, 5, 0, 1, 0, 0, max_range=6)
|
|
assert found is target
|
|
assert impact == (3, 5, 0)
|
|
assert steps == 3
|
|
|
|
|
|
def test_find_ranged_target_stops_at_a_wall(registry):
|
|
world, field = make_world(registry)
|
|
field.get_tile(2, 5, 0).room = TileLayerInstance("wood_wall")
|
|
shooter = make_human(registry, position=EntityPosition("field", 0, 5, 0))
|
|
target = make_human(registry, position=EntityPosition("field", 5, 5, 0))
|
|
world.add_entity(shooter)
|
|
world.add_entity(target)
|
|
|
|
found, impact, _steps = find_ranged_target(world, "field", 0, 5, 0, 1, 0, 0, max_range=6)
|
|
assert found is None
|
|
assert impact == (2, 5, 0)
|
|
|
|
|
|
def test_find_ranged_target_respects_max_range(registry):
|
|
world, _field = make_world(registry)
|
|
shooter = make_human(registry, position=EntityPosition("field", 0, 5, 0))
|
|
target = make_human(registry, position=EntityPosition("field", 9, 5, 0))
|
|
world.add_entity(shooter)
|
|
world.add_entity(target)
|
|
|
|
found, _impact, _steps = find_ranged_target(world, "field", 0, 5, 0, 1, 0, 0, max_range=3)
|
|
assert found is None
|
|
|
|
|
|
def test_perform_shoot_hits_and_damages_target(registry):
|
|
world, _field = make_world(registry)
|
|
shooter = make_human(registry, position=EntityPosition("field", 0, 5, 0))
|
|
target = make_human(registry, position=EntityPosition("field", 3, 5, 0))
|
|
world.add_entity(shooter)
|
|
world.add_entity(target)
|
|
pistol = registry.get_item_def("pistol_basic")
|
|
|
|
result = perform_shoot(shooter, world, registry, pistol, (1, 0, 0), rng=ScriptedRng(randints=[15]))
|
|
assert result.hit is True
|
|
assert result.blocked is False
|
|
assert result.target_entity_id == target.entity_id
|
|
assert result.attack.target_slot is not None
|
|
assert target.get_body_part(result.attack.target_slot).integrity < 100.0
|
|
|
|
|
|
def test_shield_blocks_matching_projectile_type_without_ricochet_target(registry):
|
|
world, _field = make_world(registry)
|
|
shooter = make_human(registry, position=EntityPosition("field", 0, 5, 0))
|
|
blocker = make_human(registry, position=EntityPosition("field", 3, 5, 0), is_blocking=True)
|
|
blocker.wield_item("left_hand", "wooden_shield", registry)
|
|
world.add_entity(shooter)
|
|
world.add_entity(blocker)
|
|
bow = registry.get_item_def("bow_basic") # projectile_type "arrow", which wooden_shield blocks
|
|
|
|
# ricochet direction (choice=1 -> deflect south) finds nothing there, so the bounce fizzles
|
|
result = perform_shoot(shooter, world, registry, bow, (1, 0, 0), rng=ScriptedRng(choices=[1]))
|
|
assert result.blocked is True
|
|
assert result.ricocheted is True
|
|
assert result.hit is False
|
|
assert result.target_entity_id is None # nothing found along the ricochet path
|
|
|
|
|
|
def test_shield_blocks_and_ricochet_hits_a_second_target(registry):
|
|
world, _field = make_world(registry)
|
|
shooter = make_human(registry, position=EntityPosition("field", 0, 5, 0))
|
|
blocker = make_human(registry, position=EntityPosition("field", 3, 5, 0), is_blocking=True)
|
|
blocker.wield_item("left_hand", "wooden_shield", registry)
|
|
bystander = make_human(registry, position=EntityPosition("field", 3, 7, 0)) # 2 tiles south of the blocker
|
|
world.add_entity(shooter)
|
|
world.add_entity(blocker)
|
|
world.add_entity(bystander)
|
|
bow = registry.get_item_def("bow_basic")
|
|
|
|
result = perform_shoot(shooter, world, registry, bow, (1, 0, 0), rng=ScriptedRng(randints=[15], choices=[1]))
|
|
assert result.blocked is True
|
|
assert result.ricocheted is True
|
|
assert result.target_entity_id == bystander.entity_id
|
|
assert result.hit is True
|
|
assert result.attack.target_slot is not None
|
|
assert bystander.get_body_part(result.attack.target_slot).integrity < 100.0
|
|
|
|
|
|
def test_non_matching_projectile_type_breaks_through_shield(registry):
|
|
world, _field = make_world(registry)
|
|
shooter = make_human(registry, position=EntityPosition("field", 0, 5, 0))
|
|
blocker = make_human(registry, position=EntityPosition("field", 3, 5, 0), is_blocking=True)
|
|
blocker.wield_item("left_hand", "wooden_shield", registry) # only blocks "arrow"
|
|
world.add_entity(shooter)
|
|
world.add_entity(blocker)
|
|
pistol = registry.get_item_def("pistol_basic") # projectile_type "bullet"
|
|
|
|
# blocker is still in a blocking stance, so the opposed check still applies even though the
|
|
# shield doesn't cover "bullet" - it just can't hard-block/ricochet this projectile type
|
|
result = perform_shoot(
|
|
shooter, world, registry, pistol, (1, 0, 0), rng=ScriptedRng(randints=[15, 1], choices=["torso"])
|
|
)
|
|
assert result.blocked is False
|
|
assert result.ricocheted is False
|
|
assert result.target_entity_id == blocker.entity_id
|
|
assert result.attack.blocked_check is True
|