# TBH 추출 테이블 → 기획서용 파생 데이터(요약·곡선) 생성 # 입력: E:\BurningTimes\scratchpad\TBH_v1.2.4\textassets (레포 밖 원본) # 출력: E:\BurningTimes\공유\기획\FirstRising\레퍼런스_TBH\data\derived_*.csv (요약 수치만) import csv, os, json, math, collections SRC = r"E:\BurningTimes\scratchpad\TBH_v1.2.4\textassets" OUT = r"E:\BurningTimes\공유\기획\FirstRising\레퍼런스_TBH\data" os.makedirs(OUT, exist_ok=True) def T(n): with open(os.path.join(SRC, n + ".csv"), encoding="utf-8-sig", newline="") as fh: return [r for r in csv.DictReader(fh) if any((v or "").strip() for v in r.values())] def W(name, header, rows): with open(os.path.join(OUT, name), "w", encoding="utf-8-sig", newline="") as fh: w = csv.writer(fh); w.writerow(header); w.writerows(rows) num = lambda s: int(s) if (s or "").strip() else 0 S = {} # 1) 스테이지 레벨 배율(% → 배) sl = T("StageLevelInfoData") W("derived_stage_level_scaling.csv", ["StageLevel", "MonsterATK_x", "MonsterHP_x", "Gold_x", "EXP_x"], [[r["StageLevel"], num(r["MonsterAtkDmgMultiplier"]) / 100, num(r["MonsterHpMultiplier"]) / 100, num(r["MonsterGoldMultiplier"]) / 100, num(r["MonsterExpMultiplier"]) / 100] for r in sl]) S["stage_level_max"] = max(num(r["StageLevel"]) for r in sl) # 2) 영웅 레벨 EXP lv = T("LevelInfoData"); cum = 0; rows = [] for r in lv: cum += num(r["ExpForLevelUp"]); rows.append([r["Level"], num(r["ExpForLevelUp"]), cum]) W("derived_hero_level_exp.csv", ["Level", "ExpForLevelUp", "CumulativeExp"], rows) S["hero_exp_total_to_100"] = sum(num(r["ExpForLevelUp"]) for r in lv[:99]) # 3) 오프라인 보상 orw = T("OfflineRewardInfoData") W("derived_offline_reward.csv", ["StageLevel", "BaseGold", "BaseExp", "KillCount", "ClearCount"], [[r["StageLevel"], r["BaseGold"], r["BaseExp"], r["KillCount"], r["ClearCount"]] for r in orw]) # 4) 큐브 레벨 cl = T("CubeLevelInfoData") W("derived_cube_level_exp.csv", ["Level", "ExpForLevelUp"], [[r["Level"], r["ExpForLevelUp"]] for r in cl]) S["cube_exp_total_to_100"] = sum(num(r["ExpForLevelUp"]) for r in cl[:99]) # 5) 칸 해금 비용(누적) for n in ("InventoryInfoData", "StashInfoData", "StorageInfoData"): rs = T(n); c = 0; rows = [] for r in rs: c += num(r["CostForUnlock"]); rows.append([r["Index"], r["CostForUnlock"], c]) W(f"derived_slots_{n.replace('InfoData','').lower()}.csv", ["Index", "Cost", "Cumulative"], rows) S[f"slots_{n}"] = {"total": len(rs), "free": sum(1 for r in rs if num(r["CostForUnlock"]) == 0), "gold_total": c} # 6) 등급 표(합성 결과 가중치 → 확률) gd = T("GradeInfoData"); rows = [] for g in gd: ws = [num(g[k]) for k in ("Lower2GradeWeight", "Lower1GradeWeight", "SameGradeWeight", "Higher1GradeWeight", "Higher2GradeWeight")] tot = sum(ws) or 1 rows.append([g["GRADE"]] + [round(100 * w / tot, 2) for w in ws] + [g["InherentSlotAmount"], g["ExtraSlotAmount_Decoration"], g["ExtraSlotAmount_Engraving"], g["ExtraSlotAmount_Inscription"], g["BaseAlchemyGold"], g["BaseCubeExp"], g["BaseContamin"]]) W("derived_grade_table.csv", ["GRADE", "P_-2", "P_-1", "P_same", "P_+1", "P_+2", "Inherent", "Decoration", "Engraving", "Inscription", "BaseAlchemyGold", "BaseCubeExp", "BaseContamin"], rows) # 7) 스테이지 구조·드롭률(백만분율 → %) st = T("StageInfoData") agg = collections.defaultdict(list) for s in st: agg[(s["STAGETYPE"], s["STAGEDIFFICULITY"])].append(s) rows = [] for (t, d), ss in sorted(agg.items()): mr = sorted(set(num(s["MonsterDropItemRateM"]) / 10000 for s in ss if s["MonsterDropItemRateM"])) br = sorted(set(num(s["BossDropItemRateM"]) / 10000 for s in ss if s["BossDropItemRateM"])) lvls = [num(s["StageLevel"]) for s in ss] waves = [num(s["WaveAmount"]) for s in ss if s["WaveAmount"]] wm = [num(s["WaveMonsterAmount"]) for s in ss if s["WaveMonsterAmount"]] rows.append([t, d, len(ss), f"{min(lvls)}~{max(lvls)}", f"{min(waves)}~{max(waves)}" if waves else "", f"{min(wm)}~{max(wm)}" if wm else "", "/".join(f"{x:g}" for x in mr), "/".join(f"{x:g}" for x in br)]) W("derived_stage_structure.csv", ["STAGETYPE", "DIFFICULTY", "Count", "StageLevel", "Waves", "MonstersPerWave", "MonsterChest_%", "BossChest_%"], rows) # 8) 룬 비용 분포 rl = T("RuneLevelInfoData"); ri = T("RuneInfoData") costs = sorted(num(r["CostValue"]) for r in rl) buck = collections.Counter(int(math.log10(c)) if c > 0 else 0 for c in costs) W("derived_rune_cost_buckets.csv", ["log10_bucket", "count"], sorted(buck.items())) stat = collections.Counter(r["STATTYPE"] for r in rl) W("derived_rune_stats.csv", ["STATTYPE", "level_rows"], stat.most_common()) S["rune"] = {"nodes": len(ri), "level_rows": len(rl), "gold_total": sum(costs), "min_cost": costs[0], "max_cost": costs[-1], "median_cost": costs[len(costs) // 2]} # 8-1) 룬 비용 대분류(순서 중요: 구체적인 것부터 매칭) rcat = [("오프라인 보상", ("Offline",)), ("큐브", ("Cube",)), ("자동화·편의", ("Unlock", "OpenAll", "OpenOne", "ReduceAutoOpen", "WaveCount")), ("보관 한도", ("MaxAmount", "MaxInventorySlot")), ("상자 드롭률", ("DropChance",)), ("골드·EXP 획득", ("Gold", "Exp")), ("전투(전 영웅)", ("AllHero",))] agg = collections.OrderedDict((c, [0, 0]) for c, _ in rcat) for r in rl: for c, ks in rcat: if any(k in r["STATTYPE"] for k in ks): agg[c][0] += 1; agg[c][1] += num(r["CostValue"]); break W("derived_rune_categories.csv", ["category", "level_rows", "gold"], [[c, n, g] for c, (n, g) in agg.items()]) S["rune"]["categories"] = {c: {"rows": n, "gold": g} for c, (n, g) in agg.items()} # 8-2) 자동화·편의 해금 노드(최소 비용순) auto = collections.defaultdict(list) for r in rl: if any(k in r["STATTYPE"] for k in ("Unlock", "OpenAll", "OpenOne", "ReduceAutoOpen", "MaxAmount", "WaveCount", "MaxInventorySlot")): auto[r["STATTYPE"]].append((num(r["CostValue"]), r["Value"])) W("derived_rune_automation.csv", ["STATTYPE", "levels", "min_cost", "max_cost", "first_value"], sorted([[k, len(v), min(x[0] for x in v), max(x[0] for x in v), sorted(v)[0][1]] for k, v in auto.items()], key=lambda x: x[2])) # 9) 아이템 구성 it = T("ItemInfoData") S["items"] = {"total": len(it), "by_type": collections.Counter(i["ITEMTYPE"] for i in it), "marketable": sum(1 for i in it if i["IsCanExchangeMarketable"] == "True"), "marketable_grades": collections.Counter(i["GRADE"] for i in it if i["IsCanExchangeMarketable"] == "True" and i["ITEMTYPE"] == "GEAR")} json.dump(S, open(os.path.join(OUT, "derived_summary.json"), "w", encoding="utf-8"), ensure_ascii=False, indent=1, default=dict) print(json.dumps(S, ensure_ascii=False, indent=1, default=dict)) for r in rows: print(r)