Blender-authored clips: real death fall, fire thrust, dash lean, hit-react (Phase 1 anim triage)

Four clips authored programmatically in Blender on the PolygonSyntyCharacter
skeleton (full-body keys per pose, numeric world-space verification of the
fall: hips 0.88->0.14m) and exported per-action FBX (Key All Bones, Force
Start/End Keying, FBX Units Scale). Imported as HUMANOID - the project's
pipeline is Human rigs (CharactersAvatar isHuman=true; CLAUDE.md's 'Generic'
note was stale), so muscle-space retargeting plays these on the player AND
every monster rig with zero bone-path matching. CopyFromOther failed on the
Blender-added 'Armature' node (hierarchy mismatch) -> CreateFromThisModel;
root motion fully baked into pose (the CC owns the transform).

- A_Death_FallBack (0.90s = exactly the 54-tick corpse window): recoil ->
  fall -> flat on the back -> settle. Wired as the Death state on BOTH
  controllers, replacing the crouch-idle placeholder.
- A_Fire_OneHand (0.4s): new IsFiring param + Fire state on the player
  controller; PlayerAnimationDriveSystem routes FireActive to it (melee owns
  IsAttacking again).
- A_Dash_Lean (0.33s): new IsDashing param + Dash state; driven from the
  local player's predicted DashState window (not replicated -> remotes skip).
- A_HitReact (0.27s): imported + avatar'd, wiring deferred (needs a
  replicated hit-tick source; body flash covers the read today).

456/456 EditMode; Play-validated: forced IsDead renders the soldier flat on
the ground in-game (Blender->FBX->Humanoid->Rukhanka chain proven), Fire
state enters and plays; console clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 20:44:21 -07:00
parent 192b708b41
commit b7db292987
16 changed files with 1033 additions and 12 deletions
@@ -44,6 +44,8 @@ namespace ProjectM.Client
static readonly FastAnimatorParameter k_Speed = new FastAnimatorParameter("Speed");
static readonly FastAnimatorParameter k_IsDead = new FastAnimatorParameter("IsDead");
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)
// 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
@@ -75,7 +77,8 @@ namespace ProjectM.Client
var localJob = new LocalDriveJob
{
moveX = k_MoveX, moveZ = k_MoveZ, speed = k_Speed, isDead = k_IsDead,
isAttacking = k_IsAttacking, serverTick = serverTick, attackTicks = k_AttackAnimTicks,
isAttacking = k_IsAttacking, isFiring = k_IsFiring, isDashing = k_IsDashing,
serverTick = serverTick, attackTicks = k_AttackAnimTicks,
};
Dependency = localJob.ScheduleParallel(Dependency);
@@ -84,7 +87,8 @@ namespace ProjectM.Client
var remoteJob = new RemoteDriveJob
{
moveX = k_MoveX, moveZ = k_MoveZ, speed = k_Speed, isDead = k_IsDead,
isAttacking = k_IsAttacking, serverTick = serverTick, attackTicks = k_AttackAnimTicks,
isAttacking = k_IsAttacking, isFiring = k_IsFiring, isDashing = k_IsDashing,
serverTick = serverTick, attackTicks = k_AttackAnimTicks,
dt = dt,
prevPos = _prevPos,
seen = seen,
@@ -135,7 +139,7 @@ namespace ProjectM.Client
[WithPresent(typeof(Dead))]
partial struct LocalDriveJob : IJobEntity
{
public FastAnimatorParameter moveX, moveZ, speed, isDead, isAttacking;
public FastAnimatorParameter moveX, moveZ, speed, isDead, isAttacking, isFiring, isDashing;
public NetworkTick serverTick;
public uint attackTicks;
@@ -148,14 +152,24 @@ namespace ProjectM.Client
in MeleeCombo melee,
in AbilityCooldown fireCooldown,
in EffectiveAbilityStats abilityStats,
in DashState dashState,
EnabledRefRO<Dead> dead)
{
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)
|| FireActive(fireCooldown.NextFireTick, abilityStats.CooldownTicks, serverTick, attackTicks));
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));
// Dash lean: the LOCAL player's predicted DashState window [StartTick, IFrameUntilTick+tail).
bool dashActive = false;
if (dashState.StartTick != 0u && dashState.IFrameUntilTick != 0u && serverTick.IsValid)
{
var dStart = new NetworkTick(dashState.StartTick);
var dEnd = new NetworkTick(TickUtil.NonZero(dashState.IFrameUntilTick + 6u));
dashActive = dStart.IsValid && dEnd.IsValid && !dStart.IsNewerThan(serverTick) && dEnd.IsNewerThan(serverTick);
}
if (a.HasParameter(isDashing)) a.SetParameterValue(isDashing, dashActive);
}
}
@@ -166,7 +180,7 @@ namespace ProjectM.Client
[WithPresent(typeof(Dead))]
partial struct RemoteDriveJob : IJobEntity
{
public FastAnimatorParameter moveX, moveZ, speed, isDead, isAttacking;
public FastAnimatorParameter moveX, moveZ, speed, isDead, isAttacking, isFiring, isDashing;
public NetworkTick serverTick;
public uint attackTicks;
public float dt;
@@ -195,9 +209,10 @@ 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)
|| FireActive(fireCooldown.NextFireTick, abilityStats.CooldownTicks, serverTick, attackTicks));
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));
if (a.HasParameter(isDashing)) a.SetParameterValue(isDashing, false); // DashState is not replicated to remotes
}
}