352bf3322d
Rukhanka swing animation: PlayerRigTools builds a procedural Root-bone PlayerMeleeSwing.anim and adds an IsAttacking param + MeleeSwing state to AC_PlayerTopDown (mirroring the enemy attack recipe -- no authored Synty Generic melee clip exists). PlayerAnimationDriveSystem pulses IsAttacking from the replicated MeleeCombo swing window (local + remote, NetworkTick wrap-safe, re-triggers per chained hit). CombatFeedbackSystem flashes a procedural cone slash-arc mesh matching the LIVE cleave range + half-angle on each swing (finisher wider/warmer) -- the arc IS the range telegraph. Addresses 'range isn't clear + no animation'. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
86 lines
4.2 KiB
C#
86 lines
4.2 KiB
C#
using UnityEditor;
|
|
using UnityEditor.Animations;
|
|
using UnityEngine;
|
|
|
|
namespace ProjectM.EditorTools
|
|
{
|
|
/// <summary>
|
|
/// MC-4 — builds the player's procedural MELEE SWING clip and adds the IsAttacking param + MeleeSwing state to
|
|
/// AC_PlayerTopDown. Mirrors EnemyRigTools' attack recipe (a Root-bone procedural clip — no authored Synty
|
|
/// Generic melee-swing clip exists; the slash-arc VFX in CombatFeedbackSystem carries the blade read). The clip
|
|
/// keys ONLY Root rotation (a yaw twist into the swing + a slight forward pitch), leaving the rig's authored
|
|
/// Root Y offset (feet-on-ground) untouched. PlayerAnimationDriveSystem drives IsAttacking from MeleeCombo.
|
|
/// Idempotent / re-runnable (menu: ProjectM/Animation).
|
|
/// </summary>
|
|
public static class PlayerRigTools
|
|
{
|
|
const string SwingClip = "Assets/_Project/Animation/PlayerMeleeSwing.anim";
|
|
const string PlayerController = "Assets/_Project/Animation/AC_PlayerTopDown.controller";
|
|
|
|
[MenuItem("ProjectM/Animation/Player - Build Melee Swing")]
|
|
public static void BuildPlayerMeleeSwing()
|
|
{
|
|
// Horizontal-cleave read: yaw the Root (wind back -> swing across -> recover) + a slight forward pitch.
|
|
// Root is the top of the Synty Generic rig; locomotion clips don't key Root, so no conflict and it
|
|
// returns to the authored pose on exit. Rotation-only (no position) so the baked Root Y offset stays.
|
|
var clip = new AnimationClip { frameRate = 30f };
|
|
var yaw = new AnimationCurve(
|
|
new Keyframe(0f, 0f), new Keyframe(0.06f, -22f), new Keyframe(0.16f, 34f), new Keyframe(0.32f, 0f));
|
|
var pitch = new AnimationCurve(
|
|
new Keyframe(0f, 0f), new Keyframe(0.14f, 16f), new Keyframe(0.32f, 0f));
|
|
AnimationUtility.SetEditorCurve(clip, EditorCurveBinding.FloatCurve("Root", typeof(Transform), "localEulerAnglesRaw.y"), yaw);
|
|
AnimationUtility.SetEditorCurve(clip, EditorCurveBinding.FloatCurve("Root", typeof(Transform), "localEulerAnglesRaw.x"), pitch);
|
|
var s = AnimationUtility.GetAnimationClipSettings(clip);
|
|
s.loopTime = false;
|
|
AnimationUtility.SetAnimationClipSettings(clip, s);
|
|
AssetDatabase.DeleteAsset(SwingClip);
|
|
AssetDatabase.CreateAsset(clip, SwingClip);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
var ac = AssetDatabase.LoadAssetAtPath<AnimatorController>(PlayerController);
|
|
if (ac == null) { Debug.LogError($"[PlayerRigTools] Controller missing: {PlayerController}"); return; }
|
|
|
|
if (!HasParam(ac, "IsAttacking"))
|
|
ac.AddParameter("IsAttacking", AnimatorControllerParameterType.Bool);
|
|
|
|
var sm = ac.layers[0].stateMachine;
|
|
var swingClipAsset = AssetDatabase.LoadAssetAtPath<AnimationClip>(SwingClip);
|
|
|
|
var swing = FindState(sm, "MeleeSwing");
|
|
if (swing == null)
|
|
{
|
|
swing = sm.AddState("MeleeSwing");
|
|
swing.writeDefaultValues = true;
|
|
|
|
var toSwing = sm.AddAnyStateTransition(swing);
|
|
toSwing.hasExitTime = false;
|
|
toSwing.duration = 0.05f;
|
|
toSwing.canTransitionToSelf = false;
|
|
toSwing.AddCondition(AnimatorConditionMode.If, 0f, "IsAttacking");
|
|
|
|
var fromSwing = swing.AddExitTransition();
|
|
fromSwing.hasExitTime = false;
|
|
fromSwing.duration = 0.10f;
|
|
fromSwing.AddCondition(AnimatorConditionMode.IfNot, 0f, "IsAttacking");
|
|
}
|
|
swing.motion = swingClipAsset; // (re)point at the freshly-built clip
|
|
|
|
EditorUtility.SetDirty(ac);
|
|
AssetDatabase.SaveAssets();
|
|
Debug.Log("[PlayerRigTools] AC_PlayerTopDown: IsAttacking + MeleeSwing state built; PlayerMeleeSwing.anim created.");
|
|
}
|
|
|
|
static bool HasParam(AnimatorController ac, string name)
|
|
{
|
|
foreach (var p in ac.parameters) if (p.name == name) return true;
|
|
return false;
|
|
}
|
|
|
|
static AnimatorState FindState(AnimatorStateMachine sm, string name)
|
|
{
|
|
foreach (var c in sm.states) if (c.state.name == name) return c.state;
|
|
return null;
|
|
}
|
|
}
|
|
}
|