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
{
///
/// 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.
///
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 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();
unityCamera.fieldOfView = LuneCameraMath.FieldOfView;
unityCamera.nearClipPlane = .5f;
unityCamera.farClipPlane = 700f;
camera.AddComponent().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();
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. Tests and the benchmark start bot matches themselves.
var matchObject = new GameObject("Match");
var driver = matchObject.AddComponent();
var view = matchObject.AddComponent();
view.Driver = driver;
view.Library = library;
view.ViewCamera = unityCamera;
var game = matchObject.AddComponent();
game.Driver = driver;
game.View = view;
game.Rig = rig;
game.UiAssets = AssetDatabase.LoadAssetAtPath(LuneUiAssetsBuilder.AssetPath);
var benchmark = matchObject.AddComponent();
benchmark.Driver = driver;
benchmark.View = view;
benchmark.Rig = rig;
// 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(GroundPath);
if (ground == null)
{
ground = ScriptableObject.CreateInstance();
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.
// U5.3: density .0085 veiled the map view (zoom 64–70, 70–90 m away) by 35–50 % in
// the fog colour and took the colour out of the armies; .0045 keeps 10–15 % there.
RenderSettings.fog = true;
RenderSettings.fogMode = FogMode.ExponentialSquared;
RenderSettings.fogColor = LuneMaterialFactory.Hex("6a7f96");
RenderSettings.fogDensity = .0045f;
RenderSettings.sun = sun;
const string skyPath = "Assets/Luneraria/Settings/Rendering/LuneSky.mat";
var sky = AssetDatabase.LoadAssetAtPath(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.isGlobal = true;
volume.sharedProfile = VolumeProfile();
}
/// Godot light: rotation_degrees (Euler YXZ), shining along its local -Z.
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.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(VolumeProfile profile) where T : VolumeComponent
{
if (profile.TryGet(out T component))
return component;
component = profile.Add(true);
AssetDatabase.AddObjectToAsset(component, profile);
return component;
}
/// ACES tone mapping, glow and colour adjustment of scripts/presentation/lighting.gd (U5 Mid-Lane).
static VolumeProfile VolumeProfile()
{
var profile = AssetDatabase.LoadAssetAtPath(VolumePath);
if (profile == null)
{
profile = ScriptableObject.CreateInstance();
AssetDatabase.CreateAsset(profile, VolumePath);
}
// Updated in place: recreating the overrides gave them new file ids on every build.
var tonemapping = Override(profile);
tonemapping.mode.Override(TonemappingMode.ACES);
var bloom = Override(profile);
bloom.threshold.Override(1.15f);
bloom.intensity.Override(.62f);
bloom.scatter.Override(.68f);
var colors = Override(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(profile);
white.temperature.Override(8f);
white.tint.Override(-4f);
var vignette = Override(profile);
vignette.intensity.Override(.18f);
vignette.smoothness.Override(.4f);
EditorUtility.SetDirty(profile);
return profile;
}
// ------------------------------------------------------------ terrain
static void Terrain(LuneWorldBake bake, Transform world, Dictionary materials)
{
Material terrain = AssetDatabase.LoadAssetAtPath(LuneMaterialFactory.Folder + "/World/Terrain.mat");
Material mountains = AssetDatabase.LoadAssetAtPath(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;
}
}
/// Port of LuneTerrain._mesh: grid with alternating diagonals, normals from neighbours.
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((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 worldMaterials, LuneMaterialSet set)
{
var meshes = new Dictionary();
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;
}
///
/// 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.
///
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(true))
renderer.SetPropertyBlock(block);
}
/// Prop meshes of map_props.fbx by name, with their surfaces dressed (ENV_* materials).
static Dictionary> PropLibrary(LuneMaterialSet set)
{
var result = new Dictionary>();
var root = AssetDatabase.LoadAssetAtPath(Art + "Environment/map_props.fbx");
foreach (MeshFilter filter in root.GetComponentsInChildren(true))
{
var renderer = filter.GetComponent();
Material[] materials = renderer.sharedMaterials.Select(m => set.Get(LuneMaterialSet.KeyOf(m), 2) ?? m).ToArray();
result[filter.gameObject.name] = new KeyValuePair(filter.sharedMesh, materials);
}
return result;
}
/// 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).
static Mesh BakedMesh(LuneWorldBake bake, JObject baked, string name)
{
var vertices = new List();
var normals = new List();
var uvs = new List();
var colors = new List();
var submeshes = new List();
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 _written = new HashSet();
/// 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.
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(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().sharedMesh = mesh;
var renderer = go.AddComponent();
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();
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(Art + "Buildings/" + asset + ".fbx");
var model = (GameObject)PrefabUtility.InstantiatePrefab(prefab, parent);
model.name = "Model";
materials.Dress(model, team);
return model;
}
}
}