@@ -91,6 +91,18 @@ Industrial synth, weighty impacts, a low hostile drone for Husk proximity. **Add
| Automation as progression | The colony's surviving Aether-machines compounding output while you raid |
| Persistent base + instanced expeditions | The Engine's safe Aether-bubble (`BaseAnchor`) vs. the blighted **Wild** beyond |
## The braid (locked 2026-06-08) — combat is the verb; the colony is what you fight with and for
> [[DR-028_Combat_Primary_Verb_Depth_First]] · roadmap [[Path_to_Fun]]. No fiction changes — this names how the three pillars **reinforce** instead of sitting side by side as three separate modes (fight, OR build, OR watch automation tick), which is what made it not feel like a game.
The fusion only reads as a game when the pillars **braid into one loop**, and in fiction the braid is already here:
- **Automation makes what you fight WITH.** The colony's rebooted Aether-industry refines raw Aether into the **charges, munitions, turret feed, and upgrades** your abilities and defenses burn — the factory is your war economy (The Riftbreaker).
- **Combat threatens what automation lives IN.** Husk sieges hit the **Engine and its machines**, not just the operators — defending the base is defending the economy, with a real loss beat (They Are Billions / The Riftbreaker).
- **The Sortie feeds both.** Harvesting the Blightfield is the seed the factory needs *and* the act that provokes the siege (Deep Rock Galactic / Core Keeper).
So every fight is fought *with* what the factory made and *for* the base the factory lives in. **Combat is the primary verb; the Awakening Engine and its industry are what give the verb stakes** — the same systems already built, pointed at each other instead of at a ledger.
## Locked narrative choices (2026-06-03)
- **The Echo is ambiguous** — never fully trusted; "by any means"; needs a payoff beat at the goal.
2.**Co-op base power fantasy** — 2–4 friends build and grow a shared home base (V Rising feel).
3.**Automation as progression** — production runs itself so play time compounds; the loop rewards setup, not grind.
4.**Server-authoritative & deterministic** — input-only clients, client prediction; the simulation is the source of truth.
> **Combat is the primary verb; base + automation braid around it** (locked 2026-06-08, [[DR-028_Combat_Primary_Verb_Depth_First]]). The fusion of all three is the identity — but as ONE braided loop with a single primary verb, never three co-equal, independently-deep modes. Roadmap: [[Path_to_Fun]].
1.**Action-ARPG combat — the primary verb** — twin-stick, controller-first (Hades / Risk of Rain 2 / Deep Rock Galactic feel); **skill expression over stat-checks** (dodge, read-and-react, a real ability kit). The moment-to-moment the other pillars serve.
2.**Co-op base power fantasy — braids in** — 2–4 friends defend and grow a shared home base (V Rising / The Riftbreaker feel); the base is what you fight *for* and the siege's stakes.
3.**Automation as progression — braids in** — self-running production that makes what you fight *with* (charges / munitions / turrets / upgrades) and that sieges threaten; rewards setup, not grind (The Riftbreaker / Core Keeper).
4.**Server-authoritative & deterministic** — input-only clients, client prediction; the simulation is the source of truth. Co-op is non-negotiable, so every mechanic stays server-authoritative.
- **Multiplayer (locked):** small co-op **2–4, client-hosted listen-server** (BinaryWorlds + in-proc IPC; not the experimental SingleWorld host mode), PvE.
- **World (locked):** persistent buildable **home base + instanced/procedural expeditions**.
- **Automation (locked):** **progression accelerator** — self-running production chains; data model designed to grow toward full logistics.
- **Combat-primary + depth-before-breadth (locked 2026-06-08):** combat is the primary braided verb; **no new system until one braided loop is genuinely fun**, and every milestone ends with a play/fun-gate (not just green tests). [[DR-028_Combat_Primary_Verb_Depth_First]] · roadmap [[Path_to_Fun]].
## NEXT — Combat-depth track (the fight, made fun)
**The forward plan is [[Path_to_Fun]]** (direction [[DR-028_Combat_Primary_Verb_Depth_First]], refined 2026-06-08): combat is the primary braided verb; **depth-before-breadth**, with a **falsifiable fun-gate per milestone**. The roadmap is split into a **committed Path A** and a **provisional, NOT-scheduled Path B**, with a **mandatory logged Decision Gate** between them.
- **Path A — the committed scope (a shippable game-with-a-point):** MC-0 instrument the box → MC-1 dash + committed-punishable Charger → MC-4 melee cone → *[Demo A: the Duel]* → EB-1 machines can die (structure loss-state) → EB-2 the felt spend (turret ammo from the factory) → *[Demo B: the Loop]* → END-1 a losable Core → END-2 final siege / win-lose → **[Decision Gate — log ship-vs-continue]**.
- **Status 2026-06-09: MC-0 + MC-1 CODE-COMPLETE, fun-gate pending** ([[2026-06-09_MC1_Implementation]]) — 259/259 EditMode + clean netcode Play sessions + post-build adversarial review (2 confirmed findings fixed); the operator feel pass (dash snap test, telegraph read, Charger feet) + the bench (timed vs spam) + friend read are the open gate. MC-4 starts only after the MC-1 gate passes.
- **Path B — provisional (pick ONE after the gate, re-estimate first):** MC-2 ranged + swarm + mix-director · MC-3 pure juice · MC-5 downed/revive · MC-6 multi-slot kit · EB-3 base repair · EB-4 tool-gated harvest (braided) · EB-5 craft combat power · END-5 1–4p scaling + NG+. *(END-3 Echo narrative + END-4 content-treadmill are CUT until Path A is fun and the operator wants to author.)*
Every milestone ends with a **play/fun-gate**, not a test count. **Path A forks are LOCKED** (2026-06-09 — [[DR-029_Path_A_Fork_Locks]] · [[Path_to_Fun#Locked decisions (Path A)]]); Path B forks stay open, locked via the same present-the-forks ritual at the Decision Gate.
> Paused 2026-06-08 ([[DR-028_Combat_Primary_Verb_Depth_First]]): Phases 2–4 are more breadth on systems whose payoff is combat power — resume once the fight is fun. Phases 0–1 shipped and stand.
| **— 2026-06-08 World collision + HUD scaling** | Restore world collision (lost when DR-025 made the world cosmetic) + fix HUD scaling in Play. | ✅ Done 2026-06-08 — subscene `Environment`-layer boundary ring + landmark colliders (`WorldCollisionConfig`; player blocked via the layer matrix; enemy `CollisionWorld.SphereCast`); HUD ConstantPhysicalSize→ScaleWithScreenSize. [[2026-06-08_World_Collision_HUD_Scaling]] |
| **— 2026-06-08 Inventory · Equipment (Phase 0 + 1)** | Expand combat + harvesting into per-player inventory + equipment slots that grant abilities/effects, built on the existing data-driven spine (ItemDatabase catalog, StatModifier stack, AbilityRef swap). | ✅ Done 2026-06-08 — **Phase 0**: per-player replicated `InventorySlot` buffer + ID-keyed `ItemDatabase` blob catalog + harvest reroute to the personal bag + `G` deposit-to-ledger RPC + read-only HUD panel. **Phase 1**: `EquipmentSlot` (Weapon/Armor/Trinket/Tool), weapon→`AbilityRef.Id` ability swap, gear→`StatModifier` mods (per-slot sentinels), event-driven server `EquipSystem`, click-to-equip HUD. Architecture pre-validated by 5-/4-lens adversarial review; 236/236 EditMode; Play-validated host+client. **Next: Phase 2 tool-gated harvesting** (see [[Backlog]]). [[DR-026_Inventory_Equipment_Progression_Foundation]], [[DR-027_Equipment_Slots_Phase1]] |
Promote items from [[Backlog]] here when committed.
| **— 2026-06-08 Direction: Combat-first + Path to Fun —** | Strategic pivot — combat is the PRIMARY braided verb (base + automation braid around it, not co-equal); **depth-before-breadth** + per-milestone fun-gates; inventory/equipment Phases 2–4 + automation breadth PAUSED; new **combat-depth track** designed then **refined same day** into a committed **Path A** (MC-0/1/4 · EB-1/2 · END-1/2 = a shippable game-with-a-point) + a provisional **Path B** with a mandatory **Decision Gate** between them (9-agent design + 3-critic refinement workflows). | 🧭 Direction set 2026-06-08 — [[DR-028_Combat_Primary_Verb_Depth_First]] · [[Path_to_Fun]] |
| **— 2026-06-09 MC-0 + MC-1: dash + Charger duel (code)** | Path A's first combat slice: dev-telemetry instrumentation (MC-0) + the i-frame dash vs the committed, whiff-punishable Charger (MC-1) — spec'd by the mandatory pre-code review ([[2026-06-09_MC1_Build_Spec]]). | 🟡 **Code-complete 2026-06-09 — FUN-GATE PENDING** (gate 3 of 3). `DamageEvent.SourceTick` half-open negation · predicted `DashSystem` (sharpness-override blink, no processor edit) · Charger `LungeState` commit/whiff/stagger branch · `EnemyChargerMuscle` ghost in the wave pool · all four DevTelemetry counters live to the client overlay · dash juice + i-frame hit-suppression. 259/259 EditMode; netcode Play sessions clean (live negation, lunge, telemetry pipe verified); post-build 29-agent adversarial review → 2 confirmed findings fixed in-session (rollback lower-bound on the dash override; drain-order pin). Operator: feel pass + bench + friend read. [[2026-06-09_MC1_Implementation]] |
Promote items from [[Backlog]] here when committed. **The forward plan now lives in [[Path_to_Fun]].**
> The plan to turn an engineering-complete foundation into a game that's fun to play. Direction locked in [[DR-028_Combat_Primary_Verb_Depth_First]]. This is the **forward** plan; [[Milestones]] stays the historical record, [[Backlog]] the loose pool. Living doc: the [Path A contract table](#path-a--the-proven-path-to-a-point-committed) is the only committed scope; everything in [Path B](#path-b--the-forever-track-provisional-not-scheduled) is provisional and re-derived after Path A's fun-gates pass.
## The problem this solves
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.
| **Automation** | Harvester→Conveyor→Fabricator → a ledger number nobody feels | makes the things you fight WITH (charges / munitions / turret feed / upgrades) | The Riftbreaker |
| **Combat** | stand-and-click one projectile; free respawn | the verb you spend the economy on; sieges threaten the base/machines, real loss | They Are Billions / Riftbreaker |
| **Base** | a spawn point + a build grid | what you defend and why the economy exists | V Rising / Core Keeper |
Two small-studio games prove the fusion is achievable **and** that it needs ONE primary verb: **The Riftbreaker** (combat-led — a mech defends an automated base) and **Core Keeper** (mining-led — literal conveyors + drills + boss combat, co-op). Neither makes all three co-equal.
## How to read this roadmap (the hard line) ★
The prior roadmap presented breadth as a flat, equally-weighted, fully-estimated list — which is the *exact shape* of the four-co-equal-pillars error that produced the hollow game, reborn as seventeen-co-equal-milestones. This document refuses that shape. It is split into **two physically separate sections with a hard stop between them**:
- **[Path A — The Proven Path to a Point](#path-a--the-proven-path-to-a-point-committed) (COMMITTED):** the minimal critical path to *fight-is-fun + braided-with-stakes + has-a-win/lose-condition* — **MC-0, MC-1, MC-4, EB-1, EB-2, END-1, END-2** only. Seven milestones. This is the scope. Its estimates, gates, and demos are real. Finishing Path A is a complete, shippable small game with a point.
- **[Path B — The Forever-Track](#path-b--the-forever-track-provisional-not-scheduled) (PROVISIONAL, NOT SCHEDULED):** MC-2, MC-3, MC-5, MC-6, EB-3/4/5, END-5 — depth and breadth that only earn the right to exist once Path A is proven fun. Its estimates are **indicative only and WILL be re-derived** after Path A's fun-gates pass. Do not treat it as a commitment. (END-3 narrative and END-4 content-treadmill are deferred entirely into the [Cut table](#cut--not-yet-anti-breadth-creep) — see why below.)
**The hard stop** ([Decision Gate](#the-decision-gate-mandatory-stop-after-end-2)): after END-2 ships the minimum-game-with-a-point, an **explicit logged operator decision** is required — *ship/share this minimum and stop, or commit to ONE Path B milestone* — and **no Path B milestone may begin until that decision is logged.** A solo dev with no deadline is most at risk of never shipping precisely because the forever-track always offers one more thing; this gate is the teeth of depth-before-breadth at the place it matters most.
**Estimates are coding-time, not calendar-time.** See the [calendar-time conversion](#calendar-time-the-play-budget-assumption) — the unbounded cost is *fun-tuning*, and at a realistic focused-editor budget Path A is a **multi-month** effort, not the ~8-week coding sum a reader would otherwise anchor on.
**Paused until the loop is fun** ([[DR-028_Combat_Primary_Verb_Depth_First]]): inventory/equipment Phases 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](#fun-gate-protocol) passes — with a friend on the co-op milestones, and the [instrumentation](#instrumentation-extend-the-m8-dev-tools-triad) confirming the feel claim. DR-028's literal sign-off is *"spacing/timing matters and we didn't want to stop"* — but that is a vibe until it is a **counted, falsifiable** metric, so every milestone below carries observable criteria, not "feels good." Keep the netcode/determinism rigor; just stop treating green tests as "done."
---
# PATH A — The proven path to a point (COMMITTED)
> The smallest path that earns the right to even *consider* Path B: a fight that's fun, braided to an economy you feel spending, with a base you can lose and a win/lose condition. **Execution order:** `MC-0 → MC-1 → MC-4 → [Demo A] → EB-1 → EB-2 → [Demo B] → END-1 → END-2 → [Decision Gate]`. Each milestone carries its own fun-gate; none ships until the prior loop is fun.
## Path A — the contract table (committed)
| ID | Name | Track | Risk | Coding-est | Unlocks |
|---|---|---|---|---|---|
| **MC-0** | Instrument the box (dev-overlay readback) | Combat | LOW | ~0.5 d | every fun-gate's numbers |
| **MC-1** | Fight in a Box: the dash + the question | Combat | MED–HIGH · review-gated | ~2.5–3.5 wk | the duel; the whiff-punish loop |
| **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](#secretly-multi-milestones-why-the-estimates-widened)). **Fun-tuning is the unestimated, unbounded cost** — every milestone's real schedule risk is the playtest→tune→replaytest loop, not the code (see [Risk register](#risk-register) R1/R11). That is why every feel-critical value is a live server singleton, not a baked const (see [Tuning-knob surface](#tuning-knob-surface)), and why the [calendar conversion](#calendar-time-the-play-budget-assumption) below turns these into months.*
## Path A — milestones
> Designed 2026-06-08 via a multi-agent pass (5 design lenses grounded in real games + the actual DOTS code → synthesis → 3 adversarial critics, all **go-with-changes**), then re-cut against a ground-truth code audit (see [Verified-vs-corrected build notes](#verified-vs-corrected-build-notes)) and a second critic round (netcode-feasibility · solo-scope-realism · fun/design-coherence — all go-with-changes) whose blockers + majors are folded in below.
### The thesis
Depth = a **dialogue**. Enemies ask distinct, readable questions (a committed lunge to dodge, a bolt to reposition from, a swarm to AoE); the player answers with tools that have skill and **commitment cost**. The keystone is **enemy commitment + a punishable whiff** paired with the **dash** — the dash is the *answer*, a committed lunge is the *question*; neither is fun alone (so they ship together). The repo is well-shaped for this: the predicted CharacterController, the `RespawnInvuln`/`KnockbackState`/`AttackWindup` windowed-tick idiom, the derive-don't-replicate `Dead` gate, the `StatModifier` fold, and a near-complete `CombatFeedbackSystem` juice scaffold are all already proven under prediction.
**Goal:** make every later fun-gate *measurable* before spending a friend's time. The M8 dev-tools triad (`DebugCommandRequest` + `DebugOverlay` + `DebugCommandReceiveSystem`) today only **sends** commands and never reads live values back — that gap is why the gates are unfalsifiable.
- **Scope:** add a server-only `DevTelemetry``IComponentData` (a flat struct of `uint` counters + a few `float` accumulators, **not**`[GhostField]` by default) updated at the stamp sites the later milestones already touch. Surface it to the local overlay via a handful of owner-send `[GhostField] uint`s on the predicted player (read each frame in `PresentationSystemGroup`) **or** a periodic `DebugTelemetryReport` RPC server→client (avoids any ghost-hash change). Add a read-only IMGUI readout block to `DebugOverlay` showing the live counters + derived ratios (negated-hits/dash, whiff-convert %, per-player DPS, hit-stop frames, downed/revive timers).
- **Build notes:** the dev-RPC wire type stays **unconditional** (no `#if` on the struct — the RpcCollection hash must match release/dev peers); `#if UNITY_EDITOR`-gate only the send/receive systems. Add a `SetTuning(op, valueX1000)``DebugOp` so the operator nudges live singletons (below) from the overlay without leaving Play. Pure editor-only, server-authoritative plumbing — fully Claude-headless.
- **Fun-gate:** N/A (it's the *instrument* of the gates). Done when the overlay prints a live counter that increments during play and a tuning slider changes a singleton mid-session without a recompile.
- **Claude:** all of it solo/headless. **Operator:** nothing.
- **Dependencies:** none. **Kill-risk:** none — but skipping it makes every downstream gate a debate instead of a glance.
### MC-1 — Fight in a Box: the dash + the question it answers `~2.5–3.5 wk` · risk MEDIUM–HIGH · **review-gated**
**Goal:** turn stand-and-click into a bait-and-punish **duel** — a snappy i-frame dash answering ONE **Charger**'s readable, committed, whiff-punishable lunge. This is the genuinely-smallest fun slice. *(The Swarmer moves to MC-2; it answers a different question — see [Boundary judgment](#boundary-judgment-re-cut-mc-1-default-order-mc-4-early).)*
> **This is 3–4 distinct risky slices, not one** ([secretly-multi](#secretly-multi-milestones-why-the-estimates-widened)): a new predicted `DashSystem` + replication; a Burst-affecting `CharacterProcessor` edit with its own restart/validate cycle; the `DamageEvent.SourceTick` refactor across THREE stamp sites + the negation branch; and a new Charger brain (lunge/stagger/whiff-detection) + telegraph tuning + dash juice. Each needs its own focused-editor Play-validation — hence the widened estimate.
**Scope (named systems):**
- **Dash** (Hades / Hyper Light Drifter): a NEW predicted `DashSystem` in `PredictedSimulationSystemGroup`, **`[UpdateAfter(PlayerControlSystem)]`** (it overrides the unconditionally-written `CharacterControl.MoveVelocity` during the dash window) **and gated `.WithAll<Simulate>().WithDisabled<Dead>()`** — this matches the existing two-ordered-writer pattern (`PlayerDeathStateSystem` zeroes `MoveVelocity``[UpdateBefore(PlayerControlSystem)]`; `PlayerControlSystem` sets it), so a third *unordered* writer would be a last-writer-wins determinism hazard. A dedicated `Dash` InputEvent (verbatim `Fire` clone: `[GhostField] InputEvent`, reset+Set each frame in `PlayerInputGatherSystem`). `DashState{float2 Dir; **uint StartTick;** uint IFrameUntilTick; uint RecoverUntilTick}` (predicted, **re-simulated from input** — clone `KnockbackState`'s *shape*, not its server-only-ness) + `DashCooldown{[GhostField] uint NextTick}` (`AbilityCooldown` twin). Whole-window i-frames; a short **recovery tail** (Helldivers dive) so a panic-dash is punishable, not spam.
- **Charger** (L4D2 Charger / DRG Menace) — *the keystone*: a longer-telegraph Husk variant that on commit enters a fixed-direction `LungeState` (a `KnockbackState`-shaped **server-only** field applied INSIDE `EnemyAISystem` as the sole position writer, reusing `SweptMove`); direction locks at commit so a sidestep/dash whiffs it into a **stagger/punish window** (detect the wall-stop / overshoot, extend `EnemyAttackCooldown`). The whiff-punish loop IS the skill ceiling.
- **Readable telegraph** (precondition, not polish — mostly RAMP/TUNING of an existing cue): lengthen the Charger's `AttackWindup` ticks to ≥ interp-delay + reaction (~0.45–0.6 s, ~28–36 ticks, not 18); ramp the **existing**`CombatFeedbackSystem``AttackWindup` cue into a ground-ring/scale-up. `CombatFeedbackSystem` already cues off the `AttackWindup.WindUpUntilTick` edge (a replicated `[GhostField] uint` countdown) — this is a tune, not a new cue.
- **Dash juice** (must be in MC-1, not deferred): afterimage/whoosh + directional camera nudge + i-frame shimmer, edge-detected from `DashCooldown` exactly as the scaffold already edge-detects `AbilityCooldown` for the muzzle flash.
**Build notes (honor at code time):**
- **i-frame fix — the cross-group tick-alignment blocker (review agenda item #1):** This is the HIGH-severity R2 risk, and the "server-only, no client symmetry" framing must NOT be read as "no determinism care needed." The actual subtlety, verified in code: `HealthApplyDamageSystem` is `ServerSimulation`-filtered **but runs `[UpdateInGroup(PredictedSimulationSystemGroup)]`** (and is the *sole* drainer of `DamageEvent`); the melee strike it negates is appended by `EnemyAISystem` in the **plain**`SimulationSystemGroup``[UpdateAfter(PredictedSimulationSystemGroup)]` — **a different group, drained the following tick.** So "is `DashState` active *now* at drain time" is ≥1 tick off from "was the player i-framed when the strike *landed*" — the same class as the documented "predicted-physics group is OrderFirst" and "contact `DamageEvent` drains the following tick" gotchas. The fix: `DamageEvent` is exactly `{float Amount; int SourceNetworkId}` today — add a **non-replicated `uint SourceTick`** and **stamp it at all THREE append sites** (`EnemyAISystem` strike, `ProjectileDamageSystem`, **and `TurretFireSystem`** — an un-stamped `SourceTick=0` aliases tick 0, the same "ready sentinel" hazard `TickUtil` guards). In `HealthApplyDamageSystem`, **skip a `DamageEvent` whose `SourceTick` is in `[DashState.StartTick, DashState.IFrameUntilTick]` inclusive, compared via `NetworkTick.IsNewerThan`/`TicksSince` — NEVER a raw `uint` compare and NEVER "is-DashState-active-now."** The proposed struct without `StartTick` structurally cannot express this test. **The explicit FIRST agenda item of MC-1's mandatory review is: "at the tick the server drains the melee `DamageEvent`, does the server-side `DashState` i-frame window (compared via `SourceTick`) correctly cover the strike that was appended in a later group the previous tick?"***(This same `SourceTick` field de-risks the Spitter, structure damage, revive-invuln, and weak-point stamps.)*
- **prediction-reconciliation flicker (presentation note — acceptable, not a bug):** because `Health.Current` is a `[GhostField]` and the player is predicted, a successful **server-only** i-frame dash yields a brief health-bar/prediction flicker on the owning client (the client predicts the hit; the server's authoritative non-damaged `Health.Current` corrects on the next snapshot). This is **acceptable — no desync, server-authoritative** — and must be NOTED so it isn't read as "flaky i-frames" in the very playtest that gates the track, AND so MC-3's hit-flash/`CombatFeedback` is **not** edge-fired on the corrected-away phantom hit.
- **dash-feel fix (Burst-affecting blocker):** `CharacterControl` has only `MoveVelocity`; `CharacterProcessor.HandleVelocityControl` lerps `RelativeVelocity` toward it at `CharacterComponent.GroundedMovementSharpness` (default 15) via `StandardGroundMove_Interpolated` → a flat dash `MoveVelocity`**ramps** ("walk faster"). Fix = raise `GroundedMovementSharpness` for the window **or** write `characterBody.RelativeVelocity` directly inside the processor. **This is a Burst-affecting edit to the predicted processor — do it FIRST, focused editor, Burst-off for the session, expect a restart, Play-validate before building on top** (the stale-binary / ICE hazard; [Risk register](#risk-register) R3).
- **input binding:** don't bind `Dash` to `Space` — `keyboard.spaceKey.isPressed` is part of the kbm-active scheme sentinel in `PlayerInputGatherSystem`. Use a dedicated action fed symmetrically into device-active detection.
- **replication shape:** `DashCooldown` mirrors `AbilityCooldown`/`RespawnInvuln` — `[GhostField] uint` scheduled via `TickUtil.NonZero`, compared with `NetworkTick.IsNewerThan` (never raw `uint <`). `DashSystem` gates as above, deterministic/idempotent, no wall-clock; the InputEvent (not a held bool) ensures one dash per press across the frame→tick→rollback boundary.
**Fun-gate (falsifiable):**
- **BENCH METRIC (timed vs spam):** in a fixed 10-lunge bench, a player who dashes ON the telegraph takes **≥70% fewer Charger hits** than a player who dashes on cooldown-spam; MC-0 prints both hit-counts (`dashIFrameNegatedHits`, `dashesWasted`). Spam-dash must demonstrably leave the player in the `RecoverUntilTick` tail when the real lunge lands.
- **WHIFF-PUNISH CONVERTS:** after a dodged lunge the Charger is staggered long enough for a free hit — `chargerWhiffWindowsOpened` vs `chargerWhiffPunishesLanded` > 50% on a skilled run; free-hit window ≥ one attack interval in ≥8/10 dodges.
- **READABILITY UNDER LATENCY:** at simulated ~100 ms interp delay, the tester reacts to the **tell**, not the motion — the dash starts before the Charger's position has visibly committed. If the windup must be *shortened* to feel fair, that is a fail of the band — re-tune, do not ship.
- **SNAP TEST:** the dash covers full distance in its i-frame window with no visible ramp ("blink," not "walk faster") — `RelativeVelocity` reaches dash speed within 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](#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.
**Goal:** stop offense being "auto-aim and hold." A byte-dispatched ability archetype with an instant short-range **melee cleave** that makes attacking a *positioning* decision (dash-in → cleave → dash-out). **Runs second, right after MC-1** — verified-low-risk and kills the second-most-felt hollowness; see [Boundary judgment](#boundary-judgment-re-cut-mc-1-default-order-mc-4-early). *(Also the de-risking spike for MC-6's archetype dispatch.)*
- **Archetype byte** on `AbilityDefBlob`/`EffectiveAbilityStats` (Projectile=0 keeps today's path) + a `switch` in `AbilityFireSystem` (stored as **byte** per the Burst cross-assembly enum rule; baked, no replication).
- **MeleeCone / cleave (byte=2):** server-side select all living enemies in a cone around `PlayerFacing.Direction` (**reuse `AutoTarget.Resolve`'s `dot` vs `cos(halfAngle)` cone math as a collect-all selector**), append `DamageEvent` (`SourceTick`-stamped) + `KnockbackState`. Instant, short-range, higher per-hit. No new ghost, pure server damage. `PlayerFacing.Direction` is already a replicated `[GhostField] float2`.
- **Combo glue with MC-1:** dash-in → cleave → dash-out should read as a single verb. Keep the `switch` shape generalizable to hitscan(1)/cone(2)/aoe(3) for MC-6.
- **Feel-coupling note (the trap):** the cleave's cooldown/range MUST be tuned **relative to the dash** — the dash recovery tail must not strand you mid-cleave, and the cleave range must reward the dash-in. **MC-4 cannot "pass" its gate until MC-1 has actually PASSED its gate, not merely shipped** — a combo grammar tested on a still-mushy dash is untestable, and "MC-4 passes on paper because we never validated the dash" is the R9 breadth-creep reflex wearing a combat hat.
**Fun-gate (falsifiable):**`cleaveTargetsPerSwing` averages **>1.5** during a swarm (you position to line up the cone); the dash-in/cleave/dash-out combo is chosen over the projectile when surrounded in ≥8/10 surround situations (`comboChains` counted); a blind-test watcher can tell cleave from projectile by feel/range alone.
**Tuning knobs:** cone half-angle / range / damage / knockback / cooldown — all live singletons (defaults in the [surface](#tuning-knob-surface)).
**Open questions:** cleave as a separate button (simplest, previews MC-6) vs. temporarily replacing Fire? Own cooldown (enables the dash-cleave-shoot grammar) vs. shared with Fire?
**Claude:** the archetype byte + `AbilityFireSystem` switch, the `MeleeCone` selector (lifting `AutoTarget` cone math), `DamageEvent`+`KnockbackState` append, the second-button input wiring, EditMode tests (cone selection count, byte dispatch, no self-hit). **Operator:** cone angle/range/damage feel tuned *relative to the dash*; the distinct-verb blind-test.
**Dependencies:****MC-1 (PASSED, not just shipped)** — the dash is the other half of the combo. **Kill-risk:** the cleave feels like a re-skinned auto-attack because the cone/range/cooldown let you stand still — the whole point is it PULLS you into the arena.
> ### Demo A — "The Duel" (after MC-1 + MC-4) — **first friend-playable checkpoint**
> The natural place to first satisfy DR-028's literal *"play it, **with a friend**, and not want to stop."* The Charger as the readable threat, dash + cleave + projectile, one short single siege (the existing `ThreatDirector`/`CyclePhase`/`WaveSystem` already produce one). **Include a lightweight TWO-HUMAN co-op read here** — both players dashing/cleaving the Charger and a small swarm (MPPM or a real friend, **no new systems**) — so the project's FIRST validated-fun checkpoint includes a second human, per the non-negotiable co-op pillar. A duel that's fun solo can be boring or chaotic with two; discovering that here is cheap, discovering it at Demo B (5+ milestones later) is not. **This demo's pass/fail is the green-light for the rest of Path A:** if two friends want to keep dueling, the thesis is validated; if not, re-tune MC-1's feel before spending weeks on the economy braid. *(Interdependence isn't designed yet — that's MC-5 in Path B; this read only asks "is two-player combat fun and readable," not "do they need each other.")*
### EB-1 — Machines can die: the structure loss-state `~1.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](#where-the-economy-braid--endgame-slot).)*
> **This is 5–6 slices, not one** ([secretly-multi](#secretly-multi-milestones-why-the-estimates-widened)): a ghost-hash-changing `[GhostField]` (re-bake of EVERY structure prefab), an `EnemyAISystem` targeting rewrite, a `HealthApplyDamageSystem` death-branch change, a cross-group production-ordering gate, a `SaveData` v3 migration, AND the loss-juice the milestone itself says IS the milestone — each with its own Play-validation. Hence the widened estimate.
- **Scope:** add `[GhostField] float Health` + non-replicated `float MaxHealth` to `PlacedStructure` (or a sibling `StructureHealth` on the structure ghost) — the **only net-new replicated state in the EB track**. Bake `MaxHealth` per-type from `StructureCatalogEntry` (additive column). Add an `EnemyTarget`/`Damageable` tag so `EnemyAISystem` can pick structures as targets; extend its nearest-target snapshot to include structures under a tunable aggro rule, reusing the existing `AppendToBuffer(DamageEvent)` site verbatim (`SourceTick`-stamped, per MC-1). Extend `HealthApplyDamageSystem`'s death branch (it already `DestroyEntity`s `EnemyTag`/`TrainingDummyTag` at HP≤0) to the structure tag — occupancy auto-frees because `BuildPlaceSystem` derives it from live ghosts. Client-only loss juice (flash/debris/SFX) edge-detected from the ghost prune — a pruned structure ghost = a destroyed structure (the dict-prune-is-a-kill idiom enemies use).
- **Build notes:**
-`PlacedStructure` is an ownerless **interpolated** ghost — `Health` as a `[GhostField]` replicates server→all-clients with NO `OwnerSendType`/`GhostOwner` (server mutations just propagate, exactly like `StorageEntry` on the ledger). `PlacedStructure.Type` is currently the **only**`[GhostField]` on the structure ghost, so adding `Health`**re-hashes the ghost → MANDATORY re-bake of turret + Harvester/Fabricator/Conveyor + Wall/Pylon prefabs** (budget this as the slice's structural cost).
- **Cross-group ordering (the trap EditMode can't catch):** structure death runs in `HealthApplyDamageSystem` (predicted group); production runs in `Harvester/Conveyor/Fabricator` systems (plain group `[UpdateAfter(PredictedSimulationSystemGroup)]`). "Damage-before-production so a structure killed this tick cannot also produce this tick" is an ordering that **spans the predicted/plain boundary** — exactly the silently-ignored-`UpdateBefore` + invisible-cycle hazard CLAUDE.md documents (it throws only at world-creation/Play, never in EditMode). **Gate production on a live-`Health>0` read** rather than relying on cross-group `[UpdateBefore]`, and Play-validate the ordering.
- Death is structural — batch through the existing `HealthApplyDamageSystem` ECB, at-most-once destroy per tick (a structure could take a turret hit AND a Husk hit the same tick). Structures don't predict, so a server-only next-tick `DamageEvent` drain is fine.
- **`SaveData` v3 must persist structure `RemainingHealth`** alongside the existing `RemainingTicks` cooldown, or `BaseRestoreSystem` brings bases back at full HP. **Assert-then-verify** the live `SaveData.CurrentVersion` (=2 today, `SaveData.cs:53`) and its serialized field set (`Goal`/`Ledger`/`Structures`/`StructureIo`) in code at EB-1 time before choosing the additive v3 bump.
- **Fun-gate (falsifiable — observable proxies, not inferred motive):** in a live Host+client siege a Husk the team fails to intercept visibly walks to a placed turret and **destroys it** — you watch it explode and the cell is now empty/rebuildable; **in run N+1 the team places ≥1 defensive structure at (or guarding) the cell where the breach happened in run N (observed)** — i.e. the loss demonstrably changed how they build, not just how they fight; server and all clients agree on which structures are gone (no ghost-structure desync, `execute_code` diff).
- **Open questions:** **(core feel fork — [locked](#locked-decisions-path-a))** Husks PREFER structures (They Are Billions — swarm the base) or PREFER players (DRG — hunt you) with structures as collateral? — changes whether the base is a fortress or bait; live server-singleton knob. EB-1 ships **peripheral-only**; the base-anchor-as-lose-condition is a deliberate END-1 fork. Persist structure HP in `SaveData` v3 or boot-full each session?
- **Claude:** the `StructureHealth` component + `[GhostField]`, the `EnemyAISystem` target-extension, the `HealthApplyDamageSystem` death-branch edit, the production live-Health gate, the `SaveData` v3 field, EditMode coverage (structure takes damage → dies → cell frees; save round-trips HP); validate server==client structure-destroy + the cross-group ordering via `execute_code`/Play. **Operator:** the target-preference fork (fortress vs. bait), the anchor-as-lose-condition decision, **owning the mandatory netcode review** (the `[GhostField]` re-hash + the cross-group production-ordering race), and the play-gate (does losing a machine FEEL like a loss?) watching a real siege.
- **Dependencies:** MC-4 (a fun fight is the thing the stakes amplify). **Kill-risk:** a destroyed machine reads as a silent despawn (no weight, no "oh no") — the loss is mechanically present but emotionally absent and the braid's stakes evaporate. The juice + the target-preference tuning ARE the milestone, not optional polish.
> **Note on EB-1's siege director:** EB-1 deliberately does **not** ship the MC-2 enemy-mix director (that's Path B). It runs against the *existing* `ThreatDirector`/`CyclePhase`/`WaveSystem` single-siege output. A single readable Charger-led siege is enough to prove "a siege can destroy what you built"; the weighted mix is depth layered on later.
### EB-2 — The felt spend: automation output → a depletable combat resource `~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**`Ammo` count fed from the shared `ResourceLedger`; `TurretFireSystem` decrements per shot and refuses to fire empty. A server-only reload (fold into `TurretFireSystem`) that pulls munition from the shared ledger when below capacity — so the Fabricator's output literally becomes turret uptime (reuse `StorageMath.Withdraw` + `GetSingletonEntity<ResourceLedger>`). A new munition `ItemId` (e.g. `Charge`/`AetherCell`) so automation produces something whose ONLY use is feeding the fight. Replicate ammo minimally: a **single empty/loaded enableable-or-byte** on the structure ghost (no `[GhostField]` count needed — just the empty edge) so the HUD + a `CombatFeedbackSystem` cue shows a starved turret. HUD readout (`HudSystem` observe-only): the shared munition stockpile + a per-turret empty indicator.
- **Build notes:** keep the spend **server-only** in the plain group — turret ammo is on an interpolated ghost, never predicted; mutate the server-only count, replicate only the empty EDGE. The spend MUST read the **shared `ResourceLedger`** (the untagged global ghost), NOT personal `InventorySlot` bags — co-op coherence (DR-026's latent gap; see [The co-op braid](#the-co-op-braid-the-latent-inventory-gap-stops-being-latent)). Munition production reuses `FabricatorProductionSystem`**unchanged** — author a Fabricator recipe whose `OutResourceId` is the new munition id (data-only); `ProductionMath`'s input-limited catch-up handles it. Do NOT route the munition through the *predicted* ability path yet (that's the player's own ammo — a later, higher-risk fork). Route the new id through the existing `ushort ItemId` space (DR-026, ids >3) — no wire change. Stamp `SourceTick` on the `TurretFireSystem``DamageEvent` (already required by MC-1).
- **Fun-gate (falsifiable — observable proxies, not inferred motive):** during a siege you watch turrets EAT the munition the factory made and the stockpile readout visibly drops; **in a long siege the turrets go silent (run dry) ≥1× AND a player initiates a feed/build action within ~10 s of a silent turret (counted)** — i.e. the empty state demonstrably drives a behavior; a bigger factory measurably extends how long the base holds (timed, two factory sizes); two players splitting "I build the munition line / I hold the line" clears a fixed siege faster than one doing both (co-op via the shared ledger, **needs a friend** — Demo B).
- **Tuning knobs:** turret ammo capacity + shots-per-reload (baked, cap 30 / reload 10); munition cost per shot (live singleton, 1); the Fabricator munition recipe ratio + period (baked, 2 Aether → 1 Charge); empty-turret behavior — hard-stop vs. degraded slow-fire (live singleton, default hard-stop for clearer feedback).
- **Open questions:** **(fork — [locked](#locked-decisions-path-a))** turret ammo (server-only, safe) vs. the PLAYER'S abilities consuming a charge (stronger braid but touches predicted state — a deliberate later fork the operator green-lights only if turret-ammo proves the loop). Running dry = soft fail (turrets quiet) vs. contributes to the lose condition (ties to END-1)? One munition type or per-defense types? — start with ONE.
- **Claude:** the `Ammo` count + `TurretFireSystem` decrement/refuse-empty, the ledger-fed reload, the new munition id + Fabricator recipe authoring, the empty-state bit + HUD readout, EditMode coverage (fires N then starves; reload pulls from ledger); headless-validate the factory→turret pipe via `execute_code`. **Operator:** the production-vs-depletion balance (the make-or-break feel), the turret-vs-player-ammo fork, confirming the spend READS as the factory powering the defense; **the co-op feed-vs-fight read at Demo B (friend non-negotiable).**
- **Dependencies:** EB-1 (a loss state — otherwise an under-fed base has no consequence). **Kill-risk:** the stockpile never realistically runs dry (over-produced) OR drains so fast the base is helpless — either way the player doesn't FEEL the loop, which is the entire point. The balance tuning IS the milestone.
> ### Demo B — "The Loop" (after EB-2) — **first batched friend session for the braid**
> The first **cohesive vertical slice** that shows the braid, not just a tuned fight: a 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](#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 carrying `CycleState`/`GoalProgress`/the ledger — **no new ghost, no relevancy work**); baked `Max` via `CycleDirectorAuthoring`, born-correct via `CycleDirectorSpawnSystem` (mirror the `PendingSave`/`GoalProgress` staging). A server-only `CoreDamageSystem` in the plain group `[UpdateAfter(PredictedSimulationSystemGroup)]` — a Husk reaching the Core radius (reuse `BaseGridMath.PlotCenter` + the `EnemyAIMath` in-range check) drains integrity and despawns. A `CoreRestoreSystem` — in Calm the Core regenerates toward Max (a chipped-but-survived Core is a setback you recover from). Lose-edge: `CoreIntegrity.Current<=0` during Siege sets a replicated `RunOutcome{byte=Overrun}`; the host-only persistence layer resolves the loss (see the lose-severity fork). HUD: a client-only base-integrity bar (mirror the `GoalProgress` hex-pip/bar path).
- **Build notes:** `CoreIntegrity` rides the EXISTING untagged ghost — do NOT region-tag it (`SetIsIrrelevant` would hide it cross-region; the shared-global-state rule). Core damage server-only in the plain group; a Husk reaching the Core is at-most-once-per-tick via the ECB destroyed-bitset. `RunOutcome` is a BYTE not an enum; the rollback (if chosen) is HOST-ONLY (persistence is host-authoritative). Any cooldown sentinel routes through `TickUtil.NonZero`. Sample the lose-edge once (guard re-firing while at 0 via a `RunOutcome`-already-set check).
- **Fun-gate (falsifiable):** in a live Host+client siege a Husk the team fails to intercept visibly walks to the Engine and the integrity bar ticks down — players reposition to body-block / focus it (**observed: a player abandons farming/repositions to defend the Core ≥1×**); in 3 test sieges the players can **name why they lost** (not a coin flip); a chipped-but-survived Core regenerating in Calm reads as "we got hurt but we're okay." **Crucially: re-run a Demo-B-era fun playtest WITH the Core present — if the fight is still mushy, STOP and fix the fight first.**
- **Tuning knobs:** `CoreIntegrity.Max` (baked, 100); Core-reach damage per Husk (baked/singleton, ~5 unintercepted Husks = a serious dent but not instant death); Core regen in Calm (baked, ~full recovery over one Calm); lose-severity mode hard-rollback vs. soft-drain (Tuning byte, default the operator's pick — recommend soft for co-op forgiveness).
- **Open questions:** **(lose-severity fork — the biggest, [locked](#locked-decisions-path-a))** hard rollback-to-autosave (clean, pillar-true, but a group loses minutes) vs. SOFT loss (Husks breach, drain a chunk of the shared ledger / damage structures, then the siege ends — no rollback, the base persists wounded; more co-op-forgiving, avoids save-corruption risk)? Does `CoreIntegrity` persist in `SaveData` v3 (a wounded base stays wounded) or boot full? Does a breach destroy placed structures (EB-1's job — recommend Core-bar-only here)?
- **Claude:** the component, the 3 server systems, the HUD bar, the lose-resolution wiring (rollback or soft-drain per the fork), EditMode coverage (Core drains under siege, regens in Calm, lose-edge fires once). **Operator:** the fun-gate playtest (is defending the Core engaging?), the diegetic Core/Engine placement, the lose-severity decision.
- **Dependencies:** EB-1 (the structure loss-state is the natural sibling; both make the siege threatening). **Kill-risk:** if defending the Core isn't fun, nothing downstream matters — a losable base only amplifies an already-good fight; shipping it before the fight earns it actively hurts.
### END-2 — The charge means something: the cap arms a final siege, win or lose `~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 `GoalReachedSystem` that, on the `Charge>=Target` edge (currently UNHANDLED — `CyclePhaseSystem` increments past it forever with no clamp), arms a FINAL siege via the existing `ThreatState.PendingSiegeSize` entry point (bigger size + a distinct telegraph) and sets a replicated `RunPhase{byte=FinalDefense}`. The WIN-edge — surviving the final siege during `RunPhase.FinalDefense` sets `RunOutcome{byte=Victory}`, fires the ending event (subtitle/banner first), and for the minimum simply flips into "keep playing, the base is yours" (the endless/NG+ curve is END-5, Path B). A single client-only WIN/LOSS banner in `HudSystem` (observe `RunOutcome`; reused by END-1's overrun banner). **Zero net-new writing** for the minimum (placeholder banner text), **zero new ghosts.**
- **Build notes:** arm the final siege through the EXISTING single entry point — do NOT add a parallel siege path (`CyclePhaseSystem` stays the sole `WaveState` writer, DR-017's atomic Calm→Siege seed). `RunPhase`/`RunOutcome` are BYTES, single-writer, server-decided. The banner is client-only observe-only. Guard the `GoalReached` edge so it arms EXACTLY ONCE (a `RunPhase!=Normal` guard) — and **clamp the currently-uncapped `Charge`.** If cadence moves to Aether-deposited (the fork below), keep it a server-only single writer (avoid co-op double-count).
- **Fun-gate (falsifiable — countable behavior, not vibe):** a grinding team sees the meter approach full and the Engine telegraphs the Wellspring opening; **the team treats the final siege differently from a normal one (observed: deliberate pre-siege prep — repositioning, topping up munition, a callout — before the final wave, ≥1 countable action)**; losing it stings but the continue means "try again," not "over forever"; winning produces a "we did it" moment even with placeholder text. The final siege is visibly larger/distinct (`liveEnemyCount` peak measurably exceeds a normal siege).
- **Open questions:** **(charge cadence — highest-leverage, [locked](#locked-decisions-path-a))** sieges-survived (combat-only, ships today — the only writer is `CyclePhaseSystem` +1/survived-siege) / Aether-deposited-at-the-Engine (economy braid) / both? Recommend siege-survived first (ships without an economy dependency), both long-term. **(win-resolution — [locked](#locked-decisions-path-a))** WIN = endless/NG+ (END-5) vs. a hard ending + credits + free-play? — the minimum is "keep playing"; the fork is decided at the END-2/Decision-Gate boundary. One big final wave vs. a multi-stage gauntlet + boss? — a big escalating wave for the minimum (boss is Cut).
- **Claude:** the `GoalReached` arming + `Charge` clamp, the win/loss-edge bytes, the banner, EditMode coverage (cap arms the final siege once; win-edge fires once; loss falls through to continue). **Operator:** the climax-feel fun-gate, the charge-cadence fork, the win-screen writing (or accept placeholder), the win-resolution call at the Decision Gate.
- **Dependencies:** END-1 (a losable base — otherwise "win" is hollow because you could never have lost). **Kill-risk:** the final siege is indistinguishable from a normal siege → the "win" is anticlimactic and the meter stays meaningless; it must escalate visibly.
### The Decision Gate (MANDATORY STOP after END-2) ★
END-2 completes Path A: a fight that's fun, braided to a felt economy, with a base you can lose and a real win/lose condition — **a complete, shippable small game with a point.** Before ANY Path B milestone (MC-2/MC-3/MC-5/MC-6/EB-3/EB-4/EB-5/END-5) begins, an **explicit operator decision MUST be logged** (a session note / DR):
1.**Ship/share the minimum and stop here**, or
2.**Commit to exactly ONE Path B milestone** — re-deriving its estimate from scratch against the now-known feel, and re-running this gate after it.
**No Path B milestone may start until that decision is logged.** This is the enforcement point of depth-before-breadth: the forever-track always offers one more thing, and a solo dev with no deadline is most at risk of never shipping. The gate forces the question "is the minimum game good enough to put in front of people?" before the unbounded backlog reopens. Path B's table below exists to inform that choice — not to be executed as a sprint. **And before building the chosen Path B milestone, lock its forks first via the fork-locking ritual ([Locked decisions](#locked-decisions-path-a)) — present each fork to the operator and let them decide; never auto-decide a gameplay-design question.**
---
# PATH B — The forever-track (PROVISIONAL, NOT SCHEDULED)
> **Everything below is depth and breadth that only earns the right to exist once Path A is proven fun.** Estimates are **indicative only and WILL be re-derived** after Path A's fun-gates pass and the [Decision Gate](#the-decision-gate-mandatory-stop-after-end-2) is logged — **do not treat this as a commitment or a schedule.** A solo dev with no deadline will not finish seventeen milestones; this section is a menu the operator picks ONE item from at a time, re-deciding after each. The build notes here are real (the netcode homework is done) so that *if* an item is chosen it starts on solid ground — but choosing is gated.
## Path B — indicative menu (re-derive before committing any row)
| ID | Name | Track | Risk | Indicative | Unlocks |
| **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](#cut--not-yet-anti-breadth-creep) as pure breadth wearing a low-risk-system costume; see the rationale there.)*
## Path B — milestones (provisional)
### MC-2 — Mix the questions: ranged threat + the swarm + the mix-director `~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 own `LastStep`; rebuild the swept segment from `cur - dir*LastStep`, **never `SystemAPI.Time.DeltaTime` in a plain-group system**) + swept `EnemyProjectileDamageSystem` (`SourceTick`-stamped, at-most-once `ecb.DestroyEntity`, **tunnelling regression test**). Prefer a **telegraphed ground-puddle** (L4D2 Spitter) over a fast bolt; cap speed.
- **Swarmer:** brain-0 tuned tiny/fast/near-zero-windup, spawned in count, so the dash also answers "don't get surrounded" (and MC-4's cleave answers it *better*).
- **Mix-director (the genuinely-NEW work — corrected):** the siege *scheduler already exists* — `ThreatDirectorSystem` (arms sieges, `PendingSiegeSize`, `SiegeTimeoutTicks`, post-expedition retaliation) + `CyclePhaseSystem` (Calm↔Siege) + `WaveSystem` (Lull/Spawning cadence). Do **not** re-derive pacing. The new work is replacing `WaveSystem`'s blind round-robin with a **deterministic weighted band table** (which brain spawns at which point in a siege). **CRITICAL determinism note:**`WaveSystem` overloads `SpawnCounter` as BOTH the prefab index (`SpawnCounter % prefabs.Length`) AND the ring-placement slot (`RingPosition(center, SpawnCounter, slots)`). A naive "replace the modulo" edit will **silently desync enemy ring positions server-vs-client** (a Play-only bug EditMode won't catch). The weighted pick MUST be a **pure function of an integer counter that does NOT alter `SpawnCounter`'s advance**, OR add a separate placement counter; cover it with the determinism test. **REUSE `ThreatConfig`/`ThreatState`.**
- **A within-fight power beat:** a mid-fight `UpgradePickup`/`StatModifier` spike so offense has a rising curve, not pure attrition (both already exist).
**Fun-gate (falsifiable):** a fresh tester, un-coached, dodges the lunge, repositions out of ≥1 Spitter puddle, and breaks up a Swarmer cluster within one 5-min siege (observed). **DODGEABLE-BOLT METRIC:** at the shipped cap speed, a reacting tester avoids the Spitter telegraph/puddle in ≥8/10 attempts under ~100 ms interp — if they can't, the speed is too high (this is the Play-gate, not a spec number). **PEAKS+BREATHERS:**`liveEnemyCount` over the siege shows ≥1 spike and ≥1 lull, not a flat trickle. **POWER MOMENT:** the tester reacts to the mid-fight upgrade ("now I can…") ≥1×/session.
**Tuning knobs:** Spitter speed / puddle radius / dwell / windup; Swarmer count / speed / windup; mix-band weights (baked table + a live multiplier); the existing hot `ThreatConfig`/`WaveDirector` knobs surfaced live.
**Open questions:****(fork)** puddle (area-denial, slower — recommended) vs. dodgeable bolt? **(decision — [locked](#locked-decisions-path-a))** `SaveData` v3 (=2 today, `SaveData.cs:53`) holds Goal/Ledger/Structures/StructureIo — `WaveState`/`ThreatState` are **NOT** serialized, so a save/load mid-siege resets the wave. **Assert-then-verify** the live version + field set in code at MC-2 time before choosing: accept session-only sieges (cheaper) **or** bump to v3 with `WaveState`+`ThreatState` appended (additive)? Does the mix-director key off siege-progress (Husks spawned this siege) or wave count? (integer counter either way).
**Build notes:** the Spitter is the only new ghost — ownerless interpolated, moved SERVER-ONLY in the plain `SimulationSystemGroup``[UpdateAfter(PredictedSimulationSystemGroup)]`, stock `LocalTransform` replication, reuse MC-1's `SourceTick`. **Cover the swept hit-detection with a tunnelling regression test.****Run a lighter design-review** here — it's the track's only new ghost type ([Risk register](#risk-register) R4).
**Claude:** the two enemy-projectile systems + the new ghost (duplicate-an-existing-ghost recipe), the Swarmer baked variant, the weighted mix-band table + `WaveSystem` refactor (preserving `SpawnCounter`'s ring-advance), the mid-fight power-beat wiring, the tunnelling + ring-determinism regression tests; the `SaveData` v3 migration *if* the operator chooses persistence. **Operator:** the Spitter speed/puddle Play-gate; the mix-band weight tuning; the session-vs-persisted decision; owning the lighter review.
**Dependencies:** Path A complete + the Decision Gate logged. **Kill-risk:** the Spitter is un-dodgeable under latency → reads as unfair chip damage instead of a reposition question — the puddle-over-bolt choice de-risks it. Secondary: a botched `SpawnCounter` refactor desyncs ring placement.
### MC-3 — Every hit lands (pure juice) `~0.75 wk (indicative)` · risk LOW
**Goal:** make the now-meaningful exchange *feel* like one — **real freeze-frame** hit-stop, enemy hit-flash, magnitude-scaled emphasis and knockback — all client-only, observe-only. *(MC-3 is kept **pure juice**; the co-op `EnemyStatus` amp lives in MC-5 where interdependence is the theme — see [Boundary judgment](#boundary-judgment-re-cut-mc-1-default-order-mc-4-early). A juice gate shouldn't also have to prove a co-op synergy.)*
- **REAL freeze-frame hit-stop** (3–7 frames, **local to the killer**, gated on `DamageEvent.SourceNetworkId` / the local-player edge — `_localPlayer` is already resolved in the system) — **genuinely new:**`CombatFeedbackSystem` does FOV-punch (`PrototypeCameraRig.PunchFov`) + per-magnitude shake + kill FOV punch today, **not** a held freeze. Never `Time.timeScale` (corrupts the sim) — hold the camera/anim *presentation* only. **Do NOT edge-fire on a phantom hit** that MC-1's prediction-reconciliation corrects away (see MC-1's reconciliation-flicker note).
- **Enemy mesh hit-flash** (`AnimatedLitShader` emission via per-instance `MaterialPropertyBlock`) — **DE-RISK ON DAY 1:** confirm the shadergraph exposes an emission input before committing the milestone to it; watch shared-material bleed.
- **Emphasis tiers:** size/shake/SFX scale by hit magnitude + kill tier (extend the existing `FeelConfig`-driven scaffold).
- **Live-tunable, damage-scaled directional knockback:** promote `Tuning.Knockback*` (currently compile-time consts) into a server singleton read by `ProjectileDamageSystem`/`EnemyAISystem`.
**Fun-gate (falsifiable):****WEIGHT BLIND-TEST** — a watcher ranks three hits (tickle / solid / haymaker) by feel alone in ≥8/10 trials. **KILL PUNCTUATES** — a kill is unmistakably louder than a hit; the tester can tell a kill happened without watching the health bar. **NO SIM CORRUPTION** — server==client position/health unaffected by the hit-stop, verified by an `execute_code` diff during heavy hit-stop (`localHitStopFrames` fires only for the *local* killer).
**Tuning knobs:** hit-stop frames per tier (3/5/7); flash color / duration; emphasis size/shake/SFX per tier; knockback speed / duration — all live `FeelConfig`/singleton.
**Open questions:** hit-stop on ALL hits or only kills + big hits? (all-hits feels chuggy in a swarm — recommend kills + a magnitude threshold).
**Build notes:** all juice = client-only managed `SystemBase` in `PresentationSystemGroup` that OBSERVES (the scaffold already is); read ECS via `SystemAPI.Query` + `EntityManager.CompleteDependencyBeforeRO<T>()`. `DamageEvent.SourceNetworkId` already exists — gate the freeze "local to the killer."
**Claude:** freeze-frame hit-stop, emphasis-tier scaling, the `Knockback*` const→singleton promotion, all `FeelConfig` wiring; the hit-flash `MaterialPropertyBlock` IF the emission input is confirmed. **Operator:** confirm the `AnimatedLitShader` emission input (or supply one); all feel tuning; the weight blind-test.
**Dependencies:** Path A complete + Decision Gate; MC-1 (hits must matter before juicing them). **Kill-risk:** hit-flash blocked because the shader has no emission input (then it needs a shader edit — scope creep) — de-risk day 1.
### MC-5 — The co-op keystone: downed + revive `~1.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 proven `Dead`-from-`Health` idiom). **The full idiom has THREE clauses — carry all three, not just the `Health<=0` derive:** (1) derive `Downed` from `Health<=0`; (2) **bake `Downed` DISABLED** (players spawn up); (3) the derive system must **visit downed entities via `.WithPresent<Downed>()`** to write the enabled bit on a currently-disabled entity (verified: `PlayerDeathStateSystem` uses exactly `.WithPresent<Dead>()` on the baked-DISABLED `Dead`). Rooted + fire-disabled but still present. **Derive-race fix:**`Downed` and `Dead` both derive from `Health<=0`, so replicate the discriminator — add a **`[GhostField] uint DownedUntilTick`** (bleed-out deadline; `RespawnInvuln{[GhostField] uint UntilTick}` is the exact template) so the owner derives both gates locally and doesn't mispredict downed→dead. Authoritative schedule stays server-side.
- **Proximity revive:** a `ReviveRequest``IRpcCommand` (scalar payload = downed ghostId, `BuildPlaceRequest` template), applied **server-only** in the plain group `[UpdateAfter(PredictedSimulationSystemGroup)]` (rollback would double-apply) with server-side proximity + channel re-validation; bleed-out falls through to the existing `PlayerRespawnSystem` give-up path.
- **Focus-fire priority elite** (healer/buffer aura — DRG Warden / RoR2 Mending; killing it weakens the pack), flagged with a replicated byte.
- **`EnemyStatus` co-op-amp (lives here, not MC-3):** a **server-only byte** (NOT a `[GhostField]`, NOT an enum) stamped at the existing `KnockbackState` site in `ProjectileDamageSystem`, read in `HealthApplyDamageSystem`'s sum loop to amplify summed damage so support+burst out-performs two soloists.
- **Friendly-fire fix:** do **not** add server-only `KnockbackState` to a *predicted* player (it fights prediction → rubber-band). Soft-FF = a `StatModifier` debuff / revive-channel interrupt only; gate raw-HP FF behind a Tuning toggle (default OFF).
**Fun-gate (falsifiable, needs a friend):** at least one **contested revive** (`reviveChannelsStarted` with enemies inside the revive radius) AND one **bleed-out-to-dead** in the same session; a revive attempt OR a deliberate let-them-bleed call in ≥80% of downs. **INTERDEPENDENCE:** a support+burst duo clears a fixed siege measurably faster than two identical soloists (timed; the `EnemyStatus` amp pays off). **PRIORITY BREAK:** the team breaks target to focus the elite when its aura is active in ≥8/10 elite spawns.
**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](#secretly-multi-milestones-why-the-estimates-widened)): a 4-slot serializer generalization (re-bake risk) + per-slot `StatRecompute` + classifier ghost-type-SET + 4-input wiring + HUD, each with the mandatory review and rollback-correctness tuning.
- **Slot axis:** generalize `AbilityRef{byte Id}` + `AbilityCooldown{uint}` to a **fixed `Slot0..3` struct** (4 byte ids + 4 `[GhostField]``NextFireTick` — a fixed struct over a `DynamicBuffer` to keep serializer churn to **ONE re-bake**) + 4 `Fire`-style InputEvents.
- **The real lift — per-slot `EffectiveAbilityStats`:** `StatRecomputeSystem` folds the character-wide `StatModifier` buffer into each slot, kept **unconditional/uncached** every predicted tick (a change-filter goes stale on rollback — the same discipline the single-slot case already uses).
- **Classifier:** generalize `ProjectileClassificationSystem` to a **ghost-type SET** (DR-016 flagged; keep it **non-Burst** — cross-assembly generics + predicted-spawn classification trip Burst ICEs). **Ship hitscan/cone first (no predicted spawn, no classification) so the classifier generalization is deferred until a 2nd predicted-spawn prefab exists. Fill 2–3 slots before 4.**
- Reuse the MC-4 archetype byte `switch` as the per-slot dispatcher; store all ids/ops as **byte**.
**Fun-gate (falsifiable):** two players run **different loadouts** and the readout shows complementary archetype usage (`slotFires[0..3]` all > 0 for each; not both spamming slot 0); a tester forms a combo grammar (e.g. utility→special→primary) deliberately in ≥8/10 fights; two different loadouts clear a fixed siege faster *together* than two copies of the better single loadout (timed). **NO ROLLBACK STALE:** per-slot `EffectiveAbilityStats` is correct after a forced rollback (server==client per-slot stats, via `execute_code`).
**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 `RepairRequest``IRpcCommand` (`BuildPlaceRequest` template, scalar payload = target cell/ghost-id) → a server-only `RepairSystem` (plain group, `AbilityUpgradeSystem` owner-map idiom) that spends shared-ledger resources to restore a damaged structure's `Health` toward `MaxHealth`. Reuse the in-place `StorageMath.Withdraw` + affordability check from `BuildPlaceSystem`; cost scales with missing HP (a tunable curve) so a near-dead machine costs more to save than to let die and rebuild — a real economic decision. Optionally a between-siege auto-repair **Mender** structure (additive `StructureType` byte) that slowly heals adjacent structures from the ledger. Wire a repair mode into the existing build palette (reuses `BuildPreviewMath` + the RPC-send pattern).
- **Build notes:** server-only, applied once (NOT predicted — rollback would double-apply). Repair RESTORES EB-1's `[GhostField] Health` — no new replicated field. Cost from the shared `ResourceLedger`. The Mender (if shipped) is a production-class system `[UpdateAfter(PredictedSimulationSystemGroup)]` mirroring `HarvesterProductionSystem`. Validate affordability + a live target BEFORE withdrawing (the commit-in-place rule); reject a repair on an already-destroyed/full structure silently.
- **Fun-gate (falsifiable):** after a siege chews up your base you spend the calm REPAIRING, and "repair this turret vs. build a new one vs. save for ability upgrades" is a real tradeoff (observed: the player chooses repair over rebuild ≥1× and over a different spend ≥1× in one calm); the repair cost makes you protect structures *during* the fight; a base recovers across cycles if you tend it and decays if you don't.
- **Open questions:** manual repair only (agency, a verb — recommended first) vs. auto-Mender vs. both? Full restore vs. partial? — start full.
- **Claude:** the `RepairRequest` RPC + `RepairSystem` + cost-curve math + the optional Mender + build-palette repair mode + EditMode coverage (restores HP, costs ledger, rejects on dead/full target). **Operator:** the manual-vs-Mender fork and the repair-vs-rebuild cost tuning.
- **Dependencies:** EB-1 (structures must have Health), EB-2 (a spend economy the player is engaged with). **Kill-risk:** repair is always strictly cheaper than rebuilding (no decision) OR always more expensive (dead code) — the cost curve must sit in the interesting middle.
**Goal:** resume inventory **Phase 2** ONLY as a combat-feeding loop — your equipped tool tier sets how fast you can feed the war economy.
- **Scope:** resume DR-026 Phase 2 — `RequiredToolType`/`RequiredToolTier` baked on `ResourceNode`; `ResourceHarvestSystem` gates + **scales** harvest yield by the firing player's equipped Tool-slot tier. **Braid it:** the harvested feedstock flows into the EB-2 munition pipe (better tool → more raw Aether → more Charges → more turret uptime → you fight longer) so the upgrade is felt AS combat power. Reuse the existing optional `ComponentLookup<GhostOwner>` + tier read already in `ResourceHarvestSystem`; the yield-scale is a pure `InventoryMath`/`HarvestMath` multiplier (no new replicated state — Tool is already an `EquipmentSlot``[GhostField] ItemId`). A small set: 2–3 tool tiers in `ItemDatabase`.
- **Build notes:** keep the owner-lookup OPTIONAL so owner-less projectiles (the 8 legacy harvest tests) still pass (DR-026's exact constraint); stay `[BurstCompile]`. Tier from the `EquipmentSlot` Tool ItemId → `ItemDatabase``byte Tier` (baked) — no new field. Gate is a yield MULTIPLIER (soft) by default, not a hard block-to-zero — tunable. Keep the deposit→ledger→munition pipe SHARED (co-op).
- **Fun-gate (falsifiable):** equipping a better tool visibly speeds your sortie haul AND a base measurably holds longer / abilities become affordable downstream — the gather upgrade pays out in the FIGHT, not on a stat screen; "spend on a better tool vs. a better weapon vs. more turrets" is a real loadout-economy fork; a co-op pair can specialize (one tools-up to feed, one fights) and it's strictly better.
- **Tuning knobs:** per-tier yield multiplier (baked — T1 ×1 / T2 ×1.75 / T3 ×3); gate mode soft vs. hard (server singleton, default soft); node `RequiredToolTier` per resource (baked).
- **Open questions:** soft gate (faster with the tool) vs. hard gate (nodes locked behind tiers)? Does tier affect WHICH resources or only speed? — start speed-only.
- **Claude:** the harvest gate + yield-scale, node `RequiredTool` authoring, the tool-tier catalog rows, EditMode coverage (tier scales yield; soft/hard gate; no-owner fallback preserved). **Operator:** the soft-vs-hard fork and confirming the upgrade is FELT in the fight.
- **Dependencies:** EB-2 (the munition pipe the feedstock feeds — **sequencing after EB-2 is non-negotiable**); 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 sibling `CraftSystem`) to produce ITEMS (weapons, gear, tool tiers, munition batches); the recipe is data (`ItemDatabase` + a recipe row), no new replication. A `CraftRequest``IRpcCommand` for player-initiated crafting → a server-only system that spends the shared ledger and deposits the crafted item into the requester's bag or the shared store. Tie the tier curve: crafted tool tiers (EB-4) + crafted weapons (the `AbilityRef` a weapon grants via `EquipSystem`, DR-027) + munition batches (EB-2) all come from the same Fabricator economy. Land the additive `SaveData` v3 DR-027 flagged (restore equipment+inventory atomically AND **replay equip** — a plain buffer restore wouldn't re-add the `StatModifier`s; effects are event-driven), now also covering crafted-item progression.
- **Build notes:** crafting reuses the Fabricator's input-limited deterministic catch-up (`ProductionMath`) — an item recipe is a recipe whose output is an `ItemId`, deposited via `InventoryMath.Deposit`; server-only, plain group. Bump `SaveData.CurrentVersion` (Load nulls on mismatch → clean degrade to New Game). `CraftRequest` is a one-off shared-state RPC → reliable, server-only, applied once. Keep recipe content MINIMAL — 2–3 weapons, 2–3 tool tiers, 1 munition batch. A crafted weapon sets `AbilityRef.Id` via the EXISTING `EquipSystem` path — no new combat code; it's an equippable `ItemDatabase` row with a `GrantedAbilityId`.
- **Fun-gate (falsifiable):** you craft a better weapon FROM the resources your factory refined, equip it, and immediately fight better — the **full loop (harvest→refine→craft→equip→fight) closes** and you feel each step; the craft-queue decisions (munition now vs. a weapon for next sortie vs. a tool) are a genuine economy game inside the combat game; a late-game base feels like an ARSENAL you built, and co-op players craft complementary kits from a shared factory.
- **Tuning knobs:** per-recipe cost + craft period (baked rows); craft destination — requester's bag vs. shared store (server singleton, default shared store with player-crafted consumables to the bag); tier-gate — which recipes available from start.
- **Open questions:** auto-craft vs. player-initiated vs. both? — start player-initiated. Crafted weapons permanent vs. consumable? — start permanent + equippable.
- **Claude:** the item-crafting recipe extension, the `CraftRequest` RPC + server system, the `SaveData` v3 atomic equipment/inventory/crafting restore with replay-equip, the recipe content rows, EditMode coverage (craft spends ledger → item in bag/store; save round-trips equipment+inventory+crafted progression; equip replay re-adds `StatModifier`s). **Operator:** the auto-vs-manual fork and the play-gate that the full loop FEELS like one game.
- **Dependencies:** EB-2 (the munition economy), EB-4 (tool tiers), DR-027 `EquipSystem`, **MC-6** (slots to express into — why EB-5 lands LAST). **Kill-risk:** crafting content is the classic breadth trap — a deep recipe tree nobody needs because the fight it feeds isn't deep enough. Shipping it before MC-6 re-creates the breadth-first hollowness DR-028 exists to kill.
### END-5 — Difficulty scales for 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 `ThreatDirectorSystem`**SAMPLES the live connection count ONCE** and scales `PendingSiegeSize` (the existing single entry point) by a baked per-player multiplier (the inert `ThreatConfig.SizePerExpeditionResource * 0` line at `ThreatDirectorSystem.cs:62` is the template — add a `SizePerPlayer`; **SERVER-ONLY**, never a `[GhostField]`). A join/drop does NOT resize the in-flight wave (sampled once at arm) but DOES affect the next; the bounded `SiegeTimeoutTicks` prevents a soft-lock. NG+/endless (END-2's victory flip made concrete) — on victory raise `GoalProgress.Target` + apply a global difficulty step (size/speed/brain-weight multipliers); **the persistent base carries over** (the locked pillar — NOT a roguelike reset); only the threat curve resets upward. HUD: a difficulty/NG+ tier readout (a replicated byte, observe-only).
- **Build notes:** SAMPLE the player count ONCE at siege-arm — never re-read per tick (jitter / breaks the atomic Calm→Siege seed; server-only so no misprediction). Scale through the EXISTING single entry point — no parallel sizing path. Connection count = iterate `NetworkId`-bearing connections server-side. NG+ step + Target raise are server-decided bytes/ints on the director ghost; the base/structures/ledger CARRY OVER. The tier readout is a replicated byte, client observe-only.
- **Fun-gate (falsifiable):** a 4-player session feels appropriately swarmed vs. a solo session (both fun-gate — neither trivial-solo nor impossible-at-4); a player joining mid-session doesn't break the in-flight siege but the next visibly accounts for them; after winning, the NG+ tier gives a returning group a reason to keep their base; the scaling is invisible-but-felt.
- **Tuning knobs:** `SizePerPlayer` multiplier (baked, sub-linear); NG+ Target raise + per-tier multipliers (baked, +50% Target / +15% per tier); the sub-linear curve exponent (Tuning, <1.0 so grouping is rewarded); mid-siege join policy fixed by the sample-once rule (no knob).
- **Open questions:** scaling shape — linear vs. **sub-linear** (recommended) vs. an HP-vs-count tradeoff? NG+ depth — truly endless vs. a capped ladder (pillar leans endless)? Does difficulty also scale Core integrity / siege frequency, or only wave size + brain-weight (recommended)?
- **Claude:** the sample-once scaling, the NG+/endless flip + difficulty-step multipliers, the tier HUD byte, EditMode coverage (size scales with sampled count; mid-siege join doesn't resize; NG+ raises Target). **Operator:** the 1-vs-4 fun-gate, the scaling-curve tuning, the NG+ depth decision.
- **Dependencies:** END-1, END-2 (the win flip NG+ extends), MC-5 (co-op difficulty is incomplete without the shared-crisis loop). **Kill-risk:** naive per-tick live-count scaling that makes siege size jitter or soft-locks on a mid-siege drop — the sample-once-at-arm rule is the load-bearing fix.
---
## Cross-cutting discipline
### Calendar-time: the play-budget assumption ★
The estimates above are **coding-time**, and the single biggest scope-realism distortion is reading coding-time as calendar-time for a one-person team whose throughput is gated by **focused-editor availability** (Burst-affecting edits, Play-validation, all asset/scene work) and **a friend's calendar** (the co-op fun-gates). The document admits fun-tuning is unbounded, then would otherwise present crisp day/week numbers a reader anchors on — the two statements contradict and the crisp numbers win. So, explicitly:
> **Assume ~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-affecting `CharacterProcessor` edit (own restart/validate) · the `DamageEvent.SourceTick` refactor across THREE stamp sites + the tick-windowed negation · the Charger brain (lunge/stagger/whiff) + telegraph tuning + dash juice.
- **EB-1 (~1.5–2.5 wk):** a ghost-hash-changing `[GhostField]` (re-bake every structure prefab) · an `EnemyAISystem` targeting rewrite · a `HealthApplyDamageSystem` death-branch change · a cross-group production-ordering gate · a `SaveData` v3 migration · the loss-juice the milestone says IS the milestone.
### 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.
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] uint`s on the predicted player (cross-player comparison reads both owners off the player query) **or** a periodic `DebugTelemetryReport` RPC (no ghost-hash change). All increments are at near-zero new surface — the sites already exist.
### Tuning-knob surface
> Consolidated table of every live-tunable value, so the operator tunes **defaults-first, live**. `Tuning.cs` consts are compile-time → Burst-inlined → **baked** (a recompile per tweak, Burst-affecting if inside a Bursted system). Feel-critical values are promoted to a **server singleton** (read at runtime → live-tunable in Play via the MC-0 `SetTuning` op; server==client holds because the singleton lives on the server). **Rule of thumb:** *baked* for structural/rarely-tuned values (cooldown sentinels, SourceIds, catch-up bounds, max-slot caps, per-prefab stats); *singleton* for values you tune by ear during a playtest (dash distance/i-frames/recovery, knockback, windups, hit-stop, telegraph lead, depletion rates).
| 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.
| 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](#the-decision-gate-mandatory-stop-after-end-2) hard stop; "is the existing loop tuned fun yet?"; a milestone can't pass until its dependency has PASSED, not merely shipped; Phases 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](#calendar-time-the-play-budget-assumption) (~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](#the-decision-gate-mandatory-stop-after-end-2) must be logged before any of the above is un-cut.**
## Locked decisions (Path A)
> Locked with the operator on 2026-06-09 via a **present-the-forks** exercise — gameplay-design forks are the operator's call, never auto-decided ([[DR-029_Path_A_Fork_Locks]]). These **resolve the per-milestone "Open questions" forks for Path A**; live-singleton picks stay tunable at playtest, so a lock here is a starting default, not a cage.
| Breach effect | END-1 | **Core bar only**; structure destruction stays EB-1's job | structural (no double-system) |
| Core persistence | END-1 | **Persist** — a dented Core carries across save/quit (v3) | save schema |
| Charge cadence | END-2 | **Both** — surviving sieges AND Aether deposited at the Engine fill the meter | structural (two server-only writers) |
| Win-resolution | END-2 | **Keep playing** — win banner, the base is yours; NG+/endless is the later END-5 | structural (minimal) |
| Final beat | END-2 | **One big escalating wave** (boss deferred) | live singleton (size) |
| `EnemyStatus` placement | MC-5 | **Confirmed in MC-5** (interdependence theme), not a standalone MC-3 hook | — |
**Where a pick departed from the safe default:** the operator chose **charge cadence = both** (the stronger braid) over siege-survived-only. END-2 therefore reads BOTH the siege-survived increment (`CyclePhaseSystem`, ships today) AND an Aether-deposited increment at the Engine — each a **server-only single writer** (no co-op double-count), summed into `GoalProgress.Charge`. This wires the win condition straight into the economy; budget the extra deposit-charge wiring in END-2. Every other fork took the recommended default.
### The fork-locking ritual — re-run before each Path B milestone ★
Path B forks (spitter form, downed/revive shape, the multi-slot kit, crafting, scaling/NG+, and the dormant END-3/END-4 content forks) are **deliberately left open.** When the [Decision Gate](#the-decision-gate-mandatory-stop-after-end-2) selects a Path B milestone, **lock its forks first via this same exercise: present each fork to the operator with a recommendation and let them decide.** Never auto-decide a gameplay-design question, and never mark a default "official" without an explicit okay — a locked default is a starting point the operator approved, not a call Claude made for them.
## Related
- [[DR-028_Combat_Primary_Verb_Depth_First]] — the direction this roadmap executes.
- [[Pillars]] — combat as the primary braided verb + depth-before-breadth.
- [[Identity]] — the braid in fiction (Aether economy ↔ siege stakes; the Echo/THEM payoff metered by charge — deferred to the Cut table until Path A is proven).
- [[DR-017_Persistent_Base_Player_Driven_Pacing]] — the player-driven siege loop + the deferred "base-integrity / unattended-siege teeth" END-1 cashes in.
- [[DR-026_Inventory_Equipment_Progression_Foundation]] · [[DR-027_Equipment_Slots_Phase1]] — the paused Phases 2–4 EB-4/EB-5 resume in braided form.
- [[Milestones]] — the historical record; [[Backlog]] — the loose pool (inventory/automation breadth paused).
# Session 2026-06-08 — Direction: combat-first + the combat-depth track
> A design/strategy session (no code). Operator: *"This does not feel like a game... expand the design + a clear roadmap to a core playable fun loop. Pushback where it'd substantially help. Ground it in real game design + real published games + the solo-dev constraint."*
## Goal
Diagnose why an engineering-complete project doesn't feel like a game, set a direction, and lay out a combat-depth milestone track.
## Process
- **Scan:** read [[Identity]], [[Pillars]], [[Milestones]], [[Backlog]], [[Systems_Index]], DR-004, content inventory (3 abilities = same projectile; 6 enemy prefabs = 1 brain; 8 items; structures). Confirmed the gap: vast infrastructure, hollow content; "Live interactive fire test" never done.
- **Intake gate (4 questions):** operator wants **all three pillars** (the fusion is the identity / unsure), **passion/craft**, **co-op NON-NEGOTIABLE**, **solo + Claude**.
- **Diagnosis + pushback** (delivered in chat): development was breadth-first + correctness-first → a "tech demo of a game"; the four pillars are three deep genres a solo dev can't make co-equal; the fix is braid-don't-co-equal with combat as the primary verb. Grounded in The Riftbreaker / Core Keeper / Deep Rock Galactic / Risk of Rain 2 / Left 4 Dead / Vlambeer.
- **Combat-depth design pass (ultracode workflow, 9 agents):** 5 design lenses (movement/defense · ability kit · enemy AI · co-op · game feel), each grounded in real games **and** the actual DOTS code → 1 synthesis (thesis + MC-1…MC-5) → 3 adversarial critics (netcode-feasibility · solo-scope · fun), all **go-with-changes**.
## Done (decisions + docs)
- **[[DR-028_Combat_Primary_Verb_Depth_First]]** — combat is the primary braided verb; base+automation braid around it (not co-equal); **depth-before-breadth** + per-milestone **fun-gates**; inventory Phases 2–4 + automation breadth paused.
- **[[Path_to_Fun]]** — new north-star roadmap: the braided loop + the combat-depth track **MC-1…MC-6** (re-cut per the critics) with build notes.
## Key findings from the design pass (carry into MC-1)
- **The keystone is enemy COMMITMENT + a punishable whiff paired with the dodge.** The dash is the *answer*, a committed lunge is the *question*; both are inert alone (aim is already decoupled from move → kite-strafe-and-click already beats the one brain). So MC-1 ships the dash **and** the Charger lunge as one playtest unit, validated by play, not against the current commitment-free melee.
- **Two MC-1 netcode blockers (pre-caught):** (1) i-frames must negate damage per-`DamageEvent` against the tick it was authored — stamp a non-replicated `uint SourceTick` on `DamageEvent` (`HealthApplyDamageSystem` drains the prior-tick melee event in the predicted group), not "is DashState active now." (2) `CharacterControl` has no sharpness field — also drive `CharacterComponent.GroundedMovementSharpness` near-instant for the dash window, else the dash ramps ("walk faster"). Budget the Burst-processor edit (focused editor; expect a restart).
- **Telegraph readability is a PRECONDITION, not polish:** drive the cue off the **replicated `AttackWindup` tick countdown** (enemies are interpolated ~100ms late); lengthen windups to ~27+ ticks; make enemy-projectile dodgeability a Play-gate.
- **Co-op validated early + cheap:** pull a server-only `EnemyStatus` synergy byte into MC-3 (slow+burst duo > two soloists). Downed/revive (MC-5) replicates a `[GhostField] uint DownedUntilTick` discriminator to keep the derive rollback-correct; no server-only `KnockbackState` on predicted players.
- **MC-6 (multi-slot kit) is last + review-gated** — the only HIGH-risk item; ship hitscan/cone (no predicted spawn) before generalizing `ProjectileClassificationSystem` to a ghost-type set; 2–3 slots before 4.
Operator: *"Lets refine the roadmap fully."* A second multi-agent pass refined [[Path_to_Fun]] end-to-end — 4 deepen lenses (combat track · the economy braid · endgame/win-lose · production discipline, each grounded in the actual code) → synthesis → 3 code-grounded adversarial critics (netcode-feasibility · solo-scope · fun-coherence), all **go-with-changes**. Outcome:
- **Path A / Path B hard split + a mandatory Decision Gate.** Committed **Path A** = MC-0 (instrument the box) · MC-1 (dash + committed Charger) · MC-4 (melee cone) · EB-1 (machines can die) · EB-2 (felt spend / turret ammo from the factory) · END-1 (a losable Core) · END-2 (final siege, win/lose) — *a complete, shippable small game with a point.* Everything else (MC-2/3/5/6 · EB-3/4/5 · END-5) is **provisional Path B**, re-estimated only after Path A's fun-gates pass; **END-3 narrative + END-4 content-treadmill CUT** to the revisit table (an empty event bus is negative value for a content-light solo project). No Path B work begins until an explicit ship-vs-continue decision is logged.
- **Code-grounded corrections (critics read the systems):** a THIRD `DamageEvent` stamp site (`TurretFireSystem.cs:95`); `DashState` needs an explicit `StartTick`; the i-frame negation is a cross-group tick-alignment problem (`HealthApplyDamageSystem` in the predicted group drains the strike `EnemyAISystem` appended a tick earlier in the plain group) — MC-1 mandatory-review agenda item #1. The MC-2 "wave director" is already built (Threat/Cycle/Wave) — only the weighted enemy-MIX table is new.
- **New cross-cutting discipline:** a falsifiable fun-gate protocol + MC-0 instrumentation (so feel claims are counted — e.g. timed-vs-spam dash hit-counts), a ~35-row tuning-knob surface (baked vs live server-singleton + defaults), the solo+Claude two-lane cadence (friend = an EXTERNAL Path-A dependency at EB-2's Demo B), a risk register (R1–R11), and demo checkpoints (Duel / Loop / Crisis). 10 operator forks catalogued (locked the next day — see Addendum 2).
Direction unchanged ([[DR-028_Combat_Primary_Verb_Depth_First]]); the refinement sharpened scope realism, falsifiability, and netcode precision.
## Addendum 2 — Path A forks locked (2026-06-09)
Operator process correction: *gameplay-design forks are the operator's call — present each with a recommendation, never auto-decide or mark a default "official" without an okay* (a workflow attempting to auto-lock every fork was halted mid-run). Worked the forks interactively; **Path A is now fully locked** ([[DR-029_Path_A_Fork_Locks]] · [[Path_to_Fun#Locked decisions (Path A)]]): free-aim + whole-window dash · Charger = a new prefab · cleave = its own button + cooldown · Husks push-for-base · soft-loss + wounded-base-persists (SaveData v3) · turret-only ammo with soft run-dry · win-meter = **both** sieges + Aether deposits (the one non-default pick — the stronger braid) · winning = keep-playing · final = one big escalating wave. Live-singleton picks stay tunable at playtest. **Standing rule:** re-run the same present-the-forks ritual before each Path B milestone; Path B forks stay open. Saw also [[present-forks-dont-auto-decide]].
## Next
Path A is locked and concretely specified. Operator's call: **start MC-0 + MC-1** (instrument the box, then dash + Charger committed-lunge + readable telegraph — honoring the three-site `SourceTick` i-frame fix, the `DashState.StartTick` window, and the Burst-affecting `CharacterProcessor` edit) via a normal plan→approve→build slice — or commit the refined vault docs. Per [[Path_to_Fun]], **MC-1 is the project kill-switch**: if its fun-gate fails after a real tuning pass, STOP and re-cut combat before building on it.
> The code-grounded implementation spec for MC-1 (dash + Charger duel). Produced by the mandatory adversarial design review (netcode/determinism · reuse/scope · feel-feasibility, all go-with-changes). Drives the build. Roadmap: [[Path_to_Fun]] (MC-1); locks: [[DR-029_Path_A_Fork_Locks]].
# VERDICT: go-with-changes
## IMPLEMENTATION SPEC
# MC-1 Implementation Spec (verified against code; ground-truth confirmed)
All three review lenses verified the ground-truth against the actual files. Verdict **go-with-changes**: the architecture is sound and idiom-correct; the changes are precision pins (half-open negation window, per-site SourceTick clock + NonZero, idempotent DashSystem split, component-presence Charger discriminator). One material correction confirmed in code below.
## CONFIRMED CORRECTION — the dash "blink" needs NO CharacterProcessor edit (drops R3 from MC-1)
`CharacterProcessor.HandleVelocityControl` (CharacterProcessor.cs:105-121) reads `characterComponent.GroundedMovementSharpness`**as a RW ref** and lerps `characterBody.RelativeVelocity` toward `CharacterControl.MoveVelocity` via `StandardGroundMove_Interpolated`. `CharacterComponent` is plain RW `IComponentData`. So a predicted `DashSystem` raising `GroundedMovementSharpness` to ~200 for the dash window (and restoring 15 after) produces the snap with **zero edit to the Bursted processor** — fully headless, no focused-editor Burst restart. The Tuning-knob surface itself names this as the fix ("Dash movement sharpness ~200, the GroundedMovementSharpness override fix"). **DEFAULT = sharpness-override.** The direct `RelativeVelocity` write inside the processor is kept ONLY as a documented fallback if Play shows the lerp-to-200 still visibly ramps — and only THAT fallback carries the Burst-restart. This means MC-1's only focused-editor step is the `.inputactions` wrapper regen + Play-validation.
---
## 1. Components
### EDIT `DamageEvent` (Simulation/Combat/DamageEvent.cs) — currently exactly `{float Amount; int SourceNetworkId}`, IBufferElementData, server-side, NOT replicated
Add: `public uint SourceTick;` — the raw ServerTick at which the hit LOGICALLY LANDS (the appending tick). **Confirmed zero ghost-hash impact** (the buffer is non-replicated; its doc-comment says so). Update the doc-comment to note SourceTick = the tick the strike was authored, used by the dash i-frame negation.
Add after `Fire`: `[GhostField] public InputEvent Dash;` (verbatim Fire twin). In `ToFixedString()` append `s.Append(';'); s.Append(Dash.Count);`. **Churn class: command-hash change** (InputBufferData<PlayerInput> serializer + command-collection hash) — both peers rebuild; NOT a ghost-prefab re-bake.
### NEW `DashState` (Simulation/Player/DashState.cs) — predicted, NON-replicated, derived each tick (clone KnockbackState SHAPE only)
```
public struct DashState : IComponentData {
public float2 Dir; // planar XZ dash heading, captured at dash-start
public uint StartTick; // raw ServerTick at dash-start (NonZero-coerced)
public uint IFrameUntilTick;// StartTick + iFrameWindow (NonZero); i-frames active while this .IsNewerThan(SourceTick)
public uint RecoverUntilTick;// IFrameUntilTick + recoverTail (NonZero); movement-lock tail, no i-frames
}
```
NOT a `[GhostField]` (so no player-ghost re-bake). The SERVER re-derives it every predicted tick from the replicated `Dash` InputEvent, so it is authoritative at drain time. Bake DISABLED-equivalent (all-zero) via PlayerCharacterAuthoring AddComponent. Doc-comment must state: SHAPE-clone of KnockbackState, but UNLIKE KnockbackState it lives on a PREDICTED player and is re-simulated from input (not server-only-mutated on an interpolated ghost).
### NEW `DashCooldown` (Simulation/Player/DashCooldown.cs) — AbilityCooldown twin
```
public struct DashCooldown : IComponentData { [GhostField] public uint NextTick; } // 0 = ready
```
`[GhostField]` so the owning client doesn't mispredict cooldown across rollback/reconnect — exactly AbilityCooldown.cs:28. Bake `{NextTick=0}`.
### NEW `LungeState` (Simulation/Combat/LungeState.cs) — server-only, KnockbackState twin, baked ONLY on the Charger prefab
```
public struct LungeState : IComponentData {
public float2 Dir; // fixed lunge heading, locked at commit
public float Speed; // lunge speed (units/s); 0 = not lunging
public uint UntilTick; // raw tick the lunge ends (NonZero); active while .IsNewerThan(serverTick)
}
```
NOT a `[GhostField]` (lunged position replicates via stock LocalTransform, like KnockbackState). **Component-presence IS the Charger discriminator** — no enum/brain byte (honors the Burst cross-assembly-enum rule; EnemyAISystem is `[BurstCompile]`).
### EDIT `EnemyStats` (Simulation/Combat/EnemyComponents.cs) — add per-prefab windup so the Charger telegraph does NOT globally slow every Husk
Add `public int WindupTicks;` Grunt bakes ~18 (current global), Charger bakes ~30. EnemyAISystem reads `stats.WindupTicks` instead of the global `Tuning.AttackWindupTicks` at the windup-set site (EnemyAISystem.cs:167). (The knob surface promotes `Tuning.AttackWindupTicks`→28 globally — REJECT that for the Charger; per-prefab is the right first cut. Keep a TuningConfig singleton override only if live-tuning the Charger lead is wanted; baked-per-prefab is the MC-1 default.)
Verified present as a server-only `IComponentData` singleton with the exact counters: `DashIFrameNegatedHits`, `DashesWasted`, `ChargerWhiffWindowsOpened`, `ChargerWhiffPunishesLanded`. NO blocker on telemetry home. (The MC-0 TuningConfig live-singleton for feel knobs is referenced as "building" — see Blockers: confirm it lands before wiring DashSystem to read LIVE values; until then use the baked defaults below.)
---
## 2. SourceTick stamp — all THREE sites, each with its OWN appending-tick clock, all via TickUtil.NonZero
The negation compares `DamageEvent.SourceTick` against the player's stored DashState window. The stamp MUST be the tick the strike LANDS (the appending tick), NOT the drain tick — because EnemyAISystem/TurretFireSystem append in the PLAIN group (drained tick N+1), while ProjectileDamageSystem appends in the predicted group (drained same tick).
1.**EnemyAISystem.cs:144** (melee strike) — add `SourceTick = TickUtil.NonZero(now)` (`now` = the local var already computed at line 63 = `serverTick.TickIndexForValidTick`).
2.**ProjectileDamageSystem.cs:129** — add `SourceTick = TickUtil.NonZero(nt.ServerTick.TickIndexForValidTick)` (`nt` is fetched at line 64, guarded by `haveTick`; if `!haveTick` stamp 0 — treated as no-i-frame).
4. The **MC-4 cleave** and **EB-1/EB-2** future DamageEvent appends must also stamp SourceTick (already called out in the spec).
NonZero at every site mirrors how StartTick/IFrameUntilTick are coerced, so a stamped event is never 0 and never collides with the "0 = ready" sentinel.
---
## 3. The i-frame negation in HealthApplyDamageSystem — HALF-OPEN, PER-ELEMENT
Add in HealthApplyDamageSystem.OnUpdate, inside the per-entity foreach, AFTER the RespawnInvuln gate (lines 52-64) and at the sum loop (lines 66-69). It must be **per-element** (skip only the in-window DamageEvent), NOT a whole-buffer clear like RespawnInvuln/GodMode — a same-tick turret/projectile event on the player must still apply (defensive correctness; in practice only enemy melee targets the player today, but per-element is the documented-correct shape).
**Convention pin (BLOCKER 1):** the window is `[StartTick, IFrameUntilTick)` — lower bound INCLUSIVE, upper bound EXCLUSIVE — matching RespawnInvuln/AbilityCooldown/EnemyAttackCooldown where `until.IsNewerThan(now)` is false at `now==until` (i.e. the gate OPENS at tick==until). Store `IFrameUntilTick = StartTick + iFrameWindowTicks`; tick==IFrameUntilTick is NO LONGER i-framed. `src==0` (unstamped) is treated as NEVER i-framed (damage applies — fail-safe). Use NetworkTick comparisons ONLY (never raw uint), so tick-wraparound is correct.
`telemetry` = `SystemAPI.GetSingletonRW<DevTelemetry>()` fetched once at top of OnUpdate (guard with TryGetSingleton; if absent, skip increments — keeps EditMode worlds without a telemetry singleton green).
Attributes: `[UpdateInGroup(typeof(PredictedSimulationSystemGroup))]``[UpdateAfter(typeof(PlayerControlSystem))]``[BurstCompile]` (plain ISystem; no managed). Filter `.WithAll<Simulate>().WithDisabled<Dead>()`. Verify in Play it sorts BEFORE `PredictedFixedStepSimulationSystemGroup` (PlayerControlSystem already establishes that precedent — match it; the override must land before the processor lerps that tick).
Query: `RefRW<DashState>, RefRW<DashCooldown>, RefRW<CharacterControl>, RefRW<CharacterComponent>, RefRO<PlayerInput>, RefRO<PlayerFacing>` + `.WithEntityAccess()` (need InputBufferData if reading absolute Dash count — optional, see below).
Per-tick logic (the START is an idempotent pure function of replicated input+tick → NO IsFirstTimeFullyPredictingTick guard; the OVERRIDE must run EVERY predicted pass so rollback re-applies it):
```
uint now = serverTick.TickIndexForValidTick; // serverTick = GetSingleton<NetworkTime>().ServerTick
- Does NOT write rotation — free-aim/strafe-dodge is automatic (PlayerAimSystem owns facing, not gated by dash). Confirmed PlayerAimSystem.cs:24-27.
-`dashSpeed` derived from the distance knob so distance is the tuned value: `dashSpeed = dashDistance / (iFrameWindowTicks / 60f)`.
- **Death/cleanup (major):** DashSystem is `.WithDisabled<Dead>()` so it won't visit a dead player → a player who dies mid-dash leaves a stale future window + sharpness=200, which would grant spurious i-frames + a stuck-fast respawn. FIX: in PlayerDeathStateSystem (which already visits `.WithPresent<Dead>()` and zeroes MoveVelocity, lines 30-39), ALSO when `isDead`: zero DashState (StartTick/IFrameUntilTick/RecoverUntilTick = 0) and restore `GroundedMovementSharpness = 15`. Add DashState + CharacterComponent to that query.
---
## 5. Charger LungeState branch in EnemyAISystem (server-only, sole position writer)
This is the largest hidden cost — a REWRITE of the strike branch for the Charger, not an additive field. Discriminate by `LungeState` component presence via a SECOND query pass (`.WithAll<EnemyTag, LungeState>()`) OR an optional `ComponentLookup<LungeState>` on the existing query — a second pass is cleaner and keeps the Grunt path byte-free. Recommend: keep the existing Grunt foreach unchanged; add a Charger-specific foreach that ALSO matches `LungeState`, and EXCLUDE Chargers from the Grunt pass via `.WithNone<LungeState>()` so a Charger is driven only by the Charger branch.
**Explicit state precedence (in one place):** knockback > lunge-active > lunge-commit(windup-elapse) > seek/strike. Each `continue`s after writing Position (preserve the SOLE-writer invariant — NO separate LungeSystem).
Charger per-enemy logic:
1.**Knockback wins** (a shot staggers the charge): reuse the existing knockback branch (lines 80-94); ADD `lunge.ValueRW.UntilTick = 0` there so a knocked-back Charger's lunge is cancelled (otherwise two position writers contend). Keep `windup=0`.
2.**Lunge active** (`LungeState.UntilTick` newer than serverTick): move fixed `lunge.Dir` at `lunge.Speed*dt` via `SweptMove`; hold Y. Deal contact damage DURING travel: if `EnemyAIMath.InAttackRange(pos, nearestPlayerPos, AttackRange)` append the `DamageEvent{Amount, SourceNetworkId=-1, SourceTick=NonZero(now)}` (at-most-once via cooldown). **Whiff detection:** compare intended displacement vs SweptMove-actual; if `actualTravel < intendedTravel * whiffFraction` (wall-stop) OR the lunge timer elapses without ever entering AttackRange (overshoot) → enter stagger: `cooldown.NextAttackTick = TickUtil.NonZero(now + whiffStaggerTicks)` (default 36) and clear `lunge.UntilTick`; `telemetry.ChargerWhiffWindowsOpened++`. `continue`.
- To get the wall-hit signal cleanly: have `SweptMove` return the travel fraction (it already has `out var hit` internally), or recompute `|result-pos| / |intended-pos|` at the call site.
3.**Lunge commit** (windup-elapse, replaces the instant strike at lines 144-152 FOR THE CHARGER ONLY): do NOT append the instant DamageEvent. Instead capture `lunge.Dir = normalize(targetPos - pos)` ONCE, set `lunge.Speed = chargerLungeSpeed` (default 16), `lunge.UntilTick = TickUtil.NonZero(now + lungeDurationTicks)`, clear `windup`. The lunge travels next tick.
4.**CRITICAL — cancel-on-leave-range must NOT apply to the Charger commit.** EnemyAISystem.cs:135-137 cancels the windup if the target leaves AttackRange. For the Charger, the whole point is the commit FIRES even when the player has dodged out of range. So the Charger windup must NOT use the cancel path — once the long windup elapses, it locks dir and lunges regardless of current range. (Gate the cancel to the Grunt pass only; the Charger pass has no leave-range cancel.)
5. Charger uses `stats.WindupTicks` (~30) for the telegraph, set at the windup-arm site.
The Charger's commit telegraphs via the EXISTING `[GhostField] AttackWindup.WindUpUntilTick` — CombatFeedbackSystem already edge-detects it (CombatFeedbackSystem.cs:127,132-137). The longer lead is the readable tell.
**Whiff-punish telemetry:** in HealthApplyDamageSystem, when a player-sourced DamageEvent (`SourceNetworkId >= 0`) lands on an entity that has a Charger `LungeState` AND its `EnemyAttackCooldown.NextAttackTick` is in the whiff-stagger window → `telemetry.ChargerWhiffPunishesLanded++`.
---
## 6. Charger prefab variant (new ghost, additive — no spawn-code change)
- NEW `ChargerAuthoring : MonoBehaviour` (duplicate EnemyAuthoring) that bakes the same Husk components PLUS `AddComponent<LungeState>(entity)` (zeroed) and bakes Charger EnemyStats (longer WindupTicks ~30, lunge-appropriate MoveSpeed). Keep the GhostAuthoring (interpolated, ownerless) inherited by duplicating an existing interpolated ghost prefab (Husk.prefab or UpgradePickup.prefab) per the new-ghost recipe.
- Add the Charger prefab to the `WaveEnemyPrefab` buffer pool — WaveSystem.cs:83-92 spawns it round-robin via `baked.WithPosition` unchanged. **Churn class: new ghost prefab, additive — no re-hash of Grunt/Swarmer/Brute.**
- VERIFIED: `Assets/Settings/Project M Input.inputactions` has only Move/Aim/Fire. Fire binds `<Mouse>/leftButton`, `<Gamepad>/rightTrigger`, AND `<Keyboard>/space` (lines 195-213). The wrapper `ProjectMInput.cs` is correctly routed into ProjectM.Client via `wrapperCodePath` in the .meta (gotcha already honored).
- Add a `Dash` Button action. **Do NOT bind to Space** — Space is BOTH already a Fire binding (would double-fire) AND in the kbm-active sentinel (PlayerInputGatherSystem.cs:105). Bind Dash to `<Keyboard>/leftShift` + `<Gamepad>/buttonEast` (or another free pad button — not rightTrigger which is Fire).
- Regenerate the wrapper via the importer (re-import the .inputactions on a FOCUSED editor — generated GUID-referenced code, do NOT hand-edit). wrapperCodePath stays `Assets/_Project/Scripts/Client/Input/ProjectMInput.cs`.
- In PlayerInputGatherSystem (mirror Fire exactly, lines 160-162): add `bool dashPressed = gameplay.Dash.WasPressedThisFrame() && !BuildPaletteState.Active;` then in the per-player loop `input.ValueRW.Dash = default; if (dashPressed) input.ValueRW.Dash.Set();`. **Fold the keyboard dash key into `kbmActive`** (add `keyboard.leftShiftKey.isPressed` to line 100-106) and the gamepad dash button into `gamepadActive` (add `gamepad.buttonEast.isPressed` — already partially there at line 90) so a dash-only actuation flips the active scheme correctly.
- **Churn: command-hash change** (new InputEvent on PlayerInput) — both peers rebuild; the wire type is unconditional (no `#if`) so the handshake matches; no prefab re-bake.
---
## 8. Dash juice (must be in MC-1)
Client-only, in CombatFeedbackSystem (PresentationSystemGroup, observe-only). Edge-detect the local `DashCooldown.NextTick` advance exactly as it edge-detects `AbilityCooldown.NextFireTick` for the muzzle flash (CombatFeedbackSystem.cs:189-202): on the 0→nonzero / advance edge fire afterimage/whoosh + a directional camera nudge (camera punch, NEVER Time.timeScale) + an i-frame shimmer. The client can derive its own DashState identically. **Suppress player hit-feedback while the local i-frame window is active** (the client derives DashState too) so the prediction-reconciliation Health flicker (client predicts hit → server negates → snapshot corrects) does NOT read as a phantom "I got hit" flash on a clean dodge — this is the documented acceptable-not-a-bug interaction.
-`DashIFrameNegatedHits++` — per negated event in HealthApplyDamageSystem (§3).
-`DashesWasted++` — in DashSystem, lazily WHEN the dash window CLOSES having negated 0 hits (define "wasted" = the i-frame window overlapped no incoming Charger strike SourceTick), NOT at dash-start (so it counts genuine mistimes, not early presses). Simplest: track a per-player negated-count on DashState and on window-close, if 0 negations occurred this dash, increment.
-`ChargerWhiffWindowsOpened++` — EnemyAISystem when a lunge enters stagger (§5.2).
-`ChargerWhiffPunishesLanded++` — HealthApplyDamageSystem when a player-sourced hit lands on a staggered Charger (§5).
## Knob defaults (baked until the MC-0 TuningConfig live-singleton lands; then promote the feel-critical ones)
1. NEGATION BOUNDARY (half-open window) — seed NetworkTime.ServerTick = drainTick (e.g. T+1) via the TelegraphTests SetServerTick pattern; put DashState{StartTick=S, IFrameUntilTick=S+W} on the entity; append DamageEvents with SourceTick in {S-1, S, S+1, S+W-1, S+W, S+W+1}; tick HealthApplyDamageSystem once; assert Health UNCHANGED for src in [S, S+W) (i.e. S, S+1, S+W-1 negated) and REDUCED for src==S-1, src==S+W, src==S+W+1. Pins the exact first-negated (S) and first-NOT-negated (S+W) ticks so the boundary is frozen.
2. TICK-WRAPAROUND negation — DashState.StartTick near uint.MaxValue, IFrameUntilTick wrapping past 0; DamageEvent.SourceTick straddling the wrap; assert NetworkTick.IsNewerThan negates correctly (proves no raw-uint compare leaked in).
3. SourceTick==0 fail-safe — append a DamageEvent with SourceTick=0 while a DashState window is active; assert damage APPLIES (unstamped events are never i-framed).
4. PER-ELEMENT (not whole-buffer) negation — same tick, append one in-window melee DamageEvent (SourceNetworkId=-1) AND one out-of-window event; assert ONLY the in-window event is negated, the other still subtracts (proves the dash gate is per-element, unlike the whole-buffer RespawnInvuln/GodMode clears).
5. RespawnInvuln still whole-buffer — regression: a DashState entity ALSO under RespawnInvuln negates ALL damage (RespawnInvuln path unchanged, runs before the per-element dash loop).
6. DASH START IDEMPOTENCE (rollback determinism) — register DashSystem in a bare predicted-style group; set Dash.IsSet + DashCooldown ready; run the start tick TWICE at the same ServerTick; assert identical StartTick/IFrameUntilTick/RecoverUntilTick/DashCooldown.NextTick (the start is a pure function of input+tick; no double-trigger).
7. DASH COOLDOWN GATE — dash, advance ServerTick past IFrameUntilTick but before DashCooldown.NextTick, set Dash.IsSet again; assert NO new dash (window unchanged); advance past cooldown, set again; assert a new dash starts.
8. DEATH MID-DASH CLEANUP — dash (window in the future), set Health<=0, tick PlayerDeathStateSystem; assert DashState window zeroed AND GroundedMovementSharpness restored to 15 (no spurious respawn invuln, no stuck-fast).
9. DASH VELOCITY OVERRIDE ORDERING — with DashState i-frame active, tick PlayerControlSystem then DashSystem; assert CharacterControl.MoveVelocity == Dir*dashSpeed (DashSystem overrode PlayerControl's input velocity) and GroundedMovementSharpness == dashSharpness.
10. STAMP NON-ZERO — drive each of the three append sites (or unit-test the stamp expression) and assert the produced SourceTick is never 0 (NonZero coercion at all three).
11. CHARGER COMMIT FIRES OUT OF RANGE — Charger windup elapses while the target has LEFT AttackRange; assert it does NOT cancel (LungeState entered, Dir locked to target-at-commit) — contrasts with the Grunt cancel-on-leave-range path.
12. CHARGER WHIFF -> STAGGER — give a Charger a fixed lunge Dir and a target offset so SweptMove stops short / never enters range; tick the lunge to expiry; assert EnemyAttackCooldown.NextAttackTick is extended by whiffStaggerTicks, LungeState.UntilTick cleared, and ChargerWhiffWindowsOpened incremented.
13. CHARGER KNOCKBACK CANCELS LUNGE — a Charger mid-lunge receives KnockbackState; tick EnemyAISystem; assert the knockback branch wins (position written by knockback) AND LungeState.UntilTick is cleared (no two-writer contention).
14. CROSS-GROUP i-FRAME TICK-COVERAGE REGRESSION (the required tunnelling-style test, EXPRESSED AS A TICK-COVERAGE TABLE, with an explicit PLAY-validation note) — the plain EditMode harness CANNOT reproduce the predicted-vs-plain group split (systems register unsorted, one tick, no group separation), so this test does NOT tick EnemyAISystem then HealthApplyDamageSystem to recreate N->N+1. Instead it asserts the NEGATION COVERS the cross-group offset directly: seed drainTick = T+1, DashState window [T-2, T+3], append DamageEvents with SourceTick = {T-3, T-2, T (a melee strike authored at T, drained at T+1), T+2, T+3, T+4}; assert exactly the in-window ones (T-2..T+2) negate and T-3/T+3/T+4 apply — proving a strike authored at T (and drained a tick later) is still negated by the window that covered T. Include an assertion-comment that the actual N->N+1 group timing (EnemyAISystem appends at T, drains at T+1) is a PLAY-VALIDATION item (review agenda item #1: server's DashState window at drain time covers the melee strike appended a tick earlier in the plain group), NOT EditMode-reproducible.
## BUILD ORDER
1. 1. (CLAUDE) DamageEvent.SourceTick field + stamp all THREE sites (EnemyAISystem:144, ProjectileDamageSystem:129, TurretFireSystem:95) via TickUtil.NonZero. Headless, no behavior change yet. Edit Assets .cs via MCP apply_text_edits/create_script, read_console after.
2. 2. (CLAUDE) DashState + DashCooldown components; the HealthApplyDamageSystem per-element half-open negation branch + DevTelemetry.DashIFrameNegatedHits increment (DevTelemetry already exists). Write the negation-boundary + wraparound + per-element + SourceTick==0 EditMode tests FIRST/alongside (fully headless-coverable via the TelegraphTests SetServerTick pattern). This is the kill-switch foundation — Play-validate the cross-group alignment (agenda item #1) before building feel on top.
3. 3. (CLAUDE) Dash InputEvent on PlayerInput + ToFixedString; PlayerInputGatherSystem reset+Set + fold into kbmActive/gamepadActive. Edit the .inputactions JSON (Write is fine — non-asset). THEN: (OPERATOR-FOCUSED-EDITOR) re-import the .inputactions to regen ProjectMInput.cs on a FOCUSED editor (generated GUID code); command-hash change means BOTH peers rebuild (no MPPM half-update).
4. 4. (CLAUDE) DashSystem [UpdateAfter(PlayerControlSystem)] with the sharpness-override blink (DEFAULT path — NO CharacterProcessor edit, headless). PlayerDeathStateSystem cleanup (clear DashState + restore sharpness on death). Dash-determinism + cooldown + death-cleanup + override-ordering EditMode tests. (OPERATOR-FOCUSED-EDITOR) Play-validate the SNAP TEST (RelativeVelocity reaches dash speed in 1-2 ticks) + server==client DashState window via execute_code; confirm DashSystem sorts before PredictedFixedStepSimulationSystemGroup. Verify CharacterControlUtilities.StandardGroundMove_Interpolated lerp form via unity_reflect before committing sharpness ~200.
5. 5. (OPERATOR-FOCUSED-EDITOR, FALLBACK ONLY) IF Play shows the lerp-to-200 still visibly ramps: the direct characterBody.RelativeVelocity write inside CharacterProcessor.HandleVelocityControl — THIS is the Burst-affecting edit (Burst-off for the session, expect a restart, Play-validate). Skip entirely if the sharpness path passes the SNAP TEST (expected).
6. 6. (CLAUDE) LungeState component (server-only); EnemyStats.WindupTicks per-prefab field; the Charger branch rewrite in EnemyAISystem (knockback>lunge>commit>seek precedence, SweptMove whiff detection, no cancel-on-leave-range for the Charger) + ChargerWhiffWindowsOpened/PunishesLanded increments. Charger commit/whiff/knockback-cancel EditMode tests. read_console for the Burst cross-assembly-enum/generic hazards (component-presence discriminator keeps it byte-free).
7. 7. (OPERATOR-FOCUSED-EDITOR for prefab authoring) ChargerAuthoring (duplicate EnemyAuthoring + AddComponent<LungeState>); duplicate an interpolated ghost prefab for the GhostAuthoring; add the Charger to the WaveEnemyPrefab pool via manage_scene/manage_prefabs; verify baked components via execute_code. Additive ghost — no Grunt re-hash.
8. 8. (CLAUDE) Dash juice in CombatFeedbackSystem (edge-detect DashCooldown; afterimage/whoosh/camera-nudge/shimmer; suppress player hit-feedback during the local i-frame window). DashesWasted increment (lazy, on window-close with 0 negations).
9. 9. (OPERATOR) MANDATORY netcode design review BEFORE create_script of the netcode-heavy slices (agenda item #1 = cross-group i-frame alignment), then the bench (timed vs spam, >=70% fewer hits) + the friend-read at Demo A. ALL feel tuning. This is the kill-switch gate.
## BLOCKERS
- MC-0 TuningConfig LIVE-SINGLETON for feel knobs is NOT confirmed in code. DevTelemetry (the counters) EXISTS at Simulation/Debug/DevTelemetry.cs — good — but I found no TuningConfig/feel-knob singleton (Tuning.cs is still compile-time consts only). The dash/Charger LIVE-tuning loop the fun-gate depends on requires that singleton. NOT a hard blocker for building MC-1 with BAKED defaults, but the operator must confirm MC-0's SetTuning singleton lands BEFORE the tuning pass, or DashSystem/EnemyAISystem must read baked consts first and be re-pointed at the singleton later. Either decide to land the TuningConfig singleton in MC-0 now, or accept baked-const defaults for the first Play pass.
- The mandatory netcode design review (agenda item #1: at the drain tick, does the server's DashState i-frame window compared via SourceTick correctly cover a melee strike appended a tick earlier in the plain group) MUST run BEFORE create_script of the negation + DashSystem + Charger slices, per CLAUDE.md and the operator's standing rule. This is a process gate, not a code defect — flagged so it is not skipped under time pressure.
## OPEN CONCERNS FOR OPERATOR
- DASH-FEEL FORK RESOLVED IN CODE: the 'CharacterProcessor edit is the only focused-editor piece' framing in the spec is FALSE for the DEFAULT path. CharacterProcessor.cs:117 reads GroundedMovementSharpness as a RW ref, so a predicted DashSystem raising it to ~200 produces the blink with NO processor edit and NO Burst restart (R3 drops out of MC-1). The Tuning-knob surface itself names sharpness ~200 as the fix. Recommend trying the sharpness path FIRST; keep the direct RelativeVelocity processor write as a documented fallback that carries the Burst-restart ONLY if Play shows a visible ramp. This likely makes MC-1's movement slice fully headless except the .inputactions wrapper regen.
- PER-PREFAB WINDUP vs GLOBAL PROMOTION: the knob surface proposes promoting Tuning.AttackWindupTicks to ONE singleton at 28, which would slow EVERY Husk's strike (changing the existing fight feel), not just the Charger. Recommend a per-prefab EnemyStats.WindupTicks (Grunt ~18, Charger ~30) instead; promote to a live singleton later if Charger-lead live-tuning is wanted. Confirm this is acceptable (it diverges from the literal knob-surface row).
- RE-DASH AT T+1 EDGE: if a player dashes again on the exact tick a prior strike is being drained, the new dash overwrites the window before the drain reads it, so the prior strike's coverage could change. v1 accepts this 1-tick edge (the player chose to re-dash) and the negation reads SourceTick against whatever window is current. If this proves exploitable/confusing in playtest, add a 1-slot window history (PrevStartTick/PrevUntilTick). Flagging so it is an explicit accepted edge, not a silent bug.
- PREDICTION-RECONCILIATION FLICKER is EXPECTED in the very playtest that gates the track (client predicts the hit, server's server-only i-frame negates, next snapshot corrects Health). It is acceptable/server-authoritative, masked by the local i-frame shimmer + hit-feedback suppression. Document this in the fun-gate protocol so a tester does not read it as 'flaky i-frames' and fail the gate spuriously.
- CHARGER STRIKE-BRANCH REWRITE is the largest hidden cost — today EnemyAISystem deals contact damage INSTANTLY on windup-elapse; the Charger needs windup-elapse to instead ENTER a fixed-dir lunge that travels and damages only on contact-during-travel, with whiff detection from SweptMove displacement and NO cancel-on-leave-range. Budget this as a real branch + a SweptMove signature change (return the travel fraction / wall-hit bool), not an additive field. The ground-ring/scale-up telegraph RAMP is net-new CombatFeedbackSystem presentation work (the edge-detect scaffold is reused; the ramped visual is new) — slightly understated as 'mostly tuning' in the spec.
> Implements [[2026-06-09_MC1_Build_Spec]] (the mandatory pre-code adversarial review's output). Direction: [[Path_to_Fun]] · locks: [[DR-029_Path_A_Fork_Locks]]. **Status: all code + tests + structural Play-validation done; the MC-1 FUN-GATE (feel pass + bench + friend read) is the open operator item — MC-1 is NOT "done" until it passes.**
## What was built (the full uncommitted slice)
### MC-0 — instrument the box
-`DevTelemetry` (Simulation/Debug) — server-only singleton with the four fun-gate counters + LiveEnemyCount/LastSampleTick proof-of-life. `DebugTelemetryReport : IRpcCommand` — **unconditional wire type** (RpcCollection hash parity); only the systems are `#if UNITY_EDITOR`.
-`DevTelemetrySystem` (Server) — ensures the singleton, samples each tick, ships the report to every connection every 15 ticks. `DevTelemetryReceiveSystem` (Client) — drains the RPC into the `DevTelemetryReadout` static; `DebugOverlay` renders the live counters.
- **All four counters now wired**: `DashIFrameNegatedHits` (HealthApplyDamageSystem, per negated event) · `DashesWasted` (DashSystem window-close edge, via a new `DashState.NegatedCount` server-written field; close-edge is **server-gated on the singleton** so the client's DashState is never zeroed mid-rollback) · `ChargerWhiffWindowsOpened` (EnemyAISystem, both whiff sites) · `ChargerWhiffPunishesLanded` (HealthApplyDamageSystem: player-sourced hit inside a new `LungeState.StaggerUntilTick` window, **scored once per window** by zeroing on first punish so punishes:windows ≤ 1).
### MC-1 — dash
-`DamageEvent.SourceTick` (non-replicated) stamped via `TickUtil.NonZero` at all THREE append sites (EnemyAISystem melee, ProjectileDamageSystem, TurretFireSystem); `SourceTick==0` = unstamped = never i-framed (fail-safe).
-`DashState` (predicted, non-replicated, re-simulated from input) + `DashCooldown{[GhostField] NextTick}` — baked on the player via PlayerAuthoring. `PlayerInput.Dash` InputEvent (`Fire` twin, command-hash churn only — no ghost re-bake).
-`DashSystem` (predicted, `[UpdateAfter(PlayerControlSystem)]`, Bursted): idempotent start (press + cooldown-ready + not-in-window), HALF-OPEN i-frame window `[StartTick, IFrameUntilTick)`, recovery tail (movement locked, no i-frames), **sharpness-override blink** (`GroundedMovementSharpness` 15→200 for the window — NO CharacterProcessor edit, fully headless, exactly as the spec's confirmed correction predicted).
- Negation in `HealthApplyDamageSystem`: per-element (not whole-buffer), half-open, `NetworkTick` comparisons only (wrap-safe). `PlayerDeathStateSystem` clears the window + restores sharpness on death.
- **Dash juice** in `CombatFeedbackSystem`: dash whoosh SFX + afterimage burst + camera shake + FOV punch on the `DashCooldown.NextTick` edge (muzzle-flash pattern); i-frame shimmer trail each frame the local window is active; **local hit-feedback suppressed during the local i-frame window** (masks the documented prediction-reconciliation Health flicker). All knobs in `FeelConfig` (Feature 5 block, live-pokeable).
### MC-1 — Charger
-`LungeState` (server-only; **component-presence is the discriminator** — no enum in the Bursted system) + `ChargerAuthoring` (composes WITH EnemyAuthoring on the prefab; bakes only LungeState).
-`EnemyAISystem` Charger pass (Grunt pass excludes via `.WithNone<LungeState>()`): precedence **knockback (cancels lunge) > lunge-active (SweptMove travel, contact damage, wall-stop + overshoot whiff → stagger) > seek > commit** — commit locks Dir at windup-elapse and **fires even if the target left range** (the punishable tell). Charger windup 30 ticks / lunge 16 u/s / 18 ticks / stagger 36 ticks (per-pass consts).
- **Prefab chain**: `EnemyCharger.prefab` (capsule template: Enemy.prefab duplicate + ChargerAuthoring + tuned stats HP 45 / spd 2.6 / dmg 14 / cd 48 / scale 1.0) → `EnemyChargerMuscle.prefab` via a new **`EnemyRigTools` "Build Charger (MC-1)"** menu item (builds ONLY the charger so the committed Werewolf/Kaiju outputs aren't re-serialized). Model: **SM_Chr_Muscle_Male_01** (PolygonSciFiCity — the [[Synty_Asset_Inventory]] "verified Generic rig, next-faction" path), atlas `PolygonScifi_01_A` + red tint (danger read), RootY −1.0. Added as the 4th `WaveDirector.EnemyPrefabs` round-robin entry in the Gameplay subscene (additive ghost — no re-hash of existing ghosts).
- **Play (real netcode session, server+client): zero console errors** — no `ComponentSystemSorter` cycle, no stale-Burst exception. DashState/DashCooldown baked in BOTH worlds; DashSystem sorts directly after PlayerControlSystem (the documented 1-tick fixed-step offset pattern). **Live E2E negation**: armed a window on the server player, appended in-window + out-of-window strikes → only the outside one applied (100→93), counter +1, close-edge cleanup ran. **Live Charger**: spawned from the baked pool → seek → 30-tick telegraph → lunge commit (speed 16) → contact damage → repeat; replicates to the client; killed the (stationary) player → death/respawn flow exercised. **Telemetry pipe**: server counters → RPC → client `DevTelemetryReadout` → overlay, values matching. Charger material values verified (AnimatedLitShader + SciFi atlas + red tint).
- Six "wasted" dashes were counted live with `serverCd == clientCd` — real input-driven dash starts replicated consistently (and wasted-counting works).
A 29-agent review workflow (netcode/prediction · DOTS/Burst · spec-adherence · edge-cases, each finding adversarially refuted) ran over the full diff. Ten findings were refuted as true-but-mitigated (by design, world placement, an existing test, or the documented deviations). Two were confirmed and fixed in-session:
1.**MAJOR — the dash override lacked the StartTick lower bound.**`DashState` is non-replicated → NOT restored on prediction rollback; after a press at tick D the client re-simulates pre-dash ticks S..D−1 with the post-press window visible, and an upper-bound-only `iFrameActive` stomped dash velocity + sharpness onto ticks that never had them → **dash-start overshoot + snap-back under real latency** (editor RTT≈0 masked it; the negation had the lower bound, only the movement override didn't — the bug originated in the spec's §4 pseudocode). **Fix:**`inDashWindow = StartTick != 0 && !StartTick.IsNewerThan(serverTick)` now gates both override branches; pre-dash re-sim ticks fall to the restore branch. Pinned by `Rollback_ReSimulated_PreDash_Tick_Gets_No_Override`.
2.**MINOR — DashSystem vs HealthApplyDamageSystem were unordered** in the server's predicted group, so a same-tick teammate-projectile vs dash-start negation (`src == StartTick`; ProjectileDamageSystem appends and the drain runs the SAME tick) was an unconstrained sorter tiebreak. **Fix:**`[UpdateAfter(typeof(DashSystem))]` on HealthApplyDamageSystem (chains verified disjoint — no cycle; Play-validated: dash 13 → drain 14, clean world creation).
Notable refuted-but-recorded items: the punish check compares the stagger window against the *drain* tick (correct today — the only `SourceNetworkId>=0` site drains same-tick; revisit if MC-4's cleave appends in the plain group) · the dash displacement consumes velocity one tick after the i-frame window (the documented OrderFirst 1-tick offset; the shimmer matches the true negation window) · the build-server never clears DashState at window-close (intentional — all readers are tick-guarded; pinned by `Without_Telemetry_Singleton_The_Close_Edge_Leaves_DashState_Intact`) · Charger lunge contact only tests the current nearest player (the spec'd + pre-existing Grunt targeting model; a co-op design note for later).
(Process note: the first review run returned `{confirmed: []}` because all four agents died on a subagent session limit — an empty result that masquerades as a clean pass. Check the failures list; re-run after the reset via `resumeFromRunId`.)
## Deviations from the build spec (all deliberate)
1.**Input = direct device reads** (`leftShift` / `buttonEast``wasPressedThisFrame` in PlayerInputGatherSystem), NOT a `.inputactions` Dash action — kills the only focused-editor step (wrapper regen). Migrate to the action map later if rebinding UI ever needs it.
2.**Per-pass consts for the Charger windup** (30 ticks in the Charger foreach) instead of an `EnemyStats.WindupTicks` field — same effect (Grunt feel untouched), one field less; promote to per-prefab/live-singleton when tuning demands it.
3.**`LungeState.StaggerUntilTick`** added for punish scoring (the spec inferred the window from `EnemyAttackCooldown`, which would conflate post-hit cooldown with stagger and let one window score many punishes).
## Open items (operator)
- **The MC-1 fun-gate** (gate 3): focused-editor feel pass — dash SNAP test (sharpness 200 must read as a blink, else the documented CharacterProcessor fallback + Burst restart), Charger telegraph readability, RootY feet check on the Muscle rig, then the bench (timed vs spam ≥70% fewer hits) + a friend read at Demo A. Live whiff/punish numbers only emerge with a dodging player.
- **TuningConfig live-singleton** (the spec's MC-0 blocker) still does not exist — dash/Charger knobs are baked consts (+ FeelConfig statics for presentation). Decide whether to land it before the tuning pass.
-`Assets/_Recovery/0.unity` (2.2 MB, 2026-06-08 23:36) — an untracked Unity scene-recovery artifact; review + delete (with its .meta) if it holds nothing.
- Wave-spawned Chargers appear only when a siege runs (player-driven pacing) — the round-robin makes 1 in 4 spawns a Charger.
# DR-028 — Combat is the Primary Verb (braid, don't co-equal) + Depth-Before-Breadth
## Context
After M0–M7 + inventory/equipment (Phases 0–1), the project is engineering-rich but "doesn't feel like a game" (operator, 2026-06-08). Diagnosis this session: development has been **breadth-first and correctness-first** — every milestone proved a *system replicates deterministically* (server==client, EditMode green); almost none proved a *loop is fun*. The tell: **"Live interactive fire test" sat OPEN in the [[Backlog]] after a dozen milestones** — combat, pillar #1, was never once playtested for enjoyment. Result: enormous, correct infrastructure over hollow content. Combat = ONE verb (a projectile with stat variants, a single `AbilityRef.Id`); SIX enemy prefabs share ONE brain (`EnemyAISystem` seek-nearest + contact melee); NO dodge/dash; death = free respawn. "Skill expression over stat-checks" is unmet because there is no skill surface — and aim is already decoupled from move, so kite-strafe-and-click trivially beats the only brain.
The four [[Pillars]] read as **four co-equal genres** — action combat + co-op base + automation + netcode. That is three deep genres in one game, which a solo dev (even Claude-accelerated) cannot make co-equally deep, and building them all breadth-first is *why* there is no fun. Operator intake (2026-06-08): wants ALL THREE (the fusion is the identity), **co-op NON-NEGOTIABLE**, **passion/craft** (no deadline), **solo + Claude** (content-light). Grounding in real small-studio games that fuse combat+base+automation — **The Riftbreaker** (combat-led), **Core Keeper** (mining-led, literal conveyors+drills, co-op) — confirms the fusion is achievable but ALWAYS picks one primary verb and braids the others around it; none makes all three co-equal.
## Decision
1.**Combat is the PRIMARY VERB.** Base-building and automation are not cut — they **braid around combat** as its stakes and economy: automation makes what you fight *with* (charges/munitions/turrets/upgrades); sieges threaten what automation lives *in* (the base/machines, with a real loss beat); the sortie feeds both. See [[Path_to_Fun]]. "All three" stays the identity, but as ONE braided loop with a single primary verb — never three co-equal, independently-deep modes (which is the current state and the thing that doesn't feel like a game).
2.**Why combat (not base/automation) is the verb:** co-op-non-negotiable points at *shared combat* (the most reliable co-op fun — Deep Rock Galactic / Risk of Rain 2 / Vermintide); combat is structurally furthest along; combat is the most hollow, so it has the highest fun-per-hour return.
3.**Depth-before-breadth is the operating rule.** No new SYSTEM until one braided loop is genuinely fun. The validation culture shifts: green EditMode + server==client are **necessary, not sufficient** — every milestone now ends with a **fun-gate** (play it, with a friend, and not want to stop). The netcode/determinism rigor stays (it is a real strength); "done" stops meaning "tests pass."
4.**Pause inventory/equipment Phases 2–4 and automation breadth** (recipe/throughput). They are more breadth on systems whose payoff is combat power — premature while combat is hollow. Redirect to the combat-depth track. The shipped Phases 0–1 stand; only the forward phases pause.
5.**The combat-depth track** ([[Path_to_Fun]], MC-1…MC-6) is the next work, designed this session via a multi-agent pass (5 design lenses grounded in real games + the actual DOTS code → synthesis → 3 adversarial critics: netcode-feasibility / solo-scope / fun, all "go-with-changes"). Keystone: a **dodge + a committed, whiff-punishable enemy + a readable telegraph** = the smallest "fight in a box" that turns stand-and-click into a conversation. The dash is the *answer*, the committed lunge is the *question*; they ship together.
## Consequences
- **The braid is the highest-leverage design change** and costs less than half of what's already built — most parts exist; they're pointed at a ledger instead of at each other.
- **Pillar ordering changes; no code is deleted.** [[Pillars]] revised to name combat the primary braided verb + the depth-before-breadth rule; [[Identity]] gains the braid section. The automation/base records ([[DR-014_M6_Build_Structures_Automation_Foundation]] / [[DR-020_M7_Automation_Production_Chains]]) stand — their role becomes "support the fight."
- **Combat was never play-tuned**, so MC-1 onward validates by PLAYING, not tests. The team's reflex is correctness; the discipline now is feel.
- **Two MC-1 netcode blockers are pre-caught** (must be honored at code time): (a) the dash i-frame must negate damage per-`DamageEvent` against the tick it was authored (`HealthApplyDamageSystem` runs in the predicted group and drains the prior-tick melee event) — stamp a non-replicated `uint SourceTick` on `DamageEvent`, not "is DashState active now"; (b) `CharacterControl` has no sharpness field — a flat `MoveVelocity` write ramps ("walk faster"); also drive `CharacterComponent.GroundedMovementSharpness` near-instant for the dash window. Plus the precondition: telegraphs must read off a **replicated absolute-tick countdown** (`AttackWindup`-style `[GhostField] uint`), not interpolated motion (~100ms late). Details in [[Path_to_Fun]].
- **Falsifiable / owner-revisitable:** if, building the braid, the factory turns out to be the operator's true love, re-cut around automation-as-heart (Factorio/Dyson model, combat as defense). The combat-first call is a recommendation the operator can reverse.
- **Refined 2026-06-08 (same session)** via a second multi-agent pass (4 deepen lenses → synthesis → 3 code-grounded adversarial critics, all go-with-changes): [[Path_to_Fun]] is now split into a **committed Path A** (MC-0/MC-1/MC-4/EB-1/EB-2/END-1/END-2 — the minimal path to *fight-fun + braided-with-stakes + a win/lose condition*, a shippable game-with-a-point) and a **provisional, NOT-scheduled Path B** forever-track, with a **mandatory logged Decision Gate** after END-2 before any Path B work (the enforcement teeth of depth-before-breadth). The critics' code audit added a **third `DamageEvent` stamp site** (`TurretFireSystem`) to the i-frame `SourceTick` fix and required `DashState` carry an explicit `StartTick` (the window-start the negation compares against, via `NetworkTick` — the strike is appended a tick earlier in a different system group than the drainer). Net-new replicated state stays minimal — structure `Health` (EB-1) and `CoreIntegrity`/`RunPhase`/`RunOutcome` bytes on the existing global ghost (END-1/2). Ten operator forks are catalogued in [[Path_to_Fun]] (top: lose-severity, enemy-aggro, charge-cadence).
- Supersedes the implicit "four co-equal pillars" framing of [[Pillars]]; sets [[Path_to_Fun]] as the new north-star roadmap. [[Milestones]] remains the historical record.
# DR-029 — Path A Forks Locked + the Present-the-Forks Ritual
## Context
[[Path_to_Fun]] (refined per [[DR-028_Combat_Primary_Verb_Depth_First]]) carried ~20 open forks — the committed **Path A** could not be built without locking the ones that shape its feel. The operator made a **process correction**: gameplay-design forks are the operator's to call — **present each fork with a recommendation and let the operator decide; never auto-decide a gameplay question or mark a default "official" without an explicit okay.** (An attempt to auto-lock every fork via a workflow was halted mid-run.) The forks were then worked through interactively.
## Decision
**1. Path A forks are LOCKED** (2026-06-09), via a present-the-forks exercise. The set (also tabled in [[Path_to_Fun#Locked decisions (Path A)]]):
- **MC-1 dash:** free aim during the dash · whole-window i-frames (the recovery tail punishes spam).
- **MC-1 Charger:** a new Husk **variant prefab** (distinct silhouette + telegraph), not a brain-byte.
- **MC-4 cleave:** its **own button** + its **own cooldown** (dash→cleave→shoot stays live).
- **EB-1 aggro:** Husks **push for the base/structures** (attacking players in the way) — you defend; live singleton.
- **EB-1 / END-1 persistence:** a **wounded base persists** across save/quit (structure HP + Core integrity in SaveData v3).
- **EB-2 ammo:** **turret ammo only** (server-only) from the factory; player abilities stay free for now. **One** munition type ("Charge"). Run-dry = **soft-fail** (turrets go quiet).
- **END-1 lose:** **soft loss** — a breach drains the shared ledger / damages structures, the siege ends, the base persists wounded (Tuning byte). A breach drains the **Core bar only**; structure destruction stays EB-1's job.
- **END-2 win:** the meter fills from **BOTH** surviving sieges AND Aether deposited at the Engine (the operator's one non-default pick — the stronger braid; two server-only single writers summed into `GoalProgress.Charge`). Winning = **keep playing** (the base is yours; NG+/endless is the later END-5). Final beat = **one big escalating wave** (boss deferred).
- **`EnemyStatus`** co-op damage-amp stays in **MC-5**.
Live-singleton picks remain tunable at playtest (a lock is a starting default, not a cage). The one departure from the safe default — **charge cadence = both** — wires the win condition into the economy braid and costs END-2 a little extra deposit-charge wiring.
**2. The fork-locking ritual is a standing process rule.** Path B forks stay **open**; before building any Path B milestone (chosen at the [[Path_to_Fun#The Decision Gate (MANDATORY STOP after END-2)|Decision Gate]]), **its forks are locked first via this same present-the-forks exercise.** No auto-deciding gameplay-design questions.
## Consequences
- Path A is concretely specified — Claude can build MC-0 + the MC-1 code/tests autonomously; the first genuine operator touchpoints are the **Burst-affecting `CharacterProcessor` edit** (focused editor) and the **MC-1 fun-gate** (feel + ideally a friend).
- [[Path_to_Fun]]'s "Open decisions" section is replaced by **Locked decisions (Path A)** + the ritual note; [[Backlog]] updated.
- Reversible by the same ritual — live-singleton locks flip at playtest; structural locks (cadence-both, persistence v3, turret-only ammo) are committed shapes a re-run can revisit.
- Reinforces [[DR-028_Combat_Primary_Verb_Depth_First]]'s depth-before-breadth: Path B is not pre-decided, so breadth cannot creep in via a stale default.
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.