using System; using System.Collections; using System.Collections.Generic; using System.IO; using NUnit.Framework; using TKS.OutGame; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.TestTools; namespace TKS.Tests { /// /// 아웃게임 1차(2026-11-05 #1093 · 시스템 기획 §12) — 임포터(행 수·필수 열·id 유일·참조 무결) · 공식(표 값으로 기대치) · 군량 회복·오프라인 · /// 승급 6칸 · 진화 조각 · 뽑기 확률·천장·10연 보장 · 보상 공식·별 · 편성 → 스폰 · 세이브 왕복 · 로비 탭·뒤로 · 군량 입장·결과·소탕 · 상점. /// 기대값은 전부 표(CSV·자산)에서 계산한다(C45 · 테스트에 수치 상수 없음). /// public class OutGameTests { static readonly DateTime T0 = new DateTime(2026, 11, 5, 3, 0, 0, DateTimeKind.Utc); static string[] ReadTables() { var dir = Path.Combine(Application.dataPath, OutGameTables.Folder); var texts = new string[OutGameTables.Files.Length]; for (int i = 0; i < texts.Length; i++) texts[i] = File.ReadAllText(Path.Combine(dir, OutGameTables.Files[i]), System.Text.Encoding.UTF8); return texts; } static int CsvRows(string file) { int n = 0; foreach (var l in File.ReadAllLines(Path.Combine(Application.dataPath, OutGameTables.Folder, file), System.Text.Encoding.UTF8)) if (l.Trim().Length > 0) n++; return n - 1; } static OutGameData Db { get { return OutGameData.Default; } } static BalanceConfig Cfg { get { return Resources.Load("BalanceConfig"); } } static SaveData NewSave() { return HeroRules.NewSave(Db, Cfg.playerSpawnPoints.Length, T0); } // ---------- 임포터 ---------- [Test] public void Import_Tables_RowCounts_Columns_UniqueIds_References() { var r = OutGameTables.Parse(ReadTables()); try { Assert.AreEqual(0, r.errors.Count, string.Join("\n", r.errors.ToArray())); foreach (var f in OutGameTables.Files) Assert.AreEqual(CsvRows(f), r.rowCounts[f], f + " 행 수"); var d = r.data; Assert.AreEqual(CsvRows("heroes.csv"), d.Heroes.heroes.Count); Assert.AreEqual(CsvRows("skills.csv"), d.Skills.skills.Count); Assert.AreEqual(CsvRows("gear_items.csv"), d.Gear.items.Count); Assert.AreEqual(CsvRows("hero_levels.csv"), d.Levels.rows.Count); int gearRows = 0; foreach (var h in d.Heroes.heroes) { foreach (var sid in new[] { h.skillActive, h.skillPassive }) if (sid.Length > 0) Assert.AreEqual(h.id, d.Skills.Get(sid).heroId, h.id + " ↔ " + sid); foreach (var set in h.gear) foreach (var g in set.slots) { Assert.IsNotNull(d.Gear.Get(g), h.id + " " + set.grade + " " + g); gearRows++; } if (h.isSoldier) Assert.AreEqual(0, h.gear.Count, h.id + " 병사 = 승급 없음"); } Assert.AreEqual(CsvRows("hero_gear.csv"), gearRows, "hero_gear 행 = 영웅 × 등급 × 칸"); foreach (var p in d.Gacha.pool) Assert.IsNotNull(d.Heroes.Get(p.heroId), "뽑기 풀 " + p.heroId); foreach (var id in d.Economy.StartHeroes) Assert.IsNotNull(d.Heroes.Get(id), "시작 영웅 " + id); Assert.AreEqual(1, d.Promotion.Get(d.Promotion.FirstGrade).order, "초기 등급 = order 1"); } finally { OutGameTables.Destroy(r); } } [Test] public void Import_BrokenTable_ReportsErrors() { var texts = ReadTables(); int hi = Array.IndexOf(OutGameTables.Files, "heroes.csv"); var lines = new List(texts[hi].Split('\n')); lines.Insert(2, lines[1]); // 첫 영웅 행 중복 texts[hi] = string.Join("\n", lines.ToArray()); int si = Array.IndexOf(OutGameTables.Files, "skills.csv"); texts[si] = texts[si].Replace("cooldown", "cool_down"); // 필수 열 이름 틀림 int gi = Array.IndexOf(OutGameTables.Files, "hero_gear.csv"); int cut = texts[gi].IndexOf("gear_", texts[gi].IndexOf('\n') + 1, StringComparison.Ordinal); // 머리줄(gear_id) 다음 첫 데이터 행 texts[gi] = texts[gi].Substring(0, cut) + "gear_nope_" + texts[gi].Substring(cut + 5); // 없는 장비 참조 var r = OutGameTables.Parse(texts); var all = string.Join("\n", r.errors.ToArray()); OutGameTables.Destroy(r); StringAssert.Contains("중복", all); StringAssert.Contains("cooldown", all); StringAssert.Contains("gear_nope_", all); Assert.AreEqual(CsvRows("heroes.csv"), Db.Heroes.heroes.Count, "실패한 파싱은 자산을 건드리지 않음"); } [Test] public void Import_AssetsMatchCsv() { Assert.IsTrue(Db.Complete, "Resources/OutGame 9 자산(TKS/Import OutGame Data)"); var r = OutGameTables.Parse(ReadTables()); try { var p = r.data; Assert.AreEqual(p.Heroes.heroes.Count, Db.Heroes.heroes.Count); for (int i = 0; i < p.Heroes.heroes.Count; i++) { var a = p.Heroes.heroes[i]; var b = Db.Heroes.heroes[i]; Assert.AreEqual(a.id, b.id); Assert.AreEqual(a.type, b.type, a.id); Assert.AreEqual(a.hpMult, b.hpMult, 1e-6f, a.id); Assert.AreEqual(a.atkMult, b.atkMult, 1e-6f, a.id); Assert.AreEqual(a.defMult, b.defMult, 1e-6f, a.id); Assert.AreEqual(a.art, b.art, a.id); Assert.AreEqual(a.gear.Count, b.gear.Count, a.id); } Assert.AreEqual(p.Gear.items.Count, Db.Gear.items.Count); Assert.AreEqual(p.Skills.skills.Count, Db.Skills.skills.Count); Assert.AreEqual(p.Levels.rows.Count, Db.Levels.rows.Count); Assert.AreEqual(p.Rewards.rows.Count, Db.Rewards.rows.Count); Assert.AreEqual(p.Economy.values.entries.Count, Db.Economy.values.entries.Count); foreach (var e in p.Economy.values.entries) Assert.AreEqual(e.value, Db.Economy.values.S(e.key), e.key); foreach (var e in p.Gacha.values.entries) Assert.AreEqual(e.value, Db.Gacha.values.S(e.key), e.key); } finally { OutGameTables.Destroy(r); } } // ---------- 공식 · 성장 ---------- [Test] public void Stats_Formula_FromTables() { var cfg = Cfg; var h = Db.Heroes.Get(Db.Economy.StartHeroes[1]); string grade = Db.Promotion.Get(Db.Promotion.FirstGrade).nextGrade; var s = new HeroState { id = h.id, lv = 10, grade = grade, gearTotal = 6, star = 4 }; var st = HeroRules.Compute(Db, cfg, h, s); var b = cfg.GetStats(h.type); float g = (1f + Db.Levels.Get(10).statPct / 100f) * (1f + (Db.Promotion.Get(grade).promoPct + 6 * Db.Economy.GearBonusPct) / 100f) * (1f + Db.Evolve.Get(4).starPct / 100f); Assert.AreEqual(b.maxHp * h.hpMult * g, st.hp, 1e-3f); Assert.AreEqual(b.atk * h.atkMult * g, st.atk, 1e-3f); Assert.AreEqual(b.def * h.defMult * g, st.def, 1e-3f); var e = Db.Economy; Assert.AreEqual(Mathf.RoundToInt(st.hp / e.PowerHpDiv + st.atk * e.PowerAtkW + st.def * e.PowerDefW), st.power, "전투력 = hp/10 + 공×3 + 방×2"); Assert.AreEqual(0f, st.trainPct, "훈련 = C 단계"); HeroDb.Hero soldier = null; foreach (var x in Db.Heroes.heroes) if (x.isSoldier) { soldier = x; break; } var ss = HeroRules.Compute(Db, cfg, soldier, new UnitState { id = soldier.id, lv = 1 }); Assert.AreEqual(0f, ss.promoPct + ss.gearPct + ss.starPct, "병사 = 승급·장비·진화 없음"); Assert.AreEqual(cfg.GetStats(soldier.type).maxHp * soldier.hpMult * (1f + Db.Levels.Get(1).statPct / 100f), ss.hp, 1e-3f); } [Test] public void Level_GoldExp_ChapterCap() { var d = NewSave(); Assert.AreEqual(Db.Economy.StartGold, d.gold); Assert.AreEqual(Db.Economy.StartGem, d.gem); Assert.AreEqual(Db.Economy.StartStamina, d.stamina); var hs = d.Hero(Db.Economy.StartHeroes[0]); Assert.AreEqual(1, hs.lv); Assert.AreEqual(Db.Promotion.FirstGrade, hs.grade); Assert.AreEqual(Db.Heroes.Get(hs.id).rarity, hs.star, "★ = 희귀도"); StringAssert.Contains("경험", HeroRules.CanLevelUp(Db, d, hs)); var row = Db.Levels.Get(1); HeroRules.GrantExp(hs, row.expToNext); int gold = d.gold; Assert.IsTrue(HeroRules.LevelUp(Db, d, hs)); Assert.AreEqual(2, hs.lv); Assert.AreEqual(gold - row.goldCost, d.gold); Assert.AreEqual(0, hs.exp); int cap = Db.Levels.CapFor(1); Assert.AreEqual(cap, HeroRules.LevelCap(Db, d), "클리어 최고 챕터 1 = 상한"); hs.lv = cap; hs.exp = 1000000; d.gold = 1000000; StringAssert.Contains("상한", HeroRules.CanLevelUp(Db, d, hs)); d.chapterMax = HeroRules.NextCapChapter(Db, cap); Assert.IsNull(HeroRules.CanLevelUp(Db, d, hs), "다음 챕터 클리어 → 상한 해제"); d.gold = 0; StringAssert.Contains("골드", HeroRules.CanLevelUp(Db, d, hs)); } [Test] public void Stamina_RegenOffline_Cap_GemAndAd() { var e = Db.Economy; var d = NewSave(); d.stamina = 10; d.staminaAt = SaveStore.Iso(T0); EconomyRules.TickStamina(Db, d, T0.AddSeconds(3 * e.StaminaRegenSec + 5)); Assert.AreEqual(13, d.stamina, "regen 초마다 1"); Assert.AreEqual(e.StaminaRegenSec - 5, EconomyRules.SecondsToNext(Db, d, T0.AddSeconds(3 * e.StaminaRegenSec + 5)), "남은 초"); var later = T0.AddSeconds(1000.0 * e.StaminaRegenSec); // 오프라인 오래 → 최대에서 멈춤 EconomyRules.TickStamina(Db, d, later); Assert.AreEqual(e.StaminaMax, d.stamina); Assert.IsTrue(EconomyRules.SpendStamina(Db, d, e.StageCost, later)); Assert.AreEqual(e.StaminaMax - e.StageCost, d.stamina); Assert.AreEqual(e.StaminaRegenSec, EconomyRules.SecondsToNext(Db, d, later), "가득 찬 상태에서 쓰면 그때부터 회복"); d.stamina = 0; Assert.IsFalse(EconomyRules.SpendStamina(Db, d, e.StageCost, later), "부족 = 출전 불가"); d.gem = e.GemStaminaCost * (e.GemStaminaDaily + 1); for (int i = 0; i < e.GemStaminaDaily; i++) Assert.IsNull(EconomyRules.BuyStaminaWithGem(Db, d, later)); Assert.AreEqual(e.GemStaminaAmount * e.GemStaminaDaily, d.stamina); Assert.IsNotNull(EconomyRules.BuyStaminaWithGem(Db, d, later), "보옥 구매 일일 상한"); for (int i = 0; i < e.AdStaminaDaily; i++) Assert.IsNull(EconomyRules.AdStamina(Db, d, later)); Assert.IsNotNull(EconomyRules.AdStamina(Db, d, later), "광고 일일 상한"); Assert.IsNull(EconomyRules.AdStamina(Db, d, later.AddDays(1)), "다음 날 초기화"); } [Test] public void Promote_SixSlots_GradeUp_ConsumesGear() { var d = NewSave(); var h = Db.Heroes.Get(Db.Economy.StartHeroes[0]); var hs = d.Hero(h.id); string g0 = hs.grade; var need = Db.Heroes.GearFor(h, g0); Assert.IsNotNull(need); Assert.AreEqual(HeroRules.GearSlotCount(Db), need.Count); Assert.AreEqual("장비 없음", HeroRules.CanEquip(Db, d, h, hs, 0)); StringAssert.Contains("장비 0", HeroRules.CanPromote(Db, h, hs)); foreach (var id in need) d.inv.AddGear(id, 1); for (int i = 0; i < need.Count; i++) Assert.IsTrue(HeroRules.Equip(Db, d, h, hs, i), "칸 " + i); Assert.AreEqual(need.Count, hs.gearTotal, "1칸 = 1강(누적)"); foreach (var id in need) Assert.AreEqual(0, d.inv.GearCount(id), "가방에서 빠짐"); var before = HeroRules.Compute(Db, Cfg, h, hs); Assert.IsTrue(HeroRules.Promote(Db, h, hs)); Assert.AreEqual(Db.Promotion.Get(g0).nextGrade, hs.grade, "등급 +1"); foreach (var g in hs.gear) Assert.AreEqual("", g, "6칸 비움"); Assert.AreEqual(need.Count, hs.gearTotal, "누적 장비 보너스 유지"); var after = HeroRules.Compute(Db, Cfg, h, hs); Assert.AreEqual(Db.Promotion.Get(hs.grade).promoPct, after.promoPct); Assert.Greater(after.hp, before.hp); Assert.AreEqual(Db.Promotion.Get(hs.grade).order, HeroRules.SkillCap(Db, hs), "스킬 상한 = 등급 순서"); } [Test] public void Evolve_ShardsToNextStar() { var d = NewSave(); HeroDb.Hero h = null; foreach (var x in Db.Heroes.heroes) if (!x.isSoldier && x.rarity == 1) { h = x; break; } HeroRules.Grant(Db, d, h, HeroRules.GearSlotCount(Db)); var hs = d.Hero(h.id); Assert.AreEqual(1, hs.star); int need = Db.Evolve.Get(2).shardsNeeded; hs.shards = need - 1; Assert.IsNotNull(HeroRules.CanEvolve(Db, h, hs)); hs.shards = need; Assert.IsTrue(HeroRules.Evolve(Db, h, hs)); Assert.AreEqual(2, hs.star); Assert.AreEqual(0, hs.shards); Assert.AreEqual(Db.Evolve.Get(2).starPct, HeroRules.Compute(Db, Cfg, h, hs).starPct); hs.star = Db.Evolve.MaxStar; hs.shards = 100000; Assert.AreEqual("최고 ★", HeroRules.CanEvolve(Db, h, hs)); } // ---------- 뽑기 ---------- [Test] public void Gacha_ProbSumOne_Pity_TenGuarantee_Duplicates() { var g = Db.Gacha; Assert.AreEqual(1f, g.P(1) + g.P(2) + g.P(3), 1e-4f, "확률 합 = 1"); var rng = new System.Random(1093); var d = NewSave(); int gem = d.gem; Assert.IsTrue(GachaRules.FirstTenFree(Db, d)); var ten = GachaRules.Draw(Db, d, g.TenCount, rng); Assert.AreEqual(g.TenCount, ten.Count); Assert.AreEqual(gem, d.gem, "첫 10연 무료"); Assert.IsTrue(d.gacha.firstTenUsed); Assert.AreEqual(g.CostTen, GachaRules.Cost(Db, d, g.TenCount)); d.gem = 100000000; d.gacha.pity = g.Pity - 1; var p = GachaRules.Draw(Db, d, 1, rng); Assert.AreEqual(3, p[0].tier, "천장 = ★★★ 확정"); Assert.IsTrue(p[0].pity); Assert.AreEqual(0, d.gacha.pity, "획득 시 리셋"); Assert.AreEqual(3, Db.Heroes.Get(p[0].heroId).rarity); for (int k = 0; k < 200; k++) { var batch = GachaRules.Draw(Db, d, g.TenCount, rng); bool ok = false; foreach (var x in batch) ok |= x.tier >= g.TenGuaranteeRarity; Assert.IsTrue(ok, "10연 = ★★ 이상 1 보장"); } var counts = new int[4]; int n = 20000, shards = 0, gold = 0; for (int k = 0; k < n; k++) { d.gacha.pity = 0; var x = GachaRules.Draw(Db, d, 1, rng)[0]; counts[x.tier]++; shards += x.shards; gold += x.gold; var h = Db.Heroes.Get(x.heroId); Assert.AreEqual(h.isSoldier ? 1 : h.rarity, x.tier, x.heroId + " 등급 풀"); } for (int t = 1; t <= 3; t++) Assert.AreEqual(g.P(t), counts[t] / (float)n, 0.01f, "★" + t + " 빈도 ≈ 확률"); Assert.Greater(shards, 0, "중복 영웅 = 조각"); Assert.Greater(gold, 0, "중복 병사 = 골드"); } // ---------- 보상 · 별 ---------- static BalanceConfig.StageDef FirstStage(StageKind kind) { foreach (var s in Cfg.stages) if (s.chapter == Cfg.stages[0].chapter && s.kind == kind) return s; return null; } [Test] public void Reward_FirstRepeatBoss_Stars_Sweep() { var e = Db.Economy; var d = NewSave(); var rng = new System.Random(5); var s = FirstStage(StageKind.Normal); var row = Db.Rewards.For(s.chapter); float baseGold = row.goldBase + s.stage * e.GoldPerStage; var r = EconomyRules.Compute(Db, d, s, true, true, 1, false, rng); Assert.IsTrue(r.first); Assert.AreEqual(Mathf.RoundToInt(baseGold * e.FirstClearGoldMult), r.gold); Assert.AreEqual(row.expBase, r.exp); Assert.AreEqual(row.gemFirst, r.gem); Assert.AreEqual(3, r.stars); int gold = d.gold; var ids = new List(e.StartHeroes); EconomyRules.Apply(d, r, ids); Assert.AreEqual(gold + r.gold, d.gold); Assert.AreEqual(row.expBase, d.Hero(ids[0]).exp, "출전 전원 경험"); Assert.AreEqual(3, EconomyRules.StarsOf(d, s)); Assert.AreEqual(1, d.Stage(s.chapter, s.stage, false).clears); Assert.IsTrue(EconomyRules.CanSweep(d, s)); var rep = EconomyRules.Compute(Db, d, s, true, false, row.starTurns + 1, false, rng); Assert.IsFalse(rep.first); Assert.AreEqual(Mathf.RoundToInt(baseGold * e.RepeatRewardMult), rep.gold, "반복 ×0.5"); Assert.AreEqual(Mathf.RoundToInt(row.expBase * e.RepeatRewardMult), rep.exp); Assert.AreEqual(0, rep.gem); Assert.AreEqual(1, rep.stars, "생존·턴 조건 미달 = 별 1"); EconomyRules.Apply(d, rep, ids); Assert.AreEqual(3, EconomyRules.StarsOf(d, s), "최고 별 유지"); var sw = EconomyRules.Compute(Db, d, s, true, true, 0, true, rng); Assert.IsTrue(sw.sweep); Assert.IsFalse(sw.first); Assert.AreEqual(rep.gold, sw.gold, "소탕 = 반복 보상"); var boss = FirstStage(StageKind.Boss); var br = EconomyRules.Compute(Db, d, boss, true, true, 1, false, rng); Assert.AreEqual(row.gemFirst + row.gemBoss, br.gem, "보스 첫 클리어 보옥"); Assert.IsNotNull(br.gearId, "보스 장비 확정"); Assert.LessOrEqual(Db.Gear.Get(br.gearId).order, Db.Gear.OrderOf(row.gearGradeMax), "등급 ≤ gear_grade_max"); Assert.LessOrEqual(Db.Gear.Get(br.gearId).dropFromChapter, boss.chapter); var lose = EconomyRules.Compute(Db, d, boss, false, false, 3, false, rng); Assert.AreEqual(0, lose.gold + lose.exp + lose.gem + lose.stars); Assert.IsNull(lose.gearId); } [Test] public void Save_RoundTrip_AndSanitize() { SaveStore.UseMemory(true); var d = NewSave(); d.gold = 12345; d.inv.AddGear(Db.Gear.items[0].id, 3); d.Stage(1, 2, true).stars = 2; d.presets[1].slots[0] = ""; d.gacha.pity = 7; SaveStore.Write(d, T0); var l = SaveStore.Load(); Assert.IsNotNull(l); Assert.AreEqual(SaveStore.Version, l.ver); Assert.AreEqual(JsonUtility.ToJson(d), JsonUtility.ToJson(l), "세이브 왕복 = 같은 JSON"); Assert.AreEqual(3, l.inv.GearCount(Db.Gear.items[0].id)); var broken = new SaveData { presets = null }; broken.heroes.Add(new HeroState { id = Db.Economy.StartHeroes[0], gear = null, grade = "" }); HeroRules.Sanitize(Db, broken, Cfg.playerSpawnPoints.Length); Assert.AreEqual(Db.Economy.PresetCount, broken.presets.Count); Assert.AreEqual(Cfg.playerSpawnPoints.Length, broken.presets[0].slots.Count); Assert.AreEqual(HeroRules.GearSlotCount(Db), broken.heroes[0].gear.Count); Assert.AreEqual(Db.Promotion.FirstGrade, broken.heroes[0].grade); } [Test] public void Shop_DailySix_Buy_Refresh() { var e = Db.Economy; var d = NewSave(); var rng = new System.Random(3); EconomyRules.EnsureShop(Db, d, T0, rng); Assert.AreEqual(e.ShopSlots, d.shop.items.Count); var grades = new List(e.ShopGrades); var seen = new HashSet(); foreach (var it in d.shop.items) { var g = Db.Gear.Get(it.gearId); Assert.IsTrue(grades.Contains(g.grade), it.gearId); Assert.IsTrue(seen.Add(it.gearId), "겹침 없음"); Assert.AreEqual(Mathf.RoundToInt(g.sellGold * e.ShopPriceMult), it.price); } int gold = d.gold; var first = d.shop.items[0]; Assert.IsNull(EconomyRules.BuyShop(d, 0)); Assert.AreEqual(gold - first.price, d.gold); Assert.AreEqual(1, d.inv.GearCount(first.gearId)); Assert.AreEqual("품절", EconomyRules.BuyShop(d, 0)); int gem = d.gem; Assert.IsNull(EconomyRules.RefreshShop(Db, d, rng)); Assert.AreEqual(gem - e.ShopRefreshGem, d.gem); foreach (var it in d.shop.items) Assert.IsFalse(it.sold); var day = d.shop.day; EconomyRules.EnsureShop(Db, d, T0.AddDays(1), rng); Assert.AreNotEqual(day, d.shop.day, "다음 날 새로 깜"); } // ---------- 씬(로비 · 편성 · 입장 · 결과) ---------- OutGameRoot og; BattleManager bm; WorldMapScreen wm; IEnumerator Boot() { WorldProgress.Reset(); SaveStore.UseMemory(true); SceneManager.LoadScene("Battle"); yield return null; yield return null; og = OutGameRoot.Instance; bm = BattleManager.Instance; wm = WorldMapScreen.Instance; Assert.IsNotNull(og); Assert.IsTrue(og.Session.IsNewUser, "메모리 세이브 = 신규 유저"); } [UnityTest] public IEnumerator Lobby_Tabs_Back_ComingSoon() { yield return Boot(); Assert.IsTrue(og.LobbyVisible); og.OnTab(0); Assert.AreEqual(og.HeroList, og.Current); yield return null; // PD 2026-11-10: 하단 탭 글자 = 선택 안 된 탭도 · 선택된 탭 다시 누르면 닫기(로비) Assert.AreEqual(5, og.VisibleTabLabels, "하단 탭 글자 5개 모두 보임"); og.OnTab(0); Assert.AreEqual(og.Lobby, og.Current, "선택된 탭 다시 = 닫기(로비)"); Assert.AreEqual(5, og.VisibleTabLabels, "로비에서도 탭 글자 보임"); og.OnTab(0); Assert.AreEqual(og.HeroList, og.Current, "다시 열림"); yield return null; Assert.AreEqual(og.Session.Owned(false).Count, og.HeroList.CardCount, "보유 장수 카드"); // 미보유 장수 = 빈 카드가 아니라 그 영웅 그림 카드(PD 2026-11-10 「유닛 목록에 장수들의 모습이 보이도록」) int heroTotal = 0; foreach (var h in og.Session.Db.Heroes.heroes) if (!h.isSoldier) heroTotal++; Assert.AreEqual(heroTotal - og.Session.Owned(false).Count, og.HeroList.LockedCount, "미보유 장수 카드 = 그림 카드"); Assert.Greater(og.HeroList.LockedCount, 0, "신규 유저 = 미보유 있음"); var lockedTail = og.HeroList.Rt.GetComponentsInChildren(true); int lockedWithArt = 0; foreach (var tr in lockedTail) if (tr.name == "Locked" && tr.parent.Find("Bg/Character") != null) lockedWithArt++; Assert.AreEqual(og.HeroList.LockedCount, lockedWithArt, "미보유 카드마다 일러(Character)"); og.HeroList.SetFilter(true); yield return null; Assert.AreEqual(og.Session.Owned(true).Count, og.HeroList.CardCount, "병사 필터"); var id = og.Session.Db.Economy.StartHeroes[0]; og.ShowHero(id); Assert.AreEqual(og.HeroDetail, og.Current); Assert.AreEqual(id, og.HeroDetail.HeroId); og.Back(); Assert.AreEqual(og.HeroList, og.Current, "상세 ◀ = 목록"); og.Back(); Assert.IsTrue(og.LobbyVisible, "목록 ◀ = 로비"); og.OnTab(1); Assert.AreEqual(og.Treasures, og.Current, "보물 = 수집품 화면(PD #1093 C)"); og.OnTab(2); Assert.AreEqual(og.Domestic, og.Current, "내정 = 훈련·순찰·개발(PD #1093 C)"); og.Back(); Assert.IsTrue(og.LobbyVisible, "내정 ◀ = 로비"); og.OnTab(3); Assert.IsTrue(og.ModalVisible, "동맹 = 추후 업데이트"); og.CloseModal(); og.OnTab(4); Assert.AreEqual(og.Trade, og.Current); og.Trade.SetPage(2); yield return null; Assert.AreEqual(og.Session.Db.Economy.ShopSlots, og.Trade.ShopCardCount, "장비 상점 칸"); og.Back(); Assert.IsTrue(og.LobbyVisible); og.OpenWorldMap(); yield return null; Assert.IsTrue(wm.IsVisible); og.Back(); yield return null; Assert.IsFalse(wm.IsVisible, "월드맵 ◀ = 로비"); Assert.IsTrue(og.LobbyVisible); } [UnityTest] public IEnumerator Formation_PresetToSpawn_FullAndPartial() { yield return Boot(); var s = og.Session; var cfg = bm.Config; wm.EnterStage(0, 0); yield return null; Assert.AreEqual(s.Save.ActivePreset.slots.Count, bm.Deployed.Count, "프리셋 5 → 출전 5"); Assert.AreEqual(bm.Deployed.Count, bm.AliveCount(Faction.Player)); foreach (var u in bm.AliveUnits(Faction.Player)) { LineupEntry e = null; foreach (var x in bm.Deployed) if (x.name == u.DisplayName) e = x; Assert.IsNotNull(e, u.DisplayName); Assert.Less(Vector2.Distance(cfg.playerSpawnPoints[e.slot], u.Position), 0.01f, u.DisplayName + " 자리 = 편성 순서"); var st = s.StatsOf(e.id); Assert.AreEqual(st.Hp, u.MaxHp, u.DisplayName + " HP = §3-2 공식"); Assert.AreEqual(st.Atk, u.Atk, u.DisplayName + " 공"); Assert.AreEqual(st.Def, u.Def, u.DisplayName + " 방"); } int front = s.Db.Economy.FormationFront; for (int i = 0; i < cfg.playerSpawnPoints.Length; i++) Assert.AreEqual(i < front ? cfg.playerSpawnPoints[0].y : cfg.playerSpawnPoints[cfg.playerSpawnPoints.Length - 1].y, cfg.playerSpawnPoints[i].y, 0.01f, "전열 앞줄 · 후열 뒷줄"); Assert.Greater(cfg.playerSpawnPoints[0].y, cfg.playerSpawnPoints[cfg.playerSpawnPoints.Length - 1].y, "전열이 적 쪽(위)"); var ids = s.Save.ActivePreset.slots; var partial = new List(); for (int i = 0; i < ids.Count; i++) partial.Add(i == 1 || i == 3 ? ids[i] : ""); Assert.IsNull(s.SavePreset(1, partial)); Assert.AreEqual(1, s.Save.presetIdx, "저장 = 출전 프리셋"); bm.StartStage(0); yield return null; Assert.AreEqual(2, bm.AliveCount(Faction.Player), "빈 칸 허용"); foreach (var e in bm.Deployed) Assert.IsTrue(e.slot == 1 || e.slot == 3); Assert.IsNotNull(s.SavePreset(2, new List { "", "", "", "", "" }), "최소 1"); // 편성 화면 조작: 카드 탭 = 빼기/넣기 · 자리 맞바꾸기 · 저장 og.ShowLobby(); og.Show(og.Formation); var f = og.Formation; Assert.AreEqual(1, f.Preset); f.TapCard(ids[1]); Assert.AreEqual(1, CountFilled(f.Working), "카드 탭 = 빼기"); f.TapCard(ids[1]); Assert.AreEqual(2, CountFilled(f.Working), "다시 탭 = 빈 칸에 넣기"); string a0 = f.Working[0], a3 = f.Working[3]; f.Swap(0, 3); Assert.AreEqual(a3, f.Working[0]); Assert.AreEqual(a0, f.Working[3]); f.Place(ids[4], 2); Assert.AreEqual(ids[4], f.Working[2], "끌어 놓기 = 자리 넣기"); Assert.IsTrue(f.Dirty); f.SaveNow(); Assert.IsFalse(f.Dirty); Assert.AreEqual(ids[4], s.Save.presets[1].slots[2]); } static int CountFilled(IList l) { int n = 0; foreach (var x in l) if (!string.IsNullOrEmpty(x)) n++; return n; } [UnityTest] public IEnumerator WorldMap_StaminaEntry_Result_Stars_Sweep() { yield return Boot(); var s = og.Session; var e = s.Db.Economy; og.OpenWorldMap(); yield return null; int si = wm.StageIndexOf(0, 0); var stage = bm.Config.stages[si]; int cost = EconomyRules.StageCost(s.Db, stage); s.Save.stamina = cost - 1; wm.OpenPopup(0); wm.ConfirmPopup(); yield return null; Assert.IsTrue(wm.IsVisible, "군량 부족 = 출전 안 됨"); Assert.IsTrue(og.ModalVisible, "군량 충전 팝업"); Assert.AreEqual(BattlePhase.Setup, bm.Phase); og.CloseModal(); s.Save.stamina = e.StaminaMax; int gold = s.Save.gold; int exp0 = s.Save.Hero(e.StartHeroes[0]).exp; wm.OpenPopup(0); wm.ConfirmPopup(); yield return null; Assert.AreEqual(BattlePhase.PlayerTurn, bm.Phase); Assert.AreEqual(e.StaminaMax - cost, s.Save.stamina, "스테이지 군량 소모"); foreach (var u in bm.AliveUnits(Faction.Enemy)) u.TakeDamage(u.Hp); bm.CheckBattleEnd(); yield return null; Assert.AreEqual(BattlePhase.StageClear, bm.Phase); var r = s.LastReward; Assert.IsNotNull(r); Assert.AreEqual(3, r.stars, "1턴 · 전원 생존 = 별 3"); Assert.IsTrue(og.ResultVisible, "결과 화면 = 보상 패널"); Assert.AreEqual(gold + r.gold, s.Save.gold); Assert.AreEqual(exp0 + r.exp, s.Save.Hero(e.StartHeroes[0]).exp, "출전 영웅 경험"); Assert.AreEqual(bm.Deployed.Count, r.expTo.Count); bm.ContinueAfterResult(); yield return null; Assert.IsTrue(wm.IsVisible); Assert.AreEqual(3, wm.StarsOf(si)); Assert.AreEqual(NodeState.Clear, wm.StateOf(0, 0)); Assert.IsTrue(wm.CanSweep(si), "별 3 = 소탕"); int st = s.Save.stamina; wm.Sweep(si); Assert.AreEqual(st - cost, s.Save.stamina, "소탕 = 군량만"); Assert.AreEqual(2, s.Save.Stage(stage.chapter, stage.stage, false).clears); Assert.IsTrue(s.LastReward.sweep); Assert.AreEqual(BattlePhase.Setup, bm.Phase, "소탕 = 전투 없음"); } } }