LANTERN feel pass (DR-052 + gap list): SoD facing, underwater feel, Bathynaut kit, walk/run gait, suit lamp + new Synty anim packs
- SoD facing: PlayerFacing = body-yaw only (move-facing / cast-turn / idle-hold); every fire direction re-sourced to FacingMath.ResolveAim (pre-code review blocking catch); TickWindowMath shared windows with Movement-skip; reticle/FX coupled to the damage direction; cursor-dash kept. - Underwater feel: sharpness 15->6, turn 720->360, MoveSpeed 6->4.2; TuningKnob 26-28 (0 = no-override sentinels, dev-protocol bump on DebugTuningReport); stride footsteps + silt + cadence floor; bubbles; underwater ambience bed + distant groans; camera drag + dev scroll zoom. - Bathynaut kit in-engine: dome/tank/shoulder-lamp + bare head grafted (GraftSmr rigid rebase, RecalculateTangents); EmissiveGloamSkinned shader (Rukhanka deformation); shoulder lamp CASTS (warm steady spot on body yaw). - Gait: two-ring walk/run FreeformDirectional tree (walk @0.35, run @1.0) + blended-natural StrideScale; additive Posture(Bank) + Lead(chest-lead) layers; idle = AnimationIdles Base; banking driven from facing turn rate; flat terrain (Env_SeabedKit seabed squashed - CC is planar). - New packs: Synty AnimationIdles + AnimationSwordCombat (combat pass queued) + SyntyPropBoneTool; four authored clips (sway/trudge/banks/lean) + Anim_Player_Underwater.blend + suit-kit FBX. - Validation: 411/411 EditMode green; Play smokes (server==client facing, Aim-true projectile, bank/stride live-sampled, lamp beam verified); pre-code + post-impl adversarial reviews applied. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -46,17 +46,26 @@ namespace ProjectM.Client
|
||||
static readonly FastAnimatorParameter k_IsAttacking = new FastAnimatorParameter("IsAttacking");
|
||||
static readonly FastAnimatorParameter k_IsFiring = new FastAnimatorParameter("IsFiring"); // Blender-authored A_Fire_OneHand (B7)
|
||||
static readonly FastAnimatorParameter k_IsDashing = new FastAnimatorParameter("IsDashing"); // Blender-authored A_Dash_Lean (local player only - DashState is not replicated)
|
||||
static readonly FastAnimatorParameter k_ComboStep = new FastAnimatorParameter("ComboStep"); // replicated MeleeCombo.Step -> per-step swing clips (Swing1/2/3)
|
||||
static readonly FastAnimatorParameter k_Bank = new FastAnimatorParameter("Bank");
|
||||
static readonly FastAnimatorParameter k_StrideScale = new FastAnimatorParameter("StrideScale"); // 07-16b: Locomotion playback = planarSpeed / TrudgeNaturalSpeed (foot-skate fix) // 07-16: additive Posture layer — signed facing turn rate, local player only (subtle; not derived for remotes)
|
||||
|
||||
static readonly FastAnimatorParameter k_ComboStep = new FastAnimatorParameter("ComboStep"); // replicated MeleeCombo.Step -> per-step swing clips (Swing1/2/3)
|
||||
static readonly FastAnimatorParameter k_IsCone = new FastAnimatorParameter("IsCone"); // ability archetype (replicated AbilityRef + blob): Cone -> two-hand slam, else the shot
|
||||
|
||||
// Ticks after a swing-start that IsAttacking stays true (drives the MeleeSwing state). Kept < the swing lock
|
||||
// (MeleeRecoverTicks ~16) so a CHAINED swing re-pulses the bool false->true and re-triggers the Any State
|
||||
// transition per hit. ~0.22s @ 60Hz. Presentation-only.
|
||||
const uint k_AttackAnimTicks = 13;
|
||||
const uint k_AttackAnimTicks = PlayerAimSystem.CastFacingTicks; // structurally tied (07-16 review): body-yaw cast-turn and the swing anim pulse must agree
|
||||
|
||||
// Remote prevPos cache (per ghost Entity). Pruned every frame (a vanished remote = a despawn).
|
||||
NativeParallelHashMap<Entity, float3> _prevPos;
|
||||
|
||||
// 07-16 banking: previous local-player facing + the smoothed bank value (main-thread state; one entity).
|
||||
float2 _prevLocalFacing;
|
||||
bool _bankInit;
|
||||
float _bankSmoothed;
|
||||
|
||||
|
||||
protected override void OnCreate()
|
||||
{
|
||||
_prevPos = new NativeParallelHashMap<Entity, float3>(16, Allocator.Persistent);
|
||||
@@ -77,13 +86,37 @@ namespace ProjectM.Client
|
||||
// Ability blob for the per-class special read (Cone -> slam anim); default(BlobAssetReference) if absent.
|
||||
var abilityBlob = SystemAPI.TryGetSingleton<AbilityDatabase>(out var adbSingleton) ? adbSingleton.Value : default;
|
||||
|
||||
// --- LOCAL owner (CC velocity) ---
|
||||
// 07-16 banking: signed facing turn rate -> Bank (-1..1, + = bank right, leaning INTO the turn).
|
||||
// Sampled main-thread (a single local player), smoothed so the additive pose eases in and out.
|
||||
float bankTarget = 0f;
|
||||
foreach (var facingRO in SystemAPI.Query<RefRO<PlayerFacing>>().WithAll<GhostOwnerIsLocal>())
|
||||
{
|
||||
float2 f = facingRO.ValueRO.Direction;
|
||||
if (_bankInit && math.lengthsq(f) > 1e-6f && math.lengthsq(_prevLocalFacing) > 1e-6f)
|
||||
{
|
||||
float2 a2 = math.normalize(_prevLocalFacing);
|
||||
float2 b2 = math.normalize(f);
|
||||
float signed = math.atan2(a2.x * b2.y - a2.y * b2.x, math.clamp(math.dot(a2, b2), -1f, 1f));
|
||||
float degPerSec = math.degrees(signed) / dt;
|
||||
bankTarget = math.clamp(-degPerSec / math.max(30f, FeelConfig.BankFullTurnDegPerSec), -1f, 1f);
|
||||
}
|
||||
_prevLocalFacing = f;
|
||||
_bankInit = true;
|
||||
break;
|
||||
}
|
||||
_bankSmoothed = math.lerp(_bankSmoothed, bankTarget, 1f - math.exp(-6f * dt));
|
||||
|
||||
|
||||
// --- LOCAL owner (CC velocity) ---
|
||||
var localJob = new LocalDriveJob
|
||||
{
|
||||
moveX = k_MoveX, moveZ = k_MoveZ, speed = k_Speed, isDead = k_IsDead,
|
||||
isAttacking = k_IsAttacking, isFiring = k_IsFiring, isDashing = k_IsDashing,
|
||||
comboStep = k_ComboStep, isCone = k_IsCone, abilityDb = abilityBlob,
|
||||
serverTick = serverTick, attackTicks = k_AttackAnimTicks,
|
||||
bank = k_Bank, bankValue = _bankSmoothed,
|
||||
strideScale = k_StrideScale, trudgeNaturalSpeed = math.max(0.5f, FeelConfig.TrudgeNaturalSpeed),
|
||||
runNaturalSpeed = math.max(0.5f, FeelConfig.RunNaturalSpeed),
|
||||
};
|
||||
Dependency = localJob.ScheduleParallel(Dependency);
|
||||
|
||||
@@ -96,6 +129,8 @@ namespace ProjectM.Client
|
||||
comboStep = k_ComboStep, isCone = k_IsCone, abilityDb = abilityBlob,
|
||||
serverTick = serverTick, attackTicks = k_AttackAnimTicks,
|
||||
dt = dt,
|
||||
strideScale = k_StrideScale, trudgeNaturalSpeed = math.max(0.5f, FeelConfig.TrudgeNaturalSpeed),
|
||||
runNaturalSpeed = math.max(0.5f, FeelConfig.RunNaturalSpeed),
|
||||
prevPos = _prevPos,
|
||||
seen = seen,
|
||||
};
|
||||
@@ -113,53 +148,9 @@ namespace ProjectM.Client
|
||||
if (!seen.Contains(keys[i])) _prevPos.Remove(keys[i]);
|
||||
}
|
||||
|
||||
// True while now is within [SwingStartTick, SwingStartTick + animTicks) -- a per-swing pulse that re-triggers
|
||||
// on each chained swing. NetworkTick arithmetic (wrap-safe). Presentation-only, Burst-safe.
|
||||
static bool SwingActive(in MeleeCombo mc, NetworkTick serverTick, uint animTicks)
|
||||
{
|
||||
if (mc.SwingStartTick == 0u || !serverTick.IsValid) return false;
|
||||
var start = new NetworkTick(mc.SwingStartTick);
|
||||
var end = new NetworkTick(TickUtil.NonZero(mc.SwingStartTick + animTicks));
|
||||
return start.IsValid && end.IsValid && !start.IsNewerThan(serverTick) && end.IsNewerThan(serverTick);
|
||||
}
|
||||
|
||||
// B7: the class FIRE ability finally plays a body animation - a stateless window derived from the
|
||||
// replicated AbilityCooldown ([GhostField] NextFireTick) minus the locally-derived CooldownTicks
|
||||
// (EffectiveAbilityStats recomputes identically on every world), so it works for the predicted local
|
||||
// player AND interpolated remotes with no cached edges (review B7: edge-caches false-fire on
|
||||
// relevancy/join/rollback). Reuses the swing clip until a dedicated fire clip exists.
|
||||
static bool FireActive(uint nextFireRaw, int cooldownTicks, NetworkTick serverTick, uint animTicks)
|
||||
{
|
||||
if (nextFireRaw == 0u || cooldownTicks <= 0 || !serverTick.IsValid) return false;
|
||||
uint startRaw = TickUtil.NonZero(nextFireRaw - (uint)cooldownTicks);
|
||||
var start = new NetworkTick(startRaw);
|
||||
var end = new NetworkTick(TickUtil.NonZero(startRaw + animTicks));
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
// The swing/fire tick-window predicates (SwingActive/FireActive/SocketFireAndCone) moved to
|
||||
// TickWindowMath (Simulation/Combat) 07-15 — shared verbatim with the predicted facing path
|
||||
// (PlayerAimSystem); this system passes k_AttackAnimTicks at the call sites.
|
||||
|
||||
// LOCAL: GhostOwnerIsLocal ENABLED -> exactly the owned player. WithPresent<Dead> so alive
|
||||
// (Dead-disabled) players are visited. NOTE: GhostOwnerIsLocal as a WithAll filter respects the
|
||||
@@ -169,7 +160,13 @@ namespace ProjectM.Client
|
||||
[WithPresent(typeof(Dead))]
|
||||
partial struct LocalDriveJob : IJobEntity
|
||||
{
|
||||
public FastAnimatorParameter moveX, moveZ, speed, isDead, isAttacking, isFiring, isDashing, comboStep, isCone;
|
||||
public FastAnimatorParameter bank;
|
||||
public float bankValue;
|
||||
public FastAnimatorParameter strideScale;
|
||||
public float trudgeNaturalSpeed;
|
||||
public float runNaturalSpeed;
|
||||
|
||||
public FastAnimatorParameter moveX, moveZ, speed, isDead, isAttacking, isFiring, isDashing, comboStep, isCone;
|
||||
public BlobAssetReference<AbilityDatabaseBlob> abilityDb;
|
||||
public NetworkTick serverTick;
|
||||
public uint attackTicks;
|
||||
@@ -190,8 +187,8 @@ namespace ProjectM.Client
|
||||
var a = new AnimatorParametersAspect(parametersArr, indexTable);
|
||||
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));
|
||||
SocketFireAndCone(socketCd, sockets, effSockets, abilityDb, serverTick, attackTicks, out bool firingL, out bool coneL);
|
||||
if (a.HasParameter(isAttacking)) a.SetParameterValue(isAttacking, TickWindowMath.SwingActive(melee, serverTick, attackTicks));
|
||||
TickWindowMath.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;
|
||||
@@ -202,6 +199,10 @@ namespace ProjectM.Client
|
||||
dashActive = dStart.IsValid && dEnd.IsValid && !dStart.IsNewerThan(serverTick) && dEnd.IsNewerThan(serverTick);
|
||||
}
|
||||
if (a.HasParameter(isDashing)) a.SetParameterValue(isDashing, dashActive);
|
||||
if (a.HasParameter(bank)) a.SetParameterValue(bank, bankValue);
|
||||
if (a.HasParameter(strideScale))
|
||||
a.SetParameterValue(strideScale, StrideScaleValue(math.length(body.RelativeVelocity.xz), stats.MoveSpeed, trudgeNaturalSpeed, runNaturalSpeed));
|
||||
|
||||
if (a.HasParameter(comboStep)) a.SetParameterValue(comboStep, (int)melee.Step);
|
||||
if (a.HasParameter(isCone)) a.SetParameterValue(isCone, coneL);
|
||||
}
|
||||
@@ -219,6 +220,9 @@ namespace ProjectM.Client
|
||||
public NetworkTick serverTick;
|
||||
public uint attackTicks;
|
||||
public float dt;
|
||||
public FastAnimatorParameter strideScale;
|
||||
public float trudgeNaturalSpeed;
|
||||
public float runNaturalSpeed;
|
||||
public NativeParallelHashMap<Entity, float3> prevPos;
|
||||
public NativeParallelHashSet<Entity> seen;
|
||||
|
||||
@@ -245,16 +249,30 @@ namespace ProjectM.Client
|
||||
var a = new AnimatorParametersAspect(parametersArr, indexTable);
|
||||
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));
|
||||
SocketFireAndCone(socketCd, sockets, effSockets, abilityDb, serverTick, attackTicks, out bool firingR, out bool coneR);
|
||||
if (a.HasParameter(isAttacking)) a.SetParameterValue(isAttacking, TickWindowMath.SwingActive(melee, serverTick, attackTicks));
|
||||
TickWindowMath.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(strideScale))
|
||||
a.SetParameterValue(strideScale, StrideScaleValue(math.length(vel.xz), stats.MoveSpeed, trudgeNaturalSpeed, runNaturalSpeed));
|
||||
if (a.HasParameter(comboStep)) a.SetParameterValue(comboStep, (int)melee.Step);
|
||||
if (a.HasParameter(isCone)) a.SetParameterValue(isCone, coneR);
|
||||
}
|
||||
}
|
||||
|
||||
// ParameterValue has implicit float/bool operators -> SetParameterValue(key, float) / (key, bool) compile.
|
||||
// 07-16e: playback correction against the walk→run BLENDED natural — the two-ring gait tree picks
|
||||
// the clip; this keeps residual cadence honest between/beyond the rings. Pure, Burst-safe.
|
||||
static float StrideScaleValue(float planarSpeed, float maxSpeed, float walkNatural, float runNatural)
|
||||
{
|
||||
float maxSp = math.max(0.5f, maxSpeed);
|
||||
float walkRing = walkNatural / maxSp;
|
||||
float t = math.saturate((planarSpeed / maxSp - walkRing) / math.max(0.05f, 1f - walkRing));
|
||||
float natural = math.lerp(walkNatural, runNatural, t);
|
||||
return math.clamp(planarSpeed / math.max(0.3f, natural), 0.6f, 2.2f);
|
||||
}
|
||||
|
||||
|
||||
// ParameterValue has implicit float/bool operators -> SetParameterValue(key, float) / (key, bool) compile.
|
||||
static void Write(ref AnimatorParametersAspect a, float3 p, bool isDeadVal,
|
||||
FastAnimatorParameter moveX, FastAnimatorParameter moveZ,
|
||||
FastAnimatorParameter speed, FastAnimatorParameter isDead)
|
||||
|
||||
Reference in New Issue
Block a user