ThreeKingdomsSlingshot/Assets/Tests/PlayMode/DomesticSceneTests.cs

282 lines
14 KiB
C#

using System.Collections;
using System.Collections.Generic;
using NUnit.Framework;
using TKS.OutGame;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.TestTools;
using UnityEngine.UI;
namespace TKS.Tests
{
/// <summary>
/// 내정 메뉴 개편(PD 2026-11-12) — ① 탭마다 배경 장면(표 domestic_scene.csv view 줄 · 연출 = Frame 캔버스) ② 훈련 = 표 slot 줄 수만큼 병사 · 동작이 Frame 캔버스 안 · 구령 단계가 돈다
/// ③ 순찰 = 「시간당 수확」은 닫혀 있다가 누르면 펼침 · 버튼은 「수확」 「즉시 수확」 2개만(광고 버튼 없음) · 수확 → 팝업에서 광고 2배 고름 ④ 개발 = 두루마리 위 노드 전부.
/// 기대 수(병사 자리)는 표에서 읽는다(C45).
/// </summary>
public class DomesticSceneTests
{
OutGameRoot og;
[TearDown]
public void TearDown()
{
SaveStore.UseMemory(false);
}
IEnumerator Boot()
{
WorldProgress.Reset();
SaveStore.UseMemory(true);
SceneManager.LoadScene("Battle");
yield return null;
yield return null;
og = OutGameRoot.Instance;
Assert.IsNotNull(og);
og.OnTab(2);
Assert.AreEqual(og.Domestic, og.Current);
}
static void AssertFrame(Transform t, string what)
{
var c = t.GetComponentInParent<Canvas>(true);
Assert.IsNotNull(c, what);
var tag = c.GetComponent<UiLayerInfo>();
Assert.IsTrue(tag != null && tag.rate == UiRate.Frame, what + ": 매 프레임 요소가 Frame 캔버스 밖(" + c.name + ")");
}
static List<string> ButtonNames(Transform root)
{
var l = new List<string>();
foreach (var b in root.GetComponentsInChildren<Button>(false)) l.Add(b.name);
return l;
}
[UnityTest]
public IEnumerator Training_SceneSoldiersDrillInFrameCanvas()
{
yield return Boot();
var dom = og.Domestic;
dom.SetPage(0);
yield return null;
var rows = DomesticScene.Page("train");
var view = DomesticScene.First(rows, "view");
Assert.IsNotNull(view, "표 train_view 줄 없음");
if (UISkin.Icon(view.sprite) == null) Assert.Ignore("테마에 연병장 배경 없음(" + view.sprite + ")");
var amb = dom.SceneOf(0);
Assert.IsNotNull(amb, "훈련 배경 장면 층 없음");
AssertFrame(amb.transform, "훈련 장면 층");
Assert.AreEqual(DomesticScene.All(rows, "slot").Count, dom.SoldierCount, "병사 수 = 표 train_slot 줄 수");
var drills = amb.GetComponentsInChildren<UiDrillAnim>(true);
Assert.AreEqual(dom.SoldierCount, drills.Length, "병사마다 훈련 동작");
foreach (var d in drills) AssertFrame(d.transform, "병사 " + d.name);
// 풀·꽃 흔들림(PD 2026-11-12) = 표 train_sway 줄 수 · 아래끝 고정 · Frame 캔버스
int sway = 0;
foreach (var w in amb.GetComponentsInChildren<UiWaveImage>(true))
if (w.fromBottom)
{
sway++;
AssertFrame(w.transform, "풀 흔들림 " + w.name);
}
Assert.AreEqual(DomesticScene.All(rows, "sway").Count, sway, "풀·꽃 흔들림 수 = 표");
// 구령 1회(대기 → 자세 → 모으기 → 타격 → 유지)가 실제로 돈다
var drill = DomesticScene.First(rows, "drill");
float cycle = drill.rest + drill.stand + drill.ready + drill.hit + drill.hold;
var seen = new HashSet<int>();
float t0 = Time.unscaledTime;
while (Time.unscaledTime - t0 < cycle + 0.5f)
{
seen.Add(drills[0].Stage);
yield return null;
}
Assert.IsTrue(seen.Contains(0) && seen.Contains(1) && seen.Contains(4), "동작 단계 " + string.Join(",", new List<int>(seen).ConvertAll(x => x.ToString()).ToArray()));
// 병종 탭 이름 = 게임 공통 이름(PD 2026-11-12 「보병, 창병, 기병, 궁병, 승병」 · 승병 = 게임 전체)
var types = dom.TrainTypes;
for (int i = 0; i < types.Count; i++)
{
var tab = FindActive(dom.Rt, "Type" + i);
Assert.IsNotNull(tab, "병종 탭 " + i);
Assert.AreEqual(UnitTypeInfo.Korean(types[i]), tab.GetComponentInChildren<Text>().text, "탭 글자 = 병종 이름");
}
Assert.AreEqual("승병", UnitTypeInfo.Korean(UnitType.Strategist), "책사 → 승병(게임 전체)");
// 병종을 바꾸면 그 병종 병사로(수 그대로)
dom.SetTrainType(UnitType.Cavalry);
yield return null;
Assert.AreEqual(DomesticScene.All(rows, "slot").Count, dom.SoldierCount, "기병 병사 수");
// 궁병 = 훈련 전용 그림 + 타격 때 화살(표 train_shot · PD 2026-11-12 「궁병 다시 제작」)
dom.SetTrainType(UnitType.Archer);
yield return null;
var archers = amb.GetComponentsInChildren<UiDrillAnim>(true);
Assert.AreEqual(dom.SoldierCount, archers.Length, "궁병 병사마다 훈련 동작");
bool hasShot = false;
foreach (var r in DomesticScene.All(rows, "shot")) hasShot |= UISkin.Icon(r.Sprites.Length > 1 ? r.Sprites[1] : "") != null;
if (hasShot)
{
bool flew = false;
float t1 = Time.unscaledTime;
while (Time.unscaledTime - t1 < cycle + 0.5f && !flew)
{
foreach (var a in archers) flew |= a.ShotFlying;
yield return null;
}
Assert.IsTrue(flew, "궁병 타격 때 화살이 날지 않음");
}
// 다른 탭으로 가면 훈련 장면은 숨김
dom.SetPage(1);
yield return null;
Assert.IsFalse(amb.gameObject.activeInHierarchy, "순찰 탭에서 연병장 장면이 보임");
Assert.IsNotNull(dom.SceneOf(1), "순찰 배경 장면 층 없음");
}
[UnityTest]
public IEnumerator Patrol_InfoPanel_TwoButtons_AdChoiceAfterHarvest()
{
yield return Boot();
var s = og.Session;
s.Save.patrol.startAt = SaveStore.Iso(s.Now.AddHours(-3));
var dom = og.Domestic;
dom.SetPage(1);
yield return null;
var page = dom.Rt;
// 정보 판(PD 2026-11-12 예시) = 드롭다운 없음 · 시간당 수확 3줄(골드 · 병사 경험 · 전리품) 늘 보임 · 시간 앞 시계 · 프레임 안 보상 칸 4
Assert.IsNull(FindActive(page, "RateHead"), "드롭다운 머리가 남아 있음");
Assert.IsNull(FindActive(page, "RateList"), "드롭다운 목록이 남아 있음");
var info = FindActive(page, "RateInfo");
Assert.IsNotNull(info, "시간당 수확 줄 없음");
Assert.AreEqual(3, info.childCount, "시간당 수확 = 골드 · 경험치 · 전리품 3줄");
// 용어 = 골드 · 경험치 · 전리품 · 값 = 「획득량 / 시간」만 오른쪽 정렬(PD 2026-11-12)
string[] terms = { "골드", "경험치", "전리품" };
for (int i = 0; i < 3; i++)
{
var rowT = info.GetChild(i);
Assert.AreEqual(terms[i], rowT.GetComponentInChildren<Text>().text, "줄 " + i + " 용어");
var v = rowT.Find("PerHour").GetComponent<Text>();
Assert.AreEqual(TextAnchor.MiddleRight, v.alignment, "획득량 / 시간 = 오른쪽 정렬");
StringAssert.EndsWith("/ 시간</size>", v.text, "값 = 획득량 / 시간만");
StringAssert.DoesNotContain("출전", v.text);
}
if (UISkin.Icon("clock") != null) Assert.IsNotNull(FindActive(page, "Clock"), "시간 앞 시계 없음");
var frame = FindActive(page, "RewardFrame");
if (UISkin.Icon("dom_reward_frame") != null) Assert.IsNotNull(frame, "보상 프레임 없음");
for (int i = 0; i < 4; i++) Assert.IsNotNull(FindActive(page, "Tile" + i), "보상 칸 " + i);
var names = ButtonNames(page);
Assert.Contains("Harvest", names);
Assert.Contains("Instant", names);
Assert.IsFalse(names.Contains("Ad"), "순찰 화면에 광고 버튼이 남아 있음(PD 「수확과 즉시 수확만」)");
// 수확 → 팝업(수확 · 광고 2배 고름)
int gold0 = s.Save.gold;
FindActive(page, "Harvest").GetComponent<Button>().onClick.Invoke();
yield return null;
Assert.IsTrue(og.ModalVisible, "수확을 누르면 고르는 팝업");
var modalNames = ButtonNames(og.TopRoot);
Assert.Contains("Take", modalNames);
Assert.Contains("Ad", modalNames);
Assert.AreEqual(gold0, s.Save.gold, "팝업만 열렸는데 이미 받음");
FindActive(og.TopRoot, "Take").GetComponent<Button>().onClick.Invoke();
yield return null;
Assert.Greater(s.Save.gold, gold0, "수확 = 골드 받음");
og.CloseModal();
}
[UnityTest]
public IEnumerator Dev_TreeOnScroll()
{
yield return Boot();
var dom = og.Domestic;
dom.SetPage(2);
yield return null;
Assert.IsNotNull(dom.SceneOf(2), "개발 배경 장면 층 없음");
Assert.IsNotNull(FindActive(dom.Rt, "Scroll"), "두루마리 없음");
Assert.AreEqual(og.Session.Db.Dev.InBranch(dom.Branch).Count, dom.NodeCount, "노드 전부");
// 노드 그림 아이콘(테마 dev_<node_id> · PD 2026-11-12) — 테마에 있으면 모든 노드가 그림
if (UISkin.Icon("dev_" + og.Session.Db.Dev.InBranch(dom.Branch)[0].id) != null)
Assert.AreEqual(dom.NodeCount, dom.IconNodeCount, "노드마다 그림 아이콘");
Assert.IsNotNull(FindActive(FindActive(dom.Rt, "Scroll"), "Paper"), "노드는 두루마리 종이 안");
}
/// <summary>
/// 영웅 파견(PD 2026-11-12) — 훈련 지휘관(그 병종만) · 순찰(문 앞 + 창병 2 + NPC) · 개발(왼쪽 아래) · 한 영웅 = 한 자리(옮기면 앞 자리 비움) · 해제.
/// 창병·NPC 수 = 표 domestic_scene.csv patrol_guard · patrol_npc 줄 수(C45).
/// </summary>
[UnityTest]
public IEnumerator Dispatch_CommanderGateDev_OneSlotPerHero()
{
yield return Boot();
var s = og.Session;
var db = s.Db;
var d = s.Save;
var dom = og.Domestic;
HeroDb.Hero any = null;
foreach (var hs in d.heroes)
{
var h = db.Heroes.Get(hs.id);
if (h != null && !h.isSoldier)
{
any = h;
break;
}
}
if (any == null) Assert.Ignore("보유 영웅 없음");
string train = DispatchRules.TrainSlot(any.type);
// 훈련 = 그 병종 영웅만
foreach (UnitType t in System.Enum.GetValues(typeof(UnitType)))
if (t != any.type && DomesticScreenHasType(dom, t))
{
Assert.IsNotNull(DispatchRules.Assign(db, d, DispatchRules.TrainSlot(t), any.id), "다른 병종 지휘관이 됨");
break;
}
Assert.IsNull(s.Do(() => DispatchRules.Assign(db, d, train, any.id)));
dom.SetTrainType(any.type);
dom.SetPage(0);
yield return null;
Assert.IsTrue(dom.CommanderShown, "훈련 장면에 지휘관 없음");
// 순찰로 옮기면 훈련 자리는 빔
Assert.IsNull(s.Do(() => DispatchRules.Assign(db, d, DispatchRules.Patrol, any.id)));
Assert.AreEqual("", DispatchRules.Get(d, train), "옮긴 뒤 앞 자리가 남음");
dom.SetPage(1);
yield return null;
var rows = DomesticScene.Page("patrol");
Assert.IsTrue(dom.PatrolHeroShown, "문 앞 순찰 영웅 없음");
Assert.AreEqual(DomesticScene.All(rows, "guard").Count, dom.GuardCount, "문 앞 창병 수 = 표");
Assert.AreEqual(DomesticScene.All(rows, "npc").Count, dom.NpcCount, "상인·주민 NPC 수 = 표");
int npcAnim = 0;
foreach (var fa in dom.SceneOf(1).GetComponentsInChildren<UiFrameAnim>(true))
if (fa.name.StartsWith("Npc_")) npcAnim++;
Assert.AreEqual(dom.NpcCount, npcAnim, "NPC 마다 대기 숨 고르기(유닛과 같은 R22 클립)");
foreach (var fa in dom.SceneOf(1).GetComponentsInChildren<UiFrameAnim>(true))
{
var c = fa.GetComponentInParent<Canvas>(true).GetComponent<UiLayerInfo>();
Assert.IsTrue(c != null && c.rate == UiRate.Frame, "순찰 대기 숨 고르기가 Frame 캔버스 밖");
}
// 고르기 창 = 보유 영웅 후보
dom.OpenDispatch(DispatchRules.Patrol);
yield return null;
Assert.IsTrue(og.ModalVisible, "파견 고르기 창");
Assert.AreEqual(DispatchRules.Candidates(db, d, DispatchRules.Patrol).Count, dom.DispatchChoices);
og.CloseModal();
// 개발로 옮김 → 개발 탭 왼쪽 아래 영웅 · 순찰은 빔
Assert.IsNull(s.Do(() => DispatchRules.Assign(db, d, DispatchRules.Dev, any.id)));
Assert.AreEqual("", DispatchRules.Get(d, DispatchRules.Patrol));
dom.SetPage(2);
yield return null;
Assert.IsTrue(dom.DevHeroShown, "개발 배치 영웅 없음");
s.Do(() => DispatchRules.Clear(d, DispatchRules.Dev));
yield return null;
Assert.IsFalse(dom.DevHeroShown, "해제 뒤에도 영웅이 남음");
}
static bool DomesticScreenHasType(DomesticScreen dom, UnitType t)
{
return dom.TrainTypes.Contains(t);
}
static Transform FindActive(Transform root, string name)
{
foreach (var t in root.GetComponentsInChildren<Transform>(false))
if (t.name == name)
return t;
return null;
}
}
}