"""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 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", ) )