Files

162 lines
10 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
/// <summary>
/// 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/.
/// </summary>
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;
/// <summary>Accessory width as a fraction of the anchor region's width; Back mounts on the chest instead of the head.</summary>
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); }
/// <summary>Bind-pose skinned points in <paramref name="space"/> with the dominant bone of each vertex.</summary>
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<SkinnedMeshRenderer>(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<MeshFilter>(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;
}
/// <summary>Builds {Name}_T2.prefab for every partner whose accessory static exists; returns one line per partner.</summary>
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<string>();
var scene = EditorSceneManager.NewPreviewScene();
try
{
foreach (var s in Specs.Where(s => pet < 0 || s.Pet == pet))
{
var accessory = AssetDatabase.LoadAssetAtPath<GameObject>(AccessoryPath(s));
if (accessory == null) { lines.Add(s.Name + ": accessory missing, skipped"); continue; }
var t1 = AssetDatabase.LoadAssetAtPath<GameObject>(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<Transform>(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);
}
/// <summary>Nests each {Name}_T2 prefab as Heroes/HeroN/VisualT2 (inactive, same scale as Visual); runtime shows it for tier 2.</summary>
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<int>();
try
{
foreach (var s in Specs.Where(s => pet < 0 || s.Pet == pet))
{
var t2 = AssetDatabase.LoadAssetAtPath<GameObject>(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<GameObject>(T2Path(s)) == null) throw new Exception("Missing T2 " + s.Name);
return Verify();
}
public static string Verify()
{
var arena = AssetDatabase.LoadAssetAtPath<GameObject>(Arena); var lines = new List<string>();
foreach (var s in Specs)
{
var t2 = AssetDatabase.LoadAssetAtPath<GameObject>(T2Path(s)); if (t2 == null) { lines.Add(s.Name + "=missing"); continue; }
var acc = t2.GetComponentsInChildren<Transform>(true).FirstOrDefault(t => t.name == "T2Accessory");
if (acc == null || acc.parent == t2.transform || acc.GetComponentInParent<SkinnedMeshRenderer>() != 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<Transform>(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);
}
}