from pathlib import Path import pytest from engine.character import Character from engine.defs import DefRegistry from engine.entity import EntityPosition from engine.map_loader import MapLoader from engine.tile import TileLayerInstance from engine.world import World from resources.logic.steering import advance_all_ship_rotations, advance_ship_rotation, at_helm, try_steer_ship, try_turn_ship 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 build_demo_world(registry) -> World: loader = MapLoader(DEFS_DIR / "maps") demo_world = loader.load("demo_world.json") ship = demo_world.embeddings[0].child_map world = World(registry=registry) world.add_map(demo_world) world.add_map(ship) return world def test_at_helm_true_when_standing_on_the_helm_tile(registry): world = build_demo_world(registry) sailor = make_human(registry, position=EntityPosition("ship_small", 2, 1, 0)) embedding = at_helm(sailor, world) assert embedding is not None assert embedding.child_map.map_id == "ship_small" def test_at_helm_none_elsewhere_on_the_ship(registry): world = build_demo_world(registry) sailor = make_human(registry, position=EntityPosition("ship_small", 1, 1, 0)) assert at_helm(sailor, world) is None def test_at_helm_none_when_not_aboard_an_embedded_map(registry): world = build_demo_world(registry) person = make_human(registry, position=EntityPosition("demo_world", 1, 1, 0)) assert at_helm(person, world) is None def test_steering_moves_the_ship_and_leaves_aboard_entities_local_position_untouched(registry): world = build_demo_world(registry) ship = world.maps["ship_small"] embedding = ship.parent_embedding original_anchor = embedding.anchor helmsman = make_human(registry, position=EntityPosition("ship_small", 2, 1, 0)) passenger = make_human(registry, position=EntityPosition("ship_small", 1, 1, 0)) assert try_steer_ship(helmsman, world, 0, 1, 0) is True assert embedding.anchor == (original_anchor[0], original_anchor[1] + 1, original_anchor[2]) # nobody aboard needed to move - their local coordinates are exactly as before assert (helmsman.position.x, helmsman.position.y) == (2, 1) assert (passenger.position.x, passenger.position.y) == (1, 1) def test_steering_fails_if_destination_runs_into_something_unwalkable(registry): world = build_demo_world(registry) parent = world.maps["demo_world"] # ship's footprint is x:6-9, y:6-8; moving west by 1 would occupy x:5-8, y:6-8 - # put a wall right in that path parent.get_tile(5, 7, 0).room = TileLayerInstance("wood_wall") helmsman = make_human(registry, position=EntityPosition("ship_small", 2, 1, 0)) embedding = world.maps["ship_small"].parent_embedding before = embedding.anchor assert try_steer_ship(helmsman, world, -1, 0, 0) is False assert embedding.anchor == before def test_steering_fails_off_the_parent_map_edge(registry): world = build_demo_world(registry) embedding = world.maps["ship_small"].parent_embedding embedding.move_to((0, 0, 0)) # right at the parent's edge helmsman = make_human(registry, position=EntityPosition("ship_small", 2, 1, 0)) assert try_steer_ship(helmsman, world, -1, 0, 0) is False def test_turning_the_ship_sets_desired_rotation_but_not_the_actual_one_yet(registry): world = build_demo_world(registry) embedding = world.maps["ship_small"].parent_embedding helmsman = make_human(registry, position=EntityPosition("ship_small", 2, 1, 0)) assert try_turn_ship(helmsman, world, 1) is True assert embedding.desired_rotation == 1 assert embedding.rotation == 0 # doesn't snap - advance_ship_rotation steps it there def test_repeated_turn_commands_stack_the_desired_rotation(registry): world = build_demo_world(registry) embedding = world.maps["ship_small"].parent_embedding helmsman = make_human(registry, position=EntityPosition("ship_small", 2, 1, 0)) try_turn_ship(helmsman, world, 1) try_turn_ship(helmsman, world, 1) assert embedding.desired_rotation == 2 def test_advance_ship_rotation_steps_one_quarter_turn_at_a_time(registry): world = build_demo_world(registry) embedding = world.maps["ship_small"].parent_embedding helmsman = make_human(registry, position=EntityPosition("ship_small", 2, 1, 0)) embedding.max_rotation_speed = 90.0 # 1 quarter-turn/second try_turn_ship(helmsman, world, 2) # desired_rotation = 2, two steps needed assert advance_ship_rotation(embedding, world, now=0.0) is True assert embedding.rotation == 1 # not enough time has passed yet for the second step assert advance_ship_rotation(embedding, world, now=0.5) is False assert embedding.rotation == 1 assert advance_ship_rotation(embedding, world, now=1.0) is True assert embedding.rotation == 2 def test_advance_ship_rotation_takes_the_shorter_direction(registry): world = build_demo_world(registry) embedding = world.maps["ship_small"].parent_embedding embedding.desired_rotation = 3 advance_ship_rotation(embedding, world, now=0.0) assert embedding.rotation == 3 # one counter-clockwise step, not three clockwise ones def test_advance_ship_rotation_no_op_when_already_at_desired(registry): world = build_demo_world(registry) embedding = world.maps["ship_small"].parent_embedding assert advance_ship_rotation(embedding, world, now=0.0) is False assert embedding.rotation == 0 def test_slower_max_rotation_speed_takes_longer_per_step(registry): world = build_demo_world(registry) embedding = world.maps["ship_small"].parent_embedding embedding.max_rotation_speed = 30.0 # 3 seconds per quarter-turn embedding.desired_rotation = 1 assert advance_ship_rotation(embedding, world, now=0.0) is True assert embedding.rotation == 1 assert embedding.rotation_ready_at == pytest.approx(3.0) def test_advance_ship_rotation_pauses_if_blocked_but_keeps_desired_heading(registry): world = build_demo_world(registry) parent = world.maps["demo_world"] ship = world.maps["ship_small"] embedding = ship.parent_embedding # block the intermediate rotated footprint so the step can't complete for x in range(parent.width): for y in range(parent.height): parent.get_tile(x, y, 0).room = TileLayerInstance("wood_wall") embedding.desired_rotation = 1 assert advance_ship_rotation(embedding, world, now=0.0) is False assert embedding.rotation == 0 assert embedding.desired_rotation == 1 # still wants to get there, just blocked for now def test_advance_all_ship_rotations_advances_every_embedding(registry): world = build_demo_world(registry) embedding = world.maps["ship_small"].parent_embedding embedding.desired_rotation = 1 advance_all_ship_rotations(world, now=0.0) assert embedding.rotation == 1 def test_non_helmsman_cannot_steer(registry): world = build_demo_world(registry) embedding = world.maps["ship_small"].parent_embedding before = embedding.anchor bystander = make_human(registry, position=EntityPosition("ship_small", 1, 1, 0)) assert try_steer_ship(bystander, world, 1, 0, 0) is False assert embedding.anchor == before