# 장비 파츠 스킬 — FR(FirstRising) 스킬 원본 표 → WL 런타임 레벨표(CSV) 생성 (PD 지시 #879 · 2026-10-16) # # 입력(읽기 전용 · FR 레포는 고치지 않는다): /Assets/DataTables/ # CommonSkillTable · SkillTable(하위 스킬) · EliteRuneCommonSkillTable · LegendRuneCommonSkillTable · PassiveSkillTable · Localize* # 출력: Assets/WL/PartSkill/Resources/WL/PartSkill/ # WLPartSkillLevels.csv — 액티브 · 하위 스킬 · 룬(이후 엑스트라 버프용) 단계별 수치 # WLPartPassiveLevels.csv — FR 패시브 Lv1~5 수치 + 각성 열쇠 # 규칙 # · FR 표는 엑셀 병합 칸이 빈칸으로 나온다 → 같은 스킬 안에서 빈칸 = 앞 단계 값 유지(WL 해석 · 레벨이 올라도 바뀌지 않은 값) # · 효과 문자열(Effect)의 줄바꿈 = ' | ' · 대괄호 안 쉼표 = ';' (WL CSV 는 따옴표 없는 쉼표 구분) # 실행: python -I Tools/PartSkill/build_partskill_levels.py [FR 프로젝트 루트] (기본 = 환경변수 FR_ROOT 또는 C:\BurningTimes\FirstRising) import csv, json, os, re, sys HERE = os.path.dirname(os.path.abspath(__file__)) WL = os.path.dirname(os.path.dirname(HERE)) FR = sys.argv[1] if len(sys.argv) > 1 else os.environ.get("FR_ROOT", os.path.join("C:\\", "BurningTimes", "FirstRising")) DT = os.path.join(FR, "Assets", "DataTables") OUT = os.path.join(WL, "Assets", "WL", "PartSkill", "Resources", "WL", "PartSkill") # 하위 스킬(SkillTable) 중 파츠 스킬 · 시너지 · 무기 시너지 · 장비 옵션 스킬이 쓰는 것 — 목록 = 파츠 스킬 기획 v2 분류 SUB_IDS = [10411, 10412, 10511, 10512, 10581, 10582, 10583, 10401, 10501, 10601, 10602, 10701, 11301, 10101, 11101, 10301, 10302, 11001, 11401, 110001, 110011, 110031, 110041, 110033, 110051, 110061, 900011, 900012, 900013, 900014] COLS = ["src", "id", "name", "level", "count", "int_min", "int_max", "new_spawn", "spawn_min", "spawn_max", "duration", "cool", "hit_range", "hit_angle", "splash", "target_range", "proj_speed", "proj_rand_angle", "proj_target_range", "orbit_speed", "stop_skill", "multi_hit", "seq_interval", "dmg_pct", "effects"] SRC_KEYS = {"count": "SpawnCount", "int_min": "SpawnIntervalMin", "int_max": "SpawnIntervalMax", "new_spawn": "NewSpawn", "spawn_min": "SpawnRangeMin", "spawn_max": "SpawnRangeMax", "duration": "Duration", "cool": "CoolTime", "hit_range": "HitRange", "hit_angle": "HitAngle", "splash": "SplashHitRange", "target_range": "TargetingRange", "proj_speed": "ProjectileSpeed", "proj_rand_angle": "ProjectileRandomAngle", "proj_target_range": "ProjectileTargetingRange", "orbit_speed": "OrbitSpeed", "stop_skill": "StopSkill", "multi_hit": "MultiHitCount", "seq_interval": "SequenceSpawnInterval", "dmg_pct": "DamagePercentage", "effects": "Effect"} def load(name): d = json.load(open(os.path.join(DT, name + ".json"), encoding="utf-8-sig")) return d if isinstance(d, list) else next(v for v in d.values() if isinstance(v, list)) def eff(s): if not s: return "" s = re.sub(r"\[([^\]]*)\]", lambda m: "[" + m.group(1).replace(",", ";").replace(" ", "") + "]", str(s)) return " | ".join(p.strip() for p in s.replace("\r", "").split("\n") if p.strip()) def rows_of(name, src, names, only=None, level_filter=None): out, cur_id, cur_name, prev, prev_level = [], None, "", {}, None for r in load(name): if r.get("Index"): if r["Index"] != cur_id: prev = {} cur_id = r["Index"] if r.get("Desc"): cur_name = r["Desc"] lv = r.get("Level", prev_level) prev_level = lv if only is not None and cur_id not in only: continue row = {"src": src, "id": cur_id, "name": names.get(str(cur_id), cur_name).replace(",", " "), "level": lv} for k, fk in SRC_KEYS.items(): v = r.get(fk) if v is None or v == "": v = prev.get(k, "") # 빈칸 = 앞 단계 값 유지 row[k] = eff(v) if k == "effects" else v prev[k] = row[k] if level_filter is None or level_filter(lv): out.append(row) return out def main(): loc = {str(r["Id"]): r.get("Korean", "") for r in load("LocalizeCommonSkillTable")} names = {k[:-3]: v for k, v in loc.items() if k.endswith("_01")} rows = rows_of("CommonSkillTable", "common", names) rows += rows_of("SkillTable", "sub", {}, only=set(SUB_IDS)) rows += rows_of("EliteRuneCommonSkillTable", "rune_elite", names) rows += rows_of("LegendRuneCommonSkillTable", "rune_legend", names) os.makedirs(OUT, exist_ok=True) with open(os.path.join(OUT, "WLPartSkillLevels.csv"), "w", encoding="utf-8-sig", newline="") as f: w = csv.writer(f, lineterminator="\n") w.writerow(COLS) for r in rows: w.writerow([r.get(c, "") for c in COLS]) ploc = {str(r["Id"]): r.get("Korean", "") for r in load("LocalizePassiveSkillTable")} # WL 표시 이름 = WLPartSkillDefs.csv 의 name(패시브 lv_id 기준) — FR 원문 이름 대신 WL 용어로(예: FR 110 원문 → 「범위 확장」) defs = os.path.join(OUT, "WLPartSkillDefs.csv") if os.path.exists(defs): for d in csv.DictReader(open(defs, encoding="utf-8-sig")): if d.get("lv_src") == "passive" and d.get("lv_id"): ploc[d["lv_id"]] = d["name"] prow, cur, pname = [], None, "" for r in load("PassiveSkillTable"): if r.get("Index"): cur = r["Index"] if r.get("Desc"): pname = r["Desc"] prow.append([cur, ploc.get(str(cur), pname).replace(",", " "), r.get("Level", ""), r.get("Ability", ""), r.get("AbilityValue0", ""), r.get("AbilityValue1", ""), str(r.get("OpenCommonAwakenSkill", "")).replace(",", ";"), r.get("OpenWeaponAwakenSkill", "")]) with open(os.path.join(OUT, "WLPartPassiveLevels.csv"), "w", encoding="utf-8-sig", newline="") as f: w = csv.writer(f, lineterminator="\n") w.writerow(["id", "name", "level", "ability", "v0", "v1", "open_common", "open_weapon"]) w.writerows(prow) print("levels", len(rows), "· passives", len(prow), "→", OUT) if __name__ == "__main__": main()