using ProjectM.Simulation; using Unity.Burst; using Unity.Collections; using Unity.Entities; using Unity.Mathematics; using Unity.NetCode; using Unity.Transforms; namespace ProjectM.Server { /// /// Server-only resource harvest: sweeps each surviving projectile's this-tick travel segment against /// resource-node ghosts and deposits of the node's /// into the GLOBAL resource ledger (the CycleDirector's /// buffer, resolved via — NEVER /// GetSingleton<StorageEntry>, which would collide with the base storage container). Runs in the plain /// server SimulationSystemGroup [UpdateAfter(PredictedSimulationSystemGroup)] — after /// ProjectileDamageSystem has already consumed Health-target hits and range-expired projectiles, so this /// only sees true survivors. The swept segment is reconstructed from /// (written by ProjectileMoveSystem in the fixed-step group), so it is tunnelling-safe WITHOUT depending on /// this plain group's variable-frame DeltaTime. A node hit by two projectiles in one tick deposits twice /// but is destroyed exactly once. Relies on the asserted ~1000-unit base/expedition coordinate gap so a /// base projectile can never geometrically reach an expedition node. /// [BurstCompile] [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)] [UpdateInGroup(typeof(SimulationSystemGroup))] [UpdateAfter(typeof(PredictedSimulationSystemGroup))] public partial struct ResourceHarvestSystem : ISystem { const float k_ProjectileRadius = Tuning.HarvestProjectileRadius; [BurstCompile] public void OnCreate(ref SystemState state) { state.RequireForUpdate(); state.RequireForUpdate(); state.RequireForUpdate(); } [BurstCompile] public void OnUpdate(ref SystemState state) { var ledgerEntity = SystemAPI.GetSingletonEntity(); var ledger = SystemAPI.GetBuffer(ledgerEntity); // Snapshot all nodes once this tick. var nodeEntities = new NativeList(Allocator.Temp); var nodePos = new NativeList(Allocator.Temp); var nodeRadius = new NativeList(Allocator.Temp); var nodeRemaining = new NativeList(Allocator.Temp); var nodeResource = new NativeList(Allocator.Temp); var nodePerHit = new NativeList(Allocator.Temp); foreach (var (xform, hr, node, e) in SystemAPI.Query, RefRO, RefRO>().WithEntityAccess()) { nodeEntities.Add(e); nodePos.Add(xform.ValueRO.Position.xz); nodeRadius.Add(hr.ValueRO.Value); nodeRemaining.Add(node.ValueRO.Remaining); nodeResource.Add(node.ValueRO.ResourceId); nodePerHit.Add(node.ValueRO.HarvestPerHit); } var destroyed = new NativeArray(nodeEntities.Length, Allocator.Temp); var ecb = new EntityCommandBuffer(Allocator.Temp); foreach (var (xform, proj, projEntity) in SystemAPI.Query, RefRO>().WithEntityAccess()) { float3 cur = xform.ValueRO.Position; float2 segEnd = cur.xz; float2 segStart = segEnd - proj.ValueRO.Direction * proj.ValueRO.LastStep; float2 seg = segEnd - segStart; float segLenSq = math.lengthsq(seg); int bestIdx = -1; float bestT = float.MaxValue; for (int i = 0; i < nodeEntities.Length; i++) { if (destroyed[i]) continue; float2 tp = nodePos[i]; float t = segLenSq > 1e-8f ? math.saturate(math.dot(tp - segStart, seg) / segLenSq) : 0f; float2 closest = segStart + t * seg; float hitDist = nodeRadius[i] + k_ProjectileRadius; if (math.distancesq(tp, closest) <= hitDist * hitDist && t < bestT) { bestT = t; bestIdx = i; } } if (bestIdx < 0) continue; int amount = (int)nodePerHit[bestIdx]; StorageMath.Deposit(ledger, nodeResource[bestIdx], amount); int rem = nodeRemaining[bestIdx] - amount; nodeRemaining[bestIdx] = rem; ecb.DestroyEntity(projEntity); if (rem <= 0) { if (!destroyed[bestIdx]) { destroyed[bestIdx] = true; ecb.DestroyEntity(nodeEntities[bestIdx]); } } else { // Persist the decremented Remaining (replicated GhostField) so depletion carries across ticks. SystemAPI.SetComponent(nodeEntities[bestIdx], new ResourceNode { ResourceId = nodeResource[bestIdx], Remaining = rem, HarvestPerHit = nodePerHit[bestIdx], }); } } ecb.Playback(state.EntityManager); ecb.Dispose(); destroyed.Dispose(); nodeEntities.Dispose(); nodePos.Dispose(); nodeRadius.Dispose(); nodeRemaining.Dispose(); nodeResource.Dispose(); nodePerHit.Dispose(); } } }