using ProjectM.Simulation; using Unity.Burst; using Unity.Collections; using Unity.Entities; using Unity.Mathematics; using Unity.Transforms; namespace ProjectM.Server { /// /// Server-only walk-in gate transit: a player who walks within a gate's radius (and whose region matches the /// gate's ) is transited to the gate's ToRegion at its ArrivalPos /// (RegionTag flipped + LocalTransform teleported — GhostRelevancy re-scopes their ghosts, as in /// RegionTransitSystem). Returning to BASE signals the ThreatDirector (a completed expedition can draw a /// retaliation siege) by incrementing . Plain server /// SimulationSystemGroup, ordered BEFORE CyclePhaseSystem (Gate -> ThreatDirector -> RunState) so the return is /// consumed the same tick. Arrival points are offset from the destination gate so a transited player does not /// immediately re-trigger. /// [BurstCompile] [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)] [UpdateInGroup(typeof(SimulationSystemGroup))] [UpdateBefore(typeof(CyclePhaseSystem))] public partial struct ExpeditionGateSystem : ISystem { [BurstCompile] public void OnCreate(ref SystemState state) { state.RequireForUpdate(); } [BurstCompile] public void OnUpdate(ref SystemState state) { // Snapshot gates once. var gateFrom = new NativeList(Allocator.Temp); var gateTo = new NativeList(Allocator.Temp); var gateRadiusSq = new NativeList(Allocator.Temp); var gatePos = new NativeList(Allocator.Temp); var gateArrival = new NativeList(Allocator.Temp); foreach (var (gate, xform) in SystemAPI.Query, RefRO>()) { gateFrom.Add(gate.ValueRO.FromRegion); gateTo.Add(gate.ValueRO.ToRegion); gateRadiusSq.Add(gate.ValueRO.Radius * gate.ValueRO.Radius); gatePos.Add(xform.ValueRO.Position.xz); gateArrival.Add(gate.ValueRO.ArrivalPos); } bool returnedToBase = false; foreach (var (region, xform) in SystemAPI.Query, RefRW>().WithAll()) { byte r = region.ValueRO.Region; float2 pp = xform.ValueRO.Position.xz; for (int i = 0; i < gateFrom.Length; i++) { if (gateFrom[i] != r) continue; if (math.distancesq(pp, gatePos[i]) > gateRadiusSq[i]) continue; region.ValueRW.Region = gateTo[i]; xform.ValueRW.Position = gateArrival[i]; if (gateTo[i] == RegionId.Base) returnedToBase = true; break; } } gateFrom.Dispose(); gateTo.Dispose(); gateRadiusSq.Dispose(); gatePos.Dispose(); gateArrival.Dispose(); // A player returned to base from an expedition -> signal the ThreatDirector (it sizes/arms any // retaliation siege). The gate teleports the returner out of its radius, so this fires once per return. if (returnedToBase) { if (SystemAPI.TryGetSingletonEntity(out var threatEntity)) { var threat = SystemAPI.GetComponent(threatEntity); threat.PendingReturns += 1; threat.ExpeditionsCompleted += 1; SystemAPI.SetComponent(threatEntity, threat); } // Once-per-epoch zone-clear reward: a returner banks flat Ore IFF this epoch's expedition wave was // actually cleared and not yet rewarded. Resolved ONCE here (not per-returner) so two same-tick co-op // returns pay exactly once (DR-040 BLOCKER 4) and gate re-entry before a clear can't farm (MINOR 2). if (SystemAPI.HasSingleton() && SystemAPI.TryGetSingleton(out var zoneDir) && SystemAPI.HasSingleton()) { var cycleEntity = SystemAPI.GetSingletonEntity(); var runtime = SystemAPI.GetComponent(cycleEntity); if (runtime.ClearedThisEpoch != 0 && runtime.LastRewardedEpoch != runtime.ExpeditionEpoch) { var ledger = SystemAPI.GetBuffer(SystemAPI.GetSingletonEntity()); StorageMath.Deposit(ledger, (ushort)ResourceId.Ore, zoneDir.RewardOre); runtime.LastRewardedEpoch = runtime.ExpeditionEpoch; SystemAPI.SetComponent(cycleEntity, runtime); } } } } } }