mud/src/mudlib/commands/movement.py

199 lines
6.4 KiB
Python

"""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
# 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
# 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"]))