From f271d5f47f5e9cdf573fe8c0eebd41874aec379a Mon Sep 17 00:00:00 2001 From: Luis Gonzalez Date: Sun, 12 Jul 2026 13:53:22 -0700 Subject: [PATCH] =?UTF-8?q?Docs:=20Geyser=20Build=20Spec=20+=20session=20l?= =?UTF-8?q?og=20Part=20Q=20+=20roadmap=20tick=20+=20CLAUDE.md=20gotcha=20?= =?UTF-8?q?=E2=80=94=20Phase=201.5b=20bundle=203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Blight geyser (bundle 3) shipped review-first. Records the design-review fold (2 HIGH: no edge-detect on the replicated tick; invert the invalid-guard for a baked scheduled-tick so a born-0 can't storm) and the new CLAUDE.md tick-sentinel corollary for baked [GhostField] scheduled-ticks. Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 2 +- Docs/Vault/03_Design/Geyser_Build_Spec.md | 151 ++++++++++++++++++ .../06_Roadmap/Iteration_2026-07_CoopHades.md | 9 +- ...2026-07-09_B4d_Tail_Collision_Grounding.md | 52 +++++- 4 files changed, 205 insertions(+), 9 deletions(-) create mode 100644 Docs/Vault/03_Design/Geyser_Build_Spec.md diff --git a/CLAUDE.md b/CLAUDE.md index 6d20441b1..69d8be465 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -72,7 +72,7 @@ Long-form originals + the milestone each came from: `Docs/Vault/_Meta/CLAUDE_Bui - **A system-ordering CYCLE is INVISIBLE to plain-Entities EditMode tests** (they register systems individually, unsorted) — it only throws `ComponentSystemSorter` "circular dependency cycle" at **world creation (Play)**. When you add cross-system `[UpdateBefore/After]`, re-audit the EXISTING `[Update*]` attributes of the systems you order around and **always Play-validate**. [[DR-017_Persistent_Base_Player_Driven_Pacing]] - **A dev/debug `IRpcCommand` wire TYPE must be UNCONDITIONAL (no `#if`)** — the reflection-built RpcCollection hash must match across release/dev peers or the handshake refuses; `#if UNITY_EDITOR`-gate only the send/receive SYSTEMS, never the request struct. **Re-mean bytes, don't rename**: unchanged byte VALUES keep the `[GhostField]` serializer identical → re-bake-free (only authoring *default-value* edits re-bake the subscene). - **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()`); **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. +- **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]]. - **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). - **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]]. diff --git a/Docs/Vault/03_Design/Geyser_Build_Spec.md b/Docs/Vault/03_Design/Geyser_Build_Spec.md new file mode 100644 index 000000000..1576feb30 --- /dev/null +++ b/Docs/Vault/03_Design/Geyser_Build_Spec.md @@ -0,0 +1,151 @@ +--- +title: Geyser_Build_Spec +type: note +permalink: gamevault/03-design/geyser-build-spec +--- + +# Blight Geyser — Build Spec (Phase 1.5b bundle 3, design-review-hardened) + +Review: `wf_900e9965-8f0` (3 lenses — netcode/relevancy · determinism/prediction · reuse/scope — 23 agents, +0 errors, 19 findings → **11 CONFIRMED / 8 refuted**; the [[workflow-agent-quota-failures-look-clean]] check +passed — no agents died). This spec is the fold; it supersedes the pre-review draft. Mirrors the two shipped +hazards [[Exploding_Barrels_Build_Spec]] + [[Destructible_Cover_Build_Spec]]. + +**Locked (operator, do not revisit):** Blight-biome rooms ONLY · PERMANENT (never destroyed; `RoomTag` +teardown is the only removal) · both-sides telegraphed AoE (friendly fire vs living players AND enemies, like +the shipped exploding barrels) · also a tactical lure tool. + +## 1. Core mechanism + +A `Geyser` is an ownerless **interpolated** ghost placed only in Blight rooms. One replicated field — +`[GhostField] uint NextEruptTick`, the absolute server tick of its next eruption. Server-only +`GeyserEruptSystem` (plain `SimulationSystemGroup`) fires when `ServerTick` reaches `NextEruptTick`: +radius-gathers living players + living enemies in XZ, appends `DamageEvent`s (drained next tick by +`HealthApplyDamageSystem`, exactly the barrel path), then reschedules `NextEruptTick` one period ahead of +**now**. NEVER destroys itself. The client telegraph derives the countdown from the *absolute* `NextEruptTick` +vs its own predicted `ServerTick` (never an edge on the replicated value) to grow a warning disc, and fires +the erupt burst on the tick-crossing, latched once per eruption. + +## 2. Server + +- **Component** `Geyser { [GhostField] uint NextEruptTick }` in `ProjectM.Simulation`. +- **`GeyserEruptSystem`** — `ProjectM.Server`, `[WorldSystemFilter(ServerSimulation)]` + + `[UpdateInGroup(typeof(SimulationSystemGroup))]`, **NO `[UpdateBefore/After]` edges**, + `RequireForUpdate`. Mirror `HazardExplosionSystem.cs:25-27` (`[BurstCompile]` fine — no enums, no + lifted `SystemAPI.Query`). Each tick: `TryGetSingleton`; if `!ServerTick.IsValid` → skip. + Iterate `(RefRW, LocalTransform, Entity)`: + - **Erupt condition — INVERTED from the barrel (H2):** `until = new NetworkTick(g.NextEruptTick)`. If + `g.NextEruptTick == 0u || !until.IsValid` → **skip** (unstamped = not-ready; never erupt). If + `until.IsNewerThan(serverTick)` → not yet, skip. Otherwise erupt. + - **Gather — inline, do NOT extract a helper (M5):** duplicate the ~12-line both-sides gather verbatim from + `HazardExplosionSystem.cs:59-89`, parameterized only by this geyser's position + `Tuning.GeyserEruptRadius` + + `Tuning.GeyserEruptDamage`. Players `Health>0 && RegionTag==Expedition`, XZ within radius; enemies + `Health>0`, XZ within radius. `AppendToBuffer(DamageEvent{ Amount=GeyserEruptDamage, SourceNetworkId=-1, + SourceTick=stamp })`. + - **SourceTick = NOW, not the scheduled tick (M8):** `uint stamp = + TickUtil.NonZero(serverTick.TickIndexForValidTick);` — verbatim from `HazardExplosionSystem.cs:57`. + - **Reschedule = now + period, NOT `+=` (M3):** `g.ValueRW.NextEruptTick = + TickUtil.NonZero(serverTick.TickIndexForValidTick + Tuning.GeyserPeriodTicks);` via `RefRW.ValueRW`. + - **Never** `DestroyEntity`. + +## 3. Client + +- **New sibling `GeyserTelegraphSystem`** — `ProjectM.Client`, observe-only `SystemBase`, + `[WorldSystemFilter(ClientSimulation)]` + `[UpdateInGroup(typeof(PresentationSystemGroup))]` (sibling of + `EnemyDangerTelegraphSystem`). `CompleteDependencyBeforeRO()` before reading. Read the client + `NetworkTime.ServerTick` (the **predicted** ServerTick — matches shipped enemy/HUD countdowns; NOT + InterpolationTick). Per-geyser cache keyed by `Entity`, pruned every frame (pruned = silent removal; emit + nothing on prune). +- **Disc (absolute-tick derived — the sound half of the original design):** guard `g.NextEruptTick != 0u && + new NetworkTick(g.NextEruptTick).IsValid`. `int lead = new NetworkTick(g.NextEruptTick).TicksSince(serverTick)` + (negative once passed); clamp to `[0, GeyserTelegraphTicks]`. Grow the disc at `GeyserEruptRadius` scaled by + `(1 - lead/(float)GeyserTelegraphTicks)` while `0 < lead <= GeyserTelegraphTicks`. +- **Erupt burst — fire on the ServerTick CROSSING, latched (H1):** fire when `lead` crosses `>0 → <=0` + (`serverTick` first reaches/exceeds `NextEruptTick`). Store `_lastFired[entity] = g.NextEruptTick`; refire + ONLY when `NextEruptTick` differs (the `EnemyDangerTelegraphSystem.cs:177-185` `_strikeBeeped` latch). Plus a + **"was counting down" arm-guard** — only fire if a prior frame observed `lead>0` for this entity + (`EnemyDangerTelegraphSystem.cs:139-140`). Immune to: the `0→stamp` phantom, relevancy re-entry mid-period + (a freshly re-added ghost has `lead>0` → no crossing on seed), and snapshot loss (absolute tick). Co-locates + the burst with the disc completion on the ServerTick timeline (also fixes the boom-trails-disc split). +- **Erupt VFX — reuse shipped statics (no new plumbing):** `FeedbackFx.EmitTinted`/`MakeBurst` particles + + `DynamicLightSystem.RequestFlash` + `ScorchDecalSystem.RequestScorch(pos, GeyserEruptRadius)` (bundle 2, + documented "Reusable by the future Blight-geyser hazard"). +- **Promote the disc primitive (M6):** move `BuildDisc` (+ a disc/ring material factory) out of + `WorldFeedbackSystem` (private `:203`) into `FeedbackFx` so the barrel fuse ring and the geyser telegraph + share ONE primitive. Do NOT re-implement a second private disc-builder. + +## 4. Seeding / authoring + +- **Ghost prefab `Geyser.prefab`:** duplicate **`ResourceNode.prefab`** (or `BlightClutter.prefab`) — NOT + `CoverRock.prefab` (its Environment-layer collider would block the walk-over hazard). Swap authoring → + `GeyserAuthoring` (bakes `Geyser` + `RegionTag{Expedition}`); cosmetic vent mesh; NO collider. Ownerless/ + interpolated `GhostAuthoringComponent` + `LinkedEntityGroup` come free. An inherited `HitRadius` is inert + (server scans by `Tuning.GeyserEruptRadius`) — no strip needed. +- **`GeyserFieldSpawnerAuthoring` → baked `GeyserFieldSpawner { Entity Prefab; int Count }`** singleton, + mirroring `ClutterFieldSpawnerAuthoring`. Author into `Gameplay.unity`'s subscene; assign the prefab. +- **Seeding in `RoomFieldSystem.OnUpdate` InRoom block**, sibling of cover/clutter: + - **Gate on `plan.Biome == RoomBiomeId.Blight`** (local plan, NOT `info.CurrentBiome`) — matches the cover + block's `plan.RoomType != RoomTypeId.Boss`. `RoomBiomeId.Blight == 3`. + - **Never Boss rooms** — an explicit, **commented** design choice (geyser has no collider → NOT the cover + depenetration concern; it's a boss-fight decision). + - `TryGetSingleton` + `Prefab != Entity.Null` (optional singleton; absent → skip). + - Distinct hash sub-stream `0x6E7`; keep-out ring around the room origin (like cover's `KeepOutFromOrigin=7`). + - Stamp `RoomTag{room}` with the SAME `room` local every sibling uses → free `RoomTeardown` removal. + - **Born-correct spawn-stamp (H2):** the block has no `NetworkTime` today — add `TryGetSingleton`; + if `!ServerTick.IsValid` this tick, do NOT seed geysers (the once-per-`RoomEpoch` guard retries). Otherwise + per instance `ecb.SetComponent(e, new Geyser{ NextEruptTick = + TickUtil.NonZero(serverTick.TickIndexForValidTick + Tuning.GeyserPeriodTicks) })` in the deferred loop → the + entity is fully formed before ECB playback / `GhostSendSystem` serialization → the first snapshot never + carries 0. +- **Tuning consts** in `Simulation/Tuning.cs` beside the barrel block: `GeyserPeriodTicks = 300` (5s@60), + `GeyserTelegraphTicks = 78` (~1.3s), `GeyserEruptRadius = 3.0f`, `GeyserEruptDamage = 22`. Single-source only + the constants (no shared gather helper). + +## 5. Tests + +`Assets/_Project/Tests/EditMode/GeyserEruptSystemTests.cs`, plain-Entities, mirroring +`HazardExplosionSystemTests.cs` (reuse `MakeWorld`/`MakePlayer`/`MakeEnemy`): +1. **Both-sides erupt:** living player + living enemy in radius each get exactly one `DamageEvent`; + `SourceNetworkId == -1`. +2. **Reschedule:** after eruption `NextEruptTick` advances by exactly `GeyserPeriodTicks` from now, never the + `0` sentinel. +3. **Never destroyed:** the geyser survives the eruption (opposite of the barrel destroy-once assertion). +4. **Unstamped-storm regression (H2):** seed a geyser at `serverTick=5000` with `NextEruptTick=0`; assert + **zero** `DamageEvent`s on the first ticks (the inverted guard skips — no per-tick barrage). +5. **Out-of-radius / dead filters:** dead or out-of-radius get no `DamageEvent`. + +## 6. Wire / bake classification + +- **New ghost type + prefab** `Geyser.prefab` → genuinely NEW ghost hash (NOT a byte re-mean). ONE new + `[GhostField]` (`Geyser.NextEruptTick`). +- **New systems:** `GeyserEruptSystem` (server) + `GeyserTelegraphSystem` (client). +- **New baked singleton** `GeyserFieldSpawner` authored into `Gameplay.unity` subscene (load-bearing — unwired + = zero geysers, silent). +- **Modified:** `RoomFieldSystem` (Blight seed block + `NetworkTime` spawn-stamp), `Tuning.cs` (4 consts), + `FeedbackFx` (promote `BuildDisc` + material factory), `WorldFeedbackSystem` (fuse ring calls the promoted + primitive). +- **Relevancy:** free via prefab-baked `RegionTag{Expedition}` → existing `RegionRelevancySystem` + `SetIsIrrelevant` hides it cross-region. No relevancy-system change. +- **Ordering:** edge-free server system → no new sort constraint, no Play-only cycle (confirmed). +- Re-bake: new prefab + subscene spawner + authoring defaults; full asmdef recompile for the new types. + +## 7. Changes the review FORCED vs the original design + +**HIGH** — H1 erupt VFX must NOT edge-detect the `NextEruptTick` jump (phantom-fires on `0→stamp` + relevancy +re-entry + snapshot loss) → absolute-tick `TicksSince` disc + latched `>0→<=0` crossing + arm-guard (mirror +`EnemyDangerTelegraphSystem`; `AttackWindup.cs:12` "absolute, not an edge"). Also fixes the boom-trails-disc +timeline split. · H2 INVERT the barrel's invalid-tick guard (unstamped `0` → SKIP, never erupt) + add +`NetworkTime` to `RoomFieldSystem` and stamp `NextEruptTick` born-correct at spawn (else a baked-0 geyser +erupts every tick = a party-wiping barrage). + +**MED** — M3 reschedule `= now + period`, not `+=` (catch-up storm; every in-repo sentinel is `= now + delay`). +· M5 do NOT extract a shared `HazardDamage` gather helper (`SystemAPI.Query` can't be lifted to a static; +verbatim-copy is the shipped convention; Burst-ICE risk) — duplicate inline. · M6 promote `BuildDisc` into +`FeedbackFx`. · M7 gate on `plan.Biome`, not `info.CurrentBiome`. · M8 stamp `SourceTick` at NOW, not the +scheduled `eruptTick` (tick-batching mis-negates dash i-frames). + +**LOW** — duplicate `ResourceNode`/`BlightClutter` (no collider), not `CoverRock`. · add `GeyserEruptSystemTests`. + +**Confirmed correct, no change:** plain-`SimulationSystemGroup` placement · next-tick `DamageEvent` drain · +single-apply · `SourceNetworkId=-1` attribution · dash-i-frame negation · edge-free ordering · +`RegionTag{Expedition}` relevancy scoping. diff --git a/Docs/Vault/06_Roadmap/Iteration_2026-07_CoopHades.md b/Docs/Vault/06_Roadmap/Iteration_2026-07_CoopHades.md index abdff3fc2..6f15d3d7f 100644 --- a/Docs/Vault/06_Roadmap/Iteration_2026-07_CoopHades.md +++ b/Docs/Vault/06_Roadmap/Iteration_2026-07_CoopHades.md @@ -176,8 +176,13 @@ plain"** — expand 1.5 before 1.7. Locked bundles, sequenced ground-first: shadergraph declares `_BaseColor` Unity-Per-Material, NOT Hybrid-Per-Instance (only the DOTS-authored `AnimatedLitShader` honours the EG per-instance override); operator fork → decal quads instead. Base POIs skipped (Part O baked their wear). Knobs in `DecalConfig`. -3. **Hazard variant**: Blight geyser (periodic both-sides AoE on the HazardExplosion + telegraph-disc - chassis) — mini design review first (new replicated erupt-phase surface). +3. ✅ **Hazard variant — Blight geyser** (SHIPPED, Part Q; review-FIRST `wf_900e9965-8f0` → 11 folded, + post-impl `wf_5c8299f8-299` clean): a PERMANENT periodic both-sides telegraphed AoE, Blight-biome rooms only + (operator forks). ONE new `[GhostField] uint NextEruptTick`; server `GeyserEruptSystem` (INVERTED invalid-guard + + lazy-stamp — a baked-0 tick must NEVER erupt; reschedule `=now+period`), client `GeyserTelegraphSystem` + (absolute-tick disc + latched/armed crossing burst — NEVER edge-detect the field; reuses bundle-2 scorch). + Seeded in `RoomFieldSystem` (born-correct staggered), spawner wired into the subscene. 474/474 + live + no-storm end-to-end. See [[Geyser_Build_Spec]]. 4. **Critters + weather beats**: fleeing bugs/birds; cloud-shadow drifts; Blight lightning flicker. Gate: the same "no longer static" lap, now with the ground judged explicitly. diff --git a/Docs/Vault/07_Sessions/2026/2026-07-09_B4d_Tail_Collision_Grounding.md b/Docs/Vault/07_Sessions/2026/2026-07-09_B4d_Tail_Collision_Grounding.md index b6dc4b4e1..0dcc623f6 100644 --- a/Docs/Vault/07_Sessions/2026/2026-07-09_B4d_Tail_Collision_Grounding.md +++ b/Docs/Vault/07_Sessions/2026/2026-07-09_B4d_Tail_Collision_Grounding.md @@ -355,14 +355,54 @@ Three locked sub-features, all **client-only presentation, procedural, zero netc over the dark baked ground — needs a launched expedition + a focused editor (unfocused blocks live-frame verify). Knobs to taste on the lap: `ScorchColor`/alpha, `CoverCrackMaxCount`, `RoomArenaDecalCount`. +## Part Q — Phase 1.5b bundle 3 (BLIGHT GEYSER) shipped — review-first netcode slice + +A PERMANENT periodic hazard (operator forks: **Blight-biome rooms only · permanent · both-sides telegraphed +AoE**). The genuinely-new replicated surface is ONE `[GhostField] uint NextEruptTick`; everything else reuses +the barrel/cover chassis + bundle-2's scorch pool. + +- **Design-review FIRST (`wf_900e9965-8f0`, 3 lenses / 23 agents / 0 errors → 11 CONFIRMED, 8 refuted; the + [[workflow-agent-quota-failures-look-clean]] check passed).** It caught **two HIGH defects the naive design + would have shipped**: (H1) firing the erupt VFX by EDGE-DETECTING the replicated `NextEruptTick` jump — + phantom-fires on the `0→stamp` transition + on every relevancy re-entry (death→respawn→re-enter, region + transit) + drops under snapshot loss; **fix = absolute-tick `TicksSince(predicted ServerTick)` disc + a + `>0→<=0` crossing burst LATCHED per NextEruptTick + a was-counting-down arm-guard** (the + `EnemyDangerTelegraphSystem`/`AttackWindup` "absolute, not an edge" idiom). (H2) copying the barrel's + `invalid-tick → detonate` guard VERBATIM onto a **baked [GhostField] that defaults to 0** → the 0 fails + `IsValid`, falls through, and erupts EVERY tick — a party-wiping AoE barrage; **fix = INVERT the guard (0 = + not-ready → skip) + born-correct stamp from the live ServerTick at seed**, hardened further with a + **lazy-stamp** in the erupt system so a stray 0 can neither storm nor stay inert-forever. Plus MEDs: reschedule + `= now + period` not `+=` (catch-up storm); do NOT extract a shared gather helper (`SystemAPI.Query` can't be + a static — verbatim-duplicate, the shipped convention); promote `BuildDisc` to `FeedbackFx` (shared with the + fuse ring); gate on `plan.Biome` not `info.CurrentBiome`; `SourceTick` at NOW not the scheduled tick. Durable + fold: [[Geyser_Build_Spec]]. +- **Shipped:** `Geyser` component · server `GeyserEruptSystem` (inverted+lazy guard, inline both-sides gather, + `SourceNetworkId=-1`, reschedule `=now+period`, never destroyed) · client `GeyserTelegraphSystem` (absolute-tick + growing disc + latched/armed crossing burst; erupt VFX reuse `ScorchDecalSystem.RequestScorch` + `RequestFlash`) + · `GeyserFieldSpawner` + authoring · `Geyser.prefab` (duplicated `ResourceNode.prefab` — NOT `CoverRock`, which + has an Environment collider that'd block the walk-over hazard — authoring swapped to `GeyserAuthoring`) · + `RoomFieldSystem` Blight-gated born-correct staggered seed (+`NetworkTime`) · `GeyserFieldSpawnerAuthoring` + wired into the `Gameplay.unity` subscene (`ResourceFieldSpawner` hub, Count 2). `BuildDisc` promoted to + `FeedbackFx`; `WorldFeedbackSystem` fuse ring now calls it. +- **Verified:** 474/474 EditMode (incl. the unstamped-storm regression asserting ZERO DamageEvents on a baked-0 + geyser at a high tick) · console clean · **live end-to-end** (server-spawned geyser: periodic erupt + + single-clean reschedule `=now+period` across ~7 eruptions, **no storm**; ghost replicated to the client; + telegraph disc GOs created; erupt scorch fired). **Post-impl review clean** (`wf_5c8299f8-299`, 3 lenses, 0 + findings — genuine, journal-verified). ⚠ Editing hazard hit + fixed mid-build: a `script_apply_edits` delete of + `WorldFeedbackSystem.BuildDisc` mangled the file (duplicated an `UpdateFuseRing` fragment, orphan brace) — a + brace-balanced whole-span `apply_text_edits` repaired it; prefer balanced-span replaces over structured deletes. +- **Deferred to the operator's gate lap:** the in-run VISUAL feel — telegraph disc read + erupt over the dark + Blight ground, and whether a distinct geyser mesh (interim = the ResourceNode mesh) is wanted. Knobs: + `Tuning.Geyser*` (period/telegraph/radius/damage) + `GeyserFieldSpawner.Count`. + ## Next-session intent -**Phase 1.5b bundles 1 (ground) + 2 (decals) are SHIPPED**; remaining locked bundles in order: **3) Blight -geyser** (mini design review FIRST — new replicated erupt-phase surface) → **4) critters + weather beats**. -Then the operator's 1.5 gate lap ("no longer static", ground + decals judged explicitly) → Phase 1.7 boon -overhaul (design-review-FIRST) → Phase 2 hub-ification. Taste queue: **decals feel lap (scorch/crack/scar -tuning)** · arid brightness · craters call · B5 split-panels smoke · barrel explosion VFX polish · flower -density judgment (base may read dense — operator call on the lap). +**Phase 1.5b bundles 1 (ground) + 2 (decals) + 3 (Blight geyser) are SHIPPED.** Last locked bundle: **4) +critters + weather beats** (fleeing bugs/birds · cloud-shadow drifts · Blight lightning flicker — client-only, +likely no design review). Then the operator's 1.5 gate lap ("no longer static"; ground + decals + geyser judged +explicitly) → Phase 1.7 boon overhaul (design-review-FIRST) → Phase 2 hub-ification. Taste queue: **geyser feel +lap (telegraph/erupt tuning · distinct geyser mesh?)** · decals feel lap · arid brightness · craters call · B5 +split-panels smoke · barrel explosion VFX polish · flower density. Related: [[Iteration_2026-07_CoopHades]] · [[2026-07-07_Workflow_Consolidation]] · [[2026-07-07_Expedition_Enemy_Stuck_Harvest_Feedback]]