Add detail view to commands command
This commit is contained in:
parent
6fb48e9ae1
commit
1b63f87da7
2 changed files with 266 additions and 2 deletions
|
|
@ -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] = []
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue