command system
==============

commands are registered as CommandDefinition objects with metadata.

    from mudlib.commands import CommandDefinition, register

    async def my_command(player: Player, args: str) -> None:
        player.writer.write("hello\r\n")
        await player.writer.drain()

    register(CommandDefinition("mycommand", my_command, aliases=["mc"]))

CommandDefinition fields:

    name        primary command name
    handler     async function(player, args)
    aliases     alternative names (default: [])
    mode        required player mode (default: "normal", "*" = any mode)
    help        help text (default: "")

dispatch parses input, looks up the definition, calls its handler:

    await dispatch(player, "mycommand some args")

if the command isn't found, the player gets "Unknown command: ..."

movement
--------

8 directions, each with short and long aliases:

    n/north     s/south     e/east      w/west
    ne/northeast  nw/northwest  se/southeast  sw/southwest

movement checks passability before updating position. impassable terrain
gives "You can't go that way."

nearby players see arrival/departure messages:

    jared leaves east.
    jared arrives from the west.

"nearby" = within viewport range (10 tiles) of old or new position.

look
----

look/l renders the viewport. player is always @ at center. other players
show as *. output is ANSI-colored.

adding commands
---------------

1. create src/mudlib/commands/yourcommand.py
2. import CommandDefinition and register from mudlib.commands
3. define async handler(player, args)
4. call register(CommandDefinition(...))
5. import the module in server.py so registration runs at startup

combat commands
---------------

combat moves are defined in TOML files in content/combat/. each file describes
a move (attack, defend, counter) with metadata:

    name
    type (attack/defend)
    stamina_cost
    telegraph (what the opponent sees)
    timing_window_ms (how long to react)
    damage_pct (base damage as % of attacker PL)
    counters (list of valid defensive moves)

the combat system loads these at startup and registers them as commands via
combat/commands.py. when combat mode is active, these commands become available.
players use the move name to execute it during their turn.

content-defined commands
------------------------

commands can be defined in TOML files in content/commands/. these are loaded
at startup by content.py and registered alongside python-defined commands.

format:

    name = "example"
    aliases = ["ex"]
    help = "example command help text"
    mode = "normal"

content commands currently require a python handler (body field pointing to a
callable). when the DSL arrives, this will be replaced with inline scripting.

editor mode
-----------

the edit command pushes editor mode onto the mode stack. while editor mode is
active, all input bypasses the command dispatcher and goes to the editor buffer
instead.

the editor provides:

    line editing with insert/append/delete/replace
    search and replace (regex supported)
    syntax highlighting for TOML/Python
    save/discard changes
    dirty flag tracking

quit (:q) pops editor mode and returns to normal. the editor is implemented in
editor.py and uses the mode stack to capture input.

code
----

src/mudlib/commands/__init__.py    registry + dispatch + CommandDefinition
src/mudlib/commands/movement.py    direction commands
src/mudlib/commands/look.py        look/l
src/mudlib/commands/fly.py         fly
src/mudlib/commands/quit.py        quit/q (mode="*")
src/mudlib/combat/commands.py      combat move commands (loaded from TOML)
src/mudlib/content.py              content loading (commands and combat moves)
src/mudlib/editor.py               editor mode and buffer management
src/mudlib/player.py               Player dataclass + registry
