162 lines
10 KiB
C#
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);
|
|
}
|
|
}
|