Reconcile the roadmap to the just-committed End_Of_Month_Game_Jam_Slice
("Awakening Engine Last Stand"). 9-agent grounded code audit: 13/14 slice
systems already shipped; END-2 (final siege + latching win) is the one
blocker. Decision Gate answered early as ship-the-minimum.
- DR-035 (new): adopt the slice; END-2 charge cadence LOCKED siege-survived-only
- Backlog: NEXT reframed to the slice + SL-1..SL-7 milestones (END-2 = SL-3, critical path)
- Milestones: close ledger gap (MC-1 PASS / MC-4 / base-mining / EB-1 / EB-2 / END-1) + slice row
- Path_to_Fun: top pointer (stays long-term north-star; Path B untouched)
- Slice doc: Status & Navigation cross-links
CLAUDE.md intentionally unchanged (no budget headroom; conventions doc, uncontradicted).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
112 KiB
tags, status, updated, permalink
| tags | status | updated | permalink | ||||||
|---|---|---|---|---|---|---|---|---|---|
|
active | 2026-06-08 | gamevault/06-roadmap/path-to-fun |
Path to Fun — the north-star roadmap
[ACTIVE JUNE 13–30 → the slice] Path A's spine is built — a 2026-06-13 code audit found 13/14 systems shipped (MC-0/1/4 · EB-1/2 · END-1). The operator answered the Decision Gate early, as ship the minimum: the immediate target is the End_Of_Month_Game_Jam_Slice ("Awakening Engine Last Stand"), a compact single-arena base-defense demo due June 30. The one un-built spine mechanic — END-2 (final siege + latching win, spec'd below) — is the slice's critical path. This doc stays the north-star for the full game; the slice scopes Path A to a deliverable. Milestone breakdown: Backlog#NEXT — Awakening Engine Last Stand (End-of-Month Slice) ★ · adoption: DR-035_End_Of_Month_Slice_Adoption. (Path B is untouched and provisional; the formal ship-vs-continue note is logged after June 30.)
The plan to turn an engineering-complete foundation into a game that's fun to play. Direction locked in DR-028_Combat_Primary_Verb_Depth_First. This is the forward plan; Milestones stays the historical record, Backlog the loose pool. Living doc: the Path A contract table 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
M0–M7 + 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-condition — MC-0, MC-1, MC-4, EB-1, EB-2, END-1, END-2 only. Seven milestones. This is the scope. Its estimates, gates, and demos are real. Finishing Path A is a complete, shippable small game with a point.
- Path B — The Forever-Track (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 2–4 and automation recipe/throughput breadth resume only braided (EB-4/EB-5, Path B) — cut any Phase 2–4 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 | MED–HIGH · review-gated | ~2.5–3.5 wk | the duel; the whiff-punish loop |
| MC-4 | Offense gets a verb: archetype byte + melee cone | Combat | LOW | ~0.75–1 wk | dash-in→cleave→dash-out; MC-6 spike |
| EB-1 | Machines can die: the structure loss-state | Economy | MED · review-gated | ~1.5–2.5 wk | a base you can lose; END-1's Core hook |
| EB-2 | The felt spend: output → depletable combat resource | Economy | MED | ~1–1.5 wk | factory→defense pipe; co-op shared spend |
| END-1 | The base can be lost: a Core with integrity | Endgame | MED | ~4–6 d | a real lose condition |
| END-2 | The charge means something: final siege, win/lose | Endgame | MED | ~2–4 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 2–3 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
TuningConfiglive-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
DevTelemetryIComponentData(a flat struct ofuintcounters + a fewfloataccumulators, 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 inPresentationSystemGroup) or a periodicDebugTelemetryReportRPC server→client (avoids any ghost-hash change). Add a read-only IMGUI readout block toDebugOverlayshowing 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
#ifon the struct — the RpcCollection hash must match release/dev peers);#if UNITY_EDITOR-gate only the send/receive systems. Add aSetTuning(op, valueX1000)DebugOpso 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.5–3.5 wk · risk MEDIUM–HIGH · 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 3–4 distinct risky slices, not one (secretly-multi): a new predicted
DashSystem+ replication; a Burst-affectingCharacterProcessoredit with its own restart/validate cycle; theDamageEvent.SourceTickrefactor 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
DashSysteminPredictedSimulationSystemGroup,[UpdateAfter(PlayerControlSystem)](it overrides the unconditionally-writtenCharacterControl.MoveVelocityduring the dash window) and gated.WithAll<Simulate>().WithDisabled<Dead>()— this matches the existing two-ordered-writer pattern (PlayerDeathStateSystemzeroesMoveVelocity[UpdateBefore(PlayerControlSystem)];PlayerControlSystemsets it), so a third unordered writer would be a last-writer-wins determinism hazard. A dedicatedDashInputEvent (verbatimFireclone:[GhostField] InputEvent, reset+Set each frame inPlayerInputGatherSystem).DashState{float2 Dir; **uint StartTick;** uint IFrameUntilTick; uint RecoverUntilTick}(predicted, re-simulated from input — cloneKnockbackState's shape, not its server-only-ness) +DashCooldown{[GhostField] uint NextTick}(AbilityCooldowntwin). 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(aKnockbackState-shaped server-only field applied INSIDEEnemyAISystemas the sole position writer, reusingSweptMove); direction locks at commit so a sidestep/dash whiffs it into a stagger/punish window (detect the wall-stop / overshoot, extendEnemyAttackCooldown). The whiff-punish loop IS the skill ceiling. - Readable telegraph (precondition, not polish — mostly RAMP/TUNING of an existing cue): lengthen the Charger's
AttackWindupticks to ≥ interp-delay + reaction (~0.45–0.6 s, ~28–36 ticks, not 18); ramp the existingCombatFeedbackSystemAttackWindupcue into a ground-ring/scale-up.CombatFeedbackSystemalready cues off theAttackWindup.WindUpUntilTickedge (a replicated[GhostField] uintcountdown) — 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
DashCooldownexactly as the scaffold already edge-detectsAbilityCooldownfor 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:
HealthApplyDamageSystemisServerSimulation-filtered but runs[UpdateInGroup(PredictedSimulationSystemGroup)](and is the sole drainer ofDamageEvent); the melee strike it negates is appended byEnemyAISystemin the plainSimulationSystemGroup[UpdateAfter(PredictedSimulationSystemGroup)]— a different group, drained the following tick. So "isDashStateactive 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 "contactDamageEventdrains the following tick" gotchas. The fix:DamageEventis exactly{float Amount; int SourceNetworkId}today — add a non-replicateduint SourceTickand stamp it at all THREE append sites (EnemyAISystemstrike,ProjectileDamageSystem, andTurretFireSystem— an un-stampedSourceTick=0aliases tick 0, the same "ready sentinel" hazardTickUtilguards). InHealthApplyDamageSystem, skip aDamageEventwhoseSourceTickis in[DashState.StartTick, DashState.IFrameUntilTick]inclusive, compared viaNetworkTick.IsNewerThan/TicksSince— NEVER a rawuintcompare and NEVER "is-DashState-active-now." The proposed struct withoutStartTickstructurally cannot express this test. The explicit FIRST agenda item of MC-1's mandatory review is: "at the tick the server drains the meleeDamageEvent, does the server-sideDashStatei-frame window (compared viaSourceTick) correctly cover the strike that was appended in a later group the previous tick?" (This sameSourceTickfield de-risks the Spitter, structure damage, revive-invuln, and weak-point stamps.) - prediction-reconciliation flicker (presentation note — acceptable, not a bug): because
Health.Currentis 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-damagedHealth.Currentcorrects 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/CombatFeedbackis not edge-fired on the corrected-away phantom hit. - dash-feel fix (Burst-affecting blocker):
CharacterControlhas onlyMoveVelocity;CharacterProcessor.HandleVelocityControllerpsRelativeVelocitytoward it atCharacterComponent.GroundedMovementSharpness(default 15) viaStandardGroundMove_Interpolated→ a flat dashMoveVelocityramps ("walk faster"). Fix = raiseGroundedMovementSharpnessfor the window or writecharacterBody.RelativeVelocitydirectly 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
DashtoSpace—keyboard.spaceKey.isPressedis part of the kbm-active scheme sentinel inPlayerInputGatherSystem. Use a dedicated action fed symmetrically into device-active detection. - replication shape:
DashCooldownmirrorsAbilityCooldown/RespawnInvuln—[GhostField] uintscheduled viaTickUtil.NonZero, compared withNetworkTick.IsNewerThan(never rawuint <).DashSystemgates 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 theRecoverUntilTicktail when the real lunge lands. - WHIFF-PUNISH CONVERTS: after a dodged lunge the Charger is staggered long enough for a free hit —
chargerWhiffWindowsOpenedvschargerWhiffPunishesLanded> 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") —
RelativeVelocityreaches dash speed within 1–2 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.75–1 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 liveTuningConfigknobs. Polish pass added (2026-06-10_MC4_Melee_Anim_VFX_Archetype): a Rukhanka swing animation (procedural Root-bone clip +IsAttackingdriven fromMeleeCombo) and a live cone slash-arc VFX that telegraphs the actual reach. The archetype byte SPIKE landed (thebyte Archetypedata field + theAbilityFireSystemdispatch 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) + aswitchinAbilityFireSystem(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(reuseAutoTarget.Resolve'sdotvscos(halfAngle)cone math as a collect-all selector), appendDamageEvent(SourceTick-stamped) +KnockbackState. Instant, short-range, higher per-hit. No new ghost, pure server damage.PlayerFacing.Directionis already a replicated[GhostField] float2. - Combo glue with MC-1: dash-in → cleave → dash-out should read as a single verb. Keep the
switchshape 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/WaveSystemalready 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.5–2.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 5–6 slices, not one (secretly-multi): a ghost-hash-changing
[GhostField](re-bake of EVERY structure prefab), anEnemyAISystemtargeting rewrite, aHealthApplyDamageSystemdeath-branch change, a cross-group production-ordering gate, aSaveDatav3 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-replicatedfloat MaxHealthtoPlacedStructure(or a siblingStructureHealthon the structure ghost) — the only net-new replicated state in the EB track. BakeMaxHealthper-type fromStructureCatalogEntry(additive column). Add anEnemyTarget/Damageabletag soEnemyAISystemcan pick structures as targets; extend its nearest-target snapshot to include structures under a tunable aggro rule, reusing the existingAppendToBuffer(DamageEvent)site verbatim (SourceTick-stamped, per MC-1). ExtendHealthApplyDamageSystem's death branch (it alreadyDestroyEntitysEnemyTag/TrainingDummyTagat HP≤0) to the structure tag — occupancy auto-frees becauseBuildPlaceSystemderives 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:
PlacedStructureis an ownerless interpolated ghost —Healthas a[GhostField]replicates server→all-clients with NOOwnerSendType/GhostOwner(server mutations just propagate, exactly likeStorageEntryon the ledger).PlacedStructure.Typeis currently the only[GhostField]on the structure ghost, so addingHealthre-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 inHarvester/Conveyor/Fabricatorsystems (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>0read rather than relying on cross-group[UpdateBefore], and Play-validate the ordering. - Death is structural — batch through the existing
HealthApplyDamageSystemECB, 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-tickDamageEventdrain is fine. SaveDatav3 must persist structureRemainingHealthalongside the existingRemainingTickscooldown, orBaseRestoreSystembrings bases back at full HP. Assert-then-verify the liveSaveData.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_codediff). - Tuning knobs:
StructureCatalogEntry.MaxHealthper 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
SaveDatav3 or boot-full each session? - Claude: the
StructureHealthcomponent +[GhostField], theEnemyAISystemtarget-extension, theHealthApplyDamageSystemdeath-branch edit, the production live-Health gate, theSaveDatav3 field, EditMode coverage (structure takes damage → dies → cell frees; save round-trips HP); validate server==client structure-destroy + the cross-group ordering viaexecute_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/WaveSystemsingle-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 ~1–1.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
Ammocount fed from the sharedResourceLedger;TurretFireSystemdecrements per shot and refuses to fire empty. A server-only reload (fold intoTurretFireSystem) that pulls munition from the shared ledger when below capacity — so the Fabricator's output literally becomes turret uptime (reuseStorageMath.Withdraw+GetSingletonEntity<ResourceLedger>). A new munitionItemId(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 + aCombatFeedbackSystemcue shows a starved turret. HUD readout (HudSystemobserve-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 personalInventorySlotbags — co-op coherence (DR-026's latent gap; see The co-op braid). Munition production reusesFabricatorProductionSystemunchanged — author a Fabricator recipe whoseOutResourceIdis 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 existingushort ItemIdspace (DR-026, ids >3) — no wire change. StampSourceTickon theTurretFireSystemDamageEvent(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
Ammocount +TurretFireSystemdecrement/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 viaexecute_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 5–15 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 ~4–6 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 carryingCycleState/GoalProgress/the ledger — no new ghost, no relevancy work); bakedMaxviaCycleDirectorAuthoring, born-correct viaCycleDirectorSpawnSystem(mirror thePendingSave/GoalProgressstaging). A server-onlyCoreDamageSystemin the plain group[UpdateAfter(PredictedSimulationSystemGroup)]— a Husk reaching the Core radius (reuseBaseGridMath.PlotCenter+ theEnemyAIMathin-range check) drains integrity and despawns. ACoreRestoreSystem— in Calm the Core regenerates toward Max (a chipped-but-survived Core is a setback you recover from). Lose-edge:CoreIntegrity.Current<=0during Siege sets a replicatedRunOutcome{byte=Overrun}; the host-only persistence layer resolves the loss (see the lose-severity fork). HUD: a client-only base-integrity bar (mirror theGoalProgresshex-pip/bar path). - Build notes:
CoreIntegrityrides the EXISTING untagged ghost — do NOT region-tag it (SetIsIrrelevantwould 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.RunOutcomeis a BYTE not an enum; the rollback (if chosen) is HOST-ONLY (persistence is host-authoritative). Any cooldown sentinel routes throughTickUtil.NonZero. Sample the lose-edge once (guard re-firing while at 0 via aRunOutcome-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
CoreIntegritypersist inSaveDatav3 (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 ~2–4 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
GoalReachedSystemthat, on theCharge>=Targetedge (currently UNHANDLED —CyclePhaseSystemincrements past it forever with no clamp), arms a FINAL siege via the existingThreatState.PendingSiegeSizeentry point (bigger size + a distinct telegraph) and sets a replicatedRunPhase{byte=FinalDefense}. The WIN-edge — surviving the final siege duringRunPhase.FinalDefensesetsRunOutcome{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 inHudSystem(observeRunOutcome; 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 (
CyclePhaseSystemstays the soleWaveStatewriter, DR-017's atomic Calm→Siege seed).RunPhase/RunOutcomeare BYTES, single-writer, server-decided. The banner is client-only observe-only. Guard theGoalReachededge so it arms EXACTLY ONCE (aRunPhase!=Normalguard) — and clamp the currently-uncappedCharge. 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 (
liveEnemyCountpeak measurably exceeds a normal siege). - Tuning knobs:
GoalProgress.Target(baked, currently 10 — the run-length knob); final-siege size + escalation multiplier (server singleton, ~2–3× 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
GoalReachedarming +Chargeclamp, 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):
- Ship/share the minimum and stop here, or
- 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 | ~1–1.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.5–2 wk | death-as-crisis; interdependence |
| MC-6 | The full kit: multi-slot loadout | Combat | HIGH · review-gated | ~5–6 wk | complementary builds |
| EB-3 | Base repair: spend to recover the loss | Economy | LOW | ~0.5–0.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.5–2 wk | harvest→craft→equip→fight closes |
| END-5 | 1–4p difficulty scaling + endless/NG+ | Endgame | MED | ~3–4 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 ~1–1.5 wk (indicative) · risk MEDIUM · review-gated
Goal: add the reposition question (Spitter) and the surround question (Swarmer), driven by a weighted enemy-MIX band table layered on the EXISTING siege scheduler.
- Spitter — the only genuinely-new netcode in the whole combat track: a server-spawned interpolated enemy-projectile ghost moved by a NEW plain-group
EnemyProjectileMoveSystem(stores its ownLastStep; rebuild the swept segment fromcur - dir*LastStep, neverSystemAPI.Time.DeltaTimein a plain-group system) + sweptEnemyProjectileDamageSystem(SourceTick-stamped, at-most-onceecb.DestroyEntity, tunnelling regression test). Prefer a telegraphed ground-puddle (L4D2 Spitter) over a fast bolt; cap speed. - Swarmer: brain-0 tuned tiny/fast/near-zero-windup, spawned in count, so the dash also answers "don't get surrounded" (and MC-4's cleave answers it better).
- Mix-director (the genuinely-NEW work — corrected): the siege scheduler already exists —
ThreatDirectorSystem(arms sieges,PendingSiegeSize,SiegeTimeoutTicks, post-expedition retaliation) +CyclePhaseSystem(Calm↔Siege) +WaveSystem(Lull/Spawning cadence). Do not re-derive pacing. The new work is replacingWaveSystem's blind round-robin with a deterministic weighted band table (which brain spawns at which point in a siege). CRITICAL determinism note:WaveSystemoverloadsSpawnCounteras 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 alterSpawnCounter's advance, OR add a separate placement counter; cover it with the determinism test. REUSEThreatConfig/ThreatState. - A within-fight power beat: a mid-fight
UpgradePickup/StatModifierspike 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 (3–7 frames, local to the killer, gated on
DamageEvent.SourceNetworkId/ the local-player edge —_localPlayeris already resolved in the system) — genuinely new:CombatFeedbackSystemdoes FOV-punch (PrototypeCameraRig.PunchFov) + per-magnitude shake + kill FOV punch today, not a held freeze. NeverTime.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 (
AnimatedLitShaderemission via per-instanceMaterialPropertyBlock) — 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 byProjectileDamageSystem/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.5–2 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 provenDead-from-Healthidiom). The full idiom has THREE clauses — carry all three, not just theHealth<=0derive: (1) deriveDownedfromHealth<=0; (2) bakeDownedDISABLED (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:PlayerDeathStateSystemuses exactly.WithPresent<Dead>()on the baked-DISABLEDDead). Rooted + fire-disabled but still present. Derive-race fix:DownedandDeadboth derive fromHealth<=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
ReviveRequestIRpcCommand(scalar payload = downed ghostId,BuildPlaceRequesttemplate), 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 existingPlayerRespawnSystemgive-up path. - Focus-fire priority elite (healer/buffer aura — DRG Warden / RoR2 Mending; killing it weakens the pack), flagged with a replicated byte.
EnemyStatusco-op-amp (lives here, not MC-3): a server-only byte (NOT a[GhostField], NOT an enum) stamped at the existingKnockbackStatesite inProjectileDamageSystem, read inHealthApplyDamageSystem's sum loop to amplify summed damage so support+burst out-performs two soloists.- Friendly-fire fix: do not add server-only
KnockbackStateto a predicted player (it fights prediction → rubber-band). Soft-FF = aStatModifierdebuff / 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 ~5–6 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 5–6 weeks, not 3–4 (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 fixedSlot0..3struct (4 byte ids + 4[GhostField]NextFireTick— a fixed struct over aDynamicBufferto keep serializer churn to ONE re-bake) + 4Fire-style InputEvents. - The real lift — per-slot
EffectiveAbilityStats:StatRecomputeSystemfolds the character-wideStatModifierbuffer 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
ProjectileClassificationSystemto 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 2–3 slots before 4. - Reuse the MC-4 archetype byte
switchas 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.5–0.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
RepairRequestIRpcCommand(BuildPlaceRequesttemplate, scalar payload = target cell/ghost-id) → a server-onlyRepairSystem(plain group,AbilityUpgradeSystemowner-map idiom) that spends shared-ledger resources to restore a damaged structure'sHealthtowardMaxHealth. Reuse the in-placeStorageMath.Withdraw+ affordability check fromBuildPlaceSystem; 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 (additiveStructureTypebyte) that slowly heals adjacent structures from the ledger. Wire a repair mode into the existing build palette (reusesBuildPreviewMath+ 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 sharedResourceLedger. The Mender (if shipped) is a production-class system[UpdateAfter(PredictedSimulationSystemGroup)]mirroringHarvesterProductionSystem. 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
RepairRequestRPC +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/RequiredToolTierbaked onResourceNode;ResourceHarvestSystemgates + 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 optionalComponentLookup<GhostOwner>+ tier read already inResourceHarvestSystem; the yield-scale is a pureInventoryMath/HarvestMathmultiplier (no new replicated state — Tool is already anEquipmentSlot[GhostField] ItemId). A small set: 2–3 tool tiers inItemDatabase. - 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 theEquipmentSlotTool ItemId →ItemDatabasebyte 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
RequiredToolTierper 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
RequiredToolauthoring, 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); 2–3 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.5–2 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 siblingCraftSystem) to produce ITEMS (weapons, gear, tool tiers, munition batches); the recipe is data (ItemDatabase+ a recipe row), no new replication. ACraftRequestIRpcCommandfor 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 (theAbilityRefa weapon grants viaEquipSystem, DR-027) + munition batches (EB-2) all come from the same Fabricator economy. Land the additiveSaveDatav3 DR-027 flagged (restore equipment+inventory atomically AND replay equip — a plain buffer restore wouldn't re-add theStatModifiers; 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 anItemId, deposited viaInventoryMath.Deposit; server-only, plain group. BumpSaveData.CurrentVersion(Load nulls on mismatch → clean degrade to New Game).CraftRequestis a one-off shared-state RPC → reliable, server-only, applied once. Keep recipe content MINIMAL — 2–3 weapons, 2–3 tool tiers, 1 munition batch. A crafted weapon setsAbilityRef.Idvia the EXISTINGEquipSystempath — no new combat code; it's an equippableItemDatabaserow with aGrantedAbilityId. - 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
CraftRequestRPC + server system, theSaveDatav3 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-addsStatModifiers). 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 1–4 co-op players + endless/NG+ ~3–4 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
ThreatDirectorSystemSAMPLES the live connection count ONCE and scalesPendingSiegeSize(the existing single entry point) by a baked per-player multiplier (the inertThreatConfig.SizePerExpeditionResource * 0line atThreatDirectorSystem.cs:62is the template — add aSizePerPlayer; 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 boundedSiegeTimeoutTicksprevents a soft-lock. NG+/endless (END-2's victory flip made concrete) — on victory raiseGoalProgress.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:
SizePerPlayermultiplier (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 ~5–8 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 ~7–10 weeks of coding is a ~4–7 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 2–3 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 2–3×, and the optimism is concentrated exactly where the netcode/Burst risk is highest. So:
- MC-1 (~2.5–3.5 wk): new predicted
DashSystem+ replication · the Burst-affectingCharacterProcessoredit (own restart/validate) · theDamageEvent.SourceTickrefactor across THREE stamp sites + the tick-windowed negation · the Charger brain (lunge/stagger/whiff) + telegraph tuning + dash juice. - EB-1 (~1.5–2.5 wk): a ghost-hash-changing
[GhostField](re-bake every structure prefab) · anEnemyAISystemtargeting rewrite · aHealthApplyDamageSystemdeath-branch change · a cross-group production-ordering gate · aSaveDatav3 migration · the loss-juice the milestone says IS the milestone. - MC-6 (~5–6 wk): 4-slot serializer generalization · per-slot
StatRecompute· classifier ghost-type-SET · 4-input wiring · HUD · the mandatory review + rollback-correctness tuning.
Boundary judgment: re-cut MC-1, default-order MC-4 early
MC-1 is the smallest fun slice MINUS the Swarmer. The dash + Charger + telegraph is a true bait-and-punish duel; the Swarmer answers a different question (surround/AoE) the MC-1 kit cannot answer (you can't punish a swarm by dashing through one enemy). It moves to MC-2 where "mix the questions" is the stated goal and MC-4's cleave gives it a real answer — sharpening MC-1's falsifiable claim to exactly "timed-dash beats spam-dash vs a readable lunge." MC-4 is the DEFAULT second Path A milestone — verified-low-risk (PlayerFacing replicated, AutoTarget cone math reusable, pure server damage, no new ghost) and it kills the second-most-felt hollowness; giving the player dash-in/cleave/dash-out early makes the harder fights expressible. MC-3 is kept pure juice (Path B) so its gate stays clean; the EnemyStatus co-op-amplification lives in MC-5 (where interdependence is the theme). A juice milestone whose gate must also prove a co-op damage synergy is two milestones in one coat.
Where the Economy-braid + Endgame slot
Do not run the economy/endgame strictly after the full combat kit — that is the breadth-first trap DR-028 names. But do not front-load the full economy either. The shape is a thin braid thread pulled into Path A, full breadth deferred to Path B: EB-1 (machine loss-state) + EB-2 (the felt spend) are in Path A, right after MC-4, because every combat milestone before stakes is a fight with no consequence beyond your own (free) respawn. END-1 + END-2 close Path A with a losable base and a win/lose condition. Everything deeper — the enemy-mix director (MC-2), repair (EB-3), tool-gating/crafting (EB-4/EB-5), scaling/NG+ (END-5) — is Path B, chosen one at a time after the Decision Gate. Each EB/END beat keeps its own fun-gate; none ships until the prior loop is fun.
Verified-vs-corrected build notes (ground-truth audit)
The MC-1…MC-4 + EB build notes were re-read against the actual code, then re-audited by a second critic round. Verified correct: DamageEvent is exactly {float Amount; int SourceNetworkId}; HealthApplyDamageSystem is ServerSimulation-filtered AND [UpdateInGroup(PredictedSimulationSystemGroup)] + the sole DamageEvent drainer; EnemyAISystem appends in the plain SimulationSystemGroup [UpdateAfter(PredictedSimulationSystemGroup)] (a different group, drained the following tick); CharacterControl has only MoveVelocity and CharacterProcessor lerps RelativeVelocity at GroundedMovementSharpness=15; spaceKey.isPressed is in the kbm-active sentinel; AttackWindup.WindUpUntilTick is the [GhostField] CombatFeedbackSystem already cues off; RespawnInvuln{[GhostField] uint UntilTick} is the DownedUntilTick template; PlayerDeathStateSystem uses .WithPresent<Dead>() on a baked-DISABLED Dead; PlacedStructure.Type is the only [GhostField] (so adding Health re-hashes); WaveSystem overloads SpawnCounter as prefab-index AND ring-slot; ThreatDirectorSystem.cs:62 is literally SizePerExpeditionResource * 0 // deferred; SaveData.CurrentVersion = 2. Corrected against code: (1) the i-frame negation is server-only but still tick-determinism-critical — the strike is appended a tick earlier in a different group, so the negation MUST compare SourceTick against DashState's stored [StartTick, IFrameUntilTick] window via NetworkTick, never "is-DashState-active-now"; this is MC-1's review agenda item #1. (2) DamageEvent has THREE append sites, not two — EnemyAISystem, ProjectileDamageSystem, and TurretFireSystem.cs:95 — all must stamp SourceTick. (3) the MC-2 "wave director" is largely already built (Threat/Cycle/Wave systems) — the new work is the weighted MIX table, not pacing — and the refactor must not perturb SpawnCounter's ring-advance. (4) WaveState/ThreatState are NOT in SaveData — sieges are session-only today; persisting them is a design decision, assert-then-verify the version at code time. (5) MC-3's hit-stop is genuinely new — PunchFov is an FOV kick, not a freeze.
Fun-gate protocol
Operationalizes DR-028's "play it, with a friend, and not want to stop" into a repeatable per-milestone checklist with OBSERVABLE, FALSIFIABLE criteria. A milestone is done only when all three gates pass: (1) EditMode green, (2) server==client in a real netcode Play session, (3) this fun-gate. Gates 1 and 2 are necessary-not-sufficient — they were the only bar through M7 and that is why the game is hollow.
The session ritual (every milestone): (1) Solo dry-run first — boot Game.unity, open the dev overlay (MC-0), force the exact threat the milestone introduces, play 5–10 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.csconsts 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-0SetTuningop; 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 | 2–3× | 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 2–3 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 2–4 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 (~4–7 months for Path A at ~5–8 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 2–3 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 5–15 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 ~2–4 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 2–4 + automation recipe/throughput breadth | Resume only braided: EB-4 (tool-gate) after EB-2 lands; EB-5 (crafting) after MC-6. Cut any Phase 2–4 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.
Related
- DR-028_Combat_Primary_Verb_Depth_First — the direction this roadmap executes.
- Pillars — combat as the primary braided verb + depth-before-breadth.
- Identity — the braid in fiction (Aether economy ↔ siege stakes; the Echo/THEM payoff metered by charge — deferred to the Cut table until Path A is proven).
- DR-017_Persistent_Base_Player_Driven_Pacing — the player-driven siege loop + the deferred "base-integrity / unattended-siege teeth" END-1 cashes in.
- DR-026_Inventory_Equipment_Progression_Foundation · DR-027_Equipment_Slots_Phase1 — the paused Phases 2–4 EB-4/EB-5 resume in braided form.
- Milestones — the historical record; Backlog — the loose pool (inventory/automation breadth paused).