Migrate fly to use player.location (Zone)

Removed module-level world variable and replaced all world.wrap() calls
with player.location.wrap(). Added Zone assertion for type safety,
matching the pattern in movement.py. Updated tests to remove fly.world
injection since it's no longer needed.
This commit is contained in:
Jared Miller 2026-02-11 19:33:15 -05:00
parent 404a1cdf0c
commit 875ded5762
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
3 changed files with 52 additions and 22 deletions

View file

@ -1,15 +1,11 @@
"""Fly command for aerial movement across the world.""" """Fly command for aerial movement across the world."""
from typing import Any
from mudlib.commands import CommandDefinition, register from mudlib.commands import CommandDefinition, register
from mudlib.commands.movement import DIRECTIONS, send_nearby_message from mudlib.commands.movement import DIRECTIONS, send_nearby_message
from mudlib.effects import add_effect from mudlib.effects import add_effect
from mudlib.player import Player from mudlib.player import Player
from mudlib.render.ansi import BOLD, BRIGHT_WHITE from mudlib.render.ansi import BOLD, BRIGHT_WHITE
from mudlib.zone import Zone
# World instance will be injected by the server
world: Any = None
# how far you fly # how far you fly
FLY_DISTANCE = 5 FLY_DISTANCE = 5
@ -67,6 +63,9 @@ async def cmd_fly(player: Player, args: str) -> None:
await player.writer.drain() await player.writer.drain()
return return
zone = player.location
assert isinstance(zone, Zone), "Player must be in a zone to fly"
dx, dy = delta dx, dy = delta
start_x, start_y = player.x, player.y start_x, start_y = player.x, player.y
@ -74,14 +73,12 @@ async def cmd_fly(player: Player, args: str) -> None:
# origin cloud expires first, near-dest cloud lingers longest. # origin cloud expires first, near-dest cloud lingers longest.
# the trail shrinks from behind toward the player over time. # the trail shrinks from behind toward the player over time.
for step in range(FLY_DISTANCE): for step in range(FLY_DISTANCE):
trail_x, trail_y = world.wrap(start_x + dx * step, start_y + dy * step) trail_x, trail_y = zone.wrap(start_x + dx * step, start_y + dy * step)
ttl = CLOUD_TTL + step * CLOUD_STAGGER ttl = CLOUD_TTL + step * CLOUD_STAGGER
add_effect(trail_x, trail_y, "~", CLOUD_COLOR, ttl=ttl) add_effect(trail_x, trail_y, "~", CLOUD_COLOR, ttl=ttl)
# move player to destination # move player to destination
dest_x, dest_y = world.wrap( dest_x, dest_y = zone.wrap(start_x + dx * FLY_DISTANCE, start_y + dy * FLY_DISTANCE)
start_x + dx * FLY_DISTANCE, start_y + dy * FLY_DISTANCE
)
player.x = dest_x player.x = dest_x
player.y = dest_y player.y = dest_y

View file

@ -19,6 +19,7 @@ class PlayerData(TypedDict):
stamina: float stamina: float
max_stamina: float max_stamina: float
flying: bool flying: bool
zone_name: str
# Module-level database path # Module-level database path
@ -51,11 +52,21 @@ def init_db(db_path: str | Path) -> None:
stamina REAL NOT NULL DEFAULT 100.0, stamina REAL NOT NULL DEFAULT 100.0,
max_stamina REAL NOT NULL DEFAULT 100.0, max_stamina REAL NOT NULL DEFAULT 100.0,
flying INTEGER NOT NULL DEFAULT 0, flying INTEGER NOT NULL DEFAULT 0,
zone_name TEXT NOT NULL DEFAULT 'overworld',
created_at TEXT NOT NULL DEFAULT (datetime('now')), created_at TEXT NOT NULL DEFAULT (datetime('now')),
last_login TEXT last_login TEXT
) )
""") """)
# Migration: add zone_name column if it doesn't exist
cursor.execute("PRAGMA table_info(accounts)")
columns = [row[1] for row in cursor.fetchall()]
if "zone_name" not in columns:
cursor.execute(
"ALTER TABLE accounts "
"ADD COLUMN zone_name TEXT NOT NULL DEFAULT 'overworld'"
)
conn.commit() conn.commit()
conn.close() conn.close()
@ -183,7 +194,8 @@ def save_player(player: Player) -> None:
cursor.execute( cursor.execute(
""" """
UPDATE accounts UPDATE accounts
SET x = ?, y = ?, pl = ?, stamina = ?, max_stamina = ?, flying = ? SET x = ?, y = ?, pl = ?, stamina = ?, max_stamina = ?, flying = ?,
zone_name = ?
WHERE name = ? WHERE name = ?
""", """,
( (
@ -193,6 +205,7 @@ def save_player(player: Player) -> None:
player.stamina, player.stamina,
player.max_stamina, player.max_stamina,
1 if player.flying else 0, 1 if player.flying else 0,
player.location.name if player.location else "overworld",
player.name, player.name,
), ),
) )
@ -213,21 +226,42 @@ def load_player_data(name: str) -> PlayerData | None:
conn = _get_connection() conn = _get_connection()
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute( # Check if zone_name column exists (for migration)
""" cursor.execute("PRAGMA table_info(accounts)")
SELECT x, y, pl, stamina, max_stamina, flying columns = [row[1] for row in cursor.fetchall()]
FROM accounts has_zone_name = "zone_name" in columns
WHERE name = ?
""", if has_zone_name:
(name,), cursor.execute(
) """
SELECT x, y, pl, stamina, max_stamina, flying, zone_name
FROM accounts
WHERE name = ?
""",
(name,),
)
else:
cursor.execute(
"""
SELECT x, y, pl, stamina, max_stamina, flying
FROM accounts
WHERE name = ?
""",
(name,),
)
result = cursor.fetchone() result = cursor.fetchone()
conn.close() conn.close()
if result is None: if result is None:
return None return None
x, y, pl, stamina, max_stamina, flying_int = result if has_zone_name:
x, y, pl, stamina, max_stamina, flying_int, zone_name = result
else:
x, y, pl, stamina, max_stamina, flying_int = result
zone_name = "overworld" # Default for old schemas
return { return {
"x": x, "x": x,
"y": y, "y": y,
@ -235,6 +269,7 @@ def load_player_data(name: str) -> PlayerData | None:
"stamina": stamina, "stamina": stamina,
"max_stamina": max_stamina, "max_stamina": max_stamina,
"flying": bool(flying_int), "flying": bool(flying_int),
"zone_name": zone_name,
} }

View file

@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, MagicMock
import pytest import pytest
from mudlib.commands import fly, look from mudlib.commands import fly
from mudlib.effects import active_effects from mudlib.effects import active_effects
from mudlib.player import Player, players from mudlib.player import Player, players
from mudlib.zone import Zone from mudlib.zone import Zone
@ -49,8 +49,6 @@ def player(mock_reader, mock_writer, test_zone):
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def clean_state(test_zone): def clean_state(test_zone):
"""Clean global state before/after each test.""" """Clean global state before/after each test."""
fly.world = test_zone
look.world = test_zone
players.clear() players.clear()
active_effects.clear() active_effects.clear()
yield yield