using ProjectM.Simulation; using Unity.Burst; using Unity.Entities; using Unity.NetCode; namespace ProjectM.Server { /// /// Server-only choice-of-3 boon dealer: once per (int-equality latch on /// , attached beside the catalog singleton), when the run FSM enters RoomReward it /// draws each EXPEDITION player's 3 distinct, rarity-weighted, class-filtered options via /// — deterministically seeded from Hash(RunSeed, room, NetworkId) — and writes /// the player's owner-only replicated (Pending=1). A base-region player (dead-respawned, /// late joiner) gets NO offer and never holds the gate (RunDirector counts only Pending!=0). BoonApplySystem /// (Step 10) consumes picks; the Returning-edge strip zeroes stragglers. /// /// Ordering: [UpdateAfter(RunDirectorSystem)] — on the RoomReward ENTRY tick this runs after the /// transition, so offers exist BEFORE RunDirector's exit gate first evaluates (next tick). No CyclePhase edge. /// [BurstCompile] [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)] [UpdateInGroup(typeof(SimulationSystemGroup))] [UpdateAfter(typeof(RunDirectorSystem))] public partial struct BoonOfferSystem : ISystem { [BurstCompile] public void OnCreate(ref SystemState state) { state.RequireForUpdate(); state.RequireForUpdate(); state.RequireForUpdate(); } [BurstCompile] public void OnUpdate(ref SystemState state) { var catalogEntity = SystemAPI.GetSingletonEntity(); // One-shot: attach this system's latch beside the catalog singleton (the RoomFieldState idiom). if (!SystemAPI.HasComponent(catalogEntity)) { state.EntityManager.AddComponentData(catalogEntity, new BoonOfferState()); return; // structural change — clean re-read next tick } var dirEntity = SystemAPI.GetSingletonEntity(); var info = SystemAPI.GetComponent(dirEntity); if (info.Lifecycle != RunLifecycle.RoomReward) return; var run = SystemAPI.GetComponent(dirEntity); var offered = SystemAPI.GetComponent(catalogEntity); if (offered.OfferedRoomEpoch == run.RoomEpoch) return; // this room's offers are already dealt var catalog = SystemAPI.GetComponent(catalogEntity); if (!catalog.Value.IsCreated) return; ref var pool = ref catalog.Value.Value; foreach (var (offer, owner, region, cls) in SystemAPI.Query, RefRO, RefRO, RefRO>() .WithAll()) { if (region.ValueRO.Region != RegionId.Expedition) continue; // home-bound players (dead-respawned, joiners) are dealt nothing // Deterministic per-player draw: reconnect-stable per session, replay-reproducible per (seed, room). uint offerSeed = RunMapMath.Hash(run.RunSeed, (uint)info.CurrentRoom, (uint)owner.ValueRO.NetworkId) | 1u; BoonMath.PickBoons(offerSeed, cls.ValueRO.ClassId, ref pool, out byte o0, out byte o1, out byte o2); offer.ValueRW = new BoonOffer { Pending = 1, Option0 = o0, Option1 = o1, Option2 = o2 }; } offered.OfferedRoomEpoch = run.RoomEpoch; SystemAPI.SetComponent(catalogEntity, offered); } } }