Captures the combat-as-content architecture: moves are TOML files, engine is a state machine (IDLE→TELEGRAPH→WINDOW→RESOLVE). Documents move set, counter relationships, damage formulas, and how combat initiation works without a dedicated fight command.
235 lines
6.6 KiB
Text
235 lines
6.6 KiB
Text
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).
|