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. 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(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; } } }