using System; using System.Collections.Generic; using System.Reflection; using UnityEditor; using UnityEditor.Animations; using UnityEngine; namespace ProjectM.EditorTools { /// /// Reusable editor pipeline that turns Synty Polygon character meshes into ANIMATED interpolated-ghost enemy /// prefabs, mirroring the DR-022 player-rig recipe so the same Rukhanka client-derived animation works for /// Husks (see DR-023). Three idempotent, re-runnable steps (menu: ProjectM/Animation): /// 1. Build deformation materials (duplicate the player's AnimatedLitShader mat, swap the pack atlas). /// 2. Build AC_EnemyTopDown (fork AC_PlayerTopDown + an Attack state) and the EnemyAttackWindup clip. /// 3. Build the 3 animated enemy prefabs (clone the capsule ghost template, strip the capsule, flatten the /// Synty skeleton + SMRs under the ghost root, add Animator + RigDefinitionAuthoring, set the deformation /// material, offset the Root bone for feet-on-ground). /// RigDefinitionAuthoring is set via reflection (no Rukhanka asmdef ref needed — same "by name" tactic as /// ServerStripAnimationSystem). Outputs are NEW prefabs (templates stay pristine capsules); re-point the /// gameplay subscene WaveDirector.EnemyPrefabs[] at them. /// public static class EnemyRigTools { const string PlayerMat = "Assets/_Project/Materials/M_SpaceSoldier_Animated.mat"; const string PlayerController = "Assets/_Project/Animation/AC_PlayerTopDown.controller"; const string EnemyController = "Assets/_Project/Animation/AC_EnemyTopDown.controller"; const string AttackClip = "Assets/_Project/Animation/EnemyAttackWindup.anim"; const string WerewolfAtlas = "Assets/Synty/PolygonWerewolf/Textures/PolygonWerewolf_01_A.png"; const string KaijuAtlas = "Assets/Synty/PolygonKaiju/Textures/PolygonKaiju_01.png"; const string MatWerewolf = "Assets/_Project/Materials/M_Enemy_Werewolf_Animated.mat"; const string MatWerewolfUndead = "Assets/_Project/Materials/M_Enemy_WerewolfUndead_Animated.mat"; const string MatKaiju = "Assets/_Project/Materials/M_Enemy_Kaiju_Animated.mat"; const string SyntyWerewolf = "Assets/Synty/PolygonWerewolf/Prefabs/Characters/SM_Chr_Werewolf_01.prefab"; const string SyntyKaiju = "Assets/Synty/PolygonKaiju/Prefabs/Characters/SM_Chr_Kaiju_01.prefab"; // MC-1 Charger (SciFi-City Muscle — verified Generic rig, distinct charging silhouette; see Synty_Asset_Inventory). const string ChargerAtlas = "Assets/Synty/PolygonSciFiCity/Textures/Alts/PolygonScifi_01_A.png"; const string MatCharger = "Assets/_Project/Materials/M_Enemy_Charger_Animated.mat"; const string SyntyMuscle = "Assets/Synty/PolygonSciFiCity/Prefabs/Characters/SM_Chr_Muscle_Male_01.prefab"; struct Variant { public string Name, Template, Synty, Output, Material; public float RootY; // Root-bone local Y (capsule-center -> feet; tuned per scale in Play) public float Scale; // 0 = keep the template's baked scale } static Variant[] Variants() => new[] { new Variant { Name = "Grunt (Werewolf)", Template = "Assets/_Project/Prefabs/Enemy.prefab", Synty = SyntyWerewolf, Output = "Assets/_Project/Prefabs/EnemyWerewolf.prefab", Material = MatWerewolf, RootY = -1.25f, Scale = 0f }, new Variant { Name = "Swarmer (Werewolf Undead)", Template = "Assets/_Project/Prefabs/EnemySwarmer.prefab", Synty = SyntyWerewolf, Output = "Assets/_Project/Prefabs/EnemyWerewolfUndead.prefab", Material = MatWerewolfUndead, RootY = -1.67f, Scale = 0f }, new Variant { Name = "Brute (Kaiju)", Template = "Assets/_Project/Prefabs/EnemyBrute.prefab", Synty = SyntyKaiju, Output = "Assets/_Project/Prefabs/EnemyKaiju.prefab", Material = MatKaiju, RootY = -0.52f, Scale = 0f }, ChargerVariant(), }; /// MC-1 Charger: SciFi-City Muscle silhouette via the standard DR-023 pipeline (the inventory's prescribed next-faction path). Template scale 1.0 -> RootY -1.0 (humanoid -1/scale rule); fine-tune feet in Play. static Variant ChargerVariant() => new Variant { Name = "Charger (SciFi Muscle)", Template = "Assets/_Project/Prefabs/EnemyCharger.prefab", Synty = SyntyMuscle, Output = "Assets/_Project/Prefabs/EnemyChargerMuscle.prefab", Material = MatCharger, RootY = -1.00f, Scale = 0f }; [MenuItem("ProjectM/Animation/Enemy Rigs - Build All")] public static void BuildAll() { BuildMaterials(); BuildControllerAndClip(); BuildPrefabs(); Debug.Log("[EnemyRigTools] Build All complete. Re-point WaveDirector.EnemyPrefabs[] at the new prefabs and re-bake the gameplay subscene."); } /// MC-1: build ONLY the Charger material + prefab (leaves the committed Werewolf/Kaiju outputs untouched). [MenuItem("ProjectM/Animation/Enemy Rigs - Build Charger (MC-1)")] public static void BuildCharger() { MakeMat(MatCharger, ChargerAtlas, new Color(1f, 0.42f, 0.36f, 1f)); // red-shifted SciFi atlas = danger read AssetDatabase.SaveAssets(); BuildOne(ChargerVariant()); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); Debug.Log("[EnemyRigTools] Charger built -> EnemyChargerMuscle.prefab; add it to WaveDirector.EnemyPrefabs[] in the gameplay subscene."); } // ---- 1. Materials ------------------------------------------------------------------------------------ [MenuItem("ProjectM/Animation/Enemy Rigs - 1 Build Materials")] public static void BuildMaterials() { MakeMat(MatWerewolf, WerewolfAtlas, null); // Undead = the same werewolf atlas with a sickly desaturated-green tint so the Swarmer reads distinct. MakeMat(MatWerewolfUndead, WerewolfAtlas, new Color(0.62f, 0.78f, 0.55f, 1f)); MakeMat(MatKaiju, KaijuAtlas, null); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); Debug.Log("[EnemyRigTools] Materials built."); } static void MakeMat(string dst, string atlas, Color? tint) { if (AssetDatabase.LoadAssetAtPath(PlayerMat) == null) { Debug.LogError($"[EnemyRigTools] Source material missing: {PlayerMat}"); return; } AssetDatabase.DeleteAsset(dst); if (!AssetDatabase.CopyAsset(PlayerMat, dst)) { Debug.LogError($"[EnemyRigTools] CopyAsset failed -> {dst}"); return; } AssetDatabase.ImportAsset(dst); var m = AssetDatabase.LoadAssetAtPath(dst); var tex = AssetDatabase.LoadAssetAtPath(atlas); if (tex == null) Debug.LogWarning($"[EnemyRigTools] Atlas not found: {atlas}"); if (m.HasProperty("_BaseColorMap") && tex != null) m.SetTexture("_BaseColorMap", tex); if (tint.HasValue && m.HasProperty("_BaseColor")) m.SetColor("_BaseColor", tint.Value); EditorUtility.SetDirty(m); } // ---- 2. Controller + attack clip -------------------------------------------------------------------- [MenuItem("ProjectM/Animation/Enemy Rigs - 2 Build Controller")] public static void BuildControllerAndClip() { // Attack wind-up: pitch the Root bone forward (whole-body lunge) and back, non-looping. Root is the // top of every Synty Generic rig, so one clip fits all variants; the locomotion clips don't key Root, // so no conflict and Root returns to its authored Y-offset when the state exits. var clip = new AnimationClip { frameRate = 30f }; var pitch = new AnimationCurve(new Keyframe(0f, 0f), new Keyframe(0.15f, 30f), new Keyframe(0.40f, 0f)); 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(AttackClip); AssetDatabase.CreateAsset(clip, AttackClip); AssetDatabase.SaveAssets(); if (AssetDatabase.LoadAssetAtPath(PlayerController) == null) { Debug.LogError($"[EnemyRigTools] Source controller missing: {PlayerController}"); return; } AssetDatabase.DeleteAsset(EnemyController); AssetDatabase.CopyAsset(PlayerController, EnemyController); AssetDatabase.ImportAsset(EnemyController); var ac = AssetDatabase.LoadAssetAtPath(EnemyController); if (!HasParam(ac, "IsAttacking")) ac.AddParameter("IsAttacking", AnimatorControllerParameterType.Bool); var sm = ac.layers[0].stateMachine; var attackClipAsset = AssetDatabase.LoadAssetAtPath(AttackClip); var attack = sm.AddState("Attack"); attack.motion = attackClipAsset; attack.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 toAttack = sm.AddAnyStateTransition(attack); toAttack.hasExitTime = false; toAttack.duration = 0.06f; toAttack.canTransitionToSelf = false; toAttack.AddCondition(AnimatorConditionMode.If, 0f, "IsAttacking"); var fromAttack = attack.AddExitTransition(); fromAttack.hasExitTime = false; fromAttack.duration = 0.12f; fromAttack.AddCondition(AnimatorConditionMode.IfNot, 0f, "IsAttacking"); EditorUtility.SetDirty(ac); AssetDatabase.SaveAssets(); Debug.Log("[EnemyRigTools] AC_EnemyTopDown + EnemyAttackWindup clip built."); } static bool HasParam(AnimatorController ac, string name) { foreach (var p in ac.parameters) if (p.name == name) return true; return false; } // ---- 3. Prefabs -------------------------------------------------------------------------------------- [MenuItem("ProjectM/Animation/Enemy Rigs - 3 Build Prefabs")] public static void BuildPrefabs() { foreach (var v in Variants()) BuildOne(v); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); Debug.Log("[EnemyRigTools] Animated enemy prefabs built. Re-point WaveDirector.EnemyPrefabs[] and re-bake."); } static void BuildOne(Variant v) { if (AssetDatabase.LoadAssetAtPath(v.Template) == null) { Debug.LogError($"[EnemyRigTools] Template missing: {v.Template}"); return; } if (AssetDatabase.LoadAssetAtPath(v.Synty) == null) { Debug.LogError($"[EnemyRigTools] Synty char prefab missing: {v.Synty}"); return; } // GUID-preserving: COPY the template only on the FIRST build (mints the output + its GUID). On a // re-run, modify the existing output IN PLACE so its GUID stays stable -> the gameplay subscene's // WaveDirector.EnemyPrefabs[] references never break. (DeleteAsset+CopyAsset would mint a new GUID // each run and silently orphan those refs.) if (AssetDatabase.LoadAssetAtPath(v.Output) == null) { if (!AssetDatabase.CopyAsset(v.Template, v.Output)) { Debug.LogError($"[EnemyRigTools] CopyAsset failed -> {v.Output}"); return; } AssetDatabase.ImportAsset(v.Output); } var root = PrefabUtility.LoadPrefabContents(v.Output); try { // Clean slate for idempotent re-runs: drop any previously-built rig (children + rig components) // and the primitive capsule visual; keep the root's ghost/authoring components + transform. var kids = new List(); foreach (Transform c in root.transform) kids.Add(c); foreach (var c in kids) UnityEngine.Object.DestroyImmediate(c.gameObject); foreach (var mf in root.GetComponents()) UnityEngine.Object.DestroyImmediate(mf); foreach (var mr in root.GetComponents()) UnityEngine.Object.DestroyImmediate(mr); foreach (var an in root.GetComponents()) UnityEngine.Object.DestroyImmediate(an); var oldRda = FindType("Rukhanka.Hybrid.RigDefinitionAuthoring"); if (oldRda != null) { var ex = root.GetComponent(oldRda); if (ex != null) UnityEngine.Object.DestroyImmediate(ex); } // Optional scale override (else keep the template's baked variant scale). if (v.Scale > 0f) root.transform.localScale = new Vector3(v.Scale, v.Scale, v.Scale); // Clone the Synty character (unlinked) and flatten its skeleton + SMRs under the ghost root. var syntyAsset = AssetDatabase.LoadAssetAtPath(v.Synty); var clone = (GameObject)UnityEngine.Object.Instantiate(syntyAsset); var srcAnimator = clone.GetComponentInChildren(); var avatar = srcAnimator != null ? srcAnimator.avatar : null; var children = new List(); foreach (Transform c in clone.transform) children.Add(c); foreach (var c in children) c.SetParent(root.transform, false); // keep local transforms UnityEngine.Object.DestroyImmediate(clone); // Feet-on-ground: offset the un-keyed Root bone (entity origin = capsule center ~1m up). var rootBone = root.transform.Find("Root"); if (rootBone != null) { var lp = rootBone.localPosition; lp.y = v.RootY; rootBone.localPosition = lp; } else Debug.LogWarning($"[EnemyRigTools] No 'Root' bone under {v.Output}; feet offset skipped."); // Deformation material on every SMR slot (else unskinned-static + "does not support skinning"). var mat = AssetDatabase.LoadAssetAtPath(v.Material); if (mat == null) Debug.LogWarning($"[EnemyRigTools] Material missing: {v.Material}"); 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; } // Animator on the ghost root (the entity that holds EnemyAuthoring + the Rukhanka param buffer). var anim = root.GetComponent(); if (anim == null) anim = root.AddComponent(); anim.avatar = avatar; anim.runtimeAnimatorController = AssetDatabase.LoadAssetAtPath(EnemyController); anim.applyRootMotion = false; // RigDefinitionAuthoring (Rukhanka.Hybrid) via reflection — CPU engine, match the player rig. AddRigDefinition(root); PrefabUtility.SaveAsPrefabAsset(root, v.Output); Debug.Log($"[EnemyRigTools] Built {v.Name} -> {v.Output} (rootY={v.RootY})."); } finally { PrefabUtility.UnloadPrefabContents(root); } } static void AddRigDefinition(GameObject root) { var t = FindType("Rukhanka.Hybrid.RigDefinitionAuthoring"); if (t == null) { Debug.LogError("[EnemyRigTools] RigDefinitionAuthoring type not found (Rukhanka.Hybrid)."); return; } var rda = root.GetComponent(t); if (rda == null) rda = root.AddComponent(t); SetMember(rda, "applyRootMotion", false); SetMember(rda, "boneEntityStrippingMode", 1); // match Player.prefab SetMember(rda, "animationEngine", 0); // 0 = CPU EditorUtility.SetDirty(rda); } static Type FindType(string fullName) { foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) { var t = asm.GetType(fullName); if (t != null) return t; } return null; } static void SetMember(object o, string name, object val) { const BindingFlags BF = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; var f = o.GetType().GetField(name, BF); if (f != null) { object v = f.FieldType.IsEnum ? Enum.ToObject(f.FieldType, Convert.ToInt32(val)) : Convert.ChangeType(val, f.FieldType); f.SetValue(o, v); return; } var p = o.GetType().GetProperty(name, BF); if (p != null && p.CanWrite) { object v = p.PropertyType.IsEnum ? Enum.ToObject(p.PropertyType, Convert.ToInt32(val)) : Convert.ChangeType(val, p.PropertyType); p.SetValue(o, v); return; } Debug.LogWarning($"[EnemyRigTools] Field/prop '{name}' not found on {o.GetType().Name}."); } } }