275 lines
17 KiB
C#
275 lines
17 KiB
C#
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<GameObject>("Assets/Bundles/UI/Prefab/UI_Prototype" + key + ".prefab");
|
|
var root = (GameObject)PrefabUtility.InstantiatePrefab(prefab, scene);
|
|
root.name = prefab.name;
|
|
var canvas = root.GetComponent<Canvas>();
|
|
root.GetComponent<CanvasScaler>().enabled = false;
|
|
var cameraGo = new GameObject("ReviewCamera", typeof(Camera));
|
|
UnityEngine.SceneManagement.SceneManager.MoveGameObjectToScene(cameraGo, scene);
|
|
var camera = cameraGo.GetComponent<Camera>(); 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<BootArtworkCover>();
|
|
Check(cover != null && background.GetComponent<Image>().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<Image>().sprite.rect.width / background.GetComponent<Image>().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<PrototypeConfig>(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<PrototypeFlow>(); 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<Transform>(true).Where(t => t.GetComponent<TMP_SubMeshUI>() == null).ToDictionary(t => t.name);
|
|
Func<string, Button> button = name => all["Btn_" + name].GetComponent<Button>();
|
|
if (key == "Tasks")
|
|
{
|
|
Check(button("GoDaily0").gameObject.activeSelf && !button("ClaimDaily0").gameObject.activeSelf, "Incomplete task must have only Go.");
|
|
Check(button("ClaimDaily1").gameObject.activeSelf && !button("GoDaily1").gameObject.activeSelf, "Ready task must have only Claim.");
|
|
Check(all["Txt_DailyDone2"].gameObject.activeSelf && !button("GoDaily2").gameObject.activeSelf && !button("ClaimDaily2").gameObject.activeSelf, "Claimed task must be a status label.");
|
|
Check(all["Txt_GoalDone"].gameObject.activeSelf && !button("ClaimGoal").gameObject.activeSelf, "Claimed growth task must be a status label.");
|
|
Set(flow, "_busy", true); Refresh(flow, key);
|
|
Check(!button("GoDaily0").interactable && !button("ClaimDaily1").interactable, "Busy state must block actions.");
|
|
Set(flow, "_busy", false); reply.daily.resetAt = 99999; Refresh(flow, key);
|
|
Check(!button("GoDaily0").interactable && !button("ClaimDaily1").interactable, "Reset must block stale daily actions.");
|
|
reply.daily.resetAt = 125200; Refresh(flow, key);
|
|
}
|
|
else if (key == "Signin")
|
|
{
|
|
Check(!button("Signin0").gameObject.activeSelf && button("Signin2").gameObject.activeSelf && !button("Signin3").gameObject.activeSelf,
|
|
"Signin claimed/available/locked state mismatch.");
|
|
}
|
|
else if (key == "Mail")
|
|
{
|
|
Check(button("ClaimMail").interactable && all["MailUnread0"].gameObject.activeSelf, "Selected unread reward mail state mismatch.");
|
|
var mails = save.mails; save.mails = new PrototypeMail[0]; Refresh(flow, key);
|
|
Check(all["Txt_MailEmpty"].gameObject.activeSelf && !button("ClaimAllMail").interactable, "Empty mailbox state mismatch.");
|
|
save.mails = mails; Refresh(flow, key);
|
|
}
|
|
else if (key == "Challenges")
|
|
{
|
|
Check(all["Txt_Summary"].gameObject.activeSelf, "Summary text missing.");
|
|
Check(all["Txt_Detail"].gameObject.activeSelf, "Detail text missing.");
|
|
Check(button("Start").gameObject.activeSelf, "Start button missing.");
|
|
Check(button("Challenge0").gameObject.activeSelf, "Challenge0 button missing.");
|
|
Check(button("Capture").gameObject.activeSelf, "Capture button missing.");
|
|
Check(button("Fish").gameObject.activeSelf, "Fish button missing.");
|
|
}
|
|
|
|
var safe = root.GetComponentInChildren<UiSafeArea>();
|
|
UiSafeArea.SetPreviewInsets(notch ? new Vector4(0, .04f, 0, .11f) : Vector4.zero);
|
|
safe.SendMessage("OnEnable"); safe.SendMessage("LateUpdate");
|
|
Canvas.ForceUpdateCanvases();
|
|
if (key == "Mail")
|
|
{
|
|
Check(((RectTransform)all["Btn_Mail0"]).rect.width >= 990, "Mail list cards must fill the stack width.");
|
|
Check(((RectTransform)all["MailDetail"]).rect.width >= 990, "Letter panel must fill the stack width.");
|
|
}
|
|
if (notch)
|
|
{
|
|
var safeRect = (RectTransform)safe.transform;
|
|
Check(Math.Abs(safeRect.offsetMax.y + height * .11f) < 2, "Safe-area top inset missing.");
|
|
Check(Math.Abs(safeRect.offsetMin.y - height * .04f) < 2, "Safe-area bottom inset missing.");
|
|
}
|
|
var scroll = all["Scroll_Page"].GetComponent<ScrollRect>();
|
|
if (height == 1680)
|
|
{
|
|
Check(scroll.content.rect.height > scroll.viewport.rect.height, "Short phone needs scrollable content.");
|
|
scroll.verticalNormalizedPosition = 0; Canvas.ForceUpdateCanvases();
|
|
Check(Mathf.Abs(scroll.verticalNormalizedPosition) < .01f, "Footer cannot be reached by scrolling.");
|
|
scroll.verticalNormalizedPosition = 1;
|
|
}
|
|
foreach (var text in root.GetComponentsInChildren<TextMeshProUGUI>(true)) text.ForceMeshUpdate(true);
|
|
Canvas.ForceUpdateCanvases(); camera.Render();
|
|
RenderTexture.active = target;
|
|
pixels = new Texture2D(width, height, TextureFormat.RGB24, false);
|
|
pixels.ReadPixels(new Rect(0, 0, width, height), 0, 0); pixels.Apply();
|
|
File.WriteAllBytes(Path.Combine(Output, key.ToLowerInvariant() + "_" + width + "x" + height + (notch ? "_safe" : "") + "_journal_v1.png"), pixels.EncodeToPNG());
|
|
}
|
|
finally
|
|
{
|
|
UiSafeArea.SetPreviewInsets(null); RenderTexture.active = previous;
|
|
EditorSceneManager.ClosePreviewScene(scene);
|
|
if (target != null) { target.Release(); Object.DestroyImmediate(target); }
|
|
if (pixels != null) Object.DestroyImmediate(pixels);
|
|
}
|
|
}
|
|
}
|