Files
Project-M/Assets/_Project/Scripts/Simulation/Combat/StatRecomputeSystem.cs
T
kronic 4a8220ad3e LANTERN purge B6: delete the legacy single-ability path (sockets are THE ability model)
AbilityRef, AbilityCooldown, EffectiveAbilityStats, DefaultAbility deleted.
GoInGameServerSystem seeds the per-frame 4-socket Spark loadout UNCONDITIONALLY
(was gym-only); ClassSelectReceiveSystem + DebugOp.SetClass swap FrameId +
re-seed sockets + zero SocketCooldown; ClassSwapUtil.Apply drops newAbilityId;
EquipSystem weapons become stat-sticks (GrantedAbilityId removed from the item
blob/authoring); StatRecomputeSystem folds CharacterStatsRef + sockets only;
HUD cooldown bar reads socket 0 of SocketCooldown/EffectiveSocketStats; class
HUD readers are FrameId-only (ClassForAbility/AbilityFor deleted);
DebugModifierInjectionSystem drops CycleAbility.

390 tests green; Play-verified: a non-gym spawn gets frame=2 with Sparks
[7,8,6,9] and no console errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 15:47:12 -07:00

85 lines
4.2 KiB
C#

using Unity.Burst;
using Unity.Entities;
using Unity.Mathematics;
using Unity.NetCode;
namespace ProjectM.Simulation
{
/// <summary>
/// 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.
/// </summary>
[UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
[UpdateBefore(typeof(PlayerAimSystem))]
[BurstCompile]
public partial struct StatRecomputeSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<AbilityDatabase>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var database = SystemAPI.GetSingleton<AbilityDatabase>();
ref var db = ref database.Value.Value;
foreach (var (charRef, mods, effChar) in
SystemAPI.Query<RefRO<CharacterStatsRef>, DynamicBuffer<StatModifier>,
RefRW<EffectiveCharacterStats>>()
.WithAll<Simulate>())
{
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<AbilitySocket>, DynamicBuffer<StatModifier>, DynamicBuffer<EffectiveSocketStats>>()
.WithAll<Simulate>())
{
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;
}
}
}
}
}
}