385 lines
11 KiB
Python
385 lines
11 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")
|
|
|
|
|
|
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",
|
|
)
|
|
)
|