113 lines
6.8 KiB
Python
113 lines
6.8 KiB
Python
|
|
# 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)
|