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

292 lines
15 KiB
C#
Raw Permalink Normal View History

// ─────────────────────────────────────────────────────────────────────────────
// 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();
}
}
}