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
@@ -0,0 +1,20 @@
using Unity.Entities;
namespace ProjectM.Simulation
{
/// <summary>
/// Server-only lit-fuse marker on an EXPLOSIVE clutter barrel (BlightClutter.Variant==3) that has been
/// popped: the pop sites zero the barrel's replicated Remaining (the client's unambiguous fuse cue —
/// a still-alive barrel with Remaining==0 can only mean "about to blow"; a room-teardown despawn still
/// carries Remaining&gt;0, so the client's boom-vs-puff choice at despawn cannot misfire) and add this
/// component instead of destroying. HazardExplosionSystem detonates when <see cref="ExplodeTick"/>
/// elapses: radius damage to LIVING enemies AND players (friendly fire = the bait mechanic), then the
/// single DestroyEntity. Never replicated; the fuse rides the RoomTag'd barrel, so room teardown / the
/// Staging sweep clean lit barrels for free (no orphaned-event lifecycle).
/// </summary>
public struct BarrelFuse : IComponentData
{
/// <summary>Server tick the barrel detonates (via TickUtil.NonZero — 0 never means "ready").</summary>
public uint ExplodeTick;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c677948826634c44fb355a4e00d7c5df
@@ -206,6 +206,8 @@ namespace ProjectM.Simulation
foreach (var (hx, node, he) in
SystemAPI.Query<RefRO<LocalTransform>, RefRO<ResourceNode>>().WithEntityAccess())
{
if (node.ValueRO.Remaining <= 0) continue; // spent is not a target
harvEntity.Add(he);
harvPos.Add(hx.ValueRO.Position);
harvRemaining.Add(node.ValueRO.Remaining);
@@ -218,6 +220,8 @@ namespace ProjectM.Simulation
foreach (var (hx, clutter, he) in
SystemAPI.Query<RefRO<LocalTransform>, RefRO<BlightClutter>>().WithEntityAccess())
{
if (clutter.ValueRO.Remaining <= 0) continue; // lit-fuse barrel: unhittable, detonation owns it
harvEntity.Add(he);
harvPos.Add(hx.ValueRO.Position);
harvRemaining.Add(clutter.ValueRO.Remaining);
@@ -284,7 +288,24 @@ namespace ProjectM.Simulation
if (rem <= 0)
{
harvDestroyed[i] = true;
ecb.DestroyEntity(harvEntity[i]);
if (harvIsClutter[i] && harvVariant[i] == 3)
{
// EXPLOSIVE pop (Variant 3): light the fuse instead of destroying — mirrors the
// projectile pop site; HazardExplosionSystem detonates (Exploding_Barrels_Build_Spec).
ecb.SetComponent(harvEntity[i], new BlightClutter
{
Remaining = 0,
Variant = harvVariant[i],
ScrapResourceId = harvYieldId[i],
ScrapPerHit = harvPerHit[i],
});
ecb.AddComponent(harvEntity[i], new BarrelFuse
{
ExplodeTick = TickUtil.NonZero(now + Tuning.BarrelFuseTicks),
});
}
else
ecb.DestroyEntity(harvEntity[i]);
}
else if (harvIsClutter[i])
{
@@ -107,6 +107,19 @@ namespace ProjectM.Simulation
/// first (counted at spawn; Health.Max replicates so the bar stays truthful).</summary>
public const float BossHealthPerExtraPlayer = 0.75f;
// ---- Exploding barrels (Phase 1.5 hazard; server-only detonation consts — see Exploding_Barrels_Build_Spec) ----
/// <summary>Fuse ticks between the pop (Remaining hits 0) and detonation (~0.6 s @60): long enough for the
/// popper to step out + for the replicated Remaining=0 to straddle snapshots (the client's fuse cue).</summary>
public const uint BarrelFuseTicks = 36;
/// <summary>Detonation radius (world units, XZ). Above melee reach so point-blank pops are punished, but a
/// fuse-length walk clears it comfortably.</summary>
public const float BarrelExplodeRadius = 3.25f;
/// <summary>Detonation damage — hits LIVING enemies AND players alike (friendly fire = the bait mechanic).</summary>
public const float BarrelExplodeDamage = 26f;
// ---- Expedition BOSS (a scaled Charger given a real kit by BossAISystem; server-only feel consts) ----
/// <summary>Radial SLAM AoE radius (world units): a player inside this ring at wind-up elapse eats the hit