using System; using System.IO; using System.Linq; using System.Reflection; using AIGame.EditorTools; using AOT.UI; using HotUpdate.Gameplay; using Newtonsoft.Json; using TMPro; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.UI; using Object = UnityEngine.Object; // Run through Unity Pipeline. Uses isolated preview scenes and in-memory saves; // never contacts the game server or modifies player progress. public static class ReviewSecondaryUi { static string Repo => Path.GetFullPath(Path.Combine(Application.dataPath, "../..")); static string Output => Path.Combine(Repo, "Tools/output/secondary_ui"); const BindingFlags Private = BindingFlags.Instance | BindingFlags.NonPublic; static void Check(bool valid, string message) { if (!valid) throw new Exception(message); } static void Set(object target, string field, object value) => target.GetType().GetField(field, Private).SetValue(target, value); static void Refresh(PrototypeFlow flow, string page) => typeof(PrototypeFlow).GetMethod("Refresh" + page, Private).Invoke(flow, null); public static string Build() { Check(!EditorApplication.isPlaying, "Exit Play Mode before authoring prefabs."); CreateControls(); return BuildPages(); } public static string BuildBackground() { Check(!EditorApplication.isPlaying, "Exit Play Mode before authoring prefabs."); const string path = "Assets/Bundles/UI/Bg/journal_paper_v1.png"; const string folder = "Assets/Bundles/UI/Bg"; if (!AssetDatabase.IsValidFolder(folder)) AssetDatabase.CreateFolder("Assets/Bundles/UI", "Bg"); if (!File.Exists(path)) File.Copy(Path.Combine(Output, "journal_background_generated.png"), path); AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceSynchronousImport); var importer = (TextureImporter)AssetImporter.GetAtPath(path); importer.textureType = TextureImporterType.Sprite; importer.spriteImportMode = SpriteImportMode.Single; importer.spriteBorder = Vector4.zero; importer.mipmapEnabled = false; importer.wrapMode = TextureWrapMode.Clamp; importer.filterMode = FilterMode.Bilinear; importer.maxTextureSize = 2048; importer.npotScale = TextureImporterNPOTScale.None; importer.textureCompression = TextureImporterCompression.CompressedHQ; var settings = new TextureImporterSettings(); importer.ReadTextureSettings(settings); settings.spriteMeshType = SpriteMeshType.FullRect; importer.SetTextureSettings(settings); importer.SaveAndReimport(); return BuildPages(); } static string BuildPages() { foreach (string key in new[] { "Tasks", "Signin", "Mail", "Challenges" }) { string json = Path.Combine(Repo, "Doc/UIPrefabCreater/UI_Prototype" + key + ".json"); var validation = JsonUIPrefabBuilder.Validate(json); Check(validation.Ok, string.Join("\n", validation.Errors)); Check(!string.IsNullOrEmpty(JsonUIPrefabBuilder.BuildFromJsonFile(json)), "Build failed: " + key); } return "Built Tasks, Signin, Mail and Challenges through the JSON authoring pipeline."; } static void CreateControls() { const string folder = "Assets/Bundles/UI/Texture/Journal"; if (!AssetDatabase.IsValidFolder(folder)) AssetDatabase.CreateFolder("Assets/Bundles/UI/Texture", "Journal"); Control("journal_card", "#FFFDF7", "#F8F1E4", "#DFDCCF"); Control("journal_hero", "#345D72", "#23465B", "#203D50"); Control("journal_gold_card", "#FFF2D4", "#F7DFAD", "#DCBF83"); Control("journal_primary_normal", "#FFD987", "#F1B94F", "#D5A044"); Control("journal_primary_pressed", "#EDB650", "#DCA43F", "#BB8530"); Control("journal_secondary_normal", "#EBF4F7", "#D5E5EC", "#B5CAD4"); Control("journal_secondary_pressed", "#C8DDE5", "#B5CCD7", "#99B5C3"); Control("journal_disabled", "#E4E8E7", "#D1D8D8", "#BBC7C9"); Control("journal_progress_bg", "#D8DFDF", "#D0D9D9", "#C7D2D3"); Control("journal_progress_fill", "#F6D48A", "#EDBA58", "#D9A747"); } static Color Hex(string hex) { ColorUtility.TryParseHtmlString(hex, out var color); return color; } static float RoundedDistance(float x, float y, float cx, float cy, float hx, float hy, float radius) { float dx = Mathf.Abs(x-cx)-(hx-radius), dy = Mathf.Abs(y-cy)-(hy-radius); return new Vector2(Mathf.Max(dx,0),Mathf.Max(dy,0)).magnitude + Mathf.Min(Mathf.Max(dx,dy),0)-radius; } static void Control(string name, string topHex, string bottomHex, string edgeHex) { string path = "Assets/Bundles/UI/Texture/Journal/" + name + ".png"; var texture = new Texture2D(128,128,TextureFormat.RGBA32,false); var top=Hex(topHex); var bottom=Hex(bottomHex); var edge=Hex(edgeHex); for(int y=0;y<128;y++) for(int x=0;x<128;x++) { float distance=RoundedDistance(x+.5f,y+.5f,64,66,60,58,22); float alpha=Mathf.Clamp01(.5f-distance); float shadow=Mathf.Clamp01(.5f-RoundedDistance(x+.5f,y+.5f,64,62,60,58,22))*.13f; Color color=Color.Lerp(bottom,top,Mathf.Clamp01((y-8)/116f)); color=Color.Lerp(edge,color,Mathf.Clamp01(-distance-1)); // A restrained inner highlight gives the candy controls volume. if(distance < -2 && y > 114) color=Color.Lerp(color,Color.white,.12f); color.a=alpha; if(alpha<1) { var shade=new Color(.12f,.21f,.26f,shadow); color=Color.Lerp(shade,color,alpha); color.a=Mathf.Max(alpha,shadow); } texture.SetPixel(x,y,color); } texture.Apply(); File.WriteAllBytes(path,texture.EncodeToPNG()); Object.DestroyImmediate(texture); AssetDatabase.ImportAsset(path,ImportAssetOptions.ForceSynchronousImport); var importer=(TextureImporter)AssetImporter.GetAtPath(path); importer.textureType=TextureImporterType.Sprite; importer.spriteImportMode=SpriteImportMode.Single; importer.spriteBorder=new Vector4(30,32,30,28); importer.spritePixelsPerUnit=100; importer.alphaIsTransparency=true; importer.mipmapEnabled=false; importer.textureCompression=TextureImporterCompression.Uncompressed; var settings=new TextureImporterSettings(); importer.ReadTextureSettings(settings); settings.spriteMeshType=SpriteMeshType.FullRect; importer.SetTextureSettings(settings); importer.SaveAndReimport(); } public static string Review() { Directory.CreateDirectory(Output); foreach (string key in new[] { "Tasks", "Signin", "Mail", "Challenges" }) Render(key, 1080, 1920, false); Render("Tasks", 1080, 2400, true); Render("Tasks", 1080, 1680, false); Render("Challenges", 1080, 2400, true); Render("Challenges", 1080, 1680, false); File.WriteAllText(Path.Combine(Output, "verification_journal_bg.txt"), "PASS: JSON prefabs; real RefreshTasks/Signin/Mail/Challenges with isolated save data; task states incomplete/ready/claimed, request blocking and daily reset; signin locked/available/claimed; mail empty/selected; challenges unlocked/detail/start/squad; full-screen safe-area insets; short-screen scroll reachability; journal background serialized sprite and proportional full-screen cover.\nRendered 4 pages and additional aspect/safe-area scenarios.\n"); return "PASS. Screenshots and verification: " + Output; } static void Render(string key, int width, int height, bool notch) { var scene = EditorSceneManager.NewPreviewScene(); var previous = RenderTexture.active; RenderTexture target = null; Texture2D pixels = null; try { var prefab = AssetDatabase.LoadAssetAtPath("Assets/Bundles/UI/Prefab/UI_Prototype" + key + ".prefab"); var root = (GameObject)PrefabUtility.InstantiatePrefab(prefab, scene); root.name = prefab.name; var canvas = root.GetComponent(); root.GetComponent().enabled = false; var cameraGo = new GameObject("ReviewCamera", typeof(Camera)); UnityEngine.SceneManagement.SceneManager.MoveGameObjectToScene(cameraGo, scene); var camera = cameraGo.GetComponent(); camera.scene = scene; camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = Color.white; camera.orthographic = true; camera.orthographicSize = height / 2f; camera.transform.position = new Vector3(0, 0, -10); target = new RenderTexture(width, height, 24); target.Create(); camera.targetTexture = target; canvas.renderMode = RenderMode.ScreenSpaceCamera; canvas.worldCamera = camera; canvas.planeDistance = 1; canvas.scaleFactor = 1; Canvas.ForceUpdateCanvases(); var background = root.transform.Find("Background"); var cover = background.GetComponent(); Check(cover != null && background.GetComponent().sprite != null, "Missing journal background asset or cover component."); cover.SendMessage("OnEnable"); Canvas.ForceUpdateCanvases(); var backgroundRect = (RectTransform)background; var parentRect = (RectTransform)root.transform; float aspect = background.GetComponent().sprite.rect.width / background.GetComponent().sprite.rect.height; Check(backgroundRect.rect.width >= parentRect.rect.width - 1 && backgroundRect.rect.height >= parentRect.rect.height - 1, "Journal background does not cover the page."); Check(Mathf.Abs(backgroundRect.rect.width / backgroundRect.rect.height - aspect) < .001f, "Journal background is stretched."); var config = JsonConvert.DeserializeObject(File.ReadAllText(Path.Combine(Repo, "Config/Gameplay/greenfield.json"))); var save = new PrototypeSave { schema = 10, clearedStages = new int[20] { 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, stages = new int[10] { 1, 1, 1, 1, 1, 0, 0, 0, 0, 0 }, shards = new int[10], formation = new int[6] { 0, 1, 2, -1, -1, -1 }, decorations = new int[3], campLayout = new int[3] { -1, -1, -1 }, dailyProgress = new[] { 1, 1, 1 }, dailyClaimed = new[] { 0, 0, 1 }, signinDays = 3, signinClaimed = new[] { 1, 1, 0, 0, 0, 0, 0 }, shopBought = new int[3], chests = new int[3], challengeBadges = new[] { 1, 0, 0, 0, 0 }, challengeBestMs = new[] { 4200, 0, 0, 0, 0 }, cleared = true, goalClaimed = true, mails = new[] { new PrototypeMail { mailId="review_welcome", title="欢迎来到小镇", body="亲爱的探险家:\n欢迎回到营地!送上一份冒险补给,愿你和伙伴度过愉快的一天。", sentAt=100000, expiresAt=200000, reward=new StageReward { balls=10 } }, new PrototypeMail { mailId="review_gift", title="伙伴们的见面礼", body="带上伙伴,一起出发吧。", read=true, sentAt=100000, expiresAt=200000, reward=new StageReward { coins=100 } }, new PrototypeMail { mailId="review_notice", title="营地生活小贴士", body="每日委托会在零点更新。", read=true, sentAt=100000, expiresAt=200000, reward=new StageReward() } } }; var flowGo = new GameObject("ReviewFlow"); UnityEngine.SceneManagement.SceneManager.MoveGameObjectToScene(flowGo, scene); var flow = flowGo.AddComponent(); flow.enabled = false; var panelType = typeof(PrototypeFlow).GetNestedType("Panel", BindingFlags.NonPublic); Set(flow, "_page", Activator.CreateInstance(panelType, new object[] { root })); Set(flow, "_game", new PrototypeGame(save)); Set(flow, "_config", config); var reply = new PrototypeReply { daily = new DailyStatus { resetAt = 125200 } }; Set(flow, "_reply", reply); Set(flow, "_serverUnix", 100000L); Set(flow, "_receivedAt", Time.realtimeSinceStartupAsDouble); Set(flow, "_mailSelected", "review_welcome"); Refresh(flow, key); var all = root.GetComponentsInChildren(true).Where(t => t.GetComponent() == null).ToDictionary(t => t.name); Func button = name => all["Btn_" + name].GetComponent