using ProjectM.Simulation; using Unity.Burst; using Unity.Collections; using Unity.Entities; using Unity.Mathematics; using Unity.NetCode; namespace ProjectM.Server { /// /// Server-authoritative damage application. Drains each damageable entity's /// buffer (appended by earlier /// this tick), subtracts the summed amount from , then clears the buffer so /// each hit is applied exactly once. Entities that carry character stats (players) clamp to their /// data-driven ceiling; others (training dummies) /// clamp at zero. A dead is destroyed; player death is deferred. /// Health.Current is a [GhostField], so the new value replicates to clients for display. /// /// Runs server-only () inside the prediction /// group so it shares tick timing with movement/damage, where it executes once per tick. The /// single structural change (destroying a dead dummy) is batched through a frame-allocator /// that is played back immediately to the entity manager — so a /// plain-world EditMode test needs no separate ECB system. /// [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)] [UpdateInGroup(typeof(PredictedSimulationSystemGroup))] [UpdateAfter(typeof(ProjectileDamageSystem))] [BurstCompile] public partial struct HealthApplyDamageSystem : ISystem { [BurstCompile] public void OnUpdate(ref SystemState state) { var ecb = new EntityCommandBuffer(Allocator.Temp); foreach (var (health, dmg, entity) in SystemAPI.Query, DynamicBuffer>() .WithEntityAccess()) { if (dmg.Length == 0) continue; float total = 0f; for (int i = 0; i < dmg.Length; i++) total += dmg[i].Amount; dmg.Clear(); float newHp = health.ValueRO.Current - total; // Effective max health (base + modifiers) is the runtime ceiling for entities that carry // character stats (players); others just clamp at zero. No auto-heal on a max increase. if (SystemAPI.HasComponent(entity)) newHp = math.clamp(newHp, 0f, SystemAPI.GetComponent(entity).MaxHealth); else newHp = math.max(0f, newHp); health.ValueRW.Current = newHp; // Server-authoritative death: training dummies despawn; player death is deferred (clamp only). if (health.ValueRO.Current <= 0f && SystemAPI.HasComponent(entity)) ecb.DestroyEntity(entity); } ecb.Playback(state.EntityManager); ecb.Dispose(); } } }