using System; using System.Collections.Generic; using System.IO; using System.Linq; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; /// /// T2 evolution visuals: each partner keeps its validated T1 prefab (mesh, rig, clips) and gains one evolution accessory /// parented to its own head bone (RockRhino: chest/back). Output {Name}_T2.prefab variants, then nest them as Heroes/HeroN/VisualT2. /// Accessory statics come from SetupCharacter at Characters/Animal/{Name}/{Name}T2/. /// public static class BuildT2Visuals { const string Animal = "Assets/Bundles/Characters/Animal/"; const string Arena = "Assets/Bundles/Scenes/Prototype/PrototypeArena.prefab"; const int BattleLayer = 30; sealed class Spec { public int Pet; public string Name, T1; /// Accessory width as a fraction of the anchor region's width; Back mounts on the chest instead of the head. public float Width = .75f, Drop; public bool Back; public string Bone; // Bone is set where the top-most vertices are not the mount: RockBall's small crystal tip (BubbleTurtle wears it on the shell top on purpose), ThunderLeopard's ears. Drop lowers the mount by a fraction of the height (SparkBird's crest otherwise lifts it). } static readonly Spec[] Specs = { new Spec { Pet = 0, Name = "RockBall", T1 = "RockBallStudio/RockBallStudio_Companion.prefab", Width = .55f, Bone = "bone_1" }, new Spec { Pet = 1, Name = "FireCat", T1 = "FireCatStudio/FireCatStudio_Companion.prefab", Width = .55f }, new Spec { Pet = 2, Name = "BudBud", T1 = "BudBudStudio/BudBudStudio_Companion.prefab", Width = .5f }, new Spec { Pet = 3, Name = "SparkBird", T1 = "SparkBirdStudio/SparkBirdStudio_Companion.prefab", Width = .6f, Drop = .1f }, new Spec { Pet = 4, Name = "BubbleTurtle", T1 = "BubbleTurtle/BubbleTurtle_Companion.prefab", Width = .55f }, new Spec { Pet = 5, Name = "RockRhino", T1 = "RockRhinoStudio/RockRhinoStudio_Companion.prefab", Width = .55f, Back = true, Bone = "bone_1" }, new Spec { Pet = 6, Name = "FlameLion", T1 = "FlameLion/FlameLion_Companion.prefab", Width = .55f }, new Spec { Pet = 7, Name = "VineSprite", T1 = "VineSpriteStudio/VineSpriteStudio_Companion.prefab", Width = .8f }, new Spec { Pet = 8, Name = "ThunderLeopard", T1 = "ThunderLeopardStudio/ThunderLeopardStudio_Companion.prefab", Width = .6f, Bone = "bone_4" }, new Spec { Pet = 9, Name = "TideWhale", T1 = "TideWhaleStudio/TideWhaleStudio_Companion.prefab", Width = .55f }, }; static string AccessoryPath(Spec s) => Animal + s.Name + "/" + s.Name + "T2/" + s.Name + "T2.prefab"; static string T2Path(Spec s) => Animal + s.Name + "/" + s.Name + "_T2.prefab"; static void SetLayer(Transform t) { t.gameObject.layer = BattleLayer; foreach (Transform c in t) SetLayer(c); } /// Bind-pose skinned points in with the dominant bone of each vertex. static List<(Vector3 p, Transform bone)> Skin(GameObject root, Transform space) { var result = new List<(Vector3, Transform)>(); var mesh = new Mesh(); try { foreach (var skin in root.GetComponentsInChildren(true)) { skin.BakeMesh(mesh, true); var weights = skin.sharedMesh.boneWeights; var bones = skin.bones; var v = mesh.vertices; for (int i = 0; i < v.Length; i++) result.Add((space.InverseTransformPoint(skin.transform.TransformPoint(v[i])), bones[weights[i].boneIndex0])); } } finally { UnityEngine.Object.DestroyImmediate(mesh); } return result; } static Bounds MeshBounds(GameObject go, Transform space) { var points = go.GetComponentsInChildren(true).SelectMany(f => f.sharedMesh.vertices.Select(v => space.InverseTransformPoint(f.transform.TransformPoint(v)))).ToArray(); var b = new Bounds(points[0], Vector3.zero); foreach (var p in points) b.Encapsulate(p); return b; } /// Builds {Name}_T2.prefab for every partner whose accessory static exists; returns one line per partner. public static string BuildPrefabs() => BuildSelected(-1); public static string BuildTideWhale() => BuildSelected(9); static string BuildSelected(int pet) { if (EditorApplication.isPlaying) throw new Exception("Build T2 visuals in Edit Mode"); var lines = new List(); var scene = EditorSceneManager.NewPreviewScene(); try { foreach (var s in Specs.Where(s => pet < 0 || s.Pet == pet)) { var accessory = AssetDatabase.LoadAssetAtPath(AccessoryPath(s)); if (accessory == null) { lines.Add(s.Name + ": accessory missing, skipped"); continue; } var t1 = AssetDatabase.LoadAssetAtPath(Animal + s.T1) ?? throw new Exception("Missing T1 " + s.T1); var root = (GameObject)PrefabUtility.InstantiatePrefab(t1, scene); var points = Skin(root, root.transform); float h = points.Max(x => x.p.y) - points.Min(x => x.p.y); // Anchor bone: the named one, else the bone owning most of the top 3% of vertices (the head). Transform bone = s.Bone != null ? root.GetComponentsInChildren(true).First(t => t.name == s.Bone) : points.OrderByDescending(x => x.p.y).Take(Math.Max(1, points.Count * 3 / 100)).GroupBy(x => x.bone).OrderByDescending(g => g.Count()).First().Key; var region = points.Where(x => x.bone == bone).Select(x => x.p).ToArray(); float cx = (region.Min(p => p.x) + region.Max(p => p.x)) / 2, width = region.Max(p => p.x) - region.Min(p => p.x); var crown = region.Where(p => Mathf.Abs(p.x - cx) < width * .2f).ToArray(); var top = crown.OrderByDescending(p => p.y).First(); float cz = crown.Where(p => p.y > top.y - h * .06f).Average(p => p.z); var acc = (GameObject)PrefabUtility.InstantiatePrefab(accessory, scene); acc.name = "T2Accessory"; acc.transform.SetParent(root.transform, false); var b = MeshBounds(acc, acc.transform); float scale = s.Width * width / Mathf.Max(1e-4f, Mathf.Max(b.size.x, b.size.z)); acc.transform.localScale = Vector3.one * scale; // Sink the base a quarter of its height into the head/back so it reads as worn, not floating. float accHeight = b.size.y * scale; acc.transform.localPosition = new Vector3(cx - b.center.x * scale, top.y - b.min.y * scale - accHeight * .25f - s.Drop * h, cz - b.center.z * scale); acc.transform.SetParent(bone, true); PrefabUtility.SaveAsPrefabAsset(root, T2Path(s)); lines.Add(s.Name + ": " + T2Path(s) + " bone=" + bone.name + " width=" + width.ToString("F3") + " accScale=" + scale.ToString("F3") + " top=" + (top.y + accHeight * .75f).ToString("F3") + "/" + h.ToString("F3")); UnityEngine.Object.DestroyImmediate(root); } } finally { EditorSceneManager.ClosePreviewScene(scene); } return string.Join("\n", lines); } /// Nests each {Name}_T2 prefab as Heroes/HeroN/VisualT2 (inactive, same scale as Visual); runtime shows it for tier 2. public static string ApplyArena() => ApplySelected(-1); public static string ApplyTideWhale() => ApplySelected(9); static string ApplySelected(int pet) { if (EditorApplication.isPlaying) throw new Exception("Apply T2 visuals in Edit Mode"); var root = PrefabUtility.LoadPrefabContents(Arena); var done = new List(); try { foreach (var s in Specs.Where(s => pet < 0 || s.Pet == pet)) { var t2 = AssetDatabase.LoadAssetAtPath(T2Path(s)); if (t2 == null) continue; var hero = root.transform.Find("Heroes/Hero" + s.Pet) ?? throw new Exception("Arena missing Hero" + s.Pet); var visual = hero.Find("Visual") ?? throw new Exception("Hero" + s.Pet + " has no formal T1 Visual"); var old = hero.Find("VisualT2"); if (old != null) UnityEngine.Object.DestroyImmediate(old.gameObject); var go = (GameObject)PrefabUtility.InstantiatePrefab(t2, root.scene); go.name = "VisualT2"; go.transform.SetParent(hero, false); go.transform.localPosition = visual.localPosition; go.transform.localRotation = visual.localRotation; go.transform.localScale = visual.localScale; SetLayer(go.transform); go.SetActive(false); done.Add(s.Pet); } PrefabUtility.SaveAsPrefabAsset(root, Arena); } finally { PrefabUtility.UnloadPrefabContents(root); } return "Arena VisualT2 nested for heroes " + string.Join(",", done); } public static string VerifyComplete() { foreach (var s in Specs) if (AssetDatabase.LoadAssetAtPath(T2Path(s)) == null) throw new Exception("Missing T2 " + s.Name); return Verify(); } public static string Verify() { var arena = AssetDatabase.LoadAssetAtPath(Arena); var lines = new List(); foreach (var s in Specs) { var t2 = AssetDatabase.LoadAssetAtPath(T2Path(s)); if (t2 == null) { lines.Add(s.Name + "=missing"); continue; } var acc = t2.GetComponentsInChildren(true).FirstOrDefault(t => t.name == "T2Accessory"); if (acc == null || acc.parent == t2.transform || acc.GetComponentInParent() != null) throw new Exception(s.Name + " accessory not parented to a bone"); if (PrefabUtility.GetCorrespondingObjectFromSource(t2) == null) throw new Exception(s.Name + "_T2 is not a variant of its T1 prefab"); var hero = arena.transform.Find("Heroes/Hero" + s.Pet); var v2 = hero?.Find("VisualT2"); if (v2 == null || v2.gameObject.activeSelf || v2.GetComponentsInChildren(true).Any(t => t.gameObject.layer != BattleLayer)) throw new Exception("Hero" + s.Pet + " VisualT2 missing/active/wrong layer"); if ((v2.localScale - hero.Find("Visual").localScale).sqrMagnitude > 1e-8f) throw new Exception("Hero" + s.Pet + " VisualT2 scale differs from Visual"); lines.Add(s.Name + "=" + acc.parent.name); } return "PASS T2 " + string.Join(", ", lines); } }