Hazard: exploding barrels — fused explosive clutter, friendly-fire bait

Phase 1.5 environmental hazard v1 (design-review wf_2cb10454-fdf: 13
findings confirmed + folded; Build Spec in the vault). ~25% of room
clutter seeds as EXPLOSIVE (Variant 3 on the existing replicated byte
- zero new GhostFields/prefabs/RPCs).

The FUSE is the review's fold - one mechanism closes both HIGHs:
- Pop sites (projectile sweep + isServer-gated melee harvest) do NOT
  destroy an explosive: they zero the replicated Remaining + add a
  server-only BarrelFuse (~36 ticks). The client sees Remaining=0 on a
  still-alive barrel across snapshots -> unambiguous fuse cue, and
  booms at despawn ONLY when cached Remaining<=0 - a teardown despawn
  carries Remaining>0, so portal-advance teardowns can never fire
  false booms (HIGH #1).
- HazardExplosionSystem (server, plain group, presence-gated on
  BarrelFuse, never lifecycle-gated) detonates at ExplodeTick: radius
  damage to LIVING enemies AND players (boss-slam player filter
  verbatim; friendly fire = the bait mechanic), SourceTick stamped at
  detonation = the authoring moment (HIGH #2: the authored-tick
  contract dash i-frames negate against), SourceNetworkId=-1 (the
  environment convention - a player id would consume Charger
  whiff-punish windows). Fuse rides the RoomTag'd barrel -> teardown
  cleans lit barrels free.
- Lit barrels are unhittable at both snapshot sites (no double-pop).
- Client: WorldFeedback caches Variant -> boom-vs-puff split (big
  burst + light flash + boom SFX vs the old puff); DynamicLightSystem
  gives Variant-3 barrels a red danger glow that STROBES once fused.

Verified: 470/470 EditMode (4 new: fused pop instead of destroy +
unhittable, both-sides damage w/ -1 source + authored tick, dead-player
+ radius filters, unelapsed-fuse inert); live smoke - seeded 3/8
explosive, real-sweep pop, CLIENT observed the fuse cue on the live
ghost, pinned bait enemy took exactly 26 (30->4), walk-out escape
confirmed; console clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 19:41:47 -07:00
parent cca8c1d44e
commit cd607ae156
12 changed files with 415 additions and 13 deletions
@@ -41,6 +41,7 @@ namespace ProjectM.Client
GameObject _root;
readonly Dictionary<Entity, Light> _projectileLights = new Dictionary<Entity, Light>();
readonly Dictionary<Entity, Light> _nodeLights = new Dictionary<Entity, Light>();
readonly Dictionary<Entity, Light> _barrelLights = new Dictionary<Entity, Light>();
readonly HashSet<Entity> _seen = new HashSet<Entity>();
readonly List<Entity> _dead = new List<Entity>();
readonly Stack<Light> _pool = new Stack<Light>();
@@ -123,6 +124,30 @@ namespace ProjectM.Client
}
PruneUnseen(_nodeLights);
// ---- explosive-barrel danger glow (BlightClutter.Variant==3): steady red, STROBING once the
// fuse is lit (replicated Remaining hits 0 while the barrel still exists) — the flee cue.
_seen.Clear();
foreach (var (clutter, lt, entity) in SystemAPI.Query<RefRO<BlightClutter>, RefRO<LocalTransform>>().WithEntityAccess())
{
if (clutter.ValueRO.Variant != 3) continue;
_seen.Add(entity);
if (!_barrelLights.TryGetValue(entity, out var blight))
{
if (_barrelLights.Count >= 16) continue;
blight = Rent();
_barrelLights[entity] = blight;
}
bool fused = clutter.ValueRO.Remaining <= 0;
blight.color = new Color(1f, 0.28f, 0.12f);
blight.range = fused ? 6f : 4f;
blight.intensity = fused
? 2.6f + 1.8f * Mathf.Abs(Mathf.Sin(UnityEngine.Time.time * 14f))
: 1.1f;
var bp = lt.ValueRO.Position;
blight.transform.position = new Vector3(bp.x, bp.y + 0.9f, bp.z);
}
PruneUnseen(_barrelLights);
// ---- portal light (RoomExplore only) ----
bool portalOn = false;
if (SystemAPI.TryGetSingleton<RunInfo>(out var runInfo)