diff --git a/.cursor/rules/aigame-basics.mdc b/.cursor/rules/aigame-basics.mdc
index b3a6892..05391c3 100644
--- a/.cursor/rules/aigame-basics.mdc
+++ b/.cursor/rules/aigame-basics.mdc
@@ -25,5 +25,6 @@ alwaysApply: true
- JSON:`Doc/UIPrefabCreater/{UIName}.json`
- Prefab:`Assets/Bundles/UI/Prefab/{UIName}.prefab`
- 通用贴图:`Assets/Bundles/UI/Texture/Common/`
+ - 标准控件底图必须使用九宫格:Sprite 设置有效的非零 `Border`、`Full Rect`,JSON 使用 `imageType: Sliced`;按钮各状态图同样设置。图标、插画等整图按原比例使用。
- 设计分辨率:竖屏 **`[1080, 1920]`**(不要用参考项目的 2048×1024)
- Unity 菜单:`Tools/UI/...`;转换器:`AIClient/Assets/Editor/UI/`
diff --git a/.gitignore b/.gitignore
index 62570fc..e509ab3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,11 @@ Tools/__pycache__/
Tools/**/__pycache__/
Tools/**/*.pyc
+# Local API credentials (never commit)
+.env
+Tools/*.env
+Tools/*.env.local
+
# 混元3D 登录凭证与任务中间文件(勿提交)
Tools/data/hunyuan/storage_state.json
Tools/data/hunyuan/status.json
@@ -12,8 +17,12 @@ Tools/data/hunyuan/jobs/
Tools/model_tools/out_bake/
Tools/model_tools/**/_low_work/
-# 本地输出(按需取消忽略)
-# Tools/output/
+# 本地输出 / 中间产物(勿提交)
+Tools/output/
+AIClient/Assets/Screenshots/
+AIClient/Assets/Screenshots.meta
+AIClient/Assets/Temp/
+AIClient/Assets/Temp.meta
# ===== Unity =====
AIClient/[Ll]ibrary/
@@ -51,6 +60,7 @@ Tools/cdn_root/
# AIServer
AIServer/build/
+AIServer/build-g1/
AIServer/data/*.db
AIServer/data/*.db-*
diff --git a/.vscode/settings.json b/.vscode/settings.json
index c4860d3..48ab2f0 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,3 +1,4 @@
{
- "cmake.sourceDirectory": "D:/UD/AI/AIGame/AIServer"
+ "cmake.sourceDirectory": "D:/UD/AI/AIGame/AIServer",
+ "dotnet.preferCSharpExtension": true
}
\ No newline at end of file
diff --git a/AIClient/AgentScripts/AttachGreenfield.cs b/AIClient/AgentScripts/AttachGreenfield.cs
new file mode 100644
index 0000000..03eb26b
--- /dev/null
+++ b/AIClient/AgentScripts/AttachGreenfield.cs
@@ -0,0 +1,111 @@
+using System.Linq;
+using System.Text;
+using UnityEditor;
+using UnityEditor.SceneManagement;
+using UnityEngine;
+using UnityEngine.SceneManagement;
+
+///
+/// Nests the authored A-scheme battlefield under the live battle prefab.
+/// Gameplay actors stay on PrototypeArena; the primitive ground is turned off.
+///
+public static class AttachGreenfield
+{
+ const string Arena = "Assets/Bundles/Scenes/Prototype/PrototypeArena.prefab";
+ const string Battlefield = "Assets/Bundles/Scenes/Prototype/Battlefield/Battlefield_Greenfield.prefab";
+
+ public static string Build()
+ {
+ if (EditorApplication.isPlaying) throw new System.Exception("Attach the battlefield in Edit Mode.");
+ var battlefield = AssetDatabase.LoadAssetAtPath(Battlefield);
+ if (battlefield == null) throw new System.Exception("Missing " + Battlefield);
+ var root = PrefabUtility.LoadPrefabContents(Arena);
+ try
+ {
+ var previous = root.transform.Find("Greenfield");
+ if (previous != null) Object.DestroyImmediate(previous.gameObject);
+ var instance = (GameObject)PrefabUtility.InstantiatePrefab(battlefield, root.transform);
+ instance.name = "Greenfield";
+ instance.transform.SetSiblingIndex(0);
+ foreach (var transform in instance.GetComponentsInChildren(true)) transform.gameObject.layer = 30;
+ foreach (Transform child in root.transform)
+ {
+ if (child == instance.transform) continue;
+ if (child.name == "Ground" || child.name == "Lane" || child.name == "LeftBorder" || child.name == "RightBorder" || child.name == "BattleLight")
+ child.gameObject.SetActive(false);
+ }
+ PrefabUtility.SaveAsPrefabAsset(root, Arena);
+ }
+ finally { PrefabUtility.UnloadPrefabContents(root); }
+ AssetDatabase.SaveAssets();
+ return Verify();
+ }
+
+ public static string Verify()
+ {
+ var prefab = AssetDatabase.LoadAssetAtPath(Arena);
+ if (prefab == null) throw new System.Exception("Missing arena prefab.");
+ var scene = EditorSceneManager.NewPreviewScene();
+ var instance = (GameObject)PrefabUtility.InstantiatePrefab(prefab, scene);
+ try
+ {
+ var report = new StringBuilder();
+ var greenfield = instance.transform.Find("Greenfield");
+ if (greenfield == null || !greenfield.gameObject.activeSelf) throw new System.Exception("Greenfield is missing.");
+ Require(greenfield, "Ground/GroundMesh");
+ Require(greenfield, "Routes/Lane0");
+ Require(greenfield, "Routes/Lane1");
+ Require(greenfield, "Routes/Lane2");
+ Require(greenfield, "Lighting/Sun");
+ if (greenfield.Find("Decorations/bridge") != null || greenfield.Find("Decorations/bridgeLeft") != null || greenfield.Find("Decorations/bridgeRight") != null)
+ throw new System.Exception("Bridge is still placed in the battle arena.");
+ var badLayer = greenfield.GetComponentsInChildren(true).FirstOrDefault(r => r.gameObject.layer != 30);
+ if (badLayer != null) throw new System.Exception("Greenfield renderer not on layer 30: " + badLayer.name);
+ foreach (string name in new[] { "Heroes", "Enemies", "Projectiles", "DeploymentMarkers", "SkillEffects", "BattleCamera", "DefenceLine" })
+ {
+ var node = instance.transform.Find(name);
+ if (node == null || !node.gameObject.activeSelf) throw new System.Exception(name + " must stay active.");
+ }
+ foreach (Transform child in instance.transform)
+ {
+ if (child.name == "Ground" || child.name == "Lane" || child.name == "LeftBorder" || child.name == "RightBorder" || child.name == "BattleLight")
+ {
+ if (child.gameObject.activeSelf) throw new System.Exception(child.name + " primitive is still active.");
+ }
+ }
+ var mesh = greenfield.Find("Ground/GroundMesh").GetComponentInChildren();
+ if (mesh == null || mesh.sharedMaterial == null || mesh.sharedMaterial.shader.name != "Test/XPbr")
+ throw new System.Exception("Greenfield ground shader is not Test/XPbr.");
+ report.Append("PASS nested Greenfield; primitives hidden; gameplay nodes kept.");
+ return report.ToString();
+ }
+ finally
+ {
+ Object.DestroyImmediate(instance);
+ EditorSceneManager.ClosePreviewScene(scene);
+ }
+ }
+
+ static void Require(Transform root, string path)
+ {
+ var node = root.Find(path);
+ if (node == null) throw new System.Exception("Missing " + path);
+ }
+
+ public static string FramePrefabStage()
+ {
+ var stage = PrefabStageUtility.OpenPrefab(Arena);
+ var view = SceneView.lastActiveSceneView;
+ if (view == null) throw new System.Exception("No Scene View.");
+ view.LookAt(new Vector3(0, 0, 3.5f), Quaternion.Euler(38, -35, 0), 22f);
+ view.orthographic = false;
+ view.Repaint();
+ return stage.assetPath;
+ }
+
+ public static string ClosePrefabStage()
+ {
+ StageUtility.GoToMainStage();
+ return "Closed prefab stage";
+ }
+}
diff --git a/AIClient/AgentScripts/AuditAnimalWalk.cs b/AIClient/AgentScripts/AuditAnimalWalk.cs
new file mode 100644
index 0000000..729cf70
--- /dev/null
+++ b/AIClient/AgentScripts/AuditAnimalWalk.cs
@@ -0,0 +1,70 @@
+#if UNITY_EDITOR
+using System;
+using System.IO;
+using UnityEditor;
+using UnityEditor.SceneManagement;
+using UnityEngine;
+using UnityEngine.SceneManagement;
+using Object = UnityEngine.Object;
+
+public static class AuditAnimalWalk
+{
+ const string Root = "Assets/Bundles/Characters/Animal/RockBall";
+ static GameObject Spawn(string path) => Object.Instantiate(AssetDatabase.LoadAssetAtPath(path));
+ public static string Capture()
+ {
+ string dir = Path.GetFullPath(Path.Combine(Application.dataPath, "../../Tools/output/rockball_walk_audit"));
+ Directory.CreateDirectory(dir);
+ var scene = EditorSceneManager.NewPreviewScene();
+ try
+ {
+ var unit = Spawn(Root + "/RockBall_Rigged.prefab");
+ SceneManager.MoveGameObjectToScene(unit, scene);
+ var anim = unit.GetComponentInChildren();
+ anim.enabled = false;
+ var clip = AssetDatabase.LoadAssetAtPath(Root + "/Clips/Walk.anim");
+ var light = new GameObject("AuditLight").AddComponent();
+ SceneManager.MoveGameObjectToScene(light.gameObject, scene);
+ light.type = LightType.Directional;
+ light.intensity = 1.5f;
+ light.transform.rotation = Quaternion.Euler(45, -30, 0);
+ var cam = new GameObject("AuditCamera").AddComponent();
+ SceneManager.MoveGameObjectToScene(cam.gameObject, scene);
+ cam.scene = scene;
+ cam.clearFlags = CameraClearFlags.SolidColor;
+ cam.backgroundColor = new Color(.45f,.53f,.59f);
+ cam.transform.position = new Vector3(1.1f,.75f,-1.6f);
+ cam.transform.LookAt(new Vector3(0,.28f,0));
+ cam.orthographic = true;
+ cam.orthographicSize = .43f;
+ cam.nearClipPlane = .01f;
+ var rt = new RenderTexture(512,512,24);
+ cam.targetTexture = rt;
+ var tex = new Texture2D(512,512,TextureFormat.RGB24,false);
+ var previous = RenderTexture.active;
+ try
+ {
+ for (int i = -1; i < 4; i++)
+ {
+ if (i >= 0) clip.SampleAnimation(anim.gameObject, clip.length * i / 4f);
+ cam.Render();
+ RenderTexture.active = rt;
+ tex.ReadPixels(new Rect(0,0,512,512),0,0);
+ tex.Apply();
+ File.WriteAllBytes(Path.Combine(dir, i < 0 ? "bind.png" : "walk_" + i + ".png"), tex.EncodeToPNG());
+ }
+ }
+ finally
+ {
+ RenderTexture.active = previous;
+ cam.targetTexture = null;
+ rt.Release();
+ Object.DestroyImmediate(rt);
+ Object.DestroyImmediate(tex);
+ }
+ }
+ finally { EditorSceneManager.ClosePreviewScene(scene); }
+ return dir;
+ }
+}
+#endif
diff --git a/AIClient/AgentScripts/AuditCharacterVisual.cs b/AIClient/AgentScripts/AuditCharacterVisual.cs
new file mode 100644
index 0000000..047e1e7
--- /dev/null
+++ b/AIClient/AgentScripts/AuditCharacterVisual.cs
@@ -0,0 +1,64 @@
+#if UNITY_EDITOR
+using System;
+using System.IO;
+using System.Linq;
+using UnityEditor;
+using UnityEditor.SceneManagement;
+using UnityEngine;
+using UnityEngine.SceneManagement;
+using Object=UnityEngine.Object;
+
+/// Isolated static or sampled-pose views; preserves all open scene contents.
+public static class AuditCharacterVisual
+{
+ public static string Capture(string prefabPath,string outputDirectory,float height=.55f,string clipPath="",float phase=0)
+ {
+ string dir=Path.GetFullPath(Path.Combine(Application.dataPath,"../..",outputDirectory));Directory.CreateDirectory(dir);
+ var scene=EditorSceneManager.NewPreviewScene();
+ try
+ {
+ var prefab=AssetDatabase.LoadAssetAtPath(prefabPath);
+ if(prefab==null)throw new Exception("Missing visual prefab: "+prefabPath);
+ var unit=(GameObject)PrefabUtility.InstantiatePrefab(prefab,scene);
+ var animator=unit.GetComponentInChildren();if(animator!=null)animator.enabled=false;
+ if(animator==null)
+ {
+ var filters=unit.GetComponentsInChildren();
+ var points=filters.SelectMany(m=>m.sharedMesh.vertices.Select(v=>m.transform.TransformPoint(v))).ToArray();
+ if(points.Length==0)throw new Exception("No static geometry");
+ float minY=points.Min(v=>v.y),actualHeight=points.Max(v=>v.y)-minY;
+ int triangles=filters.Sum(m=>m.sharedMesh.triangles.Length/3);
+ if(Mathf.Abs(minY)>.002f || Mathf.Abs(actualHeight-height)>.002f)throw new Exception("Static height/bottom invalid");
+ if(unit.GetComponentsInChildren().Any(r=>r.sharedMaterial==null || r.sharedMaterial.shader.name!="Test/XPbr"))throw new Exception("Static XPbr material missing");
+ File.WriteAllText(Path.Combine(dir,"static_verification.txt"),$"height={actualHeight:F6} bottom={minY:F6} triangles={triangles} XPbr=True; facing verified in front/side/back views");
+ }
+ if(!string.IsNullOrEmpty(clipPath))
+ {
+ var clip=AssetDatabase.LoadAssetAtPath(clipPath);
+ if(clip==null || animator==null)throw new Exception("Missing clip/animator");
+ clip.SampleAnimation(animator.gameObject,clip.length*phase);
+ }
+ var light=new GameObject("AuditLight").AddComponent();SceneManager.MoveGameObjectToScene(light.gameObject,scene);
+ light.type=LightType.Directional;light.intensity=1.4f;light.transform.rotation=Quaternion.Euler(40,-30,0);
+ var camera=new GameObject("AuditCamera").AddComponent();SceneManager.MoveGameObjectToScene(camera.gameObject,scene);camera.scene=scene;
+ camera.clearFlags=CameraClearFlags.SolidColor;camera.backgroundColor=new Color(.56f,.64f,.7f);
+ camera.orthographic=true;camera.orthographicSize=height*.68f;camera.nearClipPlane=.01f;camera.farClipPlane=10;
+ var rt=new RenderTexture(640,640,24);var tex=new Texture2D(640,640,TextureFormat.RGB24,false);var previous=RenderTexture.active;
+ try
+ {
+ var directions=new[]{new Vector3(0,.22f,-1),new Vector3(1,.22f,0),new Vector3(0,.22f,1),new Vector3(.7f,.3f,-1)};
+ string[] names={"front","side","back","perspective"};
+ for(int i=0;i
+/// Imports the authored battlefield component FBX/XPbr files and creates one
+/// reusable prefab per component. Ground is built separately so its collider
+/// and read/write mesh settings cannot be confused with decoration assets.
+///
+public static class BuildBattlefieldAssets
+{
+ const string Root = "Assets/Bundles/Scenes/Prototype/Battlefield/A/Components";
+ const string GroundRoot = "Assets/Bundles/Scenes/Prototype/Battlefield/A/Ground";
+ const string GroundPrefab = "Assets/Bundles/Scenes/Prototype/Battlefield/GreenfieldGround.prefab";
+ const string XpbrShader = "Test/XPbr";
+
+ static readonly string[] Parts = { "tree", "rock", "bridge", "sign", "shrub" };
+
+ public static string BuildComponents()
+ {
+ AssetDatabase.Refresh();
+ var reports = new List();
+ foreach (var type in Parts) reports.Add(BuildPart(type));
+ AssetDatabase.SaveAssets();
+ AssetDatabase.Refresh();
+ return string.Join(" | ", reports);
+ }
+
+ ///
+ /// Builds the independently authored ground asset once Hunyuan has produced
+ /// Ground/ground.fbx and its XPbr textures. The ground keeps a readable mesh
+ /// so the authored prefab can carry a non-convex MeshCollider.
+ ///
+ public static string BuildGround()
+ {
+ AssetDatabase.Refresh();
+ string fbxPath = GroundRoot + "/ground.fbx";
+ if (!File.Exists(fbxPath)) return "ground: missing fbx";
+ string basePath = GroundRoot + "/ground_base.png";
+ string mixPath = GroundRoot + "/ground_mix.png";
+ ConfigureTexture(basePath, true, true);
+ ConfigureTexture(mixPath, false, false);
+ var importer = AssetImporter.GetAtPath(fbxPath) as ModelImporter;
+ if (importer != null)
+ {
+ importer.globalScale = 1f;
+ importer.useFileScale = false;
+ importer.bakeAxisConversion = true;
+ importer.materialImportMode = ModelImporterMaterialImportMode.ImportViaMaterialDescription;
+ importer.meshCompression = ModelImporterMeshCompression.Medium;
+ importer.isReadable = true;
+ importer.addCollider = false;
+ importer.SaveAndReimport();
+ }
+ var mat = GetOrCreateMaterial(GroundRoot + "/ground.mat", "ground", basePath, mixPath);
+ var model = AssetDatabase.LoadAssetAtPath(fbxPath);
+ if (model == null) return "ground: fbx not loaded";
+ var root = new GameObject("GreenfieldGround");
+ try
+ {
+ var instance = (GameObject)PrefabUtility.InstantiatePrefab(model);
+ instance.name = "GroundMesh";
+ instance.transform.SetParent(root.transform, false);
+ PrefabUtility.UnpackPrefabInstance(instance, PrefabUnpackMode.Completely, InteractionMode.AutomatedAction);
+ foreach (var renderer in instance.GetComponentsInChildren(true)) renderer.sharedMaterial = mat;
+ foreach (var filter in instance.GetComponentsInChildren(true))
+ {
+ if (filter.sharedMesh == null) continue;
+ var collider = filter.gameObject.GetComponent();
+ if (collider == null) collider = filter.gameObject.AddComponent();
+ collider.convex = false;
+ collider.sharedMesh = filter.sharedMesh;
+ }
+ PrefabUtility.SaveAsPrefabAsset(root, GroundPrefab);
+ }
+ finally { UnityEngine.Object.DestroyImmediate(root); }
+ AssetDatabase.SaveAssets();
+ AssetDatabase.Refresh();
+ return "ground: " + GroundPrefab;
+ }
+
+ public static string BuildAll()
+ {
+ var components = BuildComponents();
+ var ground = BuildGround();
+ return components + " | " + ground;
+ }
+
+ static string BuildPart(string type)
+ {
+ string dir = Root + "/" + type;
+ string fbxPath = dir + "/" + type + ".fbx";
+ string matPath = dir + "/" + type + ".mat";
+ string basePath = dir + "/" + type + "_base.png";
+ string mixPath = dir + "/" + type + "_mix.png";
+ string prefabPath = dir + "/" + type + ".prefab";
+ if (!File.Exists(fbxPath)) return type + ": missing fbx";
+
+ ConfigureTexture(basePath, true, true);
+ ConfigureTexture(mixPath, false, false);
+ var importer = AssetImporter.GetAtPath(fbxPath) as ModelImporter;
+ if (importer != null)
+ {
+ importer.globalScale = 1f;
+ importer.useFileScale = false;
+ importer.bakeAxisConversion = true;
+ importer.materialImportMode = ModelImporterMaterialImportMode.ImportViaMaterialDescription;
+ importer.meshCompression = ModelImporterMeshCompression.Medium;
+ importer.isReadable = false;
+ importer.addCollider = false;
+ importer.SaveAndReimport();
+ }
+
+ var mat = GetOrCreateMaterial(matPath, type, basePath, mixPath);
+
+ var model = AssetDatabase.LoadAssetAtPath(fbxPath);
+ if (model == null) return type + ": fbx not loaded";
+ var instance = (GameObject)PrefabUtility.InstantiatePrefab(model);
+ instance.name = type;
+ foreach (var renderer in instance.GetComponentsInChildren()) renderer.sharedMaterial = mat;
+ instance.transform.localPosition = Vector3.zero;
+ instance.transform.localRotation = Quaternion.identity;
+ instance.transform.localScale = Vector3.one;
+ PrefabUtility.SaveAsPrefabAsset(instance, prefabPath);
+ UnityEngine.Object.DestroyImmediate(instance);
+ return type + ": " + prefabPath;
+ }
+
+ static Material GetOrCreateMaterial(string matPath, string name, string basePath, string mixPath)
+ {
+ var baseMap = AssetDatabase.LoadAssetAtPath(basePath);
+ var mixMap = AssetDatabase.LoadAssetAtPath(mixPath);
+ var shader = Shader.Find(XpbrShader) ?? Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard");
+ var mat = AssetDatabase.LoadAssetAtPath(matPath);
+ if (mat == null)
+ {
+ mat = new Material(shader) { name = name };
+ AssetDatabase.CreateAsset(mat, matPath);
+ }
+ else mat.shader = shader;
+ if (mat.HasProperty("_MainTex")) mat.SetTexture("_MainTex", baseMap);
+ if (mat.HasProperty("_MixTex")) mat.SetTexture("_MixTex", mixMap);
+ if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", Color.white);
+ if (mat.HasProperty("_Metallic")) mat.SetFloat("_Metallic", 1f);
+ if (mat.HasProperty("_Roughness")) mat.SetFloat("_Roughness", 1f);
+ if (mat.HasProperty("_AO")) mat.SetFloat("_AO", 1f);
+ EditorUtility.SetDirty(mat);
+ return mat;
+ }
+
+ static void ConfigureTexture(string path, bool srgb, bool alpha)
+ {
+ if (!File.Exists(path)) return;
+ var importer = AssetImporter.GetAtPath(path) as TextureImporter;
+ if (importer == null) return;
+ importer.textureType = TextureImporterType.Default;
+ importer.sRGBTexture = srgb;
+ importer.mipmapEnabled = true;
+ importer.alphaIsTransparency = alpha;
+ importer.SaveAndReimport();
+ }
+}
+#endif
diff --git a/AIClient/AgentScripts/BuildBattlefieldScene.cs b/AIClient/AgentScripts/BuildBattlefieldScene.cs
new file mode 100644
index 0000000..d0ffe9f
--- /dev/null
+++ b/AIClient/AgentScripts/BuildBattlefieldScene.cs
@@ -0,0 +1,354 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using UnityEditor;
+using UnityEditor.SceneManagement;
+using UnityEngine;
+
+[Serializable] public sealed class BattlefieldVec3 { public float x, y, z; public Vector3 Value => new Vector3(x, y, z); }
+[Serializable] public sealed class BattlefieldGroundSpec { public string type, gamePrefab, requirements; public int targetTriangles; }
+[Serializable] public sealed class BattlefieldPlacement { public string name; public BattlefieldVec3 position, rotation, scale; }
+[Serializable] public sealed class BattlefieldComponentSpec { public string type, prefab, label; public int targetTriangles; public bool placeInScene; public BattlefieldVec3 position, rotation, scale; public BattlefieldPlacement[] placements; }
+[Serializable] public sealed class BattlefieldAssemblySpec { public string outputPrefab, componentFolder, markerFolder, materialsFolder; }
+[Serializable] public sealed class BattlefieldManifest
+{
+ public int schemaVersion, componentTypeLimit;
+ public string sceneId, selectedDesign;
+ public BattlefieldGroundSpec ground;
+ public BattlefieldComponentSpec[] components;
+ public BattlefieldAssemblySpec assembly;
+}
+
+/// Pipeline authoring entry for the selected battlefield design. It never creates gameplay scenery at runtime.
+public static class BuildBattlefieldScene
+{
+ const string ManifestPath = "Doc/Design/Scene/Battlefield/battlefield_scene_manifest.json";
+ static string RepoRoot => Directory.GetParent(Application.dataPath).Parent.FullName;
+ static string ProjectPath(string path) => path.StartsWith("Assets/", StringComparison.Ordinal) ? path : path;
+
+ static BattlefieldManifest ReadManifest()
+ {
+ string path = Path.GetFullPath(Path.Combine(RepoRoot, ManifestPath));
+ if (!File.Exists(path)) throw new FileNotFoundException("Missing battlefield manifest", path);
+ string text = File.ReadAllText(path);
+ // JsonUtility drops nested objects in this manifest, so each object is parsed on its own.
+ var data = new BattlefieldManifest
+ {
+ schemaVersion = ExtractInt(text, "schemaVersion"),
+ sceneId = ExtractString(text, "sceneId"),
+ selectedDesign = ExtractString(text, "selectedDesign"),
+ componentTypeLimit = ExtractInt(text, "componentTypeLimit"),
+ ground = JsonUtility.FromJson(SliceObject(text, "ground")),
+ assembly = JsonUtility.FromJson(SliceObject(text, "assembly")),
+ components = ParseComponents(SliceArray(text, "components"))
+ };
+ if (data.ground == null) throw new Exception("Invalid battlefield manifest");
+ return data;
+ }
+
+ static string ExtractString(string json, string key)
+ {
+ var match = Regex.Match(json, "\"" + key + "\"\\s*:\\s*\"([^\"]*)\"");
+ return match.Success ? match.Groups[1].Value : "";
+ }
+
+ static int ExtractInt(string json, string key)
+ {
+ var match = Regex.Match(json, "\"" + key + "\"\\s*:\\s*(-?\\d+)");
+ return match.Success ? int.Parse(match.Groups[1].Value) : 0;
+ }
+
+ static BattlefieldVec3 ReadVec(string json, string key)
+ {
+ var match = Regex.Match(json, "\"" + key + "\"\\s*:\\s*\\{\\s*\"x\"\\s*:\\s*(-?[0-9.]+)\\s*,\\s*\"y\"\\s*:\\s*(-?[0-9.]+)\\s*,\\s*\"z\"\\s*:\\s*(-?[0-9.]+)");
+ if (!match.Success) return null;
+ return new BattlefieldVec3
+ {
+ x = float.Parse(match.Groups[1].Value, System.Globalization.CultureInfo.InvariantCulture),
+ y = float.Parse(match.Groups[2].Value, System.Globalization.CultureInfo.InvariantCulture),
+ z = float.Parse(match.Groups[3].Value, System.Globalization.CultureInfo.InvariantCulture)
+ };
+ }
+
+ static BattlefieldComponentSpec[] ParseComponents(string arrayJson)
+ {
+ var items = new List();
+ int depth = 0, start = -1;
+ for (int i = 0; i < arrayJson.Length; i++)
+ {
+ if (arrayJson[i] == '{') { if (depth == 0) start = i; depth++; }
+ else if (arrayJson[i] == '}')
+ {
+ depth--;
+ if (depth == 0 && start >= 0)
+ {
+ string slice = arrayJson.Substring(start, i - start + 1);
+ var item = JsonUtility.FromJson(slice);
+ if (item != null)
+ {
+ item.position = ReadVec(slice, "position") ?? item.position;
+ item.rotation = ReadVec(slice, "rotation") ?? item.rotation;
+ item.scale = ReadVec(slice, "scale") ?? item.scale;
+ item.placements = ReadPlacements(slice);
+ var place = Regex.Match(slice, "\"placeInScene\"\\s*:\\s*(true|false)");
+ item.placeInScene = !place.Success || place.Groups[1].Value == "true";
+ items.Add(item);
+ }
+ start = -1;
+ }
+ }
+ }
+ return items.ToArray();
+ }
+
+ static BattlefieldPlacement[] ReadPlacements(string slice)
+ {
+ if (slice.IndexOf("\"placements\"", StringComparison.Ordinal) < 0) return null;
+ string arrayJson = SliceArray(slice, "placements");
+ var items = new List();
+ int depth = 0, start = -1;
+ for (int i = 0; i < arrayJson.Length; i++)
+ {
+ if (arrayJson[i] == '{') { if (depth == 0) start = i; depth++; }
+ else if (arrayJson[i] == '}')
+ {
+ depth--;
+ if (depth == 0 && start >= 0)
+ {
+ string place = arrayJson.Substring(start, i - start + 1);
+ items.Add(new BattlefieldPlacement
+ {
+ name = Regex.Match(place, "\"name\"\\s*:\\s*\"([^\"]*)\"").Groups[1].Value,
+ position = ReadVec(place, "position"),
+ rotation = ReadVec(place, "rotation"),
+ scale = ReadVec(place, "scale")
+ });
+ start = -1;
+ }
+ }
+ }
+ return items.Count == 0 ? null : items.ToArray();
+ }
+
+ static void Validate(BattlefieldManifest data)
+ {
+ if (string.IsNullOrEmpty(data.selectedDesign) || (data.selectedDesign != "A" && data.selectedDesign != "B" && data.selectedDesign != "C"))
+ throw new Exception("Choose battlefield design A/B/C before assembling models.");
+ if (data.ground == null || data.ground.type != "ground" || string.IsNullOrEmpty(data.ground.gamePrefab))
+ throw new Exception("Ground must be a separate authored prefab.");
+ if (data.components == null) data.components = Array.Empty();
+ var types = new HashSet();
+ foreach (var item in data.components)
+ {
+ if (item == null || string.IsNullOrEmpty(item.type) || !types.Add(item.type)) throw new Exception("Component types must be unique.");
+ if (types.Count > 15 || types.Count > data.componentTypeLimit) throw new Exception("Battlefield component type limit exceeded.");
+ if (item.type == "ground") throw new Exception("Ground cannot be listed as a decoration component.");
+ if (string.IsNullOrEmpty(item.prefab)) throw new Exception("Missing prefab for component " + item.type);
+ if (AssetDatabase.LoadAssetAtPath(item.prefab) == null) throw new Exception("Missing component prefab " + item.prefab);
+ }
+ var groundPrefab = AssetDatabase.LoadAssetAtPath(data.ground.gamePrefab);
+ if (groundPrefab == null) throw new Exception("Missing ground prefab " + data.ground.gamePrefab);
+ var groundMeshes = groundPrefab.GetComponentsInChildren(true);
+ if (groundMeshes.Length == 0 || groundMeshes.Any(m => m.sharedMesh == null)) throw new Exception("Ground prefab has no readable mesh " + data.ground.gamePrefab);
+ var groundColliders = groundPrefab.GetComponentsInChildren(true);
+ if (groundColliders.Length == 0 || groundColliders.Any(c => c.sharedMesh == null || c.convex)) throw new Exception("Ground prefab requires a non-convex MeshCollider " + data.ground.gamePrefab);
+ if (data.assembly == null || string.IsNullOrEmpty(data.assembly.outputPrefab)) throw new Exception("Missing assembly.outputPrefab");
+ }
+
+ static Transform Group(Transform parent, string name)
+ {
+ var go = new GameObject(name); go.layer = 30; go.transform.SetParent(parent, false); return go.transform;
+ }
+
+ static void SetLayer(GameObject go, int layer)
+ {
+ foreach (var transform in go.GetComponentsInChildren(true)) transform.gameObject.layer = layer;
+ }
+
+ static GameObject AddPrefab(string assetPath, Transform parent, string name, BattlefieldVec3 position, BattlefieldVec3 rotation, BattlefieldVec3 scale)
+ {
+ var prefab = AssetDatabase.LoadAssetAtPath(assetPath);
+ var instance = (GameObject)PrefabUtility.InstantiatePrefab(prefab);
+ instance.name = name; instance.transform.SetParent(parent, false);
+ SetLayer(instance, 30);
+ if (position != null) instance.transform.localPosition = position.Value;
+ if (rotation != null) instance.transform.localEulerAngles = rotation.Value;
+ if (scale != null) instance.transform.localScale = scale.Value;
+ return instance;
+ }
+
+ ///
+ /// The baked ground is a tall slab facing Z. Lay it on XZ, cover the three lanes, and sit the top surface on y=0.
+ ///
+ static void LayGroundFlat(GameObject groundMesh)
+ {
+ groundMesh.transform.localRotation = Quaternion.Euler(-90f, 0f, 0f);
+ groundMesh.transform.localScale = Vector3.one;
+ groundMesh.transform.localPosition = Vector3.zero;
+ var bounds = RendererBounds(groundMesh);
+ const float targetWidth = 22f;
+ const float targetDepth = 26f;
+ float scale = Mathf.Max(targetWidth / Mathf.Max(0.01f, bounds.size.x), targetDepth / Mathf.Max(0.01f, bounds.size.z));
+ groundMesh.transform.localScale = Vector3.one * scale;
+ bounds = RendererBounds(groundMesh);
+ var footprintCenter = new Vector3(0f, 0f, 3.5f);
+ groundMesh.transform.position += new Vector3(footprintCenter.x - bounds.center.x, -bounds.max.y, footprintCenter.z - bounds.center.z);
+ }
+
+ static Bounds RendererBounds(GameObject go)
+ {
+ var renderers = go.GetComponentsInChildren(true);
+ if (renderers.Length == 0) throw new Exception("Ground prefab has no renderer.");
+ var bounds = renderers[0].bounds;
+ for (int i = 1; i < renderers.Length; i++) bounds.Encapsulate(renderers[i].bounds);
+ return bounds;
+ }
+
+ static string SliceObject(string text, string key)
+ {
+ int i = text.IndexOf("\"" + key + "\"", StringComparison.Ordinal);
+ if (i < 0) return "";
+ int start = text.IndexOf('{', i);
+ int depth = 0;
+ for (int c = start; c < text.Length; c++)
+ {
+ if (text[c] == '{') depth++;
+ else if (text[c] == '}') { depth--; if (depth == 0) return text.Substring(start, c - start + 1); }
+ }
+ return "";
+ }
+
+ static string SliceArray(string text, string key)
+ {
+ int i = text.IndexOf("\"" + key + "\"", StringComparison.Ordinal);
+ int start = text.IndexOf('[', i);
+ int depth = 0;
+ for (int c = start; c < text.Length; c++)
+ {
+ if (text[c] == '[') depth++;
+ else if (text[c] == ']') { depth--; if (depth == 0) return text.Substring(start, c - start + 1); }
+ }
+ return "[]";
+ }
+
+ public static string ValidateOnly()
+ {
+ var data = ReadManifest(); Validate(data);
+ return "PASS battlefield manifest design=" + data.selectedDesign + " components=" + (data.components?.Length ?? 0);
+ }
+
+ public static string Build()
+ {
+ if (EditorApplication.isPlaying) throw new Exception("Build battlefield in Edit Mode.");
+ var data = ReadManifest(); Validate(data);
+ string output = data.assembly.outputPrefab;
+ string folder = Path.GetDirectoryName(output).Replace('\\', '/');
+ if (!AssetDatabase.IsValidFolder(folder)) throw new Exception("Create the authored battlefield folder first: " + folder);
+ var root = new GameObject(data.sceneId);
+ root.layer = 30;
+ try
+ {
+ var ground = Group(root.transform, "Ground");
+ LayGroundFlat(AddPrefab(data.ground.gamePrefab, ground, "GroundMesh", null, null, null));
+ var routes = Group(root.transform, "Routes");
+ var gameplay = Group(root.transform, "Gameplay"); var spawns = Group(gameplay, "SpawnPoints"); var goals = Group(gameplay, "GoalPoints");
+ for (int lane = 0; lane < 3; lane++) { var route = Group(routes, "Lane" + lane); route.localPosition = new Vector3(lane == 0 ? 0 : lane == 1 ? -3 : 3, 0, 0); var spawn = Group(spawns, "Lane" + lane); spawn.localPosition = route.localPosition + new Vector3(0, 0, 11); var goal = Group(goals, "Lane" + lane); goal.localPosition = route.localPosition + new Vector3(0, 0, -4); }
+ var decorations = Group(root.transform, "Decorations");
+ foreach (var item in data.components)
+ {
+ if (!item.placeInScene) continue;
+ if (item.placements != null && item.placements.Length > 0)
+ {
+ for (int i = 0; i < item.placements.Length; i++)
+ {
+ var place = item.placements[i];
+ if (place.position == null || place.rotation == null || place.scale == null)
+ throw new Exception("Component " + item.type + " placement " + i + " is missing a layout transform.");
+ AddPrefab(item.prefab, decorations, string.IsNullOrEmpty(place.name) ? item.type + i : place.name, place.position, place.rotation, place.scale);
+ }
+ }
+ else
+ {
+ if (item.position == null || item.rotation == null || item.scale == null)
+ throw new Exception("Component " + item.type + " is missing a layout transform.");
+ AddPrefab(item.prefab, decorations, item.type, item.position, item.rotation, item.scale);
+ }
+ }
+ var lighting = Group(root.transform, "Lighting");
+ var sunObject = new GameObject("Sun");
+ sunObject.transform.SetParent(lighting, false);
+ sunObject.transform.localRotation = Quaternion.Euler(48f, -30f, 0f);
+ var sun = sunObject.AddComponent();
+ sun.type = LightType.Directional;
+ sun.color = new Color(1f, 0.96f, 0.88f);
+ sun.intensity = 1.1f;
+ sun.shadows = LightShadows.Soft;
+ SetLayer(root, 30);
+ PrefabUtility.SaveAsPrefabAsset(root, output);
+ }
+ finally { UnityEngine.Object.DestroyImmediate(root); }
+ AssetDatabase.SaveAssets(); AssetDatabase.Refresh();
+ return "Built battlefield prefab " + output + " from design " + data.selectedDesign + " with " + data.components.Length + " component types.";
+ }
+
+ public static string VerifyBuilt()
+ {
+ const string output = "Assets/Bundles/Scenes/Prototype/Battlefield/Battlefield_Greenfield.prefab";
+ var prefab = AssetDatabase.LoadAssetAtPath(output);
+ if (prefab == null) throw new Exception("Missing " + output);
+ var scene = EditorSceneManager.NewPreviewScene();
+ try
+ {
+ var instance = (GameObject)PrefabUtility.InstantiatePrefab(prefab, scene);
+ string[] names = { "Ground/GroundMesh", "Routes/Lane0", "Routes/Lane1", "Routes/Lane2", "Gameplay/SpawnPoints/Lane0", "Gameplay/SpawnPoints/Lane1", "Gameplay/SpawnPoints/Lane2", "Gameplay/GoalPoints/Lane0", "Gameplay/GoalPoints/Lane1", "Gameplay/GoalPoints/Lane2", "Decorations/tree", "Decorations/rock", "Decorations/sign", "Decorations/shrub", "Lighting/Sun" };
+ foreach (var name in names)
+ if (instance.transform.Find(name) == null) throw new Exception("Missing node " + name);
+ var ground = instance.transform.Find("Ground/GroundMesh");
+ var collider = ground.GetComponentInChildren(true);
+ if (collider == null || collider.sharedMesh == null || collider.convex || !collider.sharedMesh.isReadable)
+ throw new Exception("Ground collider must be a readable non-convex MeshCollider.");
+ var groundBounds = RendererBounds(ground.gameObject);
+ if (groundBounds.max.y > 0.05f || groundBounds.max.y < -0.05f) throw new Exception("Ground top is not on y=0: " + groundBounds.max.y);
+ if (groundBounds.min.x > -3f || groundBounds.max.x < 3f || groundBounds.min.z > -4f || groundBounds.max.z < 11f)
+ throw new Exception("Ground footprint does not cover the three lanes: " + groundBounds);
+ var report = new StringBuilder();
+ var decorations = instance.transform.Find("Decorations");
+ foreach (Transform child in decorations)
+ if (child.name.IndexOf("bridge", StringComparison.OrdinalIgnoreCase) >= 0) throw new Exception("Bridge is still placed: " + child.name);
+ var badLayer = instance.GetComponentsInChildren(true).FirstOrDefault(r => r.gameObject.layer != 30);
+ if (badLayer != null) throw new Exception("Renderer not on battle layer 30: " + badLayer.name + " layer=" + badLayer.gameObject.layer);
+ foreach (var decoName in new[] { "tree", "rock", "sign", "shrub" })
+ {
+ var deco = instance.transform.Find("Decorations/" + decoName);
+ var renderer = deco.GetComponentInChildren(true);
+ if (renderer == null || renderer.sharedMaterial == null || renderer.sharedMaterial.shader == null)
+ throw new Exception(decoName + " has no material.");
+ if (renderer.sharedMaterial.shader.name != "Test/XPbr") throw new Exception(decoName + " shader is " + renderer.sharedMaterial.shader.name);
+ report.Append(decoName).Append('@').Append(deco.localPosition.ToString("F1")).Append(' ');
+ }
+ return "PASS ground=" + groundBounds.size + " topY=" + groundBounds.max.y.ToString("F3") + " " + report;
+ }
+ finally { EditorSceneManager.ClosePreviewScene(scene); }
+ }
+
+ public static string FramePrefabStage()
+ {
+ const string output = "Assets/Bundles/Scenes/Prototype/Battlefield/Battlefield_Greenfield.prefab";
+ var stage = UnityEditor.SceneManagement.PrefabStageUtility.OpenPrefab(output);
+ var sv = SceneView.lastActiveSceneView;
+ if (sv == null) throw new Exception("No Scene View is open.");
+ sv.orthographic = false;
+ sv.LookAt(new Vector3(0f, 0f, 3.5f), Quaternion.Euler(38f, -35f, 0f), 22f);
+ sv.Repaint();
+ return "Framed " + stage.assetPath;
+ }
+
+ public static string ClosePrefabStage()
+ {
+ StageUtility.GoToMainStage();
+ return "Closed prefab stage";
+ }
+}
diff --git a/AIClient/AgentScripts/BuildCampAssets.cs b/AIClient/AgentScripts/BuildCampAssets.cs
new file mode 100644
index 0000000..7b67561
--- /dev/null
+++ b/AIClient/AgentScripts/BuildCampAssets.cs
@@ -0,0 +1,80 @@
+using System;
+using System.IO;
+using AIGame.EditorTools;
+using UnityEditor;
+using UnityEditor.SceneManagement;
+using UnityEngine;
+using UnityEngine.SceneManagement;
+
+public static class BuildCampAssets
+{
+ const string World="Assets/Bundles/Scenes/Prototype";
+ const string Pictures="Assets/Bundles/UI/Texture/Prototype";
+ static Material Mat(string name) => AssetDatabase.LoadAssetAtPath(World+"/Materials/Proto"+name+".mat");
+ static Transform Part(Transform parent,string name,PrimitiveType type,Vector3 position,Vector3 scale,string material)
+ {
+ var go=GameObject.CreatePrimitive(type);go.name=name;go.layer=30;go.transform.SetParent(parent,false);
+ go.transform.localPosition=position;go.transform.localScale=scale;go.GetComponent().sharedMaterial=Mat(material);
+ UnityEngine.Object.DestroyImmediate(go.GetComponent());return go.transform;
+ }
+ static void Prop(int id,Transform root)
+ {
+ if(id==0)
+ {
+ Part(root,"Foot",PrimitiveType.Cylinder,new Vector3(0,.06f,0),new Vector3(.7f,.06f,.7f),"Cream");
+ Part(root,"Pole",PrimitiveType.Cylinder,new Vector3(0,1,0),new Vector3(.09f,1,.09f),"Dark");
+ Part(root,"Finial",PrimitiveType.Sphere,new Vector3(0,2.03f,0),Vector3.one*.18f,"Yellow");
+ Part(root,"Flag",PrimitiveType.Cube,new Vector3(.43f,1.62f,0),new Vector3(.86f,.58f,.06f),"Orange");
+ Part(root,"Stripe",PrimitiveType.Cube,new Vector3(.43f,1.62f,-.04f),new Vector3(.7f,.12f,.025f),"Cream");
+ }
+ else if(id==1)
+ {
+ Part(root,"Pot",PrimitiveType.Cylinder,new Vector3(0,.27f,0),new Vector3(.8f,.27f,.8f),"Orange");
+ Part(root,"Rim",PrimitiveType.Cylinder,new Vector3(0,.53f,0),new Vector3(.9f,.055f,.9f),"Cream");
+ Part(root,"Stem",PrimitiveType.Cylinder,new Vector3(0,.9f,0),new Vector3(.09f,.4f,.09f),"Leaf");
+ for(int i=0;i<5;i++) {float a=i*Mathf.PI*2/5;Part(root,"Petal"+i,PrimitiveType.Sphere,new Vector3(Mathf.Sin(a)*.34f,1.37f+Mathf.Cos(a)*.34f,0),new Vector3(.45f,.45f,.2f),"Water");}
+ Part(root,"Heart",PrimitiveType.Sphere,new Vector3(0,1.37f,-.13f),Vector3.one*.36f,"Yellow");
+ Part(root,"Leaf",PrimitiveType.Sphere,new Vector3(.22f,.87f,0),new Vector3(.55f,.15f,.25f),"Leaf").localRotation=Quaternion.Euler(0,0,28);
+ }
+ else
+ {
+ Part(root,"Base",PrimitiveType.Cylinder,new Vector3(0,.08f,0),new Vector3(.65f,.08f,.65f),"Rock");
+ Part(root,"Post",PrimitiveType.Cylinder,new Vector3(0,.85f,0),new Vector3(.11f,.8f,.11f),"Dark");
+ Part(root,"Housing",PrimitiveType.Cube,new Vector3(0,1.72f,0),new Vector3(.6f,.7f,.45f),"Dark");
+ Part(root,"Glass",PrimitiveType.Cube,new Vector3(0,1.72f,-.05f),new Vector3(.46f,.55f,.44f),"Yellow");
+ Part(root,"Cap",PrimitiveType.Cube,new Vector3(0,2.12f,0),new Vector3(.78f,.12f,.6f),"Rock");
+ Part(root,"Top",PrimitiveType.Sphere,new Vector3(0,2.25f,0),Vector3.one*.18f,"Orange");
+ }
+ }
+ static void BuildProp(int id)
+ {
+ var scene=EditorSceneManager.NewPreviewScene();var previous=RenderTexture.active;
+ var rt=new RenderTexture(256,256,24,RenderTextureFormat.ARGB32);
+ try
+ {
+ var root=new GameObject("CampDecoration"+id);SceneManager.MoveGameObjectToScene(root,scene);root.layer=30;Prop(id,root.transform);
+ PrefabUtility.SaveAsPrefabAsset(root,World+"/CampDecoration"+id+".prefab");
+ var cameraGo=new GameObject("Camera");SceneManager.MoveGameObjectToScene(cameraGo,scene);var camera=cameraGo.AddComponent();camera.scene=scene;
+ camera.clearFlags=CameraClearFlags.SolidColor;camera.backgroundColor=Color.clear;camera.cullingMask=1<<30;camera.orthographic=true;camera.orthographicSize=1.35f;camera.allowHDR=false;
+ camera.transform.position=new Vector3(1.3f,1.7f,-4);camera.transform.LookAt(new Vector3(id==0?.22f:0,1.05f,0));camera.targetTexture=rt;
+ var lightGo=new GameObject("Light");SceneManager.MoveGameObjectToScene(lightGo,scene);var light=lightGo.AddComponent();light.type=LightType.Directional;light.intensity=1.15f;light.cullingMask=1<<30;lightGo.transform.rotation=Quaternion.Euler(35,-35,0);
+ camera.Render();RenderTexture.active=rt;var texture=new Texture2D(256,256,TextureFormat.RGBA32,false);
+ try {texture.ReadPixels(new Rect(0,0,256,256),0,0);texture.Apply();File.WriteAllBytes(Pictures+"/proto_decor_"+id+".png",texture.EncodeToPNG());}
+ finally {UnityEngine.Object.DestroyImmediate(texture);}
+ }
+ finally {RenderTexture.active=previous;EditorSceneManager.ClosePreviewScene(scene);rt.Release();UnityEngine.Object.DestroyImmediate(rt);}
+ string path=Pictures+"/proto_decor_"+id+".png";AssetDatabase.ImportAsset(path);
+ var importer=(TextureImporter)AssetImporter.GetAtPath(path);importer.textureType=TextureImporterType.Sprite;importer.spriteImportMode=SpriteImportMode.Single;importer.mipmapEnabled=false;importer.alphaIsTransparency=true;importer.textureCompression=TextureImporterCompression.Uncompressed;
+ var settings=new TextureImporterSettings();importer.ReadTextureSettings(settings);settings.spriteMeshType=SpriteMeshType.FullRect;importer.SetTextureSettings(settings);importer.SaveAndReimport();
+ }
+ public static string Build()
+ {
+ if(EditorApplication.isPlaying) throw new Exception("Build camp in Edit Mode");
+ for(int i=0;i<3;i++) BuildProp(i);
+ string repo=Directory.GetParent(Application.dataPath).Parent.FullName;
+ foreach(string screen in new[]{"Camp","Build"})
+ if(string.IsNullOrEmpty(JsonUIPrefabBuilder.BuildFromJsonFile(Path.Combine(repo,"Doc/UIPrefabCreater/UI_Prototype"+screen+".json")))) throw new Exception("Camp UI build failed");
+ SetupUiBundleCollector.Setup();AssetDatabase.SaveAssets();
+ return "Created three authored decorations and transparent portraits; generated two camp UI prefabs from JSON.";
+ }
+}
diff --git a/AIClient/AgentScripts/BuildCharacterReview.cs b/AIClient/AgentScripts/BuildCharacterReview.cs
new file mode 100644
index 0000000..1df42fe
--- /dev/null
+++ b/AIClient/AgentScripts/BuildCharacterReview.cs
@@ -0,0 +1,518 @@
+#if UNITY_EDITOR
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using UnityEditor;
+using UnityEditor.SceneManagement;
+using UnityEngine;
+using UnityEngine.SceneManagement;
+
+[Serializable] public sealed class CharacterReviewVec3 { public float x, y, z; public Vector3 Value => new Vector3(x, y, z); }
+[Serializable]
+public sealed class CharacterReviewSubject
+{
+ public string id, kind, prefab, notes;
+ public float expectedHeight = 1.8f, heightTolerance = 0.15f;
+ public int maxTriangles = 3500;
+ public bool requireXpbr = true;
+ public bool requireAnimator = false;
+ public string[] requiredClips = { "Idle", "Walk", "Attack" };
+}
+[Serializable]
+public sealed class CharacterReviewManifest
+{
+ public int schemaVersion;
+ public string sceneId, outputScene, reportPath, label;
+ public float spacing = 3f, groundSize = 24f, fieldOfView = 40f;
+ public CharacterReviewVec3 cameraPosition, cameraTarget;
+ public CharacterReviewSubject[] subjects;
+}
+
+///
+/// Builds a persistent editor scene for reviewing character / unit art before gameplay swaps.
+///
+public static class BuildCharacterReview
+{
+ const string ManifestPath = "Doc/Design/Character/character_unit_review_manifest.json";
+ const string CapturePath = "Assets/Screenshots/character_unit_review.png";
+ static string RepoRoot => Directory.GetParent(Application.dataPath).Parent.FullName;
+ static string ProjectRoot => Directory.GetParent(Application.dataPath).FullName;
+ static string AbsoluteAsset(string assetPath) => Path.GetFullPath(Path.Combine(ProjectRoot, Norm(assetPath)));
+
+ static CharacterReviewManifest ReadManifest()
+ {
+ string path = Path.GetFullPath(Path.Combine(RepoRoot, ManifestPath));
+ if (!File.Exists(path)) throw new FileNotFoundException("Missing character review manifest", path);
+ string text = File.ReadAllText(path);
+ var data = new CharacterReviewManifest
+ {
+ schemaVersion = ExtractInt(text, "schemaVersion"),
+ sceneId = ExtractString(text, "sceneId"),
+ outputScene = ExtractString(text, "outputScene"),
+ reportPath = ExtractString(text, "reportPath"),
+ label = ExtractString(text, "label"),
+ spacing = ExtractFloat(text, "spacing", 3f),
+ groundSize = ExtractFloat(text, "groundSize", 24f),
+ fieldOfView = ExtractFloat(text, "fieldOfView", 40f),
+ cameraPosition = ReadVec(text, "cameraPosition"),
+ cameraTarget = ReadVec(text, "cameraTarget"),
+ subjects = ParseSubjects(SliceArray(text, "subjects"))
+ };
+ if (string.IsNullOrEmpty(data.sceneId) || string.IsNullOrEmpty(data.outputScene))
+ throw new Exception("Character review manifest needs sceneId and outputScene.");
+ if (data.cameraPosition == null || data.cameraTarget == null)
+ throw new Exception("Character review manifest needs cameraPosition and cameraTarget.");
+ if (data.subjects == null || data.subjects.Length == 0)
+ throw new Exception("Character review manifest needs at least one subject.");
+ return data;
+ }
+
+ static string ExtractString(string json, string key)
+ {
+ var match = Regex.Match(json, "\"" + key + "\"\\s*:\\s*\"([^\"]*)\"");
+ return match.Success ? match.Groups[1].Value : "";
+ }
+ static int ExtractInt(string json, string key)
+ {
+ var match = Regex.Match(json, "\"" + key + "\"\\s*:\\s*(-?\\d+)");
+ return match.Success ? int.Parse(match.Groups[1].Value) : 0;
+ }
+ static float ExtractFloat(string json, string key, float fallback)
+ {
+ var match = Regex.Match(json, "\"" + key + "\"\\s*:\\s*(-?[0-9.]+)");
+ return match.Success ? float.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture) : fallback;
+ }
+ static bool ExtractBool(string json, string key, bool fallback)
+ {
+ var match = Regex.Match(json, "\"" + key + "\"\\s*:\\s*(true|false)");
+ return match.Success ? match.Groups[1].Value == "true" : fallback;
+ }
+ static CharacterReviewVec3 ReadVec(string json, string key)
+ {
+ var match = Regex.Match(json, "\"" + key + "\"\\s*:\\s*\\{\\s*\"x\"\\s*:\\s*(-?[0-9.]+)\\s*,\\s*\"y\"\\s*:\\s*(-?[0-9.]+)\\s*,\\s*\"z\"\\s*:\\s*(-?[0-9.]+)");
+ if (!match.Success) return null;
+ return new CharacterReviewVec3
+ {
+ x = float.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture),
+ y = float.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture),
+ z = float.Parse(match.Groups[3].Value, CultureInfo.InvariantCulture)
+ };
+ }
+ static string SliceArray(string text, string key)
+ {
+ int i = text.IndexOf("\"" + key + "\"", StringComparison.Ordinal);
+ if (i < 0) return "[]";
+ int start = text.IndexOf('[', i);
+ int depth = 0;
+ for (int c = start; c < text.Length; c++)
+ {
+ if (text[c] == '[') depth++;
+ else if (text[c] == ']') { depth--; if (depth == 0) return text.Substring(start, c - start + 1); }
+ }
+ return "[]";
+ }
+ static CharacterReviewSubject[] ParseSubjects(string arrayJson)
+ {
+ var items = new List();
+ int depth = 0, start = -1;
+ for (int i = 0; i < arrayJson.Length; i++)
+ {
+ if (arrayJson[i] == '{') { if (depth == 0) start = i; depth++; }
+ else if (arrayJson[i] == '}')
+ {
+ depth--;
+ if (depth == 0 && start >= 0)
+ {
+ string slice = arrayJson.Substring(start, i - start + 1);
+ items.Add(new CharacterReviewSubject
+ {
+ id = ExtractString(slice, "id"),
+ kind = ExtractString(slice, "kind"),
+ prefab = ExtractString(slice, "prefab"),
+ notes = ExtractString(slice, "notes"),
+ expectedHeight = ExtractFloat(slice, "expectedHeight", 1.8f),
+ heightTolerance = ExtractFloat(slice, "heightTolerance", 0.15f),
+ maxTriangles = ExtractInt(slice, "maxTriangles"),
+ requireXpbr = ExtractBool(slice, "requireXpbr", true),
+ requireAnimator = ExtractBool(slice, "requireAnimator", false),
+ requiredClips = slice.Contains("\"requiredClips\"")
+ ? Regex.Matches(SliceArray(slice,"requiredClips"), "\"([^\"]+)\"").Cast().Select(m=>m.Groups[1].Value).ToArray()
+ : new[] { "Idle", "Walk", "Attack" }
+ });
+ if (items[items.Count - 1].maxTriangles <= 0) items[items.Count - 1].maxTriangles = 3500;
+ start = -1;
+ }
+ }
+ }
+ return items.ToArray();
+ }
+
+ static string Norm(string path) => path.Replace('\\', '/');
+
+ static void EnsureFolder(string assetPath)
+ {
+ string absolute = AbsoluteAsset(assetPath);
+ string directory = Path.GetDirectoryName(absolute);
+ if (!string.IsNullOrEmpty(directory)) Directory.CreateDirectory(directory);
+ string unityDir = Path.GetDirectoryName(Norm(assetPath))?.Replace('\\', '/');
+ if (string.IsNullOrEmpty(unityDir) || !unityDir.StartsWith("Assets/", StringComparison.Ordinal)) return;
+ string[] parts = unityDir.Split('/');
+ string current = parts[0];
+ for (int i = 1; i < parts.Length; i++)
+ {
+ string next = current + "/" + parts[i];
+ if (!AssetDatabase.IsValidFolder(next)) AssetDatabase.CreateFolder(current, parts[i]);
+ current = next;
+ }
+ }
+
+ public static string ValidateOnly()
+ {
+ var data = ReadManifest();
+ foreach (var subject in data.subjects)
+ {
+ if (string.IsNullOrEmpty(subject.id) || string.IsNullOrEmpty(subject.prefab))
+ throw new Exception("Subject missing id/prefab");
+ if (AssetDatabase.LoadAssetAtPath(Norm(subject.prefab)) == null)
+ throw new Exception("Missing prefab " + subject.prefab);
+ }
+ return "PASS character review manifest subjects=" + data.subjects.Length;
+ }
+
+ public static string Build()
+ {
+ if (EditorApplication.isPlaying) throw new Exception("Build character review in Edit Mode.");
+ var data = ReadManifest();
+ string scenePath = Norm(data.outputScene);
+ EnsureFolder(scenePath);
+ var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
+
+ var root = new GameObject(data.sceneId);
+ SceneManager.MoveGameObjectToScene(root, scene);
+
+ BuildGround(root.transform, data.groundSize);
+ BuildHeightMarkers(root.transform);
+ var subjects = new GameObject("Subjects");
+ subjects.transform.SetParent(root.transform, false);
+ for (int i = 0; i < data.subjects.Length; i++)
+ {
+ var subject = data.subjects[i];
+ var prefab = AssetDatabase.LoadAssetAtPath(Norm(subject.prefab));
+ if (prefab == null) throw new Exception("Missing prefab " + subject.prefab);
+ var instance = (GameObject)PrefabUtility.InstantiatePrefab(prefab, subjects.transform);
+ instance.name = subject.id;
+ instance.transform.localPosition = new Vector3(i * data.spacing, 0f, 0f);
+ instance.transform.localRotation = Quaternion.identity;
+ AddForwardMarker(instance.transform);
+ AddNameLabel(instance.transform, subject.id + " / " + subject.kind);
+ EnsureAnimCycle(instance);
+ }
+
+ var lighting = new GameObject("Lighting");
+ lighting.transform.SetParent(root.transform, false);
+ var sun = new GameObject("Sun");
+ sun.transform.SetParent(lighting.transform, false);
+ sun.transform.rotation = Quaternion.Euler(48f, -30f, 0f);
+ var light = sun.AddComponent();
+ light.type = LightType.Directional;
+ light.color = new Color(1f, 0.96f, 0.88f);
+ light.intensity = 1.2f;
+ light.shadows = LightShadows.Soft;
+
+ var cameraGo = new GameObject("ReviewCamera");
+ cameraGo.tag = "MainCamera";
+ var camera = cameraGo.AddComponent();
+ camera.clearFlags = CameraClearFlags.SolidColor;
+ camera.backgroundColor = new Color(0.55f, 0.72f, 0.82f);
+ camera.fieldOfView = data.fieldOfView;
+ camera.nearClipPlane = 0.05f;
+ camera.farClipPlane = 80f;
+ cameraGo.AddComponent();
+ cameraGo.transform.position = data.cameraPosition.Value;
+ cameraGo.transform.rotation = Quaternion.LookRotation(data.cameraTarget.Value - cameraGo.transform.position, Vector3.up);
+
+ var title = new GameObject("ReviewLabel");
+ var text = title.AddComponent();
+ text.text = string.IsNullOrEmpty(data.label) ? data.sceneId : data.label;
+ text.anchor = TextAnchor.LowerCenter;
+ text.alignment = TextAlignment.Center;
+ text.characterSize = 0.12f;
+ text.fontSize = 48;
+ text.color = new Color(0.15f, 0.2f, 0.25f);
+ title.transform.position = data.cameraTarget.Value + new Vector3(0f, 2.6f, 0f);
+
+ RenderSettings.ambientMode = UnityEngine.Rendering.AmbientMode.Trilight;
+ RenderSettings.ambientSkyColor = new Color(0.55f, 0.65f, 0.75f);
+ RenderSettings.ambientEquatorColor = new Color(0.4f, 0.42f, 0.4f);
+ RenderSettings.ambientGroundColor = new Color(0.25f, 0.24f, 0.22f);
+
+ EditorSceneManager.SaveScene(scene, scenePath);
+ AssetDatabase.SaveAssets();
+ AssetDatabase.Refresh();
+ WriteReport(data, scenePath, "built");
+ return "Built character review scene " + scenePath + " with " + data.subjects.Length + " subjects.";
+ }
+
+ static void BuildGround(Transform parent, float size)
+ {
+ var ground = GameObject.CreatePrimitive(PrimitiveType.Plane);
+ ground.name = "Ground";
+ ground.transform.SetParent(parent, false);
+ ground.transform.localScale = Vector3.one * (size / 10f);
+ var renderer = ground.GetComponent();
+ var mat = new Material(Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard"));
+ mat.color = new Color(0.78f, 0.82f, 0.74f);
+ renderer.sharedMaterial = mat;
+ UnityEngine.Object.DestroyImmediate(ground.GetComponent());
+ }
+
+ static void BuildHeightMarkers(Transform parent)
+ {
+ var markers = new GameObject("HeightMarkers");
+ markers.transform.SetParent(parent, false);
+ markers.transform.localPosition = new Vector3(-2.2f, 0f, 0f);
+ foreach (float h in new[] { 0.5f, 1.0f, 1.8f })
+ {
+ var stick = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
+ stick.name = "Stick_" + h.ToString("0.0", CultureInfo.InvariantCulture);
+ stick.transform.SetParent(markers.transform, false);
+ stick.transform.localPosition = new Vector3(0f, h * 0.5f, 0f);
+ stick.transform.localScale = new Vector3(0.06f, h * 0.5f, 0.06f);
+ UnityEngine.Object.DestroyImmediate(stick.GetComponent());
+ var label = new GameObject("Label_" + stick.name);
+ label.transform.SetParent(stick.transform, false);
+ label.transform.localPosition = new Vector3(0.8f, 0.5f, 0f);
+ var text = label.AddComponent();
+ text.text = h.ToString("0.0", CultureInfo.InvariantCulture) + "m";
+ text.characterSize = 0.08f;
+ text.fontSize = 36;
+ text.anchor = TextAnchor.MiddleLeft;
+ text.color = new Color(0.2f, 0.25f, 0.3f);
+ }
+ }
+
+ static void AddForwardMarker(Transform subject)
+ {
+ var marker = GameObject.CreatePrimitive(PrimitiveType.Cube);
+ marker.name = "ForwardMinusZ";
+ marker.transform.SetParent(subject, false);
+ marker.transform.localPosition = new Vector3(0f, 0.05f, -0.55f);
+ marker.transform.localScale = new Vector3(0.12f, 0.04f, 0.45f);
+ UnityEngine.Object.DestroyImmediate(marker.GetComponent());
+ var renderer = marker.GetComponent();
+ var mat = new Material(Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard"));
+ mat.color = new Color(0.2f, 0.55f, 0.95f);
+ renderer.sharedMaterial = mat;
+ }
+
+ static void AddNameLabel(Transform subject, string label)
+ {
+ var go = new GameObject("NameLabel");
+ go.transform.SetParent(subject, false);
+ go.transform.localPosition = new Vector3(0f, 2.1f, 0f);
+ var text = go.AddComponent();
+ text.text = label;
+ text.anchor = TextAnchor.LowerCenter;
+ text.alignment = TextAlignment.Center;
+ text.characterSize = 0.06f;
+ text.fontSize = 40;
+ text.color = new Color(0.12f, 0.16f, 0.2f);
+ }
+
+ static void EnsureAnimCycle(GameObject subjectRoot)
+ {
+ var animator = subjectRoot.GetComponentInChildren(true);
+ if (animator == null || animator.runtimeAnimatorController == null) return;
+ bool hasMotion = false;
+ foreach (var p in animator.parameters)
+ {
+ if (p.name == "Motion" && p.type == AnimatorControllerParameterType.Int)
+ {
+ hasMotion = true;
+ break;
+ }
+ }
+ if (!hasMotion) return;
+ var cycle = subjectRoot.GetComponent();
+ if (cycle == null) cycle = subjectRoot.AddComponent();
+ var clips = animator.runtimeAnimatorController.animationClips.Select(c=>c.name).Distinct().ToArray();
+ var serialized = new SerializedObject(cycle);
+ var names = serialized.FindProperty("motionNames");
+ names.arraySize = clips.Length;
+ for(int i=0;ib.localToWorldMatrix*poses[i]).ToArray();
+ var weights=mesh.boneWeights;
+ var vertices=mesh.vertices;
+ Bounds bounds=default;
+ for(int i=0;i(true))
+ cycle.enabled = false;
+ foreach (var animator in root.GetComponentsInChildren(true))
+ {
+ if (animator.runtimeAnimatorController == null) continue;
+ foreach (var p in animator.parameters)
+ {
+ if (p.name == "Motion" && p.type == AnimatorControllerParameterType.Int)
+ {
+ animator.SetInteger("Motion", 0);
+ animator.Update(0f);
+ break;
+ }
+ }
+ }
+ var report = new StringBuilder("PASS ");
+ foreach (var subject in data.subjects)
+ {
+ var node = root.transform.Find("Subjects/" + subject.id);
+ if (node == null) throw new Exception("Missing subject node " + subject.id);
+ var renderers = node.GetComponentsInChildren(true)
+ .Where(r => r.name != "ForwardMinusZ" && r.name != "NameLabel" && r.GetComponentInParent() == null && !r.name.StartsWith("Label_", StringComparison.Ordinal))
+ .ToArray();
+ if (renderers.Length == 0) throw new Exception(subject.id + " has no mesh renderer");
+ var bounds = ActualBounds(renderers[0]);
+ for (int i = 1; i < renderers.Length; i++) bounds.Encapsulate(ActualBounds(renderers[i]));
+ if (bounds.min.y < -0.05f || bounds.min.y > 0.08f)
+ throw new Exception(subject.id + " bottom y=" + bounds.min.y.ToString("F3") + " (want near 0)");
+ float height = bounds.size.y;
+ if (Mathf.Abs(height - subject.expectedHeight) > subject.heightTolerance)
+ throw new Exception(subject.id + " height=" + height.ToString("F2") + " expected " + subject.expectedHeight);
+ int tris = 0;
+ foreach (var filter in node.GetComponentsInChildren(true))
+ if (filter.sharedMesh != null) tris += filter.sharedMesh.triangles.Length / 3;
+ foreach (var skin in node.GetComponentsInChildren(true))
+ if (skin.sharedMesh != null) tris += skin.sharedMesh.triangles.Length / 3;
+ if (tris > subject.maxTriangles)
+ throw new Exception(subject.id + " tris=" + tris + " > " + subject.maxTriangles);
+ if (subject.requireXpbr)
+ {
+ foreach (var renderer in renderers)
+ {
+ if (renderer.sharedMaterial == null || renderer.sharedMaterial.shader == null)
+ throw new Exception(subject.id + " missing material");
+ if (renderer.sharedMaterial.shader.name != "Test/XPbr")
+ throw new Exception(subject.id + " shader=" + renderer.sharedMaterial.shader.name);
+ }
+ }
+ if (node.Find("ForwardMinusZ") == null) throw new Exception(subject.id + " missing -Z marker");
+ if (subject.requireAnimator)
+ {
+ var animator = node.GetComponentInChildren(true);
+ if (animator == null || animator.runtimeAnimatorController == null)
+ throw new Exception(subject.id + " missing Animator/controller");
+ var ctrl = animator.runtimeAnimatorController;
+ var clipNames = new HashSet(StringComparer.OrdinalIgnoreCase);
+ foreach (var clip in ctrl.animationClips) clipNames.Add(clip.name);
+ foreach (var need in subject.requiredClips)
+ {
+ bool hit = clipNames.Any(n => n.IndexOf(need, StringComparison.OrdinalIgnoreCase) >= 0);
+ if (!hit) throw new Exception(subject.id + " missing clip " + need + " in " + string.Join(",", clipNames));
+ }
+ }
+ report.Append(subject.id).Append("(h=").Append(height.ToString("F2")).Append(",tris=").Append(tris).Append(") ");
+ }
+ WriteReport(data, scenePath, "validated");
+ return report.ToString();
+ }
+
+ public static string CaptureReview()
+ {
+ return OpenReview() + "; then run: unity command capture_scene_view --width 1600 --height 900 --save_path " + CapturePath;
+ }
+
+ public static string BuildOpenValidate()
+ {
+ string built = Build();
+ OpenReview();
+ string validated = ValidateReview();
+ return built + " | " + validated;
+ }
+}
+#endif
diff --git a/AIClient/AgentScripts/BuildChest.cs b/AIClient/AgentScripts/BuildChest.cs
new file mode 100644
index 0000000..a91a61c
--- /dev/null
+++ b/AIClient/AgentScripts/BuildChest.cs
@@ -0,0 +1,17 @@
+using System;
+using System.IO;
+using AIGame.EditorTools;
+using UnityEditor;
+using UnityEngine;
+
+public static class BuildChest
+{
+ public static string Build()
+ {
+ if (EditorApplication.isPlaying) throw new Exception("Build chest in Edit Mode");
+ string repo = Directory.GetParent(Application.dataPath).Parent.FullName;
+ if (string.IsNullOrEmpty(JsonUIPrefabBuilder.BuildFromJsonFile(Path.Combine(repo, "Doc/UIPrefabCreater/UI_PrototypeChest.json")))) throw new Exception("Chest prefab failed");
+ SetupUiBundleCollector.Setup(); AssetDatabase.SaveAssets();
+ return "Built Candy chest page: daily chest, owned chest list, fixed/choice contents and open buttons from JSON.";
+ }
+}
diff --git a/AIClient/AgentScripts/BuildDaily.cs b/AIClient/AgentScripts/BuildDaily.cs
new file mode 100644
index 0000000..05d6291
--- /dev/null
+++ b/AIClient/AgentScripts/BuildDaily.cs
@@ -0,0 +1,18 @@
+using System;
+using System.IO;
+using AIGame.EditorTools;
+using UnityEditor;
+using UnityEngine;
+
+public static class BuildDaily
+{
+ public static string Build()
+ {
+ if(EditorApplication.isPlaying) throw new Exception("Build daily UI in Edit Mode");
+ string repo=Directory.GetParent(Application.dataPath).Parent.FullName;
+ foreach(string name in new[]{"UI_PrototypeTasks","UI_PrototypeSignin","UI_MainHUD"})
+ if(string.IsNullOrEmpty(JsonUIPrefabBuilder.BuildFromJsonFile(Path.Combine(repo,"Doc/UIPrefabCreater/"+name+".json")))) throw new Exception("Build failed "+name);
+ SetupUiBundleCollector.Setup();AssetDatabase.SaveAssets();
+ return "Built task, seven-day sign-in and home reward-dot prefabs through JSON; reused existing sprites.";
+ }
+}
diff --git a/AIClient/AgentScripts/BuildEnemyReview.cs b/AIClient/AgentScripts/BuildEnemyReview.cs
new file mode 100644
index 0000000..b03d706
--- /dev/null
+++ b/AIClient/AgentScripts/BuildEnemyReview.cs
@@ -0,0 +1,135 @@
+#if UNITY_EDITOR
+using System;
+using System.IO;
+using System.Linq;
+using UnityEditor;
+using UnityEditor.SceneManagement;
+using UnityEngine;
+using UnityEngine.SceneManagement;
+using Object=UnityEngine.Object;
+
+public static class BuildEnemyReview
+{
+ [Serializable] public sealed class Recipe {public string id,assetRoot,staticPrefab,enemyPrefab,visualPrefab,reviewScene,usage;public float attackImpact,height,reviewOriginX;}
+ static Recipe Read(string path)
+ {
+ var r=JsonUtility.FromJson(File.ReadAllText(Path.GetFullPath(Path.Combine(Application.dataPath,"../..",path))));
+ if(string.IsNullOrEmpty(r.visualPrefab))r.visualPrefab=r.enemyPrefab;
+ if(r.reviewOriginX==0)r.reviewOriginX=20;
+ return r;
+ }
+ static bool Companion(Recipe r)=>r.usage=="companion_visual";
+ static string Prefix(Recipe r)=>Companion(r) ? "CompanionReview_"+r.id : "EnemyReview";
+ static void Folder(string path)
+ {
+ string current="Assets";
+ foreach(string part in path.Split('/').Skip(1)) {string next=current+"/"+part;if(!AssetDatabase.IsValidFolder(next))AssetDatabase.CreateFolder(current,part);current=next;}
+ }
+ public static string Publish(string recipePath="Config/Art/EnemyCharacters/RockBall.json")
+ {
+ var r=Read(recipePath);Folder(Path.GetDirectoryName(r.visualPrefab).Replace('\\','/'));
+ var scene=EditorSceneManager.NewPreviewScene();
+ try
+ {
+ var source=AssetDatabase.LoadAssetAtPath(r.assetRoot+"/"+r.id+"_Rigged.prefab");
+ var go=(GameObject)PrefabUtility.InstantiatePrefab(source,scene);go.name=r.id+(Companion(r) ? "_Companion" : "_Enemy");
+ var animator=go.GetComponentInChildren();
+ if(animator.runtimeAnimatorController.animationClips.Select(c=>c.name).Distinct().Count()!=3)throw new Exception("Three actions required before publishing character visual");
+ PrefabUtility.SaveAsPrefabAsset(go,r.visualPrefab);
+ }
+ finally {EditorSceneManager.ClosePreviewScene(scene);}
+ AssetDatabase.SaveAssets();return r.visualPrefab;
+ }
+ public static string Build(string recipePath="Config/Art/EnemyCharacters/RockBall.json")
+ {
+ if(EditorApplication.isPlaying)throw new Exception("Build in Edit Mode");
+ var r=Read(recipePath);Publish(recipePath);
+ var existing=SceneManager.GetSceneByPath(r.reviewScene);
+ if(existing.IsValid() && existing.isLoaded)
+ {
+ if(existing.isDirty)throw new Exception("Existing character review has unsaved edits; preserve before rebuilding");
+ EditorSceneManager.CloseScene(existing,true);
+ }
+ Folder(Path.GetDirectoryName(r.reviewScene).Replace('\\','/'));
+ // Additive + a separate render layer preserves any user edits in other scenes.
+ var scene=EditorSceneManager.NewScene(NewSceneSetup.EmptyScene,NewSceneMode.Additive);
+ SceneManager.SetActiveScene(scene);
+ var root=new GameObject((Companion(r) ? "CompanionProductionReview_" : "EnemyProductionReview_")+r.id);
+ const int layer=29;
+ const string floorPath="Assets/Bundles/Scenes/Review/EnemyReviewFloor.mat";
+ var mat=AssetDatabase.LoadAssetAtPath(floorPath);
+ if(mat==null){mat=new Material(Shader.Find("Universal Render Pipeline/Lit"));mat.color=new Color(.48f,.56f,.50f);AssetDatabase.CreateAsset(mat,floorPath);}
+ float spacing=Mathf.Max(1,r.height*1.4f);
+ var floor=GameObject.CreatePrimitive(PrimitiveType.Plane);floor.name="Ground_Y0";floor.transform.SetParent(root.transform,false);
+ floor.transform.position=new Vector3(r.reviewOriginX,0,0);floor.transform.localScale=Vector3.one*(.8f*spacing);floor.GetComponent().sharedMaterial=mat;
+ string[] names={"Static","Idle","Walk","Attack","Cycle"};
+ for(int i=0;i(i==0 ? r.staticPrefab : r.visualPrefab);
+ var go=(GameObject)PrefabUtility.InstantiatePrefab(asset,root.transform);go.name=Prefix(r)+"_"+names[i];
+ go.transform.position=new Vector3(r.reviewOriginX+(i-2)*spacing,0,0);
+ var label=new GameObject("NameLabel");label.transform.SetParent(go.transform,false);label.transform.position=go.transform.position+Vector3.up*(r.height+.21f);
+ label.transform.localScale=Vector3.one/go.transform.lossyScale.x;
+ var text=label.AddComponent();text.text=names[i];text.anchor=TextAnchor.MiddleCenter;text.characterSize=.01f;text.fontSize=42;text.color=new Color(.12f,.18f,.2f);
+ var marker=GameObject.CreatePrimitive(PrimitiveType.Cube);marker.name="ForwardMinusZ";marker.transform.SetParent(go.transform,false);
+ marker.transform.position=go.transform.position+new Vector3(0,.012f,-.4f);marker.transform.localScale=new Vector3(.035f,.015f,.22f)/go.transform.lossyScale.x;
+ Object.DestroyImmediate(marker.GetComponent());
+ if(i>0)
+ {
+ var cycle=go.AddComponent();cycle.enabled=false;
+ var so=new SerializedObject(cycle);var modes=so.FindProperty("motionNames");modes.arraySize=3;
+ for(int m=0;m<3;m++)modes.GetArrayElementAtIndex(m).stringValue=new[]{"Idle","Walk","Attack"}[m];
+ so.FindProperty("fixedMotion").intValue=i==4 ? -1 : i-1;
+ so.ApplyModifiedPropertiesWithoutUndo();cycle.enabled=true;
+ }
+ }
+ var sun=new GameObject(Prefix(r)+"Sun").AddComponent();sun.transform.SetParent(root.transform,false);
+ sun.type=LightType.Directional;sun.intensity=1.0f;sun.shadows=LightShadows.Soft;sun.cullingMask=1<();camera.transform.SetParent(root.transform,false);
+ camera.clearFlags=CameraClearFlags.SolidColor;camera.backgroundColor=new Color(.66f,.76f,.81f);camera.depth=50+r.reviewOriginX-20;camera.cullingMask=1<(true))t.gameObject.layer=layer;
+ RenderSettings.ambientMode=UnityEngine.Rendering.AmbientMode.Trilight;
+ RenderSettings.ambientSkyColor=new Color(.65f,.72f,.8f);
+ RenderSettings.ambientEquatorColor=new Color(.45f,.5f,.48f);
+ RenderSettings.ambientGroundColor=new Color(.3f,.28f,.25f);
+ EditorSceneManager.SaveScene(scene,r.reviewScene);
+ var view=SceneView.lastActiveSceneView;if(view!=null){view.LookAt(new Vector3(r.reviewOriginX,.35f,0),Quaternion.Euler(12,-12,0),3.6f);view.Repaint();}
+ return "Built additive review "+r.reviewScene+"; other scenes preserved";
+ }
+ public static string Capture(string recipePath="Config/Art/EnemyCharacters/RockBall.json")
+ {
+ var r=Read(recipePath);var camera=GameObject.Find(Prefix(r)+"Camera").GetComponent();
+ var units=new[]{"Idle","Walk","Attack","Cycle"}.Select(n=>GameObject.Find(Prefix(r)+"_"+n)).ToArray();
+ var cycles=units.Select(g=>g.GetComponent()).ToArray();
+ var labels=units.Select(g=>g.transform.Find("NameLabel").GetComponent()).ToArray();
+ var labelTexts=labels.Select(t=>t.text).ToArray();
+ var animators=units.Select(g=>g.GetComponentInChildren()).ToArray();
+ var transforms=units.SelectMany(g=>g.GetComponentsInChildren()).ToArray();
+ var p=transforms.Select(t=>t.localPosition).ToArray();var q=transforms.Select(t=>t.localRotation).ToArray();var s=transforms.Select(t=>t.localScale).ToArray();
+ var rt=new RenderTexture(1900,760,24);var tex=new Texture2D(1900,760,TextureFormat.RGB24,false);var active=RenderTexture.active;
+ try
+ {
+ for(int i=0;i(r.assetRoot+"/Clips/"+name+".anim");
+ clip.SampleAnimation(animators[i].gameObject,clip.length*(i==0 ? .5f : i==1 ? .25f : r.attackImpact));
+ }
+ camera.targetTexture=rt;camera.Render();RenderTexture.active=rt;tex.ReadPixels(new Rect(0,0,1900,760),0,0);tex.Apply();
+ string path="Assets/Screenshots/"+(Companion(r) ? "companion_" : "enemy_")+r.id.ToLowerInvariant()+"_review.png";
+ File.WriteAllBytes(Path.Combine(Application.dataPath,"../",path),tex.EncodeToPNG());return path;
+ }
+ finally
+ {
+ RenderTexture.active=active;camera.targetTexture=null;rt.Release();Object.DestroyImmediate(rt);Object.DestroyImmediate(tex);
+ for(int i=0;iPipeline-only import, JSON-to-Prefab build, isolated rendering and binding checks.
+public static class BuildHomeUi
+{
+ static string Repo => Directory.GetParent(Application.dataPath).Parent.FullName;
+ const string Folder = "Assets/Bundles/UI/Texture/Home";
+ static readonly string[] Icons = { "coin", "diamond", "energy", "avatar", "tasks", "gift", "mail", "trophy", "bag", "home", "battle", "pets", "chest", "calendar", "settings", "pinball" };
+ static readonly string[] Names = { "UI_MainHUD" };
+ static string Design => Path.Combine(Repo, "Doc/Design/UI/Home");
+
+ public static string Build()
+ {
+ if (EditorApplication.isPlaying) throw new Exception("Build in Edit Mode.");
+ Directory.CreateDirectory(Folder);
+ File.Copy(Path.Combine(Design, "home_camp_source.png"), Folder + "/home_camp.png", true);
+ var atlas = new Texture2D(2, 2, TextureFormat.RGBA32, false);
+ try
+ {
+ if (!atlas.LoadImage(File.ReadAllBytes(Path.Combine(Design, "home_icons_source.png")))) throw new Exception("Invalid icon sheet.");
+ for (int i = 0; i < Icons.Length; i++)
+ {
+ int col = i % 4, row = 3 - i / 4;
+ int x = Mathf.RoundToInt(col * atlas.width / 4f), y = Mathf.RoundToInt(row * atlas.height / 4f);
+ int w = Mathf.RoundToInt((col + 1) * atlas.width / 4f) - x;
+ int h = Mathf.RoundToInt((row + 1) * atlas.height / 4f) - y;
+ var icon = new Texture2D(w, h, TextureFormat.RGBA32, false);
+ try { icon.SetPixels(CleanSlice(atlas.GetPixels(x, y, w, h), w, h)); icon.Apply(); File.WriteAllBytes(Folder + "/home_ico_" + Icons[i] + ".png", icon.EncodeToPNG()); }
+ finally { UnityEngine.Object.DestroyImmediate(icon); }
+ }
+ }
+ finally { UnityEngine.Object.DestroyImmediate(atlas); }
+ AssetDatabase.Refresh();
+ foreach (var path in Directory.GetFiles(Folder, "*.png"))
+ {
+ var importer = (TextureImporter)AssetImporter.GetAtPath(path);
+ importer.textureType = TextureImporterType.Sprite;
+ importer.spriteImportMode = SpriteImportMode.Single;
+ importer.spritePixelsPerUnit = 100;
+ var settings = new TextureImporterSettings(); importer.ReadTextureSettings(settings);
+ settings.spriteMeshType = SpriteMeshType.FullRect; importer.SetTextureSettings(settings);
+ importer.alphaIsTransparency = true; importer.mipmapEnabled = false;
+ importer.textureCompression = TextureImporterCompression.Uncompressed;
+ importer.filterMode = FilterMode.Bilinear; importer.wrapMode = TextureWrapMode.Clamp;
+ importer.maxTextureSize = 2048; importer.SaveAndReimport();
+ }
+ foreach (var name in Names)
+ {
+ var path = Path.Combine(Repo, "Doc/UIPrefabCreater", name + ".json");
+ var validation = JsonUIPrefabBuilder.Validate(path);
+ if (!validation.Ok) throw new Exception(string.Join("\n", validation.Errors));
+ if (string.IsNullOrEmpty(JsonUIPrefabBuilder.BuildFromJsonFile(path))) throw new Exception("Build failed: " + name);
+ }
+ return "Imported 17 Home sprites; built Candy home HUD prefab from JSON.";
+ }
+
+ // Atlas extraction: discard isolated fragments spilling in from adjacent cells.
+ // This preserves original icon pixels and alpha; it does not repaint the source artwork.
+ static Color[] CleanSlice(Color[] pixels, int width, int height)
+ {
+ var visited = new bool[pixels.Length];
+ var components = new List>();
+ for (int start = 0; start < pixels.Length; start++)
+ {
+ if (visited[start] || pixels[start].a < 0.01f) continue;
+ var queue = new Queue(); var component = new List(); queue.Enqueue(start); visited[start] = true;
+ while (queue.Count > 0)
+ {
+ int p = queue.Dequeue(); component.Add(p);
+ int px = p % width, py = p / width;
+ for (int dy = -1; dy <= 1; dy++) for (int dx = -1; dx <= 1; dx++)
+ {
+ int nx = px + dx, ny = py + dy;
+ if (nx < 0 || ny < 0 || nx >= width || ny >= height) continue;
+ int next = ny * width + nx;
+ if (visited[next] || pixels[next].a < 0.01f) continue;
+ visited[next] = true; queue.Enqueue(next);
+ }
+ }
+ components.Add(component);
+ }
+ int largest = components.Count > 0 ? components.Max(c => c.Count) : 0;
+ foreach (var c in components.Where(c => c.Count < largest * 0.03f))
+ foreach (int p in c) pixels[p] = Color.clear;
+ return pixels;
+ }
+
+ public static string Verify()
+ {
+ int checks = 0;
+ Action check = (ok, message) => { if (!ok) throw new Exception(message); checks++; };
+ foreach (var name in Names)
+ {
+ var doc = JObject.Parse(File.ReadAllText(Path.Combine(Repo, "Doc/UIPrefabCreater", name + ".json")));
+ var prefab = AssetDatabase.LoadAssetAtPath("Assets/Bundles/UI/Prefab/" + name + ".prefab");
+ check(prefab != null, "Missing prefab " + name);
+ var scene = EditorSceneManager.NewPreviewScene();
+ var go = UnityEngine.Object.Instantiate(prefab); SceneManager.MoveGameObjectToScene(go, scene);
+ try
+ {
+ foreach (var path in ((JObject)doc["assets"]["sprites"]).Properties())
+ check(AssetDatabase.LoadAssetAtPath((string)path.Value) != null, "Missing sprite " + path.Name);
+ var images = go.GetComponentsInChildren(true);
+ check(images.All(i => i.sprite != null), "Unresolved prefab image " + name);
+ check(go.GetComponentsInChildren().Single().referenceResolution == new Vector2(1080, 1920), "Wrong resolution");
+ foreach (var b in doc["bindings"])
+ check(go.GetComponentsInChildren(true).Any(t => t.name == (string)b), "Missing binding " + b);
+ check(!go.GetComponentsInChildren(true).Any(t => t.name == "UI_ThemeSwitcher"), "Unexpected theme toolbar");
+ var view = go.AddComponent(); view.Bind();
+ string called = null; int invocations = 0, reconnect = 0;
+ view.OnHomeActionClicked += k => { called = k; invocations++; };
+ view.OnReconnectClicked += () => reconnect++;
+ view.Bind(); // Bind is safe to repeat.
+ foreach (var e in doc["events"])
+ {
+ var button = go.GetComponentsInChildren