Slice 3: Expedition Combat Spine — epoch-seeded zone waves (DR-040)

Reactivate the dormant Expedition region as a procedural combat venue.
v1 loop: walk the gate -> fight an epoch-seeded enemy wave in the
expedition -> clear -> return -> flat Ore reward (once per epoch) ->
escalated retaliation base siege.

- New sim types: ZoneEnemyTag, ZoneEnemyDirector (+ ZoneEnemyPrefab
  buffer), ZoneEnemyState, ZoneEnemyMath (grunt->charger composition
  by epoch). ZoneEnemyDirectorSystem (server, Burst): drip-spawns the
  wave at a deterministic ring under a MaxAlive cap while a player is
  out and the base is Calm; marks ClearedThisEpoch on a real clear.
  [UpdateAfter(ExpeditionFieldSystem)] only (avoids a sort cycle).
- BLOCKER 1: EnemyAISystem region-filters target selection (player +
  structure snapshots gain parallel region lists; no base structures /
  no Core fallback for expedition husks).
- BLOCKER 3: WaveSystem, ThreatDirectorSystem timeout cull, and
  CyclePhaseSystem DefendCleared + Core-breach cull all count/cull
  RegionTag{Base} husks only (the breach cull was caught region-blind
  by the post-impl review: a base breach wiped the live expedition
  wave and spuriously paid the reward).
- BLOCKER 4: reward de-duped via CycleRuntime.LastRewardedEpoch +
  ClearedThisEpoch; ExpeditionGateSystem deposits RewardOre once/epoch.
- ExpeditionFieldSystem teardown also culls zone enemies + region-
  guards the clutter loop. Subscene wired with the director + roster.

368/368 EditMode green + clean netcode Play smoke. Docs: DR-040 ->
built, session log, CLAUDE.md cross-region tag-reaudit rule.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-21 22:58:26 -07:00
parent cf45ec82ae
commit 3109b86d71
33 changed files with 1044 additions and 161 deletions
@@ -0,0 +1,49 @@
---
date: 2026-06-21
type: session
tags:
- session
- combat
- expedition
- netcode
- procgen
- slice
- slice-3
permalink: gamevault/07-sessions/2026/2026-06-21-slice3-expedition-combat-spine-build
---
# Slice 3 — Expedition Combat Spine (build)
> Built the v1 loop locked in [[DR-040_Slice3_Expedition_Combat_Spine]] — the third build slice of the co-op-roguelite redirect ([[DR-037_Procedural_Expedition_Spine_Two_Classes_Persistent_Meta]]), the heaviest netcode slice. Full [[dots-dev]] Feature track, two-review sandwich. Reuses the region split ([[DR-013_M6_Aether_Cycle_Region_Split]]), the base-local core loop ([[DR-031_Base_Mining_Loop_Cohesion]]), epoch-seeded chassis, ring math, and the threat retaliation ([[DR-017_Persistent_Base_Player_Driven_Pacing]]).
## The loop that shipped
Walk to the Expedition gate → the dormant Expedition region (`base+(1000,0,0)`) drip-spawns an **epoch-seeded enemy wave** in `Calm` → fight & clear it → return to base → a **flat Ore reward** (once per epoch) → the existing `PendingReturns` retaliation arms an **escalated base siege**. Variety comes from epoch-seeded **composition** (grunt-heavy → charger-heavy as the epoch climbs), not arena layout — the highest-leverage lever per the pre-build research.
## What shipped
- **New sim types** (`ProjectM.Simulation`): `ZoneEnemyTag` (marker); `ZoneEnemyDirector{int MaxAlive; float RingRadius; int RingSlots; int SpawnIntervalTicks; int GruntsPerWave; int ChargersPerWave; int RewardOre}` baked singleton + `ZoneEnemyPrefab` buffer (aliases the existing werewolf/charger prefabs); `ZoneEnemyState{uint SpawnCounter; int RemainingToSpawn; uint NextSpawnTick; int SeededEpoch}` (server-only, its OWN counter — never `WaveState`'s). `ZoneEnemyMath` (pure: `WaveSize` / `IsChargerSlot`, grunt count fixed, chargers grow with epoch, assigned to the LAST slots).
- **`ZoneEnemyDirectorSystem`** (server, `[BurstCompile]`): runs only while a player is out (a `PlayerTag` whose `RegionTag.Region==Expedition`) and the base is `Calm`; (re)seeds the wave per epoch; spawns one-per-cadence under the `MaxAlive` cap at a deterministic ring via `ecb.Instantiate` + `baked.WithPosition` (preserves the `[GhostField]` Scale) + `AddComponent RegionTag{Expedition}`+`ZoneEnemyTag`; marks `CycleRuntime.ClearedThisEpoch=1` on a real clear.
- **`EnemyAISystem` region filter** (BLOCKER 1): player + structure snapshots gained parallel `NativeList<byte>` regions; both seek loops gained `RefRO<RegionTag>` + a region-aware `EnemyAIMath.PickWeightedNearest` overload; an expedition husk gets no base structures and no Core fallback (idles if its arena is empty). Burst-safe byte compares.
- **Base-only cleared-checks** (BLOCKER 3): `WaveSystem`, `ThreatDirectorSystem` SiegeTimeout cull, and `CyclePhaseSystem.DefendCleared` all count/cull `RegionTag{Base}` husks only.
- **Reward de-dup** (BLOCKER 4): `CycleRuntime` gained `LastRewardedEpoch` (int) + `ClearedThisEpoch` (byte); `ExpeditionGateSystem` deposits `RewardOre` to the shared `ResourceLedger` once per epoch, only after a real clear.
- **Teardown** (`ExpeditionFieldSystem`): epoch bump also resets `ClearedThisEpoch`; destroy-loop now also culls `ZoneEnemyTag`; added the defensive `RegionTag{Expedition}` guard to the `BlightClutter` loop.
- **Subscene:** wired a `ZoneEnemyDirector` GameObject (authoring + roster = werewolf + charger) into `Gameplay.unity`, re-baked.
## Deviations from the locked plan (all deliberate)
- System named **`ZoneEnemyDirectorSystem`** (plan: `ZoneEnemySpawnSystem`).
- **Ordering = `[UpdateAfter(ExpeditionFieldSystem)]` ONLY.** The plan also sketched `[UpdateBefore(CyclePhaseSystem)]`, but ExpeditionFieldSystem is already `UpdateAfter(CyclePhaseSystem)`, so that would close a `CyclePhase→Field→Zone→CyclePhase` **sort cycle** — which throws at Play world-creation and is **invisible to plain-Entities EditMode** (a recurring trap). Caught at grounding; confirmed clean by the Play smoke.
- `ZoneEnemyState.SeededEpoch` is `int` (matches `int ExpeditionEpoch`).
## The post-impl review earned its keep again
The re-run review (`wf_7b45e3c0-b81`, 3 lenses + synth; the first attempt had died on a session-quota limit — empty findings that *look* like a clean pass, [[workflow-agent-quota-failures-look-clean]]) surfaced **1 HIGH**:
**A region-blind Core-breach cull — a THIRD cull path beyond BLOCKER 3's two.** `CyclePhaseSystem`'s overrun/soft-loss branch despawned **all** `EnemyTag` entities. Pre-slice that was safe (every enemy was base-region) — but Slice 3 makes expedition enemies share `EnemyTag`, so a **base** Core breach wiped a live **expedition** wave AND spuriously satisfied the zone director's `aliveZone==0` clear/reward edge (a free Ore reward on return). I had *rationalised* leaving this cull global during implementation ("a Core overrun is terminal"). Fixed to a Base-only filter matching the BLOCKER-3 siblings; removed the now-dead `m_AliveHusks` field; locked with regression test `CyclePhaseSystemTests.Base_Overrun_Disperses_Base_Husks_But_Spares_Expedition_Husks`. Same class of bug the post-impl review caught in [[DR-031_Base_Mining_Loop_Cohesion]] — [[post-impl-review-catches-simplification-regressions]] holds twice now.
## Verification
- **L1:** console clean (0 ProjectM errors/warnings).
- **L2:** **368/368 EditMode** (`ProjectM.Tests.EditMode`) — region-filter target correctness (+4), `ZoneEnemyMath` composition (6), `ZoneEnemyDirectorSystem` world tests (5), reward de-dup (2), the breach regression (1), + the pre-existing fixtures patched with `RegionTag{Base}`.
- **Play smoke:** ServerWorld/ClientWorld boot with **no sort-cycle, no Burst ICE**; the subscene re-baked the singleton (MaxAlive 12 / Ring 14×10 / interval 30 / 4 grunts +1 charger / 25 Ore) + the 2-prefab roster; replicating the director's real spawn sequence tags each enemy `RegionTag{Expedition}`+`ZoneEnemyTag` with no Add-throw and **baked Scale preserved** (0.8 werewolf via `WithPosition`); zero runtime errors; clean cleanup.
## Open / next
- **Fun-gate (operator):** a hands-on playtest of the full walk-gate→fight→return→reward→siege loop — the logic is unit-covered, the *feel* is not.
- **Operator fork still open:** OPTIONAL-vs-REQUIRED sortie (default OPTIONAL).
- **Deferred to v2 (logged in DR-040):** arena-layout pool; zone-theme/depth byte + pre-gate HUD; the felt-beat replicated reward + `TimedModifier` buff; SaveData v6; mini-boss; RegionRelevancy incremental caching.
@@ -1,7 +1,7 @@
---
id: DR-040
title: Slice 3 — Expedition Combat Spine (v1 plan, reviewed + locked)
status: accepted
title: Slice 3 — Expedition Combat Spine (v1 plan, reviewed + locked → built)
status: built
date: 2026-06-18
tags:
- decision
@@ -52,4 +52,18 @@ Netcode: **PROCEED WITH BLOCKERS RESOLVED** (the chassis is sound + heavily reus
- **EditMode tests (planned):** region-filter target correctness; deterministic epoch scatter (same epoch → identical spawns); WaveSystem cleared-check ignores expedition enemies; zone-clear reward deposits once per epoch. **Play-validation:** zone enemies spawn `RegionTag{Expedition}` (visible to expedition player, NOT a base-only connection); kill them, return, retaliation siege arms; expedition player gets no base-Husk aggro; server==client; relevancy cost sane.
- **Deferred to v2 (logged):** arena-layout pool + authoring; zone-theme/depth byte + pre-gate HUD ("DEPTH 5 — BOSS"); the felt-beat replicated reward + `TimedModifier` buff; SaveData v6 (persist epoch); mini-boss; layout×encounter decoupling; sequential zones; RegionRelevancy incremental caching.
- **Open (operator):** the OPTIONAL-vs-REQUIRED sortie fork (default OPTIONAL); the Slice 3 fun-gate; the still-open Slice 1 + 2 fun-gates.
- **Status:** reviewed + locked, **NOT built** — the build is the immediate next unit (start with the new components + the three surgical server-count fixes, then the `EnemyAISystem` region-filter (the riskiest, test it), then `ZoneEnemySpawnSystem` + authoring + the reward, then tests + Play-validate, then commit). Full review (verdicts/blockers/forks/sources) in the run transcript `wf_b8033e26-1f5`.
- **Status:** **BUILT + verified 2026-06-21** (see build record below). Full pre-build review (verdicts/blockers/forks/sources) in the run transcript `wf_b8033e26-1f5`.
## Build record (2026-06-21)
Built per the locked plan; all four blockers landed. **368/368 EditMode green + a clean netcode Play smoke** (ServerWorld/ClientWorld boot with no sort-cycle, the subscene re-baked the `ZoneEnemyDirector` singleton + 2-prefab roster, real prefabs instantiate `RegionTag{Expedition}`+`ZoneEnemyTag` with baked Scale preserved, zero runtime errors).
**Deviations from the locked text (all deliberate, caught at grounding):**
- **System renamed `ZoneEnemyDirectorSystem`** (was `ZoneEnemySpawnSystem` in the plan).
- **Ordering is `[UpdateAfter(ExpeditionFieldSystem)]` ONLY** — the plan's extra `[UpdateBefore(CyclePhaseSystem)]` would close a `CyclePhase→Field→Zone→CyclePhase` sort cycle that throws at Play world-creation and is invisible to EditMode (ExpeditionFieldSystem is itself `UpdateAfter(CyclePhaseSystem)`). Grounding catch.
- `ZoneEnemyState.SeededEpoch` is **`int`** (not `uint`) to match `int CycleRuntime.ExpeditionEpoch`; `CycleRuntime` gained **both** `LastRewardedEpoch` (int) **and** `ClearedThisEpoch` (byte) — the director sets `ClearedThisEpoch=1` on a real clear, the gate pays `RewardOre` once per epoch on return.
**Post-impl adversarial review (run `wf_7b45e3c0-b81`, 3 lenses + synth) — 1 HIGH found + fixed:**
- **Region-blind Core-breach cull (a THIRD cull path beyond BLOCKER 3's two).** `CyclePhaseSystem`'s overrun/soft-loss branch despawned **all** `EnemyTag` entities — pre-slice that was safe (all enemies were base), but Slice 3 makes expedition enemies share `EnemyTag`, so a **base** Core breach wiped a live **expedition** wave AND spuriously tripped the zone director's `aliveZone==0` clear/reward edge. **Fix:** Base-only region filter matching the BLOCKER-3 siblings (`ThreatDirectorSystem` timeout + `DefendCleared`); dead `m_AliveHusks` field removed. Locked with regression test `CyclePhaseSystemTests.Base_Overrun_Disperses_Base_Husks_But_Spares_Expedition_Husks`. (Lesson: a cull/query that was region-safe before a region split must be re-audited the moment a second region shares its tag — same class the post-impl review caught in DR-031.)
**Still open (operator):** the OPTIONAL-vs-REQUIRED sortie fork (default OPTIONAL); the Slice 3 fun-gate (needs a hands-on playtest of the full walk-gate→fight→return→reward→siege loop — logic is unit-covered, feel is not).