using ProjectM.Simulation;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.NetCode;
using Unity.Transforms;
namespace ProjectM.Server
{
///
/// Server-only ONE-SHOT arena dressing seeder — the gym-scoped successor to RoomFieldSystem, which was
/// deleted with the run/room FSM in the 2026-08-07 audit purge (DR-054). That deletion was correct for the FSM
/// but silently took the arena's furniture with it: the baked (14) and
/// (2) singletons kept baking, and nothing read them, so the shipping arena
/// spawned ZERO clutter and ZERO geysers. Found at the A0 gate — the operator's "the world is very static".
///
/// The old system keyed every spawn off room epochs, room plans, biome gates and RoomTag teardown. None
/// of that exists now and none of it should come back for a gym: Phase 1 is "no world — a gym", so this seeds
/// ONCE at world start and never tears down. Everything room-shaped (scatter-in-shape, budget spend-down,
/// per-room reroll) is deliberately dropped; the pocket generator owns that in Phase 2.
///
/// Contracts kept from the original, because each was load-bearing:
/// - baked.WithPosition, never FromPosition — the latter resets Scale, which is a [GhostField].
/// - Geyser NextEruptTick is stamped BORN-CORRECT off the live ServerTick through
/// , staggered per instance so eruptions desync. If NetworkTime is not valid
/// this tick we ship 0 and let GeyserEruptSystem lazy-stamp — 0 means "not ready", never "fire now".
/// - A keep-out ring around the arena origin so nothing spawns on top of the player's landing spot.
/// - Clutter variant 3 is the EXPLOSIVE hazard (Exploding_Barrels_Build_Spec); 0-2 are inert dressing.
///
/// Colliders: clutter and cover carry colliders, and enemy movement has NO pathfinding (a CollisionWorld
/// sphere-cast slide plus a depenetrate/nudge backstop). CLAUDE.md's standing rule is to re-validate that
/// movers are not frozen whenever Environment cover is added. The keep-out ring and the modest counts here are
/// the mitigation; the validation is a live mover check, not a unit test.
///
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
[UpdateInGroup(typeof(SimulationSystemGroup))]
public partial struct ArenaFieldSystem : ISystem
{
/// Nothing spawns inside this radius of the arena origin — the player's landing spot.
const float KeepOut = 6f;
/// Deterministic base seed. A gym wants the SAME arena every session, not a fresh scatter.
const uint ArenaSeed = 0x5EABEDu;
public void OnCreate(ref SystemState state)
{
// Either spawner is enough to have work to do; the arena may legitimately carry only one.
state.RequireForUpdate();
}
public void OnUpdate(ref SystemState state)
{
// One-shot for the lifetime of the world. Disabling beats a bookkeeping component: no structural
// change, no re-read tick, and it cannot double-seed on a late subscene stream.
state.Enabled = false;
var ecb = new EntityCommandBuffer(Allocator.Temp);
float3 origin = float3.zero;
if (SystemAPI.TryGetSingleton(out var anchor))
origin = BaseGridMath.PlotCenter(anchor);
int clutter = SeedClutter(ref state, ecb, origin);
int geysers = SeedGeysers(ref state, ecb, origin);
int cover = SeedCover(ref state, ecb, origin);
ecb.Playback(state.EntityManager);
ecb.Dispose();
UnityEngine.Debug.Log($"[ArenaFieldSystem] seeded arena: {clutter} clutter, {geysers} geysers, {cover} cover.");
}
/// Golden-angle spiral scatter jittered per index: even coverage, no clumping, and no rejection
/// loop — the old ScatterInShape needed a room plan that no longer exists.
static float3 Scatter(float3 origin, float radius, int i, int count, ref Random rng)
{
float t = count <= 1 ? 0.5f : (i + 0.5f) / count;
float r = KeepOut + math.sqrt(t) * math.max(0.01f, radius - KeepOut);
float a = i * 2.399963f + rng.NextFloat(-0.35f, 0.35f); // golden angle + jitter
r = math.max(KeepOut, r + rng.NextFloat(-0.6f, 0.6f));
return origin + new float3(math.cos(a) * r, 0f, math.sin(a) * r);
}
int SeedClutter(ref SystemState state, EntityCommandBuffer ecb, float3 origin)
{
if (!SystemAPI.TryGetSingleton(out var spawner) || spawner.Prefab == Entity.Null)
return 0;
int count = math.clamp(spawner.Count, 0, 24);
if (count == 0) return 0;
var baked = SystemAPI.GetComponent(spawner.Prefab);
var proto = SystemAPI.GetComponent(spawner.Prefab);
var rng = new Random(ArenaSeed ^ 0xC17u);
float radius = spawner.Radius > 0.01f ? spawner.Radius : 14f;
for (int i = 0; i < count; i++)
{
var e = ecb.Instantiate(spawner.Prefab);
ecb.SetComponent(e, baked.WithPosition(Scatter(origin, radius, i, count, ref rng)));
var bc = proto;
// ~25% EXPLOSIVE (variant 3, the hazard); the rest cycle the inert dressing meshes 0-2.
bc.Variant = rng.NextFloat() < 0.25f ? (byte)3 : (byte)(i % 3);
ecb.SetComponent(e, bc);
}
return count;
}
int SeedGeysers(ref SystemState state, EntityCommandBuffer ecb, float3 origin)
{
if (!SystemAPI.TryGetSingleton(out var spawner) || spawner.Prefab == Entity.Null)
return 0;
int count = math.clamp(spawner.Count, 0, 4);
if (count == 0) return 0;
// Born-correct scheduling: read the LIVE tick. 0 stays the "unstamped" sentinel and means NOT ready.
uint stamp = 0u;
if (SystemAPI.TryGetSingleton(out var nt) && nt.ServerTick.IsValid)
stamp = nt.ServerTick.TickIndexForValidTick;
var baked = SystemAPI.GetComponent(spawner.Prefab);
var rng = new Random(ArenaSeed ^ 0x6E7u);
for (int i = 0; i < count; i++)
{
var e = ecb.Instantiate(spawner.Prefab);
ecb.SetComponent(e, baked.WithPosition(Scatter(origin, 12f, i, count, ref rng)));
uint next = stamp != 0u
? TickUtil.NonZero(stamp + Tuning.GeyserPeriodTicks + (uint)i * 60u) // stagger so they desync
: 0u;
ecb.SetComponent(e, new Geyser { NextEruptTick = next });
}
return count;
}
int SeedCover(ref SystemState state, EntityCommandBuffer ecb, float3 origin)
{
if (!SystemAPI.TryGetSingleton(out var spawner) || spawner.Prefab == Entity.Null)
return 0;
int count = math.clamp(spawner.Count, 0, 6);
if (count == 0) return 0;
var baked = SystemAPI.GetComponent(spawner.Prefab);
var rng = new Random(ArenaSeed ^ 0xC0Eu);
for (int i = 0; i < count; i++)
{
var e = ecb.Instantiate(spawner.Prefab);
ecb.SetComponent(e, baked.WithPosition(Scatter(origin, 13f, i, count, ref rng)));
}
return count;
}
}
}