Files
Project-M/Docs/Vault/06_Roadmap/Path_to_Fun.md
T
kronic 7b3c9cc2a5 Docs: MC-4 melee anim/VFX/archetype session log; Path_to_Fun MC-4 polish
Polish-pass session log (swing animation, live range slash-arc VFX, archetype-byte spike) + roadmap MC-4 status.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 17:45:34 -07:00

111 KiB
Raw Blame History

tags, status, updated, permalink
tags status updated permalink
roadmap
design
combat
economy
endgame
north-star
active 2026-06-08 gamevault/06-roadmap/path-to-fun

Path to Fun — the north-star roadmap

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 is the only committed scope; everything in Path B 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 (COMMITTED): the minimal critical path to fight-is-fun + braided-with-stakes + has-a-win/lose-conditionMC-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 (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 — see why below.)

The hard stop (Decision Gate): 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 — 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 passes — with a friend on the co-op milestones, and the instrumentation 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

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). 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 R1/R11). That is why every feel-critical value is a live server singleton, not a baked const (see Tuning-knob surface), and why the calendar conversion 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) 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] uints 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.)

This is 34 distinct risky slices, not one (secretly-multi): 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 R3).
  • input binding: don't bind Dash to Spacekeyboard.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. (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. (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). 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.)

This is 56 slices, not one (secretly-multi): 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 DestroyEntitys 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) 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). 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) 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): 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) 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

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) 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) 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) — 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 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 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.

  • Spitterthe 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 existsThreatDirectorSystem (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) 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 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. 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): 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 StatModifiers; 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 StatModifiers). 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 newPunchFov 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] uints 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 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 (~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 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 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.