269 lines
11 KiB
Python
269 lines
11 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.inventory import Inventory
|
|
from engine.item import ItemInstance
|
|
from engine.map import Map
|
|
from engine.tile import Tile, TileLayerInstance
|
|
from engine.world import World
|
|
from resources.logic.ranged_combat import effective_aim_cone, fire_held_weapon, perform_explosion
|
|
|
|
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", 15, 15, 1)
|
|
for x in range(15):
|
|
for y in range(15):
|
|
field.set_tile(x, y, 0, Tile(flooring=TileLayerInstance("grass")))
|
|
world = World(registry=registry)
|
|
world.add_map(field)
|
|
return world, field
|
|
|
|
|
|
class FixedRng:
|
|
def __init__(self, roll=15, choice_value=None, uniform_value=0.0):
|
|
self._roll = roll
|
|
self._choice_value = choice_value
|
|
self._uniform_value = uniform_value
|
|
|
|
def randint(self, _lo, _hi):
|
|
return self._roll
|
|
|
|
def choice(self, seq):
|
|
return self._choice_value if self._choice_value is not None else seq[0]
|
|
|
|
def uniform(self, _a, _b):
|
|
return self._uniform_value
|
|
|
|
|
|
# --- effective_aim_cone -----------------------------------------------------------------
|
|
|
|
|
|
def test_effective_aim_cone_zero_for_unarmed_hand(registry):
|
|
character = make_human(registry)
|
|
assert effective_aim_cone(character, "right_hand", registry) == 0.0
|
|
|
|
|
|
def test_effective_aim_cone_zero_for_non_cone_weapon(registry):
|
|
character = make_human(registry)
|
|
character.wield_item("right_hand", "staff_oak", registry)
|
|
assert effective_aim_cone(character, "right_hand", registry) == 0.0
|
|
|
|
|
|
def test_effective_aim_cone_uses_weapon_base_cone(registry):
|
|
character = make_human(registry)
|
|
character.wield_item("right_hand", "rifle_heavy", registry)
|
|
rifle = registry.get_item_def("rifle_heavy")
|
|
assert effective_aim_cone(character, "right_hand", registry) == pytest.approx(rifle.aim_cone_degrees)
|
|
|
|
|
|
def test_effective_aim_cone_narrows_with_skill(registry):
|
|
character = make_human(registry)
|
|
character.wield_item("right_hand", "rifle_heavy", registry)
|
|
baseline = effective_aim_cone(character, "right_hand", registry)
|
|
character.bio["sleight_of_hand"] = 10
|
|
with_skill = effective_aim_cone(character, "right_hand", registry)
|
|
assert with_skill < baseline
|
|
|
|
|
|
def test_effective_aim_cone_narrows_with_attachments(registry):
|
|
character = make_human(registry)
|
|
character.wield_item("right_hand", "rifle_heavy", registry)
|
|
baseline = effective_aim_cone(character, "right_hand", registry)
|
|
character.attach_mod("right_hand", "scope_basic", registry)
|
|
with_scope = effective_aim_cone(character, "right_hand", registry)
|
|
assert with_scope == pytest.approx(baseline - 3.0)
|
|
|
|
|
|
def test_effective_aim_cone_never_below_minimum(registry):
|
|
character = make_human(registry)
|
|
character.wield_item("right_hand", "rifle_heavy", registry)
|
|
character.bio["sleight_of_hand"] = 1000 # absurdly high, should clamp not go negative
|
|
assert effective_aim_cone(character, "right_hand", registry) >= 1.0
|
|
|
|
|
|
# --- attach_mod / magazine_capacity / reload --------------------------------------------
|
|
|
|
|
|
def test_attach_mod_fails_on_non_ranged_weapon(registry):
|
|
character = make_human(registry)
|
|
character.wield_item("right_hand", "staff_oak", registry)
|
|
assert character.attach_mod("right_hand", "scope_basic", registry) is False
|
|
|
|
|
|
def test_attach_mod_succeeds_on_ranged_weapon(registry):
|
|
character = make_human(registry)
|
|
character.wield_item("right_hand", "rifle_heavy", registry)
|
|
assert character.attach_mod("right_hand", "scope_basic", registry) is True
|
|
assert "scope_basic" in character.get_held_item("right_hand").attachments
|
|
|
|
|
|
def test_magazine_capacity_includes_extended_mag_bonus(registry):
|
|
character = make_human(registry)
|
|
character.wield_item("right_hand", "rifle_heavy", registry)
|
|
base_capacity = character.magazine_capacity("right_hand", registry)
|
|
character.attach_mod("right_hand", "extended_mag", registry)
|
|
assert character.magazine_capacity("right_hand", registry) == base_capacity + 5
|
|
|
|
|
|
def test_reload_consumes_compatible_ammo_from_inventory(registry):
|
|
character = make_human(registry)
|
|
character.wield_item("right_hand", "rifle_heavy", registry)
|
|
character.inventory = Inventory(4, 4)
|
|
ammo_def = registry.get_item_def("ammo_heavy_caliber")
|
|
character.inventory.place(ItemInstance("ammo-1", "ammo_heavy_caliber", quantity=10), ammo_def, 0, 0)
|
|
|
|
assert character.reload("right_hand", registry) is True
|
|
held = character.get_held_item("right_hand")
|
|
assert held.loaded_ammo_type == "heavy_caliber"
|
|
assert held.loaded_ammo_count == 5 # rifle_heavy magazine_size
|
|
remaining = next(p for p in character.inventory.items() if p.item.item_id == "ammo-1")
|
|
assert remaining.item.quantity == 5
|
|
|
|
|
|
def test_reload_fails_without_compatible_ammo(registry):
|
|
character = make_human(registry)
|
|
character.wield_item("right_hand", "rifle_heavy", registry)
|
|
character.inventory = Inventory(4, 4)
|
|
shell_def = registry.get_item_def("ammo_shotgun_shell")
|
|
character.inventory.place(ItemInstance("ammo-1", "ammo_shotgun_shell", quantity=10), shell_def, 0, 0)
|
|
assert character.reload("right_hand", registry) is False
|
|
|
|
|
|
def test_reload_fails_when_magazine_already_full(registry):
|
|
character = make_human(registry)
|
|
character.wield_item("right_hand", "rifle_heavy", registry)
|
|
character.inventory = Inventory(4, 4)
|
|
ammo_def = registry.get_item_def("ammo_heavy_caliber")
|
|
character.inventory.place(ItemInstance("ammo-1", "ammo_heavy_caliber", quantity=20), ammo_def, 0, 0)
|
|
character.reload("right_hand", registry)
|
|
assert character.reload("right_hand", registry) is False
|
|
|
|
|
|
def test_reload_fails_for_non_ammo_weapon(registry):
|
|
character = make_human(registry)
|
|
character.wield_item("right_hand", "staff_oak", registry)
|
|
character.inventory = Inventory(4, 4)
|
|
assert character.reload("right_hand", registry) is False
|
|
|
|
|
|
# --- fire_held_weapon --------------------------------------------------------------------
|
|
|
|
|
|
def test_fire_held_weapon_fails_when_unloaded(registry):
|
|
world, _field = make_world(registry)
|
|
shooter = make_human(registry, position=EntityPosition("field", 5, 5, 0))
|
|
shooter.wield_item("right_hand", "rifle_heavy", registry)
|
|
world.add_entity(shooter)
|
|
assert fire_held_weapon(shooter, world, registry, "right_hand", (1, 0, 0)) is None
|
|
|
|
|
|
def test_fire_held_weapon_decrements_ammo_and_hits_target(registry):
|
|
world, _field = make_world(registry)
|
|
shooter = make_human(registry, position=EntityPosition("field", 0, 5, 0))
|
|
shooter.wield_item("right_hand", "rifle_heavy", registry)
|
|
shooter.inventory = Inventory(4, 4)
|
|
ammo_def = registry.get_item_def("ammo_heavy_caliber")
|
|
shooter.inventory.place(ItemInstance("ammo-1", "ammo_heavy_caliber", quantity=5), ammo_def, 0, 0)
|
|
shooter.reload("right_hand", registry)
|
|
|
|
target = make_human(registry, position=EntityPosition("field", 3, 5, 0))
|
|
world.add_entity(shooter)
|
|
world.add_entity(target)
|
|
|
|
result = fire_held_weapon(shooter, world, registry, "right_hand", (1, 0, 0), rng=FixedRng(roll=15))
|
|
assert shooter.get_held_item("right_hand").loaded_ammo_count == 4
|
|
assert result.hit is True
|
|
assert result.target_entity_id == target.entity_id
|
|
|
|
|
|
def test_fire_held_weapon_shotgun_fires_multiple_pellets(registry):
|
|
world, _field = make_world(registry)
|
|
shooter = make_human(registry, position=EntityPosition("field", 0, 5, 0))
|
|
shooter.wield_item("right_hand", "shotgun_basic", registry)
|
|
shooter.inventory = Inventory(4, 4)
|
|
shell_def = registry.get_item_def("ammo_shotgun_shell")
|
|
shooter.inventory.place(ItemInstance("shells-1", "ammo_shotgun_shell", quantity=5), shell_def, 0, 0)
|
|
shooter.reload("right_hand", registry)
|
|
world.add_entity(shooter)
|
|
|
|
result = fire_held_weapon(shooter, world, registry, "right_hand", (1, 0, 0), rng=FixedRng(roll=15))
|
|
assert len(result.pellets) == 6 # shotgun_basic pellet_count
|
|
|
|
|
|
def test_fire_held_weapon_shotgun_slug_fires_single_projectile(registry):
|
|
world, _field = make_world(registry)
|
|
shooter = make_human(registry, position=EntityPosition("field", 0, 5, 0))
|
|
shooter.wield_item("right_hand", "shotgun_basic", registry)
|
|
shooter.inventory = Inventory(4, 4)
|
|
slug_def = registry.get_item_def("ammo_shotgun_slug")
|
|
shooter.inventory.place(ItemInstance("slugs-1", "ammo_shotgun_slug", quantity=5), slug_def, 0, 0)
|
|
shooter.reload("right_hand", registry)
|
|
world.add_entity(shooter)
|
|
|
|
result = fire_held_weapon(shooter, world, registry, "right_hand", (1, 0, 0), rng=FixedRng(roll=15))
|
|
assert len(result.pellets) == 1
|
|
|
|
|
|
def test_fire_held_weapon_rocket_launcher_explodes(registry):
|
|
world, _field = make_world(registry)
|
|
shooter = make_human(registry, position=EntityPosition("field", 0, 5, 0))
|
|
shooter.wield_item("right_hand", "rocket_launcher", registry)
|
|
shooter.inventory = Inventory(4, 4)
|
|
rocket_def = registry.get_item_def("ammo_rocket")
|
|
shooter.inventory.place(ItemInstance("rockets-1", "ammo_rocket", quantity=2), rocket_def, 0, 0)
|
|
shooter.reload("right_hand", registry)
|
|
target = make_human(registry, position=EntityPosition("field", 6, 5, 0))
|
|
world.add_entity(shooter)
|
|
world.add_entity(target)
|
|
|
|
result = fire_held_weapon(shooter, world, registry, "right_hand", (1, 0, 0), rng=FixedRng(roll=15))
|
|
assert result.impact is not None
|
|
assert any(target is t for t, _dmg in result.hit_entities)
|
|
|
|
|
|
# --- perform_explosion --------------------------------------------------------------------
|
|
|
|
|
|
def test_perform_explosion_damages_entities_with_falloff_by_distance(registry):
|
|
world, field = make_world(registry)
|
|
close = make_human(registry, position=EntityPosition("field", 5, 5, 0))
|
|
far = make_human(registry, position=EntityPosition("field", 7, 5, 0))
|
|
world.add_entity(close)
|
|
world.add_entity(far)
|
|
|
|
result = perform_explosion(world, "field", (5, 5, 0), radius=2, base_damage=30.0)
|
|
hit_map = {id(t): dmg for t, dmg in result.hit_entities}
|
|
close_damage = hit_map[id(close)]
|
|
far_damage = hit_map[id(far)]
|
|
assert close_damage > far_damage
|
|
|
|
|
|
def test_perform_explosion_damages_walls_within_radius(registry):
|
|
world, field = make_world(registry)
|
|
field.get_tile(6, 5, 0).room = TileLayerInstance("wood_wall")
|
|
perform_explosion(world, "field", (5, 5, 0), radius=2, base_damage=200.0)
|
|
# 200 damage with falloff should be enough to destroy a wall within radius 1
|
|
assert field.get_tile(6, 5, 0).room is None
|
|
|
|
|
|
def test_perform_explosion_does_not_affect_tiles_outside_radius(registry):
|
|
world, field = make_world(registry)
|
|
field.get_tile(10, 5, 0).room = TileLayerInstance("wood_wall")
|
|
perform_explosion(world, "field", (5, 5, 0), radius=1, base_damage=200.0)
|
|
assert field.get_tile(10, 5, 0).room is not None
|