using ProjectM.Simulation; using Unity.Burst; using Unity.Collections; using Unity.Entities; using Unity.Mathematics; using Unity.Transforms; namespace ProjectM.Server { /// /// Server-only procedural expedition-field manager. Edge-triggered off the cycle phase (via the server-only /// ): on ENTERING Expedition for a not-yet-seeded cycle it scatters /// resource-node ghosts (seeded by CycleNumber via /// ) around the expedition region origin, each /// {Expedition}; on LEAVING Expedition it destroys every node. Runs in the plain /// server SimulationSystemGroup [UpdateAfter(CyclePhaseSystem)] so the phase edge is observed the /// same tick. Server-authoritative; clients despawn nodes via GhostDespawnSystem. Per-cycle reproducible /// (the seed is the monotonic int CycleNumber, compared by equality — never tick math; never seed 0). /// [BurstCompile] [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)] [UpdateInGroup(typeof(SimulationSystemGroup))] [UpdateAfter(typeof(CyclePhaseSystem))] public partial struct ExpeditionFieldSystem : ISystem { [BurstCompile] public void OnCreate(ref SystemState state) { state.RequireForUpdate(); state.RequireForUpdate(); } [BurstCompile] public void OnUpdate(ref SystemState state) { var cycleEntity = SystemAPI.GetSingletonEntity(); var cycle = SystemAPI.GetComponent(cycleEntity); var runtime = SystemAPI.GetComponent(cycleEntity); var spawner = SystemAPI.GetSingleton(); float3 baseCenter = new float3(0f, 1f, 0f); if (SystemAPI.TryGetSingleton(out var anchor)) baseCenter = BaseGridMath.PlotCenter(anchor); float3 origin = RegionMath.RegionOrigin(RegionId.Expedition, baseCenter); var ecb = new EntityCommandBuffer(Allocator.Temp); // SPAWN edge: entered Expedition for a cycle we have not seeded yet. if (cycle.Phase == CyclePhase.Expedition && runtime.LastSpawnedCycle != cycle.CycleNumber && spawner.Prefab != Entity.Null) { var baseXform = SystemAPI.GetComponent(spawner.Prefab); var prefabNode = SystemAPI.GetComponent(spawner.Prefab); var rng = new Random((uint)math.max(1, cycle.CycleNumber)); int count = math.max(1, spawner.Count); for (int i = 0; i < count; i++) { var node = ecb.Instantiate(spawner.Prefab); float ang = rng.NextFloat(0f, math.PI * 2f); float rad = spawner.Radius * math.sqrt(rng.NextFloat(0f, 1f)); var xform = baseXform; xform.Position = origin + new float3(math.cos(ang) * rad, 0f, math.sin(ang) * rad); ecb.SetComponent(node, xform); // Round-robin the resource type (Aether / Ore / Biomass) over the prefab's baked node. var rn = prefabNode; rn.ResourceId = (byte)(ResourceId.Aether + (byte)(i % 3)); ecb.SetComponent(node, rn); } runtime.LastSpawnedCycle = cycle.CycleNumber; } // DESTROY edge: left Expedition — clear the whole field. if (runtime.PrevPhase == CyclePhase.Expedition && cycle.Phase != CyclePhase.Expedition) { foreach (var (rn, e) in SystemAPI.Query>().WithEntityAccess()) ecb.DestroyEntity(e); } runtime.PrevPhase = cycle.Phase; SystemAPI.SetComponent(cycleEntity, runtime); ecb.Playback(state.EntityManager); } } }