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:
@@ -118,6 +118,11 @@ namespace ProjectM.Authoring
|
|||||||
sockets.Add(new AbilitySocket { SparkId = 0 });
|
sockets.Add(new AbilitySocket { SparkId = 0 });
|
||||||
AddComponent<SocketCooldown>(entity);
|
AddComponent<SocketCooldown>(entity);
|
||||||
AddComponent<FrameId>(entity);
|
AddComponent<FrameId>(entity);
|
||||||
|
// Step 1b: per-socket effective-stats buffer (4 rows), folded each predicted tick by
|
||||||
|
// StatRecomputeSystem (additive; the legacy single EffectiveAbilityStats stays until steps 2/2.5).
|
||||||
|
var effSockets = AddBuffer<EffectiveSocketStats>(entity);
|
||||||
|
for (int sk2 = 0; sk2 < SocketId.Count; sk2++)
|
||||||
|
effSockets.Add(new EffectiveSocketStats());
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,5 +113,47 @@ namespace ProjectM.Tests
|
|||||||
}
|
}
|
||||||
finally { world.Dispose(); blob.Dispose(); }
|
finally { world.Dispose(); blob.Dispose(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SocketFold_Folds_Per_Socket_With_Uniform_Band()
|
||||||
|
{
|
||||||
|
var world = new World("SocketFoldTestWorld");
|
||||||
|
var group = world.GetOrCreateSystemManaged<SimulationSystemGroup>();
|
||||||
|
group.AddSystemToUpdateList(world.GetOrCreateSystem<StatRecomputeSystem>());
|
||||||
|
group.SortSystems();
|
||||||
|
var em = world.EntityManager;
|
||||||
|
var blob = BuildDb();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var dbEntity = em.CreateEntity(typeof(AbilityDatabase));
|
||||||
|
em.SetComponentData(dbEntity, new AbilityDatabase { Value = blob });
|
||||||
|
|
||||||
|
// player carrying the socket loadout + per-socket effective buffer (plus the legacy singles the
|
||||||
|
// first fold loop still needs during the step-1b transition).
|
||||||
|
var player = em.CreateEntity(
|
||||||
|
typeof(AbilityRef), typeof(CharacterStatsRef), typeof(StatModifier),
|
||||||
|
typeof(EffectiveAbilityStats), typeof(EffectiveCharacterStats),
|
||||||
|
typeof(AbilitySocket), typeof(EffectiveSocketStats), typeof(Simulate));
|
||||||
|
em.SetComponentData(player, new AbilityRef { Id = AbilityPrimary });
|
||||||
|
em.SetComponentData(player, new CharacterStatsRef { Id = CharDefault });
|
||||||
|
var socketBuf = em.GetBuffer<AbilitySocket>(player);
|
||||||
|
var effBuf = em.GetBuffer<EffectiveSocketStats>(player);
|
||||||
|
for (int i = 0; i < SocketId.Count; i++) { socketBuf.Add(new AbilitySocket { SparkId = 0 }); effBuf.Add(new EffectiveSocketStats()); }
|
||||||
|
socketBuf[1] = new AbilitySocket { SparkId = AbilityPrimary }; // socket 1 holds the Spark; 0/2/3 empty
|
||||||
|
// uniform band: +5 flat Damage applies to every socket's fold (Phase-1 band semantics).
|
||||||
|
em.GetBuffer<StatModifier>(player).Add(new StatModifier { Target = (byte)StatTarget.Damage, Op = (byte)ModOp.Flat, Value = 5f });
|
||||||
|
|
||||||
|
group.Update();
|
||||||
|
|
||||||
|
var result = em.GetBuffer<EffectiveSocketStats>(player);
|
||||||
|
Assert.AreEqual(SocketId.Count, result.Length);
|
||||||
|
Assert.AreEqual(25f, result[1].Damage, 1e-3f, "socket 1: Primary base 20 + band 5");
|
||||||
|
Assert.AreEqual(25f, result[1].ProjectileSpeed, 1e-3f);
|
||||||
|
Assert.AreEqual(12, result[1].CooldownTicks);
|
||||||
|
Assert.AreEqual(0f, result[0].Damage, 1e-3f, "empty socket 0 -> zeroed default");
|
||||||
|
Assert.AreEqual(0f, result[2].Damage, 1e-3f, "empty socket 2 -> zeroed default");
|
||||||
|
}
|
||||||
|
finally { world.Dispose(); blob.Dispose(); }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user