BurningTimesAi/공유/기획/FirstRising/레퍼런스_TBH/tools/tbh_derive.py

113 lines
6.8 KiB
Python
Raw Permalink Normal View History

# 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)