79 lines
3.8 KiB
C#
79 lines
3.8 KiB
C#
using ProjectM.Simulation;
|
|
using Unity.Burst;
|
|
using Unity.Entities;
|
|
using Unity.NetCode;
|
|
|
|
namespace ProjectM.Server
|
|
{
|
|
/// <summary>
|
|
/// Server-only choice-of-3 boon dealer: once per <see cref="RunRuntime.RoomEpoch"/> (int-equality latch on
|
|
/// <see cref="BoonOfferState"/>, 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
|
|
/// <see cref="BoonMath.PickBoons"/> — deterministically seeded from Hash(RunSeed, room, NetworkId) — and writes
|
|
/// the player's owner-only replicated <see cref="BoonOffer"/> (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: <c>[UpdateAfter(RunDirectorSystem)]</c> — 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.
|
|
/// </summary>
|
|
[BurstCompile]
|
|
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
|
[UpdateInGroup(typeof(SimulationSystemGroup))]
|
|
[UpdateAfter(typeof(RunDirectorSystem))]
|
|
public partial struct BoonOfferSystem : ISystem
|
|
{
|
|
[BurstCompile]
|
|
public void OnCreate(ref SystemState state)
|
|
{
|
|
state.RequireForUpdate<BoonCatalog>();
|
|
state.RequireForUpdate<RunInfo>();
|
|
state.RequireForUpdate<RunRuntime>();
|
|
}
|
|
|
|
[BurstCompile]
|
|
public void OnUpdate(ref SystemState state)
|
|
{
|
|
var catalogEntity = SystemAPI.GetSingletonEntity<BoonCatalog>();
|
|
|
|
// One-shot: attach this system's latch beside the catalog singleton (the RoomFieldState idiom).
|
|
if (!SystemAPI.HasComponent<BoonOfferState>(catalogEntity))
|
|
{
|
|
state.EntityManager.AddComponentData(catalogEntity, new BoonOfferState());
|
|
return; // structural change — clean re-read next tick
|
|
}
|
|
|
|
var dirEntity = SystemAPI.GetSingletonEntity<RunInfo>();
|
|
var info = SystemAPI.GetComponent<RunInfo>(dirEntity);
|
|
if (info.Lifecycle != RunLifecycle.RoomReward)
|
|
return;
|
|
|
|
var run = SystemAPI.GetComponent<RunRuntime>(dirEntity);
|
|
var offered = SystemAPI.GetComponent<BoonOfferState>(catalogEntity);
|
|
if (offered.OfferedRoomEpoch == run.RoomEpoch)
|
|
return; // this room's offers are already dealt
|
|
|
|
var catalog = SystemAPI.GetComponent<BoonCatalog>(catalogEntity);
|
|
if (!catalog.Value.IsCreated)
|
|
return;
|
|
ref var pool = ref catalog.Value.Value;
|
|
|
|
foreach (var (offer, owner, region, cls) in
|
|
SystemAPI.Query<RefRW<BoonOffer>, RefRO<GhostOwner>, RefRO<RegionTag>, RefRO<PlayerClass>>()
|
|
.WithAll<PlayerTag>())
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
}
|