Pronounced combat animation: 3-swing sword combo, per-class specials, sword in hand

BLENDER (5 new/re-authored clips, all full-body, mirrored-left-arm axes
FIXED - Synty L shoulders mirror X/twist vs R, verified by axis probes):
- A_Swing_R2L / A_Swing_L2R (0.47s): horizontal slash + backhand with real
  anticipation (torso wound 25 deg), 2-frame strikes, follow-through past
  the target.
- A_Swing_Finisher (0.60s): overhead crash with a crouch drop into impact.
- A_Special_Slam (0.53s): two-handed ground slam - the Warrior cone finally
  LOOKS like a cone attack.
- A_Fire_OneHand re-authored punchier (cock -> full-extension thrust with
  torso commit -> recoil kick).
Export lesson: Blender 4.4 slotted actions - reassigning animation_data.
action does NOT bind the slot; force animation_data.action_slot before
export/render or you bake a stale pose.

CONTROLLER: MeleeSwing (one clip for everything) replaced by Swing1/2/3
keyed on new ComboStep int param (replicated MeleeCombo.Step); Fire gated
!IsCone (Ranger shot), new SpecialSlam state on IsFiring+IsCone (Warrior).

DRIVE SYSTEM: writes ComboStep from the replicated combo step and IsCone
from the replicated AbilityRef -> AbilityDatabase blob archetype (works for
remote teammates too).

SWORD: SM_Wep_Sword_01 (SciFiSpace - same pack/atlas as the soldier) as a
mesh-sub-asset child of Hand_R, grip rot (90,0,0) tuned live against the
running entity. Required RigDefinitionAuthoring boneEntityStrippingMode
Automatic -> None so the hand bone entity exists + is posed (stripped bones
never animate attachments). Play-verified: the energy blade rides the fist.

456/456 EditMode.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 21:18:07 -07:00
parent 7350e4dd63
commit eeaf8a4247
25 changed files with 1640 additions and 69 deletions
@@ -46,6 +46,8 @@ 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_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
@@ -72,12 +74,15 @@ namespace ProjectM.Client
// Current authoritative tick for the swing-window check (default = invalid -> IsAttacking stays false).
NetworkTick serverTick = SystemAPI.TryGetSingleton<NetworkTime>(out var nt) ? nt.ServerTick : default;
// 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) ---
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,
};
Dependency = localJob.ScheduleParallel(Dependency);
@@ -88,6 +93,7 @@ namespace ProjectM.Client
{
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,
dt = dt,
prevPos = _prevPos,
@@ -139,7 +145,8 @@ namespace ProjectM.Client
[WithPresent(typeof(Dead))]
partial struct LocalDriveJob : IJobEntity
{
public FastAnimatorParameter moveX, moveZ, speed, isDead, isAttacking, isFiring, isDashing;
public FastAnimatorParameter moveX, moveZ, speed, isDead, isAttacking, isFiring, isDashing, comboStep, isCone;
public BlobAssetReference<AbilityDatabaseBlob> abilityDb;
public NetworkTick serverTick;
public uint attackTicks;
@@ -152,6 +159,7 @@ namespace ProjectM.Client
in MeleeCombo melee,
in AbilityCooldown fireCooldown,
in EffectiveAbilityStats abilityStats,
in AbilityRef abilityRef,
in DashState dashState,
EnabledRefRO<Dead> dead)
{
@@ -170,6 +178,14 @@ 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(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);
}
}
@@ -180,7 +196,8 @@ namespace ProjectM.Client
[WithPresent(typeof(Dead))]
partial struct RemoteDriveJob : IJobEntity
{
public FastAnimatorParameter moveX, moveZ, speed, isDead, isAttacking, isFiring, isDashing;
public FastAnimatorParameter moveX, moveZ, speed, isDead, isAttacking, isFiring, isDashing, comboStep, isCone;
public BlobAssetReference<AbilityDatabaseBlob> abilityDb;
public NetworkTick serverTick;
public uint attackTicks;
public float dt;
@@ -197,6 +214,7 @@ namespace ProjectM.Client
in MeleeCombo melee,
in AbilityCooldown fireCooldown,
in EffectiveAbilityStats abilityStats,
in AbilityRef abilityRef,
EnabledRefRO<Dead> dead)
{
seen.Add(e);
@@ -213,6 +231,14 @@ namespace ProjectM.Client
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
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);
}
}