from pathlib import Path import pytest from engine.character import BodyPart, 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 TimeWarpZone, World from resources.logic.abilities import cast_time_warp_sphere from resources.logic.actions import activate_hand 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 # --- TimeWarpZone / World plumbing -------------------------------------------------------- def test_zone_contains_checks_radius_and_map_and_layer(): zone = TimeWarpZone(map_id="field", center=(5.0, 5.0, 0.0), radius=2.0, speed_factor=0.3, expires_at=10.0) assert zone.contains("field", 6.0, 5.0, 0.0) is True assert zone.contains("field", 8.0, 5.0, 0.0) is False # outside radius assert zone.contains("other_map", 6.0, 5.0, 0.0) is False assert zone.contains("field", 6.0, 5.0, 1.0) is False # different layer def test_zone_affects_respects_whitelist(): zone = TimeWarpZone( map_id="field", center=(5.0, 5.0, 0.0), radius=2.0, speed_factor=0.3, expires_at=10.0, immune_entity_ids={"caster-1"}, ) assert zone.affects("caster-1") is False assert zone.affects("anyone-else") is True def test_speed_multiplier_at_applies_zone_factor_inside_radius(registry): world = make_world(registry) world.time_warp_zones.append( TimeWarpZone(map_id="field", center=(5.0, 5.0, 0.0), radius=2.0, speed_factor=0.25, expires_at=10.0) ) assert world.speed_multiplier_at("someone", "field", 5.5, 5.0, 0.0, now=1.0) == pytest.approx(0.25) def test_speed_multiplier_at_is_1_outside_radius(registry): world = make_world(registry) world.time_warp_zones.append( TimeWarpZone(map_id="field", center=(5.0, 5.0, 0.0), radius=2.0, speed_factor=0.25, expires_at=10.0) ) assert world.speed_multiplier_at("someone", "field", 10.0, 5.0, 0.0, now=1.0) == 1.0 def test_speed_multiplier_at_ignores_expired_zones(registry): world = make_world(registry) world.time_warp_zones.append( TimeWarpZone(map_id="field", center=(5.0, 5.0, 0.0), radius=2.0, speed_factor=0.25, expires_at=10.0) ) assert world.speed_multiplier_at("someone", "field", 5.5, 5.0, 0.0, now=11.0) == 1.0 def test_speed_multiplier_at_1_for_whitelisted_entity(registry): world = make_world(registry) world.time_warp_zones.append( TimeWarpZone( map_id="field", center=(5.0, 5.0, 0.0), radius=2.0, speed_factor=0.25, expires_at=10.0, immune_entity_ids={"caster-1"}, ) ) assert world.speed_multiplier_at("caster-1", "field", 5.5, 5.0, 0.0, now=1.0) == 1.0 def test_multiple_overlapping_zones_multiply_together(registry): world = make_world(registry) world.time_warp_zones.append( TimeWarpZone(map_id="field", center=(5.0, 5.0, 0.0), radius=3.0, speed_factor=0.5, expires_at=10.0) ) world.time_warp_zones.append( TimeWarpZone(map_id="field", center=(5.0, 5.0, 0.0), radius=3.0, speed_factor=0.5, expires_at=10.0) ) assert world.speed_multiplier_at("someone", "field", 5.0, 5.0, 0.0, now=1.0) == pytest.approx(0.25) def test_expire_time_warp_zones_removes_only_expired(): world = World() active = TimeWarpZone(map_id="field", center=(0, 0, 0), radius=1, speed_factor=0.5, expires_at=10.0) expired = TimeWarpZone(map_id="field", center=(0, 0, 0), radius=1, speed_factor=0.5, expires_at=5.0) world.time_warp_zones = [active, expired] world.expire_time_warp_zones(now=6.0) assert world.time_warp_zones == [active] # --- cast_time_warp_sphere ------------------------------------------------------------------ def test_cast_time_warp_sphere_defaults_center_to_caster_position(registry): world = make_world(registry) caster = make_human(registry, position=EntityPosition("field", 4.0, 6.0, 0.0)) zone = cast_time_warp_sphere(caster, world, now=0.0) assert zone.center == (4.0, 6.0, 0.0) assert zone in world.time_warp_zones def test_cast_time_warp_sphere_caster_is_always_immune(registry): world = make_world(registry) caster = make_human(registry, position=EntityPosition("field", 4.0, 6.0, 0.0)) zone = cast_time_warp_sphere(caster, world, now=0.0) assert caster.entity_id in zone.immune_entity_ids def test_cast_time_warp_sphere_respects_explicit_whitelist(registry): world = make_world(registry) caster = make_human(registry, position=EntityPosition("field", 4.0, 6.0, 0.0)) zone = cast_time_warp_sphere(caster, world, now=0.0, whitelist={"ally-1"}) assert "ally-1" in zone.immune_entity_ids assert caster.entity_id in zone.immune_entity_ids # still auto-added def test_cast_time_warp_sphere_all_parameters_are_configurable(registry): world = make_world(registry) caster = make_human(registry, position=EntityPosition("field", 4.0, 6.0, 0.0)) zone = cast_time_warp_sphere( caster, world, now=2.0, center=(1.0, 1.0, 0.0), radius=7.0, speed_factor=0.1, duration=12.0 ) assert zone.center == (1.0, 1.0, 0.0) assert zone.radius == 7.0 assert zone.speed_factor == pytest.approx(0.1) assert zone.expires_at == pytest.approx(14.0) def test_time_warp_sphere_slows_a_non_whitelisted_entity_but_not_the_caster(registry): world = make_world(registry) caster = make_human(registry, position=EntityPosition("field", 5.0, 5.0, 0.0)) victim = make_human(registry, position=EntityPosition("field", 6.0, 5.0, 0.0)) world.add_entity(caster) world.add_entity(victim) cast_time_warp_sphere(caster, world, now=0.0, radius=3.0, speed_factor=0.2, duration=5.0) caster_mult = world.speed_multiplier_at(caster.entity_id, "field", 5.0, 5.0, 0.0, now=1.0) victim_mult = world.speed_multiplier_at(victim.entity_id, "field", 6.0, 5.0, 0.0, now=1.0) assert caster_mult == 1.0 assert victim_mult == pytest.approx(0.2) # --- item attachment point (chronowand) via activate_hand ----------------------------------- def test_chronowand_casts_time_warp_sphere_via_activate_hand(registry): world = make_world(registry) caster = make_human(registry, position=EntityPosition("field", 5.0, 5.0, 0.0)) caster.wield_item("right_hand", "chronowand", registry) world.add_entity(caster) assert world.time_warp_zones == [] result = activate_hand(caster, world, registry, "right_hand", (1, 0, 0), now=3.0) assert len(world.time_warp_zones) == 1 assert result is world.time_warp_zones[0] assert result.expires_at > 3.0 def test_species_def_carries_time_warp_sphere_ability(registry): species = registry.get_species_def("human") ability = next(a for a in species.abilities if a.id == "time_warp_sphere") assert ability.required_slots == ("head",) def test_can_perform_ability_gates_time_warp_sphere_on_head(registry): intact = make_human(registry) assert intact.can_perform_ability("time_warp_sphere", registry) is True headless = make_human(registry, body_parts=[BodyPart("head", "human_head", removed=True)]) assert headless.can_perform_ability("time_warp_sphere", registry) is False