using System.Collections; using NUnit.Framework; using TKS.OutGame; using UnityEngine; using UnityEngine.TestTools; using UnityEngine.UI; namespace TKS.Tests { /// /// 타이틀 화면(PD 2026-11-13 「장수가 모이는 연출로 시작 · 대기 움직임 · 꾸미기 가벼운 애니 · "Press Any Key" 깜박임 · 터치 → 다음」). /// ① 표대로 장수 6 · 프레임(쉼 + 대기 자세) · 로고 · 글자 ② 등장 = 화면 밖에서 제자리로 · 끝나면 글자 깜박임 ③ 누름 = 등장 중이면 건너뜀 · 글자가 뜬 뒤면 페이드 → 시작 함수 · 타이틀 닫힘 /// ④ 대기 프레임이 바뀜 ⑤ 층(P41) = 루트 Static · 연출·글자 Frame · 글자 층에 레이캐스터 없음 ⑥ 시작 흐름 훅 = 메모리 세이브(테스트·캡처)면 건너뜀(기존 테스트 = 바로 로비). /// public class TitleScreenTests { Camera cam; [SetUp] public void SetUp() { cam = new GameObject("TestCam").AddComponent(); cam.orthographic = true; TitleScreen.ForceShow = false; } [TearDown] public void TearDown() { TitleScreen.ForceShow = false; if (TitleScreen.Current != null) Object.Destroy(TitleScreen.Current.gameObject); foreach (var f in Object.FindObjectsByType(FindObjectsSortMode.None)) Object.Destroy(f.gameObject); if (cam != null) Object.Destroy(cam.gameObject); } [UnityTest] public IEnumerator Build_FollowsTable() { var rows = LoadingTable.Load(TitleScreen.TablePath); int heroRows = rows.FindAll(r => r.kind == "hero").Count; var ts = TitleScreen.Show(cam, null); yield return null; Assert.AreEqual(heroRows, ts.Heroes.Count, "표 hero 줄 수만큼 장수"); Assert.GreaterOrEqual(ts.Heroes.Count, 6); foreach (var h in ts.Heroes) Assert.GreaterOrEqual(h.frames.Length, 4, h.id + " = 쉼 + 대기 자세 3 이상"); Assert.IsNotNull(ts.Logo != null ? ts.Logo.sprite : null, "로고 그림"); Assert.IsNotNull(ts.PressText, "Press Any Key 글자"); Assert.AreEqual(rows.Find(r => r.kind == "press").text, ts.PressText.text); Assert.IsNotNull(ts.Ambient, "꾸미기 연출 층(LobbyAmbient)"); } [UnityTest] public IEnumerator Intro_GathersThenPressBlinks() { var ts = TitleScreen.Show(cam, null); ts.SetTime(0f); yield return null; var first = ts.Heroes[0]; Assert.Less(first.img.color.a, 0.01f, "시작 전 장수는 안 보임"); ts.SetTime(first.row.phase + first.row.fps * 0.3f); var target = TitleScreen.Target(first.row); Assert.Greater(Vector2.Distance(first.rt.anchoredPosition, target), 1f, "날아오는 중 = 제자리 아님"); Assert.IsFalse(ts.PressReady, "등장 중에는 글자 안 뜸"); ts.SkipIntro(); foreach (var h in ts.Heroes) { Assert.Less(Vector2.Distance(h.rt.anchoredPosition, TitleScreen.Target(h.row)), 0.01f, h.id + " 제자리"); Assert.AreEqual(0f, Mathf.Repeat(h.rt.anchoredPosition.x - h.row.w * 0.5f, 1f), 0.001f, h.id + " 왼쪽 모서리 정수 픽셀(QA r1 M6)"); Assert.IsTrue(h.landed, h.id + " 착지"); } Assert.IsTrue(ts.IntroDone); float until = Time.unscaledTime + 2f; while (!ts.PressReady && Time.unscaledTime < until) yield return null; Assert.IsTrue(ts.PressReady, "등장 끝 뒤 글자가 뜸"); float minA = 1f, maxA = 0f; for (float w = Time.unscaledTime + 1.6f; Time.unscaledTime < w;) { yield return null; minA = Mathf.Min(minA, ts.PressText.color.a); maxA = Mathf.Max(maxA, ts.PressText.color.a); } Assert.Greater(maxA - minA, 0.3f, "글자 깜박임(알파가 오르내림)"); } [UnityTest] public IEnumerator Press_SkipsIntroThenStarts() { bool started = false; var ts = TitleScreen.Show(cam, () => started = true); yield return null; ts.Press(); Assert.IsTrue(ts.IntroDone, "등장 중 누름 = 끝 모습으로"); Assert.IsFalse(ts.Leaving, "첫 누름은 건너뛰기만"); float until = Time.unscaledTime + 2f; while (!ts.PressReady && Time.unscaledTime < until) yield return null; ts.Press(); Assert.IsTrue(ts.Leaving); until = Time.unscaledTime + 3f; while (!started && Time.unscaledTime < until) yield return null; Assert.IsTrue(started, "페이드 뒤 시작 함수(로비)"); yield return null; Assert.IsNull(TitleScreen.Current, "타이틀 닫힘"); } [UnityTest] public IEnumerator Idle_FramesChange() { var ts = TitleScreen.Show(cam, null); ts.SkipIntro(); yield return null; var seen = new System.Collections.Generic.Dictionary>(); foreach (var h in ts.Heroes) seen[h.id] = new System.Collections.Generic.HashSet(); for (float w = Time.unscaledTime + 1.8f; Time.unscaledTime < w;) { yield return null; foreach (var h in ts.Heroes) seen[h.id].Add(h.img.sprite); } foreach (var h in ts.Heroes) Assert.GreaterOrEqual(seen[h.id].Count, 3, h.id + " 대기 자세가 바뀜"); } [UnityTest] public IEnumerator Layers_P41() { var ts = TitleScreen.Show(cam, null); yield return null; Assert.AreEqual(UiRate.Static, ts.RootCanvas.GetComponent().rate, "루트 = 배경만(Static)"); Assert.AreEqual(UiRate.Frame, ts.Ui.GetComponent().rate, "로고·글자 = Frame"); Assert.IsNull(ts.Ui.GetComponent(), "글자 층에 레이캐스터 없음(입력 = 폴링)"); var amb = ts.Ambient.GetComponent(); Assert.IsNotNull(amb); Assert.AreEqual(UiRate.Frame, amb.rate, "연출·장수 = Frame"); } [UnityTest] public IEnumerator Show_HidesCanvasesBelowThenRestores() { var below = new GameObject("BelowCanvas"); var bc = UiLayer.Root(below, cam, 100, 1f, UiLayer.Reference, UiRate.Static, false); var above = new GameObject("AboveCanvas"); var ac = UiLayer.Root(above, cam, 990, 0.2f, UiLayer.Reference, UiRate.Static, false); var ts = TitleScreen.Show(cam, null); yield return null; Assert.IsFalse(bc.enabled, "타이틀 아래 루트 캔버스 = 끔(QA r1 M5)"); Assert.IsTrue(ac.enabled, "타이틀 위(페이드 등) = 그대로"); ts.Close(); yield return null; Assert.IsTrue(bc.enabled, "닫으면 되살림"); Object.Destroy(below); Object.Destroy(above); } [UnityTest] public IEnumerator Stage_FitsInsideScreen() { var ts = TitleScreen.Show(cam, null); yield return null; var root = (RectTransform)ts.transform; var sz = root.rect.size; Assert.LessOrEqual(ts.Stage.localScale.x * TitleScreen.ArtW, sz.x + 0.5f, "무대 폭 ≤ 화면(contain · QA r1 B3·B4)"); Assert.LessOrEqual(ts.Stage.localScale.y * TitleScreen.ArtH, sz.y + 0.5f, "무대 높이 ≤ 화면(QA r1 B1·B2)"); Assert.GreaterOrEqual(ts.Art.localScale.x * TitleScreen.ArtW, sz.x - 0.5f, "배경 = 화면을 덮음(cover)"); Assert.GreaterOrEqual(ts.Art.localScale.y * TitleScreen.ArtH, sz.y - 0.5f); Assert.AreEqual(ts.Stage, ts.Logo.rectTransform.parent, "로고 = 무대 위(장수와 같은 배율)"); } [Test] public void Provider_SkipsOnMemorySave() { SaveStore.UseMemory(true); bool called = false; Assert.IsFalse(TitleScreen.Provide(cam, () => called = true), "메모리 세이브(테스트·캡처) = 타이틀 건너뜀 → BootFlow 가 로비"); Assert.IsNull(TitleScreen.Current); TitleScreen.ForceShow = true; Assert.IsTrue(TitleScreen.Provide(cam, () => called = true), "ForceShow = 띄움"); Assert.IsNotNull(TitleScreen.Current); Assert.IsFalse(called); } } }