108 lines
6.2 KiB
Python
108 lines
6.2 KiB
Python
# 장비 파츠 스킬 — FR(FirstRising) 스킬 원본 표 → WL 런타임 레벨표(CSV) 생성 (PD 지시 #879 · 2026-10-16)
|
|
#
|
|
# 입력(읽기 전용 · FR 레포는 고치지 않는다): <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()
|