# 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` (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 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 3–4. 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. - **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. - **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. ```