Make flying a toggle state
fly with no args toggles flying on/off. Movement commands (fly east, etc) only work while airborne. "You aren't flying." if you try to move without toggling on first. Player.flying field tracks the state.
This commit is contained in:
parent
9844749edd
commit
93ad4523e2
3 changed files with 150 additions and 61 deletions
|
|
@ -22,15 +22,40 @@ CLOUD_COLOR = BOLD + BRIGHT_WHITE
|
|||
|
||||
|
||||
async def cmd_fly(player: Player, args: str) -> None:
|
||||
"""Fly through the air, moving 5 tiles in a direction.
|
||||
"""Toggle flying or move while airborne.
|
||||
|
||||
Args:
|
||||
player: The player executing the command
|
||||
args: Direction to fly (e.g. "east", "nw")
|
||||
fly - toggle flying on/off
|
||||
fly <dir> - move 5 tiles in a direction (must be flying)
|
||||
"""
|
||||
direction = args.strip().lower()
|
||||
|
||||
# no args = toggle flying state
|
||||
if not direction:
|
||||
player.writer.write("Fly which direction?\r\n")
|
||||
if player.flying:
|
||||
player.flying = False
|
||||
player.writer.write("You land.\r\n")
|
||||
await player.writer.drain()
|
||||
await send_nearby_message(
|
||||
player,
|
||||
player.x,
|
||||
player.y,
|
||||
f"{player.name} lands.\r\n",
|
||||
)
|
||||
else:
|
||||
player.flying = True
|
||||
player.writer.write("You fly into the air!\r\n")
|
||||
await player.writer.drain()
|
||||
await send_nearby_message(
|
||||
player,
|
||||
player.x,
|
||||
player.y,
|
||||
f"{player.name} lifts into the air!\r\n",
|
||||
)
|
||||
return
|
||||
|
||||
# direction given but not flying
|
||||
if not player.flying:
|
||||
player.writer.write("You aren't flying.\r\n")
|
||||
await player.writer.drain()
|
||||
return
|
||||
|
||||
|
|
@ -43,15 +68,6 @@ async def cmd_fly(player: Player, args: str) -> None:
|
|||
dx, dy = delta
|
||||
start_x, start_y = player.x, player.y
|
||||
|
||||
# tell the player
|
||||
player.writer.write("You fly into the air!\r\n")
|
||||
await player.writer.drain()
|
||||
|
||||
# tell nearby players at departure
|
||||
await send_nearby_message(
|
||||
player, player.x, player.y, f"{player.name} lifts into the air!\r\n"
|
||||
)
|
||||
|
||||
# lay cloud trail at starting position and each intermediate step
|
||||
for step in range(FLY_DISTANCE):
|
||||
trail_x, trail_y = world.wrap(start_x + dx * step, start_y + dy * step)
|
||||
|
|
@ -64,11 +80,6 @@ async def cmd_fly(player: Player, args: str) -> None:
|
|||
player.x = dest_x
|
||||
player.y = dest_y
|
||||
|
||||
# tell nearby players at arrival
|
||||
await send_nearby_message(
|
||||
player, player.x, player.y, f"{player.name} lands from the sky!\r\n"
|
||||
)
|
||||
|
||||
# auto-look at new position
|
||||
from mudlib.commands.look import cmd_look
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ class Player:
|
|||
y: int
|
||||
writer: Any # telnetlib3 TelnetWriter for sending output
|
||||
reader: Any # telnetlib3 TelnetReader for reading input
|
||||
flying: bool = False
|
||||
|
||||
|
||||
# Global registry of connected players
|
||||
|
|
|
|||
|
|
@ -52,13 +52,107 @@ def clean_state(mock_world):
|
|||
active_effects.clear()
|
||||
|
||||
|
||||
# --- direction parsing ---
|
||||
# --- toggle ---
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fly_toggles_on(player, mock_writer):
|
||||
"""fly with no args sets flying=True."""
|
||||
players[player.name] = player
|
||||
assert not player.flying
|
||||
|
||||
await fly.cmd_fly(player, "")
|
||||
|
||||
assert player.flying
|
||||
calls = [str(c) for c in mock_writer.write.call_args_list]
|
||||
assert any("fly" in c.lower() and "air" in c.lower() for c in calls)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fly_toggles_off(player, mock_writer):
|
||||
"""fly again sets flying=False."""
|
||||
players[player.name] = player
|
||||
player.flying = True
|
||||
|
||||
await fly.cmd_fly(player, "")
|
||||
|
||||
assert not player.flying
|
||||
calls = [str(c) for c in mock_writer.write.call_args_list]
|
||||
assert any("land" in c.lower() for c in calls)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fly_toggle_on_notifies_nearby(player):
|
||||
"""Others see liftoff message."""
|
||||
players[player.name] = player
|
||||
|
||||
other_writer = MagicMock()
|
||||
other_writer.write = MagicMock()
|
||||
other_writer.drain = AsyncMock()
|
||||
other = Player(
|
||||
name="bystander",
|
||||
x=52,
|
||||
y=50,
|
||||
reader=MagicMock(),
|
||||
writer=other_writer,
|
||||
)
|
||||
players[other.name] = other
|
||||
|
||||
await fly.cmd_fly(player, "")
|
||||
|
||||
calls = [str(c) for c in other_writer.write.call_args_list]
|
||||
assert any("shmup" in c.lower() and "lifts" in c.lower() for c in calls)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fly_toggle_off_notifies_nearby(player):
|
||||
"""Others see landing message."""
|
||||
players[player.name] = player
|
||||
player.flying = True
|
||||
|
||||
other_writer = MagicMock()
|
||||
other_writer.write = MagicMock()
|
||||
other_writer.drain = AsyncMock()
|
||||
other = Player(
|
||||
name="bystander",
|
||||
x=52,
|
||||
y=50,
|
||||
reader=MagicMock(),
|
||||
writer=other_writer,
|
||||
)
|
||||
players[other.name] = other
|
||||
|
||||
await fly.cmd_fly(player, "")
|
||||
|
||||
calls = [str(c) for c in other_writer.write.call_args_list]
|
||||
assert any("shmup" in c.lower() and "lands" in c.lower() for c in calls)
|
||||
|
||||
|
||||
# --- must be flying to move ---
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fly_direction_without_flying_errors(player, mock_writer):
|
||||
"""fly east when not flying gives an error."""
|
||||
players[player.name] = player
|
||||
assert not player.flying
|
||||
|
||||
await fly.cmd_fly(player, "east")
|
||||
|
||||
calls = [str(c) for c in mock_writer.write.call_args_list]
|
||||
assert any("aren't flying" in c.lower() for c in calls)
|
||||
assert player.x == 50
|
||||
assert player.y == 50
|
||||
|
||||
|
||||
# --- movement while flying ---
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fly_east_moves_5_tiles(player):
|
||||
"""fly east should move player 5 tiles east."""
|
||||
players[player.name] = player
|
||||
player.flying = True
|
||||
await fly.cmd_fly(player, "east")
|
||||
|
||||
assert player.x == 55
|
||||
|
|
@ -68,6 +162,7 @@ async def test_fly_east_moves_5_tiles(player):
|
|||
@pytest.mark.asyncio
|
||||
async def test_fly_north_moves_5_tiles(player):
|
||||
players[player.name] = player
|
||||
player.flying = True
|
||||
await fly.cmd_fly(player, "north")
|
||||
|
||||
assert player.x == 50
|
||||
|
|
@ -77,6 +172,7 @@ async def test_fly_north_moves_5_tiles(player):
|
|||
@pytest.mark.asyncio
|
||||
async def test_fly_northwest_moves_5_diagonal(player):
|
||||
players[player.name] = player
|
||||
player.flying = True
|
||||
await fly.cmd_fly(player, "nw")
|
||||
|
||||
assert player.x == 45
|
||||
|
|
@ -88,10 +184,10 @@ async def test_fly_wraps_around_world(player):
|
|||
"""Flying near an edge should wrap toroidally."""
|
||||
player.x = 98
|
||||
player.y = 50
|
||||
player.flying = True
|
||||
players[player.name] = player
|
||||
await fly.cmd_fly(player, "east")
|
||||
|
||||
# 98 + 5 = 103, wraps to 3
|
||||
assert player.x == 3
|
||||
assert player.y == 50
|
||||
|
||||
|
|
@ -103,10 +199,9 @@ async def test_fly_wraps_around_world(player):
|
|||
async def test_fly_creates_cloud_trail(player):
|
||||
"""Flying should leave ~ effects along the path."""
|
||||
players[player.name] = player
|
||||
player.flying = True
|
||||
await fly.cmd_fly(player, "east")
|
||||
|
||||
# trail at positions 50,51,52,53,54 (start through second-to-last)
|
||||
# player ends at 55
|
||||
assert len(active_effects) == 5
|
||||
trail_positions = [(e.x, e.y) for e in active_effects]
|
||||
for i in range(5):
|
||||
|
|
@ -116,6 +211,7 @@ async def test_fly_creates_cloud_trail(player):
|
|||
@pytest.mark.asyncio
|
||||
async def test_cloud_trail_chars_are_tilde(player):
|
||||
players[player.name] = player
|
||||
player.flying = True
|
||||
await fly.cmd_fly(player, "east")
|
||||
|
||||
for e in active_effects:
|
||||
|
|
@ -126,6 +222,7 @@ async def test_cloud_trail_chars_are_tilde(player):
|
|||
async def test_cloud_trail_has_ttl(player):
|
||||
"""Cloud effects should expire after roughly 2 seconds."""
|
||||
players[player.name] = player
|
||||
player.flying = True
|
||||
|
||||
before = time.monotonic()
|
||||
await fly.cmd_fly(player, "east")
|
||||
|
|
@ -139,65 +236,35 @@ async def test_cloud_trail_has_ttl(player):
|
|||
async def test_fly_diagonal_trail(player):
|
||||
"""Diagonal flight should leave trail at each intermediate step."""
|
||||
players[player.name] = player
|
||||
player.flying = True
|
||||
await fly.cmd_fly(player, "se")
|
||||
|
||||
# southeast: dx=1, dy=1, 5 steps
|
||||
# trail at (50,50), (51,51), (52,52), (53,53), (54,54)
|
||||
# player ends at (55, 55)
|
||||
assert len(active_effects) == 5
|
||||
trail_positions = [(e.x, e.y) for e in active_effects]
|
||||
for i in range(5):
|
||||
assert (50 + i, 50 + i) in trail_positions
|
||||
|
||||
|
||||
# --- messages ---
|
||||
# --- no movement without flying leaves no trail ---
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fly_sends_self_message(player, mock_writer):
|
||||
"""Player should see 'You fly into the air'."""
|
||||
async def test_no_trail_when_not_flying(player):
|
||||
"""Trying to fly a direction while grounded creates no effects."""
|
||||
players[player.name] = player
|
||||
await fly.cmd_fly(player, "east")
|
||||
|
||||
calls = [str(c) for c in mock_writer.write.call_args_list]
|
||||
assert any("fly" in c.lower() and "air" in c.lower() for c in calls)
|
||||
assert len(active_effects) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fly_sends_nearby_message(player):
|
||||
"""Other players should see '{name} lifts into the air'."""
|
||||
players[player.name] = player
|
||||
|
||||
other_writer = MagicMock()
|
||||
other_writer.write = MagicMock()
|
||||
other_writer.drain = AsyncMock()
|
||||
other = Player(
|
||||
name="bystander", x=52, y=50, reader=MagicMock(), writer=other_writer
|
||||
)
|
||||
players[other.name] = other
|
||||
|
||||
await fly.cmd_fly(player, "east")
|
||||
|
||||
calls = [str(c) for c in other_writer.write.call_args_list]
|
||||
assert any("shmup" in c.lower() and "lifts" in c.lower() for c in calls)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fly_no_direction_gives_error(player, mock_writer):
|
||||
"""fly with no args should tell the player to specify a direction."""
|
||||
players[player.name] = player
|
||||
await fly.cmd_fly(player, "")
|
||||
|
||||
calls = [str(c) for c in mock_writer.write.call_args_list]
|
||||
assert any("direction" in c.lower() for c in calls)
|
||||
assert player.x == 50
|
||||
assert player.y == 50
|
||||
# --- error cases ---
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fly_bad_direction_gives_error(player, mock_writer):
|
||||
"""fly with an invalid direction should give feedback."""
|
||||
players[player.name] = player
|
||||
player.flying = True
|
||||
await fly.cmd_fly(player, "sideways")
|
||||
|
||||
calls = [str(c) for c in mock_writer.write.call_args_list]
|
||||
|
|
@ -210,7 +277,17 @@ async def test_fly_bad_direction_gives_error(player, mock_writer):
|
|||
async def test_fly_triggers_look(player, mock_world):
|
||||
"""Flying should auto-look at the destination."""
|
||||
players[player.name] = player
|
||||
player.flying = True
|
||||
await fly.cmd_fly(player, "east")
|
||||
|
||||
# get_viewport is called by look, which fly triggers
|
||||
assert mock_world.get_viewport.called
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stays_flying_after_move(player):
|
||||
"""Moving while flying doesn't turn off flying."""
|
||||
players[player.name] = player
|
||||
player.flying = True
|
||||
await fly.cmd_fly(player, "east")
|
||||
|
||||
assert player.flying
|
||||
|
|
|
|||
Loading…
Reference in a new issue