- 将 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 迁移与旧代码地图
279 lines
15 KiB
C#
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)!;
|
|
}
|
|
}
|
|
}
|