// ───────────────────────────────────────────────────────────────────────────── // 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; /// #906 원본 Actor.Use_Skill 사거리 확인 훅이 읽는다 — 치트키 시전 중에만 true. 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(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(); } /// 주력 스킬 즉시 발동(시험 도구도 부른다) · 결과 문장을 화면에 띄운다. public static string Fire() { return Fire(Cfg.slot); } /// #906 칸 지정 발동(1 = 주력 Q · 2 = 각성 E). 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 ? "" : " · 쿨타임") + ")"; } /// 그 칸이 WL 파츠 스킬이면 실행기에서 바로 Cast. 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 + " — 실행기에 없음(등급 미해금)"; } } /// 키 읽기 · 결과 한 줄 표시. 숨김 GameObject 1개. 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; } } }