658 lines
32 KiB
C#
658 lines
32 KiB
C#
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
|
||
{
|
||
/// <summary>
|
||
/// 아웃게임 1차(2026-11-05 #1093 · 시스템 기획 §12) — 임포터(행 수·필수 열·id 유일·참조 무결) · 공식(표 값으로 기대치) · 군량 회복·오프라인 ·
|
||
/// 승급 6칸 · 진화 조각 · 뽑기 확률·천장·10연 보장 · 보상 공식·별 · 편성 → 스폰 · 세이브 왕복 · 로비 탭·뒤로 · 군량 입장·결과·소탕 · 상점.
|
||
/// 기대값은 전부 표(CSV·자산)에서 계산한다(C45 · 테스트에 수치 상수 없음).
|
||
/// </summary>
|
||
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>("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<string>(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<string>(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<string>(e.ShopGrades);
|
||
var seen = new HashSet<string>();
|
||
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<Transform>(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<string>();
|
||
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<string> { "", "", "", "", "" }), "최소 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<string> 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, "소탕 = 전투 없음");
|
||
}
|
||
}
|
||
}
|