145 lines
8.3 KiB
C#
145 lines
8.3 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// WLWeaponAttackTable.cs — 무기별 기본 공격 파라미터 표 로더 (#825 · 2026-10-09)
|
||
//
|
||
// SOT = `Resources/WL/WLWeaponAttackTable.csv`(기획 `공유/기획팀_산출물/WL/무기스킬_기획_2026-10-09/WL_무기_기본공격_파라미터_v1.csv` 사본 · C45).
|
||
// 한 행 = 무기(weapon_id = FR GroupNo · 신규 9001~) × 콤보 단계(step). class_id = 그 무기가 쓰는 힘민지 직업(모션 세트).
|
||
// src 칸 = 그 행에서 실측인 칸 이름(M:…) · 나머지는 기획 추정값.
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
using System.Collections.Generic;
|
||
using System.Globalization;
|
||
using UnityEngine;
|
||
|
||
namespace WL.Combat.Weapon
|
||
{
|
||
public class WLWeaponAttackStep
|
||
{
|
||
public int weaponId; public string weaponName = "", family = "";
|
||
public int classId; public string animController = ""; public int unlockPhase; public string mainStat = "";
|
||
public float attackSpeedMult = 1f; public int step; public string animState = "", hitType = "";
|
||
public int hits = 1; public float dmgRatioPerHit = 1f, rangeM, arcDeg, aoeRadiusM, projSpeed; public int pierce; public float moveM;
|
||
public string hitboxPrefab = "", slashVfx = "", hitVfx = "", refSkillIds = "", src = "";
|
||
}
|
||
|
||
public static class WLWeaponAttackTable
|
||
{
|
||
public static bool Loaded { get; private set; }
|
||
public static string LastLog { get; private set; } = "";
|
||
static readonly Dictionary<int, List<WLWeaponAttackStep>> s_byWeapon = new Dictionary<int, List<WLWeaponAttackStep>>();
|
||
static readonly List<int> s_weaponIds = new List<int>();
|
||
|
||
public static IList<int> WeaponIds { get { EnsureLoaded(); return s_weaponIds; } }
|
||
|
||
public static bool EnsureLoaded()
|
||
{
|
||
if (Loaded) return true;
|
||
var cfg = WLWeaponSettings.Instance;
|
||
string path = cfg != null && !string.IsNullOrEmpty(cfg.attackTablePath) ? cfg.attackTablePath : "WL/WLWeaponAttackTable";
|
||
try
|
||
{
|
||
var ta = Resources.Load<TextAsset>(path);
|
||
if (ta == null) throw new System.IO.FileNotFoundException("Resources/" + path + ".csv 없음");
|
||
Parse(ta.text);
|
||
Loaded = s_byWeapon.Count > 0;
|
||
LastLog = "무기 공격 표 로드 — 무기 " + s_byWeapon.Count + " · 행 " + RowCount();
|
||
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_weaponIds.Clear(); EnsureLoaded(); }
|
||
|
||
static int RowCount() { int n = 0; foreach (var kv in s_byWeapon) n += kv.Value.Count; return n; }
|
||
|
||
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; }
|
||
|
||
static void Parse(string text)
|
||
{
|
||
s_byWeapon.Clear(); s_weaponIds.Clear();
|
||
var lines = text.Replace("\r\n", "\n").Replace('\r', '\n').Split('\n');
|
||
if (lines.Length < 2) return;
|
||
var header = lines[0].TrimStart('').Split(',');
|
||
var col = new Dictionary<string, int>();
|
||
for (int i = 0; i < header.Length; i++) col[header[i].Trim()] = i;
|
||
System.Func<string[], string, string> g = (cells, name) => { int ix; return col.TryGetValue(name, out ix) && ix < cells.Length ? cells[ix].Trim() : ""; };
|
||
for (int li = 1; li < lines.Length; li++)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(lines[li])) continue;
|
||
var c = lines[li].Split(',');
|
||
var s = new WLWeaponAttackStep
|
||
{
|
||
weaponId = I(g(c, "weapon_id")), weaponName = g(c, "weapon_name"), family = g(c, "family"),
|
||
classId = I(g(c, "class_id")), animController = g(c, "anim_controller"), unlockPhase = I(g(c, "unlock_phase")), mainStat = g(c, "main_stat"),
|
||
attackSpeedMult = F(g(c, "attack_speed_mult")), step = I(g(c, "step")), animState = g(c, "anim_state"), hitType = g(c, "hit_type"),
|
||
hits = Mathf.Max(1, I(g(c, "hits"))), dmgRatioPerHit = F(g(c, "dmg_ratio_per_hit")), rangeM = F(g(c, "range_m")), arcDeg = F(g(c, "arc_deg")),
|
||
aoeRadiusM = F(g(c, "aoe_radius_m")), projSpeed = F(g(c, "proj_speed")), pierce = I(g(c, "pierce")), moveM = F(g(c, "move_m")),
|
||
hitboxPrefab = g(c, "hitbox_prefab"), slashVfx = g(c, "slash_vfx"), hitVfx = g(c, "hit_vfx"), refSkillIds = g(c, "ref_skill_ids"), src = g(c, "src"),
|
||
};
|
||
if (s.weaponId <= 0) continue;
|
||
List<WLWeaponAttackStep> l;
|
||
if (!s_byWeapon.TryGetValue(s.weaponId, out l)) { l = new List<WLWeaponAttackStep>(); s_byWeapon[s.weaponId] = l; s_weaponIds.Add(s.weaponId); }
|
||
l.Add(s);
|
||
}
|
||
foreach (var kv in s_byWeapon) kv.Value.Sort((a, b) => a.step.CompareTo(b.step));
|
||
}
|
||
|
||
/// <summary>무기(FR GroupNo)의 콤보 단계 목록. 없으면 null.</summary>
|
||
public static List<WLWeaponAttackStep> Steps(int weaponId)
|
||
{
|
||
EnsureLoaded();
|
||
List<WLWeaponAttackStep> l;
|
||
return s_byWeapon.TryGetValue(weaponId, out l) ? l : null;
|
||
}
|
||
|
||
public static WLWeaponAttackStep First(int weaponId) { var l = Steps(weaponId); return l != null && l.Count > 0 ? l[0] : null; }
|
||
|
||
/// <summary>#893 — 그 계열(family) 행들의 사거리(range_m) 최댓값. 계열이 비었거나 행이 없으면 0.</summary>
|
||
public static float MaxRangeOfFamily(string family)
|
||
{
|
||
if (string.IsNullOrEmpty(family)) return 0f;
|
||
EnsureLoaded();
|
||
float best = 0f;
|
||
foreach (var kv in s_byWeapon)
|
||
for (int i = 0; i < kv.Value.Count; i++)
|
||
if (kv.Value[i].family == family && kv.Value[i].rangeM > best) best = kv.Value[i].rangeM;
|
||
return best;
|
||
}
|
||
|
||
/// <summary>무기가 쓰는 힘민지 직업 ID. 표에 없으면 0.</summary>
|
||
public static int ClassIdFor(int weaponId) { var f = First(weaponId); return f != null ? f.classId : 0; }
|
||
|
||
public static string HitTypeName(string hitType)
|
||
{
|
||
switch (hitType)
|
||
{
|
||
case "MeleeArc": return "근접 호";
|
||
case "Projectile": return "투사체";
|
||
case "Area": return "범위";
|
||
case "MultiHit": return "연타";
|
||
default: return hitType;
|
||
}
|
||
}
|
||
|
||
/// <summary>UI 한 줄 — 「검 · 직업 10101 · 근접 호 3단 · 사거리 1.5 m · 배율 1.0/1.1/1.6」.</summary>
|
||
public static string Describe(int weaponId)
|
||
{
|
||
var l = Steps(weaponId);
|
||
if (l == null || l.Count == 0) return "공격 표 없음 (그룹 " + weaponId + ")";
|
||
var f = l[0];
|
||
var sb = new System.Text.StringBuilder();
|
||
sb.Append(f.family).Append(" · 직업 ").Append(f.classId).Append(" · ");
|
||
for (int i = 0; i < l.Count; i++) { if (i > 0) sb.Append('/'); sb.Append(HitTypeName(l[i].hitType)); if (l[i].hits > 1) sb.Append('×').Append(l[i].hits); }
|
||
sb.Append(" · 사거리 ").Append(f.rangeM.ToString("0.#")).Append(" m · 배율 ");
|
||
for (int i = 0; i < l.Count; i++) { if (i > 0) sb.Append('/'); sb.Append(l[i].dmgRatioPerHit.ToString("0.##")); }
|
||
if (f.attackSpeedMult != 1f) sb.Append(" · 속도 ×").Append(f.attackSpeedMult.ToString("0.##"));
|
||
return sb.ToString();
|
||
}
|
||
}
|
||
}
|