using Unity.Burst; using Unity.Entities; using Unity.Mathematics; using Unity.NetCode; namespace ProjectM.Simulation { /// /// Folds each modifiable entity's authored base stats (from the AbilityDatabase blob, keyed by /// AbilityRef / CharacterStatsRef) with its replicated StatModifier buffer into the /// EffectiveAbilityStats / EffectiveCharacterStats components - every predicted tick, on both worlds. /// /// Runs at the head of the predicted group (UpdateBefore PlayerAimSystem; /// AbilityFireSystem runs after PlayerAimSystem, so it sees fresh values too). Recompute is /// unconditional every tick: it is a pure function of (blob base + replicated buffer), both of which /// are restored on rollback, so predicted and server results always agree. A dirty-flag / change /// filter would be WRONG here - the Effective* components are NOT in the ghost snapshot and would go /// stale across reprediction. /// [UpdateInGroup(typeof(PredictedSimulationSystemGroup))] [UpdateBefore(typeof(PlayerAimSystem))] [BurstCompile] public partial struct StatRecomputeSystem : ISystem { [BurstCompile] public void OnCreate(ref SystemState state) { state.RequireForUpdate(); } [BurstCompile] public void OnUpdate(ref SystemState state) { var database = SystemAPI.GetSingleton(); ref var db = ref database.Value.Value; foreach (var (abilityRef, charRef, mods, effAbility, effChar) in SystemAPI.Query, RefRO, DynamicBuffer, RefRW, RefRW>() .WithAll()) { if (db.TryGetAbility(abilityRef.ValueRO.Id, out var a)) { effAbility.ValueRW = new EffectiveAbilityStats { Damage = StatMath.Apply(a.Damage, StatTarget.Damage, mods), ProjectileSpeed = StatMath.Apply(a.ProjectileSpeed, StatTarget.ProjectileSpeed, mods), Range = StatMath.Apply(a.Range, StatTarget.Range, mods), AutoTargetRange = StatMath.Apply(a.AutoTargetRange, StatTarget.AutoTargetRange, mods), AutoTargetConeRadians = StatMath.Apply(a.AutoTargetConeRadians, StatTarget.AutoTargetConeRadians, mods), CooldownTicks = (int)math.round(StatMath.Apply(a.CooldownTicks, StatTarget.CooldownTicks, mods)), }; } if (db.TryGetCharacter(charRef.ValueRO.Id, out var c)) { effChar.ValueRW = new EffectiveCharacterStats { MoveSpeed = StatMath.Apply(c.MoveSpeed, StatTarget.MoveSpeed, mods), TurnRateRadiansPerSec = StatMath.Apply(c.TurnRateRadiansPerSec, StatTarget.TurnRate, mods), MaxHealth = StatMath.Apply(c.MaxHealth, StatTarget.MaxHealth, mods), }; } } } } }