219 lines
14 KiB
C#
219 lines
14 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// WLWeaponSkillTable.cs — 무기별 고유 스킬 표 + 등급표 + 계열 축 로더 (#825 · 2026-10-09 · 기획 v2 §15·§16 + PD 답변)
|
||
//
|
||
// SOT(C45) = Resources/WL/
|
||
// WLWeaponSkillTable.csv : 기획 `WL_무기_스킬_v1.csv` 사본 — 무기(GroupNo) × 슬롯(1 주력 · 2 각성 · 3 기본 #973) → 힘민지 SkillList ID · 쿨 · 해금 등급
|
||
// WLWeaponSkillTier.csv : 기획 `WL_무기_스킬_등급표_v1.csv` 사본 — 등급 8단계(#973 G1~G8 · 기본 G3 · 주력 G5 · 각성 G7 · 짝수 = 부가 능력치)
|
||
// WLWeaponAxis.json : 계열 축(장비 옵션의 「주스탯」 자리) — PD 2026-10-09 「단검 = 치명타 확률 · 검 = 치명타 피해량」
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
using System.Collections.Generic;
|
||
using System.Globalization;
|
||
using Newtonsoft.Json;
|
||
using UnityEngine;
|
||
|
||
namespace WL.Combat.Weapon
|
||
{
|
||
public class WLWeaponSkillRow
|
||
{
|
||
public int weaponId; public string weaponName = ""; public int slot; public int skillId; public string skillName = "", skillClass = "", anim = "", nextAnim = "";
|
||
public int unlockTier = 1; public float coolS; public int mp; public float rangeM; public float dmgRatioBase = 1f;
|
||
public string awaken2Extra = "", frRef = "", vfx = "", srcCoolRef = "", src = "";
|
||
/// <summary>#883 이 칸이 WL 파츠 스킬(WLPartSkillDefs key)이면 힘민지 스킬 슬롯 대신 파츠 스킬로 돈다(skill_id 0 · 예: 파괴자 1번 = guard_shield).</summary>
|
||
public string wlSkill = "";
|
||
public bool IsWlSkill { get { return !string.IsNullOrEmpty(wlSkill); } }
|
||
/// <summary>#890 발동 조건(예: hit:Skill_ShieldAttack = 그 스킬 명중 시) — 값이 있으면 자동 시전 슬롯에 넣지 않고 WLSkillChain 이 조건 때 쿨 없이 시전한다.</summary>
|
||
public string trigger = "";
|
||
public bool IsTriggered { get { return !string.IsNullOrEmpty(trigger); } }
|
||
/// <summary>#900 dash_cast 1 = 대쉬 도착 때 이 칸 스킬이 준비돼 있으면(쿨 · 사거리) 일반 공격 대신 바로 시전(예: 묵광 방패 치기).</summary>
|
||
public bool dashCast;
|
||
/// <summary>#902 first_cast 1 = 쿨이 차 있으면 기본 공격 · 접근보다 먼저 시전(사거리 밖 적은 스킬이 점프로 붙는다 · 해머 강력한 일격).</summary>
|
||
public bool firstCast;
|
||
}
|
||
|
||
public class WLWeaponTierRow
|
||
{
|
||
public int tier; public string tierName = ""; public float statPercent, skillDmgBonus, coolMult = 1f, rangeMult = 1f;
|
||
public int awakenStep, awakenOpen, awakenLevel; public string applyPhase = "", note = "", src = "";
|
||
}
|
||
|
||
public static class WLWeaponSkillTable
|
||
{
|
||
public static bool Loaded { get; private set; }
|
||
public static string LastLog { get; private set; } = "";
|
||
static readonly Dictionary<int, List<WLWeaponSkillRow>> s_byWeapon = new Dictionary<int, List<WLWeaponSkillRow>>();
|
||
static readonly Dictionary<int, WLWeaponTierRow> s_tiers = new Dictionary<int, WLWeaponTierRow>();
|
||
|
||
static float F(string s) { float v; return float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out v) ? v : 0f; }
|
||
static int I(string s) { int v; return int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out v) ? v : 0; }
|
||
|
||
/// <summary>CSV(BOM 허용 · 쉼표 구분 · 따옴표 없음) → 행별 열 사전.</summary>
|
||
internal static List<Dictionary<string, string>> ReadCsv(string text)
|
||
{
|
||
var list = new List<Dictionary<string, string>>();
|
||
var lines = text.Replace("\r\n", "\n").Replace('\r', '\n').Split('\n');
|
||
if (lines.Length < 2) return list;
|
||
var header = lines[0].TrimStart('').Split(',');
|
||
for (int li = 1; li < lines.Length; li++)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(lines[li])) continue;
|
||
var c = lines[li].Split(',');
|
||
var d = new Dictionary<string, string>();
|
||
for (int i = 0; i < header.Length; i++) d[header[i].Trim()] = i < c.Length ? c[i].Trim() : "";
|
||
list.Add(d);
|
||
}
|
||
return list;
|
||
}
|
||
static string G(Dictionary<string, string> d, string k) { string v; return d.TryGetValue(k, out v) ? v : ""; }
|
||
|
||
public static bool EnsureLoaded()
|
||
{
|
||
if (Loaded) return true;
|
||
var cfg = WLWeaponSettings.Instance;
|
||
string sp = cfg != null && !string.IsNullOrEmpty(cfg.skillTablePath) ? cfg.skillTablePath : "WL/WLWeaponSkillTable";
|
||
string tp = cfg != null && !string.IsNullOrEmpty(cfg.tierTablePath) ? cfg.tierTablePath : "WL/WLWeaponSkillTier";
|
||
try
|
||
{
|
||
var ta = Resources.Load<TextAsset>(sp);
|
||
if (ta == null) throw new System.IO.FileNotFoundException("Resources/" + sp + ".csv 없음");
|
||
s_byWeapon.Clear();
|
||
foreach (var d in ReadCsv(ta.text))
|
||
{
|
||
var r = new WLWeaponSkillRow
|
||
{
|
||
weaponId = I(G(d, "weapon_id")), weaponName = G(d, "weapon_name"), slot = I(G(d, "slot")), skillId = I(G(d, "skill_id")),
|
||
skillName = G(d, "skill_name"), skillClass = G(d, "skill_class"), anim = G(d, "anim"), nextAnim = G(d, "next_anim"),
|
||
unlockTier = Mathf.Max(1, I(G(d, "unlock_tier"))), coolS = F(G(d, "cool_s")), mp = I(G(d, "mp")), rangeM = F(G(d, "range_m")),
|
||
dmgRatioBase = F(G(d, "dmg_ratio_base")), awaken2Extra = G(d, "awaken2_extra"), frRef = G(d, "fr_ref"), vfx = G(d, "vfx"),
|
||
srcCoolRef = G(d, "src_cool_ref"), src = G(d, "src"), wlSkill = G(d, "wl_skill"), trigger = G(d, "trigger"),
|
||
dashCast = I(G(d, "dash_cast")) == 1, firstCast = I(G(d, "first_cast")) == 1,
|
||
};
|
||
if (r.weaponId <= 0 || (r.skillId <= 0 && !r.IsWlSkill)) continue;
|
||
List<WLWeaponSkillRow> l;
|
||
if (!s_byWeapon.TryGetValue(r.weaponId, out l)) { l = new List<WLWeaponSkillRow>(); s_byWeapon[r.weaponId] = l; }
|
||
l.Add(r);
|
||
}
|
||
foreach (var kv in s_byWeapon) kv.Value.Sort((a, b) => a.slot.CompareTo(b.slot));
|
||
|
||
var tt = Resources.Load<TextAsset>(tp);
|
||
if (tt == null) throw new System.IO.FileNotFoundException("Resources/" + tp + ".csv 없음");
|
||
s_tiers.Clear();
|
||
foreach (var d in ReadCsv(tt.text))
|
||
{
|
||
var r = new WLWeaponTierRow
|
||
{
|
||
tier = I(G(d, "tier")), tierName = G(d, "tier_name"), statPercent = F(G(d, "stat_percent")), skillDmgBonus = F(G(d, "skill_dmg_bonus")),
|
||
coolMult = F(G(d, "cool_mult")), rangeMult = F(G(d, "range_mult")), awakenStep = I(G(d, "awaken_step")), awakenOpen = I(G(d, "awaken_open")),
|
||
awakenLevel = I(G(d, "awaken_level")), applyPhase = G(d, "apply_phase"), note = G(d, "note"), src = G(d, "src"),
|
||
};
|
||
if (r.tier > 0) s_tiers[r.tier] = r;
|
||
}
|
||
Loaded = s_byWeapon.Count > 0 && s_tiers.Count > 0;
|
||
LastLog = "무기 스킬 표 로드 — 무기 " + s_byWeapon.Count + " · 스킬 행 " + RowCount() + " · 등급 " + s_tiers.Count;
|
||
if (cfg != null && cfg.verboseLog != 0) Debug.Log("[WL-825] " + LastLog);
|
||
}
|
||
catch (System.Exception ex)
|
||
{
|
||
LastLog = "무기 스킬 표 로드 실패 — " + ex.Message;
|
||
Debug.LogError("[WL-825] " + LastLog);
|
||
}
|
||
return Loaded;
|
||
}
|
||
|
||
public static void Reload() { Loaded = false; s_byWeapon.Clear(); s_tiers.Clear(); EnsureLoaded(); }
|
||
static int RowCount() { int n = 0; foreach (var kv in s_byWeapon) n += kv.Value.Count; return n; }
|
||
|
||
/// <summary>무기(GroupNo)의 스킬 행(슬롯 순). 없으면 null.</summary>
|
||
public static List<WLWeaponSkillRow> Rows(int weaponId) { EnsureLoaded(); List<WLWeaponSkillRow> l; return s_byWeapon.TryGetValue(weaponId, out l) ? l : null; }
|
||
public static WLWeaponTierRow Tier(int tier) { EnsureLoaded(); WLWeaponTierRow r; return s_tiers.TryGetValue(tier, out r) ? r : null; }
|
||
|
||
/// <summary>#973 칸 이름 — 1 주력 · 2 각성 · 3 기본(PD 「희귀 G3 기본 · 전설 G5 주력 · 신화 G7 각성」).</summary>
|
||
public static string SlotName(int slot) { return slot == 1 ? "주력" : slot == 2 ? "각성" : slot == 3 ? "기본" : "칸" + slot; }
|
||
|
||
/// <summary>#973 표시 순서 = 해금 등급 순(기본 → 주력 → 각성).</summary>
|
||
public static List<WLWeaponSkillRow> ByUnlock(List<WLWeaponSkillRow> rows)
|
||
{
|
||
var l = rows != null ? new List<WLWeaponSkillRow>(rows) : new List<WLWeaponSkillRow>();
|
||
l.Sort((a, b) => a.unlockTier != b.unlockTier ? a.unlockTier.CompareTo(b.unlockTier) : a.slot.CompareTo(b.slot));
|
||
return l;
|
||
}
|
||
|
||
/// <summary>#973 무기 표시 이름 = 표 weapon_name(예: 1435 「한손검」 · 월광 · 묵광 통합) · 표에 없으면 null.</summary>
|
||
public static string WeaponName(int weaponId) { var rows = Rows(weaponId); return rows != null && rows.Count > 0 && !string.IsNullOrEmpty(rows[0].weaponName) ? rows[0].weaponName : null; }
|
||
|
||
/// <summary>표의 skill_id 가 힘민지 SkillList 에 전부 있는지(구현 순서 1 스모크 기준). 누락 ID 목록을 돌려준다.</summary>
|
||
public static List<int> MissingSkillIds()
|
||
{
|
||
var miss = new List<int>();
|
||
if (!EnsureLoaded() || table_skilllist.Ins == null) return miss;
|
||
foreach (var kv in s_byWeapon) foreach (var r in kv.Value) if (!r.IsWlSkill && table_skilllist.Ins.Get_Data_orNull(r.skillId) == null) miss.Add(r.skillId);
|
||
return miss;
|
||
}
|
||
|
||
/// <summary>UI 한 줄 — 「주력 회심의 일격(쿨 7s) · 각성 돌진 베기(전설부터 · 쿨 16s)」.</summary>
|
||
public static string Describe(int weaponId, int tier)
|
||
{
|
||
var rows = Rows(weaponId);
|
||
if (rows == null || rows.Count == 0) return "스킬 표 없음";
|
||
var sb = new System.Text.StringBuilder();
|
||
var ordered = ByUnlock(rows); // #973 기본 → 주력 → 각성
|
||
for (int i = 0; i < ordered.Count; i++)
|
||
{
|
||
var r = ordered[i];
|
||
if (i > 0) sb.Append(" · ");
|
||
sb.Append(SlotName(r.slot)).Append(' ').Append(r.skillName);
|
||
if (r.IsWlSkill) sb.Append("(파츠 스킬"); else sb.Append("(쿨 ").Append(r.coolS.ToString("0.#")).Append("s");
|
||
if (tier < r.unlockTier) { var t = Tier(r.unlockTier); sb.Append(" · ").Append(t != null ? t.tierName : ("등급 " + r.unlockTier)).Append("부터"); }
|
||
sb.Append(')');
|
||
}
|
||
return sb.ToString();
|
||
}
|
||
}
|
||
|
||
/// <summary>계열 축 정의 — 장비 옵션 「기본 수치 + 주스탯 × 등급%」의 주스탯 자리에 무기 계열별로 넣는 스탯.</summary>
|
||
public class WLWeaponAxisDef { public string key = ""; public string stat = ""; public float scale = 1f; public string name = ""; [JsonIgnore] public eStat estat; [JsonIgnore] public bool valid; }
|
||
class WLWeaponAxisJson { public List<WLWeaponAxisDef> axes; }
|
||
|
||
public static class WLWeaponAxis
|
||
{
|
||
public static bool Loaded { get; private set; }
|
||
public static string LastLog { get; private set; } = "";
|
||
static readonly Dictionary<string, WLWeaponAxisDef> s_axes = new Dictionary<string, WLWeaponAxisDef>();
|
||
|
||
public static bool EnsureLoaded()
|
||
{
|
||
if (Loaded) return true;
|
||
var cfg = WLWeaponSettings.Instance;
|
||
string path = cfg != null && !string.IsNullOrEmpty(cfg.axisPath) ? cfg.axisPath : "WL/WLWeaponAxis";
|
||
try
|
||
{
|
||
var ta = Resources.Load<TextAsset>(path);
|
||
if (ta == null) throw new System.IO.FileNotFoundException("Resources/" + path + ".json 없음");
|
||
var j = JsonConvert.DeserializeObject<WLWeaponAxisJson>(ta.text);
|
||
s_axes.Clear();
|
||
if (j != null && j.axes != null)
|
||
foreach (var a in j.axes)
|
||
{
|
||
if (a == null || string.IsNullOrEmpty(a.key)) continue;
|
||
eStat e; a.valid = System.Enum.TryParse(a.stat, out e); a.estat = a.valid ? e : eStat.ATK;
|
||
s_axes[a.key] = a;
|
||
}
|
||
Loaded = s_axes.Count > 0;
|
||
LastLog = "계열 축 로드 — " + s_axes.Count;
|
||
}
|
||
catch (System.Exception ex) { LastLog = "계열 축 로드 실패 — " + ex.Message; Debug.LogError("[WL-825] " + LastLog); }
|
||
return Loaded;
|
||
}
|
||
|
||
/// <summary>무기(GroupNo) → 축 정의(공격 표 main_stat 키). 표에 없으면 false(호출자는 직업 주스탯 유지).</summary>
|
||
public static bool TryFor(int weaponId, out WLWeaponAxisDef axis)
|
||
{
|
||
axis = null;
|
||
if (!EnsureLoaded()) return false;
|
||
var f = WLWeaponAttackTable.First(weaponId);
|
||
if (f == null || string.IsNullOrEmpty(f.mainStat)) return false;
|
||
return s_axes.TryGetValue(f.mainStat, out axis) && axis != null && axis.valid;
|
||
}
|
||
}
|
||
}
|