Project_WL/Assets/WL/Equip/WLEquipStats.cs

315 lines
19 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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.

// ─────────────────────────────────────────────────────────────────────────────
// WLEquipStats.cs — 장비 능력치를 힘민지 ActorStatInfo 에 반영 (#821 → 2026-10-09 등급 7단계 개편 #823)
//
// PD 2026-10-09 원문: 등급별 「기본 수치 + 스텟 N % 비중」 · 배율 상한 없음.
// Lead 해석(PD 미지정 부분): 「스텟」 = 직업 주스탯 · 「기본 수치」 = FR 레벨표 값 → 장비 옵션 = 기본 수치 + 주스탯 × 등급 %
// 기본 수치 = FR 레벨표 값(AttackAbility / HealthAbility) × WLEquipSettings.levelStatScale
// 주스탯 = ActorStatInfo.Get_MainStatValue() — 직업 주스탯(힘·민첩·지능) · Reset 이 끝난 뒤의 최종값
// 등급 % = Resources/WL/WLGradeTable.json tiers[].statPercent (일반 10 … 신화 400)
// 부위 환산 = parts[].statFactor · 출력 = parts[].output (공격 부위 → ATK · 체력 부위 → MaxHP)
// 훅 1 = MyValue.cs ActorStatInfo.Reset() 안 `Apply(this, m_Role)` — 주스탯이 아직 최종값이 아니므로 재계산 번호만 올린다
// (구 FR 등급 % 옵션 abilityMap 은 applyLegacyFrOptions = 1 일 때만 여기서 더한다 · 기본 0).
// 훅 2 = Reset 뒤 `EnforceBaseAttack(info)` — WLInventory.Notify → RefreshPc · WLEquipLauncher 0.5 s 틱.
// ATK : pcBaseAttackOverride > 0 → 「기본값 + 장비 공격 합」으로 맞춘다(멱등) · 0 → 원본 공식에 장비 공격 합을 더한다
// MaxHP: 장비 생명 합을 더한다 — 재계산 1회(ResetSeq)마다 1번, 그 뒤 장착이 바뀌면 차이만 더한다
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections.Generic;
using UnityEngine;
namespace WL.Equip
{
public static class WLEquipStats
{
public static string LastReport { get; private set; } = "";
public static int LastAppliedItems { get; private set; }
/// <summary>장착 장비 공격 합(기본 수치 + 주스탯 보너스) — ATK 목표값 = pcBaseAttackOverride + 이 값.</summary>
public static double LastEquipAttack { get; private set; }
public static double LastEquipHp { get; private set; }
public static double LastEquipDef { get; private set; }
public static double LastMainStat { get; private set; }
public static int BaseAttackEnforced;
public static string LastEnforceLog { get; private set; } = "";
/// <summary>훅 1 이 돈 횟수 — 생명 합을 재계산 1회당 1번만 더하기 위한 번호.</summary>
public static int ResetSeq { get; private set; }
struct Applied { public int seq; public double hp; public double atk; public double def; }
static readonly Dictionary<ActorStatInfo, Applied> s_applied = new Dictionary<ActorStatInfo, Applied>();
static readonly List<ItemOptionElement> s_tmp = new List<ItemOptionElement>(8);
static readonly HashSet<eFRAbility> s_mapped = new HashSet<eFRAbility>();
static bool s_mappedBuilt;
public static bool IsMapped(eFRAbility a)
{
if (!s_mappedBuilt)
{
s_mappedBuilt = true;
var cfg = WLEquipSettings.Instance;
if (cfg != null && cfg.abilityMap != null)
for (int i = 0; i < cfg.abilityMap.Count; i++) s_mapped.Add(cfg.abilityMap[i].ability);
}
return s_mapped.Contains(a);
}
/// <summary>훅 1 — ActorStatInfo.Reset() 안에서 부른다(PC 만). 재계산 번호를 올리고, 구 FR % 옵션은 설정이 켜진 경우만 더한다.</summary>
public static void Apply(ActorStatInfo info, eRole role)
{
if (info == null || role != eRole.PC) return;
var cfg = WLEquipSettings.Instance;
if (cfg == null || cfg.enabled_ == 0) return;
ResetSeq++;
if (cfg.applyLegacyFrOptions != 0) ApplyLegacyOptions(info, cfg);
WL.PartSkill.WLPartStats.ApplyHook1(info); // #879 파츠 패시브 · 목걸이 랜덤 스탯 = 배율(최종 공식 전) · 설정 off 면 무동작
}
static void ApplyLegacyOptions(ActorStatInfo info, WLEquipSettings cfg)
{
if (!WLInventory.EnsureLoaded()) return;
var data = WLInventory.Data;
if (data == null || cfg.abilityMap == null) return;
for (int i = 0; i < data.equipItems.Count; i++)
{
var item = WLInventory.FindItem(data.equipItems[i]);
if (item == null || !item.itemJson.IsEquipment) continue;
s_tmp.Clear();
WLEquipTables.CollectOpenedOptions(item.itemJson, s_tmp);
for (int o = 0; o < s_tmp.Count; o++)
{
var e = s_tmp[o];
for (int a = 0; a < e.Abilities.Count; a++)
{
var ab = e.Abilities[a];
for (int m = 0; m < cfg.abilityMap.Count; m++)
{
var map = cfg.abilityMap[m];
if (map.ability != ab.Ability) continue;
info.Set_Stat(true, map.stat, ab.AbilityValue * map.scale);
}
}
}
}
}
public struct Totals { public double atk; public double hp; public double def; public double mainStat; public int items; public string report; }
/// <summary>장착 장비 합계 — 부위별 (기본 수치 + 주스탯 × 등급 % × 환산 계수). UI 와 같은 계산.</summary>
public static Totals Compute(ActorStatInfo info)
{
var t = new Totals { report = "" };
var cfg = WLEquipSettings.Instance;
if (cfg == null || cfg.enabled_ == 0 || info == null) return t;
if (!WLInventory.EnsureLoaded() || !WLEquipGrades.EnsureLoaded()) { t.report = WLEquipGrades.LastLog; return t; }
var data = WLInventory.Data;
if (data == null || data.equipItems.Count == 0) { t.report = "장착 0"; return t; }
double main = 0;
try { main = info.Get_MainStatValue(); } catch { }
t.mainStat = main;
var sb = new System.Text.StringBuilder();
for (int i = 0; i < data.equipItems.Count; i++)
{
var item = WLInventory.FindItem(data.equipItems[i]);
eStat outStat; double bv, bonus; string line;
if (!ItemValues(item, main, out outStat, out bv, out bonus, out line)) continue;
t.items++;
// #879 반지 = 지금 무기 계열(근접/원거리)과 맞을 때만 공격력에 더한다
string ringName;
if (item.itemJson.EquipmentType == eFREquipmentType.Belt && WLEquipGrades.IsRingOutput(WLEquipGrades.Part(item.itemJson.EquipmentType)) &&
!WL.PartSkill.WLPartStats.RingMatches(item, out ringName)) { sb.Append(item.itemJson.Item_name).Append(": 무기 계열 불일치(0) | "); continue; }
if (outStat == eStat.MaxHP) t.hp += bv + bonus; else if (outStat == eStat.DEF) t.def += bv + bonus; else t.atk += bv + bonus;
sb.Append(item.itemJson.Item_name).Append(": ").Append(line).Append(" | ");
}
t.report = sb.ToString();
return t;
}
/// <summary>아이템 1개의 기본 수치·주스탯 보너스. line = 표시용 계산식(「공격 = 기본 7.0 + 힘 12 × 10% = 8.2」).</summary>
/// <param name="levelOverride">0 보다 크면 그 강화 레벨로 계산(팝업 「다음 레벨 증가분」 표시용 · #881)</param>
public static bool ItemValues(Item item, double mainStat, out eStat outStat, out double baseValue, out double bonus, out string line, int levelOverride = -1)
{
outStat = eStat.ATK; baseValue = 0; bonus = 0; line = "";
var cfg = WLEquipSettings.Instance;
if (cfg == null || item == null || item.itemJson == null || !item.itemJson.IsEquipment) return false;
if (!WLEquipGrades.EnsureLoaded()) return false;
var part = WLEquipGrades.Part(item.itemJson.EquipmentType);
WLGradeTier tier; int plus;
if (part == null || !WLEquipGrades.TryMap(item.itemJson, out tier, out plus)) return false;
if (WLEquipGrades.NoEffect(part, tier.tier)) { line = "효과 없음 — " + tier.name + " " + part.name + "(#895)"; return false; } // PD 「모든 무기의 일반 등급은 아무런 효과가 없어야」
if (WLEquipGrades.IsRateOutput(part)) return false; // #879 신발 이동 속도 · 목걸이 랜덤 = 배율(WL.PartSkill.WLPartStats)
outStat = WLEquipGrades.OutputStat(part);
var lt = WLEquipTables.FindItemLevelJson(item.itemJson.ItemLvAbilityID);
var lv = lt != null ? lt.FindLevelInfo(levelOverride > 0 ? levelOverride : item.rawItem.level) : null;
double raw = 0;
if (lv != null)
{
if (part.levelField == "Health") raw = lv.HealthAbility;
else if (part.levelField == "Defence") raw = lv.DefenceAbility;
else raw = lv.AttackAbility;
}
baseValue = raw * cfg.levelStatScale * part.levelFactor;
// #825 PD 2026-10-09 「단검 = 치명타 확률 · 검 = 치명타 피해량」 — 무기는 계열 축(WLWeaponAxis.json · 공격 표 main_stat)을 주스탯 자리에 넣는다.
double statValue = mainStat; string statName = MainStatName();
WL.Combat.Weapon.WLWeaponAxisDef axis;
if (item.itemJson.EquipmentType == eFREquipmentType.Weapon && WL.Combat.Weapon.WLWeaponAxis.TryFor(item.itemJson.GroupNo, out axis))
{
var live = LivePcInfo();
double v = 0; try { if (live != null) v = live.Get_Stat(axis.estat); } catch { v = 0; }
statValue = v * axis.scale; statName = axis.name;
}
bonus = statValue * (tier.statPercent / 100.0) * part.statFactor;
string outName = WLEquipGrades.OutputName(outStat);
if (WLEquipGrades.IsRingOutput(part)) { string rn; WL.PartSkill.WLPartStats.RingMatches(item, out rn); outName = rn; }
line = outName + " = 기본 " + baseValue.ToString("F1") + " + " + statName + " " + statValue.ToString("F0") +
" × " + tier.statPercent.ToString("F0") + "%" + (part.statFactor != 1.0 ? " × " + part.statFactor.ToString("F1") : "") +
" = " + (baseValue + bonus).ToString("F1");
return true;
}
/// <summary>UI 상세용 — 등급 줄 + 계산식 줄.</summary>
public static string FormulaText(Item item)
{
if (item == null || item.itemJson == null || !item.itemJson.IsEquipment) return "";
var sb = new System.Text.StringBuilder();
WLGradeTier tier; int plus;
if (WLEquipGrades.TryMap(item.itemJson, out tier, out plus))
sb.Append("등급 ").Append(WLEquipGrades.Label(item.itemJson, true))
.Append(WLEquipGrades.NoEffect(item.itemJson) ? "" : " · 주스탯 " + tier.statPercent.ToString("F0") + "% 반영").Append(tier.isS != 0 ? " · S급" : ""); // #895 일반 무기는 % 줄 생략
else
sb.Append("등급 대응 없음(FR ").Append(WLEquipTables.GradeName(item.itemJson.ItemGrade)).Append(')');
eStat o; double bv, bonus; string line;
if (ItemValues(item, CurrentMainStat(), out o, out bv, out bonus, out line)) sb.Append('\n').Append(line);
else if (WLEquipGrades.NoEffect(item.itemJson)) sb.Append('\n').Append(line); // #895 일반 무기 = 효과 없음
return sb.ToString();
}
/// <summary>살아 있는 PC 의 스탯 객체. MyValue.Get_ActorStatInfoorNull 은 호출마다 Reset 을 돌려 값이 원본 공식으로 되돌아가므로 읽기용으로 쓰지 않는다(2026-10-09 스모크 실측).</summary>
public static ActorStatInfo LivePcInfo()
{
try { if (MyValue.bMyPC && MyValue.MyPC != null) return MyValue.MyPC.Get_StatInfo(); } catch { }
return null;
}
public static double CurrentMainStat()
{
try { var info = LivePcInfo(); return info != null ? info.Get_MainStatValue() : 0; }
catch { return 0; }
}
/// <summary>직업 주스탯 이름(힘·민첩·지능) — 서버 데이터가 없으면 「주스탯」.</summary>
public static string MainStatName()
{
try
{
var sd = ServerInfo.Ins != null ? ServerInfo.Ins.m_ServerData : null;
if (sd == null) return "주스탯";
var cd = table_classconfig.Ins.Get_Data_orNull(sd.Get_EqiupmentEquip(eItem.PC));
if (cd == null) return "주스탯";
switch (cd.Get_MainStat())
{
case eMainStatType.STR: case eMainStatType.STR_DEX: case eMainStatType.STR_INT: return "힘";
case eMainStatType.DEX: case eMainStatType.DEX_STR: case eMainStatType.DEX_INT: return "민첩";
case eMainStatType.INT: case eMainStatType.INT_STR: case eMainStatType.INT_DEX: return "지능";
}
}
catch { }
return "주스탯";
}
/// <summary>UI 상단 한 줄 — PC 현재 공격력·최대 HP·주스탯·장비 합.</summary>
public static string StatusLine()
{
var info = LivePcInfo();
if (info == null) return "PC 스탯 없음(인게임에서 확인)";
try { EnforceBaseAttack(info); } catch { }
var sb = new System.Text.StringBuilder();
sb.Append("공격력 ").Append(info.Get_Stat(eStat.ATK).ToString("F1")).Append(" (최종 ").Append(info.Get_Stat(eStat.BaseDmg).ToString("F1"))
.Append(") 최대HP ").Append(info.Get_Stat(eStat.MaxHP).ToString("F0"))
.Append(" ").Append(MainStatName()).Append(' ').Append(LastMainStat.ToString("F0"))
.Append(" 장비 공격 +").Append(LastEquipAttack.ToString("F1")).Append(" · 장비 생명 +").Append(LastEquipHp.ToString("F1"))
.Append(" · 장비 방어 +").Append(LastEquipDef.ToString("F1"));
return sb.ToString();
}
/// <summary>PC 스탯 재반영(힘민지 EquipmentUI 와 같은 호출 시점). 게임 밖(타이틀)에서는 아무 일도 없다.</summary>
public static void RefreshPc()
{
try
{
if (ServerInfo.Ins == null || ServerInfo.Ins.m_ServerData == null) return;
var info = MyValue.Get_ActorStatInfoorNull(eRole.PC); // 재계산(Reset) 뒤 반영
EnforceBaseAttack(info);
var live = LivePcInfo();
if (live != null && live != info) EnforceBaseAttack(live);
}
catch (System.Exception ex) { Debug.LogWarning("[WL-821] PC 스탯 재반영 실패 — " + ex.Message); }
}
/// <summary>
/// 훅 2 — Reset 뒤 장비 합을 반영한다. PD 2026-10-09 「플레이어 기본 공격력은 5」:
/// pcBaseAttackOverride > 0 이면 ATK = 기본값 + 장비 공격 합(멱등 · BaseDmg 는 같은 비율) · 0 이면 원본 공식에 장비 공격 합을 더한다.
/// MaxHP 에는 장비 생명 합을 더한다(재계산 1회당 1번 · 장착 변경 시 차이만).
/// </summary>
public static bool EnforceBaseAttack(ActorStatInfo info)
{
var cfg = WLEquipSettings.Instance;
if (cfg == null || cfg.enabled_ == 0 || info == null) return false;
var t = Compute(info);
LastEquipAttack = t.atk; LastEquipHp = t.hp; LastMainStat = t.mainStat; LastAppliedItems = t.items; LastReport = t.report;
bool changed = false;
Applied ap;
if (!s_applied.TryGetValue(info, out ap) || ap.seq != ResetSeq) ap = new Applied { seq = ResetSeq, hp = 0, atk = 0, def = 0 };
double dHp = t.hp - ap.hp;
if (System.Math.Abs(dHp) >= 0.01) { info.Set_Stat(true, eStat.MaxHP, dHp); ap.hp = t.hp; changed = true; }
// #879 갑옷 = 방어력 — 최종 방어(FinalDef)에 차이만 더한다(생명과 같은 방식 · 재계산 1회당 1번)
double dDef = t.def - ap.def;
if (System.Math.Abs(dDef) >= 0.01) { info.Set_Stat(true, eStat.FinalDef, dDef); ap.def = t.def; changed = true; }
LastEquipDef = t.def;
if (cfg.pcBaseAttackOverride > 0)
{
double atk = info.Get_Stat(eStat.ATK);
double target = cfg.pcBaseAttackOverride + t.atk;
if (System.Math.Abs(atk - target) >= 0.01)
{
double bd = info.Get_Stat(eStat.BaseDmg);
double k = atk > 0.0001 ? bd / atk : 1.0;
info.Set_Stat(eStat.ATK, target);
info.Set_Stat(eStat.BaseDmg, target * k);
BaseAttackEnforced++;
LastEnforceLog = "ATK " + atk.ToString("F1") + " → " + target.ToString("F1") + " · BaseDmg " + bd.ToString("F1") + " → " + (target * k).ToString("F1") +
" (장비 공격 +" + t.atk.ToString("F1") + " · 생명 +" + t.hp.ToString("F1") + " · " + MainStatName() + " " + t.mainStat.ToString("F0") + ")";
if (cfg.verboseLog != 0 && BaseAttackEnforced <= 3) Debug.Log("[WL-821] 기본 공격력 보정 — " + LastEnforceLog);
changed = true;
}
}
else
{
double dAtk = t.atk - ap.atk;
if (System.Math.Abs(dAtk) >= 0.01)
{
double a0 = info.Get_Stat(eStat.ATK), b0 = info.Get_Stat(eStat.BaseDmg);
double k0 = a0 > 0.0001 ? b0 / a0 : 1.0;
info.Set_Stat(eStat.ATK, a0 + dAtk);
info.Set_Stat(eStat.BaseDmg, (a0 + dAtk) * k0);
ap.atk = t.atk;
changed = true;
}
}
s_applied[info] = ap;
return changed;
}
/// <summary>주기 폴링용 — PC 가 있으면 현재 스탯 객체를 가져와(재계산 없이) 반영한다.</summary>
public static void EnforceBaseAttackTick()
{
try
{
if (!MyValue.bMyPC || MyValue.MyPC == null) return;
EnforceBaseAttack(MyValue.MyPC.Get_StatInfo());
}
catch { }
}
}
}