import pytest from engine.entity import Entity, EntityPosition from engine.map import Map from engine.tile import Tile, TileLayerDef, TileLayerInstance from engine.world import World 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 make_world(): parent = Map("world", 10, 10, 1) for x in range(10): for y in range(10): parent.set_tile(x, y, 0, Tile(flooring=TileLayerInstance("grass"))) ship = Map("ship", 4, 3, 1) for x in range(4): for y in range(3): ship.set_tile(x, y, 0, Tile(flooring=TileLayerInstance("wood_deck"))) for x, y in [(0, 0), (1, 0), (3, 0), (0, 1), (3, 1), (0, 2), (1, 2), (2, 2), (3, 2)]: ship.get_tile(x, y, 0).room = TileLayerInstance("wood_wall") # (2,0) is the open doorway parent.embed(ship, (6, 6, 0)) world = World() world.maps = {"world": parent, "ship": ship} world.registry = FakeRegistry() return world, parent, ship def test_continuous_move_lands_at_a_fractional_position(): world, _parent, _ship = make_world() player = Entity(position=EntityPosition("world", 2.0, 2.0, 0)) assert world.try_move_continuous(player, 1, 0, 0, distance=0.35) is True assert player.position.x == pytest.approx(2.35) assert player.position.y == pytest.approx(2.0) def test_continuous_move_normalizes_diagonal_direction(): world, _parent, _ship = make_world() player = Entity(position=EntityPosition("world", 2.0, 2.0, 0)) assert world.try_move_continuous(player, 1, 1, 0, distance=1.0) is True # moved exactly 1 unit total, split evenly between x and y (not 1 in each axis) import math dx = player.position.x - 2.0 dy = player.position.y - 2.0 assert math.hypot(dx, dy) == pytest.approx(1.0) assert dx == pytest.approx(dy) def test_continuous_move_blocked_by_wall_leaves_position_unchanged(): world, _parent, ship = make_world() # ship-local (0,0) is a wood_wall; approach it from inside at (1.2,1.2) moving toward it player = Entity(position=EntityPosition("ship", 1.2, 1.2, 0)) assert world.try_move_continuous(player, -1, -1, 0, distance=1.5) is False assert player.position.x == pytest.approx(1.2) assert player.position.y == pytest.approx(1.2) def test_continuous_move_crosses_into_embedded_ship_at_fractional_local_position(): world, _parent, ship = make_world() # doorway column is parent x=8 (ship-local x=2); approach from just north of it player = Entity(position=EntityPosition("world", 8.3, 5.6, 0)) assert world.try_move_continuous(player, 0, 1, 0, distance=0.6) is True assert player.position.map_id == "ship" assert player.position.x == pytest.approx(2.3) assert player.position.y == pytest.approx(0.2) def test_continuous_move_disembarks_at_fractional_parent_position(): world, _parent, ship = make_world() player = Entity(position=EntityPosition("ship", 2.3, 0.2, 0)) assert world.try_move_continuous(player, 0, -1, 0, distance=0.6) is True assert player.position.map_id == "world" assert player.position.x == pytest.approx(8.3) assert player.position.y == pytest.approx(5.6) def test_entity_at_matches_by_tile_membership_for_fractional_positions(): world, _parent, _ship = make_world() entity = Entity(position=EntityPosition("world", 3.7, 4.2, 0)) world.add_entity(entity) assert world.entity_at("world", 3, 4, 0) is entity assert world.entity_at("world", 4, 4, 0) is None def test_rotate_position_round_trips_through_a_rotated_embedding(): world, _parent, ship = make_world() embedding = ship.parent_embedding embedding.rotate_to(1) for lx, ly in [(0.5, 0.5), (2.7, 1.1), (3.99, 0.01)]: px, py, pz = embedding.local_to_parent_pos(lx, ly, 0) rx, ry, rz = embedding.parent_to_local_pos(px, py, pz) assert rx == pytest.approx(lx) assert ry == pytest.approx(ly) def test_continuous_move_crosses_into_rotated_embedded_ship(): world, _parent, ship = make_world() embedding = ship.parent_embedding embedding.rotate_to(1) # 90 deg CW: 4x3 footprint becomes 3x4 in parent space # find where the doorway (ship-local 2,0) lands post-rotation, approach from just outside it door_parent = embedding.local_to_parent(2, 0, 0) start_x = door_parent[0] + 0.5 - 1 # one tile back along x, offset into the tile start_y = door_parent[1] + 0.5 player = Entity(position=EntityPosition("world", start_x, start_y, 0)) assert world.try_move_continuous(player, 1, 0, 0, distance=1.0) is True assert player.position.map_id == "ship"