using System.Collections.Generic; using System.IO; using System.Linq; using Luneraria.View; using UnityEditor; using UnityEditor.Animations; using UnityEngine; using UnityEngine.Rendering; namespace Luneraria.EditorTools { /// /// Builds what the unit views need (U2C): a one-mesh version of the rigid Kohlenbeißer (131 /// named parts combined per material, like Godot's rig groups), animator controllers with the /// Blender clips of the rigged figures, team ring and bar meshes and their materials. /// public static class LuneUnitLibraryBuilder { public const string LibraryPath = "Assets/Luneraria/Settings/LuneUnits.asset"; const string Generated = "Assets/Luneraria/Art/Generated"; const string Art = "Assets/Luneraria/Art/"; public static LuneUnitLibrary Build(LuneMaterialSet materials, LuneGround ground) { Directory.CreateDirectory(Generated); AssetDatabase.Refresh(); var library = AssetDatabase.LoadAssetAtPath(LibraryPath); if (library == null) { library = ScriptableObject.CreateInstance(); AssetDatabase.CreateAsset(library, LibraryPath); } library.Materials = materials; library.Ground = ground; library.Models = Models(); library.RingRound = Keep(Ring("RingRound", 40, 0f)); library.RingDiamond = Keep(Ring("RingDiamond", 4, 45f)); library.RingHexagon = Keep(Ring("RingHexagon", 6, 0f)); library.OwnerRingRound = Keep(Ring("OwnerRingRound", 40, 0f, 1f, 1.12f)); library.OwnerRingDiamond = Keep(Ring("OwnerRingDiamond", 4, 45f, 1f, 1.12f)); library.Quad = Keep(Quad()); for (int team = 0; team < 3; team++) { library.TeamRing[team] = Marker("Ring_" + team, LuneMaterialFactory.TeamColor[team], team == 2 ? .6f : 1.4f, false); library.BarFill[team] = Marker("BarFill_" + team, Color.Lerp(LuneMaterialFactory.TeamColor[team], Color.white, .15f), 1f, true); library.Bolt[team] = Marker("Bolt_" + team, LuneMaterialFactory.TeamLight[team], 3f, false); } library.HeroRing = Marker("Ring_Hero", LuneMaterialFactory.Hex("f2c96b"), 2.2f, false); for (int team = 0; team < 2; team++) { library.OwnerRing[team] = Marker("Ring_Owner_" + team, LuneMaterialFactory.TeamColor[team], 3f, false); library.AncientBarFill[team] = Marker("AncientBarFill_" + team, Color.Lerp(LuneMaterialFactory.TeamLight[team], Color.white, .15f), 1f, true); } library.BarBack = Marker("BarBack", new Color(0f, 0f, 0f, .7f), 1f, true); EditorUtility.SetDirty(library); AssetDatabase.SaveAssets(); return library; } /// Groups whose models stand for units; buildings and environment have their own views. static readonly string[] UnitGroups = { "Units", "Enemies", "Characters", "Peoples" }; /// /// One entry per exported unit model (tools/blender/export_manifest.json): rigid models by /// their id, skinned figures with an animator controller. A skinned figure also stands in /// for the Godot id it replaced (w04 is Ringborke, worker is the Arbeiter, ...). /// static List Models() { var manifest = Newtonsoft.Json.Linq.JObject.Parse(File.ReadAllText(Path.Combine(Path.GetDirectoryName(Application.dataPath), "tools/blender/export_manifest.json"))); var models = new List(); var aliases = new List(); foreach (var asset in manifest["assets"]) { string id = (string)asset["id"]; string group = (string)asset["group"]; if (!UnitGroups.Contains(group)) continue; var prefab = AssetDatabase.LoadAssetAtPath(Art + group + "/" + id + ".fbx"); if (prefab == null) continue; bool skinned = (string)asset["kind"] == "skinned"; var model = new LuneUnitLibrary.UnitModel { Id = id, Prefab = prefab, Controller = skinned ? Controller(id, Art + group + "/" + id + ".fbx") : null }; models.Add(model); string godotId = (string)asset["godot_manifest"]?["id"]; if (skinned && godotId != null && godotId != id) aliases.Add(new LuneUnitLibrary.UnitModel { Id = godotId, Prefab = prefab, Controller = model.Controller }); } // Skinned figures replace the rigid model of the same Godot id. foreach (LuneUnitLibrary.UnitModel alias in aliases) { models.RemoveAll(m => m.Id == alias.Id); models.Add(alias); } return models.OrderBy(m => m.Id, System.StringComparer.Ordinal).ToList(); } /// One state per clip of the model (named like the Blender action), idle first. static RuntimeAnimatorController Controller(string id, string fbx) { // Updated in place (stable GUID, no duplicates): existing states are replaced. string path = Generated + "/" + id + ".controller"; var controller = AssetDatabase.LoadAssetAtPath(path) ?? AnimatorController.CreateAnimatorControllerAtPath(path); AnimatorStateMachine machine = controller.layers[0].stateMachine; AnimationClip[] clips = AssetDatabase.LoadAllAssetsAtPath(fbx).OfType().Where(c => !c.name.StartsWith("__preview__")) .OrderBy(c => c.name == "idle" ? 0 : 1).ThenBy(c => c.name).ToArray(); // Unchanged clips: keep the states (rebuilding them gave new file ids on every build). ChildAnimatorState[] current = machine.states; bool same = current.Length == clips.Length && current.Select((child, index) => child.state.name == clips[index].name && child.state.motion == clips[index]).All(match => match); if (same) return controller; foreach (ChildAnimatorState child in machine.states.ToArray()) machine.RemoveState(child.state); foreach (AnimationClip clip in clips) { AnimatorState state = machine.AddState(clip.name); state.motion = clip; if (clip.name == "idle") machine.defaultState = state; } EditorUtility.SetDirty(controller); return controller; } /// Flat ring on the ground (Godot: TorusMesh inner .42, outer .52, flattened). static Mesh Ring(string name, int sides, float offsetDegrees, float inner = .42f, float outer = .52f) { var vertices = new List(); var triangles = new List(); for (int i = 0; i <= sides; i++) { float angle = (offsetDegrees + 360f * i / sides) * Mathf.Deg2Rad; var direction = new Vector3(Mathf.Cos(angle), 0f, Mathf.Sin(angle)); vertices.Add(direction * inner); vertices.Add(direction * outer); if (i < sides) { int a = i * 2; triangles.AddRange(new[] { a, a + 1, a + 3, a, a + 3, a + 2 }); } } var mesh = new Mesh { name = name }; mesh.SetVertices(vertices); mesh.SetTriangles(triangles, 0); mesh.RecalculateNormals(); mesh.RecalculateBounds(); return mesh; } static Mesh Quad() { var mesh = new Mesh { name = "MarkerQuad" }; mesh.SetVertices(new List { new Vector3(-.5f, -.5f, 0f), new Vector3(.5f, -.5f, 0f), new Vector3(-.5f, .5f, 0f), new Vector3(.5f, .5f, 0f) }); mesh.SetTriangles(new[] { 0, 2, 1, 2, 3, 1 }, 0); mesh.RecalculateNormals(); mesh.RecalculateBounds(); return mesh; } static Mesh Keep(Mesh mesh) { string path = Generated + "/" + mesh.name + ".asset"; var existing = AssetDatabase.LoadAssetAtPath(path); if (existing == null) { AssetDatabase.CreateAsset(mesh, path); return mesh; } EditorUtility.CopySerialized(mesh, existing); EditorUtility.SetDirty(existing); return existing; } /// Unshaded marker colour (Godot _flat with emission energy); bars draw on top. static Material Marker(string name, Color srgb, float energy, bool onTop) { string path = LuneMaterialFactory.Folder + "/Markers/" + name + ".mat"; Directory.CreateDirectory(Path.GetDirectoryName(path)); Shader shader = Shader.Find("Luneraria/Unlit"); var material = AssetDatabase.LoadAssetAtPath(path); if (material == null) { material = new Material(shader); AssetDatabase.CreateAsset(material, path); } material.shader = shader; Color linear = srgb.linear * energy; material.SetColor("_Color", new Color(linear.r, linear.g, linear.b, srgb.a)); material.SetFloat("_ZTest", onTop ? (float)CompareFunction.Always : (float)CompareFunction.LessEqual); material.enableInstancing = true; EditorUtility.SetDirty(material); return material; } } }