"""Movement commands for navigating the world.""" from mudlib.commands import CommandDefinition, register from mudlib.entity import Entity from mudlib.gmcp import send_map_data, send_room_info from mudlib.player import Player from mudlib.portal import Portal from mudlib.zone import Zone from mudlib.zones import get_zone # Direction mappings: command -> (dx, dy) DIRECTIONS: dict[str, tuple[int, int]] = { "n": (0, -1), "north": (0, -1), "s": (0, 1), "south": (0, 1), "e": (1, 0), "east": (1, 0), "w": (-1, 0), "west": (-1, 0), "ne": (1, -1), "northeast": (1, -1), "nw": (-1, -1), "northwest": (-1, -1), "se": (1, 1), "southeast": (1, 1), "sw": (-1, 1), "southwest": (-1, 1), } # Opposite directions for arrival messages OPPOSITE_DIRECTIONS: dict[str, str] = { "north": "south", "south": "north", "east": "west", "west": "east", "northeast": "southwest", "southwest": "northeast", "northwest": "southeast", "southeast": "northwest", } def get_direction_name(dx: int, dy: int) -> str: """Get the full direction name from deltas.""" direction_map = { (0, -1): "north", (0, 1): "south", (1, 0): "east", (-1, 0): "west", (1, -1): "northeast", (-1, -1): "northwest", (1, 1): "southeast", (-1, 1): "southwest", } return direction_map.get((dx, dy), "") async def move_player(player: Player, dx: int, dy: int, direction_name: str) -> None: """Move a player in a given direction. Args: player: The player to move dx: X delta dy: Y delta direction_name: Full name of the direction for messages """ zone = player.location assert isinstance(zone, Zone), "Player must be in a zone to move" target_x, target_y = zone.wrap(player.x + dx, player.y + dy) # Check if the target is passable (skip check in paint mode) if not player.paint_mode and not zone.is_passable(target_x, target_y): await player.send("You can't go that way.\r\n") return # Check boundary exit conditions current_boundary = None target_boundary = None for boundary in zone.boundaries: if boundary.contains(player.x, player.y): current_boundary = boundary if boundary.contains(target_x, target_y): target_boundary = boundary # If leaving a boundary with an exit check, evaluate it if ( current_boundary is not None and current_boundary is not target_boundary and current_boundary.on_exit_check and current_boundary.on_exit_check.startswith("carrying:") ): check_value = current_boundary.on_exit_check[9:] # strip "carrying:" # Check if player has any item matching name or tag from mudlib.thing import Thing has_item = False for obj in player._contents: if isinstance(obj, Thing) and ( obj.name == check_value or check_value in obj.tags ): has_item = True break if has_item: # Check failed, block movement if current_boundary.on_exit_fail: await player.send(f"{current_boundary.on_exit_fail}\r\n") return # If painting, place the brush tile at the current position before moving if player.paint_mode and player.painting: zone.terrain[player.y][player.x] = player.paint_brush # Send departure message to players in the old area opposite = OPPOSITE_DIRECTIONS[direction_name] await send_nearby_message( player, player.x, player.y, f"{player.name} leaves {direction_name}.\r\n" ) # Update position player.x = target_x player.y = target_y # Send boundary messages # If leaving a boundary (and check passed), send exit message if ( current_boundary is not None and current_boundary is not target_boundary and current_boundary.on_exit_message ): await player.send(f"{current_boundary.on_exit_message}\r\n") # If entering a boundary, send enter message if ( target_boundary is not None and target_boundary is not current_boundary and target_boundary.on_enter_message ): await player.send(f"{target_boundary.on_enter_message}\r\n") # Check for auto-trigger portals at new position portals_here = [ obj for obj in zone.contents_at(target_x, target_y) if isinstance(obj, Portal) ] if portals_here: portal = portals_here[0] # Take first portal target_zone = get_zone(portal.target_zone) if target_zone: await player.send(f"You enter {portal.name}.\r\n") await send_nearby_message( player, player.x, player.y, f"{player.name} enters {portal.name}.\r\n" ) player.move_to(target_zone, x=portal.target_x, y=portal.target_y) await send_nearby_message( player, player.x, player.y, f"{player.name} arrives.\r\n" ) from mudlib.commands.look import cmd_look await cmd_look(player, "") send_room_info(player) send_map_data(player) return # Don't do normal arrival+look else: await player.send("The portal doesn't lead anywhere.\r\n") # Stay at portal tile but show normal look from mudlib.commands.look import cmd_look await cmd_look(player, "") return # Send arrival message to players in the new area await send_nearby_message( player, player.x, player.y, f"{player.name} arrives from the {opposite}.\r\n" ) # Render new viewport to the moving player from mudlib.commands.look import cmd_look await cmd_look(player, "") send_room_info(player) send_map_data(player) async def send_nearby_message(entity: Entity, x: int, y: int, message: str) -> None: """Send a message to all players near a location, excluding the entity. Sleeping players do not receive nearby messages (they are blind to room events). Args: entity: The entity who triggered the message (excluded from receiving it) x: X coordinate of the location y: Y coordinate of the location message: The message to send """ # For now, use a simple viewport range (could be configurable) viewport_range = 10 zone = entity.location assert isinstance(zone, Zone), "Entity must be in a zone to send nearby messages" for obj in zone.contents_near(x, y, viewport_range): if obj is not entity and isinstance(obj, Entity): # Skip sleeping players (they are blind to room events) if getattr(obj, "sleeping", False): continue await obj.send(message) # Define individual movement command handlers async def move_north(player: Player, args: str) -> None: await move_player(player, 0, -1, "north") async def move_south(player: Player, args: str) -> None: await move_player(player, 0, 1, "south") async def move_east(player: Player, args: str) -> None: await move_player(player, 1, 0, "east") async def move_west(player: Player, args: str) -> None: await move_player(player, -1, 0, "west") async def move_northeast(player: Player, args: str) -> None: await move_player(player, 1, -1, "northeast") async def move_northwest(player: Player, args: str) -> None: await move_player(player, -1, -1, "northwest") async def move_southeast(player: Player, args: str) -> None: await move_player(player, 1, 1, "southeast") async def move_southwest(player: Player, args: str) -> None: await move_player(player, -1, 1, "southwest") # Register all movement commands with their aliases register(CommandDefinition("north", move_north, aliases=["n"])) register(CommandDefinition("south", move_south, aliases=["s"])) register(CommandDefinition("east", move_east, aliases=["e"])) register(CommandDefinition("west", move_west, aliases=["w"])) register(CommandDefinition("northeast", move_northeast, aliases=["ne"])) register(CommandDefinition("northwest", move_northwest, aliases=["nw"])) register(CommandDefinition("southeast", move_southeast, aliases=["se"])) register(CommandDefinition("southwest", move_southwest, aliases=["sw"]))