LANTERN P1 step 1b: per-socket effective-stats fold (additive)

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>
This commit is contained in:
2026-07-14 10:43:31 -07:00
parent 67095024b9
commit b143eb6bb5
5 changed files with 106 additions and 0 deletions
@@ -0,0 +1,27 @@
using Unity.Entities;
namespace ProjectM.Simulation
{
/// <summary>
/// Per-SOCKET effective ability stats for the LANTERN 4-socket kit — one row per socket (buffer index =
/// socket 0..3), each the authored base of that socket's <see cref="AbilitySocket.SparkId"/> (from the
/// AbilityDatabase blob) folded with the player's shared <see cref="StatModifier"/> band by
/// <c>StatRecomputeSystem</c> every predicted tick. The 4-wide analog of the single
/// <see cref="EffectiveAbilityStats"/> (which is retired once <c>AbilityFireSystem</c> + the feel layer
/// migrate to this in steps 2/2.5). Derived/local, NOT replicated — both worlds recompute it
/// deterministically from the replicated socket loadout + modifier band, so it matches under prediction
/// without being in the snapshot. The per-player <see cref="EffectiveCharacterStats"/> stays single.
/// <para>Band semantics (Phase 1): the single per-player <see cref="StatModifier"/> band (class traits +
/// boons) applies UNIFORMLY to all 4 sockets; per-socket/per-Spark warping is Phase 4.</para>
/// </summary>
[InternalBufferCapacity(4)]
public struct EffectiveSocketStats : IBufferElementData
{
public float Damage;
public float ProjectileSpeed;
public float Range;
public float AutoTargetRange;
public float AutoTargetConeRadians;
public int CooldownTicks;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 38f9e71803b1db84586fce59ff9cea7a
@@ -62,6 +62,36 @@ namespace ProjectM.Simulation
};
}
}
// 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;
}
}
}
}
}
}