748 lines
41 KiB
C#
748 lines
41 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>
|
|||
|
|
/// 아웃게임 2차(PD #1093 C · 시스템 기획 §7~§9 · §12) — 임포터(C 표 10장 행 수·참조·확률 합·트리 선행 · 깨진 표) · 훈련 공식·랭크 · 순찰 누적·오프라인·광고·즉시 ·
|
|||
|
|
/// 개발 선행·비용·도면·효과 적용·초기화 · 보물 상자 확률·중복 조각·강화·세트 단계·조건 카운터·스탯·전투 효과 · 탑 층·입장권 · 던전 요일·입장 · 투기장 메달·순위·시즌·상점 ·
|
|||
|
|
/// 적장 주간 순환·누적 · 화면(내정·보물·연무장·거래 상자) · 연무장 모드 전투 → 결과 → 복귀 · 전투 이벤트 카운터.
|
|||
|
|
/// 기대값은 전부 표(CSV·자산)에서 계산한다(C45 · 테스트에 수치 상수 없음).
|
|||
|
|
/// </summary>
|
|||
|
|
public class OutGameCTests
|
|||
|
|
{
|
|||
|
|
static readonly DateTime T0 = new DateTime(2026, 11, 5, 3, 0, 0, DateTimeKind.Utc);
|
|||
|
|
|
|||
|
|
static readonly string[] CFiles =
|
|||
|
|
{
|
|||
|
|
"training.csv", "training_ranks.csv", "patrol.csv", "dev_tree.csv", "treasures.csv", "treasure_sets.csv", "chests.csv", "tower.csv", "dungeons.csv", "arena.csv",
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
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()
|
|||
|
|
{
|
|||
|
|
var d = HeroRules.NewSave(Db, Cfg.playerSpawnPoints.Length, T0);
|
|||
|
|
ArenaRules.RollDaily(Db, d, T0);
|
|||
|
|
return d;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static DevTree.Node NodeWith(string key)
|
|||
|
|
{
|
|||
|
|
foreach (var n in Db.Dev.nodes)
|
|||
|
|
if (Array.IndexOf(n.EffectKeys, key) >= 0) return n;
|
|||
|
|
Assert.Fail("개발 노드에 " + key + " 없음");
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- 임포터 ----------
|
|||
|
|
|
|||
|
|
[Test]
|
|||
|
|
public void Import_CTables_RowsRefsProbs_AssetsMatch()
|
|||
|
|
{
|
|||
|
|
var r = OutGameTables.Parse(ReadTables());
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
Assert.AreEqual(0, r.errors.Count, string.Join("\n", r.errors.ToArray()));
|
|||
|
|
foreach (var f in CFiles) Assert.AreEqual(CsvRows(f), r.rowCounts[f], f + " 행 수");
|
|||
|
|
var d = r.data;
|
|||
|
|
Assert.AreEqual(CsvRows("training.csv"), UnitTypeInfo.Count * TrainingTable.Stats.Length * d.Training.MaxLevel, "훈련 = 병종 × 3항목 × Lv");
|
|||
|
|
Assert.AreEqual(CsvRows("training_ranks.csv"), d.Training.MaxRank);
|
|||
|
|
Assert.AreEqual(CsvRows("dev_tree.csv"), d.Dev.nodes.Count);
|
|||
|
|
Assert.AreEqual(CsvRows("treasures.csv"), d.Treasures.items.Count);
|
|||
|
|
Assert.AreEqual(CsvRows("treasure_sets.csv"), d.TreasureSets.tiers.Count);
|
|||
|
|
Assert.AreEqual(CsvRows("chests.csv"), d.Chests.chests.Count);
|
|||
|
|
Assert.AreEqual(CsvRows("tower.csv"), d.Tower.Top);
|
|||
|
|
Assert.AreEqual(CsvRows("dungeons.csv"), d.Dungeons.dungeons.Count);
|
|||
|
|
foreach (var c in d.Chests.chests)
|
|||
|
|
{
|
|||
|
|
float sum = 0f;
|
|||
|
|
foreach (var p in c.p) sum += p;
|
|||
|
|
Assert.AreEqual(1f, sum, 1e-4f, c.id + " 확률 합 = 1");
|
|||
|
|
}
|
|||
|
|
var seen = new HashSet<string>();
|
|||
|
|
foreach (var n in d.Dev.nodes)
|
|||
|
|
{
|
|||
|
|
foreach (var q in n.requires) Assert.IsTrue(seen.Contains(q.node), n.id + " 선행 = 앞 행 " + q.node);
|
|||
|
|
seen.Add(n.id);
|
|||
|
|
}
|
|||
|
|
foreach (var f in d.Tower.floors) Assert.IsNotNull(ArenaRules.FindStage(Cfg, f.templateStage), f.floor + "층 템플릿 " + f.templateStage);
|
|||
|
|
foreach (var g in d.Dungeons.dungeons) Assert.IsNotNull(ArenaRules.FindStage(Cfg, g.templateStage), g.id + " 템플릿 " + g.templateStage);
|
|||
|
|
foreach (var t in d.TreasureSets.tiers)
|
|||
|
|
foreach (var m in t.members) Assert.AreEqual(t.setId, d.Treasures.Get(m).setId, t.setId + " ↔ " + m);
|
|||
|
|
|
|||
|
|
Assert.IsTrue(Db.Complete, "Resources/OutGame 18 자산(TKS/Import OutGame Data)");
|
|||
|
|
Assert.AreEqual(d.Training.rows.Count, Db.Training.rows.Count);
|
|||
|
|
Assert.AreEqual(d.Dev.nodes.Count, Db.Dev.nodes.Count);
|
|||
|
|
Assert.AreEqual(d.Treasures.items.Count, Db.Treasures.items.Count);
|
|||
|
|
Assert.AreEqual(d.Tower.floors.Count, Db.Tower.floors.Count);
|
|||
|
|
foreach (var e in d.Arena.values.entries) Assert.AreEqual(e.value, Db.Arena.values.S(e.key), e.key);
|
|||
|
|
for (int i = 0; i < d.Chests.chests.Count; i++) Assert.AreEqual(d.Chests.chests[i].buyGem, Db.Chests.chests[i].buyGem, d.Chests.chests[i].id);
|
|||
|
|
}
|
|||
|
|
finally
|
|||
|
|
{
|
|||
|
|
OutGameTables.Destroy(r);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[Test]
|
|||
|
|
public void Import_CBroken_ReportsErrors_AssetsUnchanged()
|
|||
|
|
{
|
|||
|
|
var texts = ReadTables();
|
|||
|
|
int ci = Array.IndexOf(OutGameTables.Files, "chests.csv");
|
|||
|
|
texts[ci] = texts[ci].Replace("bronze,청동 상자,1,0.8,0.2", "bronze,청동 상자,1,0.9,0.2"); // 확률 합 ≠ 1
|
|||
|
|
int di = Array.IndexOf(OutGameTables.Files, "dev_tree.csv");
|
|||
|
|
texts[di] = texts[di].Replace("com_gold_gain:3", "com_nope:3"); // 없는 선행
|
|||
|
|
int ti = Array.IndexOf(OutGameTables.Files, "treasures.csv");
|
|||
|
|
int cut = texts[ti].IndexOf(",boss_kills,", StringComparison.Ordinal);
|
|||
|
|
texts[ti] = texts[ti].Substring(0, cut) + ",nope_key," + texts[ti].Substring(cut + ",boss_kills,".Length); // 없는 카운터
|
|||
|
|
int wi = Array.IndexOf(OutGameTables.Files, "tower.csv");
|
|||
|
|
texts[wi] = texts[wi].Replace(",1-1,", ",1-x,"); // 템플릿 형식
|
|||
|
|
var r = OutGameTables.Parse(texts);
|
|||
|
|
var all = string.Join("\n", r.errors.ToArray());
|
|||
|
|
OutGameTables.Destroy(r);
|
|||
|
|
StringAssert.Contains("bronze", all);
|
|||
|
|
StringAssert.Contains("com_nope", all);
|
|||
|
|
StringAssert.Contains("nope_key", all);
|
|||
|
|
StringAssert.Contains("1-x", all);
|
|||
|
|
Assert.AreEqual(CsvRows("treasures.csv"), Db.Treasures.items.Count, "실패한 파싱은 자산을 건드리지 않음");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- 훈련 ----------
|
|||
|
|
|
|||
|
|
[Test]
|
|||
|
|
public void Training_CostLevelRankPromote_StatFormula()
|
|||
|
|
{
|
|||
|
|
var d = NewSave();
|
|||
|
|
var T = Db.Training;
|
|||
|
|
var t = Db.Heroes.Get(Db.Economy.StartHeroes[0]).type;
|
|||
|
|
d.gold = 100000000;
|
|||
|
|
Assert.AreEqual(Mathf.RoundToInt(T.Get(t, "hp", 1).goldCost * T.RankOf(1).costMult), TrainingRules.Cost(Db, d, t, 0), "비용 = gold_cost × cost_mult");
|
|||
|
|
int gold = d.gold, spent = 0;
|
|||
|
|
for (int i = 0; i < T.MaxLevel; i++)
|
|||
|
|
{
|
|||
|
|
spent += TrainingRules.Cost(Db, d, t, 0);
|
|||
|
|
Assert.IsNull(TrainingRules.Level(Db, d, t, 0), "HP Lv " + (i + 1));
|
|||
|
|
}
|
|||
|
|
Assert.AreEqual(gold - spent, d.gold);
|
|||
|
|
Assert.AreEqual("Max", TrainingRules.CanLevel(Db, d, t, 0));
|
|||
|
|
Assert.AreEqual(T.Get(t, "hp", T.MaxLevel).statPct + T.RankOf(1).rankPct, TrainingRules.Bonus(Db, d, t, "hp"), 1e-4f);
|
|||
|
|
StringAssert.Contains("남은 레벨", TrainingRules.CanPromote(Db, d, t));
|
|||
|
|
for (int s = 1; s < 3; s++)
|
|||
|
|
for (int i = 0; i < T.MaxLevel; i++) TrainingRules.Level(Db, d, t, s);
|
|||
|
|
Assert.AreEqual(0, TrainingRules.Remaining(Db, d, t));
|
|||
|
|
int pc = TrainingRules.PromoteCost(Db, d, t);
|
|||
|
|
Assert.AreEqual(T.RankOf(1).promoteGold, pc);
|
|||
|
|
d.gold = pc - 1;
|
|||
|
|
StringAssert.Contains("골드", TrainingRules.CanPromote(Db, d, t));
|
|||
|
|
d.gold = pc;
|
|||
|
|
Assert.IsNull(TrainingRules.Promote(Db, d, t));
|
|||
|
|
Assert.AreEqual(0, d.gold);
|
|||
|
|
Assert.AreEqual(2, TrainingRules.Rank(d, t));
|
|||
|
|
Assert.AreEqual(T.MaxLevel * 3, TrainingRules.Remaining(Db, d, t), "레벨 리셋");
|
|||
|
|
Assert.AreEqual(T.RankOf(2).rankPct, TrainingRules.Bonus(Db, d, t, "hp"), 1e-4f, "랭크 보너스 유지");
|
|||
|
|
Assert.AreEqual(Mathf.RoundToInt(T.Get(t, "hp", 1).goldCost * T.RankOf(2).costMult), TrainingRules.Cost(Db, d, t, 0), "비용 × 1.6^(랭크−1)");
|
|||
|
|
d.gold = 100000000;
|
|||
|
|
for (int i = 0; i < 3; i++) TrainingRules.Level(Db, d, t, 1);
|
|||
|
|
|
|||
|
|
// §3-2: 그 병종 영웅·병사 전원 × (1 + train(항목)) · 다른 병종 = 0
|
|||
|
|
HeroDb.Hero same = null, other = null;
|
|||
|
|
foreach (var h in Db.Heroes.heroes)
|
|||
|
|
{
|
|||
|
|
if (h.isSoldier) continue;
|
|||
|
|
if (h.type == t && same == null) same = h;
|
|||
|
|
if (h.type != t && other == null) other = h;
|
|||
|
|
}
|
|||
|
|
var hs = new HeroState { id = same.id, lv = 1, grade = Db.Promotion.FirstGrade, star = 1 };
|
|||
|
|
var st = HeroRules.Compute(Db, Cfg, same, hs, d);
|
|||
|
|
var b = Cfg.GetStats(same.type);
|
|||
|
|
Assert.AreEqual(b.maxHp * same.hpMult * st.Core * (1f + TrainingRules.Bonus(Db, d, t, "hp") / 100f), st.hp, 1e-3f);
|
|||
|
|
Assert.AreEqual(b.atk * same.atkMult * st.Core * (1f + (T.RankOf(2).rankPct + T.Get(t, "atk", 3).statPct) / 100f), st.atk, 1e-3f);
|
|||
|
|
var so = HeroRules.Compute(Db, Cfg, other, new HeroState { id = other.id, lv = 1, grade = Db.Promotion.FirstGrade, star = 1 }, d);
|
|||
|
|
Assert.AreEqual(0f, so.trainHpPct + so.trainPct + so.trainDefPct, "다른 병종 = 훈련 없음");
|
|||
|
|
d.Train(t).rank = T.MaxRank;
|
|||
|
|
Assert.AreEqual("최종 랭크", TrainingRules.CanPromote(Db, d, t));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- 순찰 ----------
|
|||
|
|
|
|||
|
|
[Test]
|
|||
|
|
public void Patrol_AccumulateOfflineCap_Harvest_Ad_Instant_DevCap()
|
|||
|
|
{
|
|||
|
|
var e = Db.Economy;
|
|||
|
|
var d = NewSave();
|
|||
|
|
d.chapterMax = 3;
|
|||
|
|
var row = Db.Patrol.For(3);
|
|||
|
|
Assert.AreEqual(3f, PatrolRules.Hours(Db, d, T0.AddHours(3)), 1e-3f);
|
|||
|
|
Assert.AreEqual(e.PatrolCapHours, PatrolRules.Hours(Db, d, T0.AddHours(1000)), 1e-3f, "오프라인 = 상한에서 멈춤");
|
|||
|
|
Assert.AreEqual(Mathf.RoundToInt(row.goldPerHour * 3f), PatrolRules.Preview(Db, d, 3f, 1f).gold);
|
|||
|
|
|
|||
|
|
var rng = new System.Random(4);
|
|||
|
|
var ids = new List<string>(e.StartHeroes);
|
|||
|
|
int gold = d.gold, exp = d.Hero(ids[0]).exp;
|
|||
|
|
string why;
|
|||
|
|
var y = PatrolRules.Harvest(Db, d, T0.AddHours(5), ids, false, rng, out why);
|
|||
|
|
Assert.IsNull(why);
|
|||
|
|
Assert.AreEqual(gold + Mathf.RoundToInt(row.goldPerHour * 5f), d.gold, "골드 = 시간당 × 시간");
|
|||
|
|
Assert.AreEqual(exp + Mathf.RoundToInt(row.expPerHour * 5f), d.Hero(ids[0]).exp, "경험 = 출전 편성");
|
|||
|
|
int chests = Mathf.FloorToInt(row.chestPerHour * 5f + 1e-4f);
|
|||
|
|
Assert.AreEqual(chests, y.chests);
|
|||
|
|
Assert.AreEqual(chests, TreasureRules.ChestCount(d, e.PatrolChest));
|
|||
|
|
Assert.AreEqual(row.chestPerHour * 5f - chests, d.patrol.carryChest, 1e-3f, "소수점 이월");
|
|||
|
|
Assert.AreEqual(Mathf.FloorToInt(e.PatrolGearPerHour * 5f + 1e-4f), y.gear);
|
|||
|
|
Assert.AreEqual(1, TreasureRules.Counter(d, BattleEvents.Patrols), "순찰 카운터");
|
|||
|
|
Assert.AreEqual(0f, PatrolRules.Hours(Db, d, T0.AddHours(5)), 1e-4f, "수확 = 타이머 리셋");
|
|||
|
|
Assert.IsNull(PatrolRules.Harvest(Db, d, T0.AddHours(5), ids, false, rng, out why));
|
|||
|
|
Assert.IsNotNull(why);
|
|||
|
|
|
|||
|
|
gold = d.gold;
|
|||
|
|
var ad = PatrolRules.Harvest(Db, d, T0.AddHours(7), ids, true, rng, out why);
|
|||
|
|
Assert.AreEqual(e.PatrolAdMult, ad.mult);
|
|||
|
|
Assert.AreEqual(gold + Mathf.RoundToInt(row.goldPerHour * 2f * e.PatrolAdMult), d.gold, "광고 2배");
|
|||
|
|
for (int i = 1; i < e.PatrolAdDaily; i++) PatrolRules.Harvest(Db, d, T0.AddHours(7 + i), ids, true, rng, out why);
|
|||
|
|
Assert.IsNull(PatrolRules.Harvest(Db, d, T0.AddHours(9), ids, true, rng, out why), "광고 일일 상한");
|
|||
|
|
|
|||
|
|
var start = d.patrol.startAt;
|
|||
|
|
d.gem = e.PatrolInstantGem;
|
|||
|
|
var inst = PatrolRules.Instant(Db, d, ids, rng, out why);
|
|||
|
|
Assert.IsNotNull(inst);
|
|||
|
|
Assert.AreEqual(e.PatrolInstantHours, inst.hours, 1e-4f);
|
|||
|
|
Assert.AreEqual(0, d.gem, "즉시 수확 = 보옥");
|
|||
|
|
Assert.AreEqual(start, d.patrol.startAt, "즉시 수확은 타이머 그대로");
|
|||
|
|
Assert.IsNull(PatrolRules.Instant(Db, d, ids, rng, out why), "보옥 부족");
|
|||
|
|
|
|||
|
|
var cap = NodeWith(DevRules.PatrolCap);
|
|||
|
|
Counts.Set(d.dev.nodes, cap.id, 2);
|
|||
|
|
Assert.AreEqual(e.PatrolCapHours + cap.pctPerLv * 2f / 60f, PatrolRules.CapHours(Db, d), 1e-4f, "농업 순찰 상한 + 분");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- 개발 ----------
|
|||
|
|
|
|||
|
|
[Test]
|
|||
|
|
public void Dev_RequiresCostBlueprint_EffectsApplied_Reset()
|
|||
|
|
{
|
|||
|
|
var e = Db.Economy;
|
|||
|
|
var d = NewSave();
|
|||
|
|
d.gold = 100000000;
|
|||
|
|
d.gem = e.DevResetGem;
|
|||
|
|
d.inv.blueprint = 1000;
|
|||
|
|
DevTree.Node root = null, child = null;
|
|||
|
|
foreach (var n in Db.Dev.nodes)
|
|||
|
|
{
|
|||
|
|
if (root == null && n.requires.Count == 0) root = n;
|
|||
|
|
else if (root != null && child == null && n.requires.Count == 1 && n.requires[0].node == root.id) child = n;
|
|||
|
|
}
|
|||
|
|
StringAssert.Contains("선행", DevRules.CanUpgrade(Db, d, child.id));
|
|||
|
|
int g0, b0;
|
|||
|
|
DevRules.NextCost(root, 0, out g0, out b0);
|
|||
|
|
Assert.AreEqual(root.goldBase, g0, "Lv1 = gold_base");
|
|||
|
|
int gold = d.gold, bp = d.inv.blueprint, bpSpent = 0;
|
|||
|
|
for (int lv = 0; lv < root.lvMax; lv++)
|
|||
|
|
{
|
|||
|
|
int g, b;
|
|||
|
|
DevRules.NextCost(root, lv, out g, out b);
|
|||
|
|
Assert.AreEqual(Mathf.RoundToInt(root.goldBase * Mathf.Pow(root.goldGrowth, lv)), g);
|
|||
|
|
Assert.AreEqual(lv + 1 >= root.blueprintFromLv ? root.blueprintPerLv : 0, b, "도면 = blueprint_from_lv 이상");
|
|||
|
|
gold -= g;
|
|||
|
|
bpSpent += b;
|
|||
|
|
Assert.IsNull(DevRules.Upgrade(Db, d, root.id));
|
|||
|
|
}
|
|||
|
|
Assert.AreEqual(gold, d.gold);
|
|||
|
|
Assert.AreEqual(bp - bpSpent, d.inv.blueprint);
|
|||
|
|
Assert.AreEqual("최고 레벨", DevRules.CanUpgrade(Db, d, root.id));
|
|||
|
|
Assert.IsNull(DevRules.CanUpgrade(Db, d, child.id), "선행 충족");
|
|||
|
|
Assert.AreEqual(root.pctPerLv * root.lvMax, DevRules.Mod(Db, d, root.EffectKeys[0]), 1e-4f, "효과 = pct × Lv");
|
|||
|
|
|
|||
|
|
// 적용 지점: 군량 상한 · 군량 회복 · 스테이지 골드 · 상점 가격 · 무료 새로고침
|
|||
|
|
var cap = NodeWith(DevRules.FoodCap);
|
|||
|
|
Counts.Set(d.dev.nodes, cap.id, 3);
|
|||
|
|
Assert.AreEqual(e.StaminaMax + Mathf.RoundToInt(cap.pctPerLv * 3), EconomyRules.StaminaMax(Db, d));
|
|||
|
|
var regen = NodeWith(DevRules.FoodRegen);
|
|||
|
|
Counts.Set(d.dev.nodes, regen.id, 2);
|
|||
|
|
Assert.AreEqual(e.StaminaRegenSec - Mathf.RoundToInt(regen.pctPerLv * 2), EconomyRules.RegenSec(Db, d));
|
|||
|
|
var gg = NodeWith(DevRules.GoldGain);
|
|||
|
|
int ggLv = DevRules.Level(d, gg.id);
|
|||
|
|
var stage = Cfg.stages[0];
|
|||
|
|
var rw = Db.Rewards.For(stage.chapter);
|
|||
|
|
var r = EconomyRules.Compute(Db, d, stage, true, true, 1, false, new System.Random(1));
|
|||
|
|
Assert.AreEqual(Mathf.RoundToInt((rw.goldBase + stage.stage * e.GoldPerStage) * e.FirstClearGoldMult * (1f + gg.pctPerLv * ggLv / 100f)), r.gold, "골드 획득 %");
|
|||
|
|
var disc = NodeWith(DevRules.TradeDiscount);
|
|||
|
|
Counts.Set(d.dev.nodes, disc.id, 5);
|
|||
|
|
EconomyRules.EnsureShop(Db, d, T0, new System.Random(2));
|
|||
|
|
var it = Db.Gear.Get(d.shop.items[0].gearId);
|
|||
|
|
Assert.AreEqual(Mathf.RoundToInt(it.sellGold * e.ShopPriceMult * (1f - disc.pctPerLv * 5 / 100f)), d.shop.items[0].price, "거래 할인");
|
|||
|
|
var market = NodeWith(DevRules.TradeFreeRefresh);
|
|||
|
|
Counts.Set(d.dev.nodes, market.id, 1);
|
|||
|
|
int gem = d.gem;
|
|||
|
|
Assert.AreEqual(Mathf.RoundToInt(market.pctPerLv), EconomyRules.FreeRefreshesLeft(Db, d));
|
|||
|
|
Assert.IsNull(EconomyRules.RefreshShop(Db, d, new System.Random(3)));
|
|||
|
|
Assert.AreEqual(gem, d.gem, "시장 개설 = 무료 새로고침");
|
|||
|
|
|
|||
|
|
d.dev.blueprintSpent = bpSpent;
|
|||
|
|
bp = d.inv.blueprint;
|
|||
|
|
Assert.IsNull(DevRules.Reset(Db, d));
|
|||
|
|
Assert.AreEqual(gem - e.DevResetGem, d.gem, "초기화 = 보옥");
|
|||
|
|
Assert.AreEqual(bp + bpSpent, d.inv.blueprint, "도면 환급");
|
|||
|
|
Assert.AreEqual(0, DevRules.TotalLevels(d));
|
|||
|
|
Assert.AreEqual(e.StaminaMax, EconomyRules.StaminaMax(Db, d));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- 보물 ----------
|
|||
|
|
|
|||
|
|
[Test]
|
|||
|
|
public void Treasure_ChestProb_Duplicates_Enhance_Sets_Conditions_Effects()
|
|||
|
|
{
|
|||
|
|
var e = Db.Economy;
|
|||
|
|
var d = NewSave();
|
|||
|
|
var rng = new System.Random(1093);
|
|||
|
|
var bronze = Db.Chests.Get(e.StageChest);
|
|||
|
|
int n = 4000;
|
|||
|
|
Counts.Set(d.inv.chests, bronze.id, n);
|
|||
|
|
var freq = new int[ChestDb.ProbGrades.Length];
|
|||
|
|
int shards = 0;
|
|||
|
|
for (int i = 0; i < n; i++)
|
|||
|
|
{
|
|||
|
|
var r = TreasureRules.Open(Db, d, bronze.id, rng);
|
|||
|
|
Assert.IsNotNull(r);
|
|||
|
|
freq[Array.IndexOf(ChestDb.ProbGrades, r.grade)]++;
|
|||
|
|
shards += r.shards;
|
|||
|
|
Assert.That(r.gold, Is.InRange(bronze.goldMin, bronze.goldMax));
|
|||
|
|
}
|
|||
|
|
for (int g = 0; g < freq.Length; g++) Assert.AreEqual(bronze.p[g], freq[g] / (float)n, 0.03f, ChestDb.ProbGrades[g] + " 빈도 ≈ 확률");
|
|||
|
|
Assert.Greater(shards, 0, "중복 = 조각");
|
|||
|
|
Assert.AreEqual(0, TreasureRules.ChestCount(d, bronze.id));
|
|||
|
|
Assert.AreEqual(n * bronze.points / e.ChestPointGoal, TreasureRules.ChestCount(d, e.ChestPointReward), "상자 포인트 → 명장 상자");
|
|||
|
|
Assert.AreEqual(n * bronze.points % e.ChestPointGoal, d.inv.chestPoints);
|
|||
|
|
var top = e.TreasureGrades[e.TreasureGrades.Count - 1].Key;
|
|||
|
|
foreach (var t in d.inv.treasures) Assert.AreNotEqual(top, Db.Treasures.Get(t.id).grade, "천기 = 상자에서 안 나옴");
|
|||
|
|
|
|||
|
|
// 강화: 조각 shard_per_star × ★ + 운철 iron_per_star × ★ × 등급 배율 · 효과 = 기본 × (1 + star_pct × ★)
|
|||
|
|
var st = d.inv.treasures[0];
|
|||
|
|
var item = Db.Treasures.Get(st.id);
|
|||
|
|
int ns, ni;
|
|||
|
|
TreasureRules.EnhanceCost(Db, item, 1, out ns, out ni);
|
|||
|
|
Assert.AreEqual(e.TreasureShardPerStar, ns);
|
|||
|
|
Assert.AreEqual(Mathf.RoundToInt(e.TreasureIronPerStar * e.TreasureIronMult(item.grade)), ni);
|
|||
|
|
st.star = 0;
|
|||
|
|
st.shards = ns;
|
|||
|
|
d.inv.iron = ni - 1;
|
|||
|
|
StringAssert.Contains("운철", TreasureRules.CanEnhance(Db, d, st.id));
|
|||
|
|
d.inv.iron = ni;
|
|||
|
|
Assert.IsNull(TreasureRules.Enhance(Db, d, st.id));
|
|||
|
|
Assert.AreEqual(1, st.star);
|
|||
|
|
Assert.AreEqual(0, st.shards);
|
|||
|
|
Assert.AreEqual(0, d.inv.iron);
|
|||
|
|
Assert.AreEqual(item.basePct * (1f + e.TreasureStarPct), TreasureRules.Value(Db, d, item), 1e-4f);
|
|||
|
|
|
|||
|
|
// 조건 효과 = 카운터 cond_every 마다 cond_pct · 최대 cond_max
|
|||
|
|
TreasureRules.AddCount(d, item.condKey, item.condEvery * 2);
|
|||
|
|
Assert.AreEqual(item.basePct * (1f + e.TreasureStarPct) + 2 * item.condPct, TreasureRules.Value(Db, d, item), 1e-4f);
|
|||
|
|
TreasureRules.AddCount(d, item.condKey, item.condEvery * (item.condMax + 5));
|
|||
|
|
Assert.AreEqual(item.condMax, TreasureRules.CondStacks(d, item), "최대 cond_max");
|
|||
|
|
|
|||
|
|
// 세트: 전원 보유 = [세트] · 전원 ★N = [N성] · 가장 높은 단계 1개
|
|||
|
|
var sd = NewSave();
|
|||
|
|
var setId = Db.TreasureSets.SetIds[0];
|
|||
|
|
var tiers = Db.TreasureSets.TiersOf(setId);
|
|||
|
|
Assert.IsNull(TreasureRules.SetTier(Db, sd, setId));
|
|||
|
|
int dummy;
|
|||
|
|
foreach (var m in tiers[0].members) TreasureRules.Grant(sd, Db.Treasures.Get(m), out dummy);
|
|||
|
|
Assert.AreEqual(tiers[0], TreasureRules.SetTier(Db, sd, setId));
|
|||
|
|
foreach (var m in tiers[0].members) sd.inv.Treasure(m).star = tiers[1].stars;
|
|||
|
|
Assert.AreEqual(tiers[1], TreasureRules.SetTier(Db, sd, setId));
|
|||
|
|
string stat = tiers[1].effectKey.Replace("_pct", "");
|
|||
|
|
float expect = tiers[1].pct;
|
|||
|
|
foreach (var it in Db.Treasures.items)
|
|||
|
|
if (it.baseStat == stat && sd.inv.Treasure(it.id) != null) expect += TreasureRules.Value(Db, sd, it);
|
|||
|
|
Assert.AreEqual(expect, TreasureRules.StatPct(Db, sd, stat), 1e-4f, "스탯 % = 보물 + 세트");
|
|||
|
|
|
|||
|
|
// §3-2 공식 = × (1 + 보물(항목)) · 전투 진영 효과(crit·range·splash)
|
|||
|
|
var h = Db.Heroes.Get(e.StartHeroes[0]);
|
|||
|
|
var hs = sd.Hero(h.id);
|
|||
|
|
var s1 = HeroRules.Compute(Db, Cfg, h, hs, sd);
|
|||
|
|
var b = Cfg.GetStats(h.type);
|
|||
|
|
float hpPct = TreasureRules.StatPct(Db, sd, "hp"), atkPct = TreasureRules.StatPct(Db, sd, "atk");
|
|||
|
|
Assert.AreEqual(b.maxHp * h.hpMult * s1.Core * (1f + hpPct / 100f), s1.hp, 1e-3f);
|
|||
|
|
Assert.AreEqual(b.atk * h.atkMult * s1.Core * (1f + atkPct / 100f), s1.atk, 1e-3f);
|
|||
|
|
foreach (var it in Db.Treasures.items)
|
|||
|
|
if (sd.inv.Treasure(it.id) == null) TreasureRules.Grant(sd, it, out dummy);
|
|||
|
|
var fx = TreasureRules.BattleEffects(Db, sd);
|
|||
|
|
var keys = new Dictionary<string, float>();
|
|||
|
|
foreach (var x in fx) keys[x.Key] = x.P;
|
|||
|
|
Assert.AreEqual(TreasureRules.StatPct(Db, sd, "crit") / 100f, keys[SkillMods.SideCritPct], 1e-5f);
|
|||
|
|
Assert.AreEqual(1f + TreasureRules.StatPct(Db, sd, "range") / 100f, keys[SkillKeys.RangeMult], 1e-5f);
|
|||
|
|
Assert.AreEqual(1f + TreasureRules.StatPct(Db, sd, "splash") / 100f, keys[SkillKeys.SplashRadiusMult], 1e-5f);
|
|||
|
|
|
|||
|
|
// 스테이지 청동 상자 stage_chest_pct · 거래 구매(보옥 · 포인트)
|
|||
|
|
int got = 0, m2 = 2000;
|
|||
|
|
var rs = new System.Random(9);
|
|||
|
|
for (int i = 0; i < m2; i++)
|
|||
|
|
if (EconomyRules.Compute(Db, d, Cfg.stages[0], true, true, 1, false, rs).chest == e.StageChest) got++;
|
|||
|
|
Assert.AreEqual(e.StageChestPct, got / (float)m2, 0.03f, "스테이지 상자 확률");
|
|||
|
|
ChestDb.Chest gemBuy = null;
|
|||
|
|
foreach (var c in Db.Chests.chests)
|
|||
|
|
if (c.buyGem > 0 && c.buyPoints > 0) gemBuy = c;
|
|||
|
|
int before = TreasureRules.ChestCount(d, gemBuy.id);
|
|||
|
|
d.gem = gemBuy.buyGem;
|
|||
|
|
Assert.IsNull(TreasureRules.Buy(Db, d, gemBuy.id, "gem"));
|
|||
|
|
Assert.AreEqual(0, d.gem);
|
|||
|
|
d.inv.chestPoints = gemBuy.buyPoints;
|
|||
|
|
Assert.IsNull(TreasureRules.Buy(Db, d, gemBuy.id, "points"));
|
|||
|
|
Assert.AreEqual(0, d.inv.chestPoints);
|
|||
|
|
Assert.AreEqual(before + 2, TreasureRules.ChestCount(d, gemBuy.id));
|
|||
|
|
Assert.IsNotNull(TreasureRules.Buy(Db, d, gemBuy.id, "gem"), "보옥 부족");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- 연무장 ----------
|
|||
|
|
|
|||
|
|
[Test]
|
|||
|
|
public void Tower_TicketsFloorRewards_Ad_Daily()
|
|||
|
|
{
|
|||
|
|
var e = Db.Economy;
|
|||
|
|
var d = NewSave();
|
|||
|
|
Assert.AreEqual(e.TowerTicketsDaily, d.arena.towerTickets, "하루 입장권");
|
|||
|
|
Assert.AreEqual(1, ArenaRules.TowerNext(Db, d));
|
|||
|
|
var f1 = Db.Tower.Get(1);
|
|||
|
|
var src = ArenaRules.FindStage(Cfg, f1.templateStage);
|
|||
|
|
var st = ArenaRules.TowerStage(Db, Cfg, 1);
|
|||
|
|
Assert.AreEqual(src.enemies.Count, st.enemies.Count);
|
|||
|
|
Assert.AreEqual(src.enemies[0].hpMult * f1.hpMult, st.enemies[0].hpMult, 1e-4f, "층 hp 배율");
|
|||
|
|
Assert.AreEqual(src.enemies[0].atkMult * f1.atkMult, st.enemies[0].atkMult, 1e-4f);
|
|||
|
|
Assert.IsNull(ArenaRules.EnterTower(Db, d, T0));
|
|||
|
|
Assert.AreEqual(e.TowerTicketsDaily - 1, d.arena.towerTickets, "입장 = 1장");
|
|||
|
|
ArenaRules.TowerResult(Db, d, 1, false, new System.Random(1));
|
|||
|
|
Assert.AreEqual(0, d.arena.towerFloor, "패배 = 층 유지");
|
|||
|
|
int gem = d.gem, bp = d.inv.blueprint;
|
|||
|
|
ArenaRules.TowerResult(Db, d, 1, true, new System.Random(1));
|
|||
|
|
Assert.AreEqual(1, d.arena.towerFloor);
|
|||
|
|
Assert.AreEqual(gem + f1.rewardGem, d.gem);
|
|||
|
|
Assert.AreEqual(bp + f1.rewardBlueprint, d.inv.blueprint);
|
|||
|
|
TowerTable.Floor chestFloor = null;
|
|||
|
|
foreach (var f in Db.Tower.floors)
|
|||
|
|
if (chestFloor == null && f.chest.Length > 0) chestFloor = f;
|
|||
|
|
d.arena.towerFloor = chestFloor.floor - 1;
|
|||
|
|
ArenaRules.TowerResult(Db, d, chestFloor.floor, true, new System.Random(1));
|
|||
|
|
Assert.AreEqual(1, TreasureRules.ChestCount(d, chestFloor.chest), chestFloor.floor + "층 상자");
|
|||
|
|
d.arena.towerTickets = 0;
|
|||
|
|
Assert.IsNotNull(ArenaRules.CanTower(Db, d, T0), "입장권 없음");
|
|||
|
|
Assert.IsNull(ArenaRules.TowerAd(Db, d, T0));
|
|||
|
|
Assert.AreEqual(e.TowerAdTickets, d.arena.towerTickets);
|
|||
|
|
Assert.IsNotNull(ArenaRules.TowerAd(Db, d, T0), "광고 하루 1");
|
|||
|
|
ArenaRules.RollDaily(Db, d, T0.AddDays(1));
|
|||
|
|
Assert.AreEqual(Mathf.Max(e.TowerAdTickets, e.TowerTicketsDaily), d.arena.towerTickets, "다음 날 채움");
|
|||
|
|
d.arena.towerFloor = Db.Tower.Top;
|
|||
|
|
Assert.IsNotNull(ArenaRules.CanTower(Db, d, T0.AddDays(1)), "정복 완료");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[Test]
|
|||
|
|
public void Dungeon_WeekdayEntries_Rewards()
|
|||
|
|
{
|
|||
|
|
foreach (var dg in Db.Dungeons.dungeons)
|
|||
|
|
{
|
|||
|
|
var d = NewSave();
|
|||
|
|
DateTime open = DateTime.MinValue, closed = DateTime.MinValue;
|
|||
|
|
for (int k = 0; k < 7; k++)
|
|||
|
|
{
|
|||
|
|
var t = T0.AddDays(k);
|
|||
|
|
if (ArenaRules.OpenToday(dg, t)) { if (open == DateTime.MinValue) open = t; }
|
|||
|
|
else if (closed == DateTime.MinValue) closed = t;
|
|||
|
|
}
|
|||
|
|
Assert.AreNotEqual(DateTime.MinValue, open, dg.id + " 열리는 요일");
|
|||
|
|
if (closed != DateTime.MinValue) StringAssert.Contains("닫힘", ArenaRules.CanDungeon(Db, d, dg.id, closed));
|
|||
|
|
Assert.AreEqual(dg.entriesPerDay, ArenaRules.DungeonLeft(Db, d, dg, open));
|
|||
|
|
Assert.AreEqual(ArenaRules.FindStage(Cfg, dg.templateStage).enemies[0].hpMult * dg.hpMult, ArenaRules.DungeonStage(Db, Cfg, dg.id).enemies[0].hpMult, 1e-4f, "hp_mult");
|
|||
|
|
ArenaRules.DungeonResult(Db, d, dg.id, false, open, new System.Random(1));
|
|||
|
|
Assert.AreEqual(dg.entriesPerDay, ArenaRules.DungeonLeft(Db, d, dg, open), "패배 = 횟수 유지");
|
|||
|
|
int gold = d.gold, iron = d.inv.iron, books = d.inv.books.skill, shards = 0, gear = 0;
|
|||
|
|
foreach (var h in d.heroes) shards += h.shards;
|
|||
|
|
foreach (var c in d.inv.gear) gear += c.n;
|
|||
|
|
ArenaRules.DungeonResult(Db, d, dg.id, true, open, new System.Random(1));
|
|||
|
|
Assert.AreEqual(dg.entriesPerDay - 1, ArenaRules.DungeonLeft(Db, d, dg, open));
|
|||
|
|
int shards2 = 0, gear2 = 0;
|
|||
|
|
foreach (var h in d.heroes) shards2 += h.shards;
|
|||
|
|
foreach (var c in d.inv.gear) gear2 += c.n;
|
|||
|
|
Assert.AreEqual(gold + dg.Amount("gold"), d.gold, dg.id + " 골드");
|
|||
|
|
Assert.AreEqual(iron + dg.Amount("iron"), d.inv.iron, dg.id + " 운철");
|
|||
|
|
Assert.AreEqual(books + dg.Amount("skill_book"), d.inv.books.skill, dg.id + " 스킬서");
|
|||
|
|
Assert.AreEqual(shards + dg.Amount("shards"), shards2, dg.id + " 조각");
|
|||
|
|
Assert.AreEqual(gear + dg.Amount("gear"), gear2, dg.id + " 장비");
|
|||
|
|
for (int i = 1; i < dg.entriesPerDay; i++) ArenaRules.DungeonResult(Db, d, dg.id, true, open, new System.Random(1));
|
|||
|
|
StringAssert.Contains("완료", ArenaRules.CanDungeon(Db, d, dg.id, open));
|
|||
|
|
Assert.IsNull(ArenaRules.CanDungeon(Db, d, dg.id, open.AddDays(7)), "다음 주 같은 요일 = 다시");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[Test]
|
|||
|
|
public void Arena_SamplesMedalsRankSeason_Shop()
|
|||
|
|
{
|
|||
|
|
var A = Db.Arena;
|
|||
|
|
var d = NewSave();
|
|||
|
|
var rng = new System.Random(5);
|
|||
|
|
ArenaRules.EnsureSeason(Db, d, T0, rng);
|
|||
|
|
Assert.AreEqual(A.SampleLineups, d.arena.samples.Count, "샘플 상대");
|
|||
|
|
foreach (var s in d.arena.samples)
|
|||
|
|
{
|
|||
|
|
Assert.AreEqual(Cfg.playerSpawnPoints.Length, s.heroes.Count);
|
|||
|
|
foreach (var id in s.heroes) Assert.IsFalse(Db.Heroes.Get(id).isSoldier);
|
|||
|
|
}
|
|||
|
|
var st = ArenaRules.ArenaStage(Db, Cfg, d.arena.samples[0]);
|
|||
|
|
Assert.AreEqual(Cfg.playerSpawnPoints.Length, st.enemies.Count);
|
|||
|
|
for (int i = 0; i < st.enemies.Count; i++)
|
|||
|
|
{
|
|||
|
|
Assert.AreEqual(-Cfg.playerSpawnPoints[i].y, st.enemies[i].position.y, 1e-4f, "자리 = 위아래 반전");
|
|||
|
|
Assert.IsTrue(st.enemies[i].isGeneral);
|
|||
|
|
}
|
|||
|
|
Assert.AreEqual(d.arena.samples.Count + 1, ArenaRules.Rank(d), "점수 0 = 꼴찌");
|
|||
|
|
ArenaRules.ArenaResult(Db, d, 0, true, T0);
|
|||
|
|
ArenaRules.ArenaResult(Db, d, 1, false, T0);
|
|||
|
|
Assert.AreEqual(A.MedalWin + A.MedalLose, d.arena.medals);
|
|||
|
|
Assert.AreEqual(1, d.arena.wins);
|
|||
|
|
Assert.AreEqual(1, d.arena.losses);
|
|||
|
|
for (int i = 2; i < A.EntriesPerDay; i++) ArenaRules.ArenaResult(Db, d, 0, true, T0);
|
|||
|
|
Assert.IsNotNull(ArenaRules.CanArena(Db, d, 0, T0), "하루 상한");
|
|||
|
|
Assert.IsNull(ArenaRules.CanArena(Db, d, 0, T0.AddDays(1)), "다음 날");
|
|||
|
|
int topScore = 0;
|
|||
|
|
foreach (var s in d.arena.samples) topScore = Mathf.Max(topScore, s.score);
|
|||
|
|
d.arena.seasonScore = topScore + 1;
|
|||
|
|
Assert.AreEqual(1, ArenaRules.Rank(d));
|
|||
|
|
int gem = d.gem;
|
|||
|
|
ArenaRules.EnsureSeason(Db, d, T0.AddDays(A.SeasonDays), rng);
|
|||
|
|
Assert.AreEqual(gem + A.RewardTop10Gem, d.gem, "시즌 1위 보상");
|
|||
|
|
Assert.AreEqual(0, d.arena.seasonScore, "새 시즌");
|
|||
|
|
Assert.AreEqual(A.SampleLineups, d.arena.samples.Count);
|
|||
|
|
|
|||
|
|
var hid = d.heroes[0].id;
|
|||
|
|
int sh = d.heroes[0].shards;
|
|||
|
|
d.arena.medals = A.ShopPrice;
|
|||
|
|
Assert.IsNull(ArenaRules.BuyShards(Db, d, hid, T0.AddDays(A.SeasonDays)));
|
|||
|
|
Assert.AreEqual(sh + A.ShopShards, d.Hero(hid).shards, "메달 → 조각");
|
|||
|
|
Assert.AreEqual(0, d.arena.medals);
|
|||
|
|
StringAssert.Contains("메달", ArenaRules.BuyShards(Db, d, hid, T0.AddDays(A.SeasonDays)));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[Test]
|
|||
|
|
public void Raid_WeeklyBoss_Accumulate_Daily()
|
|||
|
|
{
|
|||
|
|
var e = Db.Economy;
|
|||
|
|
var d = NewSave();
|
|||
|
|
var boss = ArenaRules.RaidBoss(Db, d, T0);
|
|||
|
|
Assert.Contains(boss, e.RaidBosses);
|
|||
|
|
if (e.RaidBosses.Length > 1) Assert.AreNotEqual(boss, ArenaRules.RaidBoss(Db, NewSave(), T0.AddDays(7)), "주간 순환");
|
|||
|
|
var h = Db.Heroes.Get(boss);
|
|||
|
|
var st = ArenaRules.RaidStage(Db, Cfg, boss);
|
|||
|
|
BalanceConfig.EnemySpawn b = null, srcB = null;
|
|||
|
|
foreach (var x in st.enemies)
|
|||
|
|
if (x.isBoss && x.art == h.art) b = x;
|
|||
|
|
Assert.IsNotNull(b, "보스 = 그 영웅 아트");
|
|||
|
|
foreach (var s in Cfg.stages)
|
|||
|
|
foreach (var x in s.enemies)
|
|||
|
|
if (srcB == null && x.isBoss && x.art == h.art) srcB = x;
|
|||
|
|
if (srcB != null) Assert.AreEqual(srcB.hpMult * e.RaidHpMult, b.hpMult, 1e-4f, "보스 hp × raid_hp_mult");
|
|||
|
|
ArenaRules.RaidResult(Db, d, boss, 500, false, T0, new System.Random(1));
|
|||
|
|
Assert.AreEqual(500, d.arena.raidDamage);
|
|||
|
|
Assert.AreEqual(1, TreasureRules.ChestCount(d, e.RaidChest), "명장 상자");
|
|||
|
|
Assert.IsNotNull(ArenaRules.CanRaid(Db, d, T0), "하루 상한");
|
|||
|
|
var sameWeek = T0;
|
|||
|
|
for (int k = 1; k < 7; k++)
|
|||
|
|
if (ArenaRules.WeekKey(T0.AddDays(k)) == ArenaRules.WeekKey(T0)) sameWeek = T0.AddDays(k);
|
|||
|
|
if (sameWeek != T0)
|
|||
|
|
{
|
|||
|
|
Assert.IsNull(ArenaRules.CanRaid(Db, d, sameWeek));
|
|||
|
|
ArenaRules.RaidResult(Db, d, boss, 300, false, sameWeek, new System.Random(1));
|
|||
|
|
Assert.AreEqual(800, d.arena.raidDamage, "같은 주 누적");
|
|||
|
|
}
|
|||
|
|
ArenaRules.RaidBoss(Db, d, T0.AddDays(7));
|
|||
|
|
Assert.AreEqual(0, d.arena.raidDamage, "주가 바뀌면 초기화");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- 씬(화면 · 모드 전투) ----------
|
|||
|
|
|
|||
|
|
OutGameRoot og;
|
|||
|
|
BattleManager bm;
|
|||
|
|
|
|||
|
|
IEnumerator Boot()
|
|||
|
|
{
|
|||
|
|
WorldProgress.Reset();
|
|||
|
|
SaveStore.UseMemory(true);
|
|||
|
|
SceneManager.LoadScene("Battle");
|
|||
|
|
yield return null;
|
|||
|
|
yield return null;
|
|||
|
|
og = OutGameRoot.Instance;
|
|||
|
|
bm = BattleManager.Instance;
|
|||
|
|
Assert.IsNotNull(og);
|
|||
|
|
og.ShowLobby();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[UnityTest]
|
|||
|
|
public IEnumerator Screens_Domestic_Treasure_Arena_TradeChest()
|
|||
|
|
{
|
|||
|
|
yield return Boot();
|
|||
|
|
var s = og.Session;
|
|||
|
|
og.OnTab(2);
|
|||
|
|
Assert.AreEqual(og.Domestic, og.Current);
|
|||
|
|
for (int p = 0; p < DomesticScreen.PageNames.Length; p++)
|
|||
|
|
{
|
|||
|
|
og.Domestic.SetPage(p);
|
|||
|
|
yield return null;
|
|||
|
|
}
|
|||
|
|
Assert.AreEqual(s.Db.Dev.InBranch(og.Domestic.Branch).Count, og.Domestic.NodeCount, "개발 트리 노드");
|
|||
|
|
og.Domestic.SetBranch(1);
|
|||
|
|
yield return null;
|
|||
|
|
Assert.AreEqual(s.Db.Dev.InBranch(s.Db.Dev.Branches[1]).Count, og.Domestic.NodeCount, "갈래 ▶");
|
|||
|
|
og.Domestic.NodePopup(s.Db.Dev.InBranch(og.Domestic.Branch)[0].id);
|
|||
|
|
Assert.IsTrue(og.ModalVisible, "노드 탭 = 강화 팝업");
|
|||
|
|
og.CloseModal();
|
|||
|
|
|
|||
|
|
og.OnTab(1);
|
|||
|
|
Assert.AreEqual(og.Treasures, og.Current);
|
|||
|
|
og.Treasures.SetPage(0);
|
|||
|
|
yield return null;
|
|||
|
|
Assert.AreEqual(s.Db.Treasures.items.Count, og.Treasures.GridCount, "보물 칸 = 전체(미보유 회색)");
|
|||
|
|
og.Treasures.SetPage(1);
|
|||
|
|
yield return null;
|
|||
|
|
Assert.AreEqual(s.Db.TreasureSets.SetIds.Count, og.Treasures.SetCount, "세트 묶음");
|
|||
|
|
Counts.Set(s.Save.inv.chests, s.Db.Economy.StageChest, 1);
|
|||
|
|
og.Treasures.SetPage(2);
|
|||
|
|
og.Treasures.SelectChest(s.Db.Economy.StageChest);
|
|||
|
|
yield return null;
|
|||
|
|
og.Treasures.DoOpen(s.Db.Economy.StageChest);
|
|||
|
|
Assert.IsNotNull(og.Treasures.LastOpen, "열기");
|
|||
|
|
Assert.AreEqual(1, s.Save.inv.treasures.Count, "첫 보물");
|
|||
|
|
og.CloseModal();
|
|||
|
|
|
|||
|
|
og.OnTab(4);
|
|||
|
|
og.Trade.SetPage(1);
|
|||
|
|
yield return null;
|
|||
|
|
Assert.AreEqual(s.Db.Chests.chests.Count, og.Trade.ChestRowCount, "거래 > 상자");
|
|||
|
|
|
|||
|
|
og.ShowLobby();
|
|||
|
|
og.Arena.SetPage(0);
|
|||
|
|
og.Show(og.Arena);
|
|||
|
|
yield return null;
|
|||
|
|
Assert.AreEqual(og.Arena, og.Current);
|
|||
|
|
Assert.AreEqual(s.Db.Arena.SampleLineups, s.Save.arena.samples.Count, "연무장 진입 = 시즌·샘플");
|
|||
|
|
for (int p = 1; p <= 4; p++)
|
|||
|
|
{
|
|||
|
|
og.Arena.SetPage(p);
|
|||
|
|
yield return null;
|
|||
|
|
}
|
|||
|
|
og.Back();
|
|||
|
|
Assert.AreEqual(0, og.Arena.Page, "모드 ◀ = 카드");
|
|||
|
|
og.Back();
|
|||
|
|
Assert.IsTrue(og.LobbyVisible);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[UnityTest]
|
|||
|
|
public IEnumerator ModeBattle_Tower_Raid_Result_Return_Counters_SideEffects()
|
|||
|
|
{
|
|||
|
|
yield return Boot();
|
|||
|
|
var s = og.Session;
|
|||
|
|
Time.timeScale = 20f;
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// 탑 1층: 입장권 1장 → 모드 전투(층 배율 적) → 적 전멸 = 승리 → 결과 패널 → 탭 = 연무장 탑 페이지
|
|||
|
|
Assert.IsNull(s.Do(() => ArenaRules.EnterTower(s.Db, s.Save, s.Now)));
|
|||
|
|
var stage = ArenaRules.TowerStage(s.Db, bm.Config, 1);
|
|||
|
|
og.StartMode(ArenaRules.Tower, "", 1, stage, 0, 1);
|
|||
|
|
yield return null;
|
|||
|
|
Assert.IsNotNull(bm.Mode);
|
|||
|
|
Assert.AreEqual(BattlePhase.PlayerTurn, bm.Phase);
|
|||
|
|
Assert.AreEqual(stage.enemies.Count, bm.AliveCount(Faction.Enemy));
|
|||
|
|
Assert.Greater(bm.AliveCount(Faction.Player), 0, "아군 = 출전 편성");
|
|||
|
|
|
|||
|
|
// 전투 이벤트 카운터: 아군만 센다
|
|||
|
|
var p0 = bm.AliveUnits(Faction.Player)[0];
|
|||
|
|
int au = TreasureRules.Counter(s.Save, BattleEvents.ActiveUses);
|
|||
|
|
BattleEvents.Raise(BattleEvents.ActiveUses, p0);
|
|||
|
|
BattleEvents.Raise(BattleEvents.ActiveUses, bm.AliveUnits(Faction.Enemy)[0]);
|
|||
|
|
Assert.AreEqual(au + 1, TreasureRules.Counter(s.Save, BattleEvents.ActiveUses), "적 행동은 세지 않음");
|
|||
|
|
|
|||
|
|
int battles = TreasureRules.Counter(s.Save, BattleEvents.Battles);
|
|||
|
|
foreach (var u in bm.AliveUnits(Faction.Enemy)) u.TakeDamage(u.Hp);
|
|||
|
|
bm.CheckBattleEnd();
|
|||
|
|
yield return null;
|
|||
|
|
Assert.AreEqual(BattlePhase.StageClear, bm.Phase);
|
|||
|
|
Assert.IsTrue(og.ResultVisible, "결과 패널");
|
|||
|
|
Assert.AreEqual(1, s.Save.arena.towerFloor, "탑 1층 클리어");
|
|||
|
|
Assert.IsTrue(s.LastMode.win);
|
|||
|
|
Assert.AreEqual(battles + 1, TreasureRules.Counter(s.Save, BattleEvents.Battles), "전투 카운터");
|
|||
|
|
bm.ContinueAfterResult();
|
|||
|
|
yield return null;
|
|||
|
|
Assert.IsNull(bm.Mode);
|
|||
|
|
Assert.AreEqual(BattlePhase.Setup, bm.Phase);
|
|||
|
|
Assert.AreEqual(og.Arena, og.Current, "탭 = 연무장");
|
|||
|
|
Assert.AreEqual(1, og.Arena.Page, "탑 페이지로");
|
|||
|
|
|
|||
|
|
// 적장 토벌: 제한 1턴 → 적 턴을 마치면 끝 · 보스에게 준 피해 누적
|
|||
|
|
var boss = ArenaRules.RaidBoss(s.Db, s.Save, s.Now);
|
|||
|
|
og.StartMode(ArenaRules.Raid, boss, 0, ArenaRules.RaidStage(s.Db, bm.Config, boss), 1, 4);
|
|||
|
|
yield return null;
|
|||
|
|
Unit bu = null;
|
|||
|
|
foreach (var u in bm.AliveUnits(Faction.Enemy))
|
|||
|
|
if (u.IsBoss) bu = u;
|
|||
|
|
Assert.IsNotNull(bu);
|
|||
|
|
bu.TakeDamage(100);
|
|||
|
|
yield return bm.RunEnemyTurn();
|
|||
|
|
Assert.AreEqual(BattlePhase.Defeat, bm.Phase, "제한 턴 = 끝");
|
|||
|
|
yield return null;
|
|||
|
|
int dealt = 0;
|
|||
|
|
foreach (var u in bm.Units)
|
|||
|
|
if (u != null && u.Faction == Faction.Enemy && u.IsBoss) dealt += u.MaxHp - u.Hp;
|
|||
|
|
Assert.GreaterOrEqual(dealt, 100);
|
|||
|
|
Assert.AreEqual(dealt, s.Save.arena.raidDamage, "보스 피해 누적");
|
|||
|
|
Assert.GreaterOrEqual(TreasureRules.ChestCount(s.Save, s.Db.Economy.RaidChest), 1, "명장 상자");
|
|||
|
|
bm.ContinueAfterResult();
|
|||
|
|
yield return null;
|
|||
|
|
Assert.AreEqual(4, og.Arena.Page);
|
|||
|
|
|
|||
|
|
// 보물 crit = 진영 상시 효과 → 크리 배율
|
|||
|
|
TreasureDb.Item crit = null;
|
|||
|
|
foreach (var it in s.Db.Treasures.items)
|
|||
|
|
if (crit == null && it.baseStat == "crit") crit = it;
|
|||
|
|
int dummy;
|
|||
|
|
TreasureRules.Grant(s.Save, crit, out dummy);
|
|||
|
|
bm.StartStage(0);
|
|||
|
|
yield return null;
|
|||
|
|
var pu = bm.AliveUnits(Faction.Player)[0];
|
|||
|
|
Assert.AreEqual(bm.Config.critMultiplier * (1f + TreasureRules.StatPct(s.Db, s.Save, "crit") / 100f), SkillMods.CritMult(pu, bm.Config), 1e-4f, "보물 크리");
|
|||
|
|
Assert.AreEqual(bm.Config.critMultiplier, SkillMods.CritMult(bm.AliveUnits(Faction.Enemy)[0], bm.Config), 1e-4f, "적 = 미적용");
|
|||
|
|
}
|
|||
|
|
finally
|
|||
|
|
{
|
|||
|
|
Time.timeScale = 1f;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|