Project_WL/Assets/WL/Combat/Weapon/WLWeaponSkillTable.cs

219 lines
14 KiB
C#
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─────────────────────────────────────────────────────────────────────────────
// 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;
}
}
}