Hazard: Blight geyser — permanent periodic both-sides AoE (Phase 1.5b bundle 3)
Review-first netcode slice (design review wf_900e9965-8f0 -> 11 folded; post-impl wf_5c8299f8-299 clean). A PERMANENT periodic telegraphed AoE in Blight-biome rooms only; one new [GhostField] uint NextEruptTick. - Geyser component + GeyserEruptSystem (server): inverted invalid-tick guard (a baked-0 tick must NEVER erupt -> lazy-stamps born-correct instead of the party-wiping per-tick barrage), inline both-sides gather (SourceNetworkId=-1), reschedule = now + period (never +=), never destroyed. - GeyserTelegraphSystem (client): absolute-tick growing warning disc + erupt burst on the >0->=<0 crossing, latched per NextEruptTick + arm-guard (never edge-detects the replicated field -> no phantom on 0->stamp / relevancy re-entry). Reuses ScorchDecalSystem + DynamicLightSystem. - Seeded in RoomFieldSystem (Blight-gated on plan.Biome, born-correct staggered stamp from live ServerTick), GeyserFieldSpawner + authoring wired into the Gameplay subscene. Geyser.prefab duplicated from ResourceNode (no collider). - BuildDisc promoted to FeedbackFx (shared with the barrel fuse ring). - 474/474 EditMode incl. the unstamped-storm regression; live no-storm end-to-end. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using Unity.NetCode;
|
||||
using Unity.Transforms;
|
||||
|
||||
namespace ProjectM.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Erupts PERMANENT Blight <see cref="Geyser"/>s (Geyser_Build_Spec, design-review wf_900e9965-8f0): when a
|
||||
/// geyser's replicated <see cref="Geyser.NextEruptTick"/> elapses, radius-damage LIVING enemies AND players
|
||||
/// (friendly fire — the bait/lure mechanic, exactly like the exploding-barrel hazard), then RESCHEDULE
|
||||
/// NextEruptTick one period ahead of NOW and leave the geyser alive (it is permanent — <see cref="RoomTag"/>
|
||||
/// teardown is the only removal; NEVER DestroyEntity here). Player + enemy gather mirrors
|
||||
/// <see cref="HazardExplosionSystem"/> VERBATIM (Health > 0; players also RegionTag == Expedition;
|
||||
/// <see cref="DamageEvent.SourceNetworkId"/> = -1 environment convention; SourceTick stamped at NOW, the
|
||||
/// authoring tick, so dash i-frames negate against it).
|
||||
/// <para>
|
||||
/// ★ The invalid-tick guard is INVERTED from the barrel (review HIGH H2): an unstamped/invalid NextEruptTick
|
||||
/// (0, the "ready" sentinel) is NOT-READY and is SKIPPED — it must NEVER erupt. Copying the barrel's
|
||||
/// "invalid -> detonate" fall-through onto a born-0 [GhostField] would erupt every tick and wipe the party.
|
||||
/// The reschedule is <c>= now + period</c> (NEVER <c>+=</c> — an anchored accumulator catch-up-storms after any
|
||||
/// multi-period skip; every tick-sentinel in the project is <c>= now + delay</c>). Plain server
|
||||
/// <see cref="SimulationSystemGroup"/>, NO ordering edges; presence-gated on <see cref="Geyser"/>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
[UpdateInGroup(typeof(SimulationSystemGroup))]
|
||||
public partial struct GeyserEruptSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<Geyser>();
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
if (!SystemAPI.TryGetSingleton<NetworkTime>(out var netTime) || !netTime.ServerTick.IsValid)
|
||||
return;
|
||||
var serverTick = netTime.ServerTick;
|
||||
uint stamp = TickUtil.NonZero(serverTick.TickIndexForValidTick); // SourceTick = NOW (authoring tick)
|
||||
uint reschedule = TickUtil.NonZero(serverTick.TickIndexForValidTick + Tuning.GeyserPeriodTicks); // = now + period
|
||||
|
||||
var erupts = new NativeList<float3>(Allocator.Temp);
|
||||
foreach (var (geyser, lt) in SystemAPI.Query<RefRW<Geyser>, RefRO<LocalTransform>>())
|
||||
{
|
||||
uint next = geyser.ValueRO.NextEruptTick;
|
||||
var until = new NetworkTick(next);
|
||||
// INVERTED guard (review H2): an unstamped/invalid NextEruptTick (0) is NOT-READY and must NEVER
|
||||
// erupt. LAZY-STAMP it born-correct (now + period) instead — self-heals the rare case where the
|
||||
// seed-time stamp missed (NetworkTime invalid at seed), so a 0 can neither storm nor stay inert.
|
||||
if (next == 0u || !until.IsValid) { geyser.ValueRW.NextEruptTick = reschedule; continue; }
|
||||
if (until.IsNewerThan(serverTick)) continue; // still counting down
|
||||
erupts.Add(lt.ValueRO.Position);
|
||||
geyser.ValueRW.NextEruptTick = reschedule; // reschedule in place; permanent, never destroyed
|
||||
}
|
||||
|
||||
if (erupts.Length > 0)
|
||||
{
|
||||
float radiusSq = Tuning.GeyserEruptRadius * Tuning.GeyserEruptRadius;
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
|
||||
// Players: living + expedition only (the HazardExplosionSystem gather verbatim).
|
||||
foreach (var (hp, region, plt, player) in
|
||||
SystemAPI.Query<RefRO<Health>, RefRO<RegionTag>, RefRO<LocalTransform>>()
|
||||
.WithAll<PlayerTag>().WithEntityAccess())
|
||||
{
|
||||
if (hp.ValueRO.Current <= 0f || region.ValueRO.Region != RegionId.Expedition) continue;
|
||||
for (int i = 0; i < erupts.Length; i++)
|
||||
if (math.distancesq(plt.ValueRO.Position.xz, erupts[i].xz) <= radiusSq)
|
||||
ecb.AppendToBuffer(player, new DamageEvent
|
||||
{
|
||||
Amount = Tuning.GeyserEruptDamage,
|
||||
SourceNetworkId = -1,
|
||||
SourceTick = stamp,
|
||||
});
|
||||
}
|
||||
|
||||
// Enemies: living only (Health > 0 also excludes Dying corpses, the established convention).
|
||||
foreach (var (hp, elt, enemy) in
|
||||
SystemAPI.Query<RefRO<Health>, RefRO<LocalTransform>>()
|
||||
.WithAll<EnemyTag>().WithEntityAccess())
|
||||
{
|
||||
if (hp.ValueRO.Current <= 0f) continue;
|
||||
for (int i = 0; i < erupts.Length; i++)
|
||||
if (math.distancesq(elt.ValueRO.Position.xz, erupts[i].xz) <= radiusSq)
|
||||
ecb.AppendToBuffer(enemy, new DamageEvent
|
||||
{
|
||||
Amount = Tuning.GeyserEruptDamage,
|
||||
SourceNetworkId = -1,
|
||||
SourceTick = stamp,
|
||||
});
|
||||
}
|
||||
|
||||
ecb.Playback(state.EntityManager);
|
||||
ecb.Dispose();
|
||||
}
|
||||
erupts.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user