Files
Project-M/Docs/Vault/03_Design/Exploding_Barrels_Build_Spec.md

4.9 KiB

title, type, permalink
title type permalink
Exploding_Barrels_Build_Spec note gamevault/03-design/exploding-barrels-build-spec

Exploding Barrels — Build Spec (Phase 1.5 hazard v1, design-review-hardened)

Review: wf_2cb10454-fdf (3 lenses, 23 agents, 13 findings CONFIRMED / 3 refuted; the first run lost 8 verifiers to a session limit and was resumed from cache — the workflow-agent-quota-failures-look-clean protocol held). This spec is the fold; it supersedes the pre-review draft.

The core mechanism: the FUSE (one fix for both HIGHs)

On pop the barrel does NOT die. The server zeroes the barrel's REPLICATED BlightClutter.Remaining and adds a server-only BarrelFuse { uint ExplodeTick } (TickUtil.NonZero(now + Tuning.BarrelFuseTicks≈36)). The client therefore sees Remaining==0 on a still-alive barrel across multiple snapshots → an unambiguous "about to blow" cue, and the boom-vs-puff choice at despawn keys on the CACHED Remaining<=0 — a room- teardown despawn carries Remaining>0no false booms (HIGH #1: despawn-inference is cause-blind; players stand in torn-down rooms through RouteSelect — RunDirectorSystem.cs:247-251, WorldFeedbackSystem prune :96-119). Detonation is the AUTHORING moment → DamageEvent.SourceTick = TickUtil.NonZero(detonation tick) honors the authored-tick contract dash i-frames negate against (HIGH #2; DamageEvent.cs:20-24). The fuse also: dissolves the melee self-hit complaint (pop → step out), creates the bait loop (lure enemies onto a lit barrel), and — because the fuse lives ON the RoomTag'd barrel — room teardown/Staging sweep clean fused barrels for free (the orphaned-ExplosionEvent lifecycle findings dissolve).

Server

  • Seeding (RoomFieldSystem clutter loop): bc.Variant = crng.NextFloat() < 0.25f ? (byte)3 : (byte)(i % 3) (seeded rng, deterministic; variants 0-2 stay cosmetic).
  • Pop sites (BOTH: ResourceHarvestSystem projectile sweep; MeleeComboSystem isServer-gated harvest block): gate on isClutter && Variant==3 INSIDE the destroyed-bitset guard (nodes share the packed arrays with Variant slot 0 — confirmed LOW). On rem<=0 for an explosive: mark destroyed[i] (unhittable this tick), SetComponent(BlightClutter{Remaining=0,...}) + ecb.AddComponent(BarrelFuse{ExplodeTick}) — NO destroy. Both target snapshots SKIP entries with Remaining <= 0 (a fused barrel is not hittable → no double-pop).
  • HazardExplosionSystem (new, ProjectM.Server, plain SimulationSystemGroup, NO ordering attributes, NO lifecycle gating — a fused barrel must always drain (confirmed LOW: never RequireForUpdate-gate it); TryGetSingleton<NetworkTime> invalid → skip tick, fuse persists): query (BarrelFuse, LocalTransform); when ExplodeTick elapses (NetworkTick.IsNewerThan semantics): radius scan (XZ, Tuning.BarrelExplodeRadius ≈3.25) — players via the BossAISystem:62-70 gather VERBATIM (Health>0 && RegionTag==Expedition; confirmed: dead players persist at death pos for DelayTicks) + enemies (EnemyTag && Health>0, excludes Dying corpses) → AppendToBuffer(DamageEvent{ Amount=Tuning.BarrelExplodeDamage≈26, SourceNetworkId=-1, SourceTick= TickUtil.NonZero(now) })ecb.DestroyEntity(barrel) (single destroy site). SourceNetworkId = -1 (environment convention — confirmed: a player id would score+consume Charger whiff-punish windows via HealthApplyDamageSystem:102, the sole reader).
  • No chain reactions v1 (clutter has Remaining, not Health). Damage numbers as Tuning consts v1; promote to TuningConfig knobs when the knob-table extension is worth it (confirmed-LOW tension, deferred deliberately — noted here per ask-before-deferring-during-implementation: the overlay knob table is a separate mechanical slice).

Client (all new work — confirmed: NO existing per-variant path; Variant is read by zero client systems)

  • WorldFeedbackSystem: add Variant to the Cache struct; at prune, IsClutter && Variant==3 && Remaining<=0 → BIG boom (burst + DynamicLightSystem.RequestFlash large orange + low boom SFX); else the existing small puff. Teardown despawns (Remaining>0) keep the puff.
  • DynamicLightSystem: Variant==3 barrels get a red glow light (node-glow pattern); when the cached Remaining<=0 (fuse lit) the glow strobes hard — the flee cue. (Body tint via URPMaterialPropertyBaseColor on LEG children = polish, deferred.)

Tests

  • ResourceHarvestSystemTests: explosive pop → BarrelFuse added, Remaining==0, entity ALIVE; fused barrel not re-hittable.
  • New HazardExplosionSystemTests: elapsed fuse → DamageEvent(Amount, SourceNetworkId=-1) on in-radius living enemy AND living player, dead player skipped, out-of-radius skipped, barrel destroyed once; un-elapsed fuse → nothing.

Wire/bake classification

ZERO new [GhostField]s / ghost prefabs / RPCs; BarrelFuse is server-only; Variant==3 rides the existing replicated byte. No new ordering edges. Re-bake: none (code + seeded-value only).