Game Scene Split up

This commit is contained in:
2026-06-04 13:45:46 -07:00
parent dbc4a92a86
commit 16b01bec38
49 changed files with 11976 additions and 188 deletions
@@ -0,0 +1,64 @@
---
date: 2026-06-04
type: session
tags: [session, core-loop, pacing, netcode, dev-tools, scenes, m8]
---
# Session 2026-06-04 — M8: Persistent base & player-driven pacing
## Goal
Operator: *"I don't like the current pacing — the home base should feel persistent and like you aren't in a rush... when you are ready and have the inventory and prep you need for an expedition, you go. Attacks on the base will be triggered based on various events and timelines (possibly after an expedition). Built out properly — also make a separate scene and various dev tools to test/debug/validate (spawn waves, control game state, grant things). The main gameplay scene should feel like an actual game."*
Replaces the M6 **forced-timer treadmill** (Expedition 60s → Defend → Build 20s → repeat) with a **player-driven loop**: persistent **Calm** base by default → deploy when ready → **event-triggered Siege** (post-expedition retaliation) → defend → Calm. Decisions chosen by operator: **composite ThreatDirector**, **full staged build**, **new Game scene + DevSandbox**. Full rationale + the de-risked architecture: [[DR-017_Persistent_Base_Player_Driven_Pacing]].
## Process
- Phase-1 exploration (3 Explore agents: run-state/wave blast radius · scene/subscene/bootstrap wiring · dev-tools/RPC/test precedents).
- Phase-2 **adversarial design review** (Workflow: 3 critics — netcode/determinism · reuse/scope · dev-tooling/scene/build-gotchas → synthesis). Caught the **co-op-breaking global-Expedition-phase flaw** (one byte can't represent player-A-out/player-B-home) and steered: reuse-symbols-re-mean-bytes, atomic WaveState siege seed, server-only ThreatConfig/State, duplicate-scenes-not-scratch, unconditional RPC type.
- Plan approved → 5 staged steps, each `compile → read_console → EditMode → (Play where relevant)`.
## Done
### Stage 1 — Calm/Siege run-state + presence + boot-Calm + goal decouple
- `CycleComponents.cs`: re-meaned `CyclePhase``Calm=0`/`Siege=1` (+ deprecated `Expedition`/`Defend`/`Build` aliases; byte values unchanged → **no serializer re-bake**); `CycleRuntime``ExpeditionEpoch`/`LastSpawnedEpoch`/`PrevExpeditionOccupied`.
- `CyclePhaseSystem` rewritten to the **Calm↔Siege machine** (consume `ThreatState.PendingSiegeSize` → atomic `WaveState{Spawning, RemainingToSpawn=size, WaveNumber+1}` seed → Siege; exit on `DefendCleared` → Calm + goal `+1`). Boot = Calm (`CycleDirectorAuthoring` default + `CycleDirectorSpawnSystem` stamp `Phase=Calm, PhaseEndTick=0`, adds `ThreatState`). `ExpeditionFieldSystem` re-keyed off expedition-region presence + epoch. `ExpeditionGateSystem` hack deleted → increments `ThreatState.PendingReturns` on return. `WaveSystem` gate `!= Siege`.
- Tests: rewrote `CyclePhaseSystemTests` (Calm holds; PendingSiege→Siege exact size; Siege→Calm + goal once; **co-op split-presence keeps global phase Calm**); `ExpeditionGateSystemTests` → return-signals-once. **137 baseline preserved.**
### Stage 2 — Composite ThreatDirector
- `ThreatComponents.cs` (`ThreatConfig` + `ThreatState` + `ThreatStartCondition`, server-only, Heat/Schedule inert). `ThreatDirectorSystem` (Gate→ThreatDirector→CyclePhase order): post-expedition source arms a `SizeBase` siege after a telegraph; bounded-timeout collapse (no soft-lock). `ThreatConfig` baked via `CycleDirectorAuthoring` (inspector-tunable flat fields).
- Tests: `ThreatDirectorSystemTests` ×5 (return→pending; multi-return de-dup; size=config-not-curve; Immediate arms via ArmTick; empty-base auto-resolves bounded).
### Stage 3 — Dev tools over RPC + god-mode
- Unconditional `DebugCommandRequest`/`DebugOp` (Simulation); `DebugGodMode` enableable. `DebugCommandReceiveSystem` (server `#if UNITY_EDITOR` switch, reuses StorageMath/StatModifier/RegionMath/wave+cycle singletons, sender resolve). `DebugCommandSendSystem` (client static-queue → RPC, headless-friendly) + `DebugOverlay` MonoBehaviour (IMGUI). `HealthApplyDamageSystem` god-mode guard; `PlayerAuthoring` bakes `DebugGodMode` present+disabled; `AimPresentation.ForceCursorVisible` (overlay clickability); `[RuntimeInitializeOnLoadMethod]` resets.
- Tests: `DebugCommandReceiveSystemTests` ×4 + god-mode skip-damage. **137/137.**
### Stage 4 — Scenes
- Duplicated SampleScene → **`Game.unity`** (clean main, boots Calm) + **`DevSandbox.unity`** (Synty world disabled, `~DevTools`+overlay). Verified the SubScene `_SceneAsset` guid `9dc8ce2e…` + `_SceneGUID.x=3807153369` survived. `Game.unity` added to `EditorBuildSettings` index 0.
### Stage 5 — Feel pass
- `HudSystem`: Calm/Siege labels + colors, "AT BASE — deploy through the gate when you're ready" prompt, "INCURSION IN Ns" telegraph (reuses `PhaseEndTick=ArmTick` countdown), dropped vestigial "CYCLE N". `AmbientAudioSystem`: Calm/Siege stinger + drone swell mapping.
### Validation
- **EditMode 137/137** (was 127; rewrote/added per stage, no regressions), console clean.
- **Focused-editor Play (server+client), SampleScene + Game.unity:** boot **Calm** (`Phase=0, PhaseEndTick=0`); re-bakes confirmed (`ThreatConfig{SizeBase=5,Delay=300,Timeout=3600}`, player `DebugGodMode` present+disabled); armed siege → **Siege**, **exactly 4 Husks** spawned, `WaveState{Spawning,Remaining=0}`, pending consumed; **server==client** (Phase + husk ghosts replicated). `Game.unity` screenshot: calm base + "AT BASE — deploy…" HUD. (`Assets/Screenshots/M8_Game_Calm_Boot.png`.)
## Decisions
- **Created [[DR-017_Persistent_Base_Player_Driven_Pacing]]** (supersedes the M6 forced-timer rhythm of [[DR-013_M6_Aether_Cycle_Region_Split]]/[[DR-014_M6_Build_Structures_Automation_Foundation]]; reuses their netcode infra).
## Build gotchas (→ [[CLAUDE]] addendum)
- A **system-ordering cycle is invisible to plain-Entities EditMode tests** (systems registered individually); only `ComponentSystemSorter` throws at Play/world-creation. Re-audit *existing* `[Update*]` attributes when reordering — caught the gate's stale `[UpdateAfter(CyclePhaseSystem)]` vs the new ThreatDirector chain.
- **MCP `apply_text_edits` multi-edit-in-one-call can misalign** (a paired replace+delete hit the wrong line). One edit per call (or strict bottom-first), `precondition_sha256` always. `create_script` won't overwrite; `script_apply_edits replace_method` still can't target struct ISystems.
- **Re-mean bytes, don't rename**: unchanged byte values keep the `[GhostField]` serializer identical → a global-loop reframe stayed re-bake-free (only authoring default-value edits re-bake the subscene).
## Open / deferred
- Base-integrity / visible fail-state (siege currently just collapses on timeout); haul-scaled retaliation (`SizePerExpeditionResource=0`); Heat/Schedule trigger sources (baked-but-inert); dev overlay in dev *player* builds; deploy-gate 3D marker. All defaulted/tunable — see [[DR-017_Persistent_Base_Player_Driven_Pacing]].
## Next
- **Build/automation/customization expansion** on the now-persistent base (the operator's stated next direction) — the ThreatDirector inert sources + the DR-014 structure tick fields are the additive hooks.
- Multi-client (MPPM) run of the co-op split-presence + dev-tools-over-a-real-connection (the unconditional RPC type is the enabler).
- Optional: base-integrity fail-state if losing a siege should have visible teeth.
@@ -0,0 +1,56 @@
---
id: DR-017
title: M8 — Persistent base & player-driven pacing (Calm↔Siege run-state, composite ThreatDirector, dev-tools-over-RPC, Game/DevSandbox scenes)
status: accepted
date: 2026-06-04
tags:
- decision
- netcode
- core-loop
- pacing
- dev-tools
- world-architecture
- m8
permalink: gamevault/07-sessions/decisions/dr-017-persistent-base-player-driven-pacing
---
# DR-017 — Persistent base & player-driven pacing
## Context
The M6 core loop ([[DR-013_M6_Aether_Cycle_Region_Split]] / [[DR-014_M6_Build_Structures_Automation_Foundation]]) shipped a **Dome-Keeper forced-timer rhythm**: `CyclePhaseSystem` auto-advanced Expedition (~60s) → Defend (wave) → Build (~20s) → repeat, +1 goal/lap, booting straight into a 60-second Expedition timer. The operator disliked the pacing: **the home base should feel persistent and unhurried** — with build/automation/customization layered on, you should **deploy on an expedition when YOU are ready** (inventory + prep), not on a treadmill; **base attacks should be event/timeline-triggered** ("possibly after an expedition"), not every lap. Plus: a **separate dev scene + dev tools** (spawn waves, control state, grant resources/upgrades, teleport, god-mode), and the **main scene should feel like an actual game**.
Operator chose (this session): **composite, data-driven ThreatDirector** (post-expedition default-on); **full staged build**; a **new dedicated Game scene + a DevSandbox scene**. A 3-critic + synthesis design-review Workflow ran pre-code (per [[CLAUDE]]'s netcode-slice rule) and caught a co-op-breaking flaw + steered the de-risked shape below.
## Decision
1. **Global run-state = `{Calm, Siege}` only; "expedition" is per-player presence (the co-op fix).** A single global `CycleState.Phase` byte cannot represent "player A out / player B home", so it carries only the shared posture: **`Calm`** (persistent, unhurried DEFAULT — build/prep, no countdown) or **`Siege`** (event-triggered base-defense wave). Being out is per-player (server-only `RegionTag`, read client-side by the HUD's `Camera.main.x > 500`). **Reuse symbols, re-mean bytes — no rename**: `CyclePhase.Calm = 0` (was Expedition), `Siege = 1` (was Defend), `Build = 2` retired; deprecated aliases kept so HUD/audio/tests compile through the cut-over. **Byte values are unchanged → the GhostField serializer layout is identical → the const re-mean forces no re-bake** (boot-into-Calm comes from the spawn-stamp + baker default, both byte 0). `CyclePhaseSystem` (name kept, behaviour rewritten) is the Calm↔Siege machine; boot = Calm (`CycleDirectorSpawnSystem` stamps `Phase=Calm, PhaseEndTick=0`). `ExpeditionFieldSystem` re-keyed off **expedition-region player presence + a server-only `ExpeditionEpoch`** (RNG seed; field lives while anyone is out, torn down when the last leaves) — replaces the global-phase edge + `CycleNumber`-as-seed.
2. **Composite ThreatDirector (server-only, data-driven, extensible).** Two **server-only** components on the global CycleDirector ghost (neither a `[GhostField]` → no ghost re-bake; future sources are additive): **`ThreatConfig`** (baked flat scalars via `CycleDirectorAuthoring`: PostExpedition enable/delay/sizeBase/sizePerResource, StartCondition, SiegeTimeoutTicks; Heat/Schedule fields **present-but-inert**) + **`ThreatState`** runtime (`PendingSiegeSize` = the single documented entry point, `ArmTick`, `PendingReturns`, `ExpeditionsCompleted`, `SiegeStartTick`, inert `Heat`/`NextScheduledTick`). **`ThreatDirectorSystem`** (plain server `SimulationSystemGroup`, ordered **Gate → ThreatDirector → CyclePhaseSystem → Wave**) consumes the gate's per-player `PendingReturns` (de-duped: the gate teleports the returner out of its radius) and arms a siege `SizeBase` after a telegraph delay; `StartCondition.Immediate` is the default (`RequirePlayerAtBase` would soft-lock an empty base). A **bounded-resolution timeout** culls the wave after `SiegeTimeoutTicks` so an unattended/empty-base siege can never soft-lock. **Siege sizing rides WaveSystem's own `Lull→Spawning` entry**: on the Calm→Siege edge `CyclePhaseSystem` writes `WaveState{ WaveNumber+1, Phase=Spawning, RemainingToSpawn=size, NextActionTick=now }` in one atomic `SetComponent` (Phase=Spawning bypasses WaveSystem's `(WaveNumber-1)*CountPerWave` escalation recompute, which only runs while `Phase==Lull`), so the siege spawns EXACTLY the director-chosen size and WaveSystem stays the sole `WaveState` writer thereafter — **no new SiegeState component**. **Goal decoupled**: `GoalProgress.Charge += 1` per **siege survived** (single global edge, co-op-safe), moved from the retired Build→Expedition edge.
3. **Dev tools over RPC (in-editor now; connection-ready).** A single **unconditional** wire type `DebugCommandRequest : IRpcCommand { byte Op; int ArgA; int ArgB }` (+ `DebugOp` byte consts) — **never `#if`-gated** (the reflection-built RpcCollection hash must match across release/dev peers; gating the *type* breaks the handshake). Only the systems/overlay are `#if UNITY_EDITOR`: `DebugCommandSendSystem` (client, static-queue → request entities, like `StorageOpSendSystem`; also drives headless from `execute_code`), `DebugCommandReceiveSystem` (server switch, plain `SimulationSystemGroup`, reusing `StorageMath.Deposit` / `StatModifier` replace-by-SourceId / `RegionMath` / the wave+cycle singletons; sender-targeted ops resolve `SourceConnection → NetworkId → GhostOwner`), and a `DebugOverlay` MonoBehaviour (IMGUI `OnGUI`, mirroring `ConnectionUI`). Ops: SpawnWave/EndSiege/ClearEnemies/SetCalm/GrantResource/GrantUpgrade/Teleport/ToggleGod/Heal/Kill/AdvanceGoal/SetHeat. **God-mode** = server-only enableable `DebugGodMode` baked **present+DISABLED** on the player prefab (mirrors `Dead`; bit-flip, no structural change), guarded in `HealthApplyDamageSystem` beside the `RespawnInvuln` early-out. The overlay forces the OS cursor visible (`AimPresentation.ForceCursorVisible`, non-#if) so its buttons stay clickable while aiming; all debug statics reset via `[RuntimeInitializeOnLoadMethod]`.
4. **Dedicated Game scene + DevSandbox, built by DUPLICATION.** `manage_asset duplicate` SampleScene → **`Game.unity`** (clean main scene; boots Calm; kept Synty world + post-FX + glue) and → **`DevSandbox.unity`** (Synty world disabled, `~DevTools` GameObject with the overlay). Duplication (a file copy) preserves the `GameplaySubScene` `Unity.Scenes.SubScene` `_SceneAsset` guid + non-zero `_SceneGUID` Hash128 verbatim — MCP `component_properties` silently drops Hash128, so a scratch rebuild would bake nothing. Both reference the SAME `Gameplay.unity` subscene. `Game.unity` added to `EditorBuildSettings` at **index 0** (a player build boots Game). HUD/audio re-meaned to the Calm/Siege semantics (deploy prompt, incursion telegraph reusing the `PhaseEndTick=ArmTick` countdown).
## Consequences
- **Validated:** EditMode **137/137** green (rewrote `CyclePhaseSystemTests`; +`ThreatDirectorSystemTests` ×5, +`DebugCommandReceiveSystemTests` ×4, +god-mode skip-damage, +gate return-signal; incl. a co-op split-presence invariant test). **Focused-editor Play (server+client):** boots into **Calm** (`Phase=0, PhaseEndTick=0`, no countdown); the Stage-2/3 re-bakes landed (`ThreatConfig{SizeBase=5,Delay=300,Timeout=3600}` on the director, `DebugGodMode` present+disabled on the player); arming a siege → `CyclePhaseSystem`**Siege**, WaveSystem spawned **exactly 4** Husks (`WaveState{Spawning, Remaining=0, WaveNum=1}`), `ThreatState{Pending=0, SiegeStart=…}`; **server==client** (Phase + 4 Husk ghosts replicated; client connecting confirms the unconditional RPC type didn't break the handshake). `Game.unity` boots into the calm base with the new HUD ("AT BASE — deploy through the gate when you're ready"). Console clean (only the pre-existing in-editor Server-Tick-Batching warning).
- **No new asmdefs.** New code under `…/World/` (`ThreatComponents`, `ThreatDirectorSystem`), `…/Debug/` (Simulation `DebugCommandRequest`/`DebugGodMode`; Server `DebugCommandReceiveSystem`; Client `DebugCommandSendSystem`/`DebugOverlay`). Supersedes the M6 forced-timer rhythm; reuses region split / GhostRelevancy / runtime-ghost / tick-safe math / RPC recipe / gates / ledger verbatim.
- **Foundation for the build/automation expansion**: the persistent calm base is now the default state with room for the upcoming building/automation/customization layer; the ThreatDirector's inert Heat/Schedule sources + the structure tick fields ([[DR-014_M6_Build_Structures_Automation_Foundation]]) are the additive hooks.
## Open / deferred (defaulted, tunable)
- **Empty-base / unattended-siege "teeth"**: this slice ships the bounded-timeout collapse (no soft-lock) but **no base-integrity stat / fail-state** yet — defaulted to "siege just collapses"; a visible base-integrity HUD bar is a follow-up (would fold one `[GhostField]`).
- **Haul-scaled retaliation**: `SizePerExpeditionResource` baked but ships **0** (flat sieges); enabling is a one-line tuning change, no re-bake.
- **Heat + Schedule trigger sources**: config fields baked-but-inert; drop in additively (server-only, no re-bake).
- **Dev overlay in dev *player* builds**: `#if UNITY_EDITOR` only this slice (no `DEVELOPMENT_BUILD` configured); the wire type stays unconditional so switching later is just the systems' guard.
- **Deploy-gate world marker / top-down DevSandbox cam**: the deploy affordance is HUD-text for now; a 3D gate marker is polish.
## Build gotchas recorded this session
- **A system-ordering cycle is NOT caught by plain-Entities EditMode tests** (they register systems individually) — it only throws `ComponentSystemSorter` "circular dependency" at **world creation (Play)**. Caught here: a new system's `[UpdateAfter(A)]+[UpdateBefore(B)]` collided with B's pre-existing `[UpdateAfter(A)]`. Always Play-validate after adding cross-system `[Update*]` attributes; re-audit the *existing* attributes of every system you reorder around.
- **MCP `apply_text_edits` with MULTIPLE non-adjacent edits in one call can misalign** (observed: a paired replace+delete deleted the line *above* the intended one). Do one edit per call (or strictly bottom-first, verified), with `precondition_sha256`. The structured `script_apply_edits` (`insert_method`, `replace_method`) is safer for class methods — but `replace_method` still can't target `struct : ISystem`.
- **`create_script` does not overwrite** an existing path (errors); `manage_script` has only create/read/delete. Full-file rewrites = `apply_text_edits` over the whole span, or delete+create for non-GUID-referenced files (systems/tests). Its brace-balance validator will reject a botched span — a useful guard.
- **Re-mean-bytes-don't-rename** kept a global-loop reframe re-bake-free: an enum/const whose byte values are unchanged leaves the `[GhostField]` serializer identical, so only authoring *default-value* edits (not the const re-mean) trigger a subscene re-bake.
Builds on + supersedes the forced-rhythm framing of [[DR-013_M6_Aether_Cycle_Region_Split]] / [[DR-014_M6_Build_Structures_Automation_Foundation]]; reuses the RPC/runtime-ghost/tick-safe patterns from [[DR-008_M5_HomeBase_BaseLayer_Storage]] and the StatModifier path from [[DR-004_M3_DataDriven_Abilities_Modifiers]]. Serves the persistent-base + player-driven pillars in [[Pillars]].