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

145 lines
8.3 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.

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