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 /// CharacterStatsRef / the AbilitySocket loadout) with its replicated StatModifier buffer into the /// EffectiveCharacterStats / EffectiveSocketStats components - every predicted tick, on both worlds. /// (The legacy single AbilityRef -> EffectiveAbilityStats fold is deleted — LANTERN purge.) /// /// 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 (charRef, mods, effChar) in SystemAPI.Query, DynamicBuffer, RefRW>() .WithAll()) { 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), }; } } // LANTERN Phase 1 (Step 1b): per-socket fold - each socket's Spark base folded with the SHARED // StatModifier band into its EffectiveSocketStats row (uniform band; per-Spark warping is Phase 4). // Separate query so this system stays under the 7-arg cap. foreach (var (sockets, socketMods, effSockets) in SystemAPI.Query, DynamicBuffer, DynamicBuffer>() .WithAll()) { var effBuf = effSockets; // copy the buffer handle to a mutable local (foreach var is readonly -> CS1654) for (int i = 0; i < SocketId.Count && i < sockets.Length && i < effBuf.Length; i++) { if (db.TryGetAbility(sockets[i].SparkId, out var sa)) { effBuf[i] = new EffectiveSocketStats { Damage = StatMath.Apply(sa.Damage, StatTarget.Damage, socketMods), ProjectileSpeed = StatMath.Apply(sa.ProjectileSpeed, StatTarget.ProjectileSpeed, socketMods), Range = StatMath.Apply(sa.Range, StatTarget.Range, socketMods), AutoTargetRange = StatMath.Apply(sa.AutoTargetRange, StatTarget.AutoTargetRange, socketMods), AutoTargetConeRadians = StatMath.Apply(sa.AutoTargetConeRadians, StatTarget.AutoTargetConeRadians, socketMods), CooldownTicks = (int)math.round(StatMath.Apply(sa.CooldownTicks, StatTarget.CooldownTicks, socketMods)), }; } else { effBuf[i] = default; } } } } } }