Mi2dRPGamEng/tests/test_ranged_combat.py

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