Render mobs as * in viewport
Phase 3: look command now collects alive mob positions using the same wrapping-aware relative position calc as players, and renders them as * with the same priority as other players (after @ but before effects).
This commit is contained in:
parent
3eda62fa42
commit
c09a1e510d
2 changed files with 132 additions and 1 deletions
|
|
@ -4,6 +4,7 @@ from typing import Any
|
||||||
|
|
||||||
from mudlib.commands import CommandDefinition, register
|
from mudlib.commands import CommandDefinition, register
|
||||||
from mudlib.effects import get_effects_at
|
from mudlib.effects import get_effects_at
|
||||||
|
from mudlib.mobs import mobs
|
||||||
from mudlib.player import Player, players
|
from mudlib.player import Player, players
|
||||||
from mudlib.render.ansi import RESET, colorize_terrain
|
from mudlib.render.ansi import RESET, colorize_terrain
|
||||||
|
|
||||||
|
|
@ -53,8 +54,30 @@ async def cmd_look(player: Player, args: str) -> None:
|
||||||
if 0 <= rel_x < VIEWPORT_WIDTH and 0 <= rel_y < VIEWPORT_HEIGHT:
|
if 0 <= rel_x < VIEWPORT_WIDTH and 0 <= rel_y < VIEWPORT_HEIGHT:
|
||||||
other_player_positions.append((rel_x, rel_y))
|
other_player_positions.append((rel_x, rel_y))
|
||||||
|
|
||||||
|
# Build a list of (relative_x, relative_y) for alive mobs
|
||||||
|
mob_positions = []
|
||||||
|
for mob in mobs:
|
||||||
|
if not mob.alive:
|
||||||
|
continue
|
||||||
|
|
||||||
|
dx = mob.x - player.x
|
||||||
|
dy = mob.y - player.y
|
||||||
|
if dx > world.width // 2:
|
||||||
|
dx -= world.width
|
||||||
|
elif dx < -(world.width // 2):
|
||||||
|
dx += world.width
|
||||||
|
if dy > world.height // 2:
|
||||||
|
dy -= world.height
|
||||||
|
elif dy < -(world.height // 2):
|
||||||
|
dy += world.height
|
||||||
|
rel_x = dx + center_x
|
||||||
|
rel_y = dy + center_y
|
||||||
|
|
||||||
|
if 0 <= rel_x < VIEWPORT_WIDTH and 0 <= rel_y < VIEWPORT_HEIGHT:
|
||||||
|
mob_positions.append((rel_x, rel_y))
|
||||||
|
|
||||||
# Build the output with ANSI coloring
|
# Build the output with ANSI coloring
|
||||||
# priority: player @ > other players * > effects > terrain
|
# priority: player @ > other players * > mobs * > effects > terrain
|
||||||
half_width = VIEWPORT_WIDTH // 2
|
half_width = VIEWPORT_WIDTH // 2
|
||||||
half_height = VIEWPORT_HEIGHT // 2
|
half_height = VIEWPORT_HEIGHT // 2
|
||||||
|
|
||||||
|
|
@ -68,6 +91,9 @@ async def cmd_look(player: Player, args: str) -> None:
|
||||||
# Check if this is another player's position
|
# Check if this is another player's position
|
||||||
elif (x, y) in other_player_positions:
|
elif (x, y) in other_player_positions:
|
||||||
line.append(colorize_terrain("*", player.color_depth))
|
line.append(colorize_terrain("*", player.color_depth))
|
||||||
|
# Check if this is a mob's position
|
||||||
|
elif (x, y) in mob_positions:
|
||||||
|
line.append(colorize_terrain("*", player.color_depth))
|
||||||
else:
|
else:
|
||||||
# Check for active effects at this world position
|
# Check for active effects at this world position
|
||||||
world_x, world_y = world.wrap(
|
world_x, world_y = world.wrap(
|
||||||
|
|
|
||||||
|
|
@ -301,3 +301,108 @@ class TestTargetResolution:
|
||||||
assert player.mode == "combat"
|
assert player.mode == "combat"
|
||||||
# Mob has no mode_stack attribute
|
# Mob has no mode_stack attribute
|
||||||
assert not hasattr(mob, "mode_stack")
|
assert not hasattr(mob, "mode_stack")
|
||||||
|
|
||||||
|
|
||||||
|
# --- Phase 3: viewport rendering tests ---
|
||||||
|
|
||||||
|
|
||||||
|
class TestViewportRendering:
|
||||||
|
@pytest.fixture
|
||||||
|
def look_world(self):
|
||||||
|
"""A mock world that returns a flat viewport of '.' tiles."""
|
||||||
|
from mudlib.commands.look import VIEWPORT_HEIGHT, VIEWPORT_WIDTH
|
||||||
|
|
||||||
|
w = MagicMock()
|
||||||
|
w.width = 256
|
||||||
|
w.height = 256
|
||||||
|
w.get_viewport = MagicMock(
|
||||||
|
return_value=[
|
||||||
|
["." for _ in range(VIEWPORT_WIDTH)]
|
||||||
|
for _ in range(VIEWPORT_HEIGHT)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
w.wrap = lambda x, y: (x % 256, y % 256)
|
||||||
|
w.is_passable = MagicMock(return_value=True)
|
||||||
|
return w
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_mob_renders_as_star(
|
||||||
|
self, player, goblin_toml, look_world
|
||||||
|
):
|
||||||
|
"""Mob within viewport renders as * in look output."""
|
||||||
|
import mudlib.commands.look as look_mod
|
||||||
|
|
||||||
|
old = look_mod.world
|
||||||
|
look_mod.world = look_world
|
||||||
|
|
||||||
|
template = load_mob_template(goblin_toml)
|
||||||
|
# Place mob 2 tiles to the right of the player
|
||||||
|
spawn_mob(template, 2, 0)
|
||||||
|
|
||||||
|
await look_mod.cmd_look(player, "")
|
||||||
|
|
||||||
|
output = "".join(
|
||||||
|
call[0][0] for call in player.writer.write.call_args_list
|
||||||
|
)
|
||||||
|
# The center is at (10, 5), mob at relative (12, 5)
|
||||||
|
# Output should contain a * character
|
||||||
|
assert "*" in output
|
||||||
|
|
||||||
|
look_mod.world = old
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_mob_outside_viewport_not_rendered(
|
||||||
|
self, player, goblin_toml, look_world
|
||||||
|
):
|
||||||
|
"""Mob outside viewport bounds is not rendered."""
|
||||||
|
import mudlib.commands.look as look_mod
|
||||||
|
|
||||||
|
old = look_mod.world
|
||||||
|
look_mod.world = look_world
|
||||||
|
|
||||||
|
template = load_mob_template(goblin_toml)
|
||||||
|
# Place mob far away
|
||||||
|
spawn_mob(template, 100, 100)
|
||||||
|
|
||||||
|
await look_mod.cmd_look(player, "")
|
||||||
|
|
||||||
|
output = "".join(
|
||||||
|
call[0][0] for call in player.writer.write.call_args_list
|
||||||
|
)
|
||||||
|
# Should only have @ (player) and . (terrain), no *
|
||||||
|
stripped = output.replace("\033[0m", "").replace("\r\n", "")
|
||||||
|
# Remove ANSI codes for terrain colors
|
||||||
|
import re
|
||||||
|
|
||||||
|
stripped = re.sub(r"\033\[[0-9;]*m", "", stripped)
|
||||||
|
assert "*" not in stripped
|
||||||
|
|
||||||
|
look_mod.world = old
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_dead_mob_not_rendered(
|
||||||
|
self, player, goblin_toml, look_world
|
||||||
|
):
|
||||||
|
"""Dead mob (alive=False) not rendered in viewport."""
|
||||||
|
import mudlib.commands.look as look_mod
|
||||||
|
|
||||||
|
old = look_mod.world
|
||||||
|
look_mod.world = look_world
|
||||||
|
|
||||||
|
template = load_mob_template(goblin_toml)
|
||||||
|
mob = spawn_mob(template, 2, 0)
|
||||||
|
mob.alive = False
|
||||||
|
|
||||||
|
await look_mod.cmd_look(player, "")
|
||||||
|
|
||||||
|
output = "".join(
|
||||||
|
call[0][0] for call in player.writer.write.call_args_list
|
||||||
|
)
|
||||||
|
import re
|
||||||
|
|
||||||
|
stripped = re.sub(r"\033\[[0-9;]*m", "", output).replace(
|
||||||
|
"\r\n", ""
|
||||||
|
)
|
||||||
|
assert "*" not in stripped
|
||||||
|
|
||||||
|
look_mod.world = old
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue