Project_WL/Assets/WL/PartSkill/WLPartSkillState.cs

273 lines
15 KiB
C#
Raw 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.

// ─────────────────────────────────────────────────────────────────────────────
// WLPartSkillState.cs — 장착 장비 → 파츠 스킬 구성(등급 → 개방 · 단계 · 배율 · 시너지) (PD 지시 #879 · 2026-10-16)
//
// 규칙(PD 10-16): 액티브 · 패시브 모두 장비 등급으로 개방(일반 = Lv1) + 단계 상승(전설 = Lv5) · 불멸 ×1.2 · 신화 ×1.5(제안값 · WLPartSkill.json)
// 시너지 = 액티브 5단계 + 짝 패시브 5단계(장갑 · 신발) → FR 각성(Lv6) 수치 · 이름으로 동작
// 무기 시너지 = #895(2026-10-17) 무기와 무관 — 장착 장비 조건(짝 패시브 5단계 [+ req_key 5단계])만 · 여러 개 동시(EvalWeaponSynergies)
// 결합 = 불의 정령(목걸이) 5단계 + 물의 정령(반지) 5단계 → 정령왕
// 장착이 바뀌면(WLInventory.Changed) 다시 계산하고 Changed 를 낸다 — 러너 · HUD · 장비 화면이 구독한다.
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections.Generic;
using UnityEngine;
using WL.Equip;
namespace WL.PartSkill
{
/// <summary>장착 1칸의 스킬 상태.</summary>
public sealed class WLPartSlot
{
public eFREquipmentType part;
public Item item;
public WLPartSkillDef def;
public int tier, level;
public float mult = 1f;
public bool synergy; // 시너지 성립(액티브 · 무기 시너지)
public WLPartSkillDef synPartner; // 짝 패시브(또는 결합 상대)
public int synPartnerLevel;
public bool Open { get { return def != null && level > 0; } }
}
public sealed class WLPartLoadout
{
public readonly List<WLPartSlot> actives = new List<WLPartSlot>();
public readonly List<WLPartSlot> passives = new List<WLPartSlot>();
public readonly Dictionary<eFREquipmentType, WLPartSlot> byPart = new Dictionary<eFREquipmentType, WLPartSlot>();
public WLPartSlot weaponSyn; // weaponSyns 의 첫 칸(호환)
/// <summary>#895 무기 시너지 = 장착 장비 조건(짝 패시브 syn_key [+ req_key])으로만 성립 · 무기 무관 · 여러 개 동시. 조건 장비를 하나라도 끼면 후보(대기)로 들어간다.</summary>
public readonly List<WLPartSlot> weaponSyns = new List<WLPartSlot>();
public bool spiritKing;
public int spiritKingLevel = 1;
// 패시브 · 목걸이 합계(파츠 스킬 배율)
public float coolMul = 1f, rangeMul = 1f, durMul = 1f, projSpeedMul = 1f, dmgMul = 1f, moveBonus, regenPct, regenPeriod;
// 힘민지 스탯 가산(훅 1 · ActorStatInfo.Reset 안)
public readonly Dictionary<eStat, double> statAdds = new Dictionary<eStat, double>();
public int synergyCount;
public string summary = "";
public void AddStat(eStat s, double v) { double cur; statAdds.TryGetValue(s, out cur); statAdds[s] = cur + v; }
public WLPartSlot PassiveByKey(string key)
{
for (int i = 0; i < passives.Count; i++) if (passives[i].def != null && passives[i].def.key == key) return passives[i];
return null;
}
public WLPartSlot ActiveByKey(string key)
{
for (int i = 0; i < actives.Count; i++) if (actives[i].def != null && actives[i].def.key == key) return actives[i];
return null;
}
}
public static class WLPartSkillState
{
public static WLPartLoadout Current { get; private set; } = new WLPartLoadout();
public static int Version { get; private set; }
public static event System.Action Changed;
public static string LastLog { get; private set; } = "";
/// <summary>이번 실행에서 한 번이라도 성립한 시너지 키(도감 ✓ · 「시너지!」 연출 1회).</summary>
public static readonly HashSet<string> SeenSynergies = new HashSet<string>();
public static event System.Action<string> SynergyFound;
static bool s_hooked;
public static void EnsureHooked()
{
if (s_hooked) return;
s_hooked = true;
WLInventory.Changed += () => Rebuild("inventory");
LoadSeen();
}
public static WLPartLoadout Rebuild(string why)
{
var L = new WLPartLoadout();
if (!WLPartSkillData.Enabled || !WLInventory.EnsureLoaded()) { Current = L; return L; }
var S = WLPartSkillData.S;
for (int t = (int)eFREquipmentType.Weapon; t < (int)eFREquipmentType.Max; t++)
{
var part = (eFREquipmentType)t;
var it = WLInventory.EquippedInSlot(part);
if (it == null || it.itemJson == null) continue;
var def = WLPartSkillData.DefForGroup(it.itemJson.GroupNo);
// #895 무기 시너지는 무기에서 오지 않는다(아래 장비 조건) · 일반 무기 = 파츠 스킬 0(WLGradeTable noEffectTiers)
if (def != null && def.kind == WLPartKind.WeaponSyn) def = null;
if (def != null && WLEquipGrades.NoEffect(it.itemJson)) def = null;
var slot = new WLPartSlot { part = part, item = it, def = def };
WLGradeTier gt; int plus;
slot.tier = WLEquipGrades.TryMap(it.itemJson, out gt, out plus) ? gt.tier : 1;
slot.level = WLPartSkillData.TierLevel(slot.tier);
slot.mult = WLPartSkillData.TierMult(slot.tier);
L.byPart[part] = slot;
if (def == null) continue;
switch (def.kind)
{
case WLPartKind.Passive: L.passives.Add(slot); break;
case WLPartKind.Active: L.actives.Add(slot); break;
}
}
// #883 무기 기술 표가 WL 파츠 스킬을 지정한 칸(파괴자 1번 = 수호 보호막) → 그 무기에서 액티브 하나 더(칸 해금 등급부터 · 단계 = 무기 등급)
WLPartSlot wslot, shadowed = null;
if (L.byPart.TryGetValue(eFREquipmentType.Weapon, out wslot) && wslot.item != null && wslot.item.itemJson != null)
{
var wrows = WL.Combat.Weapon.WLWeaponSkillTable.Rows(wslot.item.itemJson.GroupNo);
if (wrows != null)
foreach (var r in wrows)
{
if (!r.IsWlSkill || wslot.tier < r.unlockTier) continue;
var wd = WLPartSkillData.Def(r.wlSkill);
if (wd == null || wd.kind != WLPartKind.Active) continue;
var ws = new WLPartSlot { part = eFREquipmentType.Weapon, item = wslot.item, def = wd, tier = wslot.tier, level = wslot.level, mult = wslot.mult };
// 같은 스킬이 다른 부위에도 있으면(갑옷 구원 = 수호 보호막) 하나만 돈다 — 등급 높은 쪽(같으면 그 부위 것)
var same = L.ActiveByKey(wd.key);
if (same == null) L.actives.Add(ws);
else if (ws.tier > same.tier) { L.actives[L.actives.IndexOf(same)] = ws; shadowed = same; }
}
}
// 패시브 합계
foreach (var p in L.passives) ApplyPassive(L, p);
// 시너지
foreach (var a in L.actives) EvalSynergy(L, a, S.synergyLevel);
if (shadowed != null) EvalSynergy(L, shadowed, S.synergyLevel); // 장비 화면 부위 칸 표시용
EvalWeaponSynergies(L, S.synergyLevel);
var fire = L.ActiveByKey("fire_spirit"); var water = null as WLPartSlot;
if (fire != null && !string.IsNullOrEmpty(fire.def.combineKey)) water = L.ActiveByKey(fire.def.combineKey);
if (fire != null && water != null && fire.level >= S.synergyLevel && water.level >= S.synergyLevel)
{
L.spiritKing = true;
fire.synergy = water.synergy = true; fire.synPartner = water.def; water.synPartner = fire.def;
fire.synPartnerLevel = water.level; water.synPartnerLevel = fire.level;
}
else
{
if (fire != null && water != null) { fire.synPartner = water.def; water.synPartner = fire.def; fire.synPartnerLevel = water.level; water.synPartnerLevel = fire.level; }
}
// 목걸이 랜덤 · 신발 이동 · 반지는 WLPartStats 가 더한다(부위 기본 능력치)
WLPartStats.AddPartStats(L);
L.synergyCount = 0;
foreach (var a in L.actives) if (a.synergy) L.synergyCount++;
foreach (var w in L.weaponSyns) if (w.synergy) L.synergyCount++;
var sb = new System.Text.StringBuilder();
foreach (var a in L.actives) sb.Append(a.def.name).Append(" Lv").Append(a.level).Append(a.synergy ? "★" : "").Append(' ');
foreach (var p in L.passives) sb.Append(p.def.name).Append(" Lv").Append(p.level).Append(' ');
foreach (var w in L.weaponSyns) sb.Append("무기시너지 ").Append(w.def.name).Append(w.synergy ? "★" : "(대기)").Append(' ');
if (L.spiritKing) sb.Append("정령왕★");
L.summary = sb.ToString();
Current = L;
Version++;
LastLog = why + " → " + L.summary;
if (S.verbose != 0) Debug.Log("[WL-879] 파츠 스킬 구성 " + LastLog);
// 새 시너지 = 「시너지!」 연출 1회 + 도감 기록
NoteFound(L);
try { Changed?.Invoke(); } catch (System.Exception ex) { Debug.LogException(ex); }
return L;
}
static void NoteFound(WLPartLoadout L)
{
var found = new List<string>();
foreach (var a in L.actives) if (a.synergy && a.def.key != "water_spirit") found.Add(a.def.key == "fire_spirit" ? "spirit_king" : a.def.key);
foreach (var w in L.weaponSyns) if (w.synergy) found.Add(w.def.key);
bool any = false;
foreach (var k in found)
if (SeenSynergies.Add(k)) { any = true; try { SynergyFound?.Invoke(k); } catch (System.Exception ex) { Debug.LogException(ex); } }
if (any) SaveSeen();
}
/// <summary>
/// #895 PD 「무기 시너지 효과는 무기와 관계 없이 장비한 아이템에 따라 발생」 — 모든 무기 시너지 정의를 장착 장비로만 본다.
/// 조건 = 짝 패시브(syn_key) need 단계 이상 [+ req_key(패시브 또는 액티브) need 단계 이상]. 조건 장비를 하나라도 끼면 후보(대기)로 넣는다.
/// 칸의 단계 · 배율 = 짝 패시브 장비 것(배율은 시너지 효과 세기에 쓰인다).
/// </summary>
static void EvalWeaponSynergies(WLPartLoadout L, int need)
{
L.weaponSyns.Clear();
foreach (var d in WLPartSkillData.Defs)
{
if (d == null || d.kind != WLPartKind.WeaponSyn || string.IsNullOrEmpty(d.synKey)) continue;
var p = L.PassiveByKey(d.synKey);
WLPartSlot q = null;
bool hasReq = !string.IsNullOrEmpty(d.reqKey);
if (hasReq) q = L.PassiveByKey(d.reqKey) ?? L.ActiveByKey(d.reqKey);
if (p == null && q == null) continue;
var anchor = p ?? q;
var w = new WLPartSlot { part = anchor.part, item = anchor.item, def = d, tier = anchor.tier, level = anchor.level, mult = anchor.mult,
synPartner = WLPartSkillData.Def(d.synKey), synPartnerLevel = p != null ? p.level : 0 };
w.synergy = p != null && p.level >= need && (!hasReq || (q != null && q.level >= need));
L.weaponSyns.Add(w);
}
L.weaponSyn = L.weaponSyns.Count > 0 ? L.weaponSyns[0] : null;
}
static void EvalSynergy(WLPartLoadout L, WLPartSlot a, int need)
{
if (a.def == null || string.IsNullOrEmpty(a.def.synKey)) return;
a.synPartner = WLPartSkillData.Def(a.def.synKey);
var p = L.PassiveByKey(a.def.synKey);
a.synPartnerLevel = p != null ? p.level : 0;
a.synergy = a.level >= need && p != null && p.level >= need;
}
/// <summary>패시브 값(비율) = FR 값 % ÷ 100 × 등급 배율.</summary>
public static float PassiveValue(WLPartSlot p, bool second = false)
{
if (p == null || p.def == null) return 0f;
var row = WLPartSkillData.Passive(p.def.lvId, Mathf.Max(1, p.level));
if (row == null) return 0f;
return (second ? row.v1 : row.v0) / 100f * p.mult;
}
static void ApplyPassive(WLPartLoadout L, WLPartSlot p)
{
if (!p.Open) return;
float v = PassiveValue(p), v2 = PassiveValue(p, true);
switch (p.def.behavior)
{
case "atk_mul": L.AddStat(eStat.ATK_Multiplier, v); break;
case "hp_mul": L.AddStat(eStat.HP_Multiplier, v); break;
case "def_mul": L.AddStat(eStat.DEF_Multiplier, v); break;
case "fate": L.AddStat(eStat.EXP_Multiplier, v); L.AddStat(eStat.GOLD_Multiplier, v2 > 0f ? v2 : v); break;
case "cool_down": L.coolMul *= Mathf.Max(0.1f, 1f - v); L.AddStat(eStat.COOLTIME_Multiplier, v); break;
case "range_up": L.rangeMul += v; break;
case "duration_up": L.durMul += v; break;
case "proj_speed_up": L.projSpeedMul += v; break;
case "move_up": L.moveBonus += v; break;
case "regen": L.regenPct += v; L.regenPeriod = p.def.v1 > 0f ? p.def.v1 : 5f; break;
// dodge_up · pickup_up = 대응 아이템 없음(PD 결정 대기) — 값만 보이고 효과 연결은 아이템 배정 뒤
}
}
// ── 도감(찾은 시너지) — 로컬 저장(서버 무관 · 이번 기기) ─────────────
const string SeenKey = "WL_PartSkill_Synergies";
static void LoadSeen()
{
try
{
var s = PlayerPrefs.GetString(SeenKey, "");
foreach (var k in s.Split(',')) if (!string.IsNullOrEmpty(k)) SeenSynergies.Add(k);
}
catch { }
}
static void SaveSeen()
{
try { PlayerPrefs.SetString(SeenKey, string.Join(",", new List<string>(SeenSynergies).ToArray())); PlayerPrefs.Save(); } catch { }
}
/// <summary>표시용 — 이 스킬의 지금 단계 행(시너지면 시너지 단계).</summary>
public static WLPartLevelRow RowFor(WLPartSlot s)
{
if (s == null || s.def == null) return null;
int lv = s.synergy && s.def.synLevel > 0 ? s.def.synLevel : Mathf.Max(1, s.level);
return WLPartSkillData.Level(s.def.lvSrc, s.def.lvId, lv);
}
}
}