LANTERN P1 step 2.5: client feel layer migrated to the socket kit

Per Phase1_Combat_Gym_Build_Spec §8 (the "KEEP as-is" feel layer was really a migration):
- PlayerAnimationDriveSystem: both jobs (Local/Remote) now read SocketCooldown +
  DynamicBuffer<AbilitySocket>/<EffectiveSocketStats> instead of the single AbilityCooldown/
  AbilityRef/EffectiveAbilityStats; shared SocketFireAndCone helper (any-socket-firing model:
  IsFiring if any socketed Spark's per-socket cooldown window is mid-fire; IsCone if any such
  is Cone).
- CombatFeedbackSystem: the muzzle-flash + cone-cue blocks unified into one per-socket
  fire-edge loop over SocketCooldown (per-socket uint cache); non-Cone -> muzzle flash,
  Cone -> the aimed slash-arc cue. Dropped the now-dead single-cooldown latches.
L1 clean; L2 494/494 (no regression).

Deferred to the AbilityRef-removal cleanup (surfaced, NOT silent): the FrameId server-writer +
the 2 class-HUD re-points (ClassPrepPortalHudSystem/MetaShopHudSystem) + HudSystem's ability
bar. Rationale: AbilityRef is still baked + set (EquipSystem/ClassSwapUtil), so ClassForAbility
remains a valid class signal through the transition; FrameId is baked ready. No feel regression.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-14 11:13:05 -07:00
parent 12f86c86de
commit 21f609c6c4
2 changed files with 96 additions and 103 deletions
@@ -137,6 +137,30 @@ namespace ProjectM.Client
return start.IsValid && end.IsValid && !start.IsNewerThan(serverTick) && end.IsNewerThan(serverTick);
}
// LANTERN 4-socket fire/cone anim resolution (any-socket model): firing if ANY socketed Spark's
// per-socket cooldown window is mid-fire; cone if any such active socket holds a Cone-archetype Spark.
// Replaces the single-ability FireActive + IsCone reads (AbilityCooldown/EffectiveAbilityStats/AbilityRef).
static void SocketFireAndCone(in SocketCooldown cd, DynamicBuffer<AbilitySocket> sockets,
DynamicBuffer<EffectiveSocketStats> effSockets, BlobAssetReference<AbilityDatabaseBlob> abilityDb,
NetworkTick serverTick, uint animTicks, out bool firing, out bool cone)
{
firing = false; cone = false;
int n = math.min(SocketId.Count, math.min(sockets.Length, effSockets.Length));
bool haveDb = abilityDb.IsCreated;
for (int sk = 0; sk < n; sk++)
{
byte sid = sockets[sk].SparkId;
if (sid == 0) continue;
if (!FireActive(cd.Get(sk), effSockets[sk].CooldownTicks, serverTick, animTicks)) continue;
firing = true;
if (haveDb)
{
ref var adb = ref abilityDb.Value;
if (adb.TryGetAbility(sid, out var d) && d.Archetype == (byte)AbilityArchetype.Cone) cone = true;
}
}
}
// LOCAL: GhostOwnerIsLocal ENABLED -> exactly the owned player. WithPresent<Dead> so alive
// (Dead-disabled) players are visited. NOTE: GhostOwnerIsLocal as a WithAll filter respects the
// enable bit; do NOT take it as an `in` parameter (that matches on presence -> drives remotes too).
@@ -157,9 +181,9 @@ namespace ProjectM.Client
in EffectiveCharacterStats stats,
in KinematicCharacterBody body,
in MeleeCombo melee,
in AbilityCooldown fireCooldown,
in EffectiveAbilityStats abilityStats,
in AbilityRef abilityRef,
in SocketCooldown socketCd,
[ReadOnly] DynamicBuffer<AbilitySocket> sockets,
[ReadOnly] DynamicBuffer<EffectiveSocketStats> effSockets,
in DashState dashState,
EnabledRefRO<Dead> dead)
{
@@ -167,8 +191,8 @@ namespace ProjectM.Client
float3 p = AnimParamMath.LocomotionParams(body.RelativeVelocity, facing.Direction, stats.MoveSpeed);
Write(ref a, p, dead.ValueRO, moveX, moveZ, speed, isDead);
if (a.HasParameter(isAttacking)) a.SetParameterValue(isAttacking, SwingActive(melee, serverTick, attackTicks));
if (a.HasParameter(isFiring)) a.SetParameterValue(isFiring,
FireActive(fireCooldown.NextFireTick, abilityStats.CooldownTicks, serverTick, attackTicks));
SocketFireAndCone(socketCd, sockets, effSockets, abilityDb, serverTick, attackTicks, out bool firingL, out bool coneL);
if (a.HasParameter(isFiring)) a.SetParameterValue(isFiring, firingL);
// Dash lean: the LOCAL player's predicted DashState window [StartTick, IFrameUntilTick+tail).
bool dashActive = false;
if (dashState.StartTick != 0u && dashState.IFrameUntilTick != 0u && serverTick.IsValid)
@@ -179,13 +203,7 @@ namespace ProjectM.Client
}
if (a.HasParameter(isDashing)) a.SetParameterValue(isDashing, dashActive);
if (a.HasParameter(comboStep)) a.SetParameterValue(comboStep, (int)melee.Step);
bool cone = false;
if (abilityDb.IsCreated)
{
ref var adb = ref abilityDb.Value;
if (adb.TryGetAbility(abilityRef.Id, out var adef)) cone = adef.Archetype == (byte)AbilityArchetype.Cone;
}
if (a.HasParameter(isCone)) a.SetParameterValue(isCone, cone);
if (a.HasParameter(isCone)) a.SetParameterValue(isCone, coneL);
}
}
@@ -212,9 +230,9 @@ namespace ProjectM.Client
in PlayerFacing facing,
in EffectiveCharacterStats stats,
in MeleeCombo melee,
in AbilityCooldown fireCooldown,
in EffectiveAbilityStats abilityStats,
in AbilityRef abilityRef,
in SocketCooldown socketCd,
[ReadOnly] DynamicBuffer<AbilitySocket> sockets,
[ReadOnly] DynamicBuffer<EffectiveSocketStats> effSockets,
EnabledRefRO<Dead> dead)
{
seen.Add(e);
@@ -228,17 +246,11 @@ namespace ProjectM.Client
float3 p = AnimParamMath.LocomotionParams(vel, facing.Direction, stats.MoveSpeed);
Write(ref a, p, dead.ValueRO, moveX, moveZ, speed, isDead);
if (a.HasParameter(isAttacking)) a.SetParameterValue(isAttacking, SwingActive(melee, serverTick, attackTicks));
if (a.HasParameter(isFiring)) a.SetParameterValue(isFiring,
FireActive(fireCooldown.NextFireTick, abilityStats.CooldownTicks, serverTick, attackTicks));
SocketFireAndCone(socketCd, sockets, effSockets, abilityDb, serverTick, attackTicks, out bool firingR, out bool coneR);
if (a.HasParameter(isFiring)) a.SetParameterValue(isFiring, firingR);
if (a.HasParameter(isDashing)) a.SetParameterValue(isDashing, false); // DashState is not replicated to remotes
if (a.HasParameter(comboStep)) a.SetParameterValue(comboStep, (int)melee.Step);
bool cone = false;
if (abilityDb.IsCreated)
{
ref var adb = ref abilityDb.Value;
if (adb.TryGetAbility(abilityRef.Id, out var adef)) cone = adef.Archetype == (byte)AbilityArchetype.Cone;
}
if (a.HasParameter(isCone)) a.SetParameterValue(isCone, cone);
if (a.HasParameter(isCone)) a.SetParameterValue(isCone, coneR);
}
}