Project_WL/Assets/WL/UI/Scripts/WLSkillSlotOverride.cs

292 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.

// ─────────────────────────────────────────────────────────────────────────────
// WLSkillSlotOverride.cs — 전투 패드 4슬롯 장착 **로컬 오버라이드** + MP 부족 흐림 (WL-813g §3 · #813)
//
// 813c 완료보고 §5 이월(「슬롯 장착은 서버 프리셋이라 813c 와 묶어야 함」 · 813n §1) · 기준서 v1 §B 요소 3.
//
// ■ 무엇을 하나 (UI 코드 안 · 원본 훅 0 · 핫스팟(PCActor/Actor) 변경 0)
// ① WLSkillSlotPreset.asset 에서 **내 클래스 행**(813n 4클래스 × 4슬롯)을 찾아,
// `ServerData.Equip.Set_Equip(eItem.Skill, id, slot)` — 클라 메모리 전용 공개 API(서버 호출 0) — 로 써 넣는다.
// 그 뒤 `PCActor.Set/ReSet` 이 `Get_EquipSkillID(i)` 로 읽는 **기존 `Use_Skill` 경로 그대로** 발동한다.
// ② 이미 카드가 서버 값으로 채워진 뒤라면 `SkillCard.Set(데이터)` 로 아이콘만 즉시 맞춘다(표시 동기화).
// ③ MP 가 모자란 슬롯의 버튼을 흐리게 한다(`Actor.cs:733-741` 과 **같은 식**: n_MP n_MP·SKILL_COST > MP).
//
// 🔴 실행 순서 주의(실측 · Play 없이 정적 확인)
// `PCActor.Set()`(:151-163)은 `dic_MagicData` 를 **한 번만** 만든다(`ReSet()` 은 `arr_magicData` 만 새로 읽는다).
// `Actor.Use_Skill(:707)` 은 `m_Magic = dic_MagicData[index]` 를 쓰므로, 오버라이드가 `PCActor.Set()` **뒤**에
// 적용되면 아이콘·게이트·쿨·애니는 새 스킬이지만 발사 실체(`m_SkillData.m_SkillListTableData`)는 옛 스킬로 남는다.
// → 이 컴포넌트는 `ServerData` 가 준비되는 즉시(패드 활성 이전부터) 적용을 시도하고,
// 늦게 적용됐으면 `LateApplied = true` 를 남긴다(완료보고 「미확인」 항목 · Play 검증 필요).
//
// ■ 롤백(C8)
// WLSkillSlotPreset.asset 이 없거나 overrideEnabled=false 면 **아무 것도 쓰지 않는다** = 서버 프리셋 원본.
// restoreOnDisable 이면 꺼질 때 원래 SkillID 를 그대로 복원한다.
//
// 🔴 어셈블리 주의: Assets/WL/UI/ 에 .asmdef 를 만들지 말 것(Assembly-CSharp 단일).
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.UI;
namespace WL.UI
{
[DisallowMultipleComponent]
public class WLSkillSlotOverride : MonoBehaviour
{
[Tooltip("대상 전투 패드 — 비우면 같은 오브젝트의 BattleUI 를 찾는다")]
[SerializeField] private BattleUI target;
// ── 진단(프로브가 읽는다 · 실측만 채운다)
public static bool Applied;
public static bool LateApplied;
public static int AppliedClassId;
public static int AppliedSlots;
public static string LastInfo = "";
public static int[] LastBefore = new int[0];
public static int[] LastAfter = new int[0];
private readonly List<int> _restore = new List<int>();
private bool _didApply;
private float _nextMpCheck;
private float[] _alpha;
private SkillListTableData[] _slotData;
/// <summary>Play 중에만 서버 데이터를 만진다 — ServerInfo.Ins 는 없으면 GameObject 를 만든다(에디트 모드 오염 금지).</summary>
private static ServerData LiveServerData
{
get { return Application.isPlaying && ServerInfo.Ins != null ? ServerInfo.Ins.m_ServerData : null; }
}
private void OnEnable() { Initialize(); }
private void OnDisable() { RestoreIfNeeded(); }
/// <summary>에디트 모드에서도 런타임과 같은 경로를 타게 하기 위해 OnEnable 본체를 분리한다(813c 교훈).</summary>
public string Initialize()
{
if (target == null) target = GetComponent<BattleUI>();
_nextMpCheck = 0f;
return TryApply();
}
private void Update()
{
if (!_didApply) TryApply(); // ServerData 가 늦게 준비되는 경우 재시도(가벼움 · 실패는 즉시 반환)
UpdateMpGate();
}
// ───────────────────────────────────────────── ① 슬롯 오버라이드
/// <summary>설정대로 내 클래스의 4슬롯을 써 넣는다. 설정·서버데이터가 없으면 아무 것도 하지 않는다.</summary>
public string TryApply()
{
if (!WLSkillSlotPreset.Enabled) return "WLSkillSlotPreset 없음/off — 서버 프리셋 유지";
var sdata = LiveServerData;
if (sdata == null || sdata.Equip == null || sdata.Equip.Skill == null) return "ServerData 미준비 — 재시도";
int classId = ResolveClassId(sdata);
if (classId == 0) return "클래스 ID 미확인 — 재시도";
var preset = WLSkillSlotPreset.Instance;
int n = preset.SlotCountFor(classId);
if (n <= 0) { _didApply = true; LastInfo = "클래스 " + classId + " 행 없음 — 오버라이드 0"; return LastInfo; }
string log = ApplyTo(sdata.Equip, preset, classId, _restore);
_didApply = true;
Applied = true;
AppliedClassId = classId;
AppliedSlots = n;
LateApplied = MyValue.bMyPC; // PC 가 이미 살아 있으면 dic_MagicData 는 옛 값일 수 있다(위 🔴)
LastInfo = log;
if (preset.verboseLog) Debug.Log("[WLSkillSlotOverride] " + log);
RefreshCards(sdata, preset, classId, n);
return log;
}
/// <summary>
/// SD_Equip 에 프리셋을 써 넣는다(테스트에서 합성 SD_Equip 으로도 부를 수 있게 정적 · 부작용은 인자만).
/// restore 에 원래 값을 남긴다(복원용).
/// </summary>
public static string ApplyTo(SD_Equip equip, WLSkillSlotPreset preset, int classId, List<int> restore)
{
var sb = new StringBuilder();
int p = equip.Preset;
if (!equip.Skill.ContainsKey(p)) return "Equip.Skill[" + p + "] 없음 — 오버라이드 0";
var lst = equip.Skill[p];
int n = Mathf.Min(preset.SlotCountFor(classId), lst.Count);
var before = new int[n];
var after = new int[n];
if (restore != null && restore.Count == 0)
for (int i = 0; i < lst.Count; i++) restore.Add(lst[i]);
for (int i = 0; i < n; i++) before[i] = lst[i];
for (int i = 0; i < n; i++)
{
int id = preset.SkillIdFor(classId, i);
if (id <= 0) continue; // 0 = 그 슬롯은 서버 값 유지
equip.Set_Equip(eItem.Skill, id, i);
}
for (int i = 0; i < n; i++) after[i] = lst[i];
LastBefore = before; LastAfter = after;
sb.Append("class=").Append(classId).Append(" preset=").Append(p).Append(" slots=").Append(n).Append(" · ");
for (int i = 0; i < n; i++)
{
if (i > 0) sb.Append(' ');
sb.Append('[').Append(i).Append("] ").Append(before[i]).Append("→").Append(after[i]);
}
return sb.ToString();
}
private void RestoreIfNeeded()
{
var preset = WLSkillSlotPreset.Instance;
if (preset == null || !preset.restoreOnDisable || _restore.Count == 0) { _restore.Clear(); _didApply = false; return; }
var sdata = LiveServerData;
if (sdata != null && sdata.Equip != null && sdata.Equip.Skill != null)
{
int p = sdata.Equip.Preset;
if (sdata.Equip.Skill.ContainsKey(p))
{
var lst = sdata.Equip.Skill[p];
for (int i = 0; i < lst.Count && i < _restore.Count; i++)
{
var v = lst[i];
v = _restore[i];
v.RandomizeCryptoKey();
lst[i] = v;
}
}
}
_restore.Clear();
_didApply = false;
Applied = false;
}
/// <summary>내 클래스 ID — 살아 있는 PC 가 있으면 그것, 없으면 장착 PC(Equip[eItem.PC]).</summary>
public static int ResolveClassId(ServerData sdata)
{
if (MyValue.bMyPC && MyValue.MyPC != null)
{
int id = MyValue.MyPC.Get_ID();
if (id > 0) return id;
}
if (sdata != null && sdata.Equip != null && sdata.Equip.Equip != null && sdata.Equip.Equip.ContainsKey(eItem.PC))
return sdata.Equip.Equip[eItem.PC];
return 0;
}
/// <summary>카드가 이미 서버 값으로 채워졌다면 아이콘을 새 스킬로 즉시 맞춘다(표시 동기화 · Use 타입 시각 복원).</summary>
private void RefreshCards(ServerData sdata, WLSkillSlotPreset preset, int classId, int n)
{
if (target == null || target.skillCards == null) return;
if (_slotData == null || _slotData.Length != target.skillCards.Length)
_slotData = new SkillListTableData[target.skillCards.Length];
for (int i = 0; i < n && i < target.skillCards.Length; i++)
{
int id = preset.SkillIdFor(classId, i);
if (id <= 0) continue;
var card = target.skillCards[i];
if (card == null) continue;
var data = table_skilllist.Ins.Get_Data_orNull(id);
if (data == null) continue;
_slotData[i] = data;
card.Set(data);
// SkillCard.Set<T> 는 UI 카드 기준으로 상태 라벨을 켠다 — 전투(Use) 카드에서는 다시 끈다(Set_CardType(Use) 와 같은 상태).
if (card.gos != null)
{
if (card.gos.Length > 0 && card.gos[0] != null) card.gos[0].SetActive(false);
if (card.gos.Length > 2 && card.gos[2] != null) card.gos[2].SetActive(false);
}
}
}
// ───────────────────────────────────────────── ③ MP 부족 흐림
private void UpdateMpGate()
{
var preset = WLSkillSlotPreset.Instance;
if (preset == null || !preset.mpDimEnabled || !WLSkillSlotPreset.Enabled) return;
if (target == null || target.skillCards == null) return;
if (Time.unscaledTime < _nextMpCheck) return;
_nextMpCheck = Time.unscaledTime + Mathf.Max(0.02f, preset.mpCheckInterval);
ApplyMpGate(preset);
}
/// <summary>현재 MP 로 각 슬롯 버튼 알파를 갱신한다(에디트 모드 프로브도 이 경로를 쓴다).</summary>
public string ApplyMpGate(WLSkillSlotPreset preset)
{
if (target == null) target = GetComponent<BattleUI>();
if (target == null || target.skillCards == null) return "BattleUI 없음";
if (!MyValue.bMyPC || MyValue.MyPC == null) return "PC 없음 — MP 게이트 건너뜀";
var stat = MyValue.MyPC.Get_StatInfo();
double mp = stat.Get_Stat(eStat.MP);
double costRate = stat.Get_Stat(eStat.SKILL_COST);
var sb = new StringBuilder();
int cards = target.skillCards.Length;
if (_alpha == null || _alpha.Length != cards) { _alpha = new float[cards]; for (int i = 0; i < cards; i++) _alpha[i] = preset.mpFullAlpha; }
int n = Mathf.Min(preset.slotCount, cards);
for (int i = 0; i < n; i++)
{
var card = target.skillCards[i];
if (card == null || !card.gameObject.activeSelf) continue;
// 오버라이드로 확정된 슬롯 데이터를 먼저 쓰고, 없으면 카드가 들고 있는 데이터(서버 프리셋)로 떨어진다.
var data = (_slotData != null && i < _slotData.Length && _slotData[i] != null)
? _slotData[i] : card.Get_Data() as SkillListTableData;
bool enough = true;
double need = 0d;
if (data != null)
{
need = data.n_MP - (data.n_MP * costRate); // Actor.cs:733 과 같은 식
enough = need <= mp;
}
float want = enough ? preset.mpFullAlpha : preset.mpDimAlpha;
float cur = _alpha[i];
float t = preset.mpDimLerpSeconds <= 0f ? 1f : Mathf.Clamp01(Time.unscaledDeltaTime / preset.mpDimLerpSeconds);
_alpha[i] = Mathf.Lerp(cur, want, t);
SetCardAlpha(card, _alpha[i]);
sb.Append('[').Append(i).Append("] need=").Append(need.ToString("F0"))
.Append(" mp=").Append(mp.ToString("F0")).Append(" enough=").Append(enough)
.Append(" alpha=").Append(_alpha[i].ToString("F2")).Append(' ');
}
return sb.ToString();
}
private static void SetCardAlpha(SkillCard card, float a)
{
var cg = card.GetComponent<CanvasGroup>();
if (cg == null) cg = card.gameObject.AddComponent<CanvasGroup>();
cg.alpha = a;
// 흐릴 때도 누를 수는 있게 둔다 — 실제 차단은 Actor.Use_Skill 의 MP 게이트(ToastUI 106)가 한다.
}
// ───────────────────────────────────────────── 프로브(에디트 모드 · 런타임 의존 0)
/// <summary>클래스 × 슬롯 → SkillID 표를 문자열로 만든다(설정 에셋만 읽는다).</summary>
public static string ResolveDump(WLSkillSlotPreset preset)
{
var sb = new StringBuilder();
if (preset == null) return "WLSkillSlotPreset 없음";
sb.AppendLine("overrideEnabled=" + preset.overrideEnabled + " slotCount=" + preset.slotCount +
" restoreOnDisable=" + preset.restoreOnDisable + " mpDim=" + preset.mpDimEnabled +
" dimAlpha=" + preset.mpDimAlpha.ToString("F2"));
if (preset.rows == null) return sb.Append("rows 없음").ToString();
for (int r = 0; r < preset.rows.Length; r++)
{
var row = preset.rows[r];
sb.Append("class ").Append(row.classId).Append(" :");
int n = preset.SlotCountFor(row.classId);
for (int s = 0; s < n; s++) sb.Append(" [").Append(s).Append("]=").Append(preset.SkillIdFor(row.classId, s));
sb.Append(" // ").AppendLine(row.note);
}
return sb.ToString();
}
}
}