using ProjectM.Simulation; using Unity.Burst; using Unity.Entities; using Unity.Mathematics; using Unity.NetCode; using Unity.Transforms; namespace ProjectM.Server { /// /// Server-authoritative player death→respawn timing. The "is dead" GATE is derived every predicted tick from /// replicated Health by (so movement/aim/fire stop on both server and /// owner-client); THIS system owns the timer + the authoritative recovery. Runs server-only in the plain /// AFTER the predicted group, so it observes this tick's post-damage Health. /// On first seeing Health<=0 it schedules a respawn tick; once due it refills Health to the effective max and /// repositions the player to its deterministic base spawn slot (). Health.Current /// (GhostField) + LocalTransform replicate, so the recovery reaches clients and the derived Dead clears. /// [BurstCompile] [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)] [UpdateInGroup(typeof(SimulationSystemGroup))] [UpdateAfter(typeof(PredictedSimulationSystemGroup))] public partial struct PlayerRespawnSystem : ISystem { [BurstCompile] public void OnCreate(ref SystemState state) { state.RequireForUpdate(); } [BurstCompile] public void OnUpdate(ref SystemState state) { var serverTick = SystemAPI.GetSingleton().ServerTick; if (!serverTick.IsValid) return; uint now = serverTick.TickIndexForValidTick; // Resilient spawn reference: prefer the BaseAnchor plot center, fall back to the PlayerSpawner. NEVER // hard-require PlayerSpawner (a transiently-missing singleton must not strand dead players downed forever). bool haveSpawner = SystemAPI.TryGetSingleton(out var spawner); bool haveAnchor = SystemAPI.TryGetSingleton(out var baseAnchor); if (!haveSpawner && !haveAnchor) return; // no spawn reference at all this tick — re-try next tick rather than mis-place float3 center = haveAnchor ? BaseGridMath.PlotCenter(baseAnchor) : spawner.SpawnPoint; float ringRadius = haveSpawner ? spawner.SpawnRingRadius : 2f; int ringSlots = haveSpawner ? spawner.RingSlots : 4; center = BaseGridMath.PlotCenter(baseAnchor); foreach (var (health, respawn, invuln, xform, region, owner, eff) in SystemAPI.Query, RefRW, RefRW, RefRW, RefRW, RefRO, RefRO>() .WithAll()) { if (health.ValueRO.Current > 0f) { respawn.ValueRW.RespawnTick = 0; // alive: clear any pending schedule continue; } // Dead this tick. if (respawn.ValueRO.RespawnTick == 0) { // Just died: schedule the recovery. respawn.ValueRW.RespawnTick = RespawnMath.RespawnTick(now, respawn.ValueRO.DelayTicks); } else if (RespawnMath.IsDue(now, respawn.ValueRO.RespawnTick)) { // Recover: full health at the deterministic base spawn slot. float maxHealth = eff.ValueRO.MaxHealth > 0f ? eff.ValueRO.MaxHealth : health.ValueRO.Max; health.ValueRW.Current = maxHealth; float3 pos = center + PlayerSpawnMath.SpawnOffset( owner.ValueRO.NetworkId, ringRadius, ringSlots); xform.ValueRW.Position = pos; // Death fix: respawn is at BASE, so the player's server-only RegionTag MUST return to Base too // (every other mover flips RegionTag + Position together). Dying on an expedition otherwise leaves // you at base coords still tagged Expedition -> RegionRelevancy hides all base ghosts (soft-brick). region.ValueRW.Region = RegionId.Base; // Grant brief post-respawn damage immunity so the swarm can't instantly re-kill. invuln.ValueRW.UntilTick = TickUtil.NonZero(now + (uint)math.max(0, respawn.ValueRO.InvulnTicks)); respawn.ValueRW.RespawnTick = 0; } } } } }