Game Scene Split up
This commit is contained in:
@@ -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]].
|
||||
Reference in New Issue
Block a user