From 35c544e93158061a9a294f3ab5842031cf4253ad Mon Sep 17 00:00:00 2001 From: Parley Hatch Date: Tue, 2 Jun 2026 23:40:50 -0600 Subject: [PATCH] docs(combat): design spec for combined-pool combat system experiment New sandbox experiment: a deterministic, fully-parameterized real-time combat engine (EQ/Daggerfall lineage) built on a single combined Vigor pool (health+endurance+mana) with ceiling/fatigue attrition, capacity-as-fill stagger cascade, symmetric Actors + pluggable controllers, one composable Ability model (skills==spells), and classless three-layer emergent identity. Sim-first: headless deterministic engine + batch sweep harness + CSV/JSON export for data-driven balancing. Rust engine (combat-core/combat-sim) + Python analysis layer, in its own decoupled combat/ workspace. Co-Authored-By: Claude Opus 4.8 (1M context) --- .dev/2026-06-02-combat-system/spec/design.md | 392 +++++++++++++++++++ 1 file changed, 392 insertions(+) create mode 100644 .dev/2026-06-02-combat-system/spec/design.md diff --git a/.dev/2026-06-02-combat-system/spec/design.md b/.dev/2026-06-02-combat-system/spec/design.md new file mode 100644 index 0000000..4be950d --- /dev/null +++ b/.dev/2026-06-02-combat-system/spec/design.md @@ -0,0 +1,392 @@ +# Combat System — Design Spec + +**Status:** draft for review · **Date:** 2026-06-02 · **Experiment folder:** `combat/` (to be created) + +> A standalone experiment in the reikhelm sandbox. **No dependency on `reikhelm-core`** — it +> only borrows the same conventions (pure deterministic core, serde config, ChaCha8 RNG, +> headless-first, front-ends as consumers). Lives in its own top-level folder per the +> repo's one-experiment-per-folder convention. + +--- + +## 1. Thesis + +A real-time (tick-based) combat system in the EverQuest / Daggerfall lineage — auto-attack + +skills + magic — built on one deliberately unusual idea: **a single combined resource pool** +that replaces separate HP / mana / stamina. Everything you do *and* everything done to you +draws on the same bar. Offense literally spends your survival. + +It is **a deterministic simulation first, a playable game second.** The "feel" is not +eyeballed — it is *measured* across thousands of simulated fights and tuned to target +durations and difficulty curves. The engine is pure and headless; a batch sim harness and +(later) a real-time visual front-end are both just consumers of it. + +### The two-axis model (the spine) + +Every combatant has one pool, **Vigor**, with three derived quantities: + +| Term | Meaning | +|------|---------| +| **Vigor (fill)** | Current value of the pool. Also **= your absorb capacity** (see §4). | +| **Vigor Cap (ceiling)** | `max_vigor − fatigue`. The highest your fill can regen to *right now*. | +| **Fatigue** | Accumulated, fight-scoped reduction of the cap. Recovers only out of combat or via recovery abilities. | + +Every combat verb is a point on **two axes** — cost to *fill* and cost to *ceiling*: + +| Verb class | Δ fill | Δ ceiling | Example | +|------------|--------|-----------|---------| +| Offense (skills/spells) | `+cost` | `+cost` | kick = (5, 1) | +| Auto-attack & defensive reactions (block/parry/evade) | `0` | `+small` | swing = (0, 1) | +| Recovery (spells/skills/potions) | `+cost` | `−restore` | second wind = (8, −5) | +| Damage taken | `−mitigated` | `+fatigue_share` | — | + +The entire combat vocabulary is different numbers in that grid. **Balancing the game is editing +a table.** This is both the design elegance and the core requirement (§10–12). + +--- + +## 2. Goals & Non-Goals + +**Goals** +- A combined-pool, mitigation-and-recovery combat model that *feels* tense and fair. +- Fully data-driven: every number is config; nothing balance-relevant is hardcoded. +- Deterministic and headless: a fight is reproducible from `(config, seed, loadouts)`. +- Instrumented: emits a structured event/metric stream; exports CSV/JSON for analysis. +- Batch-simulatable: run 10⁴–10⁶ fights across matchup/parameter sweeps to tune balance. +- Symmetric: players and monsters run the *identical* model; only their controller differs. +- Composable abilities: skills and spells are one system built from effect primitives. +- Emergent role identity **without classes and without training points**. + +**Non-Goals (this experiment)** +- Rendering / animation / art. (A visual front-end comes later, as a consumer.) +- Movement & real positioning. Range is abstracted to a scalar/band for now. +- Loot, XP curves, character creation, networking, persistence. +- The crafting / discovery / reagent economy that *produces* abilities — separate experiment. +- The progression system that *grows* competency — separate experiment. + +--- + +## 3. The Resource Model + +### Vigor, Cap, Fatigue +- `vigor` (current fill), `max_vigor` (level/stat-derived constant), `fatigue` (≥0, starts 0). +- `cap = max_vigor − fatigue`. `vigor` is clamped to `[0, cap]`. +- **Regen**: each tick, `vigor += regen_per_tick`, clamped to `cap`. Regen restores *fill toward + the ceiling only* — it never touches `fatigue`. (Optional config: reduced regen rate while in + combat vs. idle.) +- **Fatigue ("spend leaves a mark")**: any point that *leaves* the pool generates fatigue. + Default `fatigue += fatigue_ratio × amount_spent` for damage taken and unlabeled spend; + abilities may override with an explicit authored `fatigue_cost`. Fatigue only decreases via + **recovery abilities** (in combat) or **rest** (out of combat). It does *not* regen. + +### Damage in +- An attack's landed damage is **a percentage of the target's `max_vigor`**, set by the **con + curve** (§7) and reduced by the **mitigation pipeline** (§8). Even-con, unarmored ≈ 10%; + +5 levels ≈ 30%; −5 levels ≈ 2% (illustrative anchors; the curve is config). +- Applying damage: `vigor −= landed`; `fatigue += fatigue_ratio × landed` (a share of every hit + becomes permanent-for-the-fight ceiling loss). Then resolve stagger (§4). + +### The squeeze +You don't lose by emptying your fill once — you lose because **attrition grinds your ceiling +down**, a lower ceiling means a lower fill, and a low fill means the next big hit overruns your +capacity and triggers the stagger cascade. Everything points at the same death. + +--- + +## 4. Stagger Cascade (the death model) + +There is no instant "vigor = 0 → dead". **Capacity = current `vigor`.** A landed hit is compared +to capacity *at the moment it lands*: + +| Landed hit vs. capacity | Result | +|-------------------------|--------| +| `hit ≤ vigor` | Absorbed normally. `vigor −= hit`. | +| `vigor < hit < 1.5 × vigor` | **Dazed / knocked down** (temporary status). Regen rate reduced. | +| `hit ≥ 1.5 × vigor` | **Unconscious.** Regen zeroed until recovered. | + +Once unconscious, regen can't save you and follow-up hits grind you to 0 → **dead**. The +thresholds (`1.0×`, `1.5×`) and the dazed regen penalty are config. + +**Consequence that makes it sing:** spending offense lowers your fill, which lowers your +capacity, which raises your stagger risk *on the very next hit*. "Fire the kick at 60%, eat an +add's swing, get staggered" is a real mechanical cascade, not flavor. + +--- + +## 5. Actors & Symmetry + +**Every combatant is the same `Actor`** — same Vigor/Cap/Fatigue, same stagger cascade, same +ability resolution. Variety comes from two independent dials: + +- **Stat block** → archetype. Golem = huge `max_vigor`, big hits, *slow* regen, long attack + delay. Rat = tiny pool, fast cheap pecks. Same model, different numbers. +- **Controller** → *intent*. A pluggable decision layer that emits actions (`attack / use + ability / wait / recover`). The Actor doesn't know who drives it: + - **Player controller** — driven by input (interactive front-end) or a scripted policy (sim). + - **AI controllers** — v1: swing-on-cooldown. Later: *hold-and-punish* (wait, watch the + target's fill, unload the haymaker exactly when they've spent low to overrun capacity), + kite, heal. The AI plays the same timing meta-game the player does. + +Controllers are swappable with zero changes to combat math — critical for the sim (scripted +policies of varying skill let us measure the gradual→brutal difficulty curve). + +--- + +## 6. Abilities — Unified Skills + Spells + +**Skills and spells are one thing: an `Ability`.** "Kick" = melee delivery + physical effect + +potency. "Fireball" = projectile delivery + fire effect + potency. Both resolve through the same +two-axis cost + effect grid. + +### Composition +An ability is composed from: +- **Delivery / shape** — `self / touch / melee / projectile / beam / aoe(n)`. Sets targeting and + a base cost. +- **Effect(s)** — one or more, drawn from the **effect library** (§6.1). Each adds cost. +- **Potency** — scalar multiplier on effect magnitude *and* cost. +- **Target count** — 1..N. Multiplies cost. + +`cast_cost = f(delivery, Σ effects, potency, targets)` → an **absolute** `(fill_cost, +fatigue_cost)`. Higher potency / more effects / more targets ⇒ more expensive. The 6-target, +potency-10 AoE "cosmic nuke" costs ~90% of a *full* bar — leaving you at ~10%, one swing from the +cascade. **Power is paid in vulnerability.** + +### Cost is absolute; pay-at-start +- **Affordability gate:** you cannot begin an ability whose `fill_cost > current vigor`. Heavy + abilities are gated behind a healthy bar; as your ceiling grinds down over a fight, you + progressively **lose access** to your big hitters. +- **Pay at cast-start; fizzle or interruption = paid for nothing.** One rule covers both fizzle + risk (§9) and getting staggered mid-cast: you spent the cost and the ability never fired. + +### Cast time +- The only structural axis separating "skill" from "spell": skills are mostly instant; spells + have a **wind-up** (cast time in ticks) during which you are exposed and interruptible. Cast + time is per-ability config. + +### 6.1 Effect library (both axes) +Effects target *either* fill *or* ceiling — and the ceiling-attacking ones are the most on-thesis +content the system can have: + +- **Fill damage** — % of target `max_vigor` (con + mitigation applied). +- **Ceiling damage / curse** — drains target `fatigue` directly (attacks the real life total). +- **Regen sabotage** — reduces target regen for a duration. +- **Fatigue amplification** — raises target's fatigue-per-spend for a duration. +- **DoT / HoT** — fill drain / restore over ticks. +- **Mitigation debuff / buff** — feeds the mitigation pipeline (§8). +- **Recovery** — `−fatigue` (raises ceiling) at a `+fill` cost; the inverse verb. + +Effect types are a small enum/registry; adding one is a library entry, not an engine change. + +### 6.2 Defensive abilities +Block / parry / evade are abilities with `(0 fill, +fatigue)` and a brief **active window**: +incoming hits landing during the window are mitigated/negated (they feed the §8 pipeline). They +cost endurance, never your guard. (Reactive timing vs. the attacker's wind-up is the defensive +counterpart to timing your offense — see Designed-For §14.) + +--- + +## 7. The Con Curve (shared level-delta mapping) + +A single config-driven curve maps **level delta** to a magnitude multiplier, reused in *both* +directions: +- **Incoming damage %**: `delta = attacker_level − defender_level` → base damage % of pool. +- **Outgoing effect resist/effectiveness**: `delta = caster_effective_level − target_level` + (caster_effective_level folds in competency, §9) → fraction of the effect that lands. + +Anchors (config, illustrative): even-con baseline; ≈3× at +5; ≈0.2× at −5; roughly monotonic. +Represented as a parametric function or a small lookup table — TBD in tuning, but it is *one* +curve doing double duty. + +--- + +## 8. Mitigation Pipeline + +All "how much of this hit actually lands" reduction flows through **one ordered pipeline** so new +sources are additive contributors, never reworks: +- **Armor** — passive attrition reduction (shrinks the damage %). Does *not* protect against + stagger; it just makes you bleed fill slower so you stay in the safe band longer. +- **Con** — the level-delta curve (§7). +- **Active defenses** — block/parry/evade windows (§6.2). +- **Stances** *(designed-for)* — toggled mitigation trades. +- **Buffs/debuffs** — temporary contributors from effects. + +Player skill = *which* abilities and *when*, not gear stat-checks. Armor and con set the +backdrop; timing decides the fight. + +--- + +## 9. Progression & Identity + +### Competency (soft gates) +Each Actor has a **competency value per school** (e.g., Invocation, Fire). Competency is mostly +*not* a hard cast wall; it drives two independent checks: +- **Fizzle — "does it go off?"** `f(competency, ability difficulty)`, where difficulty is driven + by the same factors as cost (potency, #effects, #targets). Low competency vs. high difficulty ⇒ + frequent fizzle (and per §6, a fizzle means you *paid for nothing*). +- **Resist / effectiveness — "how much lands?"** the con curve (§7) with caster competency folded + into effective level. Even a sneaked-off potency-10 cast gets shrugged off by an over-con + target. + +A few **hard school gates** are allowed for structure (e.g., a school locked until competency ≥ X). + +### Two independent brakes on power +- **Cost** gates *affordability* (can you pay it?). +- **Competency** gates *reliability + effectiveness* (will it fire, will it land?). + +You cannot craft a god-spell at level 1 and skip the game: you'll mostly fizzle, and what lands is +resisted to a trickle. Progression is *earned*, not bought. + +### No classes, no training points — three-layer emergent identity +1. **Competency = who you are over time.** Your per-school profile *is* your identity; earned by + use, never allocated. +2. **Loadout / memorization = who you are this fight.** Your known set can be large, but you commit + a *limited slotted set* per encounter (re-slot only on rest / out of combat). Even an + omni-competent veteran must choose a role for this pull — breadth of knowledge ≠ breadth of + power. +3. **Shared pool = who you are this second.** One bar means you can't be nuker *and* healer at + once; the resource economy forces a moment-to-moment role with no extra rules. + +Pure classes are rigid and fight the composable model; pure à-la-carte waters down because +everyone converges on one optimal kit. The loadout commitment + competency lag keep roles sharp +with zero class definitions and zero skill points. + +**Optional knob (ship OFF):** gentle competency **atrophy** — unused schools drift toward a floor — +as a firmer long-term brake *if* veterans start homogenizing. + +### Seam +**Combat consumes competency values and emits "used successfully" events.** *How* +competency grows (use-based skill-ups, etc.) is a separate progression experiment. We build the +hook, not the leveling curve. + +--- + +## 10. Time Model & Determinism + +- **Fixed tick** (default 10 ticks/s, config). *Every* duration — attack delay, cast time, regen, + status durations, active windows — is expressed in **ticks**. Headless: ticks advance as fast as + the CPU allows. Real-time front-end: ticks advance on a wall clock. +- **Determinism:** seeded **ChaCha8** RNG with per-roll sub-streams (mirrors `reikhelm-core`), so + tweaking one parameter doesn't reshuffle unrelated rolls. A fight = + `(config, seed, loadout_A, loadout_B)` → a *reproducible* outcome + full event log. This + reproducibility is what makes the analysis valid: change one knob, observe the isolated delta. + +--- + +## 11. Configuration — the "table" + +A single serde-(de)serializable `CombatConfig` holds **all** tunables, loadable from JSON. +Categories: +- Tick rate; regen rates (idle vs in-combat); dazed regen penalty. +- `fatigue_ratio`; stagger band thresholds. +- Con curve (parameters or table). +- Mitigation: armor tiers, stance values. +- Cost formula coefficients (delivery base costs, per-effect cost, potency scaling, per-target + multiplier); auto-attack `(0, small)` default. +- Fizzle & resist curve parameters. +- Actor stat blocks & ability/effect library definitions (data, not code). + +Nothing balance-relevant is hardcoded. Editing config is the primary balancing act; structural +rule changes are the only thing requiring a recompile. + +--- + +## 12. Instrumentation, Metrics & Balancing + +### Event stream +The engine emits a structured event per meaningful tick: `{tick, actor, action, fill_before, +fill_after, ceiling, fatigue, stagger_state, fizzle?, target, landed, outcome?}`. A **recorder** +rolls events into per-fight summary metrics. + +### Per-fight metrics (export to CSV/JSON) +Time-to-resolution (ticks & seconds); outcome (win/loss/draw/timeout); #staggers, #dazes, +#unconscious; #fizzles; total fill damage dealt/taken; total ceiling damage; min fill reached; +ceiling-decay curve; near-death recoveries; effective DPS; vigor/ceiling time series (optional, +sampled). + +### Batch harness & "feel" as acceptance criteria +A harness runs **N fights across a matchup/parameter sweep** (level-delta × armor tier × loadout × +controller-skill) and exports the aggregate. The user's "feel" failure modes become *measurable +guardrails*: +- *"slow player knocked every 2s quits"* → **stagger-frequency ceiling**: from a full bar, an + even-con opponent must not stagger-lock you in under *N* hits. +- *"gradual but scales to brutally difficult"* → a **target time-to-kill curve by con-tier**: + under-con trivial, even-con a satisfying ~X-second fight, +5 winnable only with skilled play, + +8 lethal. Sweep parameters until the curve matches the intended shape. + +### Analysis layer +A Python package (`combat/analysis/`, own `.venv`) consumes the exported CSV/JSON via pandas: +sweep orchestration, difficulty-curve plots, parameter search toward target durations. + +--- + +## 13. Architecture & Layout + +``` +combat/ # own Cargo workspace — independent of root reikhelm workspace + Cargo.toml # [workspace] so the parent workspace never absorbs it + combat-core/ # pure deterministic engine: Actor, Ability, tick loop, RNG, events + # no I/O, no rendering, no real-time. The single source of truth. + combat-sim/ # binary: load config + scenarios, run batch sweeps, + # record metrics, export CSV/JSON + analysis/ # Python: pandas/plots over exported data (own .venv, own .gitignore) + configs/ # JSON configs / sane defaults + README.md + # (later) combat-wasm/ + a web playable, mirroring the reikhelm-web pattern +``` + +- **Decoupling:** root `Cargo.toml` uses explicit `members` (no globs), and `combat/Cargo.toml` + declares its own `[workspace]`, so `combat/` is a self-contained experiment that neither + pollutes nor depends on the existing crates. +- **Deps (to be created):** `combat-core` → `rand_chacha = "0.10"`, `serde = { "1", features = + ["derive"] }`; `combat-sim` → `serde_json`, plus a CSV writer (e.g. `csv`). Edition 2021, + Rust 1.96. +- **gitignore additions:** `combat/target/`, `combat/analysis/.venv/`, exported data + (e.g. `combat/**/out/`, `*.csv` under combat output dirs). + +--- + +## 14. Scope — v1 Vertical Slice vs. Designed-For + +The full vision above is the *design*. To find the fun before building all of it, **v1 proves the +core loop and the balancing capability** — which, for a data-first build, means the headless sim +matters more than visuals. + +**v1 vertical slice (build this first):** +- `combat-core`: Actor + Vigor/Cap/Fatigue + stagger cascade + regen + mitigation (armor + con) + + fixed-tick loop + seeded RNG + event emission. +- **3–4 hardcoded abilities** (auto-attack, a cheap skill, a heavy skill, a recovery) — not the + composer yet; just enough to exercise the two-axis grid. +- **One dumb-AI controller** (swing-on-cooldown) + a scripted player policy. +- `combat-sim`: run a matchup sweep, export per-fight metrics CSV. +- `analysis`: a first notebook plotting TTK and stagger-frequency by con-tier. +- **Success = the duration/difficulty curves are tunable via config to the intended shape.** If + managing one pool against the stagger cascade is tense in the data, we layer the rest on. + +**Designed-for / explicitly deferred (nothing dropped):** +- The **ability composer** + data-driven effect library (v1 hardcodes; the model is built so this + slots in). +- **Competency / fizzle / resist** as data-driven progression inputs. +- **Loadout / memorization** mechanic. +- **Telegraphs & reactive defense** (block/parry/evade timed vs. wind-up) — nearly free once + abilities have cast-time windows. +- **Stances** (mitigation knob), **ceiling-attacking effect content**, DoT/HoT/buff/debuff breadth. +- **WASM build + real-time visual playable** (another consumer of `combat-core`). +- **Crafting / discovery / reagents** (separate experiment) and **competency advancement** + (separate experiment) — combat exposes the seams. +- Smarter AI controllers (hold-and-punish, kite, heal). + +--- + +## 15. Open Questions / Risks + +- **Con curve shape** is the balance lever the whole feel rides on; it will need real sweep data + to settle. Start parametric, validate against target curves. +- **Regen vs. drain vs. spend rates** must be co-tuned, or even-con fights are either un-loseable + (regen dominates) or stagger-locked (drain dominates). The sim exists precisely to find this + balance; treat default numbers as placeholders. +- **Fizzle-cost severity**: pay-at-start + fizzle-for-nothing is brutal by design; verify it + doesn't make low-competency casting *never worth attempting* (which would stall progression). +- **Scripted "player skill" policies** for the sim are themselves a modeling choice — the + difficulty curve is only as meaningful as the policies are representative. Document the policies + used for any published balance numbers. +```