Mi2dRPGamEng/tests/test_time_warp.py

192 lines
7.5 KiB
Python

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