ThreeKingdomsSlingshot/Assets/Tests/PlayMode/OutGameTests.cs

658 lines
32 KiB
C#
Raw Permalink Normal View History

아웃게임 1차(PD #1093 세션 A) — 본영 로비 · 세이브 · 군량 · 영웅(레벨·승급 6칸·진화·스킬 표시) · 편성(전열 3/후열 2 · 프리셋 3) · 거래(뽑기·골드 장비 상점) · 스테이지 보상·별 3·소탕 - 데이터: 기획 tables/ 12장 → Assets/Data/OutGame/*.csv · 메뉴 TKS/Import OutGame Data(Editor/DataImporter) → Resources/OutGame 9 자산(HeroDb·GearDb·GachaConfig·EconomyConfig·RewardTable·LevelTable·PromotionTable·EvolveTable·SkillInfoDb) · 검증(행 수·필수 열·id 유일·hero↔skill↔gear·확률 합) 오류 시 자산 불변 · BalanceConfig.heroes[]/HeroDef 삭제 → HeroDb(기존 5인 id 유지) · economy.csv 키 17 + gacha.csv 키 2 추가(기획서 본문 값 · C45) - 세이브: persistentDataPath/save.json · ver 1 · 즉시 저장 · 오프라인 군량 회복 · 일일 초기화 · 신규 유저 = 유비·관우·장비·조운·황충 Lv1 녹 ★=희귀도 · 골드 1000 · 보옥 300 · 군량 30 · 테스트·캡처 = 메모리 전용 - 화면(uGUI 와이어프레임 · img03·06·08·11): 시작 = 본영 로비(출정 → 월드맵 · 편성 · 연무장/보물/내정 준비 중 · 동맹 추후) · 재화 바(월드맵 포함 · 군량 타이머) · 하단 탭 5 · ◀/Esc 일관 · 영웅 목록·상세·상세정보·도감 · 편성(탭·드래그) · 뽑기(천장 n/80 · 확률 공개 · 첫 10연 무료 · 10연 ★★ 보장 · 카드 순차 공개) · 장비 상점(흰/녹 6종 일일 · sell×5 · 새로고침 보옥 20) - 전투 연결: BattleManager.LineupProvider/Deployed(LineupEntry · §3-2 스탯 · 방어 배율 생성 뒤) · playerSpawnPoints = 전열 (−4/0/4, −12) · 후열 (−2/2, −14) · WorldMapScreen 훅(팝업 군량·보상·별 · 입장 군량 · 노드 별 star_on/off · 소탕 · ◀ 본영 · TopInset) · 결과 패널(별 3 조건 · 골드·경험·보옥·장비) · GameBootstrap/AutoPlayDriver 로비 시작 - B 세션 파일(ActionResolver·Combat·UnitView·BattleHUD·SlingshotInput·EnemyAI) 미변경 - 테스트 56/56(기존 41 유지 · 신규 OutGameTests 15) · console error 0 · 사본 GUI 캡처 11장(DataImporter.CaptureOutGame) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 12:11:10 +00:00
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;
로비 영웅 = 크기 표 기준으로 세움(관우만 작던 문제) · 축융 금빛 초커(턱선) · 하단 탭 글자 항상 + 선택 탭 다시 = 닫기 · 하단 탭 아이콘 5종 캐주얼 화풍(#1108 · PD 2026-11-10) PD: 「로비에 배치된 관우의 크기만 작게 보이는 건지 체크해서 수정」 · 「축융의 얼굴 턱선이 없어서 얼굴이 너무 커보여」 · 「하단 아이콘이 선택되지 않았을 때에도 글자가 보이게 · 선택된 상태일 경우 X(닫기 버튼)처럼 동작」 · 「하단 아이콘을 우리 게임 화풍(캐주얼)에 맞게 다시 그려줘」 - 로비 원인: 출전 영웅을 칸(196×340)에 그림 틀째 맞춤 → 틀 = 세 자세 합집합이라 돌진 언월도로 넓은 관우(930 px)만 ×0.21(몸 153 px · 유비 185) → UiKit.UnitStand = 크기 표 size_px 기준 배율 + 기마 머리 끝 상한(BalanceConfig.mountedArtMaxTop) · 발 = 피벗 · LobbyUnitPx 206(유비 예전 크기) - 축융: 맨 어깨라 얼굴·몸 피부가 이어져 턱이 없음 → 부분 다시 그리기 2 · 턱 호 덧칠 3 · 전체 재생성 1(피부가 하얘짐) 시험 → 채택 = 턱 아래 금빛 초커 덧칠(검정 외곽선 + 그림자 1단 · 생성 0 · 3자세) · 크기 표 눈 자동 측정 가능해짐 → adjust 1.18 → 1.02(카드 재현 확인) - 하단 탭: 글자 = 모든 탭(선택 = 흰 34 · 아니면 흐린 30) · 아이콘 글자 위로 · OnTab 선택 탭 다시 = ShowLobby · VisibleTabLabels - 아이콘: Assets/UI/Samguk/menu/tab_{hero,treasure,domestic,alliance,trade}.png = Krea 2 t2i 유닛과 같은 화풍 C · 20장 중 5 · 256² · 테마 치환(ui_delivered.csv md5) - 테스트(고친 부분만): Lobby_Tabs_Back_ComingSoon(탭 글자 5 · 선택 탭 다시 = 로비 · 다시 열림) · UILayerTests · UnitArt_SizeTable 각 1/1 · error 0 · 캡처 lobby · hero_list Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 04:50:57 +00:00
// 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;
아웃게임 1차(PD #1093 세션 A) — 본영 로비 · 세이브 · 군량 · 영웅(레벨·승급 6칸·진화·스킬 표시) · 편성(전열 3/후열 2 · 프리셋 3) · 거래(뽑기·골드 장비 상점) · 스테이지 보상·별 3·소탕 - 데이터: 기획 tables/ 12장 → Assets/Data/OutGame/*.csv · 메뉴 TKS/Import OutGame Data(Editor/DataImporter) → Resources/OutGame 9 자산(HeroDb·GearDb·GachaConfig·EconomyConfig·RewardTable·LevelTable·PromotionTable·EvolveTable·SkillInfoDb) · 검증(행 수·필수 열·id 유일·hero↔skill↔gear·확률 합) 오류 시 자산 불변 · BalanceConfig.heroes[]/HeroDef 삭제 → HeroDb(기존 5인 id 유지) · economy.csv 키 17 + gacha.csv 키 2 추가(기획서 본문 값 · C45) - 세이브: persistentDataPath/save.json · ver 1 · 즉시 저장 · 오프라인 군량 회복 · 일일 초기화 · 신규 유저 = 유비·관우·장비·조운·황충 Lv1 녹 ★=희귀도 · 골드 1000 · 보옥 300 · 군량 30 · 테스트·캡처 = 메모리 전용 - 화면(uGUI 와이어프레임 · img03·06·08·11): 시작 = 본영 로비(출정 → 월드맵 · 편성 · 연무장/보물/내정 준비 중 · 동맹 추후) · 재화 바(월드맵 포함 · 군량 타이머) · 하단 탭 5 · ◀/Esc 일관 · 영웅 목록·상세·상세정보·도감 · 편성(탭·드래그) · 뽑기(천장 n/80 · 확률 공개 · 첫 10연 무료 · 10연 ★★ 보장 · 카드 순차 공개) · 장비 상점(흰/녹 6종 일일 · sell×5 · 새로고침 보옥 20) - 전투 연결: BattleManager.LineupProvider/Deployed(LineupEntry · §3-2 스탯 · 방어 배율 생성 뒤) · playerSpawnPoints = 전열 (−4/0/4, −12) · 후열 (−2/2, −14) · WorldMapScreen 훅(팝업 군량·보상·별 · 입장 군량 · 노드 별 star_on/off · 소탕 · ◀ 본영 · TopInset) · 결과 패널(별 3 조건 · 골드·경험·보옥·장비) · GameBootstrap/AutoPlayDriver 로비 시작 - B 세션 파일(ActionResolver·Combat·UnitView·BattleHUD·SlingshotInput·EnemyAI) 미변경 - 테스트 56/56(기존 41 유지 · 신규 OutGameTests 15) · console error 0 · 사본 GUI 캡처 11장(DataImporter.CaptureOutGame) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 12:11:10 +00:00
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)");
아웃게임 1차(PD #1093 세션 A) — 본영 로비 · 세이브 · 군량 · 영웅(레벨·승급 6칸·진화·스킬 표시) · 편성(전열 3/후열 2 · 프리셋 3) · 거래(뽑기·골드 장비 상점) · 스테이지 보상·별 3·소탕 - 데이터: 기획 tables/ 12장 → Assets/Data/OutGame/*.csv · 메뉴 TKS/Import OutGame Data(Editor/DataImporter) → Resources/OutGame 9 자산(HeroDb·GearDb·GachaConfig·EconomyConfig·RewardTable·LevelTable·PromotionTable·EvolveTable·SkillInfoDb) · 검증(행 수·필수 열·id 유일·hero↔skill↔gear·확률 합) 오류 시 자산 불변 · BalanceConfig.heroes[]/HeroDef 삭제 → HeroDb(기존 5인 id 유지) · economy.csv 키 17 + gacha.csv 키 2 추가(기획서 본문 값 · C45) - 세이브: persistentDataPath/save.json · ver 1 · 즉시 저장 · 오프라인 군량 회복 · 일일 초기화 · 신규 유저 = 유비·관우·장비·조운·황충 Lv1 녹 ★=희귀도 · 골드 1000 · 보옥 300 · 군량 30 · 테스트·캡처 = 메모리 전용 - 화면(uGUI 와이어프레임 · img03·06·08·11): 시작 = 본영 로비(출정 → 월드맵 · 편성 · 연무장/보물/내정 준비 중 · 동맹 추후) · 재화 바(월드맵 포함 · 군량 타이머) · 하단 탭 5 · ◀/Esc 일관 · 영웅 목록·상세·상세정보·도감 · 편성(탭·드래그) · 뽑기(천장 n/80 · 확률 공개 · 첫 10연 무료 · 10연 ★★ 보장 · 카드 순차 공개) · 장비 상점(흰/녹 6종 일일 · sell×5 · 새로고침 보옥 20) - 전투 연결: BattleManager.LineupProvider/Deployed(LineupEntry · §3-2 스탯 · 방어 배율 생성 뒤) · playerSpawnPoints = 전열 (−4/0/4, −12) · 후열 (−2/2, −14) · WorldMapScreen 훅(팝업 군량·보상·별 · 입장 군량 · 노드 별 star_on/off · 소탕 · ◀ 본영 · TopInset) · 결과 패널(별 3 조건 · 골드·경험·보옥·장비) · GameBootstrap/AutoPlayDriver 로비 시작 - B 세션 파일(ActionResolver·Combat·UnitView·BattleHUD·SlingshotInput·EnemyAI) 미변경 - 테스트 56/56(기존 41 유지 · 신규 OutGameTests 15) · console error 0 · 사본 GUI 캡처 11장(DataImporter.CaptureOutGame) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 12:11:10 +00:00
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);
아웃게임 2차(PD #1093 세션 C) — 내정(훈련·순찰·개발) · 보물(수집품·세트·상자) · 연무장(탑·일일 던전·투기장·적장 토벌) · 거래 상자 탭 + 영웅 56명 확장(PD #1096 · 신규 effect_key 4) - 데이터: C 표 10장(training · training_ranks · patrol · dev_tree · treasures · treasure_sets · chests(+iron/buy 열) · tower · dungeons(+reward_amount) · arena(+상점 키)) + economy.csv C 키 24 → 임포트 18 자산(OutGameTables.C.cs 검증: 행·연속 Lv/랭크/층·참조·확률 합 1·선행 노드·효과 키 처리기·카운터 키 · 오류 시 자산 불변) - 영웅 56명: heroes 71 · skills 112 · hero_gear 1344 · gacha_pool 71 재복사 · 신규 키 4(team_dmg_pct_turn · cooldown_add · charm · heal_self_pct) + 기존 키 조건 열(받는 피해 p2 HP 임계/보스/근접 · p3 출전 인원 아군 전원 · 최대 HP·반격 아군 전원 · 피해 % 발동 턴) · 대장군 키는 표 미사용(처리기 유지) - 내정: 훈련(병종 5 · 3항목 Lv · cost_mult · 랭크 승급 · train_bonus → §3-2 항목별) · 순찰(챕터 행 × 시간 · 오프라인 · 상한 12h + 농업 · 소수점 이월 · 광고 2배 스텁 · 즉시 수확 보옥) · 개발(갈래 3 · 선행 · 골드·도면 비용 · 효과 키 → 군량·보상·상점·순찰·상자 적용 지점 · 초기화 보옥 + 도면 환급) - 보물: 보유 중(4열 · 등급 색 · 상세·강화) · 세트(단계 효과) · 상자(포인트 → 명장 · 확률 · 중복 조각) · 효과 hp/atk/def → HeroRules · crit/range/splash → BattleManager.SideEffectsProvider → SkillMods · 조건 카운터 8종 = Runtime/BattleEvents(전투 코드 Raise 1줄씩 · 아군만) · 스테이지 청동 상자 10 % - 연무장: BattleManager.StartMode(BattleMode = 적 편성·제한 턴·onExit) · 탑 100층(입장권·층 배율·보상·10층 상자) · 던전 3(요일·2/일·보상) · 투기장(샘플 10 · EnemyAI 방어 · 메달·주간 순위·조각 상점) · 적장 토벌(주간 순환·hp ×20·5턴·누적 피해·명장 상자·천기) · 결과 패널 → 탭 = 연무장 - 거래: 상자 탭(구매 = 골드/보옥/포인트 · 거래 할인) · 패스·월정액·챕터 팩·첫 결제 팩 = 준비 중 스텁 · 스킬서 임시 판매 제거(사당 던전) - 테스트 87/87(기존 73 유지 · 갱신 3 = 로비 탭 · 표 키 목록 · 상점 스킬서 칸 / 신규 14 = OutGameCTests 12 + 확장 키 2) · console error 0 · 캡처 = DataImporter.CaptureC Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 15:22:54 +00:00
Assert.AreEqual(og.Treasures, og.Current, "보물 = 수집품 화면(PD #1093 C)");
og.OnTab(2);
Assert.AreEqual(og.Domestic, og.Current, "내정 = 훈련·순찰·개발(PD #1093 C)");
아웃게임 1차(PD #1093 세션 A) — 본영 로비 · 세이브 · 군량 · 영웅(레벨·승급 6칸·진화·스킬 표시) · 편성(전열 3/후열 2 · 프리셋 3) · 거래(뽑기·골드 장비 상점) · 스테이지 보상·별 3·소탕 - 데이터: 기획 tables/ 12장 → Assets/Data/OutGame/*.csv · 메뉴 TKS/Import OutGame Data(Editor/DataImporter) → Resources/OutGame 9 자산(HeroDb·GearDb·GachaConfig·EconomyConfig·RewardTable·LevelTable·PromotionTable·EvolveTable·SkillInfoDb) · 검증(행 수·필수 열·id 유일·hero↔skill↔gear·확률 합) 오류 시 자산 불변 · BalanceConfig.heroes[]/HeroDef 삭제 → HeroDb(기존 5인 id 유지) · economy.csv 키 17 + gacha.csv 키 2 추가(기획서 본문 값 · C45) - 세이브: persistentDataPath/save.json · ver 1 · 즉시 저장 · 오프라인 군량 회복 · 일일 초기화 · 신규 유저 = 유비·관우·장비·조운·황충 Lv1 녹 ★=희귀도 · 골드 1000 · 보옥 300 · 군량 30 · 테스트·캡처 = 메모리 전용 - 화면(uGUI 와이어프레임 · img03·06·08·11): 시작 = 본영 로비(출정 → 월드맵 · 편성 · 연무장/보물/내정 준비 중 · 동맹 추후) · 재화 바(월드맵 포함 · 군량 타이머) · 하단 탭 5 · ◀/Esc 일관 · 영웅 목록·상세·상세정보·도감 · 편성(탭·드래그) · 뽑기(천장 n/80 · 확률 공개 · 첫 10연 무료 · 10연 ★★ 보장 · 카드 순차 공개) · 장비 상점(흰/녹 6종 일일 · sell×5 · 새로고침 보옥 20) - 전투 연결: BattleManager.LineupProvider/Deployed(LineupEntry · §3-2 스탯 · 방어 배율 생성 뒤) · playerSpawnPoints = 전열 (−4/0/4, −12) · 후열 (−2/2, −14) · WorldMapScreen 훅(팝업 군량·보상·별 · 입장 군량 · 노드 별 star_on/off · 소탕 · ◀ 본영 · TopInset) · 결과 패널(별 3 조건 · 골드·경험·보옥·장비) · GameBootstrap/AutoPlayDriver 로비 시작 - B 세션 파일(ActionResolver·Combat·UnitView·BattleHUD·SlingshotInput·EnemyAI) 미변경 - 테스트 56/56(기존 41 유지 · 신규 OutGameTests 15) · console error 0 · 사본 GUI 캡처 11장(DataImporter.CaptureOutGame) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 12:11:10 +00:00
og.Back();
아웃게임 2차(PD #1093 세션 C) — 내정(훈련·순찰·개발) · 보물(수집품·세트·상자) · 연무장(탑·일일 던전·투기장·적장 토벌) · 거래 상자 탭 + 영웅 56명 확장(PD #1096 · 신규 effect_key 4) - 데이터: C 표 10장(training · training_ranks · patrol · dev_tree · treasures · treasure_sets · chests(+iron/buy 열) · tower · dungeons(+reward_amount) · arena(+상점 키)) + economy.csv C 키 24 → 임포트 18 자산(OutGameTables.C.cs 검증: 행·연속 Lv/랭크/층·참조·확률 합 1·선행 노드·효과 키 처리기·카운터 키 · 오류 시 자산 불변) - 영웅 56명: heroes 71 · skills 112 · hero_gear 1344 · gacha_pool 71 재복사 · 신규 키 4(team_dmg_pct_turn · cooldown_add · charm · heal_self_pct) + 기존 키 조건 열(받는 피해 p2 HP 임계/보스/근접 · p3 출전 인원 아군 전원 · 최대 HP·반격 아군 전원 · 피해 % 발동 턴) · 대장군 키는 표 미사용(처리기 유지) - 내정: 훈련(병종 5 · 3항목 Lv · cost_mult · 랭크 승급 · train_bonus → §3-2 항목별) · 순찰(챕터 행 × 시간 · 오프라인 · 상한 12h + 농업 · 소수점 이월 · 광고 2배 스텁 · 즉시 수확 보옥) · 개발(갈래 3 · 선행 · 골드·도면 비용 · 효과 키 → 군량·보상·상점·순찰·상자 적용 지점 · 초기화 보옥 + 도면 환급) - 보물: 보유 중(4열 · 등급 색 · 상세·강화) · 세트(단계 효과) · 상자(포인트 → 명장 · 확률 · 중복 조각) · 효과 hp/atk/def → HeroRules · crit/range/splash → BattleManager.SideEffectsProvider → SkillMods · 조건 카운터 8종 = Runtime/BattleEvents(전투 코드 Raise 1줄씩 · 아군만) · 스테이지 청동 상자 10 % - 연무장: BattleManager.StartMode(BattleMode = 적 편성·제한 턴·onExit) · 탑 100층(입장권·층 배율·보상·10층 상자) · 던전 3(요일·2/일·보상) · 투기장(샘플 10 · EnemyAI 방어 · 메달·주간 순위·조각 상점) · 적장 토벌(주간 순환·hp ×20·5턴·누적 피해·명장 상자·천기) · 결과 패널 → 탭 = 연무장 - 거래: 상자 탭(구매 = 골드/보옥/포인트 · 거래 할인) · 패스·월정액·챕터 팩·첫 결제 팩 = 준비 중 스텁 · 스킬서 임시 판매 제거(사당 던전) - 테스트 87/87(기존 73 유지 · 갱신 3 = 로비 탭 · 표 키 목록 · 상점 스킬서 칸 / 신규 14 = OutGameCTests 12 + 확장 키 2) · console error 0 · 캡처 = DataImporter.CaptureC Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 15:22:54 +00:00
Assert.IsTrue(og.LobbyVisible, "내정 ◀ = 로비");
아웃게임 1차(PD #1093 세션 A) — 본영 로비 · 세이브 · 군량 · 영웅(레벨·승급 6칸·진화·스킬 표시) · 편성(전열 3/후열 2 · 프리셋 3) · 거래(뽑기·골드 장비 상점) · 스테이지 보상·별 3·소탕 - 데이터: 기획 tables/ 12장 → Assets/Data/OutGame/*.csv · 메뉴 TKS/Import OutGame Data(Editor/DataImporter) → Resources/OutGame 9 자산(HeroDb·GearDb·GachaConfig·EconomyConfig·RewardTable·LevelTable·PromotionTable·EvolveTable·SkillInfoDb) · 검증(행 수·필수 열·id 유일·hero↔skill↔gear·확률 합) 오류 시 자산 불변 · BalanceConfig.heroes[]/HeroDef 삭제 → HeroDb(기존 5인 id 유지) · economy.csv 키 17 + gacha.csv 키 2 추가(기획서 본문 값 · C45) - 세이브: persistentDataPath/save.json · ver 1 · 즉시 저장 · 오프라인 군량 회복 · 일일 초기화 · 신규 유저 = 유비·관우·장비·조운·황충 Lv1 녹 ★=희귀도 · 골드 1000 · 보옥 300 · 군량 30 · 테스트·캡처 = 메모리 전용 - 화면(uGUI 와이어프레임 · img03·06·08·11): 시작 = 본영 로비(출정 → 월드맵 · 편성 · 연무장/보물/내정 준비 중 · 동맹 추후) · 재화 바(월드맵 포함 · 군량 타이머) · 하단 탭 5 · ◀/Esc 일관 · 영웅 목록·상세·상세정보·도감 · 편성(탭·드래그) · 뽑기(천장 n/80 · 확률 공개 · 첫 10연 무료 · 10연 ★★ 보장 · 카드 순차 공개) · 장비 상점(흰/녹 6종 일일 · sell×5 · 새로고침 보옥 20) - 전투 연결: BattleManager.LineupProvider/Deployed(LineupEntry · §3-2 스탯 · 방어 배율 생성 뒤) · playerSpawnPoints = 전열 (−4/0/4, −12) · 후열 (−2/2, −14) · WorldMapScreen 훅(팝업 군량·보상·별 · 입장 군량 · 노드 별 star_on/off · 소탕 · ◀ 본영 · TopInset) · 결과 패널(별 3 조건 · 골드·경험·보옥·장비) · GameBootstrap/AutoPlayDriver 로비 시작 - B 세션 파일(ActionResolver·Combat·UnitView·BattleHUD·SlingshotInput·EnemyAI) 미변경 - 테스트 56/56(기존 41 유지 · 신규 OutGameTests 15) · console error 0 · 사본 GUI 캡처 11장(DataImporter.CaptureOutGame) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 12:11:10 +00:00
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, "소탕 = 전투 없음");
}
}
}