mud/src/mudlib/commands/help.py

416 lines
12 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]
# Check if move is locked
if move is not None and move.unlock_condition:
base = move.command or move.name
if base not in player.unlocked_moves:
cond = move.unlock_condition
if cond.type == "kill_count":
lock_msg = f"[LOCKED] Defeat {cond.threshold} enemies to unlock."
elif cond.type == "mob_kills":
mob = cond.mob_name
lock_msg = f"[LOCKED] Defeat {cond.threshold} {mob}s to unlock."
else:
lock_msg = "[LOCKED]"
lines.append(f" {lock_msg}")
# 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]
# Check if any variant is locked
if variants and variants[0].unlock_condition:
base = variants[0].command or variants[0].name.split()[0]
if base not in player.unlocked_moves:
cond = variants[0].unlock_condition
if cond.type == "kill_count":
lock_msg = f"[LOCKED] Defeat {cond.threshold} enemies to unlock."
elif cond.type == "mob_kills":
mob = cond.mob_name
lock_msg = f"[LOCKED] Defeat {cond.threshold} {mob}s to unlock."
else:
lock_msg = "[LOCKED]"
lines.append(f" {lock_msg}")
# 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:
# Hide admin commands from non-admins
if defn.admin and not player.is_admin:
continue
# 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")
async def cmd_client(player: Player, args: str) -> None:
"""Show client protocol negotiation and terminal capabilities."""
lines = ["client protocols"]
# Protocol status
gmcp = "active" if player.gmcp_enabled else "not active"
msdp = "active" if player.msdp_enabled else "not active"
lines.append(f" GMCP: {gmcp}")
lines.append(f" MSDP: {msdp}")
# Terminal info
lines.append("terminal")
# Terminal type from TTYPE negotiation
ttype = None
if player.writer is not None:
ttype = player.writer.get_extra_info("TERM") or None
lines.append(f" type: {ttype or 'unknown'}")
# Terminal size from NAWS
cols, rows = 80, 24
if player.writer is not None:
cols = player.writer.get_extra_info("cols") or 80
rows = player.writer.get_extra_info("rows") or 24
lines.append(f" size: {cols}x{rows}")
# Color depth
lines.append(f" colors: {player.color_depth}")
# MTTS capabilities
caps = player.caps
mtts_flags = []
if caps.ansi:
mtts_flags.append("ANSI")
if caps.vt100:
mtts_flags.append("VT100")
if caps.utf8:
mtts_flags.append("UTF-8")
if caps.colors_256:
mtts_flags.append("256 colors")
if caps.truecolor:
mtts_flags.append("truecolor")
if caps.mouse_tracking:
mtts_flags.append("mouse tracking")
if caps.screen_reader:
mtts_flags.append("screen reader")
if caps.proxy:
mtts_flags.append("proxy")
if caps.mnes:
mtts_flags.append("MNES")
if caps.mslp:
mtts_flags.append("MSLP")
if caps.ssl:
mtts_flags.append("SSL")
if mtts_flags:
lines.append(f" MTTS: {', '.join(mtts_flags)}")
else:
lines.append(" MTTS: none detected")
await player.send("\r\n".join(lines) + "\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",
)
)
# Register the client command
register(
CommandDefinition(
"client",
cmd_client,
aliases=[],
mode="*",
help="show client protocol and terminal info",
)
)
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, client\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",
)
)