b143eb6bb5
Per Phase1_Combat_Gym_Build_Spec §7 (additive; legacy single EffectiveAbilityStats + its fold stay until steps 2/2.5 migrate readers, then retired): - EffectiveSocketStats: 4-wide derived (non-replicated) per-socket effective-stats buffer. - PlayerAuthoring bakes it (4 rows). - StatRecomputeSystem: a SECOND fold loop (separate query, under the 7-arg cap) folds each socket's SparkId blob base with the SHARED StatModifier band (uniform; per-Spark warping = Phase 4) into EffectiveSocketStats[i]; empty socket (SparkId 0) -> zeroed default. Fixed CS1654 by writing through a mutable local copy of the buffer handle. L1 clean; L2 489/489 (new SocketFold_Folds_Per_Socket_With_Uniform_Band). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
98 lines
5.2 KiB
C#
98 lines
5.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
|
|
/// 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.
|
|
/// </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 (abilityRef, charRef, mods, effAbility, effChar) in
|
|
SystemAPI.Query<RefRO<AbilityRef>, RefRO<CharacterStatsRef>, DynamicBuffer<StatModifier>,
|
|
RefRW<EffectiveAbilityStats>, RefRW<EffectiveCharacterStats>>()
|
|
.WithAll<Simulate>())
|
|
{
|
|
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),
|
|
};
|
|
}
|
|
}
|
|
|
|
// 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; the legacy single fold above stays
|
|
// until AbilityFireSystem + the feel layer migrate to the buffer (steps 2/2.5).
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|