Befund aus dem Spieltest des U3-Nachtrags: Nichts legte LuneNetPlay in die Szene. Das Spiel begann direkt in einer Partie, der Startbildschirm aus U4 war unerreichbar, und der Verlassen-Dialog beendete das Spiel statt zum Hauptmenü zu führen. - LuneWorldBuilder legt LuneNetPlay auf das Match-Objekt; Szene neu gebaut. - Das Menü bekommt ein eigenes UIDocument über dem HUD. Vorher hing es an keinem Panel, und das HUD hätte es beim ersten Partiestart geleert. - Rückkehr über „Menü“ → „Zum Menü“: HUD und Aufstellungsflächen weg, Sitzung beendet, leere Karte. Die Kamera ruht hinter dem Menü und beginnt jede Partie ungedreht. Die Bund-Wahl ist markiert, die Knopfreihe passt in die Tafel. - Tests und Benchmark ohne Startbildschirm (LuneMatchDriver.DirectStart, -luneBenchmark); Bot-Partien und Fingerabdrücke unverändert. - Neu: LuneSmoke (-luneSmoke <Ordner>), Selbsttest eines Players nach lune_smoke.gd. StartScreenTests führt ihn im Editor aus. - Tests: EditMode 29/29, PlayMode 14/14 (StartScreenTests 26/26), Verify 718/0, Fingerabdrücke DCE95C96556F3A1A und 7B8E43AEFC0D60C5 unverändert. macOS-Player: Selbsttest 26/26, Bilder in docs/port/screenshots/u4/startbildschirm/. Ein zusammenhängender PlayMode-Lauf auf dem Endstand und ein Player-Benchmark stehen aus, weil der Mac zugeklappt schlief (siehe U4-NETWORK.md). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
566 lines
27 KiB
C#
566 lines
27 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using Luneraria.Core;
|
|
using Luneraria.View;
|
|
using Newtonsoft.Json.Linq;
|
|
using UnityEditor;
|
|
using UnityEditor.SceneManagement;
|
|
using UnityEngine;
|
|
using UnityEngine.Rendering;
|
|
using UnityEngine.Rendering.Universal;
|
|
|
|
namespace Luneraria.EditorTools
|
|
{
|
|
/// <summary>
|
|
/// Builds the G1 map scene (U2B) reproducibly from three sources:
|
|
/// the world bake of the Godot generator (terrain, lanes, platform, bridge, bases, landmarks,
|
|
/// vegetation, veins, grotto stairs, deployment areas, lights),
|
|
/// the FBX models of the Blender pipeline (towers, Ancients, grotto, props),
|
|
/// the C# core's map (building ids, positions and facing).
|
|
/// Unity -batchmode -projectPath . -executeMethod Luneraria.EditorTools.LuneWorldBuilder.BuildScene
|
|
/// The scene owns no rules; buildings carry views that the match binding (U2C) drives.
|
|
/// </summary>
|
|
public static class LuneWorldBuilder
|
|
{
|
|
public const string ScenePath = "Assets/Luneraria/Scenes/G1_Map.unity";
|
|
public const string GeneratedFolder = "Assets/Luneraria/World/Generated";
|
|
public const string GroundPath = "Assets/Luneraria/World/LuneGround.asset";
|
|
public const string VolumePath = "Assets/Luneraria/Settings/Rendering/LuneVolume.asset";
|
|
const string Art = "Assets/Luneraria/Art/";
|
|
|
|
public static void BuildScene()
|
|
{
|
|
int code = 0;
|
|
try
|
|
{
|
|
Build();
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
Debug.LogError("LUNE_WORLD failed: " + error);
|
|
code = 1;
|
|
}
|
|
EditorApplication.Exit(code);
|
|
}
|
|
|
|
public static void Build()
|
|
{
|
|
LuneProjectSetup.Configure();
|
|
LuneWorldBake bake = LuneWorldBake.Load();
|
|
LuneMaterialSet materials = LuneMaterialFactory.Build(bake, out Dictionary<int, Material> worldMaterials);
|
|
LuneGround ground = BuildGround(bake);
|
|
Directory.CreateDirectory(GeneratedFolder);
|
|
_written.Clear();
|
|
Directory.CreateDirectory(Path.GetDirectoryName(ScenePath));
|
|
AssetDatabase.Refresh();
|
|
|
|
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
|
Environment(bake);
|
|
var world = new GameObject("World").transform;
|
|
Terrain(bake, world, worldMaterials);
|
|
int meshes = Nodes(bake, world, worldMaterials, materials);
|
|
var buildings = Buildings(materials, ground);
|
|
LuneUnitLibrary library = LuneUnitLibraryBuilder.Build(materials, ground);
|
|
var camera = new GameObject("Main Camera");
|
|
camera.tag = "MainCamera";
|
|
var unityCamera = camera.AddComponent<Camera>();
|
|
unityCamera.fieldOfView = LuneCameraMath.FieldOfView;
|
|
unityCamera.nearClipPlane = .5f;
|
|
unityCamera.farClipPlane = 700f;
|
|
camera.AddComponent<UniversalAdditionalCameraData>().renderPostProcessing = true;
|
|
LuneCameraMath.Pose(new Vector3(0f, ground.StandHeight(new Vec2(0f, 0f)), 0f), LuneCameraMath.OverviewZoom, 0f, out Vector3 position, out Quaternion rotation);
|
|
camera.transform.SetPositionAndRotation(position, rotation);
|
|
var rig = camera.AddComponent<LuneCameraRig>();
|
|
rig.Ground = ground;
|
|
rig.Zoom = LuneCameraMath.OverviewZoom;
|
|
// The match on the host and its views; the game controller with the HUD (U3) lets the
|
|
// human seat play; the start screen (U4) comes first and starts the matches. Tests and
|
|
// the benchmark skip it and start bot matches themselves.
|
|
var matchObject = new GameObject("Match");
|
|
var driver = matchObject.AddComponent<LuneMatchDriver>();
|
|
var view = matchObject.AddComponent<LuneMatchView>();
|
|
view.Driver = driver;
|
|
view.Library = library;
|
|
view.ViewCamera = unityCamera;
|
|
var game = matchObject.AddComponent<Luneraria.UI.LuneGame>();
|
|
game.Driver = driver;
|
|
game.View = view;
|
|
game.Rig = rig;
|
|
game.UiAssets = AssetDatabase.LoadAssetAtPath<Luneraria.UI.LuneUiAssets>(LuneUiAssetsBuilder.AssetPath);
|
|
var benchmark = matchObject.AddComponent<LuneBenchmark>();
|
|
benchmark.Driver = driver;
|
|
benchmark.View = view;
|
|
benchmark.Rig = rig;
|
|
var netPlay = matchObject.AddComponent<Luneraria.UI.LuneNetPlay>();
|
|
netPlay.Game = game;
|
|
netPlay.Driver = driver;
|
|
netPlay.View = view;
|
|
netPlay.Rig = rig;
|
|
netPlay.UiAssets = game.UiAssets;
|
|
// Meshes of an earlier build that this build no longer produces.
|
|
foreach (string stale in AssetDatabase.FindAssets("t:Mesh", new[] { GeneratedFolder }).Select(AssetDatabase.GUIDToAssetPath).Where(p => !_written.Contains(p)).ToList())
|
|
AssetDatabase.DeleteAsset(stale);
|
|
EditorSceneManager.SaveScene(scene, ScenePath);
|
|
var scenes = EditorBuildSettings.scenes.Where(s => s.path != ScenePath).ToList();
|
|
scenes.Insert(0, new EditorBuildSettingsScene(ScenePath, true));
|
|
EditorBuildSettings.scenes = scenes.ToArray();
|
|
AssetDatabase.SaveAssets();
|
|
Debug.Log(string.Format("LUNE_WORLD built {0}: {1} generated meshes, {2} buildings", ScenePath, meshes, buildings));
|
|
}
|
|
|
|
// ------------------------------------------------------------ ground heights
|
|
static LuneGround BuildGround(LuneWorldBake bake)
|
|
{
|
|
var ground = AssetDatabase.LoadAssetAtPath<LuneGround>(GroundPath);
|
|
if (ground == null)
|
|
{
|
|
ground = ScriptableObject.CreateInstance<LuneGround>();
|
|
AssetDatabase.CreateAsset(ground, GroundPath);
|
|
}
|
|
JToken inner = bake.Data["terrain"]["inner"];
|
|
ground.WalkExtent = (float)inner["extent"];
|
|
ground.WalkStep = (float)inner["step"];
|
|
ground.WalkSize = (int)inner["size"];
|
|
ground.Walk = bake.Floats(inner["walk"]);
|
|
JToken stand = bake.Data["stand"];
|
|
ground.StandExtent = (float)stand["extent"];
|
|
ground.StandStep = (float)stand["step"];
|
|
ground.StandSize = (int)stand["size"];
|
|
ground.Stand = bake.Floats(stand["heights"]);
|
|
EditorUtility.SetDirty(ground);
|
|
return ground;
|
|
}
|
|
|
|
// ------------------------------------------------------------ environment
|
|
static void Environment(LuneWorldBake bake)
|
|
{
|
|
JToken lights = bake.Data["lights"];
|
|
JProperty sunEntry = ((JObject)lights).Properties().First(p => p.Name != "fill" && p.Name != "environment");
|
|
Light sun = DirectionalLight("Sun", sunEntry.Value, true);
|
|
sun.shadowStrength = 1f;
|
|
// U5: crisp cascades over the Mid-Lane slice (was Unity's default 2).
|
|
sun.shadowBias = .02f;
|
|
sun.shadowNormalBias = .35f;
|
|
sun.shadowNearPlane = .2f;
|
|
DirectionalLight("Fill", lights["fill"], false);
|
|
JToken environment = lights["environment"];
|
|
RenderSettings.ambientMode = AmbientMode.Flat;
|
|
RenderSettings.ambientLight = LuneMaterialFactory.GammaEncoded(LuneMaterialFactory.ColorOf(environment["ambient_color"]), (float)environment["ambient_energy"] * 1.6f);
|
|
// U5: aerial depth over the valley; the Mid-Lane reads further away than the towers.
|
|
RenderSettings.fog = true;
|
|
RenderSettings.fogMode = FogMode.ExponentialSquared;
|
|
RenderSettings.fogColor = LuneMaterialFactory.Hex("6a7f96");
|
|
RenderSettings.fogDensity = .0085f;
|
|
RenderSettings.sun = sun;
|
|
const string skyPath = "Assets/Luneraria/Settings/Rendering/LuneSky.mat";
|
|
var sky = AssetDatabase.LoadAssetAtPath<Material>(skyPath);
|
|
if (sky == null)
|
|
{
|
|
sky = new Material(Shader.Find("Skybox/Procedural"));
|
|
AssetDatabase.CreateAsset(sky, skyPath);
|
|
}
|
|
sky.SetColor("_SkyTint", LuneMaterialFactory.Hex("2a3d58"));
|
|
sky.SetColor("_GroundColor", LuneMaterialFactory.Hex("1a1c24"));
|
|
sky.SetFloat("_Exposure", 1.05f);
|
|
sky.SetFloat("_AtmosphereThickness", .8f);
|
|
EditorUtility.SetDirty(sky);
|
|
RenderSettings.skybox = sky;
|
|
RenderSettings.defaultReflectionMode = DefaultReflectionMode.Skybox;
|
|
RenderSettings.reflectionIntensity = 1f;
|
|
var volume = new GameObject("PostProcess").AddComponent<Volume>();
|
|
volume.isGlobal = true;
|
|
volume.sharedProfile = VolumeProfile();
|
|
}
|
|
|
|
/// <summary>Godot light: rotation_degrees (Euler YXZ), shining along its local -Z.</summary>
|
|
static Light DirectionalLight(string name, JToken entry, bool shadows)
|
|
{
|
|
var degrees = entry["rotation_degrees"];
|
|
float x = (float)degrees[0] * Mathf.Deg2Rad;
|
|
float y = (float)degrees[1] * Mathf.Deg2Rad;
|
|
// Ry(y) * Rx(x) * (0, 0, -1)
|
|
var afterX = new Vector3(0f, Mathf.Sin(x), -Mathf.Cos(x));
|
|
var godotDirection = new Vector3(afterX.x * Mathf.Cos(y) + afterX.z * Mathf.Sin(y), afterX.y, -afterX.x * Mathf.Sin(y) + afterX.z * Mathf.Cos(y));
|
|
var light = new GameObject(name).AddComponent<Light>();
|
|
light.type = LightType.Directional;
|
|
light.transform.rotation = Quaternion.LookRotation(LuneSpace.FromGodot(godotDirection), Vector3.up);
|
|
light.color = LuneMaterialFactory.ColorOf(entry["color"]);
|
|
light.intensity = (float)entry["energy"];
|
|
light.shadows = shadows ? LightShadows.Soft : LightShadows.None;
|
|
return light;
|
|
}
|
|
|
|
static T Override<T>(VolumeProfile profile) where T : VolumeComponent
|
|
{
|
|
if (profile.TryGet(out T component))
|
|
return component;
|
|
component = profile.Add<T>(true);
|
|
AssetDatabase.AddObjectToAsset(component, profile);
|
|
return component;
|
|
}
|
|
|
|
/// <summary>ACES tone mapping, glow and colour adjustment of scripts/presentation/lighting.gd (U5 Mid-Lane).</summary>
|
|
static VolumeProfile VolumeProfile()
|
|
{
|
|
var profile = AssetDatabase.LoadAssetAtPath<VolumeProfile>(VolumePath);
|
|
if (profile == null)
|
|
{
|
|
profile = ScriptableObject.CreateInstance<VolumeProfile>();
|
|
AssetDatabase.CreateAsset(profile, VolumePath);
|
|
}
|
|
// Updated in place: recreating the overrides gave them new file ids on every build.
|
|
var tonemapping = Override<Tonemapping>(profile);
|
|
tonemapping.mode.Override(TonemappingMode.ACES);
|
|
var bloom = Override<Bloom>(profile);
|
|
bloom.threshold.Override(1.15f);
|
|
bloom.intensity.Override(.62f);
|
|
bloom.scatter.Override(.68f);
|
|
var colors = Override<ColorAdjustments>(profile);
|
|
colors.contrast.Override(14f);
|
|
colors.saturation.Override(16f);
|
|
// U5: a cool valley shadow, warm sun on stone — stylized realism of the reference MOBAs.
|
|
var white = Override<WhiteBalance>(profile);
|
|
white.temperature.Override(8f);
|
|
white.tint.Override(-4f);
|
|
var vignette = Override<Vignette>(profile);
|
|
vignette.intensity.Override(.18f);
|
|
vignette.smoothness.Override(.4f);
|
|
EditorUtility.SetDirty(profile);
|
|
return profile;
|
|
}
|
|
|
|
// ------------------------------------------------------------ terrain
|
|
static void Terrain(LuneWorldBake bake, Transform world, Dictionary<int, Material> materials)
|
|
{
|
|
Material terrain = AssetDatabase.LoadAssetAtPath<Material>(LuneMaterialFactory.Folder + "/World/Terrain.mat");
|
|
Material mountains = AssetDatabase.LoadAssetAtPath<Material>(LuneMaterialFactory.Folder + "/World/TerrainEdge.mat") ?? terrain;
|
|
var group = new GameObject("Terrain").transform;
|
|
group.SetParent(world, false);
|
|
foreach (string key in new[] { "inner", "outer" })
|
|
{
|
|
JToken grid = bake.Data["terrain"][key];
|
|
Mesh mesh = GridMesh(key == "inner" ? "Ground" : "Mountains", bake.Floats(grid["ground"]), bake.Floats(grid["colors"], 4),
|
|
(int)grid["size"], (float)grid["extent"], (float)grid["step"], (float)grid["hole"]);
|
|
string label = mesh.name;
|
|
mesh = Save(mesh);
|
|
// Weg 2: the walkable ground is the lane tier, the surrounding mountains the edge tier.
|
|
var go = Place(label, group, mesh, new[] { key == "inner" ? terrain : mountains }, key == "inner");
|
|
go.isStatic = true;
|
|
}
|
|
}
|
|
|
|
/// <summary>Port of LuneTerrain._mesh: grid with alternating diagonals, normals from neighbours.</summary>
|
|
static Mesh GridMesh(string name, float[] heights, float[] colors, int n, float extent, float step, float hole)
|
|
{
|
|
var vertices = new Vector3[n * n];
|
|
var normals = new Vector3[n * n];
|
|
var tints = new Color[n * n];
|
|
for (int j = 0; j < n; j++)
|
|
{
|
|
for (int i = 0; i < n; i++)
|
|
{
|
|
int index = j * n + i;
|
|
vertices[index] = LuneSpace.FromGodot(new Vector3(-extent + i * step, heights[index], -extent + j * step));
|
|
float left = heights[j * n + Math.Max(0, i - 1)];
|
|
float right = heights[j * n + Math.Min(n - 1, i + 1)];
|
|
float up = heights[Math.Max(0, j - 1) * n + i];
|
|
float down = heights[Math.Min(n - 1, j + 1) * n + i];
|
|
normals[index] = LuneSpace.FromGodot(new Vector3(left - right, 2f * step, up - down).normalized);
|
|
tints[index] = new Color(colors[index * 4], colors[index * 4 + 1], colors[index * 4 + 2], colors[index * 4 + 3]);
|
|
}
|
|
}
|
|
var indices = new List<int>((n - 1) * (n - 1) * 6);
|
|
for (int j = 0; j < n - 1; j++)
|
|
{
|
|
for (int i = 0; i < n - 1; i++)
|
|
{
|
|
if (hole > 0f)
|
|
{
|
|
float cx = Mathf.Abs(-extent + (i + .5f) * step);
|
|
float cz = Mathf.Abs(-extent + (j + .5f) * step);
|
|
if (Mathf.Max(cx, cz) < hole)
|
|
continue;
|
|
}
|
|
int a = j * n + i;
|
|
int b = a + 1;
|
|
int c = a + n;
|
|
int d = c + 1;
|
|
if ((i + j) % 2 == 0)
|
|
indices.AddRange(new[] { a, b, d, a, d, c });
|
|
else
|
|
indices.AddRange(new[] { a, b, c, b, d, c });
|
|
}
|
|
}
|
|
var mesh = new Mesh { name = name, indexFormat = IndexFormat.UInt32 };
|
|
mesh.SetVertices(vertices);
|
|
mesh.SetNormals(normals);
|
|
mesh.SetColors(tints);
|
|
mesh.SetTriangles(indices, 0);
|
|
mesh.RecalculateBounds();
|
|
return mesh;
|
|
}
|
|
|
|
// ------------------------------------------------------------ baked nodes
|
|
static int Nodes(LuneWorldBake bake, Transform world, Dictionary<int, Material> worldMaterials, LuneMaterialSet set)
|
|
{
|
|
var meshes = new Dictionary<int, Mesh>();
|
|
var bakedMeshes = (JArray)bake.Data["meshes"];
|
|
var props = PropLibrary(set);
|
|
var map = new LuneMap(new LuneBalance());
|
|
foreach (JObject node in bake.Data["nodes"])
|
|
{
|
|
string name = (string)node["name"];
|
|
if (name.StartsWith("@"))
|
|
name = "Part";
|
|
Mesh mesh;
|
|
Material[] materials;
|
|
if (node["prop"] != null)
|
|
{
|
|
var prop = props[(string)node["prop"]];
|
|
mesh = prop.Key;
|
|
materials = prop.Value;
|
|
}
|
|
else
|
|
{
|
|
int index = (int)node["mesh"];
|
|
var baked = (JObject)bakedMeshes[index];
|
|
if (!meshes.TryGetValue(index, out mesh))
|
|
{
|
|
mesh = Save(BakedMesh(bake, baked, name + "_" + index));
|
|
meshes[index] = mesh;
|
|
}
|
|
var surfaces = (JArray)baked["surfaces"];
|
|
materials = new Material[surfaces.Count];
|
|
for (int s = 0; s < surfaces.Count; s++)
|
|
{
|
|
JToken reference = node["material"] ?? surfaces[s]["material"];
|
|
materials[s] = reference != null && worldMaterials.TryGetValue((int)reference, out Material found) ? found : null;
|
|
}
|
|
if (name.StartsWith("Vein_"))
|
|
{
|
|
JToken parameters = node["instance_params"];
|
|
materials = new[] { LuneMaterialFactory.VeinMaterial(name, LuneMaterialFactory.ColorOf(parameters["tint"]), (float)parameters["seed"]) };
|
|
}
|
|
}
|
|
var transforms = (JArray)node["transforms"];
|
|
bool shadows = (bool)node["cast_shadow"];
|
|
bool visible = (bool)node["visible"];
|
|
var group = new GameObject(name).transform;
|
|
group.SetParent(world, false);
|
|
for (int k = 0; k < transforms.Count; k++)
|
|
{
|
|
float[] t = LuneWorldBake.Transform(transforms[k]);
|
|
LuneSpace.FromGodot(t, 0, out Vector3 position, out Quaternion rotation, out Vector3 scale);
|
|
GameObject go = transforms.Count == 1 ? group.gameObject : new GameObject(name + "_" + k);
|
|
if (transforms.Count > 1)
|
|
go.transform.SetParent(group, false);
|
|
go.transform.localPosition = position;
|
|
go.transform.localRotation = rotation;
|
|
go.transform.localScale = scale;
|
|
AddRenderer(go, mesh, materials, shadows);
|
|
ApplyTier(map, go);
|
|
go.isStatic = visible;
|
|
}
|
|
group.gameObject.SetActive(visible);
|
|
}
|
|
return meshes.Count + 2;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Weg 2: a node beyond the lane corridor keeps the edge tier (Surface _Quality 0),
|
|
/// so the mountains and rim vegetation skip the PBR maps the slice pays for.
|
|
/// </summary>
|
|
static void ApplyTier(LuneMap map, GameObject go)
|
|
{
|
|
float quality = LuneZones.QualityOfWorld(map, go.transform.position);
|
|
if (quality > .5f)
|
|
return;
|
|
var block = new MaterialPropertyBlock();
|
|
block.SetFloat("_Quality", 0f);
|
|
foreach (Renderer renderer in go.GetComponentsInChildren<Renderer>(true))
|
|
renderer.SetPropertyBlock(block);
|
|
}
|
|
|
|
/// <summary>Prop meshes of map_props.fbx by name, with their surfaces dressed (ENV_* materials).</summary>
|
|
static Dictionary<string, KeyValuePair<Mesh, Material[]>> PropLibrary(LuneMaterialSet set)
|
|
{
|
|
var result = new Dictionary<string, KeyValuePair<Mesh, Material[]>>();
|
|
var root = AssetDatabase.LoadAssetAtPath<GameObject>(Art + "Environment/map_props.fbx");
|
|
foreach (MeshFilter filter in root.GetComponentsInChildren<MeshFilter>(true))
|
|
{
|
|
var renderer = filter.GetComponent<MeshRenderer>();
|
|
Material[] materials = renderer.sharedMaterials.Select(m => set.Get(LuneMaterialSet.KeyOf(m), 2) ?? m).ToArray();
|
|
result[filter.gameObject.name] = new KeyValuePair<Mesh, Material[]>(filter.sharedMesh, materials);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// <summary>A baked Godot mesh in Unity: x mirrored (with the handedness change the picture and
|
|
/// the clockwise front faces are kept, so the index order stays).</summary>
|
|
static Mesh BakedMesh(LuneWorldBake bake, JObject baked, string name)
|
|
{
|
|
var vertices = new List<Vector3>();
|
|
var normals = new List<Vector3>();
|
|
var uvs = new List<Vector2>();
|
|
var colors = new List<Color>();
|
|
var submeshes = new List<int[]>();
|
|
bool hasNormals = true;
|
|
bool hasUvs = true;
|
|
bool hasColors = true;
|
|
foreach (JObject surface in baked["surfaces"])
|
|
{
|
|
int start = vertices.Count;
|
|
float[] v = bake.Floats(surface["vertices"], 3);
|
|
int count = v.Length / 3;
|
|
for (int i = 0; i < count; i++)
|
|
vertices.Add(LuneSpace.FromGodot(new Vector3(v[i * 3], v[i * 3 + 1], v[i * 3 + 2])));
|
|
if (surface["normals"] != null)
|
|
{
|
|
float[] n = bake.Floats(surface["normals"], 3);
|
|
for (int i = 0; i < count; i++)
|
|
normals.Add(LuneSpace.FromGodot(new Vector3(n[i * 3], n[i * 3 + 1], n[i * 3 + 2])));
|
|
}
|
|
else
|
|
{
|
|
hasNormals = false;
|
|
}
|
|
if (surface["uvs"] != null)
|
|
{
|
|
float[] uv = bake.Floats(surface["uvs"], 2);
|
|
for (int i = 0; i < count; i++)
|
|
uvs.Add(new Vector2(uv[i * 2], uv[i * 2 + 1]));
|
|
}
|
|
else
|
|
{
|
|
hasUvs = false;
|
|
for (int i = 0; i < count; i++)
|
|
uvs.Add(Vector2.zero);
|
|
}
|
|
if (surface["colors"] != null)
|
|
{
|
|
float[] c = bake.Floats(surface["colors"], 4);
|
|
for (int i = 0; i < count; i++)
|
|
colors.Add(new Color(c[i * 4], c[i * 4 + 1], c[i * 4 + 2], c[i * 4 + 3]));
|
|
}
|
|
else
|
|
{
|
|
hasColors = false;
|
|
for (int i = 0; i < count; i++)
|
|
colors.Add(Color.white);
|
|
}
|
|
int[] indices = surface["indices"] != null ? bake.Ints(surface["indices"]) : Enumerable.Range(0, count).ToArray();
|
|
submeshes.Add(indices.Select(i => i + start).ToArray());
|
|
}
|
|
var mesh = new Mesh { name = name, indexFormat = vertices.Count > 65000 ? IndexFormat.UInt32 : IndexFormat.UInt16 };
|
|
mesh.SetVertices(vertices);
|
|
if (hasUvs)
|
|
mesh.SetUVs(0, uvs);
|
|
if (hasColors)
|
|
mesh.SetColors(colors);
|
|
mesh.subMeshCount = submeshes.Count;
|
|
for (int s = 0; s < submeshes.Count; s++)
|
|
mesh.SetTriangles(submeshes[s], s);
|
|
if (hasNormals && normals.Count == vertices.Count)
|
|
mesh.SetNormals(normals);
|
|
else
|
|
mesh.RecalculateNormals();
|
|
mesh.RecalculateBounds();
|
|
return mesh;
|
|
}
|
|
|
|
static readonly HashSet<string> _written = new HashSet<string>();
|
|
|
|
/// <summary>Writes a generated mesh; an existing asset is updated in place so its GUID (and the
|
|
/// scene's reference) stays the same from build to build.</summary>
|
|
static Mesh Save(Mesh mesh)
|
|
{
|
|
string path = GeneratedFolder + "/" + mesh.name + ".asset";
|
|
if (!_written.Add(path))
|
|
throw new InvalidOperationException("duplicate generated mesh " + path);
|
|
var existing = AssetDatabase.LoadAssetAtPath<Mesh>(path);
|
|
if (existing == null)
|
|
{
|
|
AssetDatabase.CreateAsset(mesh, path);
|
|
return mesh;
|
|
}
|
|
EditorUtility.CopySerialized(mesh, existing);
|
|
EditorUtility.SetDirty(existing);
|
|
UnityEngine.Object.DestroyImmediate(mesh);
|
|
return existing;
|
|
}
|
|
|
|
static GameObject Place(string name, Transform parent, Mesh mesh, Material[] materials, bool shadows)
|
|
{
|
|
var go = new GameObject(name);
|
|
go.transform.SetParent(parent, false);
|
|
AddRenderer(go, mesh, materials, shadows);
|
|
return go;
|
|
}
|
|
|
|
static void AddRenderer(GameObject go, Mesh mesh, Material[] materials, bool shadows)
|
|
{
|
|
go.AddComponent<MeshFilter>().sharedMesh = mesh;
|
|
var renderer = go.AddComponent<MeshRenderer>();
|
|
renderer.sharedMaterials = materials;
|
|
renderer.shadowCastingMode = shadows ? ShadowCastingMode.On : ShadowCastingMode.Off;
|
|
}
|
|
|
|
// ------------------------------------------------------------ strategic buildings
|
|
static int Buildings(LuneMaterialSet materials, LuneGround ground)
|
|
{
|
|
var root = new GameObject("Buildings").transform;
|
|
var map = new LuneMap(new LuneBalance());
|
|
const float PlazaLift = .08f;
|
|
int count = 0;
|
|
foreach (BuildingDef definition in map.Buildings)
|
|
{
|
|
Vec2 at = definition.Position;
|
|
string asset;
|
|
Vec2 facing;
|
|
float height = ground.WalkHeight(at);
|
|
if (definition.Tier == "core")
|
|
{
|
|
asset = "ancient";
|
|
facing = (-at).Normalized();
|
|
height += PlazaLift;
|
|
}
|
|
else if (definition.Tier == "base")
|
|
{
|
|
asset = "kernwacht";
|
|
facing = (at - map.CorePosition(definition.Team)).Normalized();
|
|
}
|
|
else
|
|
{
|
|
asset = definition.Tier == "outer" ? "grenzwacht" : "bastion";
|
|
facing = (map.LanePoint(definition.Team, definition.Lane, definition.Progress) - at).Normalized();
|
|
}
|
|
var view = new GameObject(definition.Id).AddComponent<LuneBuildingView>();
|
|
view.transform.SetParent(root, false);
|
|
view.transform.position = LuneSpace.World(at, height);
|
|
view.BuildingId = definition.Id;
|
|
view.Team = definition.Team;
|
|
view.Tier = definition.Tier;
|
|
GameObject model = Model(asset, view.transform, materials, definition.Team);
|
|
model.transform.localRotation = LuneSpace.Facing(facing);
|
|
view.Show(LuneBuildingView.Look.Intact);
|
|
count++;
|
|
}
|
|
var grotto = new GameObject("BossGrotto").transform;
|
|
grotto.SetParent(root, false);
|
|
grotto.position = LuneSpace.World(LuneMap.Grotto, 1.7f);
|
|
Model("bossgrotte", grotto, materials, 2);
|
|
return count + 1;
|
|
}
|
|
|
|
static GameObject Model(string asset, Transform parent, LuneMaterialSet materials, int team)
|
|
{
|
|
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(Art + "Buildings/" + asset + ".fbx");
|
|
var model = (GameObject)PrefabUtility.InstantiatePrefab(prefab, parent);
|
|
model.name = "Model";
|
|
materials.Dress(model, team);
|
|
return model;
|
|
}
|
|
}
|
|
}
|