@@ -9,7 +9,7 @@ Multiplayer game on **Unity DOTS (Entities) + Netcode for Entities** — server-
- **Size check** — bash: `wc -c CLAUDE.md` · PowerShell: `(Get-Item CLAUDE.md).Length`. Must be `< 40960`.
- **Size check** — bash: `wc -c CLAUDE.md` · PowerShell: `(Get-Item CLAUDE.md).Length`. Must be `< 40960`.
- **Archive, don't delete.** When trimming, append the verbose / least-hot detail to the obsidian reference note `Docs/Vault/_Meta/CLAUDE_Build_Gotchas_Archive.md` under a **new dated heading** (never overwrite an older snapshot), and leave a one-line pointer + the relevant `[[DR-###]]` link here.
- **Archive, don't delete.** When trimming, append the verbose / least-hot detail to the obsidian reference note `Docs/Vault/_Meta/CLAUDE_Build_Gotchas_Archive.md` under a **new dated heading** (never overwrite an older snapshot), and leave a one-line pointer + the relevant `[[DR-###]]` link here.
- **Net-zero rule:** every addition is paid for by a condensation elsewhere. Keep only the hottest, highest-recurrence operational rules inline (flag them **★**); depth lives in the archive + DRs.
- **Net-zero rule:** every addition is paid for by a condensation elsewhere. Keep only the hottest, highest-recurrence operational rules inline (flag them **★**); depth lives in the archive + DRs.
## Stack — Unity 6.5.1 (`6000.5.1f1`, stable) as of 2026-06-27
## Stack — Unity 6.5.1 (`6000.5.1f1`, stable) as of 2026-06-27
@@ -74,7 +74,7 @@ Long-form originals + the milestone each came from: `Docs/Vault/_Meta/CLAUDE_Bui
- **Derive enableable gates instead of replicating them.** e.g. player `Dead` = a LOCAL enableable derived every predicted tick from replicated `Health<=0` (rollback-correct, no `[GhostEnabledBit]`). To write the bit on a disabled entity the query must visit it (`.WithPresent<Dead>()`); **bake the enableable DISABLED** so instances spawn off. Respawn/death *timing* is server-only.
- **Derive enableable gates instead of replicating them.** e.g. player `Dead` = a LOCAL enableable derived every predicted tick from replicated `Health<=0` (rollback-correct, no `[GhostEnabledBit]`). To write the bit on a disabled entity the query must visit it (`.WithPresent<Dead>()`); **bake the enableable DISABLED** so instances spawn off. Respawn/death *timing* is server-only.
- **Cooldown/spawn "next tick" sentinels:** route every stored tick through **`TickUtil.NonZero(...)`** (a computed `ServerTick+delay` can wrap to 0, the "ready" sentinel) and compare with `NetworkTick.IsNewerThan` / `.TicksSince`, **never** raw `uint <` / subtraction. **★ A BAKED `[GhostField]` scheduled-tick defaults to 0 → the "invalid-tick ⇒ fire" guard that's safe for a runtime-ADDED fuse STORMS it (0 fails `IsValid`, falls through, fires every tick); for a baked/periodic tick INVERT it (0 = not-ready → skip/lazy-stamp) + stamp born-correct at spawn. Client cues off a periodic tick ride the ABSOLUTE tick + a value-latch + a was-counting-down arm-guard — never edge-detect the field increment (phantom-fires on `0→stamp` + relevancy re-entry).** See [[Geyser_Build_Spec]].
- **Cooldown/spawn "next tick" sentinels:** route every stored tick through **`TickUtil.NonZero(...)`** (a computed `ServerTick+delay` can wrap to 0, the "ready" sentinel) and compare with `NetworkTick.IsNewerThan` / `.TicksSince`, **never** raw `uint <` / subtraction. **★ A BAKED `[GhostField]` scheduled-tick defaults to 0 → the "invalid-tick ⇒ fire" guard that's safe for a runtime-ADDED fuse STORMS it (0 fails `IsValid`, falls through, fires every tick); for a baked/periodic tick INVERT it (0 = not-ready → skip/lazy-stamp) + stamp born-correct at spawn. Client cues off a periodic tick ride the ABSOLUTE tick + a value-latch + a was-counting-down arm-guard — never edge-detect the field increment (phantom-fires on `0→stamp` + relevancy re-entry).** See [[Geyser_Build_Spec]].
- **`GhostRelevancy` for region splits:** use `GhostRelevancyMode.SetIsIrrelevant` (not `SetIsRelevant`) so untagged/global ghosts stay relevant for free — only enumerate cross-region ghosts to hide. `RegionTag{byte Region}` is **server-only, NOT a `[GhostField]`**. **★ A 2nd region sharing an EXISTING tag (`EnemyTag`) → re-audit every query/cull over it: once-safe global despawns/cleared-checks then wipe or block cross-region (DR-031, DR-040).** `RelevantGhostForConnection` = `{int Connection=NetworkId.Value; int Ghost=ghostId}`. See [[DR-013_M6_Aether_Cycle_Region_Split]].
- **`GhostRelevancy` for region splits:** use `GhostRelevancyMode.SetIsIrrelevant` (not `SetIsRelevant`) so untagged/global ghosts stay relevant for free — only enumerate cross-region ghosts to hide. `RegionTag{byte Region}` is **server-only, NOT a `[GhostField]`**. **★ A 2nd region sharing an EXISTING tag (`EnemyTag`) → re-audit every query/cull over it: once-safe global despawns/cleared-checks then wipe or block cross-region (DR-031, DR-040).** `RelevantGhostForConnection` = `{int Connection=NetworkId.Value; int Ghost=ghostId}`. See [[DR-013_M6_Aether_Cycle_Region_Split]].
- **Shared GLOBAL state (cycle phase, resource ledger, goal meter) rides an UNTAGGED ghost**, never a region-tagged one (`SetIsIrrelevant` would hide it cross-region). Resolve the ledger via its DISTINCT `ResourceLedger` tag (the multi-`StorageEntry` "multiple instances" rule — EB-2 line).
- **Shared GLOBAL state (resource ledger, `RunInfo`, meta tiers) rides the UNTAGGED director ghost**, never a region-tagged one (`SetIsIrrelevant` would hide it cross-region). Resolve the ledger via its DISTINCT `ResourceLedger` tag (the multi-`StorageEntry` "multiple instances" rule).
- **Frontend world lifecycle (menu → on-demand worlds) ★:** use `CreateClientWorld`/`CreateServerWorld` (they register the `ServerWorld`/`ClientWorld` statics the UI reads; `CreateLocalWorld` was internal pre-6.5, PUBLIC on 6.5.0); menu world via `DefaultWorldInitialization.Initialize(name, false)`. **Never dispose/create worlds inside an ECS system** — do it on a frame-boundary coroutine (`SessionRunner`, `DontDestroyOnLoad`). The gameplay subscene streams in ONLY if a netcode world is the `DefaultGameObjectInjectionWorld` at `LoadScene` time. See [[DR-019_Frontend_Menu_Settings_Saves_Build]].
- **Frontend world lifecycle (menu → on-demand worlds) ★:** use `CreateClientWorld`/`CreateServerWorld` (they register the `ServerWorld`/`ClientWorld` statics the UI reads; `CreateLocalWorld` was internal pre-6.5, PUBLIC on 6.5.0); menu world via `DefaultWorldInitialization.Initialize(name, false)`. **Never dispose/create worlds inside an ECS system** — do it on a frame-boundary coroutine (`SessionRunner`, `DontDestroyOnLoad`). The gameplay subscene streams in ONLY if a netcode world is the `DefaultGameObjectInjectionWorld` at `LoadScene` time. See [[DR-019_Frontend_Menu_Settings_Saves_Build]].
### Physics & character controller
### Physics & character controller
@@ -86,13 +86,11 @@ Long-form originals + the milestone each came from: `Docs/Vault/_Meta/CLAUDE_Bui
### Build / structures / grid
### Build / structures / grid
- **Build-grid math must be deterministic + integer-stable:** corner-origin, center-returning, **half-open** cell bounds, `math.floor`. Lock `CellSize`/`PlotSize` as a coordinate space once (`BaseGridMath`) — changing them invalidates placed structures.
- **Build-grid math must be deterministic + integer-stable:** corner-origin, center-returning, **half-open** cell bounds, `math.floor`. Lock `CellSize`/`PlotSize` as a coordinate space once (`BaseGridMath`) — changing them invalidates placed structures.
- **Structures:** bake the tick fields (catch-up linchpin); only `Type` replicates (client derives `Cell`); **occupancy is DERIVED** from live ghosts, never baked. See [[DR-014_M6_Build_Structures_Automation_Foundation]].
- **Structures:** only `Type` replicates (client derives `Cell`); **occupancy is DERIVED** from live ghosts, never baked. See [[DR-014_M6_Build_Structures_Automation_Foundation]].
- **Co-op placement atomicity:** commit `StorageMath.Withdraw` + cell-reservation **in-place in the RPC foreach** (only `Instantiate` via ECB) so two same-tick requests for one cell can't both pass.
- **Co-op placement atomicity:** commit `StorageMath.Withdraw` + cell-reservation **in-place in the RPC foreach** (only `Instantiate` via ECB) so two same-tick requests for one cell can't both pass. Ledger spends generally: afford→act else SOFT-FAIL (no cooldown-burn), read LIVE in-loop (no hoist); a Health-less entity silently drops OUT of an aggro snapshot (snapshot ABOVE the early-return).
- **Siege-era systems ★ (EB-1/2 · END-1/2 — mothball-bound at Phase 2; these INVARIANTS outlive them):** ★ a Health-less machine silently drops OUT of the aggro snapshot (snapshot ABOVE the early-return; aggro weight <1 gets SQUARED) · spend from the ONE `GetSingletonEntity<ResourceLedger>`, afford→act else SOFT-FAIL (no cooldown-burn), read LIVE in-loop (no hoist) · `CyclePhaseSystem` = SOLE Phase writer (loss edges live there; transient flags, NOT latching) · **`RunOutcome{[GhostField] byte}` is REPLICATED — never client-derive the win/lose banner.** [[DR-032_EB1_Machines_Can_Die]] · [[DR-033_EB2_Felt_Spend_Charge_Economy]] · [[DR-034_END1_Losable_Core]] · [[DR-036_END2_Final_Siege_Win_Lose]].
- **DR-051 purge (07-15) ★:** siege/cycle/core/turret/automation + legacy `AbilityRef` path + onboarding **DELETED** (git = the archive; retired bullets → gotchas archive 07-15). **Retired byte VALUES stay reserved, never renumbered** (`StructureType` 1-4, `ResourceId.Charge`, `DebugOp` 3/10/11, `TuningKnob` 20-23); `DebugOp.SpawnWave`/`EndSiege` RE-MEANT (force-wave / quiet-arena). **Waves UNGATED** — a baked `WaveDirectorAuthoring` decides by placement. Sockets are THE ability model (frame loadout seeded unconditionally at spawn); `CharacterId`→`FrameKind`. [[DR-051_Lantern_Realignment_Purge]].
- **`GoalProgress{[GhostField] int Charge,Target}`** (win meter — ≠ the `ResourceId.Charge` ammo) rides the CycleDirector ghost; buffs/tiers reuse `StatModifier`→`EffectiveAbilityStats`.
- **Disk persistence (`SaveData`, single-slot atomic JSON, versioned) ★:** **born-correct load** — `CycleDirectorSpawnSystem`(the ledger/RunInfo/meta host) applies the menu-staged`PendingSave` AT SPAWN; `BaseRestoreSystem` replays structures charge-free + HP. **v7 = a FRESH EPOCH: `MinLoadableVersion= CurrentVersion = 7`** (older saves rejected → New Game); additive again going forward. `RunDirectorSystem`'s terminal bank is the sole autosave trigger. See [[DR-019_Frontend_Menu_Settings_Saves_Build]] + [[DR-051_Lantern_Realignment_Purge]].
### Presentation / juice / VFX
### Presentation / juice / VFX
- **All juice/HUD = client-only observe-only `SystemBase` in `PresentationSystemGroup`** (once/frame, no rollback double-fire), never mutates the sim. Read ECS via `SystemAPI.Query` + `EntityManager.CompleteDependencyBeforeRO<T>()` — NOT MonoBehaviour `LateUpdate` (job-safety throw). `Entity` = a stable client dict key per ghost lifetime — **prune the cache each frame** (a pruned ghost = a kill/loss → death VFX); **never `DestroyEntity` a ghost client-side** (`GhostDespawnSystem` owns despawn). Hit-stop = camera punch, **never `Time.timeScale`**.
- **All juice/HUD = client-only observe-only `SystemBase` in `PresentationSystemGroup`** (once/frame, no rollback double-fire), never mutates the sim. Read ECS via `SystemAPI.Query` + `EntityManager.CompleteDependencyBeforeRO<T>()` — NOT MonoBehaviour `LateUpdate` (job-safety throw). `Entity` = a stable client dict key per ghost lifetime — **prune the cache each frame** (a pruned ghost = a kill/loss → death VFX); **never `DestroyEntity` a ghost client-side** (`GhostDespawnSystem` owns despawn). Hit-stop = camera punch, **never `Time.timeScale`**.
@@ -103,7 +101,7 @@ Long-form originals + the milestone each came from: `Docs/Vault/_Meta/CLAUDE_Bui
### Art import (HDRP store packs → URP)
### Art import (HDRP store packs → URP)
- BefourStudios HDRP art: convert via `EnvArtTools.cs`, never switch pipelines (HDRP breaks EG); Synty = URP-native → archive 2026-07-06.
- BefourStudios HDRP art: convert via `EnvArtTools.cs`, never switch pipelines (HDRP breaks EG); Synty = URP-native → archive 2026-07-06.
- **World = the LANTERN murk ★ (DR-051; Synty biomes deleted):** ONE look — `PostFX_Lantern.asset` (ACES; needs URP HDR grading) + `Env_SeabedKit.prefab`(ArtStaging-sourced) + unified RenderSettings (NO skybox; Exp² teal fog {0.02,0.10,0.12}; flat ambient {0.03,0.055,0.08}; density knob 0.035 play / 0.075 staging; camera clearFlags **SolidColor deep-water** — else no-skybox corners bleed blue). `ScenePolicy.IsGameplayScene()` gates the dynamic-look systems — never re-add `scene.name` string checks. `WorldAtmosphereSystem` = water-column murk.
- **A dark-lit screenshot MASKS material bugs — verify material *values*.** `shader.GetPropertyType(idx)`-guard before `GetColor`/`GetFloat`/`GetTexture` (`S_General`'s `_BaseColorMultiply` is a float → `GetColor` returns black). Gate emission on the `_Emissive` flag + a fixture name; keep converted env metallic low (0.1–0.2).
- **A dark-lit screenshot MASKS material bugs — verify material *values*.** `shader.GetPropertyType(idx)`-guard before `GetColor`/`GetFloat`/`GetTexture` (`S_General`'s `_BaseColorMultiply` is a float → `GetColor` returns black). Gate emission on the `_Emissive` flag + a fixture name; keep converted env metallic low (0.1–0.2).
- **An EG per-instance tint (`URPMaterialPropertyBaseColor`) darkens a ghost ONLY if the shader's `_BaseColor` is Hybrid-Per-Instance** (ShaderGraph `overrideHLSLDeclaration:true`+`hlslDeclarationOverride:2`). Enemies flash because `AnimatedLitShader` is DOTS-authored; a **stock Synty prop graph (`Generic_Basic`) is Unity-Per-Material → the override RENDERS but silently no-ops.** Check the graph before planning a per-instance tint on a baked/prop mesh; else use procedural decal quads (07-12 cover damage-cracks, Part P).
- **An EG per-instance tint (`URPMaterialPropertyBaseColor`) darkens a ghost ONLY if the shader's `_BaseColor` is Hybrid-Per-Instance** (ShaderGraph `overrideHLSLDeclaration:true`+`hlslDeclarationOverride:2`). Enemies flash because `AnimatedLitShader` is DOTS-authored; a **stock Synty prop graph (`Generic_Basic`) is Unity-Per-Material → the override RENDERS but silently no-ops.** Check the graph before planning a per-instance tint on a baked/prop mesh; else use procedural decal quads (07-12 cover damage-cracks, Part P).
@@ -137,7 +135,7 @@ Full rationale: [[DR-022_Animation_Pipeline_Rukhanka_Synty]] · [[DR-023_Enemy_A
## Bootstrap & worlds
## Bootstrap & worlds
-`ProjectM.Simulation.GameBootstrap : ClientServerBootstrap` overrides `Initialize` with `AutoConnectPort = 0` (M4 — listen/connect is explicit via the `ConnectionConfig` singleton + per-world ConnectionControlSystems). **Editor default = instant-into-game + MPPM** (creates `ServerWorld` (`WorldFlags.GameServer`) + `ClientWorld` (`WorldFlags.GameClient`)); the `ProjectM/Boot Into Menu (Editor)` EditorPref flips the MAIN editor to the frontend path. **Player builds boot the UITK frontend menu** (`return false` → one menu world, no netcode worlds until a menu choice). See [[DR-019_Frontend_Menu_Settings_Saves_Build]].
-`ProjectM.Simulation.GameBootstrap : ClientServerBootstrap` overrides `Initialize` with `AutoConnectPort = 0` (M4 — listen/connect is explicit via the `ConnectionConfig` singleton + per-world ConnectionControlSystems). **Editor default = instant-into-game + MPPM** (creates `ServerWorld` (`WorldFlags.GameServer`) + `ClientWorld` (`WorldFlags.GameClient`)); the `ProjectM/Boot Into Menu (Editor)` EditorPref flips the MAIN editor to the frontend path. **Player builds boot the UITK frontend menu** (`return false` → one menu world, no netcode worlds until a menu choice). See [[DR-019_Frontend_Menu_Settings_Saves_Build]].
- **Scenes:** `Assets/Scenes/MainMenu.unity` (build index 0) boots the UITK frontend (menu world only); `Assets/Scenes/Game.unity` (index 1) holds gameplay with `Assets/_Project/Subscenes/Gameplay.unity` wired in as the baked subscene (GameObject `GameplaySubScene`). `SampleScene`/`DevSandbox` are kept as reference/dev scenes. The on-demand lifecycle (`WorldLauncher`/`SessionRunner`/`MainMenuController`) creates the right worlds per menu choice (Single/Host/Join), THEN `LoadScene(Game)` (subscene-streaming rule above).
- **Scenes (the DR-051 contract — exactly these four):** `MainMenu.unity` (build 0, UITK frontend) · `Game.unity` (build 1, the seabed arena; subscene`Gameplay.unity`) · `DevSandbox.unity` (renamed from Gym; dev tooling + subscene `GymSub.unity`; the `DebugOverlay`/F1-F2 dev scripts gate on this scene NAME) · `ArtStaging.unity`(art viewing, no player; the look's source of truth). All share the LANTERN look (see World bullet). The on-demand lifecycle (`WorldLauncher`/`SessionRunner`/`MainMenuController`) creates the right worlds per menu choice (Single/Host/Join), THEN `LoadScene(Game)` (subscene-streaming rule above).
- **Direction = LANTERN ★ — pivot LOCKED 2026-07-13 ([[DR-048_Lantern_Adoption_Full_Pivot]]).** Co-op action-RPG, *light is territory* (seed-pinned pocket-graph; SoD manual-aim skillshots; suit-frames + Sparks + wild mutations). Supersedes the Awakening-Engine fiction + the Co-op Hades iteration. Operative roadmap [[Roadmap_Lantern_Slice]]; **existing code (combat feel, ability/boon plumbing, run/hub lifecycle, save, regions/relevancy) is QUARRY, not foundation** — keep/rework/mothball per [[Lantern_Strip_Mothball_Inventory]]. **No world code until the ★review-first world-model spike ([[Lantern_World_Model_Spike]]) passes its design review.** The prior co-op-Hades core-loop (ready-check multi-room RUNS; `RunDirectorSystem`/`BossState`; DR-044/045/046) is salvage — invariants archived 07-13 in the gotchas archive. ★ **general gotcha kept: a serialized prefab bool ignores the C# initializer — flip the value in the prefab.**
- **Direction = LANTERN ★ — pivot LOCKED 2026-07-13 ([[DR-048_Lantern_Adoption_Full_Pivot]]).** Co-op action-RPG, *light is territory* (seed-pinned pocket-graph; SoD manual-aim skillshots; suit-frames + Sparks + wild mutations). Supersedes the Awakening-Engine fiction + the Co-op Hades iteration. Operative roadmap [[Roadmap_Lantern_Slice]]; **existing code (combat feel, ability/boon plumbing, run/hub lifecycle, save, regions/relevancy) is QUARRY, not foundation** — keep/rework/mothball per [[Lantern_Strip_Mothball_Inventory]]. **No world code until the ★review-first world-model spike ([[Lantern_World_Model_Spike]]) passes its design review.** The prior co-op-Hades core-loop (ready-check multi-room RUNS; `RunDirectorSystem`/`BossState`; DR-044/045/046) is salvage — invariants archived 07-13 in the gotchas archive. ★ **general gotcha kept: a serialized prefab bool ignores the C# initializer — flip the value in the prefab.**
Operator brief: the re-enabled Synty world didn't match ArtStaging; old systems/scenes kept getting in the way. "The new direction is absolute — don't worry about preserving stuff. Exactly these scenes: DevSandbox (Gym renamed, old one deleted), ArtStaging, Game, MainMenu — all one look. Then the new character model, fully in, so movement/animation tuning can start."
Three plan-mode forks locked by the operator: **delete outright** (not mothball) · **Game = LANTERN seabed arena** · **legacy AbilityRef/CharacterId refactor INCLUDED**.
## Shipped (10 commits on main, every one compile-clean + tests green)
| Commit | What |
|---|---|
| `511e78556` | Checkpoint of the operator's working tree (Gym→GymSub repoint, SampleScene deletion, ai.assistant pkg re-added) |
- **The misalign hazard is alive**: two apply_text_edits landed on wrong spans mid-session (BuildSendSystem orphan fragment; RunDirectorSystem double-`if`) — caught by the re-read-neighbors habit both times. The span validator also false-positives ("duplicate ResourceSprite", tuple-deconstruction "unbalanced braces") — `replace_method`/`delete_method` are the reliable fallback; `validate:"basic"` clears some but not all.
- **Deleting a component file can take a KEEPER with it**: `RuntimePlacedTag` (save marker) lived inside AutomationComponents.cs. Grep the whole file's declarations before deleting "a dead system's components".
- **"Frozen enemies" that aren't**: AFK-player samples caught identical enemy positions + flat HP across 60s and looked like the no-pathfinding freeze — it was mid-cycle sampling; the player had died and respawned between samples. Check respawn-invuln ticks before diagnosing a freeze.
- **Synty character prefabs are 20-SMR variant containers** — a flatten copies ALL of them; only the active ones matter. Strip inactive SMRs after the flatten (17 removed from Player.prefab).
- **Removing the skybox exposes camera clearFlags** — corners render the default blue; set SolidColor + deep-water background wherever the LANTERN look lands.
- The renamed DevSandbox **revived**`DebugOverlay`/`PixelArtDevControls`/`ClassSwitchHotkeySystem` (they gate on the "DevSandbox" scene name and had been dead in Gym).
## Verified end-state
- Game (Play): murk fog/ambient live via the retuned atmosphere system, seabed kit + warm/cold pools, Drowner wave spawns → seeks → strikes → kills the AFK player → respawn. Zero errors.
**Movement/animation tuning** (the operator's stated priority — now unblocked): CC feel knobs + locomotion blend + clip pass on the suit, live TuningConfig knobs already in the overlay. Follow-ups parked in DR-051 (EnemyRigTools templates, suit attachments via /art-dev, fog-density taste pass, HUD vocab).
Post-DR-048 the repo still half-spoke the old direction: three visual regimes (Game daylight-desert · Gym/DevSandbox ACES-scifi · ArtStaging murk-without-postfx), `WorldAtmosphereSystem` repainting Game to meadow/amber every frame, six presentation systems string-gated to `scene.name == "Game"`, two overlapping dev scenes, and the dead siege/cycle/automation web resurfacing in every search. The operator declared the new direction absolute ("don't worry about preserving stuff") and asked for a realignment so progress stops being a fight. Plan approved 2026-07-15; all parts shipped same-session.
## Decisions
1.**Delete, don't mothball** (operator-confirmed, overrides the inventory's reversibility stance): the MOTHBALL-verdict systems are GONE — automation chain (Harvester/Conveyor/Fabricator + ProductionMath), EB-2 turret defense, END-1 losable Core, END-2 win/lose (GoalProgress/RunOutcome/RunPhase), CyclePhaseSystem/CycleState/CycleRuntime, ThreatDirector/ThreatState/ThreatConfig, the whole first-run Onboarding slice. Git history is the archive. REWORK-verdict quarry (boons, run lifecycle, regions/relevancy, save HOW, build/grid, ledger/harvest, meta shop) survives.
2.**Wire-compat discipline held through deletion**: byte VALUES stay reserved, never renumbered — `StructureType` 1-4, `ResourceId.Charge`=4, `DebugOp` 3/10/11, `TuningKnob` 20-23. `DebugOp.SpawnWave`(0) and `EndSiege`(1) are RE-MEANT (force-next-wave / quiet-the-arena) on the same bytes.
3.**Waves are UNGATED** — `WaveSystem` runs wherever a `WaveDirectorAuthoring` is baked (placement decides; GymSub has none). The old CycleState-Siege gate died with the cycle.
4.**Save epoch v7, fresh** (operator pre-approved): `MinLoadableVersion = CurrentVersion = 7`; goal/core/outcome + conveyor/machine-IO fields dropped; `RollTerminalCampaignForward` deleted. `RunDirectorSystem`'s terminal bank is now the sole autosave trigger.
5.**Sockets are THE ability model**: legacy `AbilityRef`/`AbilityCooldown`/`EffectiveAbilityStats`/`DefaultAbility` deleted; `GoInGameServerSystem` seeds the per-frame 4-socket Spark loadout UNCONDITIONALLY (was gym-only — the linchpin); class swap = FrameId + socket re-seed + `SocketCooldown` reset; weapons are stat-sticks (`GrantedAbilityId` gone); HUD cooldown bar reads socket 0.
6.**`CharacterId` → `FrameKind`** (enum + call sites; byte values unchanged; `CharacterStatsDefinition` SO class name deferred — asset-binding risk).
7.**The scene contract — exactly these project scenes**: `MainMenu` (build 0) · `Game` (build 1, the seabed-arena slice Phase 2 grows into) · `DevSandbox` (RENAMED from Gym, GUID preserved → GymSub wiring + the "DevSandbox"-gated F1/F2 dev scripts revived) · `ArtStaging` (art viewing, no player). Old DevSandbox.unity deleted; SampleScene already gone.
8.**One look, one source of truth**: `PostFX_Lantern.asset` (ACES + cool bloom 0.6 + vignette 0.28 + teal filter) on all three lit scenes; `Env_SeabedKit.prefab` (ArtStaging's seabed/flora/snow/caustics/warm-pool/StagingAmbiance, prefab-ized) placed in Game + DevSandbox and CONNECTED in ArtStaging; unified RenderSettings (no skybox, Exp² teal fog {0.02,0.10,0.12}, flat ambient {0.03,0.055,0.08}; density is the per-scene readability knob: 0.035 play / 0.075 staging); camera clearFlags SolidColor deep-water (no-skybox corners). PostFX_DarkSciFi/PostFX_Daylight/Sky_DaytimeProcedural deleted.
9.**`ScenePolicy.IsGameplayScene()`** (Game + DevSandbox) replaces the six scattered `scene.name == "Game"` gates. `WorldAtmosphereSystem` rewritten as water-column murk (biomes re-meant kelp/trench/gloam-bloom; Ground_Arid tint block deleted).
10.**Arena roster**: WaveDirector + ZoneEnemyDirector = `[Drowner, Grindylow, Drowner, Drowner]` (kind slots; re-pointed BEFORE deleting the 10 old-theme enemy prefabs — the null-skip pool-collapse hazard). `EnemySpit` kept for a future ranged kind. Landmark/prop collider groups deleted from the subscene (their visuals died with the biomes → invisible walls).
11.**Bathynaut on the player**: `PlayerRigTools.BuildBathynautPlayer` swaps the suit body onto `Player.prefab` IN PLACE (GUID preserved; ghost surface unchanged → no netcode review needed). Both frames share the visual for now. **Movement/animation tuning is unblocked.**
## Evidence
Tests 502 → 390 green at every batch boundary; Play world-creation verified after B3, B6, P3, P4, P5 (zero console errors); full loop Play-verified (waves spawn → seek → strike → player death → respawn); suit renders + idles under the murk with all 9 Rukhanka drive params.
## Follow-ups
-`EnemyRigTools``Variant.Template` paths reference deleted prefabs (Enemy.prefab etc.) — re-point to `EnemyDrowner.prefab` as the chassis template before the next rig rebuild.
- Suit kitbash attachments (brass dome, tank pack, shoulder lamp) — one `/art-dev` export + bone-parent pass.
- Fog-density + grade tuning with the operator (knobs, hue locked); ★ A0 sign-off effectively rides the new shared look.
- HUD still speaks old vocab (Aether/Ore/Bio, AT BASE, HUSKS) — the currency re-mean + HUD re-skin are later phases.
@@ -471,3 +471,14 @@ The project pivoted to **LANTERN** ([[DR-048_Lantern_Adoption_Full_Pivot]]; desi
- **Core loop = ready-check multi-room RUNS ★ (DR-044/045/046):** base = pure SPEND hub. `RunDirectorSystem` = sole `RunInfo` writer (`[UpdateBefore(CyclePhaseSystem)]`; nothing else in the room chain touches CyclePhase); banks Charge+retaliation once-per-`RunEpoch` on boss-clear ONLY; strips the boon band + `BoonOffer`s on exit; teleports ONLY launch-stamped `RunParticipant`s (released on Returning). Rooms spawn per `RoomEpoch`, `RoomTag`-torn-down, gate on a PORTAL (`RunLifecycle.RoomExplore=6` loot window; `PortalInteractRequest`→server `PortalCommand`). Class + per-run PREP spent at base (Staging-gated RPCs via shared `ClassSwapUtil`; prep `StatModifier` band stripped on Returning). `Health.Max` is a `[GhostField]`. Boss = server-only `BossState`/`BossAISystem` (sole boss mover). ★ **a serialized prefab bool ignores the C# initializer — flip the value in the prefab.** Feel/tuning detail → [[DR-044_Expedition_Redesign_Shipped_Demo_Polish]] · [[DR-045_Combat_Demo_Feel_Boss_Fight]] · [[DR-046_Base_Expedition_Ties_Portal_Class_Prep]].
- **Core loop = ready-check multi-room RUNS ★ (DR-044/045/046):** base = pure SPEND hub. `RunDirectorSystem` = sole `RunInfo` writer (`[UpdateBefore(CyclePhaseSystem)]`; nothing else in the room chain touches CyclePhase); banks Charge+retaliation once-per-`RunEpoch` on boss-clear ONLY; strips the boon band + `BoonOffer`s on exit; teleports ONLY launch-stamped `RunParticipant`s (released on Returning). Rooms spawn per `RoomEpoch`, `RoomTag`-torn-down, gate on a PORTAL (`RunLifecycle.RoomExplore=6` loot window; `PortalInteractRequest`→server `PortalCommand`). Class + per-run PREP spent at base (Staging-gated RPCs via shared `ClassSwapUtil`; prep `StatModifier` band stripped on Returning). `Health.Max` is a `[GhostField]`. Boss = server-only `BossState`/`BossAISystem` (sole boss mover). ★ **a serialized prefab bool ignores the C# initializer — flip the value in the prefab.** Feel/tuning detail → [[DR-044_Expedition_Redesign_Shipped_Demo_Polish]] · [[DR-045_Combat_Demo_Feel_Boss_Fight]] · [[DR-046_Base_Expedition_Ties_Portal_Class_Prep]].
The general gotcha "a serialized prefab bool ignores the C# initializer — flip the value in the prefab" is kept inline in CLAUDE.md (broadly applicable beyond this loop).
The general gotcha "a serialized prefab bool ignores the C# initializer — flip the value in the prefab" is kept inline in CLAUDE.md (broadly applicable beyond this loop).
Archived verbatim from CLAUDE.md when the systems they governed were DELETED (LANTERN realignment, DR-051).
The surviving invariants (ledger spend-in-place soft-fail; snapshot-above-early-return) stay inline in CLAUDE.md.
- **Siege-era systems ★ (EB-1/2 · END-1/2 — mothball-bound at Phase 2; these INVARIANTS outlive them):** ★ a Health-less machine silently drops OUT of the aggro snapshot (snapshot ABOVE the early-return; aggro weight <1 gets SQUARED) · spend from the ONE `GetSingletonEntity<ResourceLedger>`, afford→act else SOFT-FAIL (no cooldown-burn), read LIVE in-loop (no hoist) · `CyclePhaseSystem` = SOLE Phase writer (loss edges live there; transient flags, NOT latching) · **`RunOutcome{[GhostField] byte}` is REPLICATED — never client-derive the win/lose banner.** [[DR-032_EB1_Machines_Can_Die]] · [[DR-033_EB2_Felt_Spend_Charge_Economy]] · [[DR-034_END1_Losable_Core]] · [[DR-036_END2_Final_Siege_Win_Lose]].
- **`GoalProgress{[GhostField] int Charge,Target}`** (win meter — ≠ the `ResourceId.Charge` ammo) rides the CycleDirector ghost; buffs/tiers reuse `StatModifier`→`EffectiveAbilityStats`.
- (Netcode bullet, pre-purge form): **Shared GLOBAL state (cycle phase, resource ledger, goal meter) rides an UNTAGGED ghost**, never a region-tagged one (`SetIsIrrelevant` would hide it cross-region). Resolve the ledger via its DISTINCT `ResourceLedger` tag (the multi-`StorageEntry` "multiple instances" rule — EB-2 line).
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.