466 lines
38 KiB
C#
466 lines
38 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Numerics;
|
||
using UnityEngine;
|
||
using VS.Sim;
|
||
using Vector2 = UnityEngine.Vector2;
|
||
|
||
namespace VS.EditorTools
|
||
{
|
||
// 전투 모드 검사 (battle.json 설계값 기준)
|
||
public static partial class HeadlessRun
|
||
{
|
||
/// 자동 플레이: 열린 칸을 차례로 돌며 마나가 그 카드 값 이상(+ threshold 까지 모은 뒤)이면 바로 소환(아래 가운데) — 여러 종류가 섞인 무리
|
||
static Battle AutoBattle(Game g, int stage, int slots, int maxMana, float threshold, out float time, int seed = 3, BattleSetup setup = null)
|
||
{
|
||
var b = new Battle(g.BC, stage, maxMana, seed, setup);
|
||
b.Start();
|
||
time = 0; int next = 0;
|
||
while (b.State == BattleState.Fighting && time < 400)
|
||
{
|
||
var d = g.BattleUnit(next % slots);
|
||
if (d != null && b.Mana >= Mathf.Max(d.cost, Mathf.Min(threshold, maxMana))) { b.Deploy(d); next++; }
|
||
b.Tick(0.05f); time += 0.05f;
|
||
}
|
||
return b;
|
||
}
|
||
|
||
static List<string> L(params string[] s) => new List<string>(s);
|
||
/// 맵 고정: 강 없는 평지(또는 mapType) · 장애물 없음 · 성 x = 9
|
||
static BattleSetup Fixed(List<string> shots, List<string> gimmicks, int mapType = 0) => new BattleSetup { mapType = mapType, obstacles = 0, castleX = 9f, shots = shots, gimmicks = gimmicks };
|
||
/// 시험 유닛: 체력 100000 · 공격 0 · speed 0 = 제자리
|
||
static BUnitDef Dummy(float speed = 0f) => new BUnitDef { id = 1, name = "시험", cost = 0, hp = 100000f, damage = 0f, interval = 99f, range = 0.1f, speed = speed, radius = 0.45f };
|
||
static BUnit Put(Battle b, BUnitDef d, float x, float y) { b.Deploy(d); var u = b.Units[b.Units.Count - 1]; u.pos = new Vector2(x, y); return u; }
|
||
static bool TickUntil(Battle b, Func<bool> cond, float sec) { for (float t = 0; t < sec; t += 0.05f) { b.Tick(0.05f); if (cond()) return true; } return false; }
|
||
/// 성(x = 9) 바로 아래 줄에서 시험 유닛(반지름 0.45)이 성 가장자리로부터 edge 칸 떨어지는 y
|
||
static float InY(BattleCfg c, float edge) => c.castle.y - c.castle.radius - 0.45f - edge;
|
||
|
||
static void BattleTests(Tables t)
|
||
{
|
||
var c = t.Battle;
|
||
Check("B1.config", c != null && c.units.Count == 6 && c.units[0].cost == 1 && c.slotStart == 1 && c.slotMax == 6 && c.manaMaxBase == 10);
|
||
var g = NewGame(t, out _);
|
||
Check("B2.initial", g.BattleSlots == 1 && g.BattleMaxMana == 10 && g.BattleStage == 1);
|
||
var b = g.NewBattle();
|
||
Check("B2.castle", Math.Abs(b.CastleMaxHp - c.castle.hp) < 0.5f && Math.Abs(b.Mana - c.manaStart) < 0.001f && b.State == BattleState.Ready);
|
||
// B3 마나 충전 · 상한 · 마지막 1분 2배
|
||
b.Start();
|
||
for (int i = 0; i < 20; i++) b.Tick(0.05f);
|
||
Check("B3.regen", Math.Abs(b.Mana - (c.manaStart + 1f / c.manaRegenSeconds)) < 0.02f, b.Mana.ToString("0.000"));
|
||
for (int i = 0; i < 400; i++) b.Tick(0.05f);
|
||
Check("B3.cap", Math.Abs(b.Mana - 10f) < 0.001f);
|
||
var b3 = g.NewBattle(); b3.Start(); b3.Mana = 0; b3.TimeLeft = c.lastMinuteSeconds - 1;
|
||
b3.Tick(1f);
|
||
Check("B3.lastminute", Math.Abs(b3.Mana - c.lastMinuteRegenMul / c.manaRegenSeconds) < 0.01f);
|
||
// B4 소환 = 카드를 누르면 아래 가운데에서 즉시 (위치 선택 없음)
|
||
var b4 = g.NewBattle(); b4.Start();
|
||
var u1 = g.BattleUnit(0);
|
||
Check("B4.deploy", b4.Deploy(u1) && b4.Units.Count == 1 && Math.Abs(b4.Mana - (c.manaStart - 1)) < 0.001f);
|
||
var sp = b4.SpawnPoint;
|
||
Check("B4.spawn", Math.Abs(sp.x - c.arenaW * 0.5f) < 1e-4f && sp.y < c.arenaH * 0.1f && Vector2.Distance(b4.Units[0].pos, sp) <= c.spawnJitter + 1e-4f, $"spawn={sp} pos={b4.Units[0].pos}");
|
||
b4.Mana = 0.5f;
|
||
Check("B4.nomana", !b4.Deploy(u1));
|
||
// D1 카드 1장 = 1마리 (모든 카드)
|
||
var b1 = new Battle(c, 1, 100, 3); b1.Start(); b1.Mana = 100f;
|
||
bool oneEach = true;
|
||
foreach (var d in c.units) { int n0 = b1.Units.Count; if (!b1.Deploy(d) || b1.Units.Count != n0 + 1 || Vector2.Distance(b1.Units[n0].pos, b1.SpawnPoint) > c.spawnJitter + 1e-4f) oneEach = false; }
|
||
Check("D1.one_per_card", oneEach && b1.Spawned == c.units.Count, $"spawned={b1.Spawned}/{c.units.Count}");
|
||
// B5 다리로 건넘 (지상) · 곧장 (비행) — 다리 2개 맵 · 성 공격 끔
|
||
var b5s = Fixed(L(), L(), 2);
|
||
var b5 = new Battle(c, 1, 10, 7, b5s); b5.Start();
|
||
b5.Deploy(u1);
|
||
var unit = b5.Units[0]; bool crossedOnBridge = true, crossed = false;
|
||
for (int i = 0; i < 1200 && unit.alive && !unit.attacking; i++)
|
||
{
|
||
b5.Tick(0.05f);
|
||
if (Math.Abs(unit.pos.y - c.riverY) < c.riverHalf) { crossed = true; if (!b5.Map.OnBridge(unit.pos.x)) crossedOnBridge = false; }
|
||
}
|
||
Check("B5.bridge", crossed && crossedOnBridge);
|
||
var drag = c.units.Find(u => u.flying);
|
||
var b5f = new Battle(c, 1, 15, 7, b5s); b5f.Start(); b5f.Mana = 15; b5f.Deploy(drag);
|
||
var dr = b5f.Units[0]; var d0 = dr.pos; var toCastle = (b5f.CastlePos - d0).normalized;
|
||
for (int i = 0; i < 60; i++) b5f.Tick(0.05f);
|
||
var moved = dr.pos - d0;
|
||
Check("B5.flying_straight", Vector2.Angle(moved, toCastle) < 2f && moved.magnitude > 4f, $"angle={Vector2.Angle(moved, toCastle):0.0} moved={moved.magnitude:0.00}");
|
||
// B6 성 방어: 검사 1마리만 보내면 성에 거의 피해 못 주고 쓰러진다
|
||
var b6 = g.NewBattle(); b6.Start(); b6.Deploy(u1);
|
||
for (int i = 0; i < 1200; i++) b6.Tick(0.05f);
|
||
Check("B6.defend", b6.Units.Count == 0 && b6.Killed == 1 && b6.CastleHp >= b6.CastleMaxHp - u1.damage * 2, $"castle={b6.CastleHp} killed={b6.Killed}");
|
||
// B7 자동 플레이: 1칸(검사)만으로 1단계 승리 — 물량으로 (많이 쓰러지면서 이긴다)
|
||
var w1 = AutoBattle(g, 1, 1, 10, 5f, out float tw1);
|
||
Check("B7.stage1_win", w1.State == BattleState.Won, $"state={w1.State} t={tw1:0.0} castle={w1.CastleHp:0}");
|
||
Check("E2.swarm_win", w1.State == BattleState.Won && w1.Spawned >= 30 && w1.Killed >= 10, $"spawned={w1.Spawned} killed={w1.Killed}");
|
||
Debug.Log($"[VS] BATTLE stage1 slots1 → {w1.State} t={tw1:0.0}s spawned={w1.Spawned} killed={w1.Killed} 성 회복 {w1.CastleHeals}회 map={w1.MapName}");
|
||
for (int st = 2; st <= 8; st++)
|
||
{
|
||
var r1 = AutoBattle(g, st, 1, 10, 5f, out float ta);
|
||
var r6 = AutoBattle(g, st, 6, 15, 7f, out float tb);
|
||
Debug.Log($"[VS] BATTLE stage{st} castleHP={r1.CastleMaxHp:0} atk={r1.Atk:0} 성={string.Join("+", r1.ShotNames())}/{string.Join("+", r1.GimmickNames())} · 1칸/10마나 → {r1.State} {ta:0}s 성 남은 {r1.CastleHp / r1.CastleMaxHp:P0} 소환 {r1.Spawned} · 6칸/15마나 → {r6.State} {tb:0}s 성 남은 {r6.CastleHp / r6.CastleMaxHp:P0} 소환 {r6.Spawned} 소환수 처치 {r6.MinionsKilled}");
|
||
// 사거리 해석 비교(시험 배율) — 성 사거리 ×0.5 · ×0.25(= 예전 화살 7칸의 절반 3.5칸)
|
||
foreach (var mul in c.testRangeMuls)
|
||
{
|
||
if (mul == 1f) continue;
|
||
var q1 = AutoBattle(g, st, 1, 10, 5f, out float qa, 3, new BattleSetup { rangeMul = mul });
|
||
var q6 = AutoBattle(g, st, 6, 15, 7f, out float qb, 3, new BattleSetup { rangeMul = mul });
|
||
Debug.Log($"[VS] BATTLE └ 사거리 ×{mul} · 1칸 → {q1.State} {qa:0}s 성 남은 {q1.CastleHp / q1.CastleMaxHp:P0} · 6칸 → {q6.State} {qb:0}s 성 남은 {q6.CastleHp / q6.CastleMaxHp:P0}");
|
||
}
|
||
}
|
||
// B9 강화
|
||
var g9 = NewGame(t, out _);
|
||
g9.Coin = BigInteger.Parse("100000000000");
|
||
var p1 = g9.SlotUnlockPrice();
|
||
Check("B9.slot", g9.UnlockBattleSlot() == BuyResult.Ok && g9.BattleSlots == 2 && g9.Coin == BigInteger.Parse("100000000000") - p1);
|
||
while (g9.UnlockBattleSlot() == BuyResult.Ok) { }
|
||
Check("B9.slot_max", g9.BattleSlots == c.slotMax && g9.UnlockBattleSlot() == BuyResult.Max);
|
||
Check("B9.mana", g9.UpgradeBattleMana() == BuyResult.Ok && g9.BattleMaxMana == 11);
|
||
while (g9.UpgradeBattleMana() == BuyResult.Ok) { }
|
||
Check("B9.mana_cap", g9.BattleMaxMana == c.manaMaxCap && g9.NewBattle().MaxMana == c.manaMaxCap);
|
||
// B10 승리 보상 · 다음 단계
|
||
var g10 = NewGame(t, out _);
|
||
var wb = AutoBattle(g10, 1, 1, 10, 5f, out _);
|
||
var coin0 = g10.Coin; var reward = g10.StageWinReward(1, true); // 첫 클리어 = 기본 × 등급 firstMul
|
||
Check("B10.reward", g10.WinBattle(wb) == reward && reward > g10.BattleReward(1) && g10.Coin == coin0 + reward && g10.BattleStage == 2 && g10.NewBattle().CastleMaxHp > wb.CastleMaxHp, $"reward={reward} base={g10.BattleReward(1)}");
|
||
// B11 스테이지 — 줄마다 이름 · 다음에 깰 스테이지까지 열림 · 고른 스테이지로 전투 · 지난 스테이지·시험 승리는 진행 그대로 · 마지막을 깨면 전부 클리어
|
||
int stN = c.StageCount; bool stNamed = stN > 0;
|
||
for (int st = 1; st <= stN; st++) stNamed &= !string.IsNullOrEmpty(c.StageName(st));
|
||
Check("B11.stages", stNamed && stN == c.stageLoadouts.Count, $"stages={stN}");
|
||
var g11 = NewGame(t, out _);
|
||
Check("B11.open", g11.StageOpen(1) && !g11.StageOpen(2) && !g11.StageCleared(1) && g11.StageFocus == 1);
|
||
var stPick = g11.NewBattle(3, 7, null);
|
||
Check("B11.pick", stPick.Stage == 3 && stPick.CastleMaxHp == c.CastleHpAt(3) && Mathf.Approximately(stPick.Atk, c.CastleAtkAt(3)), $"hp={stPick.CastleMaxHp}");
|
||
Battle WonAt(Game gg, int st, BattleSetup set = null) { var w = gg.NewBattle(st, 7, set); w.Start(); w.CastleHpDebugKill(); w.Tick(0.05f); w.Tick(0.05f); return w; }
|
||
g11.S.battleStage = 3; // 1 · 2 클리어
|
||
var stReplay = WonAt(g11, 1); var stReward = g11.WinBattle(stReplay);
|
||
g11.WinBattle(WonAt(g11, 3, Fixed(L("normal"), L()))); // 시험 설정으로 이김 = 기록 안 함
|
||
Check("B11.replay_test", stReplay.State == BattleState.Won && stReward == g11.BattleReward(1) && g11.BattleStage == 3 && g11.StageCleared(2) && g11.StageOpen(3) && !g11.StageOpen(4), $"stage={g11.BattleStage}");
|
||
g11.S.battleStage = stN; g11.WinBattle(WonAt(g11, stN));
|
||
Check("B11.all_clear", g11.BattleStage == stN + 1 && g11.StageCleared(stN) && g11.StageOpen(stN) && g11.StageFocus == stN);
|
||
// B12 덱 · 등급 — 처음 덱 = 유닛 순서 · 빼기 = 빈 칸 · 넣기 = 열린 빈 칸 먼저 · 놓기 = 맞바꿈 · 프리셋끼리 따로 · 저장 왕복 · 등급 7개 순서 · 유닛마다 등급
|
||
var g12 = NewGame(t, out _); var dk = g12.ActiveDeck; var us = c.units;
|
||
bool inOrder = dk.Length == c.slotMax;
|
||
for (int i = 0; i < dk.Length && i < us.Count; i++) inOrder &= dk[i] == us[i].id;
|
||
Check("B12.deck_default", inOrder && g12.BattleUnit(0) == us[0] && g12.DeckReady && g12.DeckPresets == c.deckPresets && c.deckPresets >= 2, $"presets={c.deckPresets}");
|
||
g12.DeckRemove(us[0].id); g12.DeckRemove(us[1].id);
|
||
bool emptied = g12.BattleUnit(0) == null && !g12.DeckReady && g12.DeckPos(us[0].id) < 0;
|
||
bool addOpen = g12.DeckAdd(us[1].id) && g12.DeckPos(us[1].id) == 0; // 열린 칸(0)이 먼저 — 잠긴 빈 칸(1)보다
|
||
g12.DeckPut(3, us[0].id); g12.DeckPut(0, us[4].id); // 3번 칸 덮어쓰기 · 덱 안 유닛끼리 맞바꿈
|
||
bool put = g12.ActiveDeck[3] == us[0].id && g12.DeckPos(us[3].id) < 0 && g12.ActiveDeck[0] == us[4].id && g12.ActiveDeck[4] == us[1].id && Mathf.Approximately(g12.DeckAvgCost, us[4].cost);
|
||
Check("B12.deck_edit", emptied && addOpen && put, string.Join(",", g12.ActiveDeck));
|
||
g12.SelectDeck(1); bool other = g12.ActiveDeck[0] == us[0].id && g12.DeckSel == 1;
|
||
g12.SelectDeck(0);
|
||
var sv = JsonUtility.FromJson<SaveData>(JsonUtility.ToJson(g12.S));
|
||
Check("B12.preset_save", other && g12.ActiveDeck[0] == us[4].id && sv.decks != null && sv.decks.Count == c.deckPresets && sv.decks[0].ids[0] == us[4].id && sv.decks[1].ids[0] == us[0].id);
|
||
string[] gradeNames = { "일반", "고급", "희귀", "에픽", "전설", "불멸", "신화" };
|
||
bool grades = c.grades != null && c.grades.Count == gradeNames.Length;
|
||
for (int i = 0; grades && i < gradeNames.Length; i++) grades &= c.grades[i].id == i + 1 && c.grades[i].name == gradeNames[i] && !string.IsNullOrEmpty(c.grades[i].color);
|
||
foreach (var u in us) grades &= c.Grade(u.grade) != null;
|
||
Check("B12.grades", grades && c.Grade(7).color == "rainbow");
|
||
// B13 유닛 목록 = PD 가 고른 유닛 그림(battle.json roster) · 번호 겹침 없음 · 이름 · 그림 파일 있음 · 보유 유닛 그림도 그 안(고른 그림만 씀)
|
||
var artFiles = System.IO.Directory.GetFiles("Assets/Resources/Art/Units", "px_*.png");
|
||
var rosterArts = new HashSet<int>(); bool rosterOk = c.roster != null && c.roster.Count > 0 && c.roster.Count <= artFiles.Length;
|
||
if (c.roster != null) foreach (var r in c.roster) rosterOk &= rosterArts.Add(r.art) && !string.IsNullOrEmpty(r.name) && System.IO.File.Exists($"Assets/Resources/Art/Units/px_{r.art}.png") && VS.View.UnitArt.Get(r.art) != null;
|
||
int ownedArts = 0; foreach (var u in us) { int a = VS.View.UnitArt.ArtOf(u.id); if (a > 0) { ownedArts++; rosterOk &= rosterArts.Contains(a); } }
|
||
Check("B13.roster", rosterOk && ownedArts > 0, $"files={artFiles.Length} roster={c.roster?.Count} owned art={ownedArts}");
|
||
|
||
MapTests(c);
|
||
CastleTests(c);
|
||
FlagTests(c);
|
||
StrengthRegenTests(c);
|
||
Stage50Tests(c, t); // 스테이지 50 — 표 · 지형 · 필드 · 새 기술 · 새 소환 · 성 능력 · 보상(HeadlessRun.Stage50.cs)
|
||
}
|
||
|
||
// ---------------- 깃발: 맵을 누른 곳 → 내 유닛이 깃발에 먼저 닿은 뒤 성으로 ----------------
|
||
static void FlagTests(BattleCfg c)
|
||
{
|
||
var walker = Dummy(1.2f); walker.range = 0.8f;
|
||
var fly = Dummy(1.5f); fly.range = 0.8f; fly.flying = true;
|
||
var ready = new Battle(c, 1, 10, 3, Fixed(L(), L()));
|
||
Check("F1.not_before_start", !ready.SetFlag(new Vector2(3f, 12f)) && !ready.HasFlag);
|
||
var fb = new Battle(c, 1, 15, 3, Fixed(L(), L())); fb.Start();
|
||
var before = Put(fb, walker, fb.SpawnPoint.x, fb.SpawnPoint.y);
|
||
for (int i = 0; i < 20; i++) fb.Tick(0.05f); // 깃발 전에 이미 성 쪽으로 걷던 유닛
|
||
var flagAt = new Vector2(3f, 12f);
|
||
Check("F1.flag_set", fb.SetFlag(flagAt) && fb.HasFlag && Vector2.Distance(fb.FlagPos, flagAt) < 1e-4f);
|
||
var after = Put(fb, walker, fb.SpawnPoint.x, fb.SpawnPoint.y); // 깃발 뒤에 소환한 유닛
|
||
var flyer = Put(fb, fly, fb.SpawnPoint.x, fb.SpawnPoint.y);
|
||
var probe = new[] { before, after, flyer }; var touched = new bool[3]; var early = new bool[3];
|
||
for (int i = 0; i < 2400 && !(before.attacking && after.attacking && flyer.attacking); i++)
|
||
{
|
||
fb.Tick(0.05f);
|
||
for (int k = 0; k < 3; k++) { if (Vector2.Distance(probe[k].pos, flagAt) <= c.flagReach + 1e-3f) touched[k] = true; if (probe[k].attacking && !touched[k]) early[k] = true; }
|
||
}
|
||
Check("F2.existing_unit", touched[0] && before.attacking && !early[0], $"pos={before.pos}");
|
||
Check("F3.new_unit", touched[1] && after.attacking && !early[1], $"pos={after.pos}");
|
||
Check("F4.flying", touched[2] && flyer.attacking && !early[2], $"pos={flyer.pos}");
|
||
// F5 새 깃발 = 성을 치던 유닛도 다시 새 깃발로 → 닿은 뒤 다시 성
|
||
var flag2 = new Vector2(15f, 20f); fb.SetFlag(flag2);
|
||
bool left = false, touched2 = false;
|
||
for (int i = 0; i < 2400 && !(touched2 && before.attacking); i++)
|
||
{
|
||
fb.Tick(0.05f);
|
||
if (!before.attacking) left = true;
|
||
if (Vector2.Distance(before.pos, flag2) <= c.flagReach + 1e-3f) touched2 = true;
|
||
}
|
||
Check("F5.reflag", left && touched2 && before.attacking && fb.FlagVer == 2, $"pos={before.pos}");
|
||
// F6 강물·바위를 누르면 가장 가까운 빈 칸에 꽂힘
|
||
var fr = new Battle(c, 1, 10, 3, Fixed(L(), L(), 2)); fr.Start();
|
||
var water = new Vector2(9f, c.riverY);
|
||
Check("F6.snap_free", fr.Map.Blocked(water) && fr.SetFlag(water) && !fr.Map.Blocked(fr.FlagPos) && Vector2.Distance(fr.FlagPos, water) < 2f, $"flag={fr.FlagPos}");
|
||
}
|
||
|
||
// ---------------- 적은 내 유닛보다 매우 강하게(물량 컨셉) · 성벽 회복 ----------------
|
||
static void StrengthRegenTests(BattleCfg c)
|
||
{
|
||
// E1 성·소환수의 공격력·체력 ≥ 내 가장 강한 유닛의 2배 (1단계 기준 · 단계마다 더 커짐)
|
||
float maxHp = 0f, maxDmg = 0f; foreach (var u in c.units) { maxHp = Mathf.Max(maxHp, u.hp); maxDmg = Mathf.Max(maxDmg, u.damage); }
|
||
Check("E1.castle_strong", c.castle.hp >= 2f * maxHp && c.castle.atk >= 2f * maxDmg, $"성 체력 {c.castle.hp} 공격 {c.castle.atk} · 내 최고 체력 {maxHp} 공격 {maxDmg}");
|
||
// 싸우는 소환수(근접 · 원거리)만 — 스테이지 50 특수 소환(수호병 · 폭탄병 · 토템 = kind)은 「먼저 잡으라고」 만든 유닛이라 기획서 수치(체력 4000 · 800 · 3000) 그대로(확정 전)
|
||
bool sOk = true; foreach (var s in c.summons) if (string.IsNullOrEmpty(s.kind) && (s.hp < 2f * maxHp || s.damage < 2f * maxDmg)) sOk = false;
|
||
Check("E1.summon_strong", sOk, string.Join(" · ", c.summons.FindAll(s => string.IsNullOrEmpty(s.kind)).ConvertAll(s => $"{s.name} 체력 {s.hp} 공격 {s.damage}")));
|
||
var basic = c.units[0];
|
||
Check("E1.one_shot", c.castle.atk * c.Shot("normal").dmgPct / 100f >= basic.hp, $"일반 {c.castle.atk * c.Shot("normal").dmgPct / 100f} vs {basic.name} 체력 {basic.hp}");
|
||
|
||
// C 성벽 회복: 맞은 뒤 regenDelay 초 동안 그대로 → 그때부터 regenInterval 초마다 최대 체력의 regenPct % · 최대 체력까지
|
||
var k = c.castle;
|
||
var rb = new Battle(c, 1, 10, 1, Fixed(L(), L())); rb.Start();
|
||
rb.HitCastle(rb.CastleMaxHp * 0.5f);
|
||
float hp0 = rb.CastleHp, step = rb.CastleMaxHp * k.regenPct / 100f;
|
||
for (int i = 0; i < Mathf.FloorToInt((k.regenDelay - 0.1f) / 0.05f); i++) rb.Tick(0.05f);
|
||
Check("C1.regen_wait", rb.CastleHp == hp0 && rb.CastleHeals == 0, $"hp={rb.CastleHp}");
|
||
for (int i = 0; i < 4; i++) rb.Tick(0.05f);
|
||
Check("C2.regen_first", Near(rb.CastleHp - hp0, step, 0.5) && rb.CastleHeals == 1 && rb.Fx.Exists(f => f.type == "heal"), $"+{rb.CastleHp - hp0} step={step}");
|
||
int n10 = Mathf.RoundToInt(10f / k.regenInterval);
|
||
for (int i = 0; i < Mathf.RoundToInt(10f / 0.05f); i++) rb.Tick(0.05f);
|
||
Check("C2.regen_rate", Math.Abs(rb.CastleHeals - 1 - n10) <= 1 && Near(rb.CastleHp - hp0, step * rb.CastleHeals, 1.0), $"10초 회복 {rb.CastleHeals - 1}회");
|
||
rb.CastleHp = rb.CastleMaxHp - step * 0.5f;
|
||
for (int i = 0; i < 60; i++) rb.Tick(0.05f);
|
||
Check("C3.regen_cap", rb.CastleHp == rb.CastleMaxHp, $"hp={rb.CastleHp}/{rb.CastleMaxHp}");
|
||
// C4 계속 맞는 동안은 회복 없음
|
||
var hitter = Dummy(); hitter.damage = 1f; hitter.interval = 1f; hitter.range = 0.8f;
|
||
var rc = new Battle(c, 1, 10, 1, Fixed(L(), L())); rc.Start();
|
||
rc.HitCastle(rc.CastleMaxHp * 0.5f);
|
||
Put(rc, hitter, 9f, InY(c, 0.3f));
|
||
for (int i = 0; i < Mathf.RoundToInt((k.regenDelay * 3f) / 0.05f); i++) rc.Tick(0.05f);
|
||
Check("C4.regen_reset", rc.CastleHeals == 0 && rc.CastleHp < rc.CastleMaxHp * 0.5f, $"heals={rc.CastleHeals} hp={rc.CastleHp}");
|
||
}
|
||
|
||
// ---------------- 맵 4종 · 무작위 생성 · 장애물 우회 ----------------
|
||
static void MapTests(BattleCfg c)
|
||
{
|
||
int laneCells = c.BridgeCells(null); // 무작위 맵 다리 = 보통 다리(지나갈 칸 수)
|
||
for (int k = 0; k <= 3; k++)
|
||
{
|
||
var mb = new Battle(c, 1, 10, 100 + k, new BattleSetup { mapType = k });
|
||
var mp = mb.Map; int row = Mathf.FloorToInt(c.riverY - 0.5f), lanes = 0;
|
||
for (int x = 0; x < mp.W; x++) if (mp.Walk[row * mp.W + x]) lanes++;
|
||
Check("M1.type" + k, mp.Type == k && mp.Bridges.Length == k && mp.River == (k > 0) && lanes == (k > 0 ? k * laneCells : mp.W) && mp.Reachable(mb.SpawnPoint), $"lanes={lanes} bridges={string.Join(",", mp.Bridges)}");
|
||
}
|
||
int n = 400, reach = 0, top = 0, near = 0, withObs = 0, blocking = 0; var cnt = new int[4];
|
||
float far = c.obstacleDistMax + c.obstacleRadiusMax * c.obstacleSpacing * c.obstaclePiecesMax * 0.5f + c.obstacleWobble + 0.01f;
|
||
for (int s = 0; s < n; s++)
|
||
{
|
||
var mb = new Battle(c, 1, 10, 1000 + s, new BattleSetup { mapType = BattleSetup.RandomMap }); var mp = mb.Map;
|
||
cnt[mp.Type]++;
|
||
if (mp.Reachable(mb.SpawnPoint)) reach++;
|
||
if (Math.Abs(mp.Castle.y - c.castle.y) < 1e-4f && mp.Castle.y >= c.arenaH * 0.75f && mp.Castle.x >= c.castle.xMin - 1e-4f && mp.Castle.x <= c.castle.xMax + 1e-4f) top++;
|
||
bool ok = true; foreach (var o in mp.Obstacles) if (o.pos.y <= c.riverY + c.riverHalf || Vector2.Distance(o.pos, mp.Castle) > far) ok = false;
|
||
if (ok) near++;
|
||
if (mp.Obstacles.Count > 0) withObs++;
|
||
if (!mp.Clear(new Vector2(mp.Castle.x, mp.Castle.y - c.castle.radius - c.castleSeedPad - 0.3f), new Vector2(mp.Castle.x, c.riverY + c.riverHalf + 0.5f))) blocking++;
|
||
}
|
||
Check("M2.random_types", cnt[0] > n / 10 && cnt[1] > n / 10 && cnt[2] > n / 10 && cnt[3] > n / 10, string.Join(",", cnt));
|
||
Check("M2.reachable", reach == n, $"{reach}/{n}");
|
||
Check("M2.castle_top", top == n, $"{top}/{n}");
|
||
Check("M2.obstacles_near_castle", near == n, $"{near}/{n}");
|
||
Check("M2.obstacle_rate", withObs > n / 2 && withObs < n, $"{withObs}/{n}");
|
||
Check("M3.blocking", blocking > 0, $"성 바로 앞 직선이 막힌 맵 {blocking}/{n}");
|
||
Debug.Log($"[VS] MAP 400판: 평지/다리1/다리2/다리3 = {string.Join("/", cnt)} · 바위·언덕 있음 {withObs} · 성 앞 직선 막힘 {blocking}");
|
||
|
||
// P1 성 앞을 가로막는 바위 벽 → 끝을 돌아서 도착 · 막힌 칸 진입 0
|
||
var wall = new List<BObstacle>(); for (float x = 5f; x <= 12.6f; x += 1.5f) wall.Add(new BObstacle { pos = new Vector2(x, 22f), r = 1f, kind = 0 });
|
||
var ps = Fixed(L(), L()); ps.customObstacles = wall;
|
||
var pb = new Battle(c, 1, 10, 3, ps); pb.Start();
|
||
var walker = Dummy(1.2f); walker.range = 0.8f;
|
||
var pu = Put(pb, walker, 9f, c.spawnY);
|
||
bool noBlock = true; float maxDx = 0f, pathLen = 0f; var prev = pu.pos;
|
||
for (int i = 0; i < 1600 && !pu.attacking; i++)
|
||
{
|
||
pb.Tick(0.05f);
|
||
if (pb.Map.Blocked(pu.pos)) noBlock = false;
|
||
maxDx = Mathf.Max(maxDx, Mathf.Abs(pu.pos.x - 9f)); pathLen += (pu.pos - prev).magnitude; prev = pu.pos;
|
||
}
|
||
float straight = Vector2.Distance(new Vector2(9f, c.spawnY), pb.CastlePos) - c.castle.radius - walker.radius - walker.range;
|
||
Check("P1.detour_reach", pu.attacking, $"pos={pu.pos}");
|
||
Check("P1.detour_noblock", noBlock);
|
||
Check("P1.detour_around", maxDx > 4.2f && pathLen > straight * 1.05f, $"maxDx={maxDx:0.00} len={pathLen:0.0} straight={straight:0.0}");
|
||
// P2 무작위 맵 12판(바위·언덕 켬): 전부 도착 · 막힌 칸 진입 0 · 강은 다리 위로만
|
||
bool allReach = true, noBlock2 = true, bridgeOnly = true; int obsMaps = 0;
|
||
for (int s = 1; s <= 12; s++)
|
||
{
|
||
var rb = new Battle(c, 1, 10, 500 + s, new BattleSetup { mapType = BattleSetup.RandomMap, obstacles = 1, shots = L(), gimmicks = L() }); rb.Start();
|
||
if (rb.Map.Obstacles.Count > 0) obsMaps++;
|
||
var ru = Put(rb, walker, rb.SpawnPoint.x, rb.SpawnPoint.y);
|
||
for (int i = 0; i < 2400 && !ru.attacking; i++)
|
||
{
|
||
rb.Tick(0.05f);
|
||
if (rb.Map.Blocked(ru.pos)) noBlock2 = false;
|
||
if (rb.Map.River && Math.Abs(ru.pos.y - c.riverY) < c.riverHalf && !rb.Map.OnBridge(ru.pos.x)) bridgeOnly = false;
|
||
}
|
||
if (!ru.attacking) { allReach = false; Debug.Log($"[VS] P2 seed {500 + s} 도착 못 함 type={rb.Map.Type} pos={ru.pos}"); }
|
||
}
|
||
Check("P2.reach_all", allReach && obsMaps == 12, $"obsMaps={obsMaps}");
|
||
Check("P2.no_block", noBlock2);
|
||
Check("P2.bridge_only", bridgeOnly);
|
||
}
|
||
|
||
// ---------------- 성 기믹 3종 · 투사체 5종 · 사거리 · 시험 설정 ----------------
|
||
static void CastleTests(BattleCfg c)
|
||
{
|
||
float atk = c.castle.atk; // 1단계 공격력
|
||
var dm = Dummy();
|
||
// G1 파이어 볼: 맞은 유닛 dmgPct · 둘레 splashRadius 안 splashPct · 밖 0
|
||
var fb = c.Skill("fireball");
|
||
var g1 = new Battle(c, 1, 10, 1, Fixed(L(), L("fireball"))); g1.Start();
|
||
float fy = InY(c, fb.range - 1.2f);
|
||
var ua = Put(g1, dm, 9f, fy); var ub = Put(g1, dm, 10.2f, fy); var ud = Put(g1, dm, 7.8f, fy); var uc = Put(g1, dm, 9f + fb.splashRadius + 2f, fy);
|
||
g1.CastleSkills[0].cd = 0f;
|
||
TickUntil(g1, () => ua.hp < dm.hp, 5f);
|
||
Check("G1.fireball_hit", Near(dm.hp - ua.hp, atk * fb.dmgPct / 100f, 0.01), $"{dm.hp - ua.hp}");
|
||
Check("G1.fireball_splash", Near(dm.hp - ub.hp, atk * fb.splashPct / 100f, 0.01) && Near(dm.hp - ud.hp, atk * fb.splashPct / 100f, 0.01), $"{dm.hp - ub.hp} {dm.hp - ud.hp}");
|
||
Check("G1.fireball_outside", uc.hp == dm.hp && g1.Fx.Exists(f => f.type == "blast"));
|
||
// G2 라이트닝: 즉시(탄 없이) dmgPct + 기절 동안 이동·공격 멈춤 → 풀리면 다시 이동
|
||
var lt = c.Skill("lightning");
|
||
var g2 = new Battle(c, 1, 10, 1, Fixed(L(), L("lightning"))); g2.Start();
|
||
var wk = Dummy(1.2f);
|
||
var ux = Put(g2, wk, 9f, InY(c, lt.range + 0.8f)); // 사거리 밖에서 걸어 들어옴
|
||
g2.CastleSkills[0].cd = 0f;
|
||
bool shotSeen = false;
|
||
TickUntil(g2, () => { if (g2.Shots.Count > 0) shotSeen = true; return ux.hp < wk.hp; }, 5f);
|
||
Check("G2.lightning_instant", Near(wk.hp - ux.hp, atk * lt.dmgPct / 100f, 0.01) && !shotSeen && g2.Fx.Exists(f => f.type == "lightning"), $"{wk.hp - ux.hp}");
|
||
var pz = ux.pos; for (int i = 0; i < 20; i++) g2.Tick(0.05f);
|
||
Check("G2.stun_freeze", (ux.pos - pz).magnitude < 1e-4f && ux.stun > 0f && !ux.attacking, $"stun={ux.stun:0.00}");
|
||
for (int i = 0; i < 20; i++) g2.Tick(0.05f);
|
||
Check("G2.stun_recover", ux.stun <= 0f && (ux.pos - pz).magnitude > 0.3f, $"moved={(ux.pos - pz).magnitude:0.00}");
|
||
// G3 소환(근접): interval 마다 · 최대 max · 성 둘레 집에 주둔
|
||
var sm = c.Summon("summonMelee");
|
||
var g3 = new Battle(c, 1, 10, 1, Fixed(L(), L("summonMelee"))); g3.Start();
|
||
int maxSeen = 0;
|
||
for (float tt = 0; tt < sm.interval * (sm.max + 3); tt += 0.05f) { g3.Tick(0.05f); maxSeen = Math.Max(maxSeen, g3.Minions.Count); }
|
||
Check("G3.summon_cap", g3.Minions.Count == sm.max && maxSeen == sm.max && g3.MinionsSpawned == sm.max, $"now={g3.Minions.Count} max={maxSeen}");
|
||
bool atHome = true; foreach (var m in g3.Minions) if ((m.pos - m.home).magnitude > 0.1f || Vector2.Distance(m.home, g3.CastlePos) > c.castle.radius + sm.homeDist + 1f) atHome = false;
|
||
Check("G3.summon_home", atHome);
|
||
// G4 사거리 밖 유닛 = 가만히 · 사거리 안 = 공격 · 내 진영(강 아래)으로는 안 내려감
|
||
var farU = Put(g3, Dummy(), 9f, 8f);
|
||
var homes = g3.Minions.ConvertAll(m => m.pos);
|
||
for (int i = 0; i < 100; i++) g3.Tick(0.05f);
|
||
bool idle = farU.hp == 100000f; for (int i = 0; i < homes.Count; i++) if ((g3.Minions[i].pos - homes[i]).magnitude > 0.1f) idle = false;
|
||
Check("G4.guard_idle", idle);
|
||
var h0 = g3.Minions[0].home;
|
||
var nearU = Put(g3, Dummy(), h0.x, h0.y - 3.5f);
|
||
float minY = float.MaxValue; bool leash = true;
|
||
for (int i = 0; i < 100; i++) { g3.Tick(0.05f); foreach (var m in g3.Minions) { minY = Mathf.Min(minY, m.pos.y); if (Vector2.Distance(m.pos, m.home) > g3.Guard(m.def) + 1f) leash = false; } }
|
||
Check("G4.guard_attack", nearU.hp < 100000f && farU.hp == 100000f, $"near={nearU.hp} far={farU.hp}");
|
||
Check("G4.no_base", leash && minY > c.riverY, $"minY={minY:0.0}");
|
||
// G5 소환(원거리): 제자리에서 탄으로 공격
|
||
var sr = c.Summon("summonRanged");
|
||
var g5 = new Battle(c, 1, 10, 1, Fixed(L(), L("summonRanged"))); g5.Start();
|
||
g5.Summoners[0].cd = 0f;
|
||
TickUntil(g5, () => g5.Minions.Count > 0 && (g5.Minions[0].pos - g5.Minions[0].home).magnitude < 0.05f, 10f);
|
||
var rm = g5.Minions[0]; var rh = rm.home;
|
||
var tgt = Put(g5, Dummy(), rh.x, rh.y - (sr.attackRange - 0.5f));
|
||
bool minionShot = false; for (int i = 0; i < 80; i++) { g5.Tick(0.05f); if (g5.Shots.Exists(s => s.owner == 2)) minionShot = true; }
|
||
Check("G5.ranged_shot", minionShot && tgt.hp < 100000f && (rm.pos - rh).magnitude < 0.05f, $"hp={tgt.hp}");
|
||
// G6 내 유닛도 소환수를 공격해 쓰러뜨린다
|
||
var g6 = new Battle(c, 1, 15, 1, Fixed(L(), L("summonMelee"))); g6.Start();
|
||
g6.Summoners[0].cd = 0f;
|
||
TickUntil(g6, () => g6.Minions.Count > 0 && (g6.Minions[0].pos - g6.Minions[0].home).magnitude < 0.05f, 10f);
|
||
var kd = c.units[2];
|
||
var strong = new BUnitDef { id = kd.id, name = kd.name, cost = 0, hp = 100000f, damage = kd.damage, interval = kd.interval, range = kd.range, speed = kd.speed, radius = kd.radius };
|
||
Put(g6, strong, g6.Minions[0].home.x, g6.Minions[0].home.y - 4f);
|
||
TickUntil(g6, () => g6.MinionsKilled > 0, g6.Minions[0].maxHp / (kd.damage / kd.interval) + 15f);
|
||
Check("G6.fight_back", g6.MinionsKilled >= 1);
|
||
|
||
// S1 일반: 단일 dmgPct
|
||
var sn = c.Shot("normal");
|
||
var s1 = new Battle(c, 1, 10, 1, Fixed(L("normal"), L())); s1.Start();
|
||
var t1 = Put(s1, dm, 9f, InY(c, sn.range - 1f)); s1.CastleShots[0].cd = 0f;
|
||
TickUntil(s1, () => t1.hp < dm.hp, 5f);
|
||
Check("S1.normal", Near(dm.hp - t1.hp, atk * sn.dmgPct / 100f, 0.01), $"{dm.hp - t1.hp}");
|
||
// S2 범위: 맞은 유닛 dmgPct + 둘레 splashPct · 밖 0
|
||
var ss = c.Shot("splash");
|
||
var s2 = new Battle(c, 1, 10, 1, Fixed(L("splash"), L())); s2.Start();
|
||
float sy2 = InY(c, ss.range - 1f);
|
||
var a2 = Put(s2, dm, 9f, sy2); var b2 = Put(s2, dm, 10.2f, sy2); var c2 = Put(s2, dm, 9f + ss.splashRadius + 1.5f, sy2); s2.CastleShots[0].cd = 0f;
|
||
TickUntil(s2, () => a2.hp < dm.hp, 5f);
|
||
Check("S2.splash", Near(dm.hp - a2.hp, atk * ss.dmgPct / 100f, 0.01) && Near(dm.hp - b2.hp, atk * ss.splashPct / 100f, 0.01) && c2.hp == dm.hp, $"{dm.hp - a2.hp} {dm.hp - b2.hp} {dm.hp - c2.hp}");
|
||
// S3 멀티샷: 부채꼴 count 발 → 각 방향 유닛 1명씩 동시에
|
||
var sml = c.Shot("multi");
|
||
var s3 = new Battle(c, 1, 10, 1, Fixed(L("multi"), L())); s3.Start();
|
||
var fan = new List<BUnit>();
|
||
for (int i = 0; i < sml.count; i++)
|
||
{
|
||
float ang = (270f - sml.spreadDeg * 0.5f + sml.spreadDeg * i / (sml.count - 1)) * Mathf.Deg2Rad;
|
||
float dist = c.castle.radius + 0.45f + sml.range - 1.5f - (i == sml.count / 2 ? 0.1f : 0f); // 가운데 유닛이 가장 가까움 = 조준 방향
|
||
fan.Add(Put(s3, dm, 9f + Mathf.Cos(ang) * dist, c.castle.y + Mathf.Sin(ang) * dist));
|
||
}
|
||
s3.CastleShots[0].cd = 0f;
|
||
s3.Tick(0.05f);
|
||
int bolts = s3.Shots.FindAll(s => s.type == "multi").Count;
|
||
TickUntil(s3, () => !s3.Shots.Exists(s => s.type == "multi"), 3f);
|
||
int hitN = 0; foreach (var u in fan) if (Near(dm.hp - u.hp, atk * sml.dmgPct / 100f, 0.01)) hitN++;
|
||
Check("S3.multi", bolts == sml.count && hitN == sml.count, $"bolts={bolts} hit={hitN}");
|
||
// S4 감속: 맞으면 이동 속도 −slowPct% · slowSeconds 뒤 원래대로
|
||
var sl = c.Shot("slow");
|
||
var s4 = new Battle(c, 1, 10, 1, Fixed(L("slow"), L())); s4.Start();
|
||
var su = Put(s4, wk, 9f, InY(c, sl.range + 0.5f)); s4.CastleShots[0].cd = 0f;
|
||
TickUntil(s4, () => su.slow > 0f, 5f);
|
||
Check("S4.slow_hit", Near(wk.hp - su.hp, atk * sl.dmgPct / 100f, 0.01) && Near(su.slowMul, 1f - sl.slowPct / 100f, 1e-4), $"dmg={wk.hp - su.hp} mul={su.slowMul}");
|
||
var q0 = su.pos; for (int i = 0; i < 10; i++) s4.Tick(0.05f);
|
||
float slowDist = (su.pos - q0).magnitude;
|
||
s4.CastleShots.Clear(); s4.Shots.Clear();
|
||
su.pos = new Vector2(9f, c.spawnY + 2f); // 성에 닿아 멈추지 않게 멀리 옮겨 감속이 풀리길 기다림
|
||
for (int i = 0; i < 70; i++) s4.Tick(0.05f);
|
||
var q1 = su.pos; for (int i = 0; i < 10; i++) s4.Tick(0.05f);
|
||
float normDist = (su.pos - q1).magnitude;
|
||
Check("S4.slow_speed", Near(slowDist, wk.speed * 0.5f * (1f - sl.slowPct / 100f), 0.05), $"slow={slowDist:0.000}");
|
||
Check("S4.slow_recover", su.slow <= 0f && Near(normDist, wk.speed * 0.5f, 0.05), $"normal={normDist:0.000}");
|
||
// S5 레이저: 선 위 전원 같은 순간 · 선 밖 0
|
||
var sz = c.Shot("laser");
|
||
var s5 = new Battle(c, 1, 10, 1, Fixed(L("laser"), L())); s5.Start();
|
||
var la = Put(s5, dm, 9f, InY(c, 0.5f)); var lb = Put(s5, dm, 9f, InY(c, sz.range * 0.5f)); var lc = Put(s5, dm, 9f, InY(c, sz.range - 0.2f)); var lo = Put(s5, dm, 12.5f, InY(c, sz.range * 0.5f));
|
||
s5.CastleShots[0].cd = 0f; s5.Tick(0.05f);
|
||
float lz = atk * sz.dmgPct / 100f;
|
||
Check("S5.laser_line", Near(dm.hp - la.hp, lz, 0.01) && Near(dm.hp - lb.hp, lz, 0.01) && Near(dm.hp - lc.hp, lz, 0.01) && s5.Fx.Exists(f => f.type == "laser"), $"{dm.hp - la.hp} {dm.hp - lb.hp} {dm.hp - lc.hp}");
|
||
Check("S5.laser_off", lo.hp == dm.hp);
|
||
|
||
// R 사거리: 기본(일반) = 예전 화살 7칸의 절반 · 스킬마다 다름 · 밖이면 안 쏨 · 시험 배율로 늘림
|
||
Check("R1.default_half", Near(sn.range, 7f * 0.5f, 0.01) && Near(c.Skill("fireball").range, 10f * 0.5f, 0.01), $"일반 {sn.range} · 파이어 볼 {c.Skill("fireball").range}");
|
||
var ranges = new HashSet<float>(); foreach (var d in c.shots) ranges.Add(d.range); foreach (var d in c.skills) ranges.Add(d.range);
|
||
Check("R2.per_skill", ranges.Count >= 3, string.Join(",", ranges));
|
||
var r3 = new Battle(c, 1, 10, 1, Fixed(L("normal"), L())); r3.Start();
|
||
var ro = Put(r3, dm, 9f, InY(c, sn.range + 0.6f)); r3.CastleShots[0].cd = 0f; // 사거리 밖 = 안 맞음
|
||
for (int i = 0; i < 60; i++) r3.Tick(0.05f);
|
||
float mul = c.testRangeMuls[c.testRangeMuls.Length - 1];
|
||
var rs = Fixed(L("normal"), L()); rs.rangeMul = mul;
|
||
var r4 = new Battle(c, 1, 10, 1, rs); r4.Start();
|
||
var ri = Put(r4, dm, 9f, InY(c, sn.range + 0.6f)); r4.CastleShots[0].cd = 0f; // 같은 자리 · 시험 배율이면 맞음
|
||
for (int i = 0; i < 60; i++) r4.Tick(0.05f);
|
||
Check("R3.out_of_range", ro.hp == dm.hp && ri.hp < dm.hp && Near(r4.Range(sn), sn.range * mul, 1e-4), $"×{mul}");
|
||
|
||
// X 시험 설정 · 단계 기본 구성
|
||
var xs = new BattleSetup { mapType = 3, obstacles = 0, shots = L("multi", "laser"), gimmicks = L("lightning", "summonRanged"), rangeMul = 0.5f };
|
||
var xb = new Battle(c, 1, 10, 9, xs);
|
||
Check("X1.setup", xb.IsTest && xb.Map.Type == 3 && xb.Map.Obstacles.Count == 0 && xb.CastleShots.Count == 2 && xb.CastleShots[0].def.id == "multi" && xb.CastleShots[1].def.id == "laser"
|
||
&& xb.CastleSkills.Count == 1 && xb.CastleSkills[0].def.id == "lightning" && xb.Summoners.Count == 1 && xb.Summoners[0].def.id == "summonRanged" && Near(xb.Range(c.Shot("laser")), c.Shot("laser").range * 0.5f, 1e-4));
|
||
bool loOk = true; var loBad = new List<int>();
|
||
for (int st = 1; st <= c.StageCount; st++)
|
||
{
|
||
var sb = new Battle(c, st, 10, st); var l = c.Loadout(st);
|
||
var ids = sb.CastleShots.ConvertAll(w => w.def.id); var gids = new HashSet<string>(sb.CastleSkills.ConvertAll(w => w.def.id)); gids.UnionWith(sb.Summoners.ConvertAll(s => s.def.id)); gids.UnionWith(sb.Powers.ConvertAll(p => p.def.id));
|
||
if (string.Join(",", ids) != string.Join(",", l.shots) || !gids.SetEquals(l.gimmicks) || sb.IsTest) { loOk = false; loBad.Add(st); }
|
||
}
|
||
Check("X2.stage_loadout", loOk, string.Join(",", loBad));
|
||
}
|
||
}
|
||
}
|