525 lines
34 KiB
C#
525 lines
34 KiB
C#
#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<ShrinkAppSettings>(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<ReplacedPersonAppSettings>();
|
||
appSettings.hostingMode = ShrinkAppHostingMode.ContextLoader;
|
||
appSettings.verboseLogging = false;
|
||
AssetDatabase.CreateAsset(appSettings, appSettingsPath);
|
||
}
|
||
|
||
const string modSettingsPath = ResourceRoot + "/ShrinkModFrameworkSettings.asset";
|
||
var modSettings = AssetDatabase.LoadAssetAtPath<ShrinkModFrameworkSettings>(modSettingsPath);
|
||
if (modSettings == null)
|
||
{
|
||
modSettings = ScriptableObject.CreateInstance<ShrinkModFrameworkSettings>();
|
||
modSettings.verboseLogging = false;
|
||
AssetDatabase.CreateAsset(modSettings, modSettingsPath);
|
||
}
|
||
|
||
const string profilePath = "Assets/Resources/ShrinkAppComposition.asset";
|
||
var profile = AssetDatabase.LoadAssetAtPath<ShrinkAppCompositionProfile>(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<Sprite>(paths[0]);
|
||
_attack = AssetDatabase.LoadAssetAtPath<Sprite>(paths[1]);
|
||
_dodge = AssetDatabase.LoadAssetAtPath<Sprite>(paths[2]);
|
||
_counter = AssetDatabase.LoadAssetAtPath<Sprite>(paths[3]);
|
||
}
|
||
|
||
private static void EnsureFont()
|
||
{
|
||
DeleteAssetIfExists(FontAssetPath);
|
||
var source = AssetDatabase.LoadAssetAtPath<Font>(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<HorizontalLayoutGroup>();
|
||
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<HorizontalLayoutGroup>();
|
||
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<HorizontalLayoutGroup>();
|
||
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<HorizontalLayoutGroup>();
|
||
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<CanvasGroup>();
|
||
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>(); canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||
var scaler = canvasObject.GetComponent<CanvasScaler>();
|
||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.referenceResolution = new Vector2(1920, 1080); scaler.matchWidthOrHeight = .5f;
|
||
Stretch(canvasObject.GetComponent<RectTransform>());
|
||
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<ReplacedSafeArea>();
|
||
var screens = FullRect(safe, "Screens", Vector2.zero, Vector2.one);
|
||
BuildTitle(screens);
|
||
BuildChapter(screens);
|
||
BuildDeck(screens);
|
||
BuildLan(screens);
|
||
var battlePrefab = AssetDatabase.LoadAssetAtPath<GameObject>(BattlePrefabPath) ?? throw new InvalidOperationException("Battle prefab missing.");
|
||
var battle = (GameObject)PrefabUtility.InstantiatePrefab(battlePrefab);
|
||
battle.transform.SetParent(screens, false); Stretch(battle.GetComponent<RectTransform>());
|
||
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<HorizontalLayoutGroup>(); 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<VerticalLayoutGroup>();
|
||
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<HorizontalLayoutGroup>(); 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<VerticalLayoutGroup>();
|
||
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<RectMask2D>();
|
||
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<VerticalLayoutGroup>();
|
||
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<ContentSizeFitter>(); fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||
var row = Button(content, "RowTemplate", "卡牌", Vector2.zero, Vector2.one, 17);
|
||
row.gameObject.AddComponent<LayoutElement>().preferredHeight = 54; row.gameObject.SetActive(false);
|
||
var scroll = root.gameObject.AddComponent<ScrollRect>(); 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<ReplacedCardDragHandler>();
|
||
var layout = button.gameObject.AddComponent<LayoutElement>(); layout.preferredWidth = 132; layout.minWidth = 116;
|
||
var rect = button.GetComponent<RectTransform>(); 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<RectTransform>());
|
||
return result;
|
||
}
|
||
|
||
private static RectTransform FullRect(Transform parent, string name, Vector2 min, Vector2 max)
|
||
{
|
||
var result = new GameObject(name, typeof(RectTransform)).GetComponent<RectTransform>();
|
||
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>(); 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<TextMeshProUGUI>(); 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<Button>(); button.targetGraphic = image;
|
||
Text(image.transform, "Label", label, size, TextAlignmentOptions.Center, Vector2.zero, Vector2.one, new Vector2(8, 4), new Vector2(-8, -4));
|
||
return button;
|
||
}
|
||
|
||
private static TMP_InputField Input(Transform parent, string name, string placeholder, Vector2 min, Vector2 max)
|
||
{
|
||
var background = Image(parent, name, null, new Color(.92f, .90f, .84f, 1), min, max); background.raycastTarget = true;
|
||
var area = FullRect(background.transform, "TextArea", new Vector2(.04f, .1f), new Vector2(.96f, .9f)); area.gameObject.AddComponent<RectMask2D>();
|
||
var text = Text(area, "Text", string.Empty, 22, TextAlignmentOptions.Left, Vector2.zero, Vector2.one); text.color = Color.black;
|
||
var hint = Text(area, "Placeholder", placeholder, 22, TextAlignmentOptions.Left, Vector2.zero, Vector2.one); hint.color = new Color(.2f, .2f, .2f, .45f);
|
||
var input = background.gameObject.AddComponent<TMP_InputField>(); input.textViewport = area; input.textComponent = (TMP_Text)text; input.placeholder = hint;
|
||
return input;
|
||
}
|
||
|
||
private static void Anchor(GameObject target, string id)
|
||
{
|
||
var anchor = target.AddComponent<ShrinkTutorialAnchor>(); anchor.SetAnchorId(id);
|
||
}
|
||
|
||
private static void Stretch(RectTransform rect)
|
||
{
|
||
rect.anchorMin = Vector2.zero; rect.anchorMax = Vector2.one; rect.offsetMin = Vector2.zero; rect.offsetMax = Vector2.zero;
|
||
}
|
||
|
||
private static void BuildTutorialAssets()
|
||
{
|
||
DeleteAssetIfExists(ResourceRoot + "/ReplacedPersonFirstMatchTutorial.asset");
|
||
DeleteAssetIfExists(ResourceRoot + "/ReplacedPersonTutorialDatabase.asset");
|
||
DeleteAssetIfExists(ResourceRoot + "/ShrinkTutorialSettings.asset");
|
||
DeleteAssetIfExists(ResourceRoot + "/ReplacedPersonTutorialSettings.asset");
|
||
var tutorial = ScriptableObject.CreateInstance<ShrinkTutorialData>();
|
||
tutorial.tutorialId = "replaced-person.first-match";
|
||
tutorial.steps = new List<ShrinkTutorialStep>
|
||
{
|
||
Step("rp.normal-hand", "rp.card-selected", "选择一张普通牌。牌面顺序会从左到右继承到结束牌。", ShrinkTutorialDialogAnchor.Top),
|
||
Step("rp.end-hand", "rp.end-selected", "选择结束牌。它提供三个效果槽,并锁定本次提交。", ShrinkTutorialDialogAnchor.Top),
|
||
Step("rp.matchup", "rp.matchup-submitted", "后手能看到先手牌序。对齐克制关系后再锁定。", ShrinkTutorialDialogAnchor.Bottom),
|
||
Step("rp.reroll", "rp.reroll-marked", "标记一个效果槽;初次骰点不高于 2 时会自动重掷一次。", ShrinkTutorialDialogAnchor.Top)
|
||
};
|
||
AssetDatabase.CreateAsset(tutorial, ResourceRoot + "/ReplacedPersonFirstMatchTutorial.asset");
|
||
var database = ScriptableObject.CreateInstance<ShrinkTutorialDatabase>();
|
||
var serialized = new SerializedObject(database); var tutorials = serialized.FindProperty("tutorials"); tutorials.arraySize = 1; tutorials.GetArrayElementAtIndex(0).objectReferenceValue = tutorial; serialized.ApplyModifiedPropertiesWithoutUndo();
|
||
AssetDatabase.CreateAsset(database, ResourceRoot + "/ReplacedPersonTutorialDatabase.asset");
|
||
var settings = ScriptableObject.CreateInstance<ShrinkTutorialSettings>(); settings.database = database; settings.playerPrefsStorageKey = "ReplacedPerson.Tutorial"; settings.showSkipButton = true; settings.canvasSortingOrder = 5000;
|
||
settings.name = "ReplacedPersonTutorialSettings";
|
||
AssetDatabase.CreateAsset(settings, ResourceRoot + "/ReplacedPersonTutorialSettings.asset");
|
||
}
|
||
|
||
private static ShrinkTutorialStep Step(string anchor, string signal, string text, ShrinkTutorialDialogAnchor dialogAnchor) => new()
|
||
{
|
||
targetMode = ShrinkTutorialTargetMode.AnchorId,
|
||
anchorId = anchor,
|
||
completeCondition = ShrinkTutorialCompleteCondition.CustomEvent,
|
||
customEventName = signal,
|
||
fallbackText = text,
|
||
dialogAnchor = dialogAnchor,
|
||
maskShape = ShrinkTutorialMaskShape.Rect,
|
||
maskPadding = 10,
|
||
waitForTarget = true,
|
||
waitTimeout = 3
|
||
};
|
||
|
||
private static void BuildScene()
|
||
{
|
||
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
||
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(DemoPrefabPath) ?? throw new InvalidOperationException("Demo prefab missing.");
|
||
var instance = (GameObject)PrefabUtility.InstantiatePrefab(prefab, scene); instance.name = "ReplacedPersonDemo";
|
||
var cameraObject = new GameObject("Main Camera", typeof(Camera), typeof(AudioListener));
|
||
cameraObject.tag = "MainCamera";
|
||
cameraObject.transform.position = new Vector3(0, 0, -10);
|
||
var camera = cameraObject.GetComponent<Camera>();
|
||
camera.clearFlags = CameraClearFlags.SolidColor;
|
||
camera.backgroundColor = new Color(.02f, .025f, .03f, 1);
|
||
camera.cullingMask = 0;
|
||
camera.orthographic = true;
|
||
camera.depth = -100;
|
||
new GameObject("EventSystem", typeof(EventSystem), typeof(StandaloneInputModule));
|
||
EditorSceneManager.SaveScene(scene, ScenePath);
|
||
}
|
||
|
||
private static void AddToBuildSettings()
|
||
{
|
||
var scenes = EditorBuildSettings.scenes.ToList();
|
||
var existing = scenes.FindIndex(value => value.path == ScenePath);
|
||
if (existing >= 0) scenes[existing] = new EditorBuildSettingsScene(ScenePath, true);
|
||
else scenes.Add(new EditorBuildSettingsScene(ScenePath, true));
|
||
EditorBuildSettings.scenes = scenes.ToArray();
|
||
}
|
||
|
||
private static void DeleteAssetIfExists(string path) { if (AssetDatabase.LoadMainAssetAtPath(path) != null) AssetDatabase.DeleteAsset(path); }
|
||
}
|
||
}
|