298 lines
19 KiB
C#
298 lines
19 KiB
C#
|
|
using System;
|
||
|
|
using System.Collections.Generic;
|
||
|
|
using System.Globalization;
|
||
|
|
using UnityEngine;
|
||
|
|
|
||
|
|
namespace TKS.OutGame
|
||
|
|
{
|
||
|
|
/// <summary>
|
||
|
|
/// C 단계 표 10장(PD #1093 C · 기획서 §7~§9) 해석·검증 — training · training_ranks · patrol · dev_tree · treasures · treasure_sets · chests · tower · dungeons · arena.
|
||
|
|
/// 검증 = 필수 열 · id 유일 · 연속 Lv/랭크/층 · 참조(세트 ↔ 보물 · 상자 · 장비 등급 · 선행 노드) · 확률 합 1 · 효과 키 처리기(DevRules · BattleEvents) · 오류가 있으면 임포터가 자산을 바꾸지 않는다.
|
||
|
|
/// </summary>
|
||
|
|
public static partial class OutGameTables
|
||
|
|
{
|
||
|
|
static void ParseC(Dictionary<string, List<Dictionary<string, string>>> rows, OutGameData d, List<string> e)
|
||
|
|
{
|
||
|
|
d.Training = ParseTraining(rows["training.csv"], rows["training_ranks.csv"], e);
|
||
|
|
d.Patrol = ParsePatrol(rows["patrol.csv"], d, e);
|
||
|
|
d.Dev = ParseDev(rows["dev_tree.csv"], e);
|
||
|
|
d.Chests = ParseChests(rows["chests.csv"], e);
|
||
|
|
d.TreasureSets = ParseSets(rows["treasure_sets.csv"], e);
|
||
|
|
d.Treasures = ParseTreasures(rows["treasures.csv"], d, e);
|
||
|
|
d.Tower = ParseTower(rows["tower.csv"], d, e);
|
||
|
|
d.Dungeons = ParseDungeons(rows["dungeons.csv"], e);
|
||
|
|
d.Arena = ScriptableObject.CreateInstance<ArenaConfig>();
|
||
|
|
KeyValues(rows["arena.csv"], "arena.csv", ArenaConfig.RequiredKeys, d.Arena.values, e);
|
||
|
|
CheckEconomyC(d, e);
|
||
|
|
}
|
||
|
|
|
||
|
|
static TrainingTable ParseTraining(List<Dictionary<string, string>> rows, List<Dictionary<string, string>> rankRows, List<string> e)
|
||
|
|
{
|
||
|
|
var t = ScriptableObject.CreateInstance<TrainingTable>();
|
||
|
|
const string f = "training.csv";
|
||
|
|
if (Cols(rows, f, e, "unit_type", "stat", "lv", "gold_cost", "stat_pct"))
|
||
|
|
{
|
||
|
|
foreach (var r in rows)
|
||
|
|
{
|
||
|
|
UnitType type;
|
||
|
|
if (!UnitTypeInfo.TryParseKey(Get(r, "unit_type"), out type)) { e.Add(f + " 행 " + Get(r, "#") + ": unit_type 「" + Get(r, "unit_type") + "」 모름"); continue; }
|
||
|
|
var stat = Get(r, "stat");
|
||
|
|
if (Array.IndexOf(TrainingTable.Stats, stat) < 0) { e.Add(f + " 행 " + Get(r, "#") + ": stat 「" + stat + "」 ≠ hp|atk|def"); continue; }
|
||
|
|
t.rows.Add(new TrainingTable.Row { type = type, stat = stat, lv = Int(r, "lv", f, e), goldCost = Int(r, "gold_cost", f, e), statPct = Float(r, "stat_pct", f, e) });
|
||
|
|
}
|
||
|
|
int max = t.MaxLevel;
|
||
|
|
foreach (UnitType type in Enum.GetValues(typeof(UnitType)))
|
||
|
|
foreach (var stat in TrainingTable.Stats)
|
||
|
|
for (int lv = 1; lv <= max; lv++)
|
||
|
|
if (t.Get(type, stat, lv) == null) e.Add(f + ": " + UnitTypeInfo.Key(type) + " " + stat + " Lv " + lv + " 없음");
|
||
|
|
}
|
||
|
|
const string g = "training_ranks.csv";
|
||
|
|
if (Cols(rankRows, g, e, "rank", "name_ko", "rank_pct", "promote_gold", "cost_mult"))
|
||
|
|
{
|
||
|
|
foreach (var r in rankRows)
|
||
|
|
{
|
||
|
|
var pg = Get(r, "promote_gold");
|
||
|
|
t.ranks.Add(new TrainingTable.Rank
|
||
|
|
{
|
||
|
|
rank = Int(r, "rank", g, e), nameKo = Get(r, "name_ko"), rankPct = Float(r, "rank_pct", g, e),
|
||
|
|
promoteGold = pg.Length == 0 ? 0 : Int(r, "promote_gold", g, e), costMult = Float(r, "cost_mult", g, e),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
for (int i = 0; i < t.ranks.Count; i++)
|
||
|
|
{
|
||
|
|
if (t.ranks[i].rank != i + 1) e.Add(g + ": rank 가 1부터 연속이 아님(" + (i + 1) + "번째 = " + t.ranks[i].rank + ")");
|
||
|
|
bool last = i == t.ranks.Count - 1;
|
||
|
|
if (last != (t.ranks[i].promoteGold == 0)) e.Add(g + ": 랭크 " + t.ranks[i].rank + " promote_gold — 최종 랭크만 비어야 함");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return t;
|
||
|
|
}
|
||
|
|
|
||
|
|
static PatrolTable ParsePatrol(List<Dictionary<string, string>> rows, OutGameData d, List<string> e)
|
||
|
|
{
|
||
|
|
var t = ScriptableObject.CreateInstance<PatrolTable>();
|
||
|
|
const string f = "patrol.csv";
|
||
|
|
if (!Cols(rows, f, e, "chapter", "gold_per_hour", "exp_per_hour", "gear_grade", "chest_bronze_per_hour")) return t;
|
||
|
|
var seen = new HashSet<string>();
|
||
|
|
foreach (var r in rows)
|
||
|
|
{
|
||
|
|
var row = new PatrolTable.Row
|
||
|
|
{
|
||
|
|
chapter = Int(r, "chapter", f, e), goldPerHour = Int(r, "gold_per_hour", f, e), expPerHour = Int(r, "exp_per_hour", f, e),
|
||
|
|
gearGrade = Get(r, "gear_grade"), chestPerHour = Float(r, "chest_bronze_per_hour", f, e),
|
||
|
|
};
|
||
|
|
if (!Unique(seen, row.chapter.ToString(CultureInfo.InvariantCulture), f, r, e)) continue;
|
||
|
|
if (d.Gear == null || d.Gear.OrderOf(row.gearGrade) == 0) e.Add(f + ": 챕터 " + row.chapter + " gear_grade 「" + row.gearGrade + "」 가 gear_items.csv 등급에 없음");
|
||
|
|
t.rows.Add(row);
|
||
|
|
}
|
||
|
|
return t;
|
||
|
|
}
|
||
|
|
|
||
|
|
static DevTree ParseDev(List<Dictionary<string, string>> rows, List<string> e)
|
||
|
|
{
|
||
|
|
var t = ScriptableObject.CreateInstance<DevTree>();
|
||
|
|
const string f = "dev_tree.csv";
|
||
|
|
if (!Cols(rows, f, e, "branch", "node_id", "name_ko", "kind", "effect_key", "pct_per_lv", "lv_max", "gold_base", "gold_growth", "blueprint_from_lv", "blueprint_per_lv", "requires")) return t;
|
||
|
|
var seen = new HashSet<string>();
|
||
|
|
foreach (var r in rows)
|
||
|
|
{
|
||
|
|
var n = new DevTree.Node
|
||
|
|
{
|
||
|
|
branch = Get(r, "branch"), id = Get(r, "node_id"), nameKo = Get(r, "name_ko"), kind = Get(r, "kind"), effectKey = Get(r, "effect_key"),
|
||
|
|
pctPerLv = Float(r, "pct_per_lv", f, e), lvMax = Int(r, "lv_max", f, e), goldBase = Int(r, "gold_base", f, e), goldGrowth = Float(r, "gold_growth", f, e),
|
||
|
|
blueprintFromLv = Int(r, "blueprint_from_lv", f, e), blueprintPerLv = Int(r, "blueprint_per_lv", f, e),
|
||
|
|
};
|
||
|
|
if (!Unique(seen, n.id, f, r, e)) continue;
|
||
|
|
string at = f + " " + n.id;
|
||
|
|
if (n.kind != "small" && n.kind != "big") e.Add(at + ": kind 「" + n.kind + "」 ≠ small|big");
|
||
|
|
if (n.lvMax < 1) e.Add(at + ": lv_max < 1");
|
||
|
|
if (n.EffectKeys.Length == 0) e.Add(at + ": effect_key 비었음");
|
||
|
|
foreach (var k in n.EffectKeys)
|
||
|
|
if (!DevRules.Known(k)) e.Add(at + ": effect_key 「" + k + "」 처리기 없음");
|
||
|
|
foreach (var part in Get(r, "requires").Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
|
||
|
|
{
|
||
|
|
var kv = part.Split(':');
|
||
|
|
int lv;
|
||
|
|
if (kv.Length != 2 || !int.TryParse(kv[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out lv)) { e.Add(at + ": requires 「" + part + "」 ≠ node:lv"); continue; }
|
||
|
|
var pre = t.Get(kv[0]); // 선행 = 앞 행(순환 없음)
|
||
|
|
if (pre == null) e.Add(at + ": requires 「" + kv[0] + "」 가 앞 행에 없음");
|
||
|
|
else if (pre.branch != n.branch) e.Add(at + ": requires 「" + kv[0] + "」 가 다른 갈래");
|
||
|
|
else if (lv < 1 || lv > pre.lvMax) e.Add(at + ": requires " + kv[0] + " Lv " + lv + " > lv_max " + pre.lvMax);
|
||
|
|
n.requires.Add(new DevTree.Req { node = kv[0], lv = lv });
|
||
|
|
}
|
||
|
|
t.nodes.Add(n);
|
||
|
|
}
|
||
|
|
return t;
|
||
|
|
}
|
||
|
|
|
||
|
|
static ChestDb ParseChests(List<Dictionary<string, string>> rows, List<string> e)
|
||
|
|
{
|
||
|
|
var t = ScriptableObject.CreateInstance<ChestDb>();
|
||
|
|
const string f = "chests.csv";
|
||
|
|
if (!Cols(rows, f, e, "id", "name_ko", "points", "p_rare", "p_epic", "p_legend", "p_myth", "gold_min", "gold_max", "gem_min", "gem_max", "source",
|
||
|
|
"iron_min", "iron_max", "buy_gold", "buy_gem", "buy_points")) return t;
|
||
|
|
var seen = new HashSet<string>();
|
||
|
|
foreach (var r in rows)
|
||
|
|
{
|
||
|
|
var c = new ChestDb.Chest
|
||
|
|
{
|
||
|
|
id = Get(r, "id"), nameKo = Get(r, "name_ko"), points = Int(r, "points", f, e),
|
||
|
|
goldMin = Int(r, "gold_min", f, e), goldMax = Int(r, "gold_max", f, e), gemMin = Int(r, "gem_min", f, e), gemMax = Int(r, "gem_max", f, e),
|
||
|
|
ironMin = Int(r, "iron_min", f, e), ironMax = Int(r, "iron_max", f, e), source = Get(r, "source"),
|
||
|
|
buyGold = Int(r, "buy_gold", f, e), buyGem = Int(r, "buy_gem", f, e), buyPoints = Int(r, "buy_points", f, e),
|
||
|
|
};
|
||
|
|
for (int i = 0; i < ChestDb.ProbGrades.Length; i++) c.p[i] = Float(r, "p_" + ChestDb.ProbGrades[i], f, e);
|
||
|
|
if (!Unique(seen, c.id, f, r, e)) continue;
|
||
|
|
float sum = 0f;
|
||
|
|
foreach (var p in c.p) sum += p;
|
||
|
|
if (Mathf.Abs(sum - 1f) > 1e-4f) e.Add(f + ": " + c.id + " 등급 확률 합 = " + sum.ToString("0.####", CultureInfo.InvariantCulture) + " ≠ 1");
|
||
|
|
if (c.goldMin > c.goldMax || c.gemMin > c.gemMax || c.ironMin > c.ironMax) e.Add(f + ": " + c.id + " min > max");
|
||
|
|
t.chests.Add(c);
|
||
|
|
}
|
||
|
|
return t;
|
||
|
|
}
|
||
|
|
|
||
|
|
static TreasureSetDb ParseSets(List<Dictionary<string, string>> rows, List<string> e)
|
||
|
|
{
|
||
|
|
var t = ScriptableObject.CreateInstance<TreasureSetDb>();
|
||
|
|
const string f = "treasure_sets.csv";
|
||
|
|
if (!Cols(rows, f, e, "set_id", "name_ko", "members", "tier_cond", "effect_key", "pct")) return t;
|
||
|
|
foreach (var r in rows)
|
||
|
|
{
|
||
|
|
var tier = new TreasureSetDb.Tier { setId = Get(r, "set_id"), nameKo = Get(r, "name_ko"), tierCond = Get(r, "tier_cond"), effectKey = Get(r, "effect_key"), pct = Float(r, "pct", f, e) };
|
||
|
|
tier.members.AddRange(Get(r, "members").Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries));
|
||
|
|
string at = f + " 행 " + Get(r, "#") + " " + tier.setId;
|
||
|
|
if (tier.tierCond == "set") tier.stars = 0;
|
||
|
|
else if (!tier.tierCond.EndsWith("star", StringComparison.Ordinal) || !int.TryParse(tier.tierCond.Substring(0, tier.tierCond.Length - 4), NumberStyles.Integer, CultureInfo.InvariantCulture, out tier.stars) || tier.stars < 1)
|
||
|
|
e.Add(at + ": tier_cond 「" + tier.tierCond + "」 ≠ set|Nstar");
|
||
|
|
if (Array.IndexOf(TreasureRules.SetEffectKeys, tier.effectKey) < 0) e.Add(at + ": effect_key 「" + tier.effectKey + "」 ≠ hp_pct|atk_pct|def_pct");
|
||
|
|
if (tier.members.Count == 0) e.Add(at + ": members 비었음");
|
||
|
|
foreach (var o in t.tiers)
|
||
|
|
{
|
||
|
|
if (o.setId != tier.setId) continue;
|
||
|
|
if (o.tierCond == tier.tierCond) e.Add(at + ": tier_cond 「" + tier.tierCond + "」 중복");
|
||
|
|
if (string.Join(";", o.members.ToArray()) != string.Join(";", tier.members.ToArray())) e.Add(at + ": 같은 세트의 members 가 행마다 다름");
|
||
|
|
}
|
||
|
|
t.tiers.Add(tier);
|
||
|
|
}
|
||
|
|
return t;
|
||
|
|
}
|
||
|
|
|
||
|
|
static TreasureDb ParseTreasures(List<Dictionary<string, string>> rows, OutGameData d, List<string> e)
|
||
|
|
{
|
||
|
|
var t = ScriptableObject.CreateInstance<TreasureDb>();
|
||
|
|
const string f = "treasures.csv";
|
||
|
|
if (!Cols(rows, f, e, "id", "name_ko", "grade", "set_id", "base_stat", "base_pct", "cond_key", "cond_every", "cond_pct", "cond_max", "star_max", "shards_dup")) return t;
|
||
|
|
var seen = new HashSet<string>();
|
||
|
|
foreach (var r in rows)
|
||
|
|
{
|
||
|
|
var it = new TreasureDb.Item
|
||
|
|
{
|
||
|
|
id = Get(r, "id"), nameKo = Get(r, "name_ko"), grade = Get(r, "grade"), setId = Get(r, "set_id"), baseStat = Get(r, "base_stat"), basePct = Float(r, "base_pct", f, e),
|
||
|
|
condKey = Get(r, "cond_key"), condEvery = Int(r, "cond_every", f, e), condPct = Float(r, "cond_pct", f, e), condMax = Int(r, "cond_max", f, e),
|
||
|
|
starMax = Int(r, "star_max", f, e), shardsDup = Int(r, "shards_dup", f, e),
|
||
|
|
};
|
||
|
|
if (!Unique(seen, it.id, f, r, e)) continue;
|
||
|
|
string at = f + " " + it.id;
|
||
|
|
if (Array.IndexOf(TreasureDb.BaseStats, it.baseStat) < 0) e.Add(at + ": base_stat 「" + it.baseStat + "」 ≠ hp|atk|def|crit|range|splash");
|
||
|
|
if (Array.IndexOf(BattleEvents.Keys, it.condKey) < 0) e.Add(at + ": cond_key 「" + it.condKey + "」 카운터 없음");
|
||
|
|
if (it.condEvery < 1) e.Add(at + ": cond_every < 1");
|
||
|
|
if (it.starMax < 1) e.Add(at + ": star_max < 1");
|
||
|
|
if (it.setId.Length > 0)
|
||
|
|
{
|
||
|
|
var tiers = d.TreasureSets != null ? d.TreasureSets.TiersOf(it.setId) : null;
|
||
|
|
if (tiers == null || tiers.Count == 0) e.Add(at + ": set_id 「" + it.setId + "」 가 treasure_sets.csv 에 없음");
|
||
|
|
else if (!tiers[0].members.Contains(it.id)) e.Add(at + ": 세트 「" + it.setId + "」 members 에 없음");
|
||
|
|
}
|
||
|
|
t.items.Add(it);
|
||
|
|
}
|
||
|
|
if (d.TreasureSets != null)
|
||
|
|
foreach (var tier in d.TreasureSets.tiers)
|
||
|
|
foreach (var m in tier.members)
|
||
|
|
{
|
||
|
|
var it = t.Get(m);
|
||
|
|
if (it == null) e.Add("treasure_sets.csv: " + tier.setId + " member 「" + m + "」 가 treasures.csv 에 없음");
|
||
|
|
else if (it.setId != tier.setId) e.Add("treasure_sets.csv: " + tier.setId + " member 「" + m + "」 의 set_id = " + it.setId);
|
||
|
|
}
|
||
|
|
return t;
|
||
|
|
}
|
||
|
|
|
||
|
|
static TowerTable ParseTower(List<Dictionary<string, string>> rows, OutGameData d, List<string> e)
|
||
|
|
{
|
||
|
|
var t = ScriptableObject.CreateInstance<TowerTable>();
|
||
|
|
const string f = "tower.csv";
|
||
|
|
if (!Cols(rows, f, e, "floor", "hp_mult", "atk_mult", "template_stage", "reward_gem", "reward_blueprint", "chest")) return t;
|
||
|
|
foreach (var r in rows)
|
||
|
|
{
|
||
|
|
var fl = new TowerTable.Floor
|
||
|
|
{
|
||
|
|
floor = Int(r, "floor", f, e), hpMult = Float(r, "hp_mult", f, e), atkMult = Float(r, "atk_mult", f, e), templateStage = Get(r, "template_stage"),
|
||
|
|
rewardGem = Int(r, "reward_gem", f, e), rewardBlueprint = Int(r, "reward_blueprint", f, e), chest = Get(r, "chest"),
|
||
|
|
};
|
||
|
|
int ch, st;
|
||
|
|
if (!ArenaRules.ParseStageKey(fl.templateStage, out ch, out st)) e.Add(f + ": " + fl.floor + "층 template_stage 「" + fl.templateStage + "」 ≠ 챕터-스테이지");
|
||
|
|
if (fl.chest.Length > 0 && (d.Chests == null || d.Chests.Get(fl.chest) == null)) e.Add(f + ": " + fl.floor + "층 chest 「" + fl.chest + "」 가 chests.csv 에 없음");
|
||
|
|
t.floors.Add(fl);
|
||
|
|
}
|
||
|
|
for (int i = 0; i < t.floors.Count; i++)
|
||
|
|
if (t.floors[i].floor != i + 1) e.Add(f + ": floor 가 1부터 연속이 아님(" + (i + 1) + "번째 = " + t.floors[i].floor + ")");
|
||
|
|
return t;
|
||
|
|
}
|
||
|
|
|
||
|
|
static DungeonDb ParseDungeons(List<Dictionary<string, string>> rows, List<string> e)
|
||
|
|
{
|
||
|
|
var t = ScriptableObject.CreateInstance<DungeonDb>();
|
||
|
|
const string f = "dungeons.csv";
|
||
|
|
if (!Cols(rows, f, e, "id", "name_ko", "reward_kind", "weekday", "entries_per_day", "template_stage", "hp_mult", "reward_amount")) return t;
|
||
|
|
var seen = new HashSet<string>();
|
||
|
|
var sep = new[] { ';' };
|
||
|
|
foreach (var r in rows)
|
||
|
|
{
|
||
|
|
var dg = new DungeonDb.Dungeon
|
||
|
|
{
|
||
|
|
id = Get(r, "id"), nameKo = Get(r, "name_ko"), entriesPerDay = Int(r, "entries_per_day", f, e), templateStage = Get(r, "template_stage"), hpMult = Float(r, "hp_mult", f, e),
|
||
|
|
};
|
||
|
|
if (!Unique(seen, dg.id, f, r, e)) continue;
|
||
|
|
string at = f + " " + dg.id;
|
||
|
|
dg.rewardKinds.AddRange(Get(r, "reward_kind").Split(sep, StringSplitOptions.RemoveEmptyEntries));
|
||
|
|
dg.weekdays.AddRange(Get(r, "weekday").Split(sep, StringSplitOptions.RemoveEmptyEntries));
|
||
|
|
foreach (var a in Get(r, "reward_amount").Split(sep, StringSplitOptions.RemoveEmptyEntries))
|
||
|
|
{
|
||
|
|
int v;
|
||
|
|
if (!int.TryParse(a, NumberStyles.Integer, CultureInfo.InvariantCulture, out v)) e.Add(at + ": reward_amount 「" + a + "」 정수 아님");
|
||
|
|
dg.rewardAmounts.Add(v);
|
||
|
|
}
|
||
|
|
foreach (var k in dg.rewardKinds)
|
||
|
|
if (Array.IndexOf(DungeonDb.RewardKinds, k) < 0) e.Add(at + ": reward_kind 「" + k + "」 모름");
|
||
|
|
foreach (var w in dg.weekdays)
|
||
|
|
if (Array.IndexOf(DungeonDb.Weekdays, w) < 0) e.Add(at + ": weekday 「" + w + "」 ≠ mon~sun");
|
||
|
|
if (dg.rewardAmounts.Count != dg.rewardKinds.Count) e.Add(at + ": reward_amount 수 " + dg.rewardAmounts.Count + " ≠ reward_kind 수 " + dg.rewardKinds.Count);
|
||
|
|
int ch, st;
|
||
|
|
if (!ArenaRules.ParseStageKey(dg.templateStage, out ch, out st)) e.Add(at + ": template_stage 「" + dg.templateStage + "」 ≠ 챕터-스테이지");
|
||
|
|
if (dg.entriesPerDay < 1) e.Add(at + ": entries_per_day < 1");
|
||
|
|
t.dungeons.Add(dg);
|
||
|
|
}
|
||
|
|
return t;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>economy.csv C 키 참조(상자 id · 보물 등급 · 적장 영웅).</summary>
|
||
|
|
static void CheckEconomyC(OutGameData d, List<string> e)
|
||
|
|
{
|
||
|
|
var ec = d.Economy;
|
||
|
|
if (ec == null || !AllKeys(ec.values, EconomyConfig.RequiredKeys)) return;
|
||
|
|
foreach (var id in new[] { ec.ChestPointReward, ec.StageChest, ec.PatrolChest, ec.RaidChest })
|
||
|
|
if (d.Chests == null || d.Chests.Get(id) == null) e.Add("economy.csv: 상자 「" + id + "」 가 chests.csv 에 없음");
|
||
|
|
if (ec.TreasureGrades.Count == 0) e.Add("economy.csv: treasure_iron_mult 「grade:배율;…」 형식 아님");
|
||
|
|
foreach (var g in ChestDb.ProbGrades)
|
||
|
|
if (ec.TreasureGradeOrder(g) == 0) e.Add("economy.csv: treasure_iron_mult 에 상자 확률 등급 「" + g + "」 없음");
|
||
|
|
if (d.Treasures != null)
|
||
|
|
foreach (var t in d.Treasures.items)
|
||
|
|
if (ec.TreasureGradeOrder(t.grade) == 0) e.Add("treasures.csv: " + t.id + " grade 「" + t.grade + "」 가 treasure_iron_mult 에 없음");
|
||
|
|
foreach (var id in ec.RaidBosses)
|
||
|
|
if (d.Heroes == null || d.Heroes.Get(id) == null) e.Add("economy.csv: raid_bosses 「" + id + "」 가 heroes.csv 에 없음");
|
||
|
|
if (ec.PatrolCapHours <= 0f) e.Add("economy.csv: patrol_cap_hours ≤ 0");
|
||
|
|
if (ec.ChestPointGoal < 1) e.Add("economy.csv: chest_point_goal < 1");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|