using UnityEditor; using UnityEditor.Animations; using UnityEngine; namespace ProjectM.EditorTools { /// /// 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). /// 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(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(SwingClip); var swing = FindState(sm, "MeleeSwing"); if (swing == null) { swing = sm.AddState("MeleeSwing"); swing.writeDefaultValues = false; // partial (Root-only) clip: MUST be false, else WDV resets every un-keyed bone (Hips/Spine/legs) to identity and the body collapses into the floor 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; } } }