Docs: Geyser Build Spec + session log Part Q + roadmap tick + CLAUDE.md gotcha — Phase 1.5b bundle 3
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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 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).
|
- **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<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.
|
- **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 (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]].
|
- **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]].
|
||||||
|
|||||||
@@ -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<Geyser>`. Mirror `HazardExplosionSystem.cs:25-27` (`[BurstCompile]` fine — no enums, no
|
||||||
|
lifted `SystemAPI.Query`). Each tick: `TryGetSingleton<NetworkTime>`; if `!ServerTick.IsValid` → skip.
|
||||||
|
Iterate `(RefRW<Geyser>, 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<Geyser>.ValueRW`.
|
||||||
|
- **Never** `DestroyEntity`.
|
||||||
|
|
||||||
|
## 3. Client
|
||||||
|
|
||||||
|
- **New sibling `GeyserTelegraphSystem`** — `ProjectM.Client`, observe-only `SystemBase`,
|
||||||
|
`[WorldSystemFilter(ClientSimulation)]` + `[UpdateInGroup(typeof(PresentationSystemGroup))]` (sibling of
|
||||||
|
`EnemyDangerTelegraphSystem`). `CompleteDependencyBeforeRO<Geyser>()` 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<GeyserFieldSpawner>` + `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<NetworkTime>`;
|
||||||
|
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.
|
||||||
@@ -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
|
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
|
`AnimatedLitShader` honours the EG per-instance override); operator fork → decal quads instead. Base POIs
|
||||||
skipped (Part O baked their wear). Knobs in `DecalConfig`.
|
skipped (Part O baked their wear). Knobs in `DecalConfig`.
|
||||||
3. **Hazard variant**: Blight geyser (periodic both-sides AoE on the HazardExplosion + telegraph-disc
|
3. ✅ **Hazard variant — Blight geyser** (SHIPPED, Part Q; review-FIRST `wf_900e9965-8f0` → 11 folded,
|
||||||
chassis) — mini design review first (new replicated erupt-phase surface).
|
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.
|
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.
|
Gate: the same "no longer static" lap, now with the ground judged explicitly.
|
||||||
|
|||||||
@@ -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
|
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`.
|
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
|
## Next-session intent
|
||||||
|
|
||||||
**Phase 1.5b bundles 1 (ground) + 2 (decals) are SHIPPED**; remaining locked bundles in order: **3) Blight
|
**Phase 1.5b bundles 1 (ground) + 2 (decals) + 3 (Blight geyser) are SHIPPED.** Last locked bundle: **4)
|
||||||
geyser** (mini design review FIRST — new replicated erupt-phase surface) → **4) critters + weather beats**.
|
critters + weather beats** (fleeing bugs/birds · cloud-shadow drifts · Blight lightning flicker — client-only,
|
||||||
Then the operator's 1.5 gate lap ("no longer static", ground + decals judged explicitly) → Phase 1.7 boon
|
likely no design review). Then the operator's 1.5 gate lap ("no longer static"; ground + decals + geyser judged
|
||||||
overhaul (design-review-FIRST) → Phase 2 hub-ification. Taste queue: **decals feel lap (scorch/crack/scar
|
explicitly) → Phase 1.7 boon overhaul (design-review-FIRST) → Phase 2 hub-ification. Taste queue: **geyser feel
|
||||||
tuning)** · arid brightness · craters call · B5 split-panels smoke · barrel explosion VFX polish · flower
|
lap (telegraph/erupt tuning · distinct geyser mesh?)** · decals feel lap · arid brightness · craters call · B5
|
||||||
density judgment (base may read dense — operator call on the lap).
|
split-panels smoke · barrel explosion VFX polish · flower density.
|
||||||
|
|
||||||
Related: [[Iteration_2026-07_CoopHades]] · [[2026-07-07_Workflow_Consolidation]] ·
|
Related: [[Iteration_2026-07_CoopHades]] · [[2026-07-07_Workflow_Consolidation]] ·
|
||||||
[[2026-07-07_Expedition_Enemy_Stuck_Harvest_Feedback]]
|
[[2026-07-07_Expedition_Enemy_Stuck_Harvest_Feedback]]
|
||||||
|
|||||||
Reference in New Issue
Block a user