161 lines
8.7 KiB
C#
161 lines
8.7 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
|
// WLSkillChain.cs — 조건 발동 스킬: 무기 기술 표 trigger 칸(예: hit:Skill_ShieldAttack)의 스킬을 그 조건 때 쿨 없이 시전
|
|
//
|
|
// PD 원문(#890 · 2026-10-17): 「한손검 전설 스킬은 별도의 쿨타임 없이 "방패 치기 명중 시" 발동 되도록 해줘.」
|
|
//
|
|
// ■ 원리 — 타격 확정 훅(CombatEvents.HitConfirmed)에서 내 PC 가 쏜 스킬(피해 정보 Beater_pd)이 명중하면, 낀 무기의 표 행 중
|
|
// trigger = "hit:<그 스킬 eEffect>" 이고 등급이 해금 단계 이상인 행을 찾아 delay 초 뒤 시전한다(같은 프레임 재진입 회피).
|
|
// 원본 Use_Skill 은 스킬 동작 중이면 실패한다 → cancelTrigger 1 이면 앞 스킬의 남은 동작을 Play_Idle 로 끊고 바로 잇는다.
|
|
// 시전 = 시험 모드(쿨 무시 · MP 0 복사본 · 사거리 = 표 range_m) — 그 행은 자동 시전 슬롯에 안 들어간다(WLWeaponSkillSlots).
|
|
// ■ 값 = Resources/WL/WLSkillChain.json (C45) · 끄기 = enabled 0(구독 0) · 원본 수정 0줄
|
|
// 🔴 Debug.LogError 금지(원본 ErrorLogHook 이 팝업을 띄운다).
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
using System.Collections.Generic;
|
|
using System.Reflection;
|
|
using UnityEngine;
|
|
using WL.Combat.Core;
|
|
using WL.Equip;
|
|
|
|
namespace WL.Combat.Weapon
|
|
{
|
|
[System.Serializable]
|
|
public class WLSkillChainJson
|
|
{
|
|
public int enabled = 0;
|
|
public float delay = 0.12f; // 명중 → 시전까지(s)
|
|
public int cancelTrigger = 1; // 1 = 앞 스킬 남은 동작을 끊고 잇는다 · 0 = 끝날 때까지 기다린다
|
|
public float maxWait = 1f; // cancelTrigger 0 일 때 최대 대기(s)
|
|
public float minRange = 0f; // 이어 쓰는 스킬의 시전 거리 하한(m) — 명중 직후라 대상이 밀려나도 끊기지 않게
|
|
public string note = "";
|
|
}
|
|
|
|
public static class WLSkillChain
|
|
{
|
|
public const string ResourcesPath = "WL/WLSkillChain";
|
|
public static int Triggered, Cast, Failed;
|
|
public static string LastLog = "", LastFail = "";
|
|
|
|
static WLSkillChainJson s_cfg;
|
|
static WLSkillChainRunner s_runner;
|
|
static PCActor s_pc;
|
|
static WLWeaponSkillRow s_row;
|
|
static float s_at, s_deadline;
|
|
static bool s_pending;
|
|
static readonly Dictionary<int, SkillListTableData> s_data = new Dictionary<int, SkillListTableData>();
|
|
|
|
static WLSkillChainJson Cfg
|
|
{
|
|
get
|
|
{
|
|
if (s_cfg != null) return s_cfg;
|
|
s_cfg = new WLSkillChainJson();
|
|
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; }
|
|
return s_cfg;
|
|
}
|
|
}
|
|
|
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
|
static void Boot()
|
|
{
|
|
s_cfg = null; s_pending = false; s_data.Clear();
|
|
if (Cfg.enabled == 0) { CombatEvents.HitConfirmed.Remove(OnHit); return; }
|
|
CombatEvents.HitConfirmed.Add(OnHit);
|
|
}
|
|
|
|
static void OnHit(in HitConfirmedEvent e)
|
|
{
|
|
try
|
|
{
|
|
if (s_pending) return; // 이미 예약됨(같은 시전의 다단 히트)
|
|
var sk = e.projectile != null ? e.projectile.m_SkillListTableData : null;
|
|
var pc = e.attacker as PCActor;
|
|
if (sk == null || pc == null || !pc.IsMainPC()) return;
|
|
var w = WLInventory.EquippedInSlot(eFREquipmentType.Weapon);
|
|
if (w == null || w.itemJson == null) return;
|
|
var rows = WLWeaponSkillTable.Rows(w.itemJson.GroupNo);
|
|
if (rows == null) return;
|
|
string trig = "hit:" + sk.e_Skill;
|
|
WLGradeTier gt; int plus;
|
|
int tier = WLEquipGrades.TryMap(w.itemJson, out gt, out plus) ? gt.tier : 1;
|
|
foreach (var r in rows)
|
|
{
|
|
if (!r.IsTriggered || r.trigger != trig || tier < r.unlockTier || r.skillId == sk.n_SkillID) continue;
|
|
var c = Cfg;
|
|
s_pc = pc; s_row = r; s_at = Time.time + c.delay; s_deadline = s_at + c.maxWait; s_pending = true;
|
|
EnsureRunner();
|
|
Triggered++;
|
|
LastLog = "예약 " + r.skillName + " ← " + sk.e_Skill + " 명중 #" + Triggered;
|
|
break;
|
|
}
|
|
}
|
|
catch (System.Exception ex) { Failed++; LastLog = "명중 처리 실패 — " + ex.Message; }
|
|
}
|
|
|
|
static void EnsureRunner()
|
|
{
|
|
if (!DSUtil.CheckNull(s_runner)) return;
|
|
var go = new GameObject("[WL890] SkillChainRunner");
|
|
go.hideFlags = HideFlags.HideAndDontSave;
|
|
s_runner = go.AddComponent<WLSkillChainRunner>();
|
|
}
|
|
|
|
internal static void Tick()
|
|
{
|
|
if (!s_pending || Time.time < s_at) return;
|
|
var pc = s_pc; var r = s_row;
|
|
if (DSUtil.CheckNull(pc) || pc.IsDead() || r == null) { s_pending = false; return; }
|
|
var c = Cfg;
|
|
// #908 이동 중(조이스틱 1 · 구르기)이면 잇지 않는다 — 이어 붙인 스킬이 이동 입력에 바로 스킵(WLSkillCancel)되어 첫 프레임만 번쩍였다(실측 2 프레임)
|
|
if (pc.JoystickStatus == 1 || pc.Get_CurAnim() == eAnim.Dodge || pc.Get_CurAnim() == eAnim.Run) { s_pending = false; Failed++; LastLog = "이동 중 — 연계 안 함 " + r.skillName; return; }
|
|
if (pc.Get_CurAnim() >= eAnim.Skill)
|
|
{
|
|
if (c.cancelTrigger != 0) pc.Play_Idle(); // 앞 스킬(방패 치기)의 남은 동작을 끊고 잇는다
|
|
else if (Time.time < s_deadline) return;
|
|
}
|
|
s_pending = false;
|
|
try
|
|
{
|
|
var data = Prep(pc, r);
|
|
if (data == null) { Failed++; LastLog = "스킬 데이터 없음 " + r.skillId; return; }
|
|
bool ok = pc.Use_Skill(data, true, Mathf.Clamp(r.slot - 1, 0, 5), true);
|
|
if (ok) { Cast++; LastLog = "발동 " + r.skillName + " #" + Cast; }
|
|
else
|
|
{
|
|
Failed++;
|
|
var tg = pc.Get_Target();
|
|
LastFail = "발동 안 됨 " + r.skillName + " · 동작 " + pc.Get_CurAnim() + " · 대상 " + (DSUtil.CheckNull(tg) ? "없음" : Vector3.Distance(tg.Get_position(), pc.Get_position()).ToString("F2") + " m / 사거리 " + data.f_SkillRange) + " · CC " + pc.IsCC();
|
|
LastLog = LastFail;
|
|
}
|
|
}
|
|
catch (System.Exception ex) { Failed++; LastLog = "발동 실패 — " + ex.Message; }
|
|
}
|
|
|
|
static MethodInfo s_clone; static FieldInfo s_fId;
|
|
/// <summary>시험 시전용 복사본(원본 표 데이터 무수정) — MP 0 · 사거리 = 표 range_m · 직업 칸 = PC 직업.</summary>
|
|
static SkillListTableData Prep(PCActor pc, WLWeaponSkillRow r)
|
|
{
|
|
SkillListTableData d;
|
|
if (s_data.TryGetValue(r.skillId, out d)) return d;
|
|
var src = table_skilllist.Ins != null ? table_skilllist.Ins.Get_Data_orNull(r.skillId) : null;
|
|
if (src == null) return null;
|
|
if (s_clone == null) s_clone = typeof(object).GetMethod("MemberwiseClone", BindingFlags.Instance | BindingFlags.NonPublic);
|
|
if (s_fId == null) s_fId = typeof(Actor).GetField("m_ID", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
|
d = (SkillListTableData)s_clone.Invoke(src, null);
|
|
d.n_MP = 0;
|
|
if (r.rangeM > 0f) d.f_SkillRange = r.rangeM;
|
|
d.f_SkillRange = Mathf.Max(d.f_SkillRange, Cfg.minRange);
|
|
if (s_fId != null) d.n_SkillClassID = (int)s_fId.GetValue(pc);
|
|
s_data[r.skillId] = d;
|
|
return d;
|
|
}
|
|
}
|
|
|
|
/// <summary>WLSkillChain 의 시간 축. 숨김 GameObject 1개 · 예약이 있을 때만 일한다.</summary>
|
|
internal sealed class WLSkillChainRunner : MonoBehaviour
|
|
{
|
|
void Update() { WLSkillChain.Tick(); }
|
|
}
|
|
}
|