Project_WL/Assets/WL/Combat/Weapon/WLSkillHotkey.cs

165 lines
9.5 KiB
C#

// ─────────────────────────────────────────────────────────────────────────────
// WLSkillHotkey.cs — 키 하나로 낀 무기의 주력 스킬(1번 칸)을 즉시 발동 (PD #890 · 2026-10-17)
//
// PD 원문: 「q 버튼을 누르면 장비의 주력 스킬이 즉시 발동 되도록 해줘.」
//
// ■ 동작 — key(기본 Q)를 누르면 slot(1 = 주력) 칸 스킬을 원본 Use_Skill 로 바로 쓴다(ignoreCooldown 1 = 쿨 무시 시험 모드 · 0 = 쿨 지킴).
// 그 칸이 WL 파츠 스킬(예: 파괴자 1번 = 수호 보호막)이면 파츠 스킬 실행기에서 그 스킬을 바로 Cast(상시형은 안내만).
// 결과 한 줄을 화면 위쪽에 띄운다. 원본 Use_Skill 이 실패하는 경우 = 다른 스킬 동작 중 · 대상 없음 · 사거리 밖.
// ■ 값 = Resources/WL/WLSkillHotkey.json (C45) · 끄기 = enabled 0(오브젝트 0) · 원본 수정 0줄
// 🔴 Debug.LogError 금지(원본 ErrorLogHook 이 팝업을 띄운다).
// ─────────────────────────────────────────────────────────────────────────────
using System.Reflection;
using UnityEngine;
using WL.Equip;
namespace WL.Combat.Weapon
{
[System.Serializable]
public class WLSkillHotkeyJson
{
public int enabled = 0;
public string key = "Q";
public int slot = 1; // 1 = 주력 · 2 = 각성
public string key2 = ""; // #906 두 번째 키(E = 각성) · 비우면 없음
public int slot2 = 2;
public int ignoreRange = 1; // #906 1 = 사거리 밖에서도 즉시 시전(원본 Use_Skill 사거리 확인을 이 키로 쓸 때만 건너뜀)
public int ignoreCooldown = 1;
public int cancelCurrent = 1; // 1 = 다른 스킬 동작 중이면 끊고 바로 쓴다(원본 Use_Skill 은 스킬 중이면 실패)
public float messageSeconds = 1.5f;
public float messageFontScale = 0.026f;
public float messageTopRatio = 0.18f;
public string note = "";
}
public static class WLSkillHotkey
{
public const string ResourcesPath = "WL/WLSkillHotkey";
public static int Fired, Failed;
public static string LastLog = "";
internal static string Message = "";
internal static float MessageUntil;
internal static KeyCode Key = KeyCode.Q, Key2 = KeyCode.None;
/// <summary>#906 원본 Actor.Use_Skill 사거리 확인 훅이 읽는다 — 치트키 시전 중에만 true.</summary>
public static bool IgnoreRangeNow;
static WLSkillHotkeyJson s_cfg;
static WLSkillHotkeyRunner s_runner;
static FieldInfo s_fArr;
internal static WLSkillHotkeyJson Cfg
{
get
{
if (s_cfg != null) return s_cfg;
s_cfg = new WLSkillHotkeyJson();
var ta = Resources.Load<TextAsset>(ResourcesPath);
if (ta == null) { LastLog = "설정 없음 " + ResourcesPath; return s_cfg; }
try { JsonUtility.FromJsonOverwrite(ta.text, s_cfg); } catch (System.Exception ex) { LastLog = "설정 파싱 실패 — " + ex.Message; }
KeyCode k; Key = System.Enum.TryParse(s_cfg.key, true, out k) ? k : KeyCode.Q;
Key2 = !string.IsNullOrEmpty(s_cfg.key2) && System.Enum.TryParse(s_cfg.key2, true, out k) ? k : KeyCode.None;
return s_cfg;
}
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
static void Boot()
{
s_cfg = null;
if (Cfg.enabled == 0 || !DSUtil.CheckNull(s_runner)) return; // 꺼져 있으면 오브젝트 0
var go = new GameObject("[WL890] SkillHotkey");
go.hideFlags = HideFlags.HideAndDontSave;
s_runner = go.AddComponent<WLSkillHotkeyRunner>();
}
/// <summary>주력 스킬 즉시 발동(시험 도구도 부른다) · 결과 문장을 화면에 띄운다.</summary>
public static string Fire() { return Fire(Cfg.slot); }
/// <summary>#906 칸 지정 발동(1 = 주력 Q · 2 = 각성 E).</summary>
public static string Fire(int slot)
{
string r;
try { r = FireInner(slot); } catch (System.Exception ex) { Failed++; r = "발동 실패 — " + ex.Message; }
LastLog = r; Message = r; MessageUntil = Time.unscaledTime + Cfg.messageSeconds;
return r;
}
static string FireInner(int slot)
{
var c = Cfg;
var pc = MyValue.MyPC;
if (DSUtil.CheckNull(pc) || pc.IsDead()) { Failed++; return "PC 없음"; }
int idx = Mathf.Clamp(slot - 1, 0, 5);
string nm = slot == 1 ? "주력" : "각성";
// #906 무기 기술 표의 그 칸 — 해금(등급) 확인 · 조건 발동 칸은 표에서 스킬 데이터
var w = WLInventory.EquippedInSlot(eFREquipmentType.Weapon);
var rows = w != null && w.itemJson != null ? WLWeaponSkillTable.Rows(w.itemJson.GroupNo) : null;
WLWeaponSkillRow row = null;
if (rows != null) foreach (var rr in rows) if (rr.slot == slot) { row = rr; break; }
if (row != null && WLWeaponSkillSlots.LastTier < row.unlockTier) { Failed++; return nm + " 스킬 미해금(" + row.skillName + " · 등급 " + row.unlockTier + "부터)"; }
if (s_fArr == null) s_fArr = typeof(PCActor).GetField("arr_magicData", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
var arr = s_fArr != null ? s_fArr.GetValue(pc) as SkillListTableData[] : null;
var data = arr != null && idx < arr.Length ? arr[idx] : null;
if (data == null && row != null && !row.IsWlSkill && row.skillId > 0) data = WLWeaponSkillSlots.SkillDataFor(pc as PCActor, row.skillId); // #906 조건 발동 칸
if (data == null) return FirePartSkill(slot);
if (pc.Get_CurAnim() >= eAnim.Skill)
{
if (c.cancelCurrent == 0) { Failed++; return "스킬 사용 중 — " + data.e_Skill; }
pc.Play_Idle(); // #890 PD 「치트키 Q 를 눌렀을 때는 쿨타임을 무시하고 주력 스킬이 발동」 — 진행 중 스킬을 끊는다
}
bool ok;
IgnoreRangeNow = c.ignoreRange != 0;
try { ok = pc.Use_Skill(data, true, idx, c.ignoreCooldown != 0); } finally { IgnoreRangeNow = false; }
if (ok) { Fired++; return nm + " 스킬 발동 · " + data.e_Skill; }
Failed++; return nm + " 발동 안 됨 · " + data.e_Skill + "(대상" + (c.ignoreRange != 0 ? "" : " · 사거리") + (c.ignoreCooldown != 0 ? "" : " · 쿨타임") + ")";
}
/// <summary>그 칸이 WL 파츠 스킬이면 실행기에서 바로 Cast.</summary>
static string FirePartSkill(int slot)
{
var w = WLInventory.EquippedInSlot(eFREquipmentType.Weapon);
var rows = w != null && w.itemJson != null ? WLWeaponSkillTable.Rows(w.itemJson.GroupNo) : null;
WLWeaponSkillRow row = null;
if (rows != null) foreach (var r in rows) if (r.slot == slot) { row = r; break; }
if (row == null || !row.IsWlSkill) { Failed++; return (slot == 1 ? "주력" : "각성") + " 스킬 없음(등급 미해금 · 표에 없음)"; }
var run = WL.PartSkill.WLPartSkillRunner.Instance;
if (run == null) { Failed++; return "파츠 스킬 실행기 없음"; }
foreach (var sk in run.Skills)
{
if (sk == null || sk.def == null || sk.def.key != row.wlSkill) continue;
if (sk.Persistent) { Failed++; return row.skillName + " — 상시 유지형(즉시 발동 없음)"; }
sk.Cast(); sk.cd = Mathf.Max(0.2f, sk.CoolSec);
Fired++; return (slot == 1 ? "주력" : "각성") + " 스킬 발동 · " + row.skillName + "(파츠 스킬)";
}
Failed++; return row.skillName + " — 실행기에 없음(등급 미해금)";
}
}
/// <summary>키 읽기 · 결과 한 줄 표시. 숨김 GameObject 1개.</summary>
internal sealed class WLSkillHotkeyRunner : MonoBehaviour
{
GUIStyle _style;
void Update()
{
var c = WLSkillHotkey.Cfg;
if (Input.GetKeyDown(WLSkillHotkey.Key)) WLSkillHotkey.Fire(c.slot);
if (WLSkillHotkey.Key2 != KeyCode.None && Input.GetKeyDown(WLSkillHotkey.Key2)) WLSkillHotkey.Fire(c.slot2); // #906 E = 각성
}
void OnGUI()
{
var msg = WLSkillHotkey.Message;
if (string.IsNullOrEmpty(msg) || Time.unscaledTime > WLSkillHotkey.MessageUntil) return;
var c = WLSkillHotkey.Cfg;
if (_style == null) _style = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleCenter, fontStyle = FontStyle.Bold };
_style.fontSize = Mathf.Max(14, Mathf.RoundToInt(Screen.height * c.messageFontScale));
var r = new Rect(0f, Screen.height * c.messageTopRatio, Screen.width, _style.fontSize * 2f);
var keep = GUI.color;
GUI.color = new Color(0f, 0f, 0f, 0.75f); GUI.Label(new Rect(r.x + 2f, r.y + 2f, r.width, r.height), msg, _style);
GUI.color = new Color(0.6f, 0.95f, 1f); GUI.Label(r, msg, _style);
GUI.color = keep;
}
}
}