using UnityEditor; using UnityEditor.Animations; using UnityEngine; namespace ProjectM.EditorTools { /// /// MC-4 — builds the player's MELEE SWING clip + adds the IsAttacking param + MeleeSwing state to AC_PlayerTopDown. /// The swing clip is built FROM the full idle pose (every bone keyed) + a Root YAW twist on top. A clip that keys /// ONLY the Root makes Rukhanka collapse every un-keyed bone (Hips/Spine/legs) to identity for the state's /// duration -> the body sinks into the floor; writeDefaultValues does NOT prevent it (2026-06-11 fix). Basing on /// idle leaves nothing un-keyed; the Root yaw is a vertical-axis (height-preserving) twist that reads as a /// horizontal slash, paired with the CombatFeedbackSystem slash-arc VFX. 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() { var ac = AssetDatabase.LoadAssetAtPath(PlayerController); if (ac == null) { Debug.LogError($"[PlayerRigTools] Controller missing: {PlayerController}"); return; } var idle = FindIdleClip(ac); if (idle == null) { Debug.LogError("[PlayerRigTools] No Idle-state clip to base the swing on."); return; } // Full idle pose (every bone keyed) + a Root yaw twist. See the class summary for why a Root-only clip sinks. var clip = AssetDatabase.LoadAssetAtPath(SwingClip); if (clip == null) { clip = new AnimationClip { frameRate = 30f }; AssetDatabase.CreateAsset(clip, SwingClip); } var yaw = new AnimationCurve( new Keyframe(0f, 0f), new Keyframe(0.06f, -25f), new Keyframe(0.16f, 48f), new Keyframe(0.30f, 0f)); AnimRigUtil.BuildRootYawOverlayClip(clip, idle, yaw); if (!AnimRigUtil.HasParam(ac, "IsAttacking")) ac.AddParameter("IsAttacking", AnimatorControllerParameterType.Bool); var sm = ac.layers[0].stateMachine; var swing = FindState(sm, "MeleeSwing"); if (swing == null) { swing = sm.AddState("MeleeSwing"); swing.writeDefaultValues = false; 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 = clip; EditorUtility.SetDirty(ac); AssetDatabase.SaveAssets(); Debug.Log("[PlayerRigTools] AC_PlayerTopDown: IsAttacking + idle-based MeleeSwing built (no un-keyed-bone collapse)."); } /// LANTERN P5 (realignment): swap the Bathynaut suit body onto Player.prefab IN PLACE (GUID /// preserved — the subscene PlayerSpawner refs never break). Mirrors the proven EnemyRigTools.BuildOne /// recipe: strip the old Synty-soldier skeleton + SMR children (every ROOT component — ghost/authoring/ /// CC/Animator/RigDefinitionAuthoring — stays), flatten the suit body's skeleton + SMRs under the player /// root, keep the feet-on-ground Root offset, re-slot the deformation material, and adopt the clone's /// avatar (same shared humanoid Characters avatar — AC_PlayerTopDown's muscle clips retarget). Both /// frames share this visual until per-frame player models land. Idempotent / re-runnable. [MenuItem("ProjectM/Animation/Player - Build Bathynaut Player (LANTERN)")] public static void BuildBathynautPlayer() { const string synty = "Assets/Synty/PolygonSciFiSpace/Prefabs/Characters/SM_Chr_SpaceSoldier_HelmetArmor_Male_01.prefab"; const string matPath = "Assets/_Project/Materials/M_SuitFrame_Bathynaut_Animated.mat"; const string output = "Assets/_Project/Prefabs/Player.prefab"; const float rootY = -0.90f; // the player's existing feet-on-ground offset (entity origin = capsule center) var syntyAsset = AssetDatabase.LoadAssetAtPath(synty); var mat = AssetDatabase.LoadAssetAtPath(matPath); if (syntyAsset == null) { Debug.LogError($"[PlayerRigTools] Suit body prefab missing: {synty}"); return; } if (mat == null) { Debug.LogError($"[PlayerRigTools] Deformation material missing: {matPath}"); return; } var root = PrefabUtility.LoadPrefabContents(output); try { // Strip ONLY the old visual (skeleton + SMR children); root components stay untouched. var kids = new System.Collections.Generic.List(); foreach (Transform c in root.transform) kids.Add(c); foreach (var c in kids) Object.DestroyImmediate(c.gameObject); // Clone the suit body (unlinked) and flatten its skeleton + SMRs under the player root. var clone = (GameObject)Object.Instantiate(syntyAsset); var srcAnimator = clone.GetComponentInChildren(); var avatar = srcAnimator != null ? srcAnimator.avatar : null; var children = new System.Collections.Generic.List(); foreach (Transform c in clone.transform) children.Add(c); foreach (var c in children) c.SetParent(root.transform, false); // keep local transforms Object.DestroyImmediate(clone); // Feet-on-ground: offset the un-keyed Root bone. var rootBone = root.transform.Find("Root"); if (rootBone != null) { var lp = rootBone.localPosition; lp.y = rootY; rootBone.localPosition = lp; } else Debug.LogWarning("[PlayerRigTools] No 'Root' bone after the swap; feet offset skipped."); // Deformation material on every SMR slot (else unskinned-static under Entities Graphics). foreach (var smr in root.GetComponentsInChildren(true)) { var mats = smr.sharedMaterials; for (int i = 0; i < mats.Length; i++) mats[i] = mat; smr.sharedMaterials = mats; } var anim = root.GetComponent(); if (anim != null && avatar != null) anim.avatar = avatar; // controller (AC_PlayerTopDown) stays PrefabUtility.SaveAsPrefabAsset(root, output); Debug.Log("[PlayerRigTools] Bathynaut suit body swapped onto Player.prefab (in place)."); } finally { PrefabUtility.UnloadPrefabContents(root); } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } static AnimationClip FindIdleClip(AnimatorController ac) { foreach (var c in ac.layers[0].stateMachine.states) if (c.state.name == "Idle") return c.state.motion as AnimationClip; return null; } static AnimatorState FindState(AnimatorStateMachine sm, string name) { foreach (var c in sm.states) if (c.state.name == name) return c.state; return null; } } }