Movement now evaluates boundary enter/exit conditions. Exit checks can block movement based on carrying items (by name or tag). Enter and exit messages sent when crossing boundary borders. All boundary logic lives in move_player() before position update.
251 lines
8.2 KiB
Python
251 lines
8.2 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
|
|
|
|
# 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"]))
|