ThreeKingdomsSlingshot/Assets/Scripts/OutGame/Data/OutGameTables.C.cs

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");
}
}
}