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

28 KiB
Raw Permalink Blame History

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.
  • Controllerintent. 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 / shapeself / 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 / curseraises 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).
  • Recoveryfatigue (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:

  1. 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.
  2. Expire finishing statuses and active-defense windows (a window covering tick t is inclusive of hits landing on tick t).
  3. Resolve completing casts → enqueue their hits/effects for steps 34.
  4. 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).
  5. Tick DoT/HoT and other per-tick effects.
  6. 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-corerand_chacha = "0.10", serde = { "1", features = ["derive"] }; combat-simserde_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.