// ───────────────────────────────────────────────────────────────────────────── // WLPartStats.cs — 장비 부위 기본 능력치 중 「배율 · 조건」 몫 (PD 10-16 원문 #879) // // PD: 무기 = 무기 타입별 공격력 · 갑옷 = 방어력 · 장갑 = 생명력 · 신발(부츠) = 이동속도 · 반지 = 근접 공격력과 원거리 공격력 중 랜덤 · 목걸이 = 랜덤 스텟 // - 평평한 값(공격 · 생명 · 방어)과 반지의 근접/원거리 공격력 = WLEquipStats(기존 식 · 부위 표 WLGradeTable.json parts) 가 더한다. // - 여기 = 신발 이동 속도 · 목걸이 랜덤 스탯 · 반지 근접/원거리 판정 · 파츠 패시브 스탯을 「훅 1」(ActorStatInfo.Reset 안 · 최종 공식 전)에 더한다. // - 반지 종류 · 목걸이 스탯 = 아이템 고유 번호(_id)로 정한다(같은 아이템은 늘 같은 값 · 새로 얻으면 다시 뽑힘). // - 벨트 → 반지 이름 = WLPartStats.json renameItems(FR 원본 파일 무수정 · 로드 뒤 표시 이름만). // 값 = Resources/WL/PartSkill/WLPartStats.json(C45) · 제안값. // ───────────────────────────────────────────────────────────────────────────── using System.Collections.Generic; using UnityEngine; using WL.Equip; namespace WL.PartSkill { public static class WLPartStats { public static string LastHook1 { get; private set; } = ""; public static int Hook1Count; static int Hash(Item it) { if (it == null || it.rawItem == null) return 0; unchecked { int h = it.rawItem._id * 73856093 ^ it.rawItem.index * 19349663; return h & 0x7fffffff; } } public static int TierOf(Item it) { WLGradeTier t; int plus; return it != null && it.itemJson != null && WLEquipGrades.TryMap(it.itemJson, out t, out plus) ? t.tier : 1; } static float At(float[] arr, int i) { return arr == null || arr.Length == 0 ? 0f : arr[Mathf.Clamp(i, 0, arr.Length - 1)]; } // ── 반지 ──────────────────────────────────────────────────────────── public static WLPartRingKind RingKind(Item it) { var k = WLPartSkillData.Stats.ringKinds; if (k == null || k.Count == 0 || it == null) return null; return k[Hash(it) % k.Count]; } /// 장착 무기의 계열(WLWeaponAttackTable family) — Sword · Hammer · Bow … public static string EquippedWeaponFamily() { var w = WLInventory.EquippedInSlot(eFREquipmentType.Weapon); if (w == null || w.itemJson == null) return ""; var f = WL.Combat.Weapon.WLWeaponAttackTable.First(w.itemJson.GroupNo); return f != null ? f.family : ""; } public static string WeaponReach(string family) { var S = WLPartSkillData.Stats; if (S.meleeFamilies != null && S.meleeFamilies.Contains(family)) return "melee"; if (S.rangedFamilies != null && S.rangedFamilies.Contains(family)) return "ranged"; return ""; } /// 반지 효과가 지금 무기와 맞는가(근접 무기 = 근접 공격력 · 원거리 무기 = 원거리 공격력). public static bool RingMatches(Item ring, out string ringName) { var k = RingKind(ring); ringName = k != null ? k.name : "공격력"; if (k == null) return true; return WeaponReach(EquippedWeaponFamily()) == k.id; } // ── 목걸이 · 신발 ──────────────────────────────────────────────────── public static WLPartStatPool NecklaceStat(Item it) { var pool = WLPartSkillData.Stats.necklacePool; if (pool == null || pool.Count == 0 || it == null) return null; return pool[Hash(it) % pool.Count]; } public static float NecklaceValue(Item it, out WLPartStatPool p) { p = NecklaceStat(it); return p != null ? At(p.perTier, TierOf(it)) : 0f; } public static float ShoesMove(Item it) { return it == null ? 0f : At(WLPartSkillData.Stats.shoesMovePerTier, TierOf(it)); } /// 표시 문자열 — 「이동 속도 +3%」. public static string PoolText(WLPartStatPool p, float v) { if (p == null) return ""; return p.name + " +" + (v * p.pct).ToString(p.pct >= 100f ? "0.#" : "0.##") + "%"; } /// WLPartSkillState.Rebuild 가 부른다 — 목걸이 랜덤 스탯 · 신발 이동 속도를 구성에 더한다. public static void AddPartStats(WLPartLoadout L) { WLPartSlot s; if (L.byPart.TryGetValue(eFREquipmentType.Shose, out s) && s.item != null) L.moveBonus += ShoesMove(s.item); if (L.byPart.TryGetValue(eFREquipmentType.Necklace, out s) && s.item != null) { WLPartStatPool p; float v = NecklaceValue(s.item, out p); if (p != null && v != 0f) { switch (p.stat) { case "MOVE": L.moveBonus += v; break; case "PART_SKILL_DMG": L.dmgMul += v; break; case "PART_SKILL_COOL": L.coolMul *= Mathf.Max(0.1f, 1f - v); break; default: eStat e; if (System.Enum.TryParse(p.stat, out e)) L.AddStat(e, v); break; } } } } /// 훅 1 — WLEquipStats.Apply(ActorStatInfo.Reset 안 · PC 만)에서 부른다. 최종 공식(ATK × 배율 등) 전에 배율을 더한다. public static void ApplyHook1(ActorStatInfo info) { if (info == null || !WLPartSkillData.Enabled) return; WLPartSkillState.EnsureHooked(); if (WLPartSkillState.Version == 0) WLPartSkillState.Rebuild("hook1"); var L = WLPartSkillState.Current; var sb = new System.Text.StringBuilder(); foreach (var kv in L.statAdds) { if (kv.Value == 0d) continue; info.Set_Stat(true, kv.Key, kv.Value); sb.Append(kv.Key).Append('+').Append(kv.Value.ToString("0.###")).Append(' '); } Hook1Count++; LastHook1 = sb.ToString(); } // ── 이름 바꾸기(벨트 → 반지) ───────────────────────────────────────── public static int RenameItems(IList items) { if (items == null || !WLPartSkillData.EnsureLoaded()) return 0; var rules = WLPartSkillData.Stats.renameItems; if (rules == null || rules.Count == 0) return 0; int n = 0; for (int i = 0; i < items.Count; i++) { var j = items[i]; if (j == null || string.IsNullOrEmpty(j.Item_name)) continue; for (int r = 0; r < rules.Count; r++) { var rule = rules[r]; if (rule == null || (int)j.EquipmentType != rule.equipType || string.IsNullOrEmpty(rule.from) || !j.Item_name.Contains(rule.from)) continue; j.Item_name = j.Item_name.Replace(rule.from, rule.to ?? ""); n++; } } return n; } // ── UI 한 줄 ──────────────────────────────────────────────────────── /// 장비 1개의 기본 능력치 한 줄(슬롯 아래 · 상세 팝업). 평평한 값은 WLEquipStats.ItemValues 를 쓴다. public static string ItemStatLine(Item it, bool shortForm) { if (it == null || it.itemJson == null || !it.itemJson.IsEquipment) return ""; var type = it.itemJson.EquipmentType; if (type == eFREquipmentType.Shose) return "이동 속도 +" + (ShoesMove(it) * 100f).ToString("0.#") + "%"; if (type == eFREquipmentType.Necklace) { WLPartStatPool p; float v = NecklaceValue(it, out p); return p != null ? PoolText(p, v) : ""; } eStat o; double bv, bonus; string line; if (!WLEquipStats.ItemValues(it, WLEquipStats.CurrentMainStat(), out o, out bv, out bonus, out line)) return ""; if (type == eFREquipmentType.Belt) { string rn; bool ok = RingMatches(it, out rn); if (shortForm) return ok ? rn + " +" + (bv + bonus).ToString("F1") : "" + rn + " +" + (bv + bonus).ToString("F1") + ""; return rn + " +" + (bv + bonus).ToString("F1") + (ok ? "" : " (지금 무기와 맞지 않아 효과 없음)"); } return WLEquipGrades.OutputName(o) + " +" + (bv + bonus).ToString("F1"); } } }