reikhelm/.dev/2026-06-02-combat-system/spec/design.md
Parley Hatch e112ef5ee9 docs(combat): harden spec after two critic passes
Adversarial review found no blockers but several serious issues; fixed:
- determinism: require vendoring reikhelm-core's order-independent, version-
  stable Rng::fork (not DefaultHasher) + integer fixed-point internal state
  with documented per-stage rounding; carve out chance()'s IEEE-deterministic
  f64 compare as the sole hot-path exception
- pin a canonical within-tick resolution order (incl. step-0 controller
  decisions in actor-id order) as a determinism invariant
- close internal-consistency dead-ends: fatigue clamp [0,max_vigor], cap=0 as
  the explicit attrition-collapse trigger, regen floor + no-soft-lock rule,
  fix inverted ceiling-damage verb
- enrich instrumentation (pre-mitigation damage, per-stage mitigation, blocked-
  action + fail-reason, fill-flow accounting) + anti-turtle guardrail metric

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 23:51:12 -06:00

492 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 (§1012).
---
## 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` (starts 0).
- `fatigue` is clamped to `[0, max_vigor]`; `cap = max_vigor fatigue` (so `cap ∈ [0,
max_vigor]`). `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`. Config carries an idle and a (lower) in-combat
rate, and a **non-zero floor**: regen is reduced when dazed and by regen-sabotage effects but
never reaches 0 *except when unconscious* — so an actor can always claw back toward affording an
action (no soft-lock; see §6).
- **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.
- **Ceiling collapse is the real loss condition.** Because every incoming hit adds fatigue,
even pure defense erodes your ceiling over time — *turtling is not viable*. When `fatigue`
reaches `max_vigor`, `cap = 0`, capacity is 0, and the actor collapses (→ unconscious → dead per
§4). Attrition kills by collapsing the ceiling, not by a single drain.
### 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.
**Reconciling with "no instant death":** "no instant `vigor = 0` → dead" means a single *fill*
subtraction never kills you outright. You are lost only when (a) a burst **overruns capacity**
(the cascade above) or (b) **attrition collapses your ceiling** (§3: `fatigue → max_vigor`, so
`cap → 0`). When `cap = 0`, capacity is 0 and the next hit trivially clears `1.5 × 0`
unconscious — that *is* the intended attrition-death path, fully consistent with the rule.
**Reaching `cap = 0` is itself the collapse trigger** (treated as an instantaneous self-overrun →
unconscious), so an actor who self-fatigues to zero in a lull collapses immediately without waiting
for an incoming hit — the §3 state-threshold and this §4 mechanism describe the same moment.
**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.
- **No soft-lock.** Auto-attack is always affordable (`0` fill), so an actor is never fully
action-locked — at worst reduced to auto-attack-and-regen (a *losing* position, not a frozen
one). Recovery abilities are authored **cheap (low or zero `fill_cost`)** so they stay reachable
when you most need them; together with the regen floor (§3), pay-at-start/fizzle can put you in
danger but cannot strand you in a permanently unrecoverable state.
### 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** — *raises* target `fatigue` directly, lowering their cap (attacks the
real life total). The inverse of Recovery, below.
- **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.
**Caveat on the "double duty":** the curve takes a **delta**, but its two callers feed
differently-shaped domains — incoming damage uses raw `attacker_level defender_level` (bounded
by the level range), while outgoing effectiveness uses a *competency-inflated* effective-level
delta (potentially wider). Keep the curve's input domain explicit and range-check the
competency-derived delta against it; if competency inflation pushes the effective range past where
the damage-tuned curve behaves, the two callers may need to **split into separate curves**. v1 has
no competency (§14), so this won't surface until the progression layer lands — write the curve call
so a later split is cheap, not a rewrite.
---
## 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 <school> 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.
### Reproducibility (load-bearing for all analysis)
A fight = `(config, seed, loadout_A, loadout_B)` → a *bit-reproducible* outcome + event log.
"Change one knob, observe the isolated delta" only holds if unrelated rolls don't reshuffle and
arithmetic doesn't drift. Two hard requirements — neither is satisfied by "ChaCha8" alone:
- **Order-independent, version-stable RNG forking.** Reuse `reikhelm-core`'s `Rng::fork` contract
*verbatim* (vendor `reikhelm-core/src/rng.rs` as a module): a hand-rolled integer mix (splitmix64
+ FNV-1a), **`&self` (non-mutating) and order-independent** — explicitly **not**
`std::collections::hash_map::DefaultHasher`, whose output is version-unstable. Sub-streams are
keyed by a stable string, e.g. `fork("actor{id}:{roll_kind}:tick{t}")`, so adding an effect to
one ability never shifts another actor's or another tick's stream. *This property* — not
ChaCha8 itself — is what makes the isolated-delta guarantee true.
- **Integer fixed-point internal state.** `vigor`, `cap`, `fatigue`, and all magnitudes are stored
as scaled integers (e.g. milli-units); every formula (con %, potency multiply, `fatigue_ratio ×
amount`, each mitigation stage) is evaluated in integer arithmetic with **documented rounding at
each pipeline stage**. This mirrors reikhelm-core's integer-space discipline and gives bit-exact
results across compilers / opt-levels / machines — IEEE-754 `f64` *multiply chains* do **not**
(ChaCha8 gives a portable random *stream*, not portable *arithmetic*). All **magnitude**
arithmetic stays integer; `f64` is otherwise confined to config parsing. **One deliberate
exception:** the vendored `Rng::chance(p: f64)` does its roll as a `u64 → f64` unit-interval
divide-and-compare (`rng.rs:106-117`) — a single division + compare, which *is* IEEE-deterministic
across platforms — so probabilistic checks (fizzle, §9) may use it. That single compare is the
only f64 allowed in the hot path; if even that's unwanted, add an integer-threshold `chance`
variant (compare the raw `u64` bits against `p` scaled to `u64`). Either way, no f64 ever touches
a magnitude. Avoid iteration-order-dependent containers (`HashMap`) in the hot path; use
ordered/`BTreeMap` or `Vec` keyed by actor-id.
### Canonical within-tick resolution order (a determinism invariant)
Several things resolve on one tick and the order changes outcomes (regen-before-damage yields
fewer staggers than regen-after; sequential hits differ from batched), so it is **fixed**, not an
implementation detail:
0. **Controllers decide**, in ascending actor-id order, *before* anything else resolves. Starting a
cast or auto-attack pays its cost **here** (cast-start payment, §6) and opens cooldowns/windows;
instant abilities enqueue their effects for step 2. Pinning this first, by actor-id, is what
makes the affordability gate and stagger checks see a deterministic state.
1. **Expire** finishing statuses and active-defense windows (a window covering tick *t* is
*inclusive* of hits landing on tick *t*).
2. **Resolve completing casts** → enqueue their hits/effects for steps 34.
3. **Apply incoming hits sequentially in ascending actor-id order** (sequential, not batched: hit 1
lowers `vigor`, raising hit 2's stagger odds), each followed immediately by its **stagger check**
(§4) and its **fatigue add** (§3).
4. **Tick** DoT/HoT and other per-tick effects.
5. **Apply regen** (clamped to `cap`).
Determinism is part of the contract — any reordering (including controller-decision timing, step 0)
is a breaking change to recorded data.
---
## 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. Outcomes alone can't tell you *why* a
config feels bad, so the schema records **causes**, not just results:
`{tick, actor, action, fill_before, fill_after, ceiling, fatigue, stagger_state, target,
damage_pre_mitigation, mitigation_by_stage[armor, con, defense, buff], landed,
fail_reason ∈ {none, competency_fizzle, interrupted}, action_blocked_reason?, outcome?}`.
A **recorder** rolls events into per-fight summary metrics. Each added field is load-bearing for
tuning:
- **Pre-mitigation damage + per-stage mitigation deltas** — you cannot tune the con curve (§7, the
balance lever) from the final `landed` number alone; you need each stage's contribution.
- **`action_blocked_reason`** (`cant_afford` / `on_cooldown` / `dazed` / …) — a "wanted to act but
couldn't" signal. Without it, a fight where an actor spent 40% of ticks unable to afford anything
is indistinguishable from one where it chose to wait, and the soft-lock/turtle modes are invisible.
- **`fail_reason` splits competency-fizzle from interruption** — otherwise "competency too low" and
"wind-ups too long" collapse into one signal you can't separate.
### Per-fight metrics (export to CSV/JSON)
Time-to-resolution (ticks & seconds); outcome (win/loss/draw/timeout); #staggers, #dazes,
#unconscious; #competency_fizzles, #interruptions; blocked-actions by reason; total fill damage
dealt/taken; total ceiling damage; min fill reached; ceiling-decay curve; near-death recoveries;
effective DPS; **fill-flow accounting (gained-by-regen vs. spent-on-actions vs. lost-to-damage)**
§15 names regen-vs-drain-vs-spend as the make-or-break co-tuning, so the sums must be visible;
vigor/ceiling time series (optional, sampled).
### Anti-turtle guardrail
Absorbed hits still erode the ceiling (§3), so passive-forever loses — but **auto-attack-only** can
still dominate if abilities aren't *efficient enough* (favorable damage-per-resource, or utility
auto-attack lacks). Track it explicitly: **fraction of fights won by an auto-attack-only policy.**
If that's high, the interesting verbs are a tax players opt out of — contradicting the thesis — and
the fix is a damage-per-resource/utility edge, not a tuning nudge.
### 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.
- **34 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.
- **Auto-attack-only dominance** (see §12 guardrail) — the deepest balance risk: if abilities don't
beat auto-attack on damage-per-resource or bring utility it lacks, the optimal policy is to never
use the interesting verbs. Structural, not a number; watch the guardrail metric from day one.
- **Fixed-point rounding bias** (§10) — integer rounding at each pipeline stage can accumulate a
systematic skew (e.g. always-floor quietly favors the defender). Pick a rounding rule
deliberately and confirm it isn't biasing outcomes over long fights.
```