Mi2dRPGamEng/tests/test_shooting.py

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