from engine.entity import Entity, EntityPosition from engine.map import Map from engine.tile import Tile, TileLayerDef, TileLayerInstance from engine.world import World def make_world(): parent = Map("world", 10, 10, 1) child = Map("ship", 4, 3, 1) for x in range(4): for y in range(3): child.set_tile(x, y, 0, Tile(flooring=TileLayerInstance("wood_deck"))) # leave (2, 0, 0) open as the doorway, wall the rest of the front/back/sides for x, y in [(0, 0), (1, 0), (3, 0), (0, 2), (3, 2)]: tile = child.get_tile(x, y, 0) tile.room = TileLayerInstance("wood_wall") for x in range(10): for y in range(10): parent.set_tile(x, y, 0, Tile(flooring=TileLayerInstance("grass"))) parent.embed(child, (6, 6, 0)) return parent, child def test_local_to_parent_and_back_are_inverses(): _parent, child = make_world() embedding = child.parent_embedding for lx, ly, lz in [(0, 0, 0), (2, 0, 0), (3, 2, 0)]: px, py, pz = embedding.local_to_parent(lx, ly, lz) assert embedding.parent_to_local(px, py, pz) == (lx, ly, lz) def test_embedding_at_detects_child_footprint(): parent, child = make_world() embedding = child.parent_embedding assert parent.embedding_at(8, 6, 0) is embedding # inside ship footprint (6..9, 6..8) assert parent.embedding_at(0, 0, 0) is None # outside ship footprint def test_embed_defaults_max_rotation_speed_and_desired_rotation(): _parent, child = make_world() embedding = child.parent_embedding assert embedding.max_rotation_speed == 90.0 assert embedding.desired_rotation == embedding.rotation == 0 assert embedding.rotation_ready_at == 0.0 def test_embed_accepts_a_custom_max_rotation_speed(): parent = Map("world", 10, 10, 1) child = Map("ship", 2, 2, 1) embedding = parent.embed(child, (0, 0, 0), rotation=1, max_rotation_speed=45.0) assert embedding.max_rotation_speed == 45.0 assert embedding.rotation == embedding.desired_rotation == 1 class FakeRegistry: def get_tile_layer_def(self, layer, def_id): if layer == "room" and def_id == "wood_wall": return TileLayerDef(id="wood_wall", layer="room", walkable=False, blocks_los=True) return None def test_try_move_boards_ship_from_parent_map(): parent, child = make_world() world = World() world.maps = {"world": parent, "ship": child} world.registry = FakeRegistry() player = Entity(position=EntityPosition("world", 7, 5, 0)) # just below the doorway at (8,6) assert world.try_move(player, 1, 1, 0) is True assert player.position.map_id == "ship" assert (player.position.x, player.position.y, player.position.z) == (2, 0, 0) def test_try_move_disembarks_ship_back_to_parent_map(): parent, child = make_world() world = World() world.maps = {"world": parent, "ship": child} world.registry = FakeRegistry() # standing in the doorway (ship-local 2,0,0), step north off the ship's edge player = Entity(position=EntityPosition("ship", 2, 0, 0)) assert world.try_move(player, 0, -1, 0) is True assert player.position.map_id == "world" assert (player.position.x, player.position.y, player.position.z) == (8, 5, 0) def test_try_move_blocked_by_wall(): parent, child = make_world() world = World() world.maps = {"world": parent, "ship": child} world.registry = FakeRegistry() player = Entity(position=EntityPosition("ship", 1, 1, 0)) assert world.try_move(player, -1, -1, 0) is False # (0,0) is a wood_wall assert player.position.map_id == "ship" assert (player.position.x, player.position.y, player.position.z) == (1, 1, 0) def test_rotated_embedding_local_to_parent_and_back_are_inverses(): _parent, child = make_world() embedding = child.parent_embedding embedding.rotate_to(1) # 90 degrees clockwise: 4x3 footprint becomes 3x4 assert embedding.footprint_size() == (3, 4) for lx, ly, lz in [(0, 0, 0), (2, 0, 0), (3, 2, 0), (1, 1, 0)]: px, py, pz = embedding.local_to_parent(lx, ly, lz) assert embedding.parent_to_local(px, py, pz) == (lx, ly, lz) def test_boarding_still_works_after_ship_rotates(): parent, child = make_world() embedding = child.parent_embedding embedding.rotate_to(1) world = World() world.maps = {"world": parent, "ship": child} world.registry = FakeRegistry() # the doorway (ship-local 2,0) now lands at a different parent cell post-rotation door_parent = embedding.local_to_parent(2, 0, 0) approach = (door_parent[0] - 1, door_parent[1] - 1, 0) player = Entity(position=EntityPosition("world", *approach)) assert world.try_move(player, 1, 1, 0) is True assert player.position.map_id == "ship" assert (player.position.x, player.position.y, player.position.z) == (2, 0, 0) def test_moving_and_rotating_ship_does_not_touch_aboard_entities_local_position(): _parent, child = make_world() embedding = child.parent_embedding sailor = Entity(position=EntityPosition("ship", 2, 1, 0)) original_local = (sailor.position.x, sailor.position.y, sailor.position.z) embedding.move_to((0, 0, 0)) embedding.rotate_to(2) assert (sailor.position.x, sailor.position.y, sailor.position.z) == original_local def test_disembarking_resolves_against_current_anchor_after_ship_moves(): parent, child = make_world() embedding = child.parent_embedding world = World() world.maps = {"world": parent, "ship": child} world.registry = FakeRegistry() embedding.move_to((5, 5, 0)) # ship has sailed elsewhere since boarding player = Entity(position=EntityPosition("ship", 2, 0, 0)) assert world.try_move(player, 0, -1, 0) is True assert player.position.map_id == "world" # disembarks relative to the ship's *current* anchor, not the original (6,6,0) assert (player.position.x, player.position.y, player.position.z) == (7, 4, 0)