============================== 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.]. 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.