Files
Project-M/Docs/Vault/06_Roadmap/Path_to_Fun.md
T
kronic 6769fc3de9 Docs: END-2 session log + DR-036; Backlog/Path_to_Fun/Milestones; CLAUDE.md END-2 line
Path A spine COMPLETE (14/14): Backlog SL-3 blocker cleared + marked done; Path_to_Fun END-2 done + banner; Milestones END-2 row. CLAUDE.md gains the END-2 gotcha line (replicate the outcome, don't client-derive; SiegeTimeout off during the final), net-zero via EB-1/EB-2/END-1/M7/inventory/build-grid condensations (40,445 then 40,510 w/ history note, under the 40,960 limit). DR-036 + session log capture the design, the operator forks (halt+banner, Target=4, SaveData v5), and the pre-coding + post-impl adversarial reviews.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 12:38:46 -07:00

510 lines
113 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.
---
tags:
- roadmap
- design
- combat
- economy
- endgame
- north-star
status: active
updated: 2026-06-08
permalink: gamevault/06-roadmap/path-to-fun
---
# Path to Fun — the north-star roadmap
> **[ACTIVE JUNE 1330 → the slice]** Path A's spine is **COMPLETE** — all **14/14 systems shipped** (MC-0/1/4 · EB-1/2 · END-1 · **END-2 built 2026-06-13 as slice SL-3**, [[DR-036_END2_Final_Siege_Win_Lose]]). The operator answered the [Decision Gate](#the-decision-gate-mandatory-stop-after-end2) **early, as *ship the minimum***: the immediate target is the **[[End_Of_Month_Game_Jam_Slice]]** ("Awakening Engine Last Stand"), a compact single-arena base-defense demo due June 30. With END-2 in, the slice is **winnable end-to-end**; the remaining slice work is **tuning + polish, not code** (SL-1/2/4/5/6/7) — and the final-siege **fun-gate** is the one open operator validation. This doc stays the **north-star for the full game**; the slice scopes Path A to a deliverable. Milestone breakdown: [[Backlog#NEXT — Awakening Engine Last Stand (End-of-Month Slice) ★]] · adoption: [[DR-035_End_Of_Month_Slice_Adoption]]. *(Path B is untouched and provisional; the formal ship-vs-continue note is logged after June 30.)*
> The plan to turn an engineering-complete foundation into a game that's fun to play. Direction locked in [[DR-028_Combat_Primary_Verb_Depth_First]]. This is the **forward** plan; [[Milestones]] stays the historical record, [[Backlog]] the loose pool. Living doc: the [Path A contract table](#path-a--the-proven-path-to-a-point-committed) is the only committed scope; everything in [Path B](#path-b--the-forever-track-provisional-not-scheduled) is provisional and re-derived after Path A's fun-gates pass.
## The problem this solves
M0M7 + inventory/equipment built deep, correct infrastructure but a hollow game (operator, 2026-06-08: *"this does not feel like a game"*). Root cause: **breadth-first, correctness-first** development — every milestone proved a system *replicates deterministically*; none proved a loop is *fun*. Combat — pillar #1 — is one projectile and one enemy brain, never once playtested for enjoyment. The four pillars read as four co-equal genres, which a solo dev can't make co-equally deep, and building them breadth-first is *why* there's no fun.
## The fix, in one sentence
Make **combat the primary verb**, braid base + automation around it as stakes and economy, and go **depth-before-breadth**: no new system until one braided loop is genuinely fun, with a **falsifiable play/fun-gate at every milestone**.
## The braided loop (the target)
> You and your friends raid the Blightfield for raw Aether; your automated base refines it into the ammo, charges, turrets, and upgrades you fight with; and you spend them surviving escalating sieges that hit the base you're standing in — so every fight is fought *with* what your factory made and *for* the base your factory lives in.
| Pillar | Today (separate mode) | Braided (one loop) | Real-game model |
|---|---|---|---|
| **Automation** | Harvester→Conveyor→Fabricator → a ledger number nobody feels | makes the things you fight WITH (charges / munitions / turret feed / upgrades) | The Riftbreaker |
| **Combat** | stand-and-click one projectile; free respawn | the verb you spend the economy on; sieges threaten the base/machines, real loss | They Are Billions / Riftbreaker |
| **Base** | a spawn point + a build grid | what you defend and why the economy exists | V Rising / Core Keeper |
Two small-studio games prove the fusion is achievable **and** that it needs ONE primary verb: **The Riftbreaker** (combat-led — a mech defends an automated base) and **Core Keeper** (mining-led — literal conveyors + drills + boss combat, co-op). Neither makes all three co-equal.
## How to read this roadmap (the hard line) ★
The prior roadmap presented breadth as a flat, equally-weighted, fully-estimated list — which is the *exact shape* of the four-co-equal-pillars error that produced the hollow game, reborn as seventeen-co-equal-milestones. This document refuses that shape. It is split into **two physically separate sections with a hard stop between them**:
- **[Path A — The Proven Path to a Point](#path-a--the-proven-path-to-a-point-committed) (COMMITTED):** the minimal critical path to *fight-is-fun + braided-with-stakes + has-a-win/lose-condition***MC-0, MC-1, MC-4, EB-1, EB-2, END-1, END-2** only. Seven milestones. This is the scope. Its estimates, gates, and demos are real. Finishing Path A is a complete, shippable small game with a point.
- **[Path B — The Forever-Track](#path-b--the-forever-track-provisional-not-scheduled) (PROVISIONAL, NOT SCHEDULED):** MC-2, MC-3, MC-5, MC-6, EB-3/4/5, END-5 — depth and breadth that only earn the right to exist once Path A is proven fun. Its estimates are **indicative only and WILL be re-derived** after Path A's fun-gates pass. Do not treat it as a commitment. (END-3 narrative and END-4 content-treadmill are deferred entirely into the [Cut table](#cut--not-yet-anti-breadth-creep) — see why below.)
**The hard stop** ([Decision Gate](#the-decision-gate-mandatory-stop-after-end-2)): after END-2 ships the minimum-game-with-a-point, an **explicit logged operator decision** is required — *ship/share this minimum and stop, or commit to ONE Path B milestone* — and **no Path B milestone may begin until that decision is logged.** A solo dev with no deadline is most at risk of never shipping precisely because the forever-track always offers one more thing; this gate is the teeth of depth-before-breadth at the place it matters most.
**Estimates are coding-time, not calendar-time.** See the [calendar-time conversion](#calendar-time-the-play-budget-assumption) — the unbounded cost is *fun-tuning*, and at a realistic focused-editor budget Path A is a **multi-month** effort, not the ~8-week coding sum a reader would otherwise anchor on.
**Paused until the loop is fun** ([[DR-028_Combat_Primary_Verb_Depth_First]]): inventory/equipment Phases 24 and automation recipe/throughput breadth resume **only braided** (EB-4/EB-5, Path B) — cut any Phase 24 work that doesn't feed the fight ([[Backlog]]).
## The validation-culture change
Green EditMode + server==client stay **necessary, not sufficient** — they were the *only* bar through M7 and that is why the game is hollow. A milestone is **done** only when all three gates pass: **(1)** EditMode green, **(2)** server==client verified in a real netcode Play session, **(3)** the [fun-gate protocol](#fun-gate-protocol) passes — with a friend on the co-op milestones, and the [instrumentation](#instrumentation-extend-the-m8-dev-tools-triad) confirming the feel claim. DR-028's literal sign-off is *"spacing/timing matters and we didn't want to stop"* — but that is a vibe until it is a **counted, falsifiable** metric, so every milestone below carries observable criteria, not "feels good." Keep the netcode/determinism rigor; just stop treating green tests as "done."
---
# PATH A — The proven path to a point (COMMITTED)
> The smallest path that earns the right to even *consider* Path B: a fight that's fun, braided to an economy you feel spending, with a base you can lose and a win/lose condition. **Execution order:** `MC-0 → MC-1 → MC-4 → [Demo A] → EB-1 → EB-2 → [Demo B] → END-1 → END-2 → [Decision Gate]`. Each milestone carries its own fun-gate; none ships until the prior loop is fun.
## Path A — the contract table (committed)
| ID | Name | Track | Risk | Coding-est | Unlocks |
|---|---|---|---|---|---|
| **MC-0** | Instrument the box (dev-overlay readback) | Combat | LOW | ~0.5 d | every fun-gate's numbers |
| **MC-1** | Fight in a Box: the dash + the question | Combat | MEDHIGH · review-gated | ~2.53.5 wk | the duel; the whiff-punish loop |
| **MC-4** | Offense gets a verb: archetype byte + melee cone | Combat | LOW | ~0.751 wk | dash-in→cleave→dash-out; MC-6 spike |
| **EB-1** | Machines can die: the structure loss-state | Economy | MED · review-gated | ~1.52.5 wk | a base you can lose; END-1's Core hook |
| **EB-2** | The felt spend: output → depletable combat resource | Economy | MED | ~11.5 wk | factory→defense pipe; co-op shared spend |
| **END-1** | The base can be lost: a Core with integrity | Endgame | MED | ~46 d | a real lose condition |
| **END-2** ✅ | The charge means something: final siege, win/lose | Endgame | MED | ~24 d | a win beat; the minimum point — **DONE 2026-06-13 (SL-3)** |
*Estimates are solo + Claude **coding-time only**. They are wider than the prior draft because several of these milestones are secretly 23 slices each (see the [secretly-multi note](#secretly-multi-milestones-why-the-estimates-widened)). **Fun-tuning is the unestimated, unbounded cost** — every milestone's real schedule risk is the playtest→tune→replaytest loop, not the code (see [Risk register](#risk-register) R1/R11). That is why every feel-critical value is a live server singleton, not a baked const (see [Tuning-knob surface](#tuning-knob-surface)), and why the [calendar conversion](#calendar-time-the-play-budget-assumption) below turns these into months.*
## Path A — milestones
> Designed 2026-06-08 via a multi-agent pass (5 design lenses grounded in real games + the actual DOTS code → synthesis → 3 adversarial critics, all **go-with-changes**), then re-cut against a ground-truth code audit (see [Verified-vs-corrected build notes](#verified-vs-corrected-build-notes)) and a second critic round (netcode-feasibility · solo-scope-realism · fun/design-coherence — all go-with-changes) whose blockers + majors are folded in below.
### The thesis
Depth = a **dialogue**. Enemies ask distinct, readable questions (a committed lunge to dodge, a bolt to reposition from, a swarm to AoE); the player answers with tools that have skill and **commitment cost**. The keystone is **enemy commitment + a punishable whiff** paired with the **dash** — the dash is the *answer*, a committed lunge is the *question*; neither is fun alone (so they ship together). The repo is well-shaped for this: the predicted CharacterController, the `RespawnInvuln`/`KnockbackState`/`AttackWindup` windowed-tick idiom, the derive-don't-replicate `Dead` gate, the `StatModifier` fold, and a near-complete `CombatFeedbackSystem` juice scaffold are all already proven under prediction.
### MC-0 — Instrument the box (dev-overlay readback) `~0.5 d` · risk LOW · ✅ DONE (2026-06-10)
> **Status:** telemetry counters **and** the `TuningConfig` live-tuning singleton — 10 dash/Charger knobs nudgeable from the dev overlay mid-Play, no recompile — both landed. See [[2026-06-10_MC0_TuningConfig_LiveTuning]]. The MC-1 fun-gate's playtest→nudge→replay loop is now unblocked.
**Goal:** make every later fun-gate *measurable* before spending a friend's time. The M8 dev-tools triad (`DebugCommandRequest` + `DebugOverlay` + `DebugCommandReceiveSystem`) today only **sends** commands and never reads live values back — that gap is why the gates are unfalsifiable.
- **Scope:** add a server-only `DevTelemetry` `IComponentData` (a flat struct of `uint` counters + a few `float` accumulators, **not** `[GhostField]` by default) updated at the stamp sites the later milestones already touch. Surface it to the local overlay via a handful of owner-send `[GhostField] uint`s on the predicted player (read each frame in `PresentationSystemGroup`) **or** a periodic `DebugTelemetryReport` RPC server→client (avoids any ghost-hash change). Add a read-only IMGUI readout block to `DebugOverlay` showing the live counters + derived ratios (negated-hits/dash, whiff-convert %, per-player DPS, hit-stop frames, downed/revive timers).
- **Build notes:** the dev-RPC wire type stays **unconditional** (no `#if` on the struct — the RpcCollection hash must match release/dev peers); `#if UNITY_EDITOR`-gate only the send/receive systems. Add a `SetTuning(op, valueX1000)` `DebugOp` so the operator nudges live singletons (below) from the overlay without leaving Play. Pure editor-only, server-authoritative plumbing — fully Claude-headless.
- **Fun-gate:** N/A (it's the *instrument* of the gates). Done when the overlay prints a live counter that increments during play and a tuning slider changes a singleton mid-session without a recompile.
- **Claude:** all of it solo/headless. **Operator:** nothing.
- **Dependencies:** none. **Kill-risk:** none — but skipping it makes every downstream gate a debate instead of a glance.
### MC-1 — Fight in a Box: the dash + the question it answers `~2.53.5 wk` · risk MEDIUMHIGH · **review-gated**
> **Status (2026-06-10):** ✅ **FUN-GATE PASSED** — the dash duel is fun (operator: *"it's fun, the dash feels fine"*); the project kill-switch is CLEARED, the combat thesis holds. Code [[2026-06-09_MC1_Implementation]]; gate passed [[2026-06-10_MC4_Combo_Melee]].
**Goal:** turn stand-and-click into a bait-and-punish **duel** — a snappy i-frame dash answering ONE **Charger**'s readable, committed, whiff-punishable lunge. This is the genuinely-smallest fun slice. *(The Swarmer moves to MC-2; it answers a different question — see [Boundary judgment](#boundary-judgment-re-cut-mc-1-default-order-mc-4-early).)*
> **This is 34 distinct risky slices, not one** ([secretly-multi](#secretly-multi-milestones-why-the-estimates-widened)): a new predicted `DashSystem` + replication; a Burst-affecting `CharacterProcessor` edit with its own restart/validate cycle; the `DamageEvent.SourceTick` refactor across THREE stamp sites + the negation branch; and a new Charger brain (lunge/stagger/whiff-detection) + telegraph tuning + dash juice. Each needs its own focused-editor Play-validation — hence the widened estimate.
**Scope (named systems):**
- **Dash** (Hades / Hyper Light Drifter): a NEW predicted `DashSystem` in `PredictedSimulationSystemGroup`, **`[UpdateAfter(PlayerControlSystem)]`** (it overrides the unconditionally-written `CharacterControl.MoveVelocity` during the dash window) **and gated `.WithAll<Simulate>().WithDisabled<Dead>()`** — this matches the existing two-ordered-writer pattern (`PlayerDeathStateSystem` zeroes `MoveVelocity` `[UpdateBefore(PlayerControlSystem)]`; `PlayerControlSystem` sets it), so a third *unordered* writer would be a last-writer-wins determinism hazard. A dedicated `Dash` InputEvent (verbatim `Fire` clone: `[GhostField] InputEvent`, reset+Set each frame in `PlayerInputGatherSystem`). `DashState{float2 Dir; **uint StartTick;** uint IFrameUntilTick; uint RecoverUntilTick}` (predicted, **re-simulated from input** — clone `KnockbackState`'s *shape*, not its server-only-ness) + `DashCooldown{[GhostField] uint NextTick}` (`AbilityCooldown` twin). Whole-window i-frames; a short **recovery tail** (Helldivers dive) so a panic-dash is punishable, not spam.
- **Charger** (L4D2 Charger / DRG Menace) — *the keystone*: a longer-telegraph Husk variant that on commit enters a fixed-direction `LungeState` (a `KnockbackState`-shaped **server-only** field applied INSIDE `EnemyAISystem` as the sole position writer, reusing `SweptMove`); direction locks at commit so a sidestep/dash whiffs it into a **stagger/punish window** (detect the wall-stop / overshoot, extend `EnemyAttackCooldown`). The whiff-punish loop IS the skill ceiling.
- **Readable telegraph** (precondition, not polish — mostly RAMP/TUNING of an existing cue): lengthen the Charger's `AttackWindup` ticks to ≥ interp-delay + reaction (~0.450.6 s, ~2836 ticks, not 18); ramp the **existing** `CombatFeedbackSystem` `AttackWindup` cue into a ground-ring/scale-up. `CombatFeedbackSystem` already cues off the `AttackWindup.WindUpUntilTick` edge (a replicated `[GhostField] uint` countdown) — this is a tune, not a new cue.
- **Dash juice** (must be in MC-1, not deferred): afterimage/whoosh + directional camera nudge + i-frame shimmer, edge-detected from `DashCooldown` exactly as the scaffold already edge-detects `AbilityCooldown` for the muzzle flash.
**Build notes (honor at code time):**
- **i-frame fix — the cross-group tick-alignment blocker (review agenda item #1):** This is the HIGH-severity R2 risk, and the "server-only, no client symmetry" framing must NOT be read as "no determinism care needed." The actual subtlety, verified in code: `HealthApplyDamageSystem` is `ServerSimulation`-filtered **but runs `[UpdateInGroup(PredictedSimulationSystemGroup)]`** (and is the *sole* drainer of `DamageEvent`); the melee strike it negates is appended by `EnemyAISystem` in the **plain** `SimulationSystemGroup` `[UpdateAfter(PredictedSimulationSystemGroup)]`**a different group, drained the following tick.** So "is `DashState` active *now* at drain time" is ≥1 tick off from "was the player i-framed when the strike *landed*" — the same class as the documented "predicted-physics group is OrderFirst" and "contact `DamageEvent` drains the following tick" gotchas. The fix: `DamageEvent` is exactly `{float Amount; int SourceNetworkId}` today — add a **non-replicated `uint SourceTick`** and **stamp it at all THREE append sites** (`EnemyAISystem` strike, `ProjectileDamageSystem`, **and `TurretFireSystem`** — an un-stamped `SourceTick=0` aliases tick 0, the same "ready sentinel" hazard `TickUtil` guards). In `HealthApplyDamageSystem`, **skip a `DamageEvent` whose `SourceTick` is in `[DashState.StartTick, DashState.IFrameUntilTick]` inclusive, compared via `NetworkTick.IsNewerThan`/`TicksSince` — NEVER a raw `uint` compare and NEVER "is-DashState-active-now."** The proposed struct without `StartTick` structurally cannot express this test. **The explicit FIRST agenda item of MC-1's mandatory review is: "at the tick the server drains the melee `DamageEvent`, does the server-side `DashState` i-frame window (compared via `SourceTick`) correctly cover the strike that was appended in a later group the previous tick?"** *(This same `SourceTick` field de-risks the Spitter, structure damage, revive-invuln, and weak-point stamps.)*
- **prediction-reconciliation flicker (presentation note — acceptable, not a bug):** because `Health.Current` is a `[GhostField]` and the player is predicted, a successful **server-only** i-frame dash yields a brief health-bar/prediction flicker on the owning client (the client predicts the hit; the server's authoritative non-damaged `Health.Current` corrects on the next snapshot). This is **acceptable — no desync, server-authoritative** — and must be NOTED so it isn't read as "flaky i-frames" in the very playtest that gates the track, AND so MC-3's hit-flash/`CombatFeedback` is **not** edge-fired on the corrected-away phantom hit.
- **dash-feel fix (Burst-affecting blocker):** `CharacterControl` has only `MoveVelocity`; `CharacterProcessor.HandleVelocityControl` lerps `RelativeVelocity` toward it at `CharacterComponent.GroundedMovementSharpness` (default 15) via `StandardGroundMove_Interpolated` → a flat dash `MoveVelocity` **ramps** ("walk faster"). Fix = raise `GroundedMovementSharpness` for the window **or** write `characterBody.RelativeVelocity` directly inside the processor. **This is a Burst-affecting edit to the predicted processor — do it FIRST, focused editor, Burst-off for the session, expect a restart, Play-validate before building on top** (the stale-binary / ICE hazard; [Risk register](#risk-register) R3).
- **input binding:** don't bind `Dash` to `Space``keyboard.spaceKey.isPressed` is part of the kbm-active scheme sentinel in `PlayerInputGatherSystem`. Use a dedicated action fed symmetrically into device-active detection.
- **replication shape:** `DashCooldown` mirrors `AbilityCooldown`/`RespawnInvuln``[GhostField] uint` scheduled via `TickUtil.NonZero`, compared with `NetworkTick.IsNewerThan` (never raw `uint <`). `DashSystem` gates as above, deterministic/idempotent, no wall-clock; the InputEvent (not a held bool) ensures one dash per press across the frame→tick→rollback boundary.
**Fun-gate (falsifiable):**
- **BENCH METRIC (timed vs spam):** in a fixed 10-lunge bench, a player who dashes ON the telegraph takes **≥70% fewer Charger hits** than a player who dashes on cooldown-spam; MC-0 prints both hit-counts (`dashIFrameNegatedHits`, `dashesWasted`). Spam-dash must demonstrably leave the player in the `RecoverUntilTick` tail when the real lunge lands.
- **WHIFF-PUNISH CONVERTS:** after a dodged lunge the Charger is staggered long enough for a free hit — `chargerWhiffWindowsOpened` vs `chargerWhiffPunishesLanded` > 50% on a skilled run; free-hit window ≥ one attack interval in ≥8/10 dodges.
- **READABILITY UNDER LATENCY:** at simulated ~100 ms interp delay, the tester reacts to the **tell**, not the motion — the dash starts before the Charger's position has visibly committed. If the windup must be *shortened* to feel fair, that is a fail of the band — re-tune, do not ship.
- **SNAP TEST:** the dash covers full distance in its i-frame window with no visible ramp ("blink," not "walk faster") — `RelativeVelocity` reaches dash speed within 12 ticks.
**Tuning knobs:** Dash distance / i-frame window / recovery tail / cooldown / sharpness-override; Charger windup / lunge speed / stagger window — all **live server singletons** with the defaults in the [Tuning-knob surface](#tuning-knob-surface). (Recovery tail is the most-tuned value in the track.)
**Open questions:** Does the dash commit to its direction or keep `PlayerAimSystem` facing free? Whole-window i-frames vs active-frames-only (recommend whole-window v1 — more forgiving, simpler). Charger as a new prefab variant or a brain-discriminator byte on the existing Husk? *(The MoveVelocity-writer fork is now decided by the build note: `DashSystem` `[UpdateAfter(PlayerControlSystem)]` + `.WithDisabled<Dead>()`.)*
**Claude:** all code — `DashSystem`, `DashState`/`DashCooldown`, `DamageEvent.SourceTick` + all THREE stamp sites + the tick-windowed negation branch, the Charger brain branch in `EnemyAISystem`, Dash input wiring, dash-juice hooks, EditMode tests (dash-window negation across the cross-group tick boundary, Charger commit/stagger, a tunnelling-style regression on i-frame tick coverage). **Operator:** the Burst-affecting `CharacterProcessor` edit on a FOCUSED editor (+ likely restart); ALL feel tuning; running the bench + the friend-read at Demo A; **owning the mandatory netcode review** (agenda item #1 above).
**Dependencies:** MC-0 (so the bench is measurable). **Kill-risk:** the dash doesn't FEEL like a blink (the sharpness override is make-or-break) OR the telegraph is unreadable under latency OR the i-frame negation mis-aligns across the group boundary and reads as "flaky/cheap" — any one collapses the duel into spam/RNG and nothing downstream matters. **MC-1 is the kill-switch for the whole project**: if its gate fails after a real tuning pass, STOP and re-cut combat — do not build on an unfun core.
### MC-4 — Offense gets a verb: ability ARCHETYPE byte + melee cone `~0.751 wk` · risk LOW
> **Status (2026-06-10):** 🔨 **CODE-COMPLETE + reviewed** — built as the **combo-chain** variant with melee as the **PRIMARY** verb (left-click/pad-West; ranged demoted to right-click/pad-LT), per the operator's forks. Predicted-replicated combo `Step`, server-only cleave, 9 live `TuningConfig` knobs. **Polish pass added** ([[2026-06-10_MC4_Melee_Anim_VFX_Archetype]]): a Rukhanka **swing animation** (procedural Root-bone clip + `IsAttacking` driven from `MeleeCombo`) and a **live cone slash-arc VFX** that telegraphs the actual reach. The **archetype byte SPIKE landed** (the `byte Archetype` data field + the `AbilityFireSystem` dispatch read-point; full hitscan/cone/aoe dispatch is still MC-6). 294/294 EditMode green, Play-validated (no cycle, re-bake server==client, controller re-baked to 5 params). The MC-4 **fun-gate** is the open operator item. See [[2026-06-10_MC4_Combo_Melee]] · [[2026-06-10_MC4_Melee_Anim_VFX_Archetype]] · [[DR-030_MC4_Combo_Melee_Primary_Verb]].
**Goal:** stop offense being "auto-aim and hold." A byte-dispatched ability archetype with an instant short-range **melee cleave** that makes attacking a *positioning* decision (dash-in → cleave → dash-out). **Runs second, right after MC-1** — verified-low-risk and kills the second-most-felt hollowness; see [Boundary judgment](#boundary-judgment-re-cut-mc-1-default-order-mc-4-early). *(Also the de-risking spike for MC-6's archetype dispatch.)*
- **Archetype byte** on `AbilityDefBlob`/`EffectiveAbilityStats` (Projectile=0 keeps today's path) + a `switch` in `AbilityFireSystem` (stored as **byte** per the Burst cross-assembly enum rule; baked, no replication).
- **MeleeCone / cleave (byte=2):** server-side select all living enemies in a cone around `PlayerFacing.Direction` (**reuse `AutoTarget.Resolve`'s `dot` vs `cos(halfAngle)` cone math as a collect-all selector**), append `DamageEvent` (`SourceTick`-stamped) + `KnockbackState`. Instant, short-range, higher per-hit. No new ghost, pure server damage. `PlayerFacing.Direction` is already a replicated `[GhostField] float2`.
- **Combo glue with MC-1:** dash-in → cleave → dash-out should read as a single verb. Keep the `switch` shape generalizable to hitscan(1)/cone(2)/aoe(3) for MC-6.
- **Feel-coupling note (the trap):** the cleave's cooldown/range MUST be tuned **relative to the dash** — the dash recovery tail must not strand you mid-cleave, and the cleave range must reward the dash-in. **MC-4 cannot "pass" its gate until MC-1 has actually PASSED its gate, not merely shipped** — a combo grammar tested on a still-mushy dash is untestable, and "MC-4 passes on paper because we never validated the dash" is the R9 breadth-creep reflex wearing a combat hat.
**Fun-gate (falsifiable):** `cleaveTargetsPerSwing` averages **>1.5** during a swarm (you position to line up the cone); the dash-in/cleave/dash-out combo is chosen over the projectile when surrounded in ≥8/10 surround situations (`comboChains` counted); a blind-test watcher can tell cleave from projectile by feel/range alone.
**Tuning knobs:** cone half-angle / range / damage / knockback / cooldown — all live singletons (defaults in the [surface](#tuning-knob-surface)).
**Open questions:** cleave as a separate button (simplest, previews MC-6) vs. temporarily replacing Fire? Own cooldown (enables the dash-cleave-shoot grammar) vs. shared with Fire?
**Claude:** the archetype byte + `AbilityFireSystem` switch, the `MeleeCone` selector (lifting `AutoTarget` cone math), `DamageEvent`+`KnockbackState` append, the second-button input wiring, EditMode tests (cone selection count, byte dispatch, no self-hit). **Operator:** cone angle/range/damage feel tuned *relative to the dash*; the distinct-verb blind-test.
**Dependencies:** **MC-1 (PASSED, not just shipped)** — the dash is the other half of the combo. **Kill-risk:** the cleave feels like a re-skinned auto-attack because the cone/range/cooldown let you stand still — the whole point is it PULLS you into the arena.
> ### Demo A — "The Duel" (after MC-1 + MC-4) — **first friend-playable checkpoint**
> The natural place to first satisfy DR-028's literal *"play it, **with a friend**, and not want to stop."* The Charger as the readable threat, dash + cleave + projectile, one short single siege (the existing `ThreatDirector`/`CyclePhase`/`WaveSystem` already produce one). **Include a lightweight TWO-HUMAN co-op read here** — both players dashing/cleaving the Charger and a small swarm (MPPM or a real friend, **no new systems**) — so the project's FIRST validated-fun checkpoint includes a second human, per the non-negotiable co-op pillar. A duel that's fun solo can be boring or chaotic with two; discovering that here is cheap, discovering it at Demo B (5+ milestones later) is not. **This demo's pass/fail is the green-light for the rest of Path A:** if two friends want to keep dueling, the thesis is validated; if not, re-tune MC-1's feel before spending weeks on the economy braid. *(Interdependence isn't designed yet — that's MC-5 in Path B; this read only asks "is two-player combat fun and readable," not "do they need each other.")*
### EB-1 — Machines can die: the structure loss-state `~1.52.5 wk` · risk MEDIUM · **review-gated**
**Goal:** make a built structure destructible so a siege can actually take something from you — the stakes the whole economy is for. *(This is also END-1's mechanical sibling — EB-1 destroys peripheral machines; END-1 adds the losable Core. See [the interleave note](#where-the-economy-braid--endgame-slot).)*
> **This is 56 slices, not one** ([secretly-multi](#secretly-multi-milestones-why-the-estimates-widened)): a ghost-hash-changing `[GhostField]` (re-bake of EVERY structure prefab), an `EnemyAISystem` targeting rewrite, a `HealthApplyDamageSystem` death-branch change, a cross-group production-ordering gate, a `SaveData` v3 migration, AND the loss-juice the milestone itself says IS the milestone — each with its own Play-validation. Hence the widened estimate.
- **Scope:** add `[GhostField] float Health` + non-replicated `float MaxHealth` to `PlacedStructure` (or a sibling `StructureHealth` on the structure ghost) — the **only net-new replicated state in the EB track**. Bake `MaxHealth` per-type from `StructureCatalogEntry` (additive column). Add an `EnemyTarget`/`Damageable` tag so `EnemyAISystem` can pick structures as targets; extend its nearest-target snapshot to include structures under a tunable aggro rule, reusing the existing `AppendToBuffer(DamageEvent)` site verbatim (`SourceTick`-stamped, per MC-1). Extend `HealthApplyDamageSystem`'s death branch (it already `DestroyEntity`s `EnemyTag`/`TrainingDummyTag` at HP≤0) to the structure tag — occupancy auto-frees because `BuildPlaceSystem` derives it from live ghosts. Client-only loss juice (flash/debris/SFX) edge-detected from the ghost prune — a pruned structure ghost = a destroyed structure (the dict-prune-is-a-kill idiom enemies use).
- **Build notes:**
- `PlacedStructure` is an ownerless **interpolated** ghost — `Health` as a `[GhostField]` replicates server→all-clients with NO `OwnerSendType`/`GhostOwner` (server mutations just propagate, exactly like `StorageEntry` on the ledger). `PlacedStructure.Type` is currently the **only** `[GhostField]` on the structure ghost, so adding `Health` **re-hashes the ghost → MANDATORY re-bake of turret + Harvester/Fabricator/Conveyor + Wall/Pylon prefabs** (budget this as the slice's structural cost).
- **Cross-group ordering (the trap EditMode can't catch):** structure death runs in `HealthApplyDamageSystem` (predicted group); production runs in `Harvester/Conveyor/Fabricator` systems (plain group `[UpdateAfter(PredictedSimulationSystemGroup)]`). "Damage-before-production so a structure killed this tick cannot also produce this tick" is an ordering that **spans the predicted/plain boundary** — exactly the silently-ignored-`UpdateBefore` + invisible-cycle hazard CLAUDE.md documents (it throws only at world-creation/Play, never in EditMode). **Gate production on a live-`Health>0` read** rather than relying on cross-group `[UpdateBefore]`, and Play-validate the ordering.
- Death is structural — batch through the existing `HealthApplyDamageSystem` ECB, at-most-once destroy per tick (a structure could take a turret hit AND a Husk hit the same tick). Structures don't predict, so a server-only next-tick `DamageEvent` drain is fine.
- **`SaveData` v3 must persist structure `RemainingHealth`** alongside the existing `RemainingTicks` cooldown, or `BaseRestoreSystem` brings bases back at full HP. **Assert-then-verify** the live `SaveData.CurrentVersion` (=2 today, `SaveData.cs:53`) and its serialized field set (`Goal`/`Ledger`/`Structures`/`StructureIo`) in code at EB-1 time before choosing the additive v3 bump.
- **Fun-gate (falsifiable — observable proxies, not inferred motive):** in a live Host+client siege a Husk the team fails to intercept visibly walks to a placed turret and **destroys it** — you watch it explode and the cell is now empty/rebuildable; **in run N+1 the team places ≥1 defensive structure at (or guarding) the cell where the breach happened in run N (observed)** — i.e. the loss demonstrably changed how they build, not just how they fight; server and all clients agree on which structures are gone (no ghost-structure desync, `execute_code` diff).
- **Tuning knobs:** `StructureCatalogEntry.MaxHealth` per type (baked — Turret 200 / machines 120 / Conveyor 60 / Wall 400 / Pylon 150); the siege structure-vs-player aggro rule (server singleton, live); Husk-vs-structure damage multiplier (server singleton, default 1.0).
- **Open questions:** **(core feel fork — [locked](#locked-decisions-path-a))** Husks PREFER structures (They Are Billions — swarm the base) or PREFER players (DRG — hunt you) with structures as collateral? — changes whether the base is a fortress or bait; live server-singleton knob. EB-1 ships **peripheral-only**; the base-anchor-as-lose-condition is a deliberate END-1 fork. Persist structure HP in `SaveData` v3 or boot-full each session?
- **Claude:** the `StructureHealth` component + `[GhostField]`, the `EnemyAISystem` target-extension, the `HealthApplyDamageSystem` death-branch edit, the production live-Health gate, the `SaveData` v3 field, EditMode coverage (structure takes damage → dies → cell frees; save round-trips HP); validate server==client structure-destroy + the cross-group ordering via `execute_code`/Play. **Operator:** the target-preference fork (fortress vs. bait), the anchor-as-lose-condition decision, **owning the mandatory netcode review** (the `[GhostField]` re-hash + the cross-group production-ordering race), and the play-gate (does losing a machine FEEL like a loss?) watching a real siege.
- **Dependencies:** MC-4 (a fun fight is the thing the stakes amplify). **Kill-risk:** a destroyed machine reads as a silent despawn (no weight, no "oh no") — the loss is mechanically present but emotionally absent and the braid's stakes evaporate. The juice + the target-preference tuning ARE the milestone, not optional polish.
> **Note on EB-1's siege director:** EB-1 deliberately does **not** ship the MC-2 enemy-mix director (that's Path B). It runs against the *existing* `ThreatDirector`/`CyclePhase`/`WaveSystem` single-siege output. A single readable Charger-led siege is enough to prove "a siege can destroy what you built"; the weighted mix is depth layered on later.
### EB-2 — The felt spend: automation output → a depletable combat resource `~11.5 wk` · risk MEDIUM
**Goal:** turn the ledger number into combat power you NOTICE spending and running out of — close the automation→combat loop. **The braid's keystone is EB-1 + EB-2 together:** a turret you FED from your factory's output, destroyed by a siege you must spend that same output to survive, with a real loss when you fail. If that loop is fun, the braid is proven.
- **Scope:** pick ONE depletable resource to start — **turret AMMO** (cleanest, no player-prediction). A per-turret **server-only** `Ammo` count fed from the shared `ResourceLedger`; `TurretFireSystem` decrements per shot and refuses to fire empty. A server-only reload (fold into `TurretFireSystem`) that pulls munition from the shared ledger when below capacity — so the Fabricator's output literally becomes turret uptime (reuse `StorageMath.Withdraw` + `GetSingletonEntity<ResourceLedger>`). A new munition `ItemId` (e.g. `Charge`/`AetherCell`) so automation produces something whose ONLY use is feeding the fight. Replicate ammo minimally: a **single empty/loaded enableable-or-byte** on the structure ghost (no `[GhostField]` count needed — just the empty edge) so the HUD + a `CombatFeedbackSystem` cue shows a starved turret. HUD readout (`HudSystem` observe-only): the shared munition stockpile + a per-turret empty indicator.
- **Build notes:** keep the spend **server-only** in the plain group — turret ammo is on an interpolated ghost, never predicted; mutate the server-only count, replicate only the empty EDGE. The spend MUST read the **shared `ResourceLedger`** (the untagged global ghost), NOT personal `InventorySlot` bags — co-op coherence (DR-026's latent gap; see [The co-op braid](#the-co-op-braid-the-latent-inventory-gap-stops-being-latent)). Munition production reuses `FabricatorProductionSystem` **unchanged** — author a Fabricator recipe whose `OutResourceId` is the new munition id (data-only); `ProductionMath`'s input-limited catch-up handles it. Do NOT route the munition through the *predicted* ability path yet (that's the player's own ammo — a later, higher-risk fork). Route the new id through the existing `ushort ItemId` space (DR-026, ids >3) — no wire change. Stamp `SourceTick` on the `TurretFireSystem` `DamageEvent` (already required by MC-1).
- **Fun-gate (falsifiable — observable proxies, not inferred motive):** during a siege you watch turrets EAT the munition the factory made and the stockpile readout visibly drops; **in a long siege the turrets go silent (run dry) ≥1× AND a player initiates a feed/build action within ~10 s of a silent turret (counted)** — i.e. the empty state demonstrably drives a behavior; a bigger factory measurably extends how long the base holds (timed, two factory sizes); two players splitting "I build the munition line / I hold the line" clears a fixed siege faster than one doing both (co-op via the shared ledger, **needs a friend** — Demo B).
- **Tuning knobs:** turret ammo capacity + shots-per-reload (baked, cap 30 / reload 10); munition cost per shot (live singleton, 1); the Fabricator munition recipe ratio + period (baked, 2 Aether → 1 Charge); empty-turret behavior — hard-stop vs. degraded slow-fire (live singleton, default hard-stop for clearer feedback).
- **Open questions:** **(fork — [locked](#locked-decisions-path-a))** turret ammo (server-only, safe) vs. the PLAYER'S abilities consuming a charge (stronger braid but touches predicted state — a deliberate later fork the operator green-lights only if turret-ammo proves the loop). Running dry = soft fail (turrets quiet) vs. contributes to the lose condition (ties to END-1)? One munition type or per-defense types? — start with ONE.
- **Claude:** the `Ammo` count + `TurretFireSystem` decrement/refuse-empty, the ledger-fed reload, the new munition id + Fabricator recipe authoring, the empty-state bit + HUD readout, EditMode coverage (fires N then starves; reload pulls from ledger); headless-validate the factory→turret pipe via `execute_code`. **Operator:** the production-vs-depletion balance (the make-or-break feel), the turret-vs-player-ammo fork, confirming the spend READS as the factory powering the defense; **the co-op feed-vs-fight read at Demo B (friend non-negotiable).**
- **Dependencies:** EB-1 (a loss state — otherwise an under-fed base has no consequence). **Kill-risk:** the stockpile never realistically runs dry (over-produced) OR drains so fast the base is helpless — either way the player doesn't FEEL the loop, which is the entire point. The balance tuning IS the milestone.
> ### Demo B — "The Loop" (after EB-2) — **first batched friend session for the braid**
> The first **cohesive vertical slice** that shows the braid, not just a tuned fight: a 515 min arc where you dodge the Charger's lunge and carve the swarm — every hit landing — while your turrets eat the munition your factory made (EB-2) and a siege can destroy what you built (EB-1). This is the demo that proves *"it feels like a game,"* not just *"the fight is fun."* **Needs a human friend** — the feed-vs-fight co-op specialization is the gate, and a friend's availability is an **external scheduling dependency on the critical path** (see [Solo + Claude cadence](#solo--claude-cadence)): pre-schedule it; the spine can block on a calendar, not just on code.
### END-1 — The base can be lost: a Core with integrity `~46 d` · risk MEDIUM
**Goal:** give the siege teeth — Husks that break through attack the Engine Core; if its integrity hits 0 the base is overrun.
- **Scope:** a `CoreIntegrity{[GhostField] int Current, Max}` on the existing GLOBAL CycleDirector ghost (the untagged ghost already carrying `CycleState`/`GoalProgress`/the ledger — **no new ghost, no relevancy work**); baked `Max` via `CycleDirectorAuthoring`, born-correct via `CycleDirectorSpawnSystem` (mirror the `PendingSave`/`GoalProgress` staging). A server-only `CoreDamageSystem` in the plain group `[UpdateAfter(PredictedSimulationSystemGroup)]` — a Husk reaching the Core radius (reuse `BaseGridMath.PlotCenter` + the `EnemyAIMath` in-range check) drains integrity and despawns. A `CoreRestoreSystem` — in Calm the Core regenerates toward Max (a chipped-but-survived Core is a setback you recover from). Lose-edge: `CoreIntegrity.Current<=0` during Siege sets a replicated `RunOutcome{byte=Overrun}`; the host-only persistence layer resolves the loss (see the lose-severity fork). HUD: a client-only base-integrity bar (mirror the `GoalProgress` hex-pip/bar path).
- **Build notes:** `CoreIntegrity` rides the EXISTING untagged ghost — do NOT region-tag it (`SetIsIrrelevant` would hide it cross-region; the shared-global-state rule). Core damage server-only in the plain group; a Husk reaching the Core is at-most-once-per-tick via the ECB destroyed-bitset. `RunOutcome` is a BYTE not an enum; the rollback (if chosen) is HOST-ONLY (persistence is host-authoritative). Any cooldown sentinel routes through `TickUtil.NonZero`. Sample the lose-edge once (guard re-firing while at 0 via a `RunOutcome`-already-set check).
- **Fun-gate (falsifiable):** in a live Host+client siege a Husk the team fails to intercept visibly walks to the Engine and the integrity bar ticks down — players reposition to body-block / focus it (**observed: a player abandons farming/repositions to defend the Core ≥1×**); in 3 test sieges the players can **name why they lost** (not a coin flip); a chipped-but-survived Core regenerating in Calm reads as "we got hurt but we're okay." **Crucially: re-run a Demo-B-era fun playtest WITH the Core present — if the fight is still mushy, STOP and fix the fight first.**
- **Tuning knobs:** `CoreIntegrity.Max` (baked, 100); Core-reach damage per Husk (baked/singleton, ~5 unintercepted Husks = a serious dent but not instant death); Core regen in Calm (baked, ~full recovery over one Calm); lose-severity mode hard-rollback vs. soft-drain (Tuning byte, default the operator's pick — recommend soft for co-op forgiveness).
- **Open questions:** **(lose-severity fork — the biggest, [locked](#locked-decisions-path-a))** hard rollback-to-autosave (clean, pillar-true, but a group loses minutes) vs. SOFT loss (Husks breach, drain a chunk of the shared ledger / damage structures, then the siege ends — no rollback, the base persists wounded; more co-op-forgiving, avoids save-corruption risk)? Does `CoreIntegrity` persist in `SaveData` v3 (a wounded base stays wounded) or boot full? Does a breach destroy placed structures (EB-1's job — recommend Core-bar-only here)?
- **Claude:** the component, the 3 server systems, the HUD bar, the lose-resolution wiring (rollback or soft-drain per the fork), EditMode coverage (Core drains under siege, regens in Calm, lose-edge fires once). **Operator:** the fun-gate playtest (is defending the Core engaging?), the diegetic Core/Engine placement, the lose-severity decision.
- **Dependencies:** EB-1 (the structure loss-state is the natural sibling; both make the siege threatening). **Kill-risk:** if defending the Core isn't fun, nothing downstream matters — a losable base only amplifies an already-good fight; shipping it before the fight earns it actively hurts.
### END-2 — The charge means something: the cap arms a final siege, win or lose `~24 d` · risk MEDIUM · ✅ DONE (2026-06-13, as slice SL-3)
> **Status (2026-06-13):** ✅ **CODE-COMPLETE + validated** — built as slice **SL-3**, COMPLETING Path A's spine. `RunPhase` (server-only) + `RunOutcome` (`[GhostField]`) on the CycleDirector ghost; new `GoalReachedSystem` arms the final siege at `Charge>=Target` (=4) ×live `FinalSiegeMultiplier`; `CyclePhaseSystem` latches **Victory** (final cleared) / **Loss** (Core breached) + halts the director; SaveData **v5** persists the outcome (a won/lost run loads finished). End-behavior LOCKED **halt + banner** (operator chose the clean terminal end over the spec's "keep playing"; the retry/quit overlay → SL-5, the pause menu is the in-session hatch). 342/342 EditMode green; Play-validated server==client + no ordering cycle; pre-coding + post-impl adversarial reviews. The **fun-gate** is the open operator item. See [[DR-036_END2_Final_Siege_Win_Lose]] · [[2026-06-13_SL3_END2_Final_Siege_Win_Lose]].
**Goal:** at `GoalProgress.Charge>=Target` the Engine begins opening the Wellspring — a final, larger escalating siege — and surviving it fires the WIN beat. The meter is no longer a number that stops at 10. **This is the minimum "the game has a point."**
- **Scope:** a server-only `GoalReachedSystem` that, on the `Charge>=Target` edge (currently UNHANDLED — `CyclePhaseSystem` increments past it forever with no clamp), arms a FINAL siege via the existing `ThreatState.PendingSiegeSize` entry point (bigger size + a distinct telegraph) and sets a replicated `RunPhase{byte=FinalDefense}`. The WIN-edge — surviving the final siege during `RunPhase.FinalDefense` sets `RunOutcome{byte=Victory}`, fires the ending event (subtitle/banner first), and for the minimum simply flips into "keep playing, the base is yours" (the endless/NG+ curve is END-5, Path B). A single client-only WIN/LOSS banner in `HudSystem` (observe `RunOutcome`; reused by END-1's overrun banner). **Zero net-new writing** for the minimum (placeholder banner text), **zero new ghosts.**
- **Build notes:** arm the final siege through the EXISTING single entry point — do NOT add a parallel siege path (`CyclePhaseSystem` stays the sole `WaveState` writer, DR-017's atomic Calm→Siege seed). `RunPhase`/`RunOutcome` are BYTES, single-writer, server-decided. The banner is client-only observe-only. Guard the `GoalReached` edge so it arms EXACTLY ONCE (a `RunPhase!=Normal` guard) — and **clamp the currently-uncapped `Charge`.** If cadence moves to Aether-deposited (the fork below), keep it a server-only single writer (avoid co-op double-count).
- **Fun-gate (falsifiable — countable behavior, not vibe):** a grinding team sees the meter approach full and the Engine telegraphs the Wellspring opening; **the team treats the final siege differently from a normal one (observed: deliberate pre-siege prep — repositioning, topping up munition, a callout — before the final wave, ≥1 countable action)**; losing it stings but the continue means "try again," not "over forever"; winning produces a "we did it" moment even with placeholder text. The final siege is visibly larger/distinct (`liveEnemyCount` peak measurably exceeds a normal siege).
- **Tuning knobs:** `GoalProgress.Target` (baked, currently 10 — the run-length knob); final-siege size + escalation multiplier (server singleton, ~23× normal); charge-per-source (Tuning, +1/siege).
- **Open questions:** **(charge cadence — highest-leverage, [locked](#locked-decisions-path-a))** sieges-survived (combat-only, ships today — the only writer is `CyclePhaseSystem` +1/survived-siege) / Aether-deposited-at-the-Engine (economy braid) / both? Recommend siege-survived first (ships without an economy dependency), both long-term. **(win-resolution — [locked](#locked-decisions-path-a))** WIN = endless/NG+ (END-5) vs. a hard ending + credits + free-play? — the minimum is "keep playing"; the fork is decided at the END-2/Decision-Gate boundary. One big final wave vs. a multi-stage gauntlet + boss? — a big escalating wave for the minimum (boss is Cut).
- **Claude:** the `GoalReached` arming + `Charge` clamp, the win/loss-edge bytes, the banner, EditMode coverage (cap arms the final siege once; win-edge fires once; loss falls through to continue). **Operator:** the climax-feel fun-gate, the charge-cadence fork, the win-screen writing (or accept placeholder), the win-resolution call at the Decision Gate.
- **Dependencies:** END-1 (a losable base — otherwise "win" is hollow because you could never have lost). **Kill-risk:** the final siege is indistinguishable from a normal siege → the "win" is anticlimactic and the meter stays meaningless; it must escalate visibly.
### The Decision Gate (MANDATORY STOP after END-2) ★
END-2 completes Path A: a fight that's fun, braided to a felt economy, with a base you can lose and a real win/lose condition — **a complete, shippable small game with a point.** Before ANY Path B milestone (MC-2/MC-3/MC-5/MC-6/EB-3/EB-4/EB-5/END-5) begins, an **explicit operator decision MUST be logged** (a session note / DR):
1. **Ship/share the minimum and stop here**, or
2. **Commit to exactly ONE Path B milestone** — re-deriving its estimate from scratch against the now-known feel, and re-running this gate after it.
**No Path B milestone may start until that decision is logged.** This is the enforcement point of depth-before-breadth: the forever-track always offers one more thing, and a solo dev with no deadline is most at risk of never shipping. The gate forces the question "is the minimum game good enough to put in front of people?" before the unbounded backlog reopens. Path B's table below exists to inform that choice — not to be executed as a sprint. **And before building the chosen Path B milestone, lock its forks first via the fork-locking ritual ([Locked decisions](#locked-decisions-path-a)) — present each fork to the operator and let them decide; never auto-decide a gameplay-design question.**
---
# PATH B — The forever-track (PROVISIONAL, NOT SCHEDULED)
> **Everything below is depth and breadth that only earns the right to exist once Path A is proven fun.** Estimates are **indicative only and WILL be re-derived** after Path A's fun-gates pass and the [Decision Gate](#the-decision-gate-mandatory-stop-after-end-2) is logged — **do not treat this as a commitment or a schedule.** A solo dev with no deadline will not finish seventeen milestones; this section is a menu the operator picks ONE item from at a time, re-deciding after each. The build notes here are real (the netcode homework is done) so that *if* an item is chosen it starts on solid ground — but choosing is gated.
## Path B — indicative menu (re-derive before committing any row)
| ID | Name | Track | Risk | Indicative | Unlocks |
|---|---|---|---|---|---|
| **MC-2** | Mix the questions: ranged + swarm + mix-director | Combat | MED · review-gated | ~11.5 wk | reposition + surround; siege mix table |
| **MC-3** | Every hit lands (pure juice) | Combat | LOW | ~0.75 wk | weight; freeze-frame |
| **MC-5** | The co-op keystone: downed + revive | Combat | MED · review-gated | ~1.52 wk | death-as-crisis; interdependence |
| **MC-6** | The full kit: multi-slot loadout | Combat | HIGH · review-gated | ~56 wk | complementary builds |
| **EB-3** | Base repair: spend to recover the loss | Economy | LOW | ~0.50.75 wk | loss→recover half of the loop |
| **EB-4** | Tool-gated harvest, braided | Economy | LOW | ~0.75 wk | gear-tier → feedstock → fight |
| **EB-5** | Craft combat power: the Fabricator builds your arsenal | Economy | MED | ~1.52 wk | harvest→craft→equip→fight closes |
| **END-5** | 14p difficulty scaling + endless/NG+ | Endgame | MED | ~34 d sys | co-op scaling; a reason to keep the base |
*(END-3 narrative beats and END-4 content-treadmill are NOT in this menu — they are deferred into the [Cut table](#cut--not-yet-anti-breadth-creep) as pure breadth wearing a low-risk-system costume; see the rationale there.)*
## Path B — milestones (provisional)
### MC-2 — Mix the questions: ranged threat + the swarm + the mix-director `~11.5 wk (indicative)` · risk MEDIUM · **review-gated**
**Goal:** add the **reposition** question (Spitter) and the **surround** question (Swarmer), driven by a weighted enemy-MIX band table layered on the EXISTING siege scheduler.
- **Spitter** — *the only genuinely-new netcode in the whole combat track*: a server-spawned **interpolated** enemy-projectile ghost moved by a NEW plain-group `EnemyProjectileMoveSystem` (stores its own `LastStep`; rebuild the swept segment from `cur - dir*LastStep`, **never `SystemAPI.Time.DeltaTime` in a plain-group system**) + swept `EnemyProjectileDamageSystem` (`SourceTick`-stamped, at-most-once `ecb.DestroyEntity`, **tunnelling regression test**). Prefer a **telegraphed ground-puddle** (L4D2 Spitter) over a fast bolt; cap speed.
- **Swarmer:** brain-0 tuned tiny/fast/near-zero-windup, spawned in count, so the dash also answers "don't get surrounded" (and MC-4's cleave answers it *better*).
- **Mix-director (the genuinely-NEW work — corrected):** the siege *scheduler already exists*`ThreatDirectorSystem` (arms sieges, `PendingSiegeSize`, `SiegeTimeoutTicks`, post-expedition retaliation) + `CyclePhaseSystem` (Calm↔Siege) + `WaveSystem` (Lull/Spawning cadence). Do **not** re-derive pacing. The new work is replacing `WaveSystem`'s blind round-robin with a **deterministic weighted band table** (which brain spawns at which point in a siege). **CRITICAL determinism note:** `WaveSystem` overloads `SpawnCounter` as BOTH the prefab index (`SpawnCounter % prefabs.Length`) AND the ring-placement slot (`RingPosition(center, SpawnCounter, slots)`). A naive "replace the modulo" edit will **silently desync enemy ring positions server-vs-client** (a Play-only bug EditMode won't catch). The weighted pick MUST be a **pure function of an integer counter that does NOT alter `SpawnCounter`'s advance**, OR add a separate placement counter; cover it with the determinism test. **REUSE `ThreatConfig`/`ThreatState`.**
- **A within-fight power beat:** a mid-fight `UpgradePickup`/`StatModifier` spike so offense has a rising curve, not pure attrition (both already exist).
**Fun-gate (falsifiable):** a fresh tester, un-coached, dodges the lunge, repositions out of ≥1 Spitter puddle, and breaks up a Swarmer cluster within one 5-min siege (observed). **DODGEABLE-BOLT METRIC:** at the shipped cap speed, a reacting tester avoids the Spitter telegraph/puddle in ≥8/10 attempts under ~100 ms interp — if they can't, the speed is too high (this is the Play-gate, not a spec number). **PEAKS+BREATHERS:** `liveEnemyCount` over the siege shows ≥1 spike and ≥1 lull, not a flat trickle. **POWER MOMENT:** the tester reacts to the mid-fight upgrade ("now I can…") ≥1×/session.
**Tuning knobs:** Spitter speed / puddle radius / dwell / windup; Swarmer count / speed / windup; mix-band weights (baked table + a live multiplier); the existing hot `ThreatConfig`/`WaveDirector` knobs surfaced live.
**Open questions:** **(fork)** puddle (area-denial, slower — recommended) vs. dodgeable bolt? **(decision — [locked](#locked-decisions-path-a))** `SaveData` v3 (=2 today, `SaveData.cs:53`) holds Goal/Ledger/Structures/StructureIo — `WaveState`/`ThreatState` are **NOT** serialized, so a save/load mid-siege resets the wave. **Assert-then-verify** the live version + field set in code at MC-2 time before choosing: accept session-only sieges (cheaper) **or** bump to v3 with `WaveState`+`ThreatState` appended (additive)? Does the mix-director key off siege-progress (Husks spawned this siege) or wave count? (integer counter either way).
**Build notes:** the Spitter is the only new ghost — ownerless interpolated, moved SERVER-ONLY in the plain `SimulationSystemGroup` `[UpdateAfter(PredictedSimulationSystemGroup)]`, stock `LocalTransform` replication, reuse MC-1's `SourceTick`. **Cover the swept hit-detection with a tunnelling regression test.** **Run a lighter design-review** here — it's the track's only new ghost type ([Risk register](#risk-register) R4).
**Claude:** the two enemy-projectile systems + the new ghost (duplicate-an-existing-ghost recipe), the Swarmer baked variant, the weighted mix-band table + `WaveSystem` refactor (preserving `SpawnCounter`'s ring-advance), the mid-fight power-beat wiring, the tunnelling + ring-determinism regression tests; the `SaveData` v3 migration *if* the operator chooses persistence. **Operator:** the Spitter speed/puddle Play-gate; the mix-band weight tuning; the session-vs-persisted decision; owning the lighter review.
**Dependencies:** Path A complete + the Decision Gate logged. **Kill-risk:** the Spitter is un-dodgeable under latency → reads as unfair chip damage instead of a reposition question — the puddle-over-bolt choice de-risks it. Secondary: a botched `SpawnCounter` refactor desyncs ring placement.
### MC-3 — Every hit lands (pure juice) `~0.75 wk (indicative)` · risk LOW
**Goal:** make the now-meaningful exchange *feel* like one — **real freeze-frame** hit-stop, enemy hit-flash, magnitude-scaled emphasis and knockback — all client-only, observe-only. *(MC-3 is kept **pure juice**; the co-op `EnemyStatus` amp lives in MC-5 where interdependence is the theme — see [Boundary judgment](#boundary-judgment-re-cut-mc-1-default-order-mc-4-early). A juice gate shouldn't also have to prove a co-op synergy.)*
- **REAL freeze-frame hit-stop** (37 frames, **local to the killer**, gated on `DamageEvent.SourceNetworkId` / the local-player edge — `_localPlayer` is already resolved in the system) — **genuinely new:** `CombatFeedbackSystem` does FOV-punch (`PrototypeCameraRig.PunchFov`) + per-magnitude shake + kill FOV punch today, **not** a held freeze. Never `Time.timeScale` (corrupts the sim) — hold the camera/anim *presentation* only. **Do NOT edge-fire on a phantom hit** that MC-1's prediction-reconciliation corrects away (see MC-1's reconciliation-flicker note).
- **Enemy mesh hit-flash** (`AnimatedLitShader` emission via per-instance `MaterialPropertyBlock`) — **DE-RISK ON DAY 1:** confirm the shadergraph exposes an emission input before committing the milestone to it; watch shared-material bleed.
- **Emphasis tiers:** size/shake/SFX scale by hit magnitude + kill tier (extend the existing `FeelConfig`-driven scaffold).
- **Live-tunable, damage-scaled directional knockback:** promote `Tuning.Knockback*` (currently compile-time consts) into a server singleton read by `ProjectileDamageSystem`/`EnemyAISystem`.
**Fun-gate (falsifiable):** **WEIGHT BLIND-TEST** — a watcher ranks three hits (tickle / solid / haymaker) by feel alone in ≥8/10 trials. **KILL PUNCTUATES** — a kill is unmistakably louder than a hit; the tester can tell a kill happened without watching the health bar. **NO SIM CORRUPTION** — server==client position/health unaffected by the hit-stop, verified by an `execute_code` diff during heavy hit-stop (`localHitStopFrames` fires only for the *local* killer).
**Tuning knobs:** hit-stop frames per tier (3/5/7); flash color / duration; emphasis size/shake/SFX per tier; knockback speed / duration — all live `FeelConfig`/singleton.
**Open questions:** hit-stop on ALL hits or only kills + big hits? (all-hits feels chuggy in a swarm — recommend kills + a magnitude threshold).
**Build notes:** all juice = client-only managed `SystemBase` in `PresentationSystemGroup` that OBSERVES (the scaffold already is); read ECS via `SystemAPI.Query` + `EntityManager.CompleteDependencyBeforeRO<T>()`. `DamageEvent.SourceNetworkId` already exists — gate the freeze "local to the killer."
**Claude:** freeze-frame hit-stop, emphasis-tier scaling, the `Knockback*` const→singleton promotion, all `FeelConfig` wiring; the hit-flash `MaterialPropertyBlock` IF the emission input is confirmed. **Operator:** confirm the `AnimatedLitShader` emission input (or supply one); all feel tuning; the weight blind-test.
**Dependencies:** Path A complete + Decision Gate; MC-1 (hits must matter before juicing them). **Kill-risk:** hit-flash blocked because the shader has no emission input (then it needs a shader edit — scope creep) — de-risk day 1.
### MC-5 — The co-op keystone: downed + revive `~1.52 wk (indicative)` · risk MEDIUM · **review-gated**
**Goal:** death becomes a shared crisis with a heroic choice — push to revive vs. hold the line. Makes co-op interdependent, not parallel.
- **Downed → Dead** three-state: `Downed` = a derived enableable (the proven `Dead`-from-`Health` idiom). **The full idiom has THREE clauses — carry all three, not just the `Health<=0` derive:** (1) derive `Downed` from `Health<=0`; (2) **bake `Downed` DISABLED** (players spawn up); (3) the derive system must **visit downed entities via `.WithPresent<Downed>()`** to write the enabled bit on a currently-disabled entity (verified: `PlayerDeathStateSystem` uses exactly `.WithPresent<Dead>()` on the baked-DISABLED `Dead`). Rooted + fire-disabled but still present. **Derive-race fix:** `Downed` and `Dead` both derive from `Health<=0`, so replicate the discriminator — add a **`[GhostField] uint DownedUntilTick`** (bleed-out deadline; `RespawnInvuln{[GhostField] uint UntilTick}` is the exact template) so the owner derives both gates locally and doesn't mispredict downed→dead. Authoritative schedule stays server-side.
- **Proximity revive:** a `ReviveRequest` `IRpcCommand` (scalar payload = downed ghostId, `BuildPlaceRequest` template), applied **server-only** in the plain group `[UpdateAfter(PredictedSimulationSystemGroup)]` (rollback would double-apply) with server-side proximity + channel re-validation; bleed-out falls through to the existing `PlayerRespawnSystem` give-up path.
- **Focus-fire priority elite** (healer/buffer aura — DRG Warden / RoR2 Mending; killing it weakens the pack), flagged with a replicated byte.
- **`EnemyStatus` co-op-amp (lives here, not MC-3):** a **server-only byte** (NOT a `[GhostField]`, NOT an enum) stamped at the existing `KnockbackState` site in `ProjectileDamageSystem`, read in `HealthApplyDamageSystem`'s sum loop to amplify summed damage so support+burst out-performs two soloists.
- **Friendly-fire fix:** do **not** add server-only `KnockbackState` to a *predicted* player (it fights prediction → rubber-band). Soft-FF = a `StatModifier` debuff / revive-channel interrupt only; gate raw-HP FF behind a Tuning toggle (default OFF).
**Fun-gate (falsifiable, needs a friend):** at least one **contested revive** (`reviveChannelsStarted` with enemies inside the revive radius) AND one **bleed-out-to-dead** in the same session; a revive attempt OR a deliberate let-them-bleed call in ≥80% of downs. **INTERDEPENDENCE:** a support+burst duo clears a fixed siege measurably faster than two identical soloists (timed; the `EnemyStatus` amp pays off). **PRIORITY BREAK:** the team breaks target to focus the elite when its aura is active in ≥8/10 elite spawns.
**Tuning knobs:** bleed-out / revive-channel / revive-radius / elite-aura radius+strength / `EnemyStatus` amp + duration — all live singletons; FriendlyFire toggle (Tuning, default OFF).
**Open questions:** **RUN THE ADVERSARIAL NETCODE/DETERMINISM REVIEW BEFORE CODING.** Downed = fully rooted or crawl-at-reduced-speed? Does reviving cost a resource (ties to EB)? Elite = new brain or a flagged Husk variant with an aura component?
**Claude (after the review):** the `Downed` enableable (baked-disabled + `.WithPresent<Downed>()` derive) + `DownedUntilTick` `[GhostField]`, the `ReviveRequest` RPC + server handler, the elite aura byte + effect, the `EnemyStatus` amp; EditMode tests for the derive-race, the baked-disabled-bit write, and the revive proximity gate. **Operator:** decide the forks; run/own the review; two-player playtests; channel/bleed-out feel.
**Dependencies:** Path A complete + Decision Gate; the fight must be worth a revive before death-as-crisis means anything. **Kill-risk:** the downed/dead derive-race mispredicts (owner flickers downed↔dead) because the discriminator isn't replicated correctly, OR "downed never triggers" because `Downed` wasn't baked-disabled / the derive didn't `.WithPresent<Downed>()` — both pre-flagged; the review must validate before coding.
### MC-6 — The full kit: multi-slot loadout `~56 wk (indicative)` · risk HIGH · **review-gated**
**Goal:** offense matches the defense and the threat roster — a RoR2 four-slot kit (Primary / Secondary / Utility=Dash / Special) over distinct archetypes, so two players bring complementary builds. **Deliberately last in any combat path** — a kit only pays off once a meaningful fight, threats, feel, and co-op exist to express into.
> **This is 56 weeks, not 34** ([secretly-multi](#secretly-multi-milestones-why-the-estimates-widened)): a 4-slot serializer generalization (re-bake risk) + per-slot `StatRecompute` + classifier ghost-type-SET + 4-input wiring + HUD, each with the mandatory review and rollback-correctness tuning.
- **Slot axis:** generalize `AbilityRef{byte Id}` + `AbilityCooldown{uint}` to a **fixed `Slot0..3` struct** (4 byte ids + 4 `[GhostField]` `NextFireTick` — a fixed struct over a `DynamicBuffer` to keep serializer churn to **ONE re-bake**) + 4 `Fire`-style InputEvents.
- **The real lift — per-slot `EffectiveAbilityStats`:** `StatRecomputeSystem` folds the character-wide `StatModifier` buffer into each slot, kept **unconditional/uncached** every predicted tick (a change-filter goes stale on rollback — the same discipline the single-slot case already uses).
- **Classifier:** generalize `ProjectileClassificationSystem` to a **ghost-type SET** (DR-016 flagged; keep it **non-Burst** — cross-assembly generics + predicted-spawn classification trip Burst ICEs). **Ship hitscan/cone first (no predicted spawn, no classification) so the classifier generalization is deferred until a 2nd predicted-spawn prefab exists. Fill 23 slots before 4.**
- Reuse the MC-4 archetype byte `switch` as the per-slot dispatcher; store all ids/ops as **byte**.
**Fun-gate (falsifiable):** two players run **different loadouts** and the readout shows complementary archetype usage (`slotFires[0..3]` all > 0 for each; not both spamming slot 0); a tester forms a combo grammar (e.g. utility→special→primary) deliberately in ≥8/10 fights; two different loadouts clear a fixed siege faster *together* than two copies of the better single loadout (timed). **NO ROLLBACK STALE:** per-slot `EffectiveAbilityStats` is correct after a forced rollback (server==client per-slot stats, via `execute_code`).
**Tuning knobs:** per-slot cooldowns (live, differentiated); per-archetype base stats (baked blob); slot→input bindings (input asset, operator).
**Open questions:** **MANDATORY ADVERSARIAL DESIGN REVIEW BEFORE `create_script`.** Which 3 archetypes ship first (projectile/hitscan/cone are no-predicted-spawn-friendly; ground-AoE forces the classifier work)? Fixed loadout vs. swappable at the base (swap = EB territory)? Utility hard-wired to Dash, or a free slot?
**Claude (after the review):** the `Slot0..3` generalization, the 4-input wiring, per-slot `StatRecompute` fold, the archetype dispatch per slot, the classifier ghost-type SET when the 2nd predicted spawn lands, the 4-cooldown HUD; EditMode tests for per-slot stat folding and rollback-correctness. **Operator:** own the review; the slot bindings; balancing the four archetypes; the two-loadout co-op timing gate.
**Dependencies:** Path A + MC-2…MC-5 (a fight, threats, feel, co-op to express into). **Kill-risk:** per-slot stats mispredict on rollback → slot stats diverge; OR the classifier generalization trips a Burst ICE — both pre-flagged; the review must validate the serializer + classifier plan before coding.
> ### (Deferred) Demo D — "The Loadout" (after MC-6)
> Two players bring complementary 4-slot kits and feel like different classes. Reads as a game but as *depth on top of* a game that already exists — deliberately last. The operator can self-validate with an MPPM second client for a first pass; not a primary friend checkpoint.
### EB-3 — Base repair: spend to recover what the siege took `~0.50.75 wk (indicative)` · risk LOW
**Goal:** close the loss→recover half of the loop so a destroyed base is a setback you pay to undo, not a dead end. *(Tiny, pure reuse.)*
- **Scope:** a `RepairRequest` `IRpcCommand` (`BuildPlaceRequest` template, scalar payload = target cell/ghost-id) → a server-only `RepairSystem` (plain group, `AbilityUpgradeSystem` owner-map idiom) that spends shared-ledger resources to restore a damaged structure's `Health` toward `MaxHealth`. Reuse the in-place `StorageMath.Withdraw` + affordability check from `BuildPlaceSystem`; cost scales with missing HP (a tunable curve) so a near-dead machine costs more to save than to let die and rebuild — a real economic decision. Optionally a between-siege auto-repair **Mender** structure (additive `StructureType` byte) that slowly heals adjacent structures from the ledger. Wire a repair mode into the existing build palette (reuses `BuildPreviewMath` + the RPC-send pattern).
- **Build notes:** server-only, applied once (NOT predicted — rollback would double-apply). Repair RESTORES EB-1's `[GhostField] Health` — no new replicated field. Cost from the shared `ResourceLedger`. The Mender (if shipped) is a production-class system `[UpdateAfter(PredictedSimulationSystemGroup)]` mirroring `HarvesterProductionSystem`. Validate affordability + a live target BEFORE withdrawing (the commit-in-place rule); reject a repair on an already-destroyed/full structure silently.
- **Fun-gate (falsifiable):** after a siege chews up your base you spend the calm REPAIRING, and "repair this turret vs. build a new one vs. save for ability upgrades" is a real tradeoff (observed: the player chooses repair over rebuild ≥1× and over a different spend ≥1× in one calm); the repair cost makes you protect structures *during* the fight; a base recovers across cycles if you tend it and decays if you don't.
- **Tuning knobs:** repair cost curve (server singleton, default linear 1 Ore / 5 HP, possibly super-linear); Mender auto-repair rate + ledger drain (baked); repair resource type (server singleton, default Ore).
- **Open questions:** manual repair only (agency, a verb — recommended first) vs. auto-Mender vs. both? Full restore vs. partial? — start full.
- **Claude:** the `RepairRequest` RPC + `RepairSystem` + cost-curve math + the optional Mender + build-palette repair mode + EditMode coverage (restores HP, costs ledger, rejects on dead/full target). **Operator:** the manual-vs-Mender fork and the repair-vs-rebuild cost tuning.
- **Dependencies:** EB-1 (structures must have Health), EB-2 (a spend economy the player is engaged with). **Kill-risk:** repair is always strictly cheaper than rebuilding (no decision) OR always more expensive (dead code) — the cost curve must sit in the interesting middle.
### EB-4 — Tool-gated harvest, braided `~0.75 wk (indicative)` · risk LOW
**Goal:** resume inventory **Phase 2** ONLY as a combat-feeding loop — your equipped tool tier sets how fast you can feed the war economy.
- **Scope:** resume DR-026 Phase 2 — `RequiredToolType`/`RequiredToolTier` baked on `ResourceNode`; `ResourceHarvestSystem` gates + **scales** harvest yield by the firing player's equipped Tool-slot tier. **Braid it:** the harvested feedstock flows into the EB-2 munition pipe (better tool → more raw Aether → more Charges → more turret uptime → you fight longer) so the upgrade is felt AS combat power. Reuse the existing optional `ComponentLookup<GhostOwner>` + tier read already in `ResourceHarvestSystem`; the yield-scale is a pure `InventoryMath`/`HarvestMath` multiplier (no new replicated state — Tool is already an `EquipmentSlot` `[GhostField] ItemId`). A small set: 23 tool tiers in `ItemDatabase`.
- **Build notes:** keep the owner-lookup OPTIONAL so owner-less projectiles (the 8 legacy harvest tests) still pass (DR-026's exact constraint); stay `[BurstCompile]`. Tier from the `EquipmentSlot` Tool ItemId → `ItemDatabase` `byte Tier` (baked) — no new field. Gate is a yield MULTIPLIER (soft) by default, not a hard block-to-zero — tunable. Keep the deposit→ledger→munition pipe SHARED (co-op).
- **Fun-gate (falsifiable):** equipping a better tool visibly speeds your sortie haul AND a base measurably holds longer / abilities become affordable downstream — the gather upgrade pays out in the FIGHT, not on a stat screen; "spend on a better tool vs. a better weapon vs. more turrets" is a real loadout-economy fork; a co-op pair can specialize (one tools-up to feed, one fights) and it's strictly better.
- **Tuning knobs:** per-tier yield multiplier (baked — T1 ×1 / T2 ×1.75 / T3 ×3); gate mode soft vs. hard (server singleton, default soft); node `RequiredToolTier` per resource (baked).
- **Open questions:** soft gate (faster with the tool) vs. hard gate (nodes locked behind tiers)? Does tier affect WHICH resources or only speed? — start speed-only.
- **Claude:** the harvest gate + yield-scale, node `RequiredTool` authoring, the tool-tier catalog rows, EditMode coverage (tier scales yield; soft/hard gate; no-owner fallback preserved). **Operator:** the soft-vs-hard fork and confirming the upgrade is FELT in the fight.
- **Dependencies:** EB-2 (the munition pipe the feedstock feeds — **sequencing after EB-2 is non-negotiable**); 23 seeded tiers or EB-5 for climbing. **Kill-risk:** if the faster haul just inflates a ledger nobody feels spending, the tool gate is pure grind — it ONLY works braided to a felt spend.
### EB-5 — Craft combat power: the Fabricator builds your arsenal `~1.52 wk (indicative)` · risk MEDIUM
**Goal:** resume inventory **Phase 3** crafting ONLY for combat outputs — extend the Fabricator to craft weapons/munition/tool tiers. **Lands LAST in any path** (after MC-6) so the kit it crafts FOR exists.
- **Scope:** resume DR-026 Phase 3 braided — extend `FabricatorProductionSystem` (or a sibling `CraftSystem`) to produce ITEMS (weapons, gear, tool tiers, munition batches); the recipe is data (`ItemDatabase` + a recipe row), no new replication. A `CraftRequest` `IRpcCommand` for player-initiated crafting → a server-only system that spends the shared ledger and deposits the crafted item into the requester's bag or the shared store. Tie the tier curve: crafted tool tiers (EB-4) + crafted weapons (the `AbilityRef` a weapon grants via `EquipSystem`, DR-027) + munition batches (EB-2) all come from the same Fabricator economy. Land the additive `SaveData` v3 DR-027 flagged (restore equipment+inventory atomically AND **replay equip** — a plain buffer restore wouldn't re-add the `StatModifier`s; effects are event-driven), now also covering crafted-item progression.
- **Build notes:** crafting reuses the Fabricator's input-limited deterministic catch-up (`ProductionMath`) — an item recipe is a recipe whose output is an `ItemId`, deposited via `InventoryMath.Deposit`; server-only, plain group. Bump `SaveData.CurrentVersion` (Load nulls on mismatch → clean degrade to New Game). `CraftRequest` is a one-off shared-state RPC → reliable, server-only, applied once. Keep recipe content MINIMAL — 23 weapons, 23 tool tiers, 1 munition batch. A crafted weapon sets `AbilityRef.Id` via the EXISTING `EquipSystem` path — no new combat code; it's an equippable `ItemDatabase` row with a `GrantedAbilityId`.
- **Fun-gate (falsifiable):** you craft a better weapon FROM the resources your factory refined, equip it, and immediately fight better — the **full loop (harvest→refine→craft→equip→fight) closes** and you feel each step; the craft-queue decisions (munition now vs. a weapon for next sortie vs. a tool) are a genuine economy game inside the combat game; a late-game base feels like an ARSENAL you built, and co-op players craft complementary kits from a shared factory.
- **Tuning knobs:** per-recipe cost + craft period (baked rows); craft destination — requester's bag vs. shared store (server singleton, default shared store with player-crafted consumables to the bag); tier-gate — which recipes available from start.
- **Open questions:** auto-craft vs. player-initiated vs. both? — start player-initiated. Crafted weapons permanent vs. consumable? — start permanent + equippable.
- **Claude:** the item-crafting recipe extension, the `CraftRequest` RPC + server system, the `SaveData` v3 atomic equipment/inventory/crafting restore with replay-equip, the recipe content rows, EditMode coverage (craft spends ledger → item in bag/store; save round-trips equipment+inventory+crafted progression; equip replay re-adds `StatModifier`s). **Operator:** the auto-vs-manual fork and the play-gate that the full loop FEELS like one game.
- **Dependencies:** EB-2 (the munition economy), EB-4 (tool tiers), DR-027 `EquipSystem`, **MC-6** (slots to express into — why EB-5 lands LAST). **Kill-risk:** crafting content is the classic breadth trap — a deep recipe tree nobody needs because the fight it feeds isn't deep enough. Shipping it before MC-6 re-creates the breadth-first hollowness DR-028 exists to kill.
### END-5 — Difficulty scales for 14 co-op players + endless/NG+ `~34 d sys (indicative)` · risk MEDIUM
**Goal:** make the threat scale to player count (server-only, netcode-flagged) and turn "win" into an optional endless/NG+ curve so a persistent-base co-op group keeps a reason to play after the first victory.
- **Scope:** player-count-scaled siege size — at siege-arm time `ThreatDirectorSystem` **SAMPLES the live connection count ONCE** and scales `PendingSiegeSize` (the existing single entry point) by a baked per-player multiplier (the inert `ThreatConfig.SizePerExpeditionResource * 0` line at `ThreatDirectorSystem.cs:62` is the template — add a `SizePerPlayer`; **SERVER-ONLY**, never a `[GhostField]`). A join/drop does NOT resize the in-flight wave (sampled once at arm) but DOES affect the next; the bounded `SiegeTimeoutTicks` prevents a soft-lock. NG+/endless (END-2's victory flip made concrete) — on victory raise `GoalProgress.Target` + apply a global difficulty step (size/speed/brain-weight multipliers); **the persistent base carries over** (the locked pillar — NOT a roguelike reset); only the threat curve resets upward. HUD: a difficulty/NG+ tier readout (a replicated byte, observe-only).
- **Build notes:** SAMPLE the player count ONCE at siege-arm — never re-read per tick (jitter / breaks the atomic Calm→Siege seed; server-only so no misprediction). Scale through the EXISTING single entry point — no parallel sizing path. Connection count = iterate `NetworkId`-bearing connections server-side. NG+ step + Target raise are server-decided bytes/ints on the director ghost; the base/structures/ledger CARRY OVER. The tier readout is a replicated byte, client observe-only.
- **Fun-gate (falsifiable):** a 4-player session feels appropriately swarmed vs. a solo session (both fun-gate — neither trivial-solo nor impossible-at-4); a player joining mid-session doesn't break the in-flight siege but the next visibly accounts for them; after winning, the NG+ tier gives a returning group a reason to keep their base; the scaling is invisible-but-felt.
- **Tuning knobs:** `SizePerPlayer` multiplier (baked, sub-linear); NG+ Target raise + per-tier multipliers (baked, +50% Target / +15% per tier); the sub-linear curve exponent (Tuning, <1.0 so grouping is rewarded); mid-siege join policy fixed by the sample-once rule (no knob).
- **Open questions:** scaling shape — linear vs. **sub-linear** (recommended) vs. an HP-vs-count tradeoff? NG+ depth — truly endless vs. a capped ladder (pillar leans endless)? Does difficulty also scale Core integrity / siege frequency, or only wave size + brain-weight (recommended)?
- **Claude:** the sample-once scaling, the NG+/endless flip + difficulty-step multipliers, the tier HUD byte, EditMode coverage (size scales with sampled count; mid-siege join doesn't resize; NG+ raises Target). **Operator:** the 1-vs-4 fun-gate, the scaling-curve tuning, the NG+ depth decision.
- **Dependencies:** END-1, END-2 (the win flip NG+ extends), MC-5 (co-op difficulty is incomplete without the shared-crisis loop). **Kill-risk:** naive per-tick live-count scaling that makes siege size jitter or soft-locks on a mid-siege drop — the sample-once-at-arm rule is the load-bearing fix.
---
## Cross-cutting discipline
### Calendar-time: the play-budget assumption ★
The estimates above are **coding-time**, and the single biggest scope-realism distortion is reading coding-time as calendar-time for a one-person team whose throughput is gated by **focused-editor availability** (Burst-affecting edits, Play-validation, all asset/scene work) and **a friend's calendar** (the co-op fun-gates). The document admits fun-tuning is unbounded, then would otherwise present crisp day/week numbers a reader anchors on — the two statements contradict and the crisp numbers win. So, explicitly:
> **Assume ~58 focused-editor hours/week** for Burst edits, Play-validation, solo dry-runs, and fun-tuning, plus a periodic friend session. At that budget, **Path A's ~710 weeks of coding is a ~47 MONTH calendar effort**, not two months — because the coding lane (Claude, largely headless, unfocused-OK) outruns the focused-tuning lane that actually closes each fun-gate, and the gate is what "done" means here. Path B is unbounded by construction; do not sum it.
Naming the budget makes the timeline falsifiable and stops the coding floor from masquerading as wall-clock. If the real weekly budget is higher or lower, rescale — but rescale *calendar*, never pretend the tuning cost is zero.
### Secretly-multi milestones (why the estimates widened)
Three Path A milestones are each genuinely 23 risky slices, each with its own focused-editor Play-validation cycle (and EditMode does NOT catch the ordering-cycle / stale-binary classes — they throw only at Play). Underestimating single-line milestones is how solo roadmaps silently slip 23×, and the optimism is concentrated exactly where the netcode/Burst risk is highest. So:
- **MC-1 (~2.53.5 wk):** new predicted `DashSystem` + replication · the Burst-affecting `CharacterProcessor` edit (own restart/validate) · the `DamageEvent.SourceTick` refactor across THREE stamp sites + the tick-windowed negation · the Charger brain (lunge/stagger/whiff) + telegraph tuning + dash juice.
- **EB-1 (~1.52.5 wk):** a ghost-hash-changing `[GhostField]` (re-bake every structure prefab) · an `EnemyAISystem` targeting rewrite · a `HealthApplyDamageSystem` death-branch change · a cross-group production-ordering gate · a `SaveData` v3 migration · the loss-juice the milestone says IS the milestone.
- **MC-6 (~56 wk):** 4-slot serializer generalization · per-slot `StatRecompute` · classifier ghost-type-SET · 4-input wiring · HUD · the mandatory review + rollback-correctness tuning.
### Boundary judgment: re-cut MC-1, default-order MC-4 early
**MC-1 is the smallest fun slice MINUS the Swarmer.** The dash + Charger + telegraph is a true bait-and-punish duel; the Swarmer answers a *different* question (surround/AoE) the MC-1 kit cannot answer (you can't punish a swarm by dashing through one enemy). It moves to MC-2 where "mix the questions" is the stated goal and MC-4's cleave gives it a real answer — sharpening MC-1's falsifiable claim to exactly *"timed-dash beats spam-dash vs a readable lunge."* **MC-4 is the DEFAULT second Path A milestone** — verified-low-risk (`PlayerFacing` replicated, `AutoTarget` cone math reusable, pure server damage, no new ghost) and it kills the second-most-felt hollowness; giving the player dash-in/cleave/dash-out early makes the harder fights expressible. **MC-3 is kept pure juice** (Path B) so its gate stays clean; the `EnemyStatus` co-op-amplification lives in MC-5 (where interdependence is the theme). A juice milestone whose gate must also prove a co-op damage synergy is two milestones in one coat.
### Where the Economy-braid + Endgame slot
Do **not** run the economy/endgame strictly after the full combat kit — that is the breadth-first trap DR-028 names. But do **not** front-load the full economy either. The shape is *a thin braid thread pulled into Path A, full breadth deferred to Path B*: **EB-1 (machine loss-state) + EB-2 (the felt spend) are in Path A**, right after MC-4, because every combat milestone before stakes is a fight with no consequence beyond your own (free) respawn. **END-1 + END-2** close Path A with a losable base and a win/lose condition. Everything deeper — the enemy-mix director (MC-2), repair (EB-3), tool-gating/crafting (EB-4/EB-5), scaling/NG+ (END-5) — is Path B, chosen one at a time after the Decision Gate. Each EB/END beat keeps its own fun-gate; none ships until the prior loop is fun.
### Verified-vs-corrected build notes (ground-truth audit)
The MC-1…MC-4 + EB build notes were re-read against the actual code, then re-audited by a second critic round. **Verified correct:** `DamageEvent` is exactly `{float Amount; int SourceNetworkId}`; `HealthApplyDamageSystem` is `ServerSimulation`-filtered AND `[UpdateInGroup(PredictedSimulationSystemGroup)]` + the sole `DamageEvent` drainer; `EnemyAISystem` appends in the **plain** `SimulationSystemGroup` `[UpdateAfter(PredictedSimulationSystemGroup)]` (a different group, drained the following tick); `CharacterControl` has only `MoveVelocity` and `CharacterProcessor` lerps `RelativeVelocity` at `GroundedMovementSharpness=15`; `spaceKey.isPressed` is in the kbm-active sentinel; `AttackWindup.WindUpUntilTick` is the `[GhostField]` `CombatFeedbackSystem` already cues off; `RespawnInvuln{[GhostField] uint UntilTick}` is the `DownedUntilTick` template; `PlayerDeathStateSystem` uses `.WithPresent<Dead>()` on a baked-DISABLED `Dead`; `PlacedStructure.Type` is the only `[GhostField]` (so adding `Health` re-hashes); `WaveSystem` overloads `SpawnCounter` as prefab-index AND ring-slot; `ThreatDirectorSystem.cs:62` is literally `SizePerExpeditionResource * 0 // deferred`; `SaveData.CurrentVersion = 2`. **Corrected against code:** (1) the i-frame negation is server-only **but still tick-determinism-critical** — the strike is appended a tick earlier in a different group, so the negation MUST compare `SourceTick` against `DashState`'s stored `[StartTick, IFrameUntilTick]` window via `NetworkTick`, never "is-DashState-active-now"; this is MC-1's review agenda item #1. (2) `DamageEvent` has **THREE** append sites, not two — `EnemyAISystem`, `ProjectileDamageSystem`, **and `TurretFireSystem.cs:95`** — all must stamp `SourceTick`. (3) the MC-2 "wave director" is largely **already built** (Threat/Cycle/Wave systems) — the new work is the weighted MIX table, not pacing — and the refactor must not perturb `SpawnCounter`'s ring-advance. (4) `WaveState`/`ThreatState` are NOT in `SaveData` — sieges are session-only today; persisting them is a **design decision**, assert-then-verify the version at code time. (5) MC-3's hit-stop is **genuinely new**`PunchFov` is an FOV kick, not a freeze.
### Fun-gate protocol
> Operationalizes DR-028's *"play it, with a friend, and not want to stop"* into a repeatable per-milestone checklist with OBSERVABLE, FALSIFIABLE criteria. A milestone is **done** only when all three gates pass: (1) EditMode green, (2) server==client in a real netcode Play session, (3) this fun-gate. Gates 1 and 2 are necessary-not-sufficient — they were the only bar through M7 and that is why the game is hollow.
**The session ritual (every milestone):** (1) **Solo dry-run first** — boot `Game.unity`, open the dev overlay (MC-0), force the exact threat the milestone introduces, play 510 min for *correctness-of-feel* and to read the instrumentation (NOT the fun-gate). (2) **Friend run** — two clients, one full intended arc, no coaching beyond a one-line prompt; the REAL sign-off for the co-op milestones (Demo A's two-human read, EB-2/Demo B, MC-5/Demo C) needs a human friend. (3) **Score the checklist** — every box is a yes/no a bystander could verify by watching the screen + the readout. If any kill-criterion box is no, the milestone is not done — tune or cut, do not advance.
**Universal checklist:** *Did-not-want-to-stop* (a player immediately re-engaged without prompting — pressed deploy/again, or said "one more"); *Readability* (the friend named the threat's intent *before* it resolved — dodged on the tell, not the hit); *No feel-regressions* (hit-stop never touched `Time.timeScale`; no rubber-band on the local player; server==client still holds during juice; the prediction-reconciliation flicker is noted-acceptable, not chased); *Instrumentation agreed with feel* (the readout corroborates the subjective claim). Per-milestone observable criteria are in each milestone's fun-gate field.
**Anti-gaming rule:** the instrumentation **corroborates**, it does not **define** fun. A slice that hits every number but the friend stops after one arc has FAILED the gate. Numbers exist to *falsify a false feel-claim* ("the dash feels skillful" but negated-hits/dash is 0.1), never to override a true negative. **Corollary (no inferred motive in a gate):** every fun-gate box must be a behavior a bystander can SEE — never a "because they remember…" or "they say this is it." Where the prior draft inferred motive, it's been replaced with a countable proxy (EB-1: places a structure at the breach cell next run; EB-2: feeds within ~10 s of a silent turret; END-2: a deliberate pre-final-siege prep action).
### Instrumentation (extend the M8 dev-tools triad)
Built in **MC-0**, the measurement vehicle for every gate. A server-only `DevTelemetry` struct of `uint` counters + `float` accumulators updated at sites the milestones already touch — `dashIFrameNegatedHits`/`dashesWasted` where `HealthApplyDamageSystem` negates against `DashState`'s stored tick window; `chargerWhiff*` in `EnemyAISystem` at the lunge wall-stop/overshoot; `damageDealt` per `GhostOwner` in `ProjectileDamageSystem`/the cleave path; `cleaveTargetsPerSwing`/`comboChains` (MC-4); `localHitStopFrames` (MC-3); the `EnemyStatus`-amp bonus + `reviveChannelsStarted`/`elitePriorityKills` (MC-5); `slotFires[0..3]` (MC-6). Surfaced to the overlay via owner-send `[GhostField] uint`s on the predicted player (cross-player comparison reads both owners off the player query) **or** a periodic `DebugTelemetryReport` RPC (no ghost-hash change). All increments are at near-zero new surface — the sites already exist.
### Tuning-knob surface
> Consolidated table of every live-tunable value, so the operator tunes **defaults-first, live**. `Tuning.cs` consts are compile-time → Burst-inlined → **baked** (a recompile per tweak, Burst-affecting if inside a Bursted system). Feel-critical values are promoted to a **server singleton** (read at runtime → live-tunable in Play via the MC-0 `SetTuning` op; server==client holds because the singleton lives on the server). **Rule of thumb:** *baked* for structural/rarely-tuned values (cooldown sentinels, SourceIds, catch-up bounds, max-slot caps, per-prefab stats); *singleton* for values you tune by ear during a playtest (dash distance/i-frames/recovery, knockback, windups, hit-stop, telegraph lead, depletion rates).
| Knob | Milestone | Path | Baked / Singleton | Default | Notes |
|---|---|---|---|---|---|
| Dash distance (units) | MC-1 | A | **Singleton** | 4.0 | feel-critical |
| Dash i-frame window (ticks) | MC-1 | A | **Singleton** | 12 (~0.2 s) | whole-window; the `SourceTick`-window negation reads `[StartTick, IFrameUntilTick]` |
| Dash recovery tail (ticks) | MC-1 | A | **Singleton** | 9 (~0.15 s) | the punish-the-spam knob — most-tuned in the track |
| Dash cooldown (ticks) | MC-1 | A | **Singleton** | 45 (~0.75 s) | route via `TickUtil.NonZero` |
| Dash movement sharpness | MC-1 | A | Baked const | ~200 (near-instant) | the `GroundedMovementSharpness` override fix |
| Charger telegraph lead (ticks) | MC-1 | A | **Singleton** | 30 (~0.5 s) | ≥ interp-delay + reaction; THE readability knob |
| Charger lunge speed (units/s) | MC-1 | A | **Singleton** | 16 | committed fixed-dir |
| Charger whiff-stagger (ticks) | MC-1 | A | **Singleton** | 36 (~0.6 s) | extends `EnemyAttackCooldown` |
| Husk attack windup | MC-1 | A | **Singleton** (promote `Tuning.AttackWindupTicks`) | 28 | currently baked const 18 |
| Melee-cone half-angle / range / dmg | MC-4 | A | **Singleton** | 45° / 3.0 / 1.6× | reuse `AutoTarget` cone math |
| Cleave cooldown (ticks) | MC-4 | A | **Singleton** | 36 | tuned RELATIVE to the dash recovery tail |
| Structure `MaxHealth` per type | EB-1 | A | Baked (`StructureCatalogEntry`) | Turret 200 / machine 120 / wall 400 | re-bakes structure ghosts |
| Siege structure-vs-player aggro | EB-1 | A | **Singleton** | nearest-structure-then-player | the fortress-vs-bait fork |
| Turret ammo cap / reload | EB-2 | A | Baked | 30 / 10 | per turret type |
| Munition cost per shot | EB-2 | A | **Singleton** | 1 | the spend rate |
| Fabricator munition recipe | EB-2 | A | Baked | 2 Aether → 1 Charge | data-only recipe |
| `CoreIntegrity.Max` / regen | END-1 | A | Baked | 100 / full-over-one-Calm | the base lose-bar |
| Lose-severity mode | END-1 | A | Baked toggle | soft (co-op-forgiving) | hard-rollback vs. soft-drain |
| `GoalProgress.Target` | END-2 | A | Baked | 10 | the run-length knob; clamp the cap |
| Final-siege size multiplier | END-2 | A | **Singleton** | 23× | the climax escalation |
| Spitter projectile speed | MC-2 | B | **Singleton** | 9 (cap) | "dodgeable under ~100 ms" is a Play-gate |
| Spitter puddle radius / lifetime | MC-2 | B | **Singleton** | 2.5 / 90t | telegraphed puddle preferred |
| Wave weighted-band table | MC-2 | B | Baked (data asset) | per-band weights | deterministic; must not perturb `SpawnCounter` ring-advance |
| Wave spike/lull intensity | MC-2 | B | **Singleton** | 1.5× / 0.4× | the pacing knob |
| Swarmer count / windup / speed | MC-2 | B | Baked (per-prefab) | 8 / ~3t / fast | authored stats |
| Hit-stop frames (hit/kill) | MC-3 | B | **Singleton** | 3 / 6 | local to the killer; never `Time.timeScale` |
| Knockback speed / duration | MC-3 | B | **Singleton** (promote `Tuning.Knockback*`) | 8.0 / 8t | DR-028 calls for this promotion |
| Downed bleed-out (ticks) | MC-5 | B | **Singleton** | 600 (~10 s) | the `DownedUntilTick` deadline |
| Revive channel / radius | MC-5 | B | **Singleton** | 120 (~2 s) / 2.5 | exposure-during-channel is the tension |
| Elite focus-fire aura | MC-5 | B | **Singleton** | +25% pack buff | killing it weakens the pack |
| `EnemyStatus` synergy amp | MC-5 | B | **Singleton** | +30% | the duo-synergy knob |
| Friendly-fire mode | MC-5 | B | Baked toggle | OFF (soft-only) | raw-HP FF gated behind this |
| Per-slot fire/cooldown | MC-6 | B | Baked (per-ability) | per-archetype | authored |
| Repair cost curve | EB-3 | B | **Singleton** | 1 Ore / 5 HP | must sit in the interesting middle |
| Per-tier harvest yield mult | EB-4 | B | Baked (tier curve) | T1 ×1 / T2 ×1.75 / T3 ×3 | felt downstream in the fight |
| `SizePerPlayer` multiplier | END-5 | B | Baked | sub-linear (exp <1.0) | sample-once at siege-arm |
| NG+ Target raise / per-tier step | END-5 | B | Baked | +50% / +15% | persistent base carries over |
**Workflow:** ship every singleton with the default above (defaults-first — autonomous polish, consult only on real forks). During a fun-gate the operator nudges singletons live via the overlay's `SetTuning` op, watches the instrumentation, lands a value, and Claude bakes the landed default back into the authoring component afterward. Baked consts change only between sessions (a Burst-affecting one demands a focused editor).
### Solo + Claude cadence
Two lanes run in **parallel**. **Systems lane (Claude-led, mostly headless):** new `IComponentData`/`ISystem`, RPC wire types, the `SourceTick`-negation fix, swept hit-detection + tunnelling regression tests, the wave band-table integer math, the MC-0 instrumentation, all EB/END server-only systems, EditMode tests — via the MCP script-edit path (`apply_text_edits`/`create_script`, one edit per call), `refresh_unity scope=scripts`, EditMode runs. Advances overnight on an **unfocused** editor *as long as it avoids Burst-affecting edits to already-Bursted systems*. **Art/content lane (operator-led, focused editor):** enemy prefab variants via `EnemyRigTools` (GUID-preserving), telegraph/dash VFX/SFX in the `CombatFeedbackSystem` scaffold, hit-flash shadergraph confirmation, biome dressing, ability/archetype blob authoring.
**HARD-requires the operator + a FOCUSED editor:** any Burst-affecting edit to an already-Bursted system — the canonical case is the MC-1 `CharacterProcessor` sharpness/`RelativeVelocity` write (and the MC-6 per-slot recompute). Editing a Bursted ISystem's query set unfocused can leave a STALE binary → a runtime `InvalidOperationException` from an unrelated `GetSingleton` (Burst reports the OLD line); a Burst ICE corrupts the cache ("not a known Burst entry point" + slow managed fallback) — **fix is an editor restart**, not a domain reload. Cluster Burst-affecting work into focused blocks, or run Burst OFF for the session and re-enable to validate. Also focused: Play-validation of server==client for every netcode slice (EditMode does NOT catch a system-ordering cycle — it throws only at world creation/Play; the EB-1 cross-group production-ordering is exactly this class), and all asset/scene mutation.
**Requires a friend — and this is an EXTERNAL scheduling dependency ON the Path A critical path, not just a Path B nicety.** Three points where a second human is **non-negotiable** (co-op is a locked pillar): **Demo A** (the two-human "is two-player combat fun and readable" read), **EB-2 at Demo B** (the feed-vs-fight specialization gate — **ON Path A**), and **MC-5 at Demo C** (contested revive / focus-fire — Path B). MPPM virtual players prove replication and let the operator solo-dry-run both clients, but the *fun* sign-off for interdependence needs a second human. **Because EB-2's gate is on Path A, the spine can BLOCK on a friend's calendar, not just on code** — pre-schedule the friend session rather than discovering the dependency at the gate. Schedule friend sessions as **batched demo checkpoints**, not per-commit.
**Rhythm:** Claude runs the systems lane + instrumentation to a playable state unfocused → operator takes a **focused** session for the Burst-affecting edits, Play-validation, and the solo dry-run with live tuning → batched **friend** session for the co-op gates at the demos. Keep Burst-affecting work clustered so the unfocused/headless lane never trips the stale-binary hazard.
### Risk register
| # | Risk | Severity | Mitigation | De-risk sequence |
|---|---|---|---|---|
| R1 | **Feel can't be hit** — the dash/whiff loop reads in spec but isn't fun after tuning (the irreducible depth-first risk). | HIGH | MC-1 is *deliberately* the smallest fun slice; the fun-gate + MC-0 instrumentation force an honest verdict. If MC-1's gate fails after a real tuning pass, STOP and re-cut combat — don't build on an unfun core. | First milestone; the kill-switch for the whole project. |
| R2 | **Dash i-frame mispredict / cross-group tick mis-align** — negating by "is DashState active now" instead of against the authored tick window double-counts on rollback, AND the strike is appended a tick earlier in a DIFFERENT group than the drainer. | HIGH | Baked build-note: `DashState` carries `StartTick`; stamp non-replicated `uint SourceTick` on `DamageEvent` at all THREE sites; negate over `[StartTick, IFrameUntilTick]` via `NetworkTick`. **Review agenda item #1.** | MC-1, before `create_script`. |
| R3 | **Burst stale-binary / ICE** from the `CharacterProcessor` sharpness edit + the MC-6 per-slot recompute corrupting the cache. | MED | Focused-editor rule; expect a restart after the processor edit; cluster Burst-affecting edits; keep the per-slot recompute non-Burst if it touches cross-assembly generics/enums. | MC-1 (processor) and MC-6 (recompute). |
| R4 | **Spitter is the only new combat-track netcode** — interpolated enemy-projectile ghost; tunnelling under ~100 ms interp, plain-group `DeltaTime` trap, double-destroy; PLUS the `SpawnCounter` ring-advance must survive the mix-table refactor. | MED | Swept segment from stored `LastStep` (never `SystemAPI.Time.DeltaTime` in a plain-group system); at-most-once destroy; tunnelling + ring-determinism regression tests; speed-cap is a Play-gate; lighter design-review. | MC-2 (Path B). |
| R5 | **Structure Health is the only new EB netcode**`[GhostField]` on the interpolated `PlacedStructure` ghost re-bakes every structure prefab + touches every `SaveData`; the death-vs-production ordering spans the predicted/plain boundary; the loss BEAT is play-gated. | MED | Budget the re-bake; gate production on a live-`Health>0` read (not cross-group `[UpdateBefore]`); `SaveData` v3 persists structure HP; the loss-juice + target-preference tuning ARE the milestone. **EB-1 review-gated.** | EB-1. |
| R6 | **Downed/Dead derive-race + missing idiom clauses** — both derive from `Health<=0`; owner mispredicts downed→dead; OR "downed never triggers" because it wasn't baked-disabled / the derive didn't `.WithPresent<Downed>()`. | MED | Replicate the discriminator (`[GhostField] uint DownedUntilTick`); bake `Downed` DISABLED; derive via `.WithPresent<Downed>()`; authoritative schedule server-side. **MC-5 review-gated.** | MC-5, before coding. |
| R7 | **MC-6 serializer churn** — generalizing to a 4-slot struct + per-slot stats risks repeated ghost-hash re-bakes + a rollback-stale change-filter. | MED | Fixed `Slot0..3` struct (ONE re-bake); per-slot recompute unconditional/uncached; ship hitscan/cone first; fill 23 slots before 4. **Mandatory review.** | MC-6, last by design. |
| R8 | **Mid-siege player-count scaling trap** — live connection count changes mid-siege; per-tick re-count jitters wave size / soft-locks. | MED | SAMPLE the count ONCE at siege-arm, server-only; bounded `SiegeTimeoutTicks`. | END-5. |
| R9 | **Breadth-creep regression** — the correctness/breadth reflex slipping a new system in before the current loop is fun re-creates the hollowness; a downstream milestone "passing on paper" against an un-tuned upstream (e.g. MC-4 on a mushy dash). | MED | The depth-before-breadth gate + the [Decision Gate](#the-decision-gate-mandatory-stop-after-end-2) hard stop; "is the existing loop tuned fun yet?"; a milestone can't pass until its dependency has PASSED, not merely shipped; Phases 24 stay PAUSED. | Every milestone boundary; the Decision Gate. |
| R10 | **Friend-availability blocks the spine** — EB-2's co-op gate is on Path A and needs a second human; a friend's calendar is outside the operator's control. | MED | Surface it as a named external dependency; pre-schedule batched friend sessions (Demo A, Demo B); MPPM for the solo dry-run so the friend session is short and decisive. | Demo A, then Demo B (Path A). |
| R11 | **Solo bandwidth / coding-vs-calendar** — fun-tuning is the hidden unbounded cost; estimates are coding-time, gated by focused-editor + friend availability. | MED | The [calendar conversion](#calendar-time-the-play-budget-assumption) (~47 months for Path A at ~58 hrs/wk); instrumentation shortens the loop; defaults-first; batched friend sessions; Path B not summed. | Continuous. |
**Mandatory adversarial design review (before coding) at:** **MC-1** (dash damage path / cross-group i-frame alignment — agenda item #1), **EB-1** (the structure `[GhostField]` + the cross-group production-ordering race), **MC-2** (the new enemy-projectile ghost + the `SpawnCounter` refactor — lighter trigger), **MC-5** (downed root + revive), **MC-6** (full-slot loadout). This matches the project rule — run the netcode/relevancy · determinism/prediction · reuse/scope review BEFORE a netcode-heavy slice; it has pre-caught relevancy traps, singleton collisions, dt-traps, and double-destroys before.
### The co-op braid: the latent inventory gap stops being latent
DR-026 deliberately shipped a co-op GAP — each player's haul is PRIVATE (personal `InventorySlot` bags) with no shared-deposit affordance. **The Economy-braid track is that pass, by necessity:** a war economy that funds SHARED turrets, a SHARED base, and SHARED repairs from PRIVATE bags is incoherent with 2+ players. The discipline across EB: **every economy SPEND reads the shared `ResourceLedger` (the untagged global ghost), never personal bags** — turret feed (EB-2), repair (EB-3), crafting-to-shared-store (EB-5). Personal bags stay the gather→carry→deposit buffer; the `G`-key `InventoryDepositRequest` is the bridge from private haul to shared war economy. The shared ledger IS the co-op shared economy, and the deposit RPC is the shared-deposit affordance — which is why feed-vs-fight specialization (the most reliable co-op fun) is the reason combat-not-automation is the primary verb.
### The escalation seam (EB → Endgame handoff)
The braid creates the mechanical seam to the endgame, already half-baked into the code: **`ThreatConfig.SizePerExpeditionResource`** (baked-but-inert today — `ThreatDirectorSystem.cs:62` is `* 0 // haul-scaling deferred`) scales siege size by what you HAULED, so a richer sortie provokes a bigger siege ("the sortie feeds both," [[Identity]]). **`ThreatConfig.HeatEnabled`/`HeatPerHarvest`/`HeatThreshold`** (also inert) scale threat by how much your FACTORY hoards (the They-Are-Billions tension: success raises stakes). **EB-1's anchor-as-lose-condition fork** is the seam to END-1's real lose BEAT. **EB-5's recipe-gating-by-`GoalProgress`** is the seam to a future memory-restoration unlock spine. The endgame track activates fields the economy already feeds — it does not reinvent a difficulty curve.
### Demo checkpoints
The 23 moments you can put in front of someone and they'd call it a game, not a tech demo — the solo dev's external-validation beats and the natural batched-friend sessions.
- **Demo A — "The Duel" (after MC-1 + MC-4, Path A):** the Charger that telegraphs a committed lunge, a swarm; you watch a player learn to read the tell, dash through it, and punish the whiff. **Includes a two-human co-op read** (MPPM or friend, no new systems) — the FIRST validated-fun checkpoint includes a second human per the co-op pillar. Show the overlay's negated-hits/dash + whiff-convert readout next to the play.
- **Demo B — "The Loop" (after EB-2, Path A):** the 515 min escalating arc where turrets eat the munition the factory made (EB-2) and a siege can destroy what you built (EB-1). The demo that proves "it feels like a *game*," not just "the fight is fun." **First batched friend session** (the feed-vs-fight co-op specialization is the gate; this friend session is ON the critical path).
- **Demo C — "The Co-op Crisis" (after MC-5 + END-1, Path B):** a teammate goes down mid-fight; the bleed-out clock starts; someone pushes into the swarm to revive (exposed during the channel) while the other holds the line and breaks target to focus the healer-elite — the Core bar ticking behind them. The clearest "this is a co-op game" sell. **Second batched friend session.**
- **(Deferred) Demo D — "The Loadout" (after MC-6, Path B):** two players bring complementary 4-slot kits and feel like different classes. Depth *on top of* a game that already exists; the operator can self-validate with an MPPM second client.
## Cut / not yet (anti-breadth-creep)
Deliberately deferred so the proven path stays fast; each carries an explicit **revisit-trigger**. Note that **END-3 (the Echo narrative) and END-4 (the content treadmill) are CUT here, not scheduled** — they are pure breadth wearing a low-risk-system costume: each ships a working *trigger* in ~24 days but the actual *payoff* (memory beats, the THEM reveal, VO, new brains/abilities/biomes/boss, the unlock pacing) is unbounded operator content that an empty event bus does NOT deliver. For a content-light solo project, a placeholder-subtitle event bus is **negative value** (maintenance surface, no shipped payoff). They return only when the operator actively wants to write/author and Path A is proven fun.
| Cut item | Revisit-trigger |
|---|---|
| **END-3 — the Echo / charge-milestone memory beats** (the event-bus SYSTEM is ~2 d; the WRITING is unbounded) | Path A proven fun AND the operator actively wants to write; the THEM arbitration (Wellspring / lost crew / the Echo — three live readings in [[Identity]]) is committed or deliberately kept ambiguous BEFORE the final beat. |
| **END-4 — the content treadmill / memory-restoration unlock spine** (cheap-content recipe + unlock-at-charge table) | After MC-2's mix-table + MC-4's archetype byte land (they make new brains a `switch`-case) AND "is the existing brain tuned fun yet?" is yes; the unlock spine's gear-vs-memory-progression question is decided. |
| The **corrupted-fabricator boss** | END-4's cheap-brain spine + a real climax (END-2) exist to express it into. |
| **Per-Sleeper Echo VO** (shared subtitles first, if END-3 is ever un-cut) | END-3's shared-subtitle pipeline ships AND the operator wants per-player voice. |
| Dash **charges** + dash-as-stat (flat cooldown first) | MC-1's flat-cooldown dash passes its fun-gate AND the kit (MC-6) wants a dash-build axis. |
| **Dash-strike** / **pierce-through-bodies on dash** | MC-1 + MC-4 land clean and a playtest asks for an offensive dash (pierce risks swarm-edge mispredict — needs the collision-filter reviewed). |
| A **grabber/controller** enemy (the only brain touching predicted player state) | MC-5 infra ships AND a review validates it AND the existing brains are tuned fun (a hard-lock with no answer is pure frustration). |
| A **directional-weak-point tank** | A later roster pass after END-4's cheap-brain spine is proven. |
| **Raw-HP friendly fire** (soft-only by default) | A playtest explicitly wants the tension AND the soft-FF revive-interrupt isn't enough. |
| A **full 6+ brain roster in one pass** | After 3 brains + the elite prove the dispatch + tuning loop ("is the existing brain tuned fun yet?"). |
| The full **per-machine HP/repair-cost economy + multi-tier munition crafting trees** | EB-1's one loss beat + EB-2's one depletable resource (+ EB-3's repair if chosen) are proven fun first. |
| **Inventory/equipment Phases 24 + automation recipe/throughput breadth** | Resume only braided: EB-4 (tool-gate) after EB-2 lands; EB-5 (crafting) after MC-6. Cut any Phase 24 work that doesn't feed the fight. |
**Gate before each new brain/system: "is the existing loop tuned fun yet?" — and after END-2, the [Decision Gate](#the-decision-gate-mandatory-stop-after-end-2) must be logged before any of the above is un-cut.**
## Locked decisions (Path A)
> Locked with the operator on 2026-06-09 via a **present-the-forks** exercise — gameplay-design forks are the operator's call, never auto-decided ([[DR-029_Path_A_Fork_Locks]]). These **resolve the per-milestone "Open questions" forks for Path A**; live-singleton picks stay tunable at playtest, so a lock here is a starting default, not a cage.
| Fork | Milestone | Locked call | Reversibility |
|---|---|---|---|
| Dash facing | MC-1 | **Free aim during the dash** (strafe-dodge; matches the decoupled move/aim) | input shape |
| Dash i-frames | MC-1 | **Whole-window i-frames**; the recovery tail punishes spam | live singleton |
| Charger form | MC-1 | **New Husk variant prefab** (distinct silhouette + telegraph) | baked (prefab) |
| Cleave control | MC-4 | **Its own button** (enables dash→cleave→shoot) | input binding |
| Cleave cooldown | MC-4 | **Own cooldown**, not shared with Fire | live singleton |
| Enemy aggro | EB-1 | **Husks push for the base/structures** (attacking players in the way); you defend | live singleton |
| Structure-HP persistence | EB-1 | **Persist in SaveData v3** — a wounded base stays wounded | save schema |
| Munition types | EB-2 | **One munition ("Charge")** to start | baked (catalog) |
| Ammo scope | EB-2 | **Turret ammo only** (server-only); player abilities stay free for now | structural (player-ammo deferred) |
| Run-dry consequence | EB-2 | **Soft-fail** — turrets go quiet until fed | live singleton |
| Lose-severity | END-1 | **Soft loss** — breach drains the shared ledger / damages structures, siege ends, base persists wounded | Tuning byte |
| Breach effect | END-1 | **Core bar only**; structure destruction stays EB-1's job | structural (no double-system) |
| Core persistence | END-1 | **Persist** — a dented Core carries across save/quit (v3) | save schema |
| Charge cadence | END-2 | **Both** — surviving sieges AND Aether deposited at the Engine fill the meter | structural (two server-only writers) |
| Win-resolution | END-2 | **Keep playing** — win banner, the base is yours; NG+/endless is the later END-5 | structural (minimal) |
| Final beat | END-2 | **One big escalating wave** (boss deferred) | live singleton (size) |
| `EnemyStatus` placement | MC-5 | **Confirmed in MC-5** (interdependence theme), not a standalone MC-3 hook | — |
**Where a pick departed from the safe default:** the operator chose **charge cadence = both** (the stronger braid) over siege-survived-only. END-2 therefore reads BOTH the siege-survived increment (`CyclePhaseSystem`, ships today) AND an Aether-deposited increment at the Engine — each a **server-only single writer** (no co-op double-count), summed into `GoalProgress.Charge`. This wires the win condition straight into the economy; budget the extra deposit-charge wiring in END-2. Every other fork took the recommended default.
### The fork-locking ritual — re-run before each Path B milestone ★
Path B forks (spitter form, downed/revive shape, the multi-slot kit, crafting, scaling/NG+, and the dormant END-3/END-4 content forks) are **deliberately left open.** When the [Decision Gate](#the-decision-gate-mandatory-stop-after-end-2) selects a Path B milestone, **lock its forks first via this same exercise: present each fork to the operator with a recommendation and let them decide.** Never auto-decide a gameplay-design question, and never mark a default "official" without an explicit okay — a locked default is a starting point the operator approved, not a call Claude made for them.
## Related
- [[DR-028_Combat_Primary_Verb_Depth_First]] — the direction this roadmap executes.
- [[Pillars]] — combat as the primary braided verb + depth-before-breadth.
- [[Identity]] — the braid in fiction (Aether economy ↔ siege stakes; the Echo/THEM payoff metered by charge — deferred to the Cut table until Path A is proven).
- [[DR-017_Persistent_Base_Player_Driven_Pacing]] — the player-driven siege loop + the deferred "base-integrity / unattended-siege teeth" END-1 cashes in.
- [[DR-026_Inventory_Equipment_Progression_Foundation]] · [[DR-027_Equipment_Slots_Phase1]] — the paused Phases 24 EB-4/EB-5 resume in braided form.
- [[Milestones]] — the historical record; [[Backlog]] — the loose pool (inventory/automation breadth paused).