Add detail view to commands command

This commit is contained in:
Jared Miller 2026-02-08 12:41:02 -05:00
parent 6fb48e9ae1
commit 1b63f87da7
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
2 changed files with 266 additions and 2 deletions

View file

@ -1,17 +1,170 @@
"""Help and command listing commands."""
from typing import TYPE_CHECKING
from mudlib.commands import CommandDefinition, _registry, register
from mudlib.commands.movement import DIRECTIONS
from mudlib.player import Player
if TYPE_CHECKING:
from mudlib.combat.moves import CombatMove
async def _show_command_detail(player: Player, command_name: str) -> None:
"""Show detailed information about a specific command.
Args:
player: The player requesting details
command_name: Name or alias of the command to show
"""
from mudlib.combat.commands import combat_moves
# Look up the command in registry
defn = _registry.get(command_name)
# If not in registry, check if it's a combat move name (like "punch left")
if defn is None:
move = combat_moves.get(command_name)
if move is not None:
# Create a synthetic defn for display purposes
defn = CommandDefinition(
name=move.name,
handler=lambda p, a: None,
aliases=move.aliases,
mode="*",
help="",
)
await _show_single_command(player, defn, move)
return
await player.send(f"Unknown command: {command_name}\r\n")
return
# Try to get combat move data
move = combat_moves.get(defn.name)
# Check if this is a variant base command (like "punch")
# by looking for variants in combat_moves
variants = []
if move is None:
for _move_name, m in combat_moves.items():
if m.command == defn.name and m.variant:
variants.append(m)
# If we found variants, show variant overview
if variants:
await _show_variant_overview(player, defn, variants)
return
# Otherwise show single command detail
await _show_single_command(player, defn, move)
async def _show_single_command(
player: Player, defn: CommandDefinition, move: "CombatMove | None"
) -> None:
"""Show details for a single command or combat move.
Args:
player: The player requesting details
defn: Command definition
move: Combat move data if available
"""
lines = [defn.name]
# Always show aliases
if defn.aliases:
aliases_str = ", ".join(defn.aliases)
lines.append(f" aliases: {aliases_str}")
# Show type for combat moves
if move is not None:
lines.append(f" type: {move.move_type}")
# Show mode for non-wildcard modes
if defn.mode != "*":
lines.append(f" mode: {defn.mode}")
# Combat move specific details
if move is not None:
lines.append(f" stamina: {move.stamina_cost}")
lines.append(f" timing window: {move.timing_window_ms}ms")
if move.damage_pct > 0:
damage_pct = int(move.damage_pct * 100)
lines.append(f" damage: {damage_pct}%")
if move.telegraph:
lines.append(f" telegraph: {move.telegraph}")
if move.countered_by:
counters = ", ".join(move.countered_by)
lines.append(f" countered by: {counters}")
else:
# Show help text for non-combat commands
if defn.help:
lines.append(f" help: {defn.help}")
await player.send("\r\n".join(lines) + "\r\n")
async def _show_variant_overview(
player: Player, defn: CommandDefinition, variants: list["CombatMove"]
) -> None:
"""Show overview of a variant command with all its variants.
Args:
player: The player requesting details
defn: Command definition for the base command
variants: List of variant moves
"""
# Sort variants by name for consistent display
variants = sorted(variants, key=lambda m: m.name)
lines = [defn.name]
# Show type from first variant
if variants:
lines.append(f" type: {variants[0].move_type}")
# Show each variant
for move in variants:
lines.append("")
lines.append(f" {move.name}")
if move.aliases:
aliases_str = ", ".join(move.aliases)
lines.append(f" aliases: {aliases_str}")
lines.append(f" stamina: {move.stamina_cost}")
lines.append(f" timing window: {move.timing_window_ms}ms")
if move.damage_pct > 0:
damage_pct = int(move.damage_pct * 100)
lines.append(f" damage: {damage_pct}%")
if move.telegraph:
lines.append(f" telegraph: {move.telegraph}")
if move.countered_by:
counters = ", ".join(move.countered_by)
lines.append(f" countered by: {counters}")
await player.send("\r\n".join(lines) + "\r\n")
async def cmd_commands(player: Player, args: str) -> None:
"""List all available commands grouped by type.
"""List all available commands grouped by type, or show detail for one.
Args:
player: The player executing the command
args: Command arguments (unused)
args: Command arguments (command name for detail view, empty for list)
"""
# If args provided, show detail view for that command
if args.strip():
await _show_command_detail(player, args.strip())
return
# Otherwise, show full list
# Collect unique commands by CommandDefinition id (avoids alias duplication)
seen: set[int] = set()
unique_commands: list[CommandDefinition] = []

View file

@ -1,5 +1,6 @@
"""Tests for the commands listing command."""
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock
import pytest
@ -43,6 +44,23 @@ def player(mock_reader, mock_writer):
)
@pytest.fixture
def combat_moves():
"""Load and register combat moves from content directory."""
from mudlib.combat.commands import register_combat_commands
combat_dir = Path(__file__).resolve().parents[1] / "content" / "combat"
register_combat_commands(combat_dir)
from mudlib.combat import commands as combat_cmds
yield combat_cmds.combat_moves
# Clean up
combat_cmds.combat_moves = {}
# Note: commands stay registered in _registry, but that's ok for tests
@pytest.mark.asyncio
async def test_commands_command_exists():
"""Test that commands command is registered."""
@ -142,3 +160,96 @@ async def test_commands_via_alias(player):
# Should produce the same output structure
assert "Movement:" in output
assert "Other:" in output
@pytest.mark.asyncio
async def test_commands_detail_regular_command(player):
"""Test commands detail view for a regular command."""
await commands.dispatch(player, "commands look")
output = "".join([call[0][0] for call in player.writer.write.call_args_list])
assert "look" in output
assert "aliases: l" in output
assert "mode: normal" in output
@pytest.mark.asyncio
async def test_commands_detail_simple_combat_move(player, combat_moves):
"""Test commands detail view for a simple combat move."""
await commands.dispatch(player, "commands roundhouse")
output = "".join([call[0][0] for call in player.writer.write.call_args_list])
assert "roundhouse" in output
assert "aliases: rh" in output
assert "type: attack" in output
assert "stamina: 8.0" in output
assert "timing window: 2000ms" in output
assert "damage: 25%" in output
assert "{attacker} spins into a roundhouse kick!" in output
assert "countered by: duck, parry high, parry low" in output
@pytest.mark.asyncio
async def test_commands_detail_variant_base(player, combat_moves):
"""Test commands detail view for a variant base command."""
await commands.dispatch(player, "commands punch")
output = "".join([call[0][0] for call in player.writer.write.call_args_list])
assert "punch" in output
assert "type: attack" in output
# Should show both variants
assert "punch left" in output
assert "aliases: pl" in output
assert "{attacker} winds up a left hook!" in output
assert "countered by: dodge right, parry high" in output
assert "punch right" in output
assert "aliases: pr" in output
assert "{attacker} winds up a right hook!" in output
assert "countered by: dodge left, parry high" in output
# Should show shared properties in each variant
assert "stamina: 5.0" in output
assert "timing window: 1800ms" in output
assert "damage: 15%" in output
@pytest.mark.asyncio
async def test_commands_detail_specific_variant(player, combat_moves):
"""Test commands detail view for a specific variant."""
await commands.dispatch(player, "commands punch left")
output = "".join([call[0][0] for call in player.writer.write.call_args_list])
assert "punch left" in output
assert "aliases: pl" in output
assert "type: attack" in output
assert "stamina: 5.0" in output
assert "timing window: 1800ms" in output
assert "damage: 15%" in output
assert "{attacker} winds up a left hook!" in output
assert "countered by: dodge right, parry high" in output
# Should NOT show "punch right"
assert "punch right" not in output
@pytest.mark.asyncio
async def test_commands_detail_unknown_command(player):
"""Test commands detail view for an unknown command."""
await commands.dispatch(player, "commands nonexistent")
output = "".join([call[0][0] for call in player.writer.write.call_args_list])
assert "Unknown command: nonexistent" in output
@pytest.mark.asyncio
async def test_commands_detail_via_alias(player, combat_moves):
"""Test commands detail view via an alias."""
await commands.dispatch(player, "commands rh")
output = "".join([call[0][0] for call in player.writer.write.call_args_list])
# Should show the full command details, not the alias
assert "roundhouse" in output
assert "aliases: rh" in output
assert "type: attack" in output