210d307406
New PlayerRigTools.BuildBathynautPlayer (menu: ProjectM/Animation) - forks the proven EnemyRigTools recipe: strips the Synty-soldier skeleton+SMRs from Player.prefab IN PLACE (GUID preserved; ghost/authoring/CC/Animator/ RigDefinitionAuthoring untouched), flattens the SpaceSoldier_HelmetArmor suit body under the root, keeps the -0.90 feet offset, re-slots M_SuitFrame_Bathynaut_Animated on every SMR, adopts the shared CharactersAvatar (AC_PlayerTopDown muscle clips retarget). 17 inactive Synty variant SMRs stripped. Both frames share this visual until per-frame models land; the brass-dome/tank kitbash attachments are a follow-up /art-dev export. Play-verified in DevSandbox: suit renders + idles correctly under the LANTERN murk, Rukhanka rig baked with all 9 drive params, dev overlay alive in the renamed scene, no console errors. => Movement/animation tuning is now UNBLOCKED (the operator's next priority). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
144 lines
7.8 KiB
C#
144 lines
7.8 KiB
C#
using UnityEditor;
|
|
using UnityEditor.Animations;
|
|
using UnityEngine;
|
|
|
|
namespace ProjectM.EditorTools
|
|
{
|
|
/// <summary>
|
|
/// 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).
|
|
/// </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()
|
|
{
|
|
var ac = AssetDatabase.LoadAssetAtPath<AnimatorController>(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<AnimationClip>(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).");
|
|
}
|
|
|
|
/// <summary>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.</summary>
|
|
[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<GameObject>(synty);
|
|
var mat = AssetDatabase.LoadAssetAtPath<Material>(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<Transform>();
|
|
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<Animator>();
|
|
var avatar = srcAnimator != null ? srcAnimator.avatar : null;
|
|
var children = new System.Collections.Generic.List<Transform>();
|
|
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<SkinnedMeshRenderer>(true))
|
|
{
|
|
var mats = smr.sharedMaterials;
|
|
for (int i = 0; i < mats.Length; i++) mats[i] = mat;
|
|
smr.sharedMaterials = mats;
|
|
}
|
|
|
|
var anim = root.GetComponent<Animator>();
|
|
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;
|
|
}
|
|
}
|
|
}
|