Add fly command with cloud trail effects
fly <direction> moves the player 5 tiles, ignoring terrain. Leaves a trail of bright white ~ clouds that fade after 2 seconds. Effects system supports arbitrary timed visual overlays on the viewport. TinTin aliases: fn/fs/fe/fw/fne/fnw/fse/fsw.
This commit is contained in:
parent
29983776f9
commit
9844749edd
8 changed files with 504 additions and 2 deletions
10
mud.tin
10
mud.tin
|
|
@ -1,3 +1,13 @@
|
||||||
#NOP TinTin++ config for connecting to the MUD server
|
#NOP TinTin++ config for connecting to the MUD server
|
||||||
#split 0 1
|
#split 0 1
|
||||||
#session mud localhost 6789
|
#session mud localhost 6789
|
||||||
|
|
||||||
|
#NOP fly aliases: f<direction> = fly 5 in that direction
|
||||||
|
#alias {fn} {fly north}
|
||||||
|
#alias {fs} {fly south}
|
||||||
|
#alias {fe} {fly east}
|
||||||
|
#alias {fw} {fly west}
|
||||||
|
#alias {fne} {fly northeast}
|
||||||
|
#alias {fnw} {fly northwest}
|
||||||
|
#alias {fse} {fly southeast}
|
||||||
|
#alias {fsw} {fly southwest}
|
||||||
|
|
|
||||||
78
src/mudlib/commands/fly.py
Normal file
78
src/mudlib/commands/fly.py
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
"""Fly command for aerial movement across the world."""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from mudlib.commands import register
|
||||||
|
from mudlib.commands.movement import DIRECTIONS, send_nearby_message
|
||||||
|
from mudlib.effects import add_effect
|
||||||
|
from mudlib.player import Player
|
||||||
|
from mudlib.render.ansi import BOLD, BRIGHT_WHITE
|
||||||
|
|
||||||
|
# World instance will be injected by the server
|
||||||
|
world: Any = None
|
||||||
|
|
||||||
|
# how far you fly
|
||||||
|
FLY_DISTANCE = 5
|
||||||
|
|
||||||
|
# how long cloud trail lingers (seconds)
|
||||||
|
CLOUD_TTL = 2.0
|
||||||
|
|
||||||
|
# ANSI color for cloud trails
|
||||||
|
CLOUD_COLOR = BOLD + BRIGHT_WHITE
|
||||||
|
|
||||||
|
|
||||||
|
async def cmd_fly(player: Player, args: str) -> None:
|
||||||
|
"""Fly through the air, moving 5 tiles in a direction.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
player: The player executing the command
|
||||||
|
args: Direction to fly (e.g. "east", "nw")
|
||||||
|
"""
|
||||||
|
direction = args.strip().lower()
|
||||||
|
if not direction:
|
||||||
|
player.writer.write("Fly which direction?\r\n")
|
||||||
|
await player.writer.drain()
|
||||||
|
return
|
||||||
|
|
||||||
|
delta = DIRECTIONS.get(direction)
|
||||||
|
if delta is None:
|
||||||
|
player.writer.write(f"'{direction}' isn't a direction.\r\n")
|
||||||
|
await player.writer.drain()
|
||||||
|
return
|
||||||
|
|
||||||
|
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)
|
||||||
|
add_effect(trail_x, trail_y, "~", CLOUD_COLOR, ttl=CLOUD_TTL)
|
||||||
|
|
||||||
|
# move player to destination
|
||||||
|
dest_x, dest_y = world.wrap(
|
||||||
|
start_x + dx * FLY_DISTANCE, start_y + dy * FLY_DISTANCE
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
await cmd_look(player, "")
|
||||||
|
|
||||||
|
|
||||||
|
register("fly", cmd_fly)
|
||||||
|
|
@ -3,8 +3,9 @@
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from mudlib.commands import register
|
from mudlib.commands import register
|
||||||
|
from mudlib.effects import get_effects_at
|
||||||
from mudlib.player import Player, players
|
from mudlib.player import Player, players
|
||||||
from mudlib.render.ansi import colorize_terrain
|
from mudlib.render.ansi import RESET, colorize_terrain
|
||||||
|
|
||||||
# World instance will be injected by the server
|
# World instance will be injected by the server
|
||||||
world: Any = None
|
world: Any = None
|
||||||
|
|
@ -53,6 +54,10 @@ async def cmd_look(player: Player, args: str) -> None:
|
||||||
other_player_positions.append((rel_x, rel_y))
|
other_player_positions.append((rel_x, rel_y))
|
||||||
|
|
||||||
# Build the output with ANSI coloring
|
# Build the output with ANSI coloring
|
||||||
|
# priority: player @ > other players * > effects > terrain
|
||||||
|
half_width = VIEWPORT_WIDTH // 2
|
||||||
|
half_height = VIEWPORT_HEIGHT // 2
|
||||||
|
|
||||||
output_lines = []
|
output_lines = []
|
||||||
for y, row in enumerate(viewport):
|
for y, row in enumerate(viewport):
|
||||||
line = []
|
line = []
|
||||||
|
|
@ -64,7 +69,18 @@ async def cmd_look(player: Player, args: str) -> None:
|
||||||
elif (x, y) in other_player_positions:
|
elif (x, y) in other_player_positions:
|
||||||
line.append(colorize_terrain("*"))
|
line.append(colorize_terrain("*"))
|
||||||
else:
|
else:
|
||||||
line.append(colorize_terrain(tile))
|
# Check for active effects at this world position
|
||||||
|
world_x, world_y = world.wrap(
|
||||||
|
player.x - half_width + x,
|
||||||
|
player.y - half_height + y,
|
||||||
|
)
|
||||||
|
effects = get_effects_at(world_x, world_y)
|
||||||
|
if effects:
|
||||||
|
# use the most recent effect
|
||||||
|
e = effects[-1]
|
||||||
|
line.append(f"{e.color}{e.char}{RESET}")
|
||||||
|
else:
|
||||||
|
line.append(colorize_terrain(tile))
|
||||||
output_lines.append("".join(line))
|
output_lines.append("".join(line))
|
||||||
|
|
||||||
# Send to player
|
# Send to player
|
||||||
|
|
|
||||||
46
src/mudlib/effects.py
Normal file
46
src/mudlib/effects.py
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
"""Temporary visual effects that appear on the map and fade over time."""
|
||||||
|
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Effect:
|
||||||
|
"""A temporary visual overlay on the world map."""
|
||||||
|
|
||||||
|
x: int
|
||||||
|
y: int
|
||||||
|
char: str
|
||||||
|
color: str # ANSI color code
|
||||||
|
expires_at: float # monotonic time when this effect disappears
|
||||||
|
|
||||||
|
|
||||||
|
# Global list of active effects
|
||||||
|
active_effects: list[Effect] = []
|
||||||
|
|
||||||
|
|
||||||
|
def add_effect(x: int, y: int, char: str, color: str, ttl: float) -> None:
|
||||||
|
"""Add a temporary visual effect at (x, y).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x: World X coordinate
|
||||||
|
y: World Y coordinate
|
||||||
|
char: Character to display
|
||||||
|
color: ANSI color code for rendering
|
||||||
|
ttl: Time to live in seconds
|
||||||
|
"""
|
||||||
|
active_effects.append(
|
||||||
|
Effect(x=x, y=y, char=char, color=color, expires_at=time.monotonic() + ttl)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_effects_at(x: int, y: int) -> list[Effect]:
|
||||||
|
"""Get all non-expired effects at a given position."""
|
||||||
|
now = time.monotonic()
|
||||||
|
return [e for e in active_effects if e.x == x and e.y == y and e.expires_at > now]
|
||||||
|
|
||||||
|
|
||||||
|
def clear_expired() -> None:
|
||||||
|
"""Remove all expired effects from the active list."""
|
||||||
|
now = time.monotonic()
|
||||||
|
active_effects[:] = [e for e in active_effects if e.expires_at > now]
|
||||||
|
|
@ -9,6 +9,7 @@ import telnetlib3
|
||||||
from telnetlib3.server_shell import readline2
|
from telnetlib3.server_shell import readline2
|
||||||
|
|
||||||
import mudlib.commands
|
import mudlib.commands
|
||||||
|
import mudlib.commands.fly
|
||||||
import mudlib.commands.look
|
import mudlib.commands.look
|
||||||
import mudlib.commands.movement
|
import mudlib.commands.movement
|
||||||
import mudlib.commands.quit
|
import mudlib.commands.quit
|
||||||
|
|
@ -143,6 +144,7 @@ async def run_server() -> None:
|
||||||
log.info("world generated in %.2fs", elapsed)
|
log.info("world generated in %.2fs", elapsed)
|
||||||
|
|
||||||
# Inject world into command modules
|
# Inject world into command modules
|
||||||
|
mudlib.commands.fly.world = _world
|
||||||
mudlib.commands.look.world = _world
|
mudlib.commands.look.world = _world
|
||||||
mudlib.commands.movement.world = _world
|
mudlib.commands.movement.world = _world
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,9 @@ import pytest
|
||||||
|
|
||||||
from mudlib import commands
|
from mudlib import commands
|
||||||
from mudlib.commands import look, movement
|
from mudlib.commands import look, movement
|
||||||
|
from mudlib.effects import active_effects, add_effect
|
||||||
from mudlib.player import Player
|
from mudlib.player import Player
|
||||||
|
from mudlib.render.ansi import RESET
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
@ -267,3 +269,51 @@ async def test_look_command_shows_other_players(player, mock_world):
|
||||||
# Check that the output contains * for other players
|
# Check that the output contains * for other players
|
||||||
output = "".join([call[0][0] for call in player.writer.write.call_args_list])
|
output = "".join([call[0][0] for call in player.writer.write.call_args_list])
|
||||||
assert "*" in output
|
assert "*" in output
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_look_shows_effects_on_viewport(player, mock_world):
|
||||||
|
"""Test that active effects overlay on the viewport."""
|
||||||
|
look.world = mock_world
|
||||||
|
|
||||||
|
from mudlib.player import players
|
||||||
|
|
||||||
|
players.clear()
|
||||||
|
players[player.name] = player
|
||||||
|
|
||||||
|
active_effects.clear()
|
||||||
|
# place a cloud effect 2 tiles east of the player (viewport center is 10,5)
|
||||||
|
# player is at (5, 5), so effect at (7, 5) -> viewport (12, 5)
|
||||||
|
add_effect(7, 5, "~", "\033[1;97m", ttl=10.0)
|
||||||
|
|
||||||
|
await look.cmd_look(player, "")
|
||||||
|
|
||||||
|
output = "".join([call[0][0] for call in player.writer.write.call_args_list])
|
||||||
|
# the output should contain our cloud color applied to ~
|
||||||
|
# (bright white ~ instead of blue ~)
|
||||||
|
assert "\033[1;97m~" + RESET in output
|
||||||
|
|
||||||
|
active_effects.clear()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_effects_dont_override_player_marker(player, mock_world):
|
||||||
|
"""Effects at the player's position should not hide the @ marker."""
|
||||||
|
look.world = mock_world
|
||||||
|
|
||||||
|
from mudlib.player import players
|
||||||
|
|
||||||
|
players.clear()
|
||||||
|
players[player.name] = player
|
||||||
|
|
||||||
|
active_effects.clear()
|
||||||
|
# place an effect right at the player's position
|
||||||
|
add_effect(player.x, player.y, "~", "\033[1;97m", ttl=10.0)
|
||||||
|
|
||||||
|
await look.cmd_look(player, "")
|
||||||
|
|
||||||
|
output = "".join([call[0][0] for call in player.writer.write.call_args_list])
|
||||||
|
# player @ should still be visible at center
|
||||||
|
assert "@" in output
|
||||||
|
|
||||||
|
active_effects.clear()
|
||||||
|
|
|
||||||
84
tests/test_effects.py
Normal file
84
tests/test_effects.py
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
"""Tests for the temporary visual effects system."""
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from mudlib.effects import (
|
||||||
|
Effect,
|
||||||
|
active_effects,
|
||||||
|
add_effect,
|
||||||
|
clear_expired,
|
||||||
|
get_effects_at,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def clean_effects():
|
||||||
|
"""Clear effects before and after each test."""
|
||||||
|
active_effects.clear()
|
||||||
|
yield
|
||||||
|
active_effects.clear()
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_effect_creates_entry():
|
||||||
|
add_effect(10, 20, "~", "\033[1;97m", ttl=2.0)
|
||||||
|
assert len(active_effects) == 1
|
||||||
|
e = active_effects[0]
|
||||||
|
assert e.x == 10
|
||||||
|
assert e.y == 20
|
||||||
|
assert e.char == "~"
|
||||||
|
assert e.color == "\033[1;97m"
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_effect_sets_expiry_from_ttl():
|
||||||
|
before = time.monotonic()
|
||||||
|
add_effect(0, 0, "~", "", ttl=2.0)
|
||||||
|
after = time.monotonic()
|
||||||
|
e = active_effects[0]
|
||||||
|
assert before + 2.0 <= e.expires_at <= after + 2.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_effects_at_returns_matching():
|
||||||
|
add_effect(5, 5, "~", "c1", ttl=2.0)
|
||||||
|
add_effect(5, 5, "~", "c2", ttl=2.0)
|
||||||
|
add_effect(6, 5, "~", "c3", ttl=2.0)
|
||||||
|
|
||||||
|
at_5_5 = get_effects_at(5, 5)
|
||||||
|
assert len(at_5_5) == 2
|
||||||
|
assert {e.color for e in at_5_5} == {"c1", "c2"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_effects_at_excludes_expired():
|
||||||
|
add_effect(5, 5, "~", "alive", ttl=10.0)
|
||||||
|
# manually insert an expired effect
|
||||||
|
active_effects.append(
|
||||||
|
Effect(x=5, y=5, char="~", color="dead", expires_at=time.monotonic() - 1.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
at_5_5 = get_effects_at(5, 5)
|
||||||
|
assert len(at_5_5) == 1
|
||||||
|
assert at_5_5[0].color == "alive"
|
||||||
|
|
||||||
|
|
||||||
|
def test_clear_expired_removes_old_effects():
|
||||||
|
add_effect(1, 1, "~", "alive", ttl=10.0)
|
||||||
|
active_effects.append(
|
||||||
|
Effect(x=2, y=2, char="~", color="dead", expires_at=time.monotonic() - 1.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
clear_expired()
|
||||||
|
assert len(active_effects) == 1
|
||||||
|
assert active_effects[0].color == "alive"
|
||||||
|
|
||||||
|
|
||||||
|
def test_multiple_effects_along_path():
|
||||||
|
"""Simulates adding a cloud trail along a flight path."""
|
||||||
|
for i in range(5):
|
||||||
|
add_effect(10 + i, 20, "~", "\033[1;97m", ttl=2.0)
|
||||||
|
|
||||||
|
assert len(active_effects) == 5
|
||||||
|
# all at y=20, x from 10-14
|
||||||
|
for i, e in enumerate(active_effects):
|
||||||
|
assert e.x == 10 + i
|
||||||
|
assert e.y == 20
|
||||||
216
tests/test_fly.py
Normal file
216
tests/test_fly.py
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
"""Tests for the fly command."""
|
||||||
|
|
||||||
|
import time
|
||||||
|
from unittest.mock import AsyncMock, MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from mudlib.commands import fly, look, movement
|
||||||
|
from mudlib.effects import active_effects
|
||||||
|
from mudlib.player import Player, players
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_writer():
|
||||||
|
writer = MagicMock()
|
||||||
|
writer.write = MagicMock()
|
||||||
|
writer.drain = AsyncMock()
|
||||||
|
return writer
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_reader():
|
||||||
|
return MagicMock()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_world():
|
||||||
|
world = MagicMock()
|
||||||
|
world.width = 100
|
||||||
|
world.height = 100
|
||||||
|
world.wrap = MagicMock(side_effect=lambda x, y: (x % 100, y % 100))
|
||||||
|
viewport = [["." for _ in range(21)] for _ in range(11)]
|
||||||
|
world.get_viewport = MagicMock(return_value=viewport)
|
||||||
|
return world
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def player(mock_reader, mock_writer):
|
||||||
|
return Player(name="shmup", x=50, y=50, reader=mock_reader, writer=mock_writer)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def clean_state(mock_world):
|
||||||
|
"""Clean global state before/after each test."""
|
||||||
|
fly.world = mock_world
|
||||||
|
look.world = mock_world
|
||||||
|
movement.world = mock_world
|
||||||
|
players.clear()
|
||||||
|
active_effects.clear()
|
||||||
|
yield
|
||||||
|
players.clear()
|
||||||
|
active_effects.clear()
|
||||||
|
|
||||||
|
|
||||||
|
# --- direction parsing ---
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_fly_east_moves_5_tiles(player):
|
||||||
|
"""fly east should move player 5 tiles east."""
|
||||||
|
players[player.name] = player
|
||||||
|
await fly.cmd_fly(player, "east")
|
||||||
|
|
||||||
|
assert player.x == 55
|
||||||
|
assert player.y == 50
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_fly_north_moves_5_tiles(player):
|
||||||
|
players[player.name] = player
|
||||||
|
await fly.cmd_fly(player, "north")
|
||||||
|
|
||||||
|
assert player.x == 50
|
||||||
|
assert player.y == 45
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_fly_northwest_moves_5_diagonal(player):
|
||||||
|
players[player.name] = player
|
||||||
|
await fly.cmd_fly(player, "nw")
|
||||||
|
|
||||||
|
assert player.x == 45
|
||||||
|
assert player.y == 45
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_fly_wraps_around_world(player):
|
||||||
|
"""Flying near an edge should wrap toroidally."""
|
||||||
|
player.x = 98
|
||||||
|
player.y = 50
|
||||||
|
players[player.name] = player
|
||||||
|
await fly.cmd_fly(player, "east")
|
||||||
|
|
||||||
|
# 98 + 5 = 103, wraps to 3
|
||||||
|
assert player.x == 3
|
||||||
|
assert player.y == 50
|
||||||
|
|
||||||
|
|
||||||
|
# --- cloud trail ---
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_fly_creates_cloud_trail(player):
|
||||||
|
"""Flying should leave ~ effects along the path."""
|
||||||
|
players[player.name] = player
|
||||||
|
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):
|
||||||
|
assert (50 + i, 50) in trail_positions
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_cloud_trail_chars_are_tilde(player):
|
||||||
|
players[player.name] = player
|
||||||
|
await fly.cmd_fly(player, "east")
|
||||||
|
|
||||||
|
for e in active_effects:
|
||||||
|
assert e.char == "~"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_cloud_trail_has_ttl(player):
|
||||||
|
"""Cloud effects should expire after roughly 2 seconds."""
|
||||||
|
players[player.name] = player
|
||||||
|
|
||||||
|
before = time.monotonic()
|
||||||
|
await fly.cmd_fly(player, "east")
|
||||||
|
|
||||||
|
for e in active_effects:
|
||||||
|
remaining = e.expires_at - before
|
||||||
|
assert 1.5 <= remaining <= 2.5
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_fly_diagonal_trail(player):
|
||||||
|
"""Diagonal flight should leave trail at each intermediate step."""
|
||||||
|
players[player.name] = player
|
||||||
|
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 ---
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_fly_sends_self_message(player, mock_writer):
|
||||||
|
"""Player should see 'You fly into the air'."""
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
||||||
|
await fly.cmd_fly(player, "sideways")
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_fly_triggers_look(player, mock_world):
|
||||||
|
"""Flying should auto-look at the destination."""
|
||||||
|
players[player.name] = player
|
||||||
|
await fly.cmd_fly(player, "east")
|
||||||
|
|
||||||
|
# get_viewport is called by look, which fly triggers
|
||||||
|
assert mock_world.get_viewport.called
|
||||||
Loading…
Reference in a new issue