379 lines
21 KiB
C#
379 lines
21 KiB
C#
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|||
|
|
// WLPartSkillData.cs — 장비 파츠 스킬 데이터 로더 (PD 지시 #879 · 2026-10-16)
|
|||
|
|
//
|
|||
|
|
// PD 원문(10-15 · 10-16): 무기 = 기본 공격 + 공격 스킬 · 갑옷 = 방어 스킬 · 장갑 · 신발 = 패시브 · 목걸이 · 반지 = 공격 또는 방어 스킬.
|
|||
|
|
// 스킬은 쿨마다 자동 발동(VS 류) · FR 스킬 이식 · 액티브 · 패시브 모두 장비 등급으로 개방 + 레벨 상승 ·
|
|||
|
|
// 액티브 5단계 + 짝 패시브 5단계 = 시너지 스킬(임시 버프 수준 수치 OK) · 이후 FR 룬으로 엑스트라 버프(데이터만으로 붙도록 효과 목록 구조 유지).
|
|||
|
|
// 데이터(C45 · 코드 상수 0) = Resources/WL/PartSkill/
|
|||
|
|
// WLPartSkill.json 설정(등급 → 단계 · 배율 · 거리 환산 · HUD)
|
|||
|
|
// WLPartSkillDefs.csv 스킬 정의(부위 · 동작 · 레벨표 출처 · 시너지 짝 · 이펙트 · 아이콘 · 설명)
|
|||
|
|
// WLPartSkillItemMap.csv 장비 그룹(GroupNo) → 스킬
|
|||
|
|
// WLPartSkillLevels.csv FR 원본 단계별 수치(생성 = Tools/PartSkill/build_partskill_levels.py) · WLPartSkillLevelsWL.csv = WL 변형 스킬
|
|||
|
|
// WLPartPassiveLevels.csv FR 패시브 Lv1~5
|
|||
|
|
// WLPartStats.json 부위 기본 능력치(반지 근접/원거리 · 목걸이 랜덤 풀 · 신발 이동 속도)
|
|||
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
using System.Collections.Generic;
|
|||
|
|
using System.Globalization;
|
|||
|
|
using Newtonsoft.Json;
|
|||
|
|
using UnityEngine;
|
|||
|
|
using WL.Equip;
|
|||
|
|
|
|||
|
|
namespace WL.PartSkill
|
|||
|
|
{
|
|||
|
|
public sealed class WLPartSkillHudCfg
|
|||
|
|
{
|
|||
|
|
public int enabled = 1, sortingOrder = 30;
|
|||
|
|
public float anchorX, anchorY, x = 24, y = 300, size = 92, gap = 12, passiveSize = 64, fontPx = 26, bannerFontPx = 56, bannerY = -420;
|
|||
|
|
public Dictionary<string, float[]> colors = new Dictionary<string, float[]>();
|
|||
|
|
public Color Col(string key, Color fb)
|
|||
|
|
{
|
|||
|
|
float[] c;
|
|||
|
|
if (colors != null && colors.TryGetValue(key, out c) && c != null && c.Length >= 3) return new Color(c[0], c[1], c[2], c.Length > 3 ? c[3] : 1f);
|
|||
|
|
return fb;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public sealed class WLPartSkillSettings
|
|||
|
|
{
|
|||
|
|
public int enabled = 1, verbose = 1;
|
|||
|
|
public string defsCsv = "WL/PartSkill/WLPartSkillDefs", itemMapCsv = "WL/PartSkill/WLPartSkillItemMap", passiveCsv = "WL/PartSkill/WLPartPassiveLevels";
|
|||
|
|
public string statsJson = "WL/PartSkill/WLPartStats", fxFolder = "WL/PartSkill/Fx/", iconFolder = "WL/PartSkill/Icons/";
|
|||
|
|
public List<string> levelCsvs = new List<string>();
|
|||
|
|
public float[] tierLevel = new float[0], tierMult = new float[0];
|
|||
|
|
public int synergyLevel = 5;
|
|||
|
|
public float distanceScale = 0.6f, speedScale = 0.6f, minRadius = 0.6f, damageScale = 1f, intervalUnit = 0.1f, combatRange = 9f;
|
|||
|
|
public int castOnlyNearEnemies = 1;
|
|||
|
|
public float enemyCacheSec = 0.15f, perTargetHitSec = 0.5f;
|
|||
|
|
public int maxLiveObjects = 48;
|
|||
|
|
public float projectileMaxTravel = 14f, pickupRadius = 1.2f, fxGroundY = 0.08f, hitFxScale = 0.7f;
|
|||
|
|
public int fxPoolPerKey = 8;
|
|||
|
|
public string synergyFx = "fx_synergy";
|
|||
|
|
public float synergyBannerSec = 2.2f, spiritFollowDist = 1.6f, spiritHeight = 1.4f, spiritKingHeight = 2.2f, spiritKingFollowDist = 2.2f;
|
|||
|
|
public float projectileBlockRadiusPad = 0.3f, bombFlightSec = 0.45f, corpseMemorySec = 4f;
|
|||
|
|
/// <summary>회전 도끼 · 작은 도끼 — 모델을 눕히는 각(X 축) · 초당 회전(Y 축).</summary>
|
|||
|
|
public float axeTiltDeg = 90f, axeSpinDegPerSec = 900f;
|
|||
|
|
/// <summary>#881 해머 무기 시너지 분신.</summary>
|
|||
|
|
public WLPartSkillCloneCfg clone = new WLPartSkillCloneCfg();
|
|||
|
|
/// <summary>이펙트 키 → 배율 1 일 때 그 프리팹의 반경(m) — 스킬 범위에 맞춰 크기를 정한다.</summary>
|
|||
|
|
public Dictionary<string, float> fxRadius = new Dictionary<string, float>();
|
|||
|
|
/// <summary>이펙트 키 → 켤 때 끌 하위 오브젝트 이름(지나치게 넓은 바닥 빛 등).</summary>
|
|||
|
|
public Dictionary<string, List<string>> fxHide = new Dictionary<string, List<string>>();
|
|||
|
|
public WLPartSkillHudCfg hud = new WLPartSkillHudCfg();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public enum WLPartKind { Active, Passive, WeaponSyn, Combine }
|
|||
|
|
|
|||
|
|
public sealed class WLPartSkillDef
|
|||
|
|
{
|
|||
|
|
public string key, name, synName, kindText, attr, behavior, lvSrc, synKey, combineKey, fxMain, fxHit, fxExtra, icon, desc, synDesc;
|
|||
|
|
/// <summary>#895 무기 시너지의 두 번째 장비 조건(파츠 스킬 키 · 비우면 짝 패시브 syn_key 하나만). 무기와 무관하게 장착 장비로만 성립한다.</summary>
|
|||
|
|
public string reqKey = "";
|
|||
|
|
public eFREquipmentType part;
|
|||
|
|
public WLPartKind kind;
|
|||
|
|
public int lvId, subId, synLevel;
|
|||
|
|
public int hmSkill; // #883 behavior hm_skill 이 쏘는 힘민지 SkillList ID(예: 702014 단검 투척)
|
|||
|
|
public float v1, v2, v3;
|
|||
|
|
public bool HasSynergy { get { return !string.IsNullOrEmpty(synKey) || !string.IsNullOrEmpty(combineKey); } }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>레벨표 1행(FR CommonSkillTable · SkillTable 칸 그대로). 효과 칸은 「이름[키:값;…] | …」 → effects 사전.</summary>
|
|||
|
|
public sealed class WLPartLevelRow
|
|||
|
|
{
|
|||
|
|
public string src, name, effectsText;
|
|||
|
|
public int id, level;
|
|||
|
|
public float count, intMin, intMax, newSpawn, spawnMin, spawnMax, duration, cool, hitRange, hitAngle, splash, targetRange, projSpeed, projRandAngle, projTargetRange, orbitSpeed, stopSkill, multiHit, seqInterval, dmgPct;
|
|||
|
|
public readonly Dictionary<string, Dictionary<string, float>> effects = new Dictionary<string, Dictionary<string, float>>();
|
|||
|
|
public bool Has(string effect) { return effects.ContainsKey(effect); }
|
|||
|
|
public float Eff(string effect, string param, float fallback)
|
|||
|
|
{
|
|||
|
|
Dictionary<string, float> p; float v;
|
|||
|
|
return effects.TryGetValue(effect, out p) && p != null && p.TryGetValue(param, out v) ? v : fallback;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public sealed class WLPartPassiveRow { public int id, level, ability; public string name; public float v0, v1; }
|
|||
|
|
|
|||
|
|
public sealed class WLPartStatPool { public string id, name, stat; public float[] perTier = new float[0]; public float pct = 100f; }
|
|||
|
|
public sealed class WLPartRingKind { public string id, name; }
|
|||
|
|
public sealed class WLPartRename { public int equipType; public string from, to; }
|
|||
|
|
public sealed class WLPartStatsCfg
|
|||
|
|
{
|
|||
|
|
public List<string> meleeFamilies = new List<string>(), rangedFamilies = new List<string>();
|
|||
|
|
public List<WLPartRingKind> ringKinds = new List<WLPartRingKind>();
|
|||
|
|
public List<WLPartRename> renameItems = new List<WLPartRename>();
|
|||
|
|
public float[] shoesMovePerTier = new float[0];
|
|||
|
|
public List<WLPartStatPool> necklacePool = new List<WLPartStatPool>();
|
|||
|
|
/// <summary>부위 번호 → 기본 능력치 종류 이름(빈 슬롯 안내 · PD 10-16 원문).</summary>
|
|||
|
|
public Dictionary<string, string> partStatNames = new Dictionary<string, string>();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static class WLPartSkillData
|
|||
|
|
{
|
|||
|
|
public const string SettingsPath = "WL/PartSkill/WLPartSkill";
|
|||
|
|
public static bool Loaded { get; private set; }
|
|||
|
|
public static string LastLog { get; private set; } = "";
|
|||
|
|
static bool s_tried;
|
|||
|
|
static WLPartSkillSettings s_settings;
|
|||
|
|
static WLPartStatsCfg s_stats;
|
|||
|
|
|
|||
|
|
static readonly List<WLPartSkillDef> s_defs = new List<WLPartSkillDef>();
|
|||
|
|
static readonly Dictionary<string, WLPartSkillDef> s_defByKey = new Dictionary<string, WLPartSkillDef>();
|
|||
|
|
static readonly Dictionary<int, string> s_groupToKey = new Dictionary<int, string>();
|
|||
|
|
static readonly Dictionary<string, List<WLPartLevelRow>> s_levels = new Dictionary<string, List<WLPartLevelRow>>();
|
|||
|
|
static readonly Dictionary<int, List<WLPartPassiveRow>> s_passives = new Dictionary<int, List<WLPartPassiveRow>>();
|
|||
|
|
static readonly Dictionary<string, Sprite> s_icons = new Dictionary<string, Sprite>();
|
|||
|
|
|
|||
|
|
public static WLPartSkillSettings S { get { EnsureLoaded(); return s_settings ?? (s_settings = new WLPartSkillSettings { enabled = 0 }); } }
|
|||
|
|
public static WLPartStatsCfg Stats { get { EnsureLoaded(); return s_stats ?? (s_stats = new WLPartStatsCfg()); } }
|
|||
|
|
public static bool Enabled { get { return EnsureLoaded() && s_settings != null && s_settings.enabled != 0; } }
|
|||
|
|
public static IList<WLPartSkillDef> Defs { get { EnsureLoaded(); return s_defs; } }
|
|||
|
|
|
|||
|
|
public static void Reload() { Loaded = false; s_tried = false; s_icons.Clear(); EnsureLoaded(); }
|
|||
|
|
|
|||
|
|
public static bool EnsureLoaded()
|
|||
|
|
{
|
|||
|
|
if (Loaded) return true;
|
|||
|
|
if (s_tried) return false;
|
|||
|
|
s_tried = true;
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
var ta = Resources.Load<TextAsset>(SettingsPath);
|
|||
|
|
if (ta == null) throw new System.IO.FileNotFoundException("Resources/" + SettingsPath + ".json 없음");
|
|||
|
|
s_settings = JsonConvert.DeserializeObject<WLPartSkillSettings>(ta.text) ?? new WLPartSkillSettings();
|
|||
|
|
if (s_settings.hud == null) s_settings.hud = new WLPartSkillHudCfg();
|
|||
|
|
var st = Resources.Load<TextAsset>(s_settings.statsJson);
|
|||
|
|
s_stats = st != null ? (JsonConvert.DeserializeObject<WLPartStatsCfg>(st.text) ?? new WLPartStatsCfg()) : new WLPartStatsCfg();
|
|||
|
|
LoadDefs(); LoadItemMap(); LoadLevels(); LoadPassives();
|
|||
|
|
Loaded = true;
|
|||
|
|
LastLog = "파츠 스킬 표 로드 — 정의 " + s_defs.Count + " · 아이템 " + s_groupToKey.Count + " · 레벨표 " + s_levels.Count + " · 패시브 " + s_passives.Count;
|
|||
|
|
if (s_settings.verbose != 0) Debug.Log("[WL-879] " + LastLog);
|
|||
|
|
}
|
|||
|
|
catch (System.Exception ex)
|
|||
|
|
{
|
|||
|
|
LastLog = "파츠 스킬 표 로드 실패 — " + ex.Message;
|
|||
|
|
Debug.LogError("[WL-879] " + LastLog);
|
|||
|
|
}
|
|||
|
|
return Loaded;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── CSV ─────────────────────────────────────────────────────────────
|
|||
|
|
static List<Dictionary<string, string>> ReadCsv(string path)
|
|||
|
|
{
|
|||
|
|
var rows = new List<Dictionary<string, string>>();
|
|||
|
|
var ta = Resources.Load<TextAsset>(path);
|
|||
|
|
if (ta == null) { Debug.LogWarning("[WL-879] CSV 없음 — Resources/" + path); return rows; }
|
|||
|
|
string text = ta.text;
|
|||
|
|
if (text.Length > 0 && text[0] == '') text = text.Substring(1);
|
|||
|
|
var lines = text.Replace("\r", "").Split('\n');
|
|||
|
|
if (lines.Length == 0) return rows;
|
|||
|
|
var head = SplitCsv(lines[0]);
|
|||
|
|
for (int i = 1; i < lines.Length; i++)
|
|||
|
|
{
|
|||
|
|
if (string.IsNullOrWhiteSpace(lines[i])) continue;
|
|||
|
|
var cells = SplitCsv(lines[i]);
|
|||
|
|
var d = new Dictionary<string, string>(head.Count);
|
|||
|
|
for (int c = 0; c < head.Count; c++) d[head[c].Trim()] = c < cells.Count ? cells[c].Trim() : "";
|
|||
|
|
rows.Add(d);
|
|||
|
|
}
|
|||
|
|
return rows;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static List<string> SplitCsv(string line)
|
|||
|
|
{
|
|||
|
|
var list = new List<string>(24);
|
|||
|
|
var sb = new System.Text.StringBuilder();
|
|||
|
|
bool q = false;
|
|||
|
|
for (int i = 0; i < line.Length; i++)
|
|||
|
|
{
|
|||
|
|
char ch = line[i];
|
|||
|
|
if (q)
|
|||
|
|
{
|
|||
|
|
if (ch == '"') { if (i + 1 < line.Length && line[i + 1] == '"') { sb.Append('"'); i++; } else q = false; }
|
|||
|
|
else sb.Append(ch);
|
|||
|
|
}
|
|||
|
|
else if (ch == '"') q = true;
|
|||
|
|
else if (ch == ',') { list.Add(sb.ToString()); sb.Length = 0; }
|
|||
|
|
else sb.Append(ch);
|
|||
|
|
}
|
|||
|
|
list.Add(sb.ToString());
|
|||
|
|
return list;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static string G(Dictionary<string, string> d, string k) { string v; return d.TryGetValue(k, out v) ? v : ""; }
|
|||
|
|
public static float F(string s, float fb = 0f) { float v; return float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out v) ? v : fb; }
|
|||
|
|
static int I(string s, int fb = 0) { int v; return int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out v) ? v : fb; }
|
|||
|
|
|
|||
|
|
static void LoadDefs()
|
|||
|
|
{
|
|||
|
|
s_defs.Clear(); s_defByKey.Clear();
|
|||
|
|
foreach (var r in ReadCsv(s_settings.defsCsv))
|
|||
|
|
{
|
|||
|
|
var d = new WLPartSkillDef
|
|||
|
|
{
|
|||
|
|
key = G(r, "key"), name = G(r, "name"), synName = G(r, "syn_name"), kindText = G(r, "kind"), attr = G(r, "attr"), behavior = G(r, "behavior"),
|
|||
|
|
lvSrc = G(r, "lv_src"), lvId = I(G(r, "lv_id")), subId = I(G(r, "sub_id")), synKey = G(r, "syn_key"), combineKey = G(r, "combine_key"),
|
|||
|
|
synLevel = I(G(r, "syn_level")), fxMain = G(r, "fx_main"), fxHit = G(r, "fx_hit"), fxExtra = G(r, "fx_extra"), icon = G(r, "icon"),
|
|||
|
|
v1 = F(G(r, "v1")), v2 = F(G(r, "v2")), v3 = F(G(r, "v3")), desc = G(r, "desc"), synDesc = G(r, "syn_desc"), hmSkill = I(G(r, "hm_skill")),
|
|||
|
|
reqKey = G(r, "req_key")
|
|||
|
|
};
|
|||
|
|
if (string.IsNullOrEmpty(d.key)) continue;
|
|||
|
|
eFREquipmentType part;
|
|||
|
|
d.part = System.Enum.TryParse(G(r, "part"), out part) ? part : eFREquipmentType.None;
|
|||
|
|
switch (d.kindText)
|
|||
|
|
{
|
|||
|
|
case "passive": d.kind = WLPartKind.Passive; break;
|
|||
|
|
case "weapon_syn": d.kind = WLPartKind.WeaponSyn; break;
|
|||
|
|
case "combine": d.kind = WLPartKind.Combine; break;
|
|||
|
|
default: d.kind = WLPartKind.Active; break;
|
|||
|
|
}
|
|||
|
|
s_defs.Add(d); s_defByKey[d.key] = d;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static void LoadItemMap()
|
|||
|
|
{
|
|||
|
|
s_groupToKey.Clear();
|
|||
|
|
foreach (var r in ReadCsv(s_settings.itemMapCsv))
|
|||
|
|
{
|
|||
|
|
int g = I(G(r, "group_no")); string k = G(r, "key");
|
|||
|
|
if (g > 0 && !string.IsNullOrEmpty(k)) s_groupToKey[g] = k;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static void LoadLevels()
|
|||
|
|
{
|
|||
|
|
s_levels.Clear();
|
|||
|
|
if (s_settings.levelCsvs == null) return;
|
|||
|
|
foreach (var path in s_settings.levelCsvs)
|
|||
|
|
foreach (var r in ReadCsv(path))
|
|||
|
|
{
|
|||
|
|
var row = new WLPartLevelRow
|
|||
|
|
{
|
|||
|
|
src = G(r, "src"), id = I(G(r, "id")), name = G(r, "name"), level = I(G(r, "level"), 1),
|
|||
|
|
count = F(G(r, "count")), intMin = F(G(r, "int_min")), intMax = F(G(r, "int_max")), newSpawn = F(G(r, "new_spawn")),
|
|||
|
|
spawnMin = F(G(r, "spawn_min")), spawnMax = F(G(r, "spawn_max")), duration = F(G(r, "duration")), cool = F(G(r, "cool")),
|
|||
|
|
hitRange = F(G(r, "hit_range")), hitAngle = F(G(r, "hit_angle")), splash = F(G(r, "splash")), targetRange = F(G(r, "target_range")),
|
|||
|
|
projSpeed = F(G(r, "proj_speed")), projRandAngle = F(G(r, "proj_rand_angle")), projTargetRange = F(G(r, "proj_target_range")),
|
|||
|
|
orbitSpeed = F(G(r, "orbit_speed")), stopSkill = F(G(r, "stop_skill")), multiHit = F(G(r, "multi_hit")), seqInterval = F(G(r, "seq_interval")),
|
|||
|
|
dmgPct = F(G(r, "dmg_pct")), effectsText = G(r, "effects")
|
|||
|
|
};
|
|||
|
|
ParseEffects(row);
|
|||
|
|
string key = row.src + ":" + row.id;
|
|||
|
|
List<WLPartLevelRow> list;
|
|||
|
|
if (!s_levels.TryGetValue(key, out list)) s_levels[key] = list = new List<WLPartLevelRow>();
|
|||
|
|
list.Add(row);
|
|||
|
|
}
|
|||
|
|
foreach (var l in s_levels.Values) l.Sort((a, b) => a.level.CompareTo(b.level));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>「Knock[Distance:5;Speed:10] | Penetration」 → {Knock:{Distance:5,Speed:10}, Penetration:{}}</summary>
|
|||
|
|
static void ParseEffects(WLPartLevelRow row)
|
|||
|
|
{
|
|||
|
|
row.effects.Clear();
|
|||
|
|
if (string.IsNullOrEmpty(row.effectsText)) return;
|
|||
|
|
foreach (var part in row.effectsText.Split('|'))
|
|||
|
|
{
|
|||
|
|
var p = part.Trim(); if (p.Length == 0) continue;
|
|||
|
|
int b = p.IndexOf('[');
|
|||
|
|
string name = b < 0 ? p : p.Substring(0, b).Trim();
|
|||
|
|
var args = new Dictionary<string, float>();
|
|||
|
|
if (b >= 0)
|
|||
|
|
{
|
|||
|
|
int e = p.LastIndexOf(']');
|
|||
|
|
string inner = e > b ? p.Substring(b + 1, e - b - 1) : p.Substring(b + 1);
|
|||
|
|
foreach (var kv in inner.Split(';'))
|
|||
|
|
{
|
|||
|
|
int c = kv.IndexOf(':'); if (c <= 0) continue;
|
|||
|
|
args[kv.Substring(0, c).Trim()] = F(kv.Substring(c + 1).Trim());
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
row.effects[name] = args;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static void LoadPassives()
|
|||
|
|
{
|
|||
|
|
s_passives.Clear();
|
|||
|
|
foreach (var r in ReadCsv(s_settings.passiveCsv))
|
|||
|
|
{
|
|||
|
|
var p = new WLPartPassiveRow { id = I(G(r, "id")), name = G(r, "name"), level = I(G(r, "level"), 1), ability = I(G(r, "ability")), v0 = F(G(r, "v0")), v1 = F(G(r, "v1")) };
|
|||
|
|
List<WLPartPassiveRow> list;
|
|||
|
|
if (!s_passives.TryGetValue(p.id, out list)) s_passives[p.id] = list = new List<WLPartPassiveRow>();
|
|||
|
|
list.Add(p);
|
|||
|
|
}
|
|||
|
|
foreach (var l in s_passives.Values) l.Sort((a, b) => a.level.CompareTo(b.level));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 조회 ────────────────────────────────────────────────────────────
|
|||
|
|
public static WLPartSkillDef Def(string key) { EnsureLoaded(); WLPartSkillDef d; return key != null && s_defByKey.TryGetValue(key, out d) ? d : null; }
|
|||
|
|
|
|||
|
|
public static WLPartSkillDef DefForGroup(int groupNo)
|
|||
|
|
{
|
|||
|
|
EnsureLoaded(); string k;
|
|||
|
|
return s_groupToKey.TryGetValue(groupNo, out k) ? Def(k) : null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>그 스킬을 주는 장비 그룹들(도감 · 상세 안내).</summary>
|
|||
|
|
public static List<int> GroupsOf(string key)
|
|||
|
|
{
|
|||
|
|
EnsureLoaded();
|
|||
|
|
var l = new List<int>();
|
|||
|
|
foreach (var kv in s_groupToKey) if (kv.Value == key) l.Add(kv.Key);
|
|||
|
|
return l;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>레벨표 행 — 그 레벨이 없으면 가장 가까운 낮은 레벨(없으면 첫 행).</summary>
|
|||
|
|
public static WLPartLevelRow Level(string src, int id, int level)
|
|||
|
|
{
|
|||
|
|
EnsureLoaded();
|
|||
|
|
List<WLPartLevelRow> l;
|
|||
|
|
if (!s_levels.TryGetValue(src + ":" + id, out l) || l.Count == 0) return null;
|
|||
|
|
WLPartLevelRow best = l[0];
|
|||
|
|
for (int i = 0; i < l.Count; i++) if (l[i].level <= level) best = l[i];
|
|||
|
|
return best;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static int MaxLevel(string src, int id)
|
|||
|
|
{
|
|||
|
|
EnsureLoaded();
|
|||
|
|
List<WLPartLevelRow> l;
|
|||
|
|
return s_levels.TryGetValue(src + ":" + id, out l) && l.Count > 0 ? l[l.Count - 1].level : 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static WLPartPassiveRow Passive(int id, int level)
|
|||
|
|
{
|
|||
|
|
EnsureLoaded();
|
|||
|
|
List<WLPartPassiveRow> l;
|
|||
|
|
if (!s_passives.TryGetValue(id, out l) || l.Count == 0) return null;
|
|||
|
|
WLPartPassiveRow best = l[0];
|
|||
|
|
for (int i = 0; i < l.Count; i++) if (l[i].level <= level) best = l[i];
|
|||
|
|
return best;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static int TierLevel(int tier)
|
|||
|
|
{
|
|||
|
|
var t = S.tierLevel;
|
|||
|
|
if (t == null || t.Length == 0) return 0;
|
|||
|
|
return Mathf.RoundToInt(t[Mathf.Clamp(tier, 0, t.Length - 1)]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static float TierMult(int tier)
|
|||
|
|
{
|
|||
|
|
var t = S.tierMult;
|
|||
|
|
if (t == null || t.Length == 0) return 1f;
|
|||
|
|
return t[Mathf.Clamp(tier, 0, t.Length - 1)];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static Sprite Icon(string key)
|
|||
|
|
{
|
|||
|
|
if (string.IsNullOrEmpty(key)) return null;
|
|||
|
|
Sprite s;
|
|||
|
|
if (s_icons.TryGetValue(key, out s)) return s;
|
|||
|
|
s = Resources.Load<Sprite>(S.iconFolder + key);
|
|||
|
|
s_icons[key] = s;
|
|||
|
|
return s;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|