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:
@@ -64,9 +64,7 @@ namespace ProjectM.Client
|
||||
bool _slashActive;
|
||||
float _slashRange, _slashHalf; // live cone geometry re-sampled each frame for the per-frame sweep rebuild
|
||||
int _slashSweepSign = 1; // alternate sweep direction per combo step (reads as alternating strikes)
|
||||
uint _lastConeFireTick; // own latch — the muzzle block owns _lastLocalFireTick and runs first
|
||||
double _lastHoldTime; // C4: last hit-stop hold time (throttle so a horde wipe doesn't stutter)
|
||||
bool _coneTickInit;
|
||||
|
||||
// Remote teammates' melee cleave arcs (deferred-items pass, co-op): one pooled slash renderer per remote
|
||||
// player, edge-detected from the replicated MeleeCombo.SwingStartTick (the local player keeps _slashMr).
|
||||
@@ -91,8 +89,8 @@ namespace ProjectM.Client
|
||||
Vector3 _lastFootPos; float _footTimer; bool _footInit; // footstep edge-detect (local player locomotion)
|
||||
|
||||
Entity _localPlayer = Entity.Null;
|
||||
uint _lastLocalFireTick;
|
||||
bool _fireTickInit;
|
||||
uint[] _lastSocketFire = new uint[SocketId.Count]; // LANTERN per-socket fire-edge cache (was _lastLocalFireTick)
|
||||
bool _socketFireInit;
|
||||
uint _lastLocalDashTick;
|
||||
bool _dashTickInit;
|
||||
uint _lastLocalSwingTick;
|
||||
@@ -315,31 +313,67 @@ namespace ProjectM.Client
|
||||
}
|
||||
}
|
||||
|
||||
// C3: a CONE ability (the Warrior cleave) gets the dedicated cone-arc cue below, NOT the projectile muzzle
|
||||
// flash + zap (those read as a ranged shot). Detect the local ability archetype once and gate the muzzle.
|
||||
bool localIsCone = false;
|
||||
if (_localPlayer != Entity.Null && EntityManager.HasComponent<AbilityRef>(_localPlayer)
|
||||
&& SystemAPI.TryGetSingleton<AbilityDatabase>(out var muzDb) && muzDb.Value.IsCreated)
|
||||
// LANTERN 4-socket fire feedback: edge-detect each socket's SocketCooldown (raw uint edge, cosmetic
|
||||
// only like dash/melee). A non-Cone Spark -> muzzle flash + zap; a Cone Spark -> the aimed slash-arc
|
||||
// cue (server-only cleave has no projectile). Replaces the single-AbilityCooldown muzzle + cone blocks.
|
||||
if (_localPlayer != Entity.Null
|
||||
&& EntityManager.HasComponent<SocketCooldown>(_localPlayer)
|
||||
&& EntityManager.HasBuffer<AbilitySocket>(_localPlayer)
|
||||
&& EntityManager.HasBuffer<EffectiveSocketStats>(_localPlayer)
|
||||
&& SystemAPI.TryGetSingleton<AbilityDatabase>(out var fireDb) && fireDb.Value.IsCreated)
|
||||
{
|
||||
ref var muzAdb = ref muzDb.Value.Value;
|
||||
if (muzAdb.TryGetAbility(EntityManager.GetComponentData<AbilityRef>(_localPlayer).Id, out var muzDef)
|
||||
&& muzDef.Archetype == (byte)AbilityArchetype.Cone)
|
||||
localIsCone = true;
|
||||
}
|
||||
|
||||
// Local-player fire feedback: AbilityCooldown.NextFireTick advances on each shot.
|
||||
// Raw uint inequality is intentional here: only the edge (a new shot) matters and the worst case
|
||||
// of a tick wrap is a single dropped/duplicated muzzle flash — purely cosmetic, never the sim.
|
||||
if (_localPlayer != Entity.Null && EntityManager.HasComponent<AbilityCooldown>(_localPlayer))
|
||||
{
|
||||
uint nextFire = EntityManager.GetComponentData<AbilityCooldown>(_localPlayer).NextFireTick;
|
||||
if (_fireTickInit && nextFire != 0 && nextFire != _lastLocalFireTick && !localIsCone)
|
||||
var scd = EntityManager.GetComponentData<SocketCooldown>(_localPlayer);
|
||||
var socks = EntityManager.GetBuffer<AbilitySocket>(_localPlayer, true);
|
||||
var effs = EntityManager.GetBuffer<EffectiveSocketStats>(_localPlayer, true);
|
||||
ref var fireAdb = ref fireDb.Value.Value;
|
||||
Vector3 sface = Vector3.forward;
|
||||
if (EntityManager.HasComponent<PlayerFacing>(_localPlayer))
|
||||
{
|
||||
Burst(_muzzleFx, cfg != null ? cfg.Muzzle : null, (Vector3)localPos + Vector3.up * 0.9f, 8);
|
||||
PlayClip(_fireClip, (Vector3)localPos, 0.5f);
|
||||
var sfd = EntityManager.GetComponentData<PlayerFacing>(_localPlayer).Direction;
|
||||
if (math.lengthsq(sfd) > 1e-6f) sface = new Vector3(sfd.x, 0f, sfd.y).normalized;
|
||||
}
|
||||
_lastLocalFireTick = nextFire;
|
||||
_fireTickInit = true;
|
||||
float2 sfdir = new float2(sface.x, sface.z);
|
||||
int sn = math.min(SocketId.Count, math.min(socks.Length, effs.Length));
|
||||
for (int sk = 0; sk < sn; sk++)
|
||||
{
|
||||
uint nf = scd.Get(sk);
|
||||
bool edge = _socketFireInit && nf != 0 && nf != _lastSocketFire[sk];
|
||||
_lastSocketFire[sk] = nf;
|
||||
if (!edge) continue;
|
||||
byte sid = socks[sk].SparkId;
|
||||
if (sid == 0) continue;
|
||||
bool coneSpark = fireAdb.TryGetAbility(sid, out var sdef) && sdef.Archetype == (byte)AbilityArchetype.Cone;
|
||||
if (!coneSpark)
|
||||
{
|
||||
Burst(_muzzleFx, cfg != null ? cfg.Muzzle : null, (Vector3)localPos + Vector3.up * 0.9f, 8);
|
||||
PlayClip(_fireClip, (Vector3)localPos, 0.5f);
|
||||
continue;
|
||||
}
|
||||
var es = effs[sk];
|
||||
float coneRange = Mathf.Max(0.1f, es.Range);
|
||||
float coneHalf = Mathf.Clamp(es.AutoTargetConeRadians, 0.01f, 3.14159f);
|
||||
bool coneConnected = false; Vector3 coneHit = (Vector3)localPos; float cnd = float.MaxValue;
|
||||
float coneCos = Mathf.Cos(coneHalf);
|
||||
foreach (var kv in _cache)
|
||||
{
|
||||
if (!kv.Value.IsEnemy) continue;
|
||||
if (MeleeConeMath.InCone(localPos, sfdir, coneRange, coneCos, kv.Value.Pos))
|
||||
{
|
||||
float cd2 = math.distancesq(localPos, kv.Value.Pos);
|
||||
if (cd2 < cnd) { cnd = cd2; coneHit = (Vector3)kv.Value.Pos; coneConnected = true; }
|
||||
}
|
||||
}
|
||||
TriggerSlash((Vector3)localPos, sfdir, coneRange, coneHalf, 1, 1, coneConnected);
|
||||
PlayClip(_swingClip, (Vector3)localPos, 0.5f);
|
||||
PrototypeCameraRig.AddShake(0.06f);
|
||||
if (coneConnected)
|
||||
{
|
||||
Burst(_hitFx, cfg != null ? cfg.Hit : null, coneHit + Vector3.up * 0.7f, FeelConfig.HitBurstCount);
|
||||
PlayClip(_meleeConnectClip, coneHit, FeelConfig.MeleeConnectVolume);
|
||||
PrototypeCameraRig.PunchFov(FeelConfig.MeleeConnectFovKick, FeelConfig.HitStopDurationMs);
|
||||
}
|
||||
}
|
||||
_socketFireInit = true;
|
||||
}
|
||||
|
||||
// Local-player dash feedback (MC-1): DashCooldown.NextTick advances exactly once per dash
|
||||
@@ -423,59 +457,6 @@ namespace ProjectM.Client
|
||||
_swingTickInit = true;
|
||||
}
|
||||
|
||||
// Slice-2 deferred: the Warrior CONE ability had no client VFX (server-only cleave). Edge-detect the
|
||||
// PREDICTED AbilityCooldown (raw uint, like the dash) and reuse the slash-arc sweep with the ability's
|
||||
// own effective geometry — an aimed cone IS an arc.
|
||||
if (_localPlayer != Entity.Null
|
||||
&& EntityManager.HasComponent<AbilityCooldown>(_localPlayer)
|
||||
&& EntityManager.HasComponent<AbilityRef>(_localPlayer)
|
||||
&& EntityManager.HasComponent<EffectiveAbilityStats>(_localPlayer))
|
||||
{
|
||||
uint nextFire = EntityManager.GetComponentData<AbilityCooldown>(_localPlayer).NextFireTick;
|
||||
if (_coneTickInit && nextFire != 0 && nextFire != _lastConeFireTick
|
||||
&& SystemAPI.TryGetSingleton<AbilityDatabase>(out var coneDb) && coneDb.Value.IsCreated)
|
||||
{
|
||||
ref var coneAdb = ref coneDb.Value.Value;
|
||||
if (coneAdb.TryGetAbility(EntityManager.GetComponentData<AbilityRef>(_localPlayer).Id, out var coneDef)
|
||||
&& coneDef.Archetype == (byte)AbilityArchetype.Cone)
|
||||
{
|
||||
var ceff = EntityManager.GetComponentData<EffectiveAbilityStats>(_localPlayer);
|
||||
Vector3 cface = Vector3.forward;
|
||||
if (EntityManager.HasComponent<PlayerFacing>(_localPlayer))
|
||||
{
|
||||
var cfd = EntityManager.GetComponentData<PlayerFacing>(_localPlayer).Direction;
|
||||
if (math.lengthsq(cfd) > 1e-6f) cface = new Vector3(cfd.x, 0f, cfd.y).normalized;
|
||||
}
|
||||
float coneRange = Mathf.Max(0.1f, ceff.Range);
|
||||
float coneHalf = Mathf.Clamp(ceff.AutoTargetConeRadians, 0.01f, 3.14159f);
|
||||
// C3: client cone-overlap over the cached enemy snapshot -> an immediate "you hit" read + thunk
|
||||
// (the server-only cone damage arrives a few ticks later), mirroring the melee connect path.
|
||||
bool coneConnected = false; Vector3 coneHit = (Vector3)localPos; float cnd = float.MaxValue;
|
||||
float coneCos = Mathf.Cos(coneHalf);
|
||||
float2 cfdir = new float2(cface.x, cface.z);
|
||||
foreach (var kv in _cache)
|
||||
{
|
||||
if (!kv.Value.IsEnemy) continue;
|
||||
if (MeleeConeMath.InCone(localPos, cfdir, coneRange, coneCos, kv.Value.Pos))
|
||||
{
|
||||
float cd2 = math.distancesq(localPos, kv.Value.Pos);
|
||||
if (cd2 < cnd) { cnd = cd2; coneHit = (Vector3)kv.Value.Pos; coneConnected = true; }
|
||||
}
|
||||
}
|
||||
TriggerSlash((Vector3)localPos, new float2(cface.x, cface.z), coneRange, coneHalf, 1, 1, coneConnected);
|
||||
PlayClip(_swingClip, (Vector3)localPos, 0.5f);
|
||||
PrototypeCameraRig.AddShake(0.06f);
|
||||
if (coneConnected)
|
||||
{
|
||||
Burst(_hitFx, cfg != null ? cfg.Hit : null, coneHit + Vector3.up * 0.7f, FeelConfig.HitBurstCount);
|
||||
PlayClip(_meleeConnectClip, coneHit, FeelConfig.MeleeConnectVolume);
|
||||
PrototypeCameraRig.PunchFov(FeelConfig.MeleeConnectFovKick, FeelConfig.HitStopDurationMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
_lastConeFireTick = nextFire;
|
||||
_coneTickInit = true;
|
||||
}
|
||||
|
||||
// Footsteps (combat feel): edge-detect local locomotion from the position delta; a soft step at a cadence.
|
||||
if (_localPlayer != Entity.Null)
|
||||
|
||||
Reference in New Issue
Block a user