==============================
combat system — moves as data
==============================

combat in this MUD is designed around the principle that moves are content,
not engine code. the engine provides a state machine and timing system. moves
are data files that define telegraphs, windows, and counters. this keeps the
combat system extensible without touching python.


core design principles
======================

1. combat moves are CONTENT (TOML files in content/combat/)
2. the engine is a STATE MACHINE that processes timing and resolution
3. direction matters — punch right is countered by dodge left
4. no "fight" command — first attack initiates combat
5. after combat starts, target is implicit until encounter ends


command structure
=================

commands are always a single word. the dispatcher splits input on the first
space: first word is the command, the rest is args.

some moves have directional variants. the direction is an argument to the
command, not part of the command name::

  punch right frank    → command "punch", args "right frank"
  punch left           → command "punch", args "left"
  roundhouse frank     → command "roundhouse", args "frank"
  sweep                → command "sweep", args ""

variant moves (punch, dodge, parry) require a direction. simple moves
(roundhouse, sweep, duck, jump) don't.

each variant has its own short alias registered as a standalone command::

  pr frank             → "punch right frank" (pr is a direct command)
  dl                   → "dodge left"
  f                    → "parry high"

aliases are defined in the TOML file and registered at startup. they bypass
the variant parsing entirely — the handler already knows which move it is.


the state machine
=================

::

  IDLE → TELEGRAPH → WINDOW → RESOLVE → IDLE

IDLE:
  no active move. attacker can initiate attack. defender can do nothing
  combat-specific.

TELEGRAPH:
  attacker has declared a move. defender sees the telegraph message.
  "goku winds up a right hook!"
  defender can queue a defense during this phase.
  duration: brief (implementation decides, not move-defined).

WINDOW:
  the timing window opens. defender can still queue defense.
  if defender queued correct counter during TELEGRAPH or WINDOW, they succeed.
  duration: move.timing_window_ms (defined in TOML).

RESOLVE:
  timing window closes. check if defense counters attack.
  calculate damage based on attacker PL, damage_pct, and defense success.
  apply stamina costs.
  check for knockouts (PL = 0) or exhaustion (stamina = 0).
  return to IDLE.


entity stats
============

each Entity (player, mob, NPC) has::

  pl: float = 100.0
    power level. acts as both health AND damage multiplier.
    when PL hits 0, entity is knocked out.

  stamina: float = 100.0
    current stamina. each move costs stamina.
    when stamina hits 0, entity passes out.

  max_stamina: float = 100.0
    stamina ceiling. can be affected by training, items, etc (future).


the move set
============

ATTACKS:
  punch right/left (pr/pl)
    basic hooks. fast, low cost.

  roundhouse (rh)
    big spinning kick. higher damage, tighter parry window.

  sweep (sw)
    leg sweep. knocks down if not countered.

DEFENSES:
  dodge right/left (dr/dl)
    sidestep. counters opposite-side attacks.

  parry high/low (f/v)
    block attacks. tighter timing window than dodge.

  duck
    crouch under high attacks. counters roundhouse.

  jump
    leap over low attacks. counters sweep.


counter relationships
=====================

defined in each move's TOML file via the "countered_by" field.

::

  punch right → countered_by: [dodge left, parry high]
  punch left → countered_by: [dodge right, parry high]
  roundhouse → countered_by: [duck, parry high, parry low]
  sweep → countered_by: [jump, parry low]

the engine doesn't know these relationships. it just checks:
"is defender's move in attacker's move.countered_by list?"


damage formula
==============

if defense succeeds (is in countered_by list)::

  damage = 0
  defender gets "you countered the attack!" message

if defense fails (wrong move or no move)::

  damage = attacker.pl * move.damage_pct
  defender.pl -= damage

if no defense was attempted::

  damage = attacker.pl * move.damage_pct * 1.5
  defender.pl -= damage
  defender gets "you took the hit full force!" message

stamina costs are always applied, regardless of outcome::

  attacker.stamina -= move.stamina_cost
  if defender attempted defense:
    defender.stamina -= defense_move.stamina_cost


starting combat
===============

NO explicit "fight" command. instead::

  punch right goku
    if player is not in combat: starts encounter with goku, pushes "combat" mode
    if player is already in combat: error, already fighting someone else

after combat starts, target is implicit::

  punch left
  dodge right
  roundhouse

all commands assume the current opponent.


exiting combat
==============

combat ends when:

- one combatant's PL reaches 0 (knockout)
- one combatant's stamina reaches 0 (exhaustion)
- one combatant flees (future: flee command)
- one combatant is killed (future: lethal combat toggle)

when combat ends, both entities' mode stacks pop back to "normal".


TOML move definitions
=====================

moves live in content/combat/. two formats: simple and variant.

**simple move** (one file, one command)::

  # content/combat/roundhouse.toml
  name = "roundhouse"
  aliases = ["rh"]
  move_type = "attack"
  stamina_cost = 8.0
  telegraph = "{attacker} spins into a roundhouse kick!"
  timing_window_ms = 600
  damage_pct = 0.25
  countered_by = ["duck", "parry high", "parry low"]

**variant move** (one file, directional variants)::

  # content/combat/punch.toml
  name = "punch"
  move_type = "attack"
  stamina_cost = 5.0
  timing_window_ms = 800
  damage_pct = 0.15

  [variants.left]
  aliases = ["pl"]
  telegraph = "{attacker} winds up a left hook!"
  countered_by = ["dodge right", "parry high"]

  [variants.right]
  aliases = ["pr"]
  telegraph = "{attacker} winds up a right hook!"
  countered_by = ["dodge left", "parry high"]

shared properties (stamina_cost, timing_window_ms, damage_pct) are defined at
the top level. variants inherit these and can override them. variant-specific
properties (telegraph, countered_by, aliases) live under [variants.<key>].

each variant produces a CombatMove with a qualified name like "punch left".
the ``command`` field tracks the base command ("punch") and ``variant`` tracks
the key ("left"). simple moves have command=name and variant="".

TOML field reference:

- name: the command name (simple) or base command (variant)
- move_type: "attack" or "defense"
- stamina_cost: stamina consumed when using this move
- timing_window_ms: how long defender has to respond
- telegraph: shown to defender during TELEGRAPH phase. {attacker} replaced
- damage_pct: fraction of attacker's PL dealt as damage
- countered_by: list of qualified move names that counter this move
- aliases: short command aliases registered as standalone commands


handler architecture
====================

three types of command handlers, created at registration time:

**direct handler** — for simple moves (roundhouse, sweep) and variant aliases
(pl, pr). the move is bound at registration via closure. args = target name.

**variant handler** — for base variant commands (punch, dodge, parry). parses
first arg as direction, looks up the variant, passes remaining args as target.

**core functions** — do_attack() and do_defend() contain the actual combat
logic. they take (player, target_args, move) and handle encounter creation,
stamina checks, telegraph sending, etc. handlers call into these.

this means adding a new move = add a TOML file. the registration code groups
variants automatically, creates the right handler type, and registers aliases.


future features (NOT YET IMPLEMENTED)
======================================

stuns:
  successful parry stuns opponent for 1 tick.
  during stun, engine shows combo sequence in shorthand:
  "you can follow up with: pr/pl/sw or pr/pl/rh"
  player has narrow window to input the sequence.

combos:
  if player executes shown sequence during stun window, bonus damage applied.
  after successful combo, chance to chain (diminishing probability).
  longer chains = exponentially harder timing.

special moves:
  moves that require conditions (opponent stunned, player at high PL, etc).
  defined via "requires" field in TOML.

lethal combat:
  toggle that makes PL=0 mean death, not knockout.
  affects NPC encounters in dangerous zones.

multi-combatant:
  extend encounter to support more than 1v1.
  each attacker/defender pair still follows same state machine.
