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