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