This commit is contained in:
2026-08-17 18:18:13 +08:00
parent 5765f411e9
commit d2cd3b9fa8
188 changed files with 32929 additions and 2 deletions
@@ -0,0 +1,22 @@
{
"name": "ReplacedPerson.Editor",
"rootNamespace": "ReplacedPerson.Editor",
"references": [
"ReplacedPerson.Core",
"ReplacedPerson.Runtime",
"ShrinkApp.Core.Runtime",
"ShrinkContext.AppAdapter.Runtime",
"ShrinkModFramework.Runtime",
"ShrinkTutorial.Runtime",
"UnityEngine.UI",
"Unity.TextMeshPro"
],
"includePlatforms": ["Editor"],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 04e8103b42db45f49966a408dc47dda1
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,524 @@
#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); }
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ebf6b9d7b155d3547a01f5a0aa2b2154
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: