Refactor look/examine targeting and improve room rendering
This commit is contained in:
parent
8424404d27
commit
f40ee68f9a
5 changed files with 108 additions and 108 deletions
|
|
@ -3,50 +3,55 @@
|
|||
from mudlib.commands import CommandDefinition, register
|
||||
from mudlib.entity import Entity
|
||||
from mudlib.player import Player
|
||||
from mudlib.targeting import find_entity_on_tile, find_in_inventory, find_thing_on_tile
|
||||
from mudlib.thing import Thing
|
||||
from mudlib.zone import Zone
|
||||
|
||||
|
||||
def _find_object_in_inventory(name: str, player: Player) -> Thing | Entity | None:
|
||||
"""Find an object in player inventory by name or alias."""
|
||||
name_lower = name.lower()
|
||||
for obj in player.contents:
|
||||
# Only examine Things and Entities
|
||||
if not isinstance(obj, (Thing, Entity)):
|
||||
continue
|
||||
async def examine_target(
|
||||
player: Player,
|
||||
target_name: str,
|
||||
*,
|
||||
prefer_inventory: bool = True,
|
||||
) -> None:
|
||||
"""Resolve and describe a target for examine/look style commands."""
|
||||
zone = player.location if isinstance(player.location, Zone) else None
|
||||
|
||||
# Match by name
|
||||
if obj.name.lower() == name_lower:
|
||||
return obj
|
||||
# look <thing> should prioritize entities/ground; direct examine keeps
|
||||
# historical inventory-first behavior.
|
||||
ordered_finders = []
|
||||
if prefer_inventory:
|
||||
ordered_finders.append(lambda: find_in_inventory(target_name, player))
|
||||
ordered_finders.append(lambda: find_entity_on_tile(target_name, player))
|
||||
if zone is not None:
|
||||
ordered_finders.append(
|
||||
lambda: find_thing_on_tile(target_name, zone, player.x, player.y)
|
||||
)
|
||||
if not prefer_inventory:
|
||||
ordered_finders.append(lambda: find_in_inventory(target_name, player))
|
||||
|
||||
# Match by alias (Things have aliases)
|
||||
if isinstance(obj, Thing) and name_lower in (a.lower() for a in obj.aliases):
|
||||
return obj
|
||||
found: Thing | Entity | None = None
|
||||
for finder in ordered_finders:
|
||||
found = finder()
|
||||
if found is not None:
|
||||
break
|
||||
|
||||
return None
|
||||
if found is None:
|
||||
await player.send("You don't see that here.\r\n")
|
||||
return
|
||||
|
||||
if isinstance(found, Entity):
|
||||
if getattr(found, "description", ""):
|
||||
await player.send(f"{found.description}\r\n")
|
||||
else:
|
||||
await player.send(f"{found.name} is {found.posture}.\r\n")
|
||||
return
|
||||
|
||||
def _find_object_at_position(name: str, player: Player) -> Thing | Entity | None:
|
||||
"""Find an object on the ground at player position by name or alias."""
|
||||
zone = player.location
|
||||
if zone is None or not isinstance(zone, Zone):
|
||||
return None
|
||||
|
||||
name_lower = name.lower()
|
||||
for obj in zone.contents_at(player.x, player.y):
|
||||
# Only examine Things and Entities
|
||||
if not isinstance(obj, (Thing, Entity)):
|
||||
continue
|
||||
|
||||
# Match by name
|
||||
if obj.name.lower() == name_lower:
|
||||
return obj
|
||||
|
||||
# Match by alias (Things have aliases)
|
||||
if isinstance(obj, Thing) and name_lower in (a.lower() for a in obj.aliases):
|
||||
return obj
|
||||
|
||||
return None
|
||||
desc = getattr(found, "description", "")
|
||||
if desc:
|
||||
await player.send(f"{desc}\r\n")
|
||||
else:
|
||||
await player.send("You see nothing special.\r\n")
|
||||
|
||||
|
||||
async def cmd_examine(player: Player, args: str) -> None:
|
||||
|
|
@ -55,26 +60,7 @@ async def cmd_examine(player: Player, args: str) -> None:
|
|||
await player.send("Examine what?\r\n")
|
||||
return
|
||||
|
||||
target_name = args.strip()
|
||||
|
||||
# Search inventory first
|
||||
found = _find_object_in_inventory(target_name, player)
|
||||
|
||||
# Then search ground
|
||||
if not found:
|
||||
found = _find_object_at_position(target_name, player)
|
||||
|
||||
# Not found anywhere
|
||||
if not found:
|
||||
await player.send("You don't see that here.\r\n")
|
||||
return
|
||||
|
||||
# Show description (both Thing and Entity have description)
|
||||
desc = getattr(found, "description", "")
|
||||
if desc:
|
||||
await player.send(f"{desc}\r\n")
|
||||
else:
|
||||
await player.send("You see nothing special.\r\n")
|
||||
await examine_target(player, args.strip(), prefer_inventory=True)
|
||||
|
||||
|
||||
register(CommandDefinition("examine", cmd_examine, aliases=["ex"], mode="*"))
|
||||
|
|
|
|||
|
|
@ -37,52 +37,11 @@ async def cmd_look(player: Player, args: str) -> None:
|
|||
player: The player executing the command
|
||||
args: Command arguments (if provided, use targeting to resolve)
|
||||
"""
|
||||
# If args provided, use targeting to resolve
|
||||
# If args provided, route directly to examine behavior.
|
||||
if args.strip():
|
||||
from mudlib.targeting import (
|
||||
find_entity_on_tile,
|
||||
find_in_inventory,
|
||||
find_thing_on_tile,
|
||||
)
|
||||
from mudlib.commands.examine import examine_target
|
||||
|
||||
target_name = args.strip()
|
||||
|
||||
# First try to find an entity on the tile
|
||||
entity = find_entity_on_tile(target_name, player)
|
||||
if entity:
|
||||
# Show entity info (name and posture)
|
||||
if hasattr(entity, "description") and entity.description:
|
||||
await player.send(f"{entity.description}\r\n")
|
||||
else:
|
||||
await player.send(f"{entity.name} is {entity.posture}.\r\n")
|
||||
return
|
||||
|
||||
# Then try to find a thing on the ground
|
||||
zone = player.location
|
||||
if zone is not None and isinstance(zone, Zone):
|
||||
thing = find_thing_on_tile(target_name, zone, player.x, player.y)
|
||||
if thing:
|
||||
# Show thing description
|
||||
desc = getattr(thing, "description", "")
|
||||
if desc:
|
||||
await player.send(f"{desc}\r\n")
|
||||
else:
|
||||
await player.send("You see nothing special.\r\n")
|
||||
return
|
||||
|
||||
# Finally try inventory
|
||||
thing = find_in_inventory(target_name, player)
|
||||
if thing:
|
||||
# Show thing description
|
||||
desc = getattr(thing, "description", "")
|
||||
if desc:
|
||||
await player.send(f"{desc}\r\n")
|
||||
else:
|
||||
await player.send("You see nothing special.\r\n")
|
||||
return
|
||||
|
||||
# Nothing found
|
||||
await player.send("You don't see that here.\r\n")
|
||||
await examine_target(player, args.strip(), prefer_inventory=False)
|
||||
return
|
||||
|
||||
zone = player.location
|
||||
|
|
@ -220,7 +179,7 @@ async def cmd_look(player: Player, args: str) -> None:
|
|||
output.append(render_nearby(nearby_entities, player))
|
||||
|
||||
# Exits line
|
||||
output.append(render_exits(zone, player.x, player.y))
|
||||
output.append(render_exits(zone, player.x, player.y, player))
|
||||
|
||||
# Send to player
|
||||
player.writer.write("\r\n".join(output) + "\r\n")
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ def render_nearby(entities: list, viewer) -> str:
|
|||
return f"Nearby: ({count}) {names}"
|
||||
|
||||
|
||||
def render_exits(zone, x: int, y: int) -> str:
|
||||
def render_exits(zone, x: int, y: int, viewer=None) -> str:
|
||||
"""Render available exits from current position.
|
||||
|
||||
Args:
|
||||
|
|
@ -94,7 +94,6 @@ def render_exits(zone, x: int, y: int) -> str:
|
|||
exits = []
|
||||
|
||||
# Check cardinal directions
|
||||
# NOTE: up/down exits deferred until z-axis movement is implemented
|
||||
if zone.is_passable(x, y - 1): # north (y decreases going up)
|
||||
exits.append("north")
|
||||
if zone.is_passable(x, y + 1): # south
|
||||
|
|
@ -103,6 +102,11 @@ def render_exits(zone, x: int, y: int) -> str:
|
|||
exits.append("east")
|
||||
if zone.is_passable(x - 1, y): # west
|
||||
exits.append("west")
|
||||
# Vertical exit is based on current altitude state.
|
||||
if viewer is not None and getattr(viewer, "flying", False):
|
||||
exits.append("down")
|
||||
elif viewer is not None:
|
||||
exits.append("up")
|
||||
|
||||
if exits:
|
||||
return f"Exits: {' '.join(exits)}"
|
||||
|
|
@ -111,6 +115,7 @@ def render_exits(zone, x: int, y: int) -> str:
|
|||
|
||||
_POSTURE_MESSAGES = {
|
||||
"standing": "is standing here.",
|
||||
"sleeping": "is sleeping here.",
|
||||
"resting": "is resting here.",
|
||||
"flying": "is flying above.",
|
||||
"fighting": "is fighting here.",
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ async def test_look_includes_exits_line(player):
|
|||
"""Look output should include 'Exits:' line."""
|
||||
await cmd_look(player, "")
|
||||
output = get_output(player)
|
||||
assert "Exits: north south east west" in output
|
||||
assert "Exits: north south east west up" in output
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -169,14 +169,36 @@ async def test_look_shows_portals(player, test_zone):
|
|||
@pytest.mark.asyncio
|
||||
async def test_look_with_args_routes_to_examine(player, test_zone):
|
||||
"""look <thing> should route to examine command logic."""
|
||||
# Add an item to examine (location param auto-adds to zone)
|
||||
Thing(name="sword", x=25, y=25, description="A sharp blade.", location=test_zone)
|
||||
called = {}
|
||||
|
||||
async def fake_examine(p, args, *, prefer_inventory=True):
|
||||
called["args"] = args
|
||||
called["prefer_inventory"] = prefer_inventory
|
||||
await p.send("examined\r\n")
|
||||
|
||||
import mudlib.commands.examine
|
||||
|
||||
original = mudlib.commands.examine.examine_target
|
||||
mudlib.commands.examine.examine_target = fake_examine # type: ignore[invalid-assignment]
|
||||
try:
|
||||
await cmd_look(player, "sword")
|
||||
finally:
|
||||
mudlib.commands.examine.examine_target = original
|
||||
|
||||
await cmd_look(player, "sword")
|
||||
output = get_output(player)
|
||||
assert called["args"] == "sword"
|
||||
assert called["prefer_inventory"] is False
|
||||
assert "examined" in output
|
||||
|
||||
# Should see the item's description (examine behavior)
|
||||
assert "A sharp blade." in output
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_look_flying_shows_down_exit(player):
|
||||
"""Flying players should see down as a vertical exit."""
|
||||
player.flying = True
|
||||
|
||||
await cmd_look(player, "")
|
||||
output = get_output(player)
|
||||
assert "Exits: north south east west down" in output
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
|
|
@ -126,6 +126,30 @@ def test_render_exits_partial():
|
|||
assert result == "Exits: north east"
|
||||
|
||||
|
||||
def test_render_exits_includes_up_for_grounded_viewer():
|
||||
"""Grounded viewer should see up as a vertical exit."""
|
||||
terrain = [["." for _ in range(3)] for _ in range(3)]
|
||||
zone = Zone(name="test", width=3, height=3, terrain=terrain, impassable={"^"})
|
||||
|
||||
class Viewer:
|
||||
flying = False
|
||||
|
||||
result = render_exits(zone, 1, 1, Viewer())
|
||||
assert result == "Exits: north south east west up"
|
||||
|
||||
|
||||
def test_render_exits_includes_down_for_flying_viewer():
|
||||
"""Flying viewer should see down as a vertical exit."""
|
||||
terrain = [["." for _ in range(3)] for _ in range(3)]
|
||||
zone = Zone(name="test", width=3, height=3, terrain=terrain, impassable={"^"})
|
||||
|
||||
class Viewer:
|
||||
flying = True
|
||||
|
||||
result = render_exits(zone, 1, 1, Viewer())
|
||||
assert result == "Exits: north south east west down"
|
||||
|
||||
|
||||
def test_render_entity_lines_empty():
|
||||
"""render_entity_lines returns empty string when no entities."""
|
||||
assert render_entity_lines([], None) == ""
|
||||
|
|
@ -169,3 +193,7 @@ def test_render_entity_lines_postures():
|
|||
# Unconscious (stamina <= 0)
|
||||
mob_exhausted = Mob(name="Goku", x=10, y=10, stamina=0)
|
||||
assert render_entity_lines([mob_exhausted], None) == "Goku is unconscious."
|
||||
|
||||
# Sleeping
|
||||
mob_sleeping = Mob(name="Goku", x=10, y=10, sleeping=True, resting=True)
|
||||
assert render_entity_lines([mob_sleeping], None) == "Goku is sleeping here."
|
||||
|
|
|
|||
Loading…
Reference in a new issue