diff --git a/docs/how/combat.txt b/docs/how/combat.txt new file mode 100644 index 0000000..ffa37f4 --- /dev/null +++ b/docs/how/combat.txt @@ -0,0 +1,235 @@ +combat system — fighting as content +=================================== + +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 + + +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 (pr) + basic right hook. fast, low cost. + + punch left (pl) + basic left hook. 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 (dr) + sidestep to the right. counters left-side attacks. + + dodge left (dl) + sidestep to the left. counters right-side attacks. + + parry high (f) + block high attacks. tighter timing window than dodge. + + parry low (v) + block low 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 definition +===================== + +example: content/combat/punch_right.toml + + name = "punch right" + aliases = ["pr"] + move_type = "attack" + stamina_cost = 5.0 + telegraph = "{attacker} winds up a right hook!" + timing_window_ms = 800 + damage_pct = 0.15 + countered_by = ["dodge left", "parry high"] + +move_type: "attack" or "defense" +telegraph: shown to defender during TELEGRAPH phase. {attacker} replaced with name +timing_window_ms: how long defender has to respond +damage_pct: fraction of attacker's PL dealt as damage +countered_by: list of move names that counter this move + + +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. + + +implementation notes +==================== + +the engine provides: + - CombatMove dataclass + TOML loader + - CombatEncounter state machine (attack/defend/tick/resolve) + - global encounter list + management functions + - process_combat() called each game loop tick + - command handlers that look up moves and call encounter methods + +content provides: + - TOML files for each move in content/combat/ + - balance tuning (stamina costs, damage_pct, timing windows) + +the command registry makes moves available as commands automatically. +"punch right" is both a move name AND a command name. the command handler +looks up the move definition and feeds it to the encounter. + +this keeps the python side generic. adding a new move = add a TOML file, +no code changes. balance changes = edit TOML values, no restart needed +(future: hot-reload content).