Files
cneicy d74c2f08ca feat(packages): 内置 SDK 包并完善 ContextLoader 集成
- 将 ShrinkEventBus、ShrinkDataSaver 及其 EventBus 集成从 gitlink 转为仓库直接维护的完整 UPM 包,补齐运行时、编辑器工具、测试与文档
- 新增 Command 和 Network 的 App 集成组件,支持 ContextLoader 服务发布、可逆注销及 Network Loopback 生命周期管理
- 更新 Starter 与演示组合逻辑,缺失模块时可注册、已有兼容安装器时可覆盖,并补充宿主启动断言
- 升级内部包依赖与 Shared CodeGen 包定义,放宽 Integration.App 包的 Git 忽略规则
- 将独立服务器生成器改为基于已编译程序集的语义扫描,支持 partial、复杂泛型、命名冲突检测及模板 SHA-256 覆写保护
- 新增 Network 语义扫描、模板保护和 App 组件生命周期测试
- 新增真实 UPM 消费工程验证脚本,校验内部版本一致性、程序集加载及 EditMode 测试
- 重构当前架构文档并归档已完成的 Cordis 迁移与旧代码地图
2026-08-18 18:06:34 +08:00

279 lines
15 KiB
C#

#nullable enable
using System.Collections;
using System.Linq;
using NUnit.Framework;
using ReplacedPerson.Runtime;
using ShrinkContext.AppAdapter;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.SceneManagement;
using UnityEngine.TestTools;
using UnityEngine.UI;
namespace ReplacedPerson.Tests
{
public sealed class ReplacedPersonSceneTests
{
[UnityTest]
public IEnumerator EntryScene_HasResponsiveCanvasAndAllScreens()
{
SceneManager.LoadScene("ReplacedPerson");
yield return null;
var root = GameObject.Find("ReplacedPersonDemo");
Assert.That(root, Is.Not.Null);
for (var frame = 0; frame < 30 &&
(ShrinkAppLoaderBootstrapper.Instance == null || !ShrinkAppLoaderBootstrapper.Instance.Host.IsRunning);
frame++)
yield return null;
var appHost = ShrinkAppLoaderBootstrapper.Instance;
Assert.That(appHost, Is.Not.Null);
Assert.That(appHost!.Host.IsRunning, Is.True);
Assert.That(appHost.Host.ActiveModuleIds, Does.Contain("demo.replaced-person"));
Assert.That(appHost.Host.Services.TryGet<ReplacedPersonGameService>(out var appService), Is.True);
Assert.That(appService, Is.Not.Null);
var camera = Camera.main;
Assert.That(camera, Is.Not.Null);
Assert.That(camera!.enabled, Is.True);
Assert.That(camera.cullingMask, Is.Zero);
var canvas = root.transform.Find("Canvas")!.GetComponent<Canvas>();
var scaler = canvas.GetComponent<CanvasScaler>();
Assert.That(canvas.renderMode, Is.EqualTo(RenderMode.ScreenSpaceOverlay));
Assert.That(scaler.referenceResolution, Is.EqualTo(new Vector2(1920, 1080)));
Assert.That(root.transform.Find("Canvas/SafeAreaRoot")!.GetComponent<ReplacedSafeArea>(), Is.Not.Null);
Assert.That(root.GetComponentsInChildren<TMP_Text>(true), Has.Length.GreaterThan(20));
Assert.That(root.GetComponentsInChildren<Button>(true), Has.Length.GreaterThan(12));
Assert.That(root.transform.Find("Canvas/SafeAreaRoot/Screens/BattleScreen/LogPanel/CardDetailText"), Is.Not.Null);
foreach (var audioName in new[] { "ui_click", "card_draw", "card_select", "lock", "dice_roll", "hit", "victory", "defeat", "error" })
Assert.That(Resources.Load<AudioClip>("ReplacedPersonAudio/" + audioName), Is.Not.Null, audioName);
}
[UnityTest]
public IEnumerator GameService_CompletesARealRoundWithAi()
{
var service = new ReplacedPersonGameService();
var match = service.StartAi("greed.tutorial", 123);
while ((match.State.Phase == Core.ReplacedMatchPhase.AwaitingLead ? match.State.LeadPlayer : 1 - match.State.LeadPlayer) != 0)
yield return null;
var player = match.State.Players[0];
var submission = new Core.ReplacedTurnSubmission
{
NormalCardIds = new System.Collections.Generic.List<string> { player.NormalHand[0] },
EndCardIds = new System.Collections.Generic.List<string> { player.EndHand[0] },
RerollSlot = 0
};
Assert.That(service.SubmitPlayer(submission).Accepted, Is.True);
Assert.That(match.State.Round, Is.GreaterThanOrEqualTo(2));
service.Dispose();
yield return null;
}
[UnityTest]
public IEnumerator DraggingCard_RestoresItsHandOrderAndLayoutPosition()
{
var canvasObject = new GameObject("DragTestCanvas", typeof(RectTransform), typeof(Canvas));
canvasObject.GetComponent<Canvas>().renderMode = RenderMode.ScreenSpaceOverlay;
var hand = new GameObject("Hand", typeof(RectTransform), typeof(HorizontalLayoutGroup));
hand.transform.SetParent(canvasObject.transform, false);
var handRect = hand.GetComponent<RectTransform>();
handRect.sizeDelta = new Vector2(480, 160);
var layout = hand.GetComponent<HorizontalLayoutGroup>();
layout.spacing = 8;
layout.childControlWidth = false;
layout.childControlHeight = false;
layout.childForceExpandWidth = false;
layout.childForceExpandHeight = false;
CreateDragCard(hand.transform, "CardA");
var dragged = CreateDragCard(hand.transform, "CardB");
CreateDragCard(hand.transform, "CardC");
Canvas.ForceUpdateCanvases();
LayoutRebuilder.ForceRebuildLayoutImmediate(handRect);
var originalIndex = dragged.transform.GetSiblingIndex();
var originalPosition = dragged.GetComponent<RectTransform>().anchoredPosition;
var clicked = false;
dragged.GetComponent<Button>().onClick.AddListener(() => clicked = true);
var eventSystem = EventSystem.current;
var ownsEventSystem = eventSystem == null;
if (eventSystem == null)
eventSystem = new GameObject("DragTestEventSystem", typeof(EventSystem)).GetComponent<EventSystem>();
var pointer = new PointerEventData(eventSystem) { position = new Vector2(540, 360), delta = new Vector2(80, 30) };
var handler = dragged.GetComponent<ReplacedCardDragHandler>();
handler.OnBeginDrag(pointer);
handler.OnDrag(pointer);
Assert.That(dragged.transform.GetSiblingIndex(), Is.EqualTo(originalIndex));
Assert.That(dragged.GetComponent<RectTransform>().anchoredPosition, Is.EqualTo(originalPosition));
Assert.That(GameObject.Find("CardDragGhost"), Is.Not.Null);
handler.OnEndDrag(pointer);
Assert.That(dragged.transform.GetSiblingIndex(), Is.EqualTo(originalIndex));
Assert.That(dragged.GetComponent<RectTransform>().anchoredPosition, Is.EqualTo(originalPosition));
Assert.That(clicked, Is.True);
Object.Destroy(canvasObject);
if (ownsEventSystem) Object.Destroy(eventSystem.gameObject);
yield return null;
}
[UnityTest]
public IEnumerator DraggingConfiguredCard_ReportsDropWithoutInvokingClick()
{
var canvasObject = new GameObject("ConfiguredDragCanvas", typeof(RectTransform), typeof(Canvas));
canvasObject.GetComponent<Canvas>().renderMode = RenderMode.ScreenSpaceOverlay;
var dragged = CreateDragCard(canvasObject.transform, "ConfiguredCard");
var clicked = false;
var dropped = false;
var dropPosition = Vector2.zero;
dragged.GetComponent<Button>().onClick.AddListener(() => clicked = true);
dragged.GetComponent<ReplacedCardDragHandler>().Configure(position =>
{
dropped = true;
dropPosition = position;
});
var eventSystem = EventSystem.current;
var ownsEventSystem = eventSystem == null;
if (eventSystem == null)
eventSystem = new GameObject("ConfiguredDragEventSystem", typeof(EventSystem)).GetComponent<EventSystem>();
var start = new PointerEventData(eventSystem) { position = new Vector2(100, 100) };
var end = new PointerEventData(eventSystem) { position = new Vector2(240, 140), delta = new Vector2(140, 40) };
var handler = dragged.GetComponent<ReplacedCardDragHandler>();
handler.OnBeginDrag(start);
handler.OnDrag(end);
handler.OnEndDrag(end);
Assert.That(dropped, Is.True);
Assert.That(clicked, Is.False);
Assert.That(dropPosition, Is.EqualTo(end.position));
yield return null;
Assert.That(GameObject.Find("CardDragGhost"), Is.Null);
Object.Destroy(canvasObject);
if (ownsEventSystem) Object.Destroy(eventSystem.gameObject);
yield return null;
}
[UnityTest]
public IEnumerator BattleHand_DuplicateCopyCancelsAndDragChangesExecutionOrder()
{
SceneManager.LoadScene("ReplacedPerson");
yield return null;
var root = GameObject.Find("ReplacedPersonDemo");
Assert.That(root, Is.Not.Null);
var safe = root.transform.Find("Canvas/SafeAreaRoot")!;
safe.Find("Screens/TitleScreen/Actions/ContinueButton")!.GetComponent<Button>().onClick.Invoke();
safe.Find("Screens/ChapterScreen/Footer/StartBattleButton")!.GetComponent<Button>().onClick.Invoke();
var phaseMotion = safe.Find("Screens/BattleScreen")!.GetComponent<ReplacedBattlePhaseMotion>();
Assert.That(phaseMotion, Is.Not.Null);
Assert.That(phaseMotion!.IsVisible, Is.True);
Assert.That(phaseMotion.CurrentTitle, Is.EqualTo("先手骰"));
var match = ReplacedPersonGameService.Current!.Match!;
var player = match.State.Players[0];
player.NormalHand.Clear();
player.NormalHand.AddRange(new[] { "counter.instant", "counter.instant", "counter.stance", "attack.kill", "dodge.flash" });
player.EndHand.Clear();
player.EndHand.Add("end.inherit");
root.SendMessage("RefreshBattle", SendMessageOptions.DontRequireReceiver);
yield return null;
var selection = safe.Find("Screens/BattleScreen/PlayerArea/SelectionText")!.GetComponent<TMP_Text>();
Card(safe, 0).GetComponent<Button>().onClick.Invoke();
yield return null;
Assert.That(selection.text, Does.Contain("1. 一瞬千击"));
Assert.That(Card(safe, 0).Find("Label")!.GetComponent<TMP_Text>().text, Does.StartWith("[1] 一瞬千击"));
Assert.That(Card(safe, 1).Find("Label")!.GetComponent<TMP_Text>().text, Does.StartWith("一瞬千击"));
Card(safe, 0).GetComponent<Button>().onClick.Invoke();
yield return null;
Assert.That(selection.text, Does.Contain("普通牌执行:未选择"));
foreach (var handIndex in new[] { 0, 2, 3 })
{
Card(safe, handIndex).GetComponent<Button>().onClick.Invoke();
yield return null;
}
Assert.That(selection.text, Does.Contain("1. 一瞬千击 → 2. 架势 → 3. 杀"));
var source = Card(safe, 3);
var target = Card(safe, 0);
var sourceRect = (RectTransform)source;
var targetRect = (RectTransform)target;
var originalPosition = sourceRect.anchoredPosition;
var startPosition = RectTransformUtility.WorldToScreenPoint(null, sourceRect.TransformPoint(sourceRect.rect.center));
var targetCenter = RectTransformUtility.WorldToScreenPoint(null, targetRect.TransformPoint(targetRect.rect.center));
var endPosition = new Vector2(targetCenter.x - targetRect.rect.width * .25f, targetCenter.y);
var begin = new PointerEventData(EventSystem.current) { position = startPosition };
var end = new PointerEventData(EventSystem.current) { position = endPosition, delta = endPosition - startPosition };
var handler = source.GetComponent<ReplacedCardDragHandler>();
handler.OnBeginDrag(begin);
handler.OnDrag(end);
handler.OnEndDrag(end);
Assert.That(sourceRect.anchoredPosition, Is.EqualTo(originalPosition));
yield return null;
Assert.That(selection.text, Does.Contain("1. 杀 → 2. 一瞬千击 → 3. 架势"));
Assert.That(Card(safe, 3).Find("Label")!.GetComponent<TMP_Text>().text, Does.StartWith("[1] 杀"));
Assert.That(Card(safe, 0).Find("Label")!.GetComponent<TMP_Text>().text, Does.StartWith("[2] 一瞬千击"));
Assert.That(Card(safe, 2).Find("Label")!.GetComponent<TMP_Text>().text, Does.StartWith("[3] 架势"));
Assert.That(GameObject.Find("CardDragGhost"), Is.Null);
Assert.That(match.State.Players[0].NormalHand.Count(id => id == "counter.instant"), Is.EqualTo(2));
}
[UnityTest]
public IEnumerator SlotResolutionMotion_PresentsClashDiceAndFinalResult()
{
var slot = new GameObject("AnimatedSlot", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
var textObject = new GameObject("Text", typeof(RectTransform), typeof(CanvasRenderer), typeof(TextMeshProUGUI));
textObject.transform.SetParent(slot.transform, false);
var textRect = textObject.GetComponent<RectTransform>();
textRect.anchorMin = Vector2.zero;
textRect.anchorMax = Vector2.one;
var motion = slot.AddComponent<ReplacedSlotResolutionMotion>();
motion.Play(new Core.ReplacedSlotResolution
{
SlotIndex = 0,
PlayerOneType = Core.ReplacedEffectType.Counter,
PlayerTwoType = Core.ReplacedEffectType.Attack,
PlayerOneRoll = 5,
PlayerTwoRoll = 2,
PlayerOneValue = 17,
PlayerTwoValue = 8,
DamageToPlayerTwo = 6,
Summary = "反击 6"
}, 0);
yield return new WaitForSecondsRealtime(.72f);
Assert.That(motion.IsPlaying, Is.True);
Assert.That(slot.transform.Find("PlayerToken")!.gameObject.activeSelf, Is.True);
Assert.That(textObject.GetComponent<TMP_Text>().text, Does.Contain("投掷中"));
Assert.That(slot.transform.Find("PlayerToken/Label")!.GetComponent<TMP_Text>().text, Does.Contain("D6"));
yield return new WaitForSecondsRealtime(1.9f);
Assert.That(motion.IsPlaying, Is.False);
Assert.That(textObject.GetComponent<TMP_Text>().text, Does.Contain("反击 6"));
Assert.That(slot.transform.Find("PlayerToken")!.gameObject.activeSelf, Is.False);
Object.Destroy(slot);
yield return null;
}
private static GameObject CreateDragCard(Transform parent, string name)
{
var card = new GameObject(name, typeof(RectTransform), typeof(CanvasRenderer), typeof(Image), typeof(Button),
typeof(LayoutElement), typeof(ReplacedCardDragHandler));
card.transform.SetParent(parent, false);
card.GetComponent<RectTransform>().sizeDelta = new Vector2(120, 140);
card.GetComponent<LayoutElement>().preferredWidth = 120;
return card;
}
private static Transform Card(Transform safeArea, int handIndex)
{
return safeArea.Find("Screens/BattleScreen/PlayerArea/NormalHand/Normal_" + handIndex)!;
}
}
}