Das G3-HUD steht im Spiel; die Abnahme (U3D) folgt. - U3B: alle Bereiche und Komponenten aus Godot übertragen (LuneHud, Lanes, Bossgrotte, nächste Welle, Kaufkarten, Armee, Held mit Items und Talenten, Angriffschampions, Team, Ereignisse, Minimap, Tooltip, Optionen, Partieende). Porträts aus den echten Modellen, einmal gerendert. - Offene G3-Punkte umgesetzt: „1 WELLE“ als Band am Porträtrand, Schadens- und Rüstungssymbole auf den Karten, Armeewert, Heldenporträts auf der Minimap. Befehlsplatz für die Ultimates der geplanten Helden reserviert (verborgen). - U3C: LuneGame verdrahtet Befehle (gleiche Schnittstelle wie die Bots), Bereit mit 1-s-Sperre nach dem Kampf, Tempo und Pause, Tasten wie in G3, Kamera-Ziele, Auswahl im Feld, Planungsvorschau und Aufstellungsflächen. Ersetzt die U2-Debug-Anzeige. Bot-Partien und Benchmark unverändert. - Szenen- und Bibliotheksbauer idempotent: Animator-Controller und Post-Processing-Profil werden an Ort und Stelle aktualisiert. Die Szene bekommt bei jedem Bau neue Objekt-IDs, der Inhalt ist gleich. - Prüfung (Port von verify_g3, Teile A–H): 699 Prüfungen ohne Fehler. EditMode 23/23, PlayMode 12/12, Fingerabdrücke der Bot-Partien unverändert (DCE95C96556F3A1A, 7B8E43AEFC0D60C5). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
188 lines
9.2 KiB
C#
188 lines
9.2 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<LuneUnitLibrary>(LibraryPath);
|
|
if (library == null)
|
|
{
|
|
library = ScriptableObject.CreateInstance<LuneUnitLibrary>();
|
|
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.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);
|
|
library.BarBack = Marker("BarBack", new Color(0f, 0f, 0f, .7f), 1f, true);
|
|
EditorUtility.SetDirty(library);
|
|
AssetDatabase.SaveAssets();
|
|
return library;
|
|
}
|
|
|
|
/// <summary>Groups whose models stand for units; buildings and environment have their own views.</summary>
|
|
static readonly string[] UnitGroups = { "Units", "Enemies", "Characters", "Peoples" };
|
|
|
|
/// <summary>
|
|
/// 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, ...).
|
|
/// </summary>
|
|
static List<LuneUnitLibrary.UnitModel> 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<LuneUnitLibrary.UnitModel>();
|
|
var aliases = new List<LuneUnitLibrary.UnitModel>();
|
|
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<GameObject>(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();
|
|
}
|
|
|
|
/// <summary>One state per clip of the model (named like the Blender action), idle first.</summary>
|
|
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<AnimatorController>(path) ?? AnimatorController.CreateAnimatorControllerAtPath(path);
|
|
AnimatorStateMachine machine = controller.layers[0].stateMachine;
|
|
AnimationClip[] clips = AssetDatabase.LoadAllAssetsAtPath(fbx).OfType<AnimationClip>().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;
|
|
}
|
|
|
|
/// <summary>Flat ring on the ground (Godot: TorusMesh inner .42, outer .52, flattened).</summary>
|
|
static Mesh Ring(string name, int sides, float offsetDegrees)
|
|
{
|
|
var vertices = new List<Vector3>();
|
|
var triangles = new List<int>();
|
|
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 * .42f);
|
|
vertices.Add(direction * .52f);
|
|
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<Vector3> { 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<Mesh>(path);
|
|
if (existing == null)
|
|
{
|
|
AssetDatabase.CreateAsset(mesh, path);
|
|
return mesh;
|
|
}
|
|
EditorUtility.CopySerialized(mesh, existing);
|
|
EditorUtility.SetDirty(existing);
|
|
return existing;
|
|
}
|
|
|
|
/// <summary>Unshaded marker colour (Godot _flat with emission energy); bars draw on top.</summary>
|
|
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<Material>(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;
|
|
}
|
|
}
|
|
}
|