273 lines
15 KiB
C#
273 lines
15 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 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);
|
||
}
|
||
}
|
||
}
|