mud/src/mudlib/commands/help.py

311 lines
9.1 KiB
Python

"""Help and command listing commands."""
from typing import TYPE_CHECKING, cast
from mudlib.commands import CommandDefinition, _registry, register, resolve_prefix
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
# Resolve command by exact match or prefix
result = resolve_prefix(command_name)
if isinstance(result, list):
# Multiple matches - show disambiguation
names = result
if len(names) == 2:
msg = f"{names[0]} or {names[1]}?\r\n"
else:
prefix = ", ".join(cast(list[str], names[:-1]))
msg = f"{prefix}, or {names[-1]}?\r\n"
await player.send(msg)
return
else:
defn = result
# If still 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]
# Show description first for combat moves (most important context)
if move is not None and move.description:
lines.append(f" {move.description}")
# 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 description from first variant (they all share the same one)
if variants and variants[0].description:
lines.append(f" {variants[0].description}")
# 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, or show detail for one.
Args:
player: The player executing the command
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] = []
for defn in _registry.values():
defn_id = id(defn)
if defn_id not in seen and not defn.hidden:
seen.add(defn_id)
unique_commands.append(defn)
# Group commands by type (excluding combat moves)
movement: list[CommandDefinition] = []
other: list[CommandDefinition] = []
for defn in unique_commands:
# Check if it's a movement command
if defn.name in DIRECTIONS:
movement.append(defn)
# Skip combat attacks and defenses
elif defn.help.startswith("Attack with") or defn.help.startswith("Defend with"):
continue
# Everything else
else:
other.append(defn)
all_commands = movement + other
all_commands.sort(key=lambda d: d.name)
names = [d.name for d in all_commands]
await player.send(" ".join(names) + "\r\n")
async def cmd_skills(player: Player, args: str) -> None:
"""List all combat skills (attacks and defenses), or show detail for one.
Args:
player: The player executing the command
args: Command arguments (skill name for detail view, empty for list)
"""
# If args provided, show detail view for that skill
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] = []
for defn in _registry.values():
defn_id = id(defn)
if defn_id not in seen and not defn.hidden:
seen.add(defn_id)
unique_commands.append(defn)
# Group combat moves
attacks: list[CommandDefinition] = []
defenses: list[CommandDefinition] = []
for defn in unique_commands:
# Check if it's a combat attack
if defn.help.startswith("Attack with"):
attacks.append(defn)
# Check if it's a combat defense
elif defn.help.startswith("Defend with"):
defenses.append(defn)
all_skills = attacks + defenses
all_skills.sort(key=lambda d: d.name)
names = [d.name for d in all_skills]
await player.send(" ".join(names) + "\r\n")
# Register the commands command
register(
CommandDefinition(
"commands",
cmd_commands,
aliases=["cmds"],
mode="*",
help="list available commands",
)
)
# Register the skills command
register(
CommandDefinition(
"skills",
cmd_skills,
aliases=[],
mode="*",
help="list combat skills",
)
)
async def cmd_help(player: Player, args: str) -> None:
"""Show help for a command or skill.
Args:
player: The player executing the command
args: Command name to get help for
"""
args = args.strip()
if not args:
await player.send(
"type help <command> for details. see also: commands, skills\r\n"
)
return
await _show_command_detail(player, args)
# Register the help command
register(
CommandDefinition(
"help",
cmd_help,
aliases=[],
mode="*",
help="show help for a command",
)
)