#nullable enable using System; using System.Collections.Generic; using System.IO; using System.Linq; using ReplacedPerson.Runtime; using ShrinkApp; using ShrinkContext.AppAdapter; using ShrinkModFramework; using ShrinkTutorial; using TMPro; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.SceneManagement; using UnityEngine.TextCore.LowLevel; using UnityEngine.UI; namespace ReplacedPerson.Editor { public static class ReplacedPersonSceneBuilder { private const string Root = "Assets/Demos/ReplacedPerson"; private const string PrefabRoot = Root + "/Prefabs"; private const string ResourceRoot = Root + "/Resources"; private const string ScenePath = Root + "/ReplacedPerson.unity"; private const string DemoPrefabPath = PrefabRoot + "/ReplacedPersonDemo.prefab"; private const string BattlePrefabPath = PrefabRoot + "/BattleScreen.prefab"; private const string ModalPrefabPath = PrefabRoot + "/ModalOverlay.prefab"; private const string FontPath = Root + "/Art/NotoSansSC-Variable.ttf"; private const string FontAssetPath = Root + "/Art/NotoSansSC SDF.asset"; private static TMP_FontAsset? _font; private static Sprite? _paper; private static Sprite? _attack; private static Sprite? _dodge; private static Sprite? _counter; [MenuItem("ShrinkSDK/Demos/Replaced Person/Rebuild Full Demo")] public static void Rebuild() { Directory.CreateDirectory(PrefabRoot); Directory.CreateDirectory(ResourceRoot); AssetDatabase.Refresh(); EnsureSdkAssets(); ConfigureSprites(); EnsureFont(); BuildBattlePrefab(); BuildModalPrefab(); BuildDemoPrefab(); BuildTutorialAssets(); BuildScene(); AddToBuildSettings(); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); Debug.Log($"[ReplacedPerson] Demo rebuilt: {ScenePath}"); } [MenuItem("ShrinkSDK/Demos/Replaced Person/Build Windows Player")] public static void BuildWindowsPlayer() { var output = Path.GetFullPath(Path.Combine(Application.dataPath, "..", "Artifacts", "ReplacedPersonBuild", "ReplacedPerson.exe")); Directory.CreateDirectory(Path.GetDirectoryName(output)!); var report = BuildPipeline.BuildPlayer(new BuildPlayerOptions { scenes = new[] { ScenePath }, locationPathName = output, target = BuildTarget.StandaloneWindows64, options = BuildOptions.Development }); if (report.summary.result != UnityEditor.Build.Reporting.BuildResult.Succeeded) throw new InvalidOperationException($"Windows build failed: errors={report.summary.totalErrors}, warnings={report.summary.totalWarnings}"); Debug.Log($"[ReplacedPerson] Windows build complete: {output}, warnings={report.summary.totalWarnings}"); } private static void EnsureSdkAssets() { const string appSettingsPath = "Assets/Resources/ShrinkAppSettings.asset"; var appSettings = AssetDatabase.LoadAssetAtPath(appSettingsPath); var appScript = appSettings == null ? null : new SerializedObject(appSettings).FindProperty("m_Script")?.objectReferenceValue; if (appSettings == null || appScript == null) { if (!AssetDatabase.IsValidFolder("Assets/Resources")) AssetDatabase.CreateFolder("Assets", "Resources"); if (!string.IsNullOrEmpty(AssetDatabase.AssetPathToGUID(appSettingsPath))) AssetDatabase.DeleteAsset(appSettingsPath); appSettings = ScriptableObject.CreateInstance(); appSettings.hostingMode = ShrinkAppHostingMode.ContextLoader; appSettings.verboseLogging = false; AssetDatabase.CreateAsset(appSettings, appSettingsPath); } const string modSettingsPath = ResourceRoot + "/ShrinkModFrameworkSettings.asset"; var modSettings = AssetDatabase.LoadAssetAtPath(modSettingsPath); if (modSettings == null) { modSettings = ScriptableObject.CreateInstance(); modSettings.verboseLogging = false; AssetDatabase.CreateAsset(modSettings, modSettingsPath); } const string profilePath = "Assets/Resources/ShrinkAppComposition.asset"; var profile = AssetDatabase.LoadAssetAtPath(profilePath); if (profile != null) { var document = profile.ResolveDocument(); if (document.entries.All(value => !string.Equals(value.id, "demo.replaced-person", StringComparison.OrdinalIgnoreCase))) document.entries.Add(new ShrinkAppCompositionEntry("demo.replaced-person")); profile.SetDocument(document); EditorUtility.SetDirty(profile); } AssetDatabase.SaveAssets(); } private static void ConfigureSprites() { var paths = new[] { Root + "/Art/ForumOriginal/paper.jpg", Root + "/Art/ForumOriginal/card-attack-a.png", Root + "/Art/ForumOriginal/card-dodge-a.png", Root + "/Art/ForumOriginal/card-counter.png", Root + "/Art/ForumOriginal/card-back.png" }; foreach (var path in paths) { if (AssetImporter.GetAtPath(path) is not TextureImporter importer) continue; importer.textureType = TextureImporterType.Sprite; importer.spriteImportMode = SpriteImportMode.Single; importer.mipmapEnabled = false; importer.alphaIsTransparency = true; importer.SaveAndReimport(); } _paper = AssetDatabase.LoadAssetAtPath(paths[0]); _attack = AssetDatabase.LoadAssetAtPath(paths[1]); _dodge = AssetDatabase.LoadAssetAtPath(paths[2]); _counter = AssetDatabase.LoadAssetAtPath(paths[3]); } private static void EnsureFont() { DeleteAssetIfExists(FontAssetPath); var source = AssetDatabase.LoadAssetAtPath(FontPath); if (source == null) throw new InvalidOperationException("CJK source font missing: " + FontPath); _font = TMP_FontAsset.CreateFontAsset(source, 48, 9, GlyphRenderMode.SDFAA, 1024, 1024, AtlasPopulationMode.Dynamic, true); _font.name = "NotoSansSC SDF"; _font.atlasPopulationMode = AtlasPopulationMode.Dynamic; AssetDatabase.CreateAsset(_font, FontAssetPath); foreach (var atlas in _font.atlasTextures) { if (atlas == null || AssetDatabase.Contains(atlas)) continue; atlas.name = _font.name + " Atlas"; AssetDatabase.AddObjectToAsset(atlas, _font); } if (_font.material != null && !AssetDatabase.Contains(_font.material)) { _font.material.name = _font.name + " Material"; AssetDatabase.AddObjectToAsset(_font.material, _font); } EditorUtility.SetDirty(_font); AssetDatabase.SaveAssets(); } private static void BuildBattlePrefab() { var screen = Screen("BattleScreen"); try { var header = Panel(screen.transform, "Header", new Color(0.05f, 0.055f, 0.06f, 0.96f), new Vector2(0, .92f), Vector2.one); Button(header.transform, "BackButton", "‹", new Vector2(0, 0), new Vector2(.07f, 1), 32); Text(header.transform, "RoundText", "回合 1", 25, TextAlignmentOptions.Center, new Vector2(.08f, 0), new Vector2(.92f, 1)); var enemy = Panel(screen.transform, "EnemyArea", new Color(.11f, .12f, .12f, .94f), new Vector2(0, .75f), new Vector2(1, .92f)); Text(enemy.transform, "EnemyStatus", "贪婪 HP 30/30 COST 10/10", 24, TextAlignmentOptions.Left, new Vector2(.025f, .56f), new Vector2(.98f, .95f)); Text(enemy.transform, "EnemyQueue", "等待敌方锁定", 22, TextAlignmentOptions.Left, new Vector2(.025f, .08f), new Vector2(.98f, .54f)); Anchor(enemy.gameObject, "rp.matchup"); var effects = FullRect(screen.transform, "EffectArea", new Vector2(.03f, .43f), new Vector2(.97f, .74f)); var layout = effects.gameObject.AddComponent(); layout.spacing = 18; layout.childAlignment = TextAnchor.MiddleCenter; layout.childControlWidth = true; layout.childControlHeight = true; layout.childForceExpandWidth = true; for (var i = 1; i <= 3; i++) { var slot = Panel(effects, "Slot" + i, i == 2 ? new Color(.29f, .11f, .10f, .94f) : new Color(.11f, .14f, .14f, .94f), Vector2.zero, Vector2.one); Text(slot.transform, "Text", $"槽 {i}\n等待拼点", 21, TextAlignmentOptions.Center, Vector2.zero, Vector2.one, new Vector2(20, 16), new Vector2(-20, -16)); } var log = Panel(screen.transform, "LogPanel", new Color(.04f, .045f, .05f, .90f), new Vector2(.03f, .30f), new Vector2(.97f, .42f)); Text(log.transform, "LogText", "战斗记录", 15, TextAlignmentOptions.TopLeft, Vector2.zero, new Vector2(.48f, 1), new Vector2(16, 10), new Vector2(-12, -10)); Text(log.transform, "CardDetailTitle", "卡牌效果", 18, TextAlignmentOptions.TopLeft, new Vector2(.51f, .60f), new Vector2(.98f, .94f)); Text(log.transform, "CardDetailText", "悬停、点击或拖动卡牌查看完整效果。", 15, TextAlignmentOptions.TopLeft, new Vector2(.51f, .06f), new Vector2(.98f, .62f)); var player = Panel(screen.transform, "PlayerArea", new Color(.08f, .085f, .09f, .97f), new Vector2(0, 0), new Vector2(1, .29f)); Text(player.transform, "PlayerStatus", "被替代之人 HP 30/30 COST 10/10", 21, TextAlignmentOptions.Left, new Vector2(.02f, .82f), new Vector2(.5f, .99f)); Text(player.transform, "SelectionText", "普通牌:未选择\n结束牌:未选择", 15, TextAlignmentOptions.TopLeft, new Vector2(.51f, .73f), new Vector2(.98f, .99f)); var normalHand = FullRect(player.transform, "NormalHand", new Vector2(.02f, .30f), new Vector2(.72f, .81f)); var normalLayout = normalHand.gameObject.AddComponent(); normalLayout.spacing = 8; normalLayout.childControlWidth = false; normalLayout.childControlHeight = true; normalLayout.childForceExpandWidth = false; normalLayout.padding = new RectOffset(0, 0, 4, 4); CardTemplate(normalHand, "CardButtonTemplate"); Anchor(normalHand.gameObject, "rp.normal-hand"); var endHand = FullRect(player.transform, "EndHand", new Vector2(.73f, .30f), new Vector2(.98f, .81f)); var endLayout = endHand.gameObject.AddComponent(); endLayout.spacing = 8; endLayout.childControlWidth = false; endLayout.childControlHeight = true; endLayout.childForceExpandWidth = false; CardTemplate(endHand, "CardButtonTemplate"); Anchor(endHand.gameObject, "rp.end-hand"); var actions = FullRect(player.transform, "Actions", new Vector2(.02f, .03f), new Vector2(.98f, .28f)); var actionLayout = actions.gameObject.AddComponent(); actionLayout.spacing = 10; actionLayout.childControlHeight = true; actionLayout.childControlWidth = true; actionLayout.childForceExpandWidth = true; Button(actions, "Reroll1Button", "骰 1", Vector2.zero, Vector2.one, 17); Button(actions, "Reroll2Button", "骰 2", Vector2.zero, Vector2.one, 17); Button(actions, "Reroll3Button", "骰 3", Vector2.zero, Vector2.one, 17); Button(actions, "PassButton", "跳过", Vector2.zero, Vector2.one, 18); Button(actions, "SubmitButton", "锁定", Vector2.zero, Vector2.one, 19, new Color(.66f, .12f, .10f, 1)); Anchor(actions.gameObject, "rp.reroll"); PrefabUtility.SaveAsPrefabAsset(screen, BattlePrefabPath); } finally { UnityEngine.Object.DestroyImmediate(screen); } } private static void BuildModalPrefab() { var root = Screen("ModalOverlay"); try { root.AddComponent(); var scrim = Image(root.transform, "Scrim", null, new Color(0, 0, 0, .72f), Vector2.zero, Vector2.one); scrim.raycastTarget = true; var panel = Panel(root.transform, "Panel", new Color(.11f, .12f, .12f, 1), new Vector2(.3f, .32f), new Vector2(.7f, .68f)); Text(panel.transform, "Title", "提示", 30, TextAlignmentOptions.Center, new Vector2(.08f, .67f), new Vector2(.92f, .94f)); Text(panel.transform, "Message", "", 20, TextAlignmentOptions.Center, new Vector2(.08f, .28f), new Vector2(.92f, .68f)); Button(panel.transform, "CloseButton", "确定", new Vector2(.32f, .08f), new Vector2(.68f, .25f), 20); PrefabUtility.SaveAsPrefabAsset(root, ModalPrefabPath); } finally { UnityEngine.Object.DestroyImmediate(root); } } private static void BuildDemoPrefab() { var root = new GameObject("ReplacedPersonDemo", typeof(RectTransform), typeof(ReplacedPersonDemoController)); try { var canvasObject = new GameObject("Canvas", typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster)); canvasObject.transform.SetParent(root.transform, false); var canvas = canvasObject.GetComponent(); canvas.renderMode = RenderMode.ScreenSpaceOverlay; var scaler = canvasObject.GetComponent(); scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.referenceResolution = new Vector2(1920, 1080); scaler.matchWidthOrHeight = .5f; Stretch(canvasObject.GetComponent()); var background = Image(canvasObject.transform, "Background", _paper, new Color(.32f, .35f, .36f, 1), Vector2.zero, Vector2.one); background.preserveAspect = false; var safe = FullRect(canvasObject.transform, "SafeAreaRoot", Vector2.zero, Vector2.one); safe.gameObject.AddComponent(); var screens = FullRect(safe, "Screens", Vector2.zero, Vector2.one); BuildTitle(screens); BuildChapter(screens); BuildDeck(screens); BuildLan(screens); var battlePrefab = AssetDatabase.LoadAssetAtPath(BattlePrefabPath) ?? throw new InvalidOperationException("Battle prefab missing."); var battle = (GameObject)PrefabUtility.InstantiatePrefab(battlePrefab); battle.transform.SetParent(screens, false); Stretch(battle.GetComponent()); BuildReward(screens); BuildSettings(screens); PrefabUtility.SaveAsPrefabAsset(root, DemoPrefabPath); } finally { UnityEngine.Object.DestroyImmediate(root); } } private static void BuildTitle(Transform parent) { var screen = Screen("TitleScreen", parent); Text(screen.transform, "Title", "被替代之人", 70, TextAlignmentOptions.Center, new Vector2(.08f, .70f), new Vector2(.92f, .94f)); Text(screen.transform, "Subtitle", "第一章 · 贪婪", 24, TextAlignmentOptions.Center, new Vector2(.15f, .62f), new Vector2(.85f, .72f)); var art = FullRect(screen.transform, "ArtTriptych", new Vector2(.08f, .20f), new Vector2(.58f, .62f)); var layout = art.gameObject.AddComponent(); layout.spacing = 20; layout.childControlHeight = true; layout.childControlWidth = true; layout.childForceExpandWidth = true; Image(art, "Attack", _attack, Color.white, Vector2.zero, Vector2.one).preserveAspect = true; Image(art, "Dodge", _dodge, Color.white, Vector2.zero, Vector2.one).preserveAspect = true; Image(art, "Counter", _counter, Color.white, Vector2.zero, Vector2.one).preserveAspect = true; var actions = FullRect(screen.transform, "Actions", new Vector2(.64f, .20f), new Vector2(.91f, .61f)); var vertical = actions.gameObject.AddComponent(); vertical.spacing = 14; vertical.childControlWidth = true; vertical.childControlHeight = true; vertical.childForceExpandHeight = true; Button(actions, "ContinueButton", "进入章节", Vector2.zero, Vector2.one, 25, new Color(.67f, .13f, .11f, 1)); Button(actions, "DeckButton", "牌组", Vector2.zero, Vector2.one, 22); Button(actions, "LanButton", "局域网", Vector2.zero, Vector2.one, 22); Button(actions, "SettingsButton", "设置", Vector2.zero, Vector2.one, 22); Text(screen.transform, "Footer", "DEMO / WINDOWS PC / v1", 14, TextAlignmentOptions.BottomLeft, new Vector2(.03f, .02f), new Vector2(.7f, .09f)); } private static void BuildChapter(Transform parent) { var screen = Screen("ChapterScreen", parent); Header(screen.transform, "章节"); var content = FullRect(screen.transform, "Content", new Vector2(.10f, .17f), new Vector2(.90f, .88f)); Text(content, "ChapterLabel", "第一章 · 贪婪", 42, TextAlignmentOptions.TopLeft, new Vector2(0, .77f), Vector2.one); Text(content, "StoryText", "", 26, TextAlignmentOptions.TopLeft, Vector2.zero, new Vector2(1, .76f)); var footer = FullRect(screen.transform, "Footer", new Vector2(.66f, .05f), new Vector2(.90f, .14f)); Button(footer, "StartBattleButton", "进入第一战", Vector2.zero, Vector2.one, 24, new Color(.67f, .13f, .11f, 1)); } private static void BuildDeck(Transform parent) { var screen = Screen("DeckScreen", parent); Header(screen.transform, "牌组编辑"); var content = FullRect(screen.transform, "Content", new Vector2(.04f, .15f), new Vector2(.96f, .88f)); Text(content, "DeckStatus", "普通牌 12/12", 22, TextAlignmentOptions.Left, new Vector2(0, .91f), Vector2.one); ScrollList(content, "CurrentDeck", "当前牌组 · 点击移除", new Vector2(0, 0), new Vector2(.48f, .89f)); ScrollList(content, "Collection", "收藏 · 点击加入", new Vector2(.52f, 0), new Vector2(1, .89f)); var footer = FullRect(screen.transform, "Footer", new Vector2(.62f, .04f), new Vector2(.96f, .12f)); var layout = footer.gameObject.AddComponent(); layout.spacing = 12; layout.childControlHeight = true; layout.childControlWidth = true; Button(footer, "ResetButton", "重置", Vector2.zero, Vector2.one, 20); Button(footer, "SaveButton", "保存", Vector2.zero, Vector2.one, 20, new Color(.67f, .13f, .11f, 1)); } private static void BuildLan(Transform parent) { var screen = Screen("LanScreen", parent); Header(screen.transform, "局域网大厅"); var content = FullRect(screen.transform, "Content", new Vector2(.18f, .16f), new Vector2(.82f, .84f)); Text(content, "StatusText", "尚未连接", 24, TextAlignmentOptions.Center, new Vector2(.05f, .68f), new Vector2(.95f, .92f)); Input(content, "AddressInput", "127.0.0.1", new Vector2(.18f, .49f), new Vector2(.82f, .62f)); Button(content, "HostButton", "创建房间", new Vector2(.18f, .31f), new Vector2(.48f, .43f), 22); Button(content, "JoinButton", "直接加入", new Vector2(.52f, .31f), new Vector2(.82f, .43f), 22, new Color(.67f, .13f, .11f, 1)); Button(content, "StartMatchButton", "进入对局", new Vector2(.34f, .16f), new Vector2(.66f, .27f), 21, new Color(.67f, .13f, .11f, 1)); Text(content, "DiscoveryText", "局域网发现会自动广播可用房间", 17, TextAlignmentOptions.Center, new Vector2(.1f, .01f), new Vector2(.9f, .14f)); } private static void BuildReward(Transform parent) { var screen = Screen("RewardScreen", parent); Text(screen.transform, "Title", "结算", 28, TextAlignmentOptions.Center, new Vector2(.2f, .87f), new Vector2(.8f, .96f)); var content = FullRect(screen.transform, "Content", new Vector2(.18f, .22f), new Vector2(.82f, .84f)); Text(content, "ResultText", "胜利", 52, TextAlignmentOptions.Center, new Vector2(.1f, .75f), new Vector2(.9f, .94f)); Text(content, "RewardText", "", 21, TextAlignmentOptions.TopLeft, new Vector2(.05f, .30f), new Vector2(.48f, .73f)); Text(content, "AppearanceText", "当前装饰:无", 20, TextAlignmentOptions.Left, new Vector2(.52f, .61f), new Vector2(.98f, .72f)); var ornaments = FullRect(content, "Ornaments", new Vector2(.52f, .30f), new Vector2(.98f, .59f)); var ornamentLayout = ornaments.gameObject.AddComponent(); ornamentLayout.spacing = 8; ornamentLayout.childControlWidth = true; ornamentLayout.childControlHeight = true; ornamentLayout.childForceExpandHeight = true; Button(ornaments, "GoldEyeButton", "金色势利眼", Vector2.zero, Vector2.one, 17); Button(ornaments, "GoldChainButton", "金链", Vector2.zero, Vector2.one, 17); Button(ornaments, "WornSuitButton", "褶皱西装", Vector2.zero, Vector2.one, 17); var footer = FullRect(screen.transform, "Footer", new Vector2(.38f, .08f), new Vector2(.62f, .17f)); Button(footer, "ContinueButton", "继续", Vector2.zero, Vector2.one, 23, new Color(.67f, .13f, .11f, 1)); } private static void BuildSettings(Transform parent) { var screen = Screen("SettingsScreen", parent); Header(screen.transform, "设置"); var content = FullRect(screen.transform, "Content", new Vector2(.24f, .25f), new Vector2(.76f, .78f)); Text(content, "TransportStatus", "LAN 传输:TCP", 28, TextAlignmentOptions.Center, new Vector2(.1f, .68f), new Vector2(.9f, .91f)); Button(content, "TcpButton", "TCP", new Vector2(.12f, .42f), new Vector2(.48f, .61f), 23); Button(content, "KcpButton", "KCP", new Vector2(.52f, .42f), new Vector2(.88f, .61f), 23); Text(content, "Note", "1920×1080 基准 · 支持 16:10 与 4:3\n拖牌与点牌共用同一提交队列", 18, TextAlignmentOptions.Center, new Vector2(.06f, .09f), new Vector2(.94f, .34f)); } private static void Header(Transform screen, string title) { var header = Panel(screen, "Header", new Color(.05f, .055f, .06f, .94f), new Vector2(0, .90f), Vector2.one); Button(header.transform, "BackButton", "‹", new Vector2(0, 0), new Vector2(.08f, 1), 34); Text(header.transform, "Title", title, 30, TextAlignmentOptions.Left, new Vector2(.10f, 0), new Vector2(.9f, 1)); } private static void ScrollList(Transform parent, string name, string title, Vector2 min, Vector2 max) { var root = Panel(parent, name, new Color(.08f, .085f, .09f, .94f), min, max); Text(root.transform, "Title", title, 20, TextAlignmentOptions.Left, new Vector2(.04f, .90f), new Vector2(.96f, .99f)); var viewport = FullRect(root.transform, "Viewport", new Vector2(.03f, .03f), new Vector2(.97f, .88f)); viewport.gameObject.AddComponent(); var content = FullRect(viewport, "Content", new Vector2(0, 1), Vector2.one); content.anchorMin = new Vector2(0, 1); content.anchorMax = new Vector2(1, 1); content.pivot = new Vector2(.5f, 1); content.sizeDelta = new Vector2(0, 0); var layout = content.gameObject.AddComponent(); layout.spacing = 6; layout.childControlWidth = true; layout.childControlHeight = true; layout.childForceExpandWidth = true; layout.childForceExpandHeight = false; layout.padding = new RectOffset(4, 4, 4, 4); var fitter = content.gameObject.AddComponent(); fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize; var row = Button(content, "RowTemplate", "卡牌", Vector2.zero, Vector2.one, 17); row.gameObject.AddComponent().preferredHeight = 54; row.gameObject.SetActive(false); var scroll = root.gameObject.AddComponent(); scroll.viewport = viewport; scroll.content = content; scroll.horizontal = false; scroll.movementType = ScrollRect.MovementType.Clamped; } private static void CardTemplate(Transform parent, string name) { var button = Button(parent, name, "卡牌", Vector2.zero, Vector2.one, 14, new Color(.90f, .87f, .78f, .98f)); button.gameObject.AddComponent(); var layout = button.gameObject.AddComponent(); layout.preferredWidth = 132; layout.minWidth = 116; var rect = button.GetComponent(); rect.sizeDelta = new Vector2(132, 132); button.gameObject.SetActive(false); } private static GameObject Screen(string name, Transform? parent = null) { var result = new GameObject(name, typeof(RectTransform)); if (parent != null) result.transform.SetParent(parent, false); Stretch(result.GetComponent()); return result; } private static RectTransform FullRect(Transform parent, string name, Vector2 min, Vector2 max) { var result = new GameObject(name, typeof(RectTransform)).GetComponent(); result.SetParent(parent, false); result.anchorMin = min; result.anchorMax = max; result.offsetMin = Vector2.zero; result.offsetMax = Vector2.zero; return result; } private static Image Panel(Transform parent, string name, Color color, Vector2 min, Vector2 max) => Image(parent, name, null, color, min, max); private static Image Image(Transform parent, string name, Sprite? sprite, Color color, Vector2 min, Vector2 max) { var rect = FullRect(parent, name, min, max); var image = rect.gameObject.AddComponent(); image.sprite = sprite; image.color = color; image.raycastTarget = false; return image; } private static TMP_Text Text(Transform parent, string name, string value, float size, TextAlignmentOptions alignment, Vector2 min, Vector2 max, Vector2? offsetMin = null, Vector2? offsetMax = null) { var rect = FullRect(parent, name, min, max); rect.offsetMin = offsetMin ?? Vector2.zero; rect.offsetMax = offsetMax ?? Vector2.zero; var text = rect.gameObject.AddComponent(); text.font = _font; text.text = value; text.fontSize = size; text.alignment = alignment; text.color = new Color(.92f, .91f, .86f, 1); text.enableWordWrapping = true; text.overflowMode = TextOverflowModes.Ellipsis; text.raycastTarget = false; return text; } private static Button Button(Transform parent, string name, string label, Vector2 min, Vector2 max, float size, Color? color = null) { var image = Image(parent, name, null, color ?? new Color(.18f, .19f, .19f, .98f), min, max); image.raycastTarget = true; var button = image.gameObject.AddComponent