299 lines
20 KiB
C#
299 lines
20 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
|
// WLSkillCancel.cs — 스킬 중 이동 · 구르기 = 스킵(그 스킬 이펙트 즉시 끔) · 점프 스킬 = 이동 입력 무시 · 이동 키를 누른 채 쓴 스킬은 발동까지 유지 (#908 · 2026-10-17)
|
|
//
|
|
// PD 원문: 「검 각성스킬 사용 중 이동하거나 구르기를 할 경우, 이펙트가 즉시 스킵되며 재생되지 않아야 … (애니메이션이 연속 재생되는 스킬의 경우,
|
|
// 특별한 지시가 없는 한 기본적으로 스킵이 가능하도록 수정해줘)」 · 「(점프로 날아가는 스킬이 발동중에는 플레이어의 이동조작을 무시해야 해)」 ·
|
|
// 「사정거리 밖에서 Q를 눌러 방패 치기를 발동하려고 할 때 대쉬만하고 방패치기가 발동되지 않는 현상」
|
|
// ■ 실측 원인(#908) — 이동 키를 누른 채 Q 를 누르면 다음 프레임의 이동 입력(PCActor.Run_byJoystick → 달리기)이 막 시작한 스킬을 끊었다
|
|
// (방패 치기 = 대쉬 1 프레임 뒤 달리기 · 해머 강력한 일격 = 점프 1 프레임 뒤 달리기 · 둘 다 발동 0).
|
|
// ■ 처리
|
|
// ① 스킬 시작(CombatEvents.SkillCast) 때 이동 키가 눌려 있었으면 그 입력은 첫 발동(SkillFired)까지 무시 — 손을 뗐다 다시 누르면 새 입력(= 스킵).
|
|
// ② lockMove 스킬(점프로 날아가는 스킬)은 스킬 동작이 끝날 때까지 이동 · 구르기 입력을 무시.
|
|
// ③ 그 밖에 스킬 동작 중 이동(달리기)이나 구르기로 스킬 상태를 벗어나면 = 스킵: 그 스킬의 원본 투사체(InGameInfo 스킬 풀 · 이 PC 가 쏜 켜진 것)를 끄고
|
|
// 스킬 연출(SkillSpectacle 차지 · 본체)을 끈다. noSkip 스킬은 제외.
|
|
// 무시한 동안 들어온 PCActor.Set_AutoBraking(누름 false · 뗌 true)은 미뤘다가 잠금이 풀릴 때 마지막 값을 적용한다.
|
|
// ④ #912 HoldMove — 파츠 스킬이 정한 시간 동안 이동 입력 무시(해머 각성 「내려친 상태로 잠시 멈추게」 · 구르기는 허용 = 스킵) · 그동안의 뗌은 EndHoldMove 에서 적용.
|
|
// ⑤ #914 endWithMotion — 스킬 동작이 입력 없이 끝나면(대기 등) 그 스킬 이펙트(투사체 · 연출)도 바로 끈다(PD 「검 각성 … 연속 공격 모션을 끝나는 시점에 맞춰서 이펙트도 소멸」).
|
|
// ⑥ #970 castLock — 시전 동작이 있는 스킬(표 s_NextAnimation · f_CastingTime)은 목록 없이 발동(SkillFired)까지 이동 · 구르기 무시 + 발동 뒤 이동해도 스킵 안 함.
|
|
// ■ 원본 훅 = PCActor.Run_byJoystick · Set_AutoBraking 첫 줄 각 1줄 · 값 = Resources/WL/WLSkillCancel.json (C45) · 끄기 = enabled 0
|
|
// 🔴 Debug.LogError 금지(원본 ErrorLogHook 팝업).
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
using System.Collections;
|
|
using System.Reflection;
|
|
using UnityEngine;
|
|
using WL.Combat.Core;
|
|
|
|
namespace WL.Combat.Weapon
|
|
{
|
|
[System.Serializable]
|
|
public class WLSkillCancelJson
|
|
{
|
|
public int enabled = 1;
|
|
public int skipOnMove = 1; // 1 = 스킬 동작 중 이동 · 구르기 = 스킵(기본)
|
|
public string[] lockMove = new string[0]; // 발동 중 이동 · 구르기 입력을 무시하는 스킬(eEffect 이름)
|
|
public string[] moveLockOnly = new string[0]; // #983 스킬 동작 중 이동 입력만 무시 · 구르기는 허용(발동 전 구르기 = 스킵 = 투사체 없음 · 발동 뒤는 noSkip 대로) — PD 「검기를 쏘기 전 회전할 때 구르기는 막으면 안돼」
|
|
public string[] noSkip = new string[0]; // 이동 · 구르기로 끊겨도 이펙트를 남기는 스킬
|
|
public string[] holdMove = new string[0]; // #911 발동 뒤 공격 이펙트(이 PC 의 원본 스킬 투사체)가 끝날 때까지 이동 입력 무시 · 구르기는 허용(= 스킵)
|
|
public string[] endWithMotion = new string[0]; // #914 스킬 동작이 입력 없이 끝나면(대기로) 그 스킬 이펙트도 바로 끈다 — 남은 이펙트 때문에 서 있지 않게
|
|
public string[] lockUntilFire = new string[0]; // #942 시전 뒤 첫 발동(SkillFired)까지 이동 · 구르기 입력 무시(holdMaxSeconds 상한) — 발동 뒤엔 자유(투사체는 noSkip 으로 유지) · PD 「플레이어가 이동해도 발동 된 스킬이 끊겨선 안돼」(실측: 이동이 발동 이벤트 전에 시전 동작을 끊어 투사체가 아예 안 나왔다)
|
|
public int holdUntilFire = 1; // 1 = 이동 키를 누른 채 쓴 스킬은 첫 발동까지 그 입력을 무시
|
|
public float holdMaxSeconds = 1.5f; // ① 의 상한(발동 이벤트가 없는 스킬 대비) · 시전 시간(f_CastingTime)이 있는 스킬은 상한 = 이 값 + 시전 시간
|
|
// #970 PD 「시전하는 모션이 존재하는 스킬은 일정 타이밍(실제 스킬 발동이 확정 된 시점)까지 못움직이게 하고, 움직여 진 경우에는 발동 된 스킬이 스킵되지 않도록」 —
|
|
// 1 = 시전 동작이 있는 스킬(SkillList s_NextAnimation 있음 = 원본 Co_Casting 시전 단계 · 또는 f_CastingTime > 0)은 이름 목록 없이 lockUntilFire + noSkip 으로 취급(발동 = SkillFired · 원본 SkillEvent 의 Shoot_Skill 직전)
|
|
public int castLock = 0;
|
|
public string note = "";
|
|
}
|
|
|
|
public static class WLSkillCancel
|
|
{
|
|
public const string ResourcesPath = "WL/WLSkillCancel";
|
|
public static int Casts, Skips, SkippedObjects, MoveBlocked, DodgeBlocked, BrakeDeferred, BrakeApplied, Errors, MotionEnds, CastLocks, SkipsAvoided;
|
|
public static string LastLog = "";
|
|
|
|
static WLSkillCancelJson s_cfg;
|
|
static PCActor s_pc;
|
|
static string s_effect = "";
|
|
static float s_castAt, s_cap = 1.5f; // s_cap = 발동까지 잠금 상한(holdMaxSeconds + 시전 시간 · #970)
|
|
static bool s_active, s_fired, s_heldAtCast, s_lock, s_hold, s_lockFire, s_noSkip, s_moveOnly; // s_lockFire = #942 발동까지 잠금 · s_noSkip = 이동해도 이펙트 유지(목록 또는 #970 시전 동작)
|
|
static int s_pendingBrake = -1; // -1 없음 · 0 = Set_AutoBraking(false) · 1 = Set_AutoBraking(true)
|
|
static GameObject s_runner;
|
|
static FieldInfo s_fDicSkill;
|
|
static PCActor s_extPc; // #912 HoldMove 대상 · 끝 시각 · 그동안 미룬 뗌
|
|
static float s_extUntil;
|
|
static bool s_extPending;
|
|
|
|
internal static WLSkillCancelJson Cfg
|
|
{
|
|
get
|
|
{
|
|
if (s_cfg != null) return s_cfg;
|
|
s_cfg = new WLSkillCancelJson();
|
|
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_active = false; s_pc = null; s_pendingBrake = -1; s_extPc = null; s_extPending = false;
|
|
CombatEvents.SkillCast.Remove(OnCast); CombatEvents.SkillFired.Remove(OnFired);
|
|
if (Cfg.enabled == 0) return;
|
|
CombatEvents.SkillCast.Add(OnCast); CombatEvents.SkillFired.Add(OnFired);
|
|
if (s_runner == null)
|
|
{
|
|
s_runner = new GameObject("[WL908] SkillCancel");
|
|
s_runner.hideFlags = HideFlags.HideAndDontSave;
|
|
Object.DontDestroyOnLoad(s_runner);
|
|
s_runner.AddComponent<WLSkillCancelRunner>();
|
|
}
|
|
}
|
|
|
|
static bool Has(string[] list, string s)
|
|
{
|
|
if (list == null || string.IsNullOrEmpty(s)) return false;
|
|
for (int i = 0; i < list.Length; i++) if (list[i] == s) return true;
|
|
return false;
|
|
}
|
|
|
|
static void OnCast(in SkillCastEvent e)
|
|
{
|
|
var pc = e.actor as PCActor;
|
|
if (pc == null || !pc.IsMainPC()) return;
|
|
var c = Cfg;
|
|
s_pc = pc; s_effect = e.skill != null ? e.skill.e_Skill.ToString() : "";
|
|
s_castAt = Time.time; s_active = true; s_fired = false;
|
|
s_heldAtCast = pc.JoystickStatus == 1;
|
|
s_lock = Has(c.lockMove, s_effect); s_moveOnly = Has(c.moveLockOnly, s_effect);
|
|
s_hold = Has(c.holdMove, s_effect);
|
|
bool castMotion = c.castLock != 0 && HasCastMotion(e.skill); // #970 시전 동작이 있는 스킬 = 발동까지 잠금 + 발동 뒤 이동해도 유지
|
|
s_lockFire = Has(c.lockUntilFire, s_effect) || castMotion;
|
|
s_noSkip = Has(c.noSkip, s_effect) || castMotion;
|
|
s_cap = c.holdMaxSeconds + (e.skill != null ? Mathf.Max(0f, e.skill.f_CastingTime) : 0f);
|
|
Casts++; if (castMotion) CastLocks++;
|
|
LastLog = "시작 " + s_effect + (s_lock ? " · 이동 잠금" : "") + (s_lockFire ? " · 발동까지 잠금(상한 " + s_cap.ToString("F1") + " s)" : "") + (castMotion ? " · 시전 동작" : "") + (s_heldAtCast ? " · 이동 키 누른 채" : "");
|
|
}
|
|
|
|
/// <summary>#970 시전 동작이 있는 스킬 — 원본 Actor.Play_Skill 이 Co_Casting(시전 → s_NextAnimation)을 도는 스킬(s_NextAnimation 있음) 또는 시전 시간이 있는 스킬.</summary>
|
|
static bool HasCastMotion(SkillListTableData s)
|
|
{
|
|
return s != null && (!string.IsNullOrEmpty(s.s_NextAnimation) || s.f_CastingTime > 0f);
|
|
}
|
|
|
|
static void OnFired(in SkillFiredEvent e) { if (s_active && ReferenceEquals(e.actor, s_pc)) s_fired = true; }
|
|
|
|
/// <summary>#952-④ 직업(무기) 강제 전환처럼 스킬이 끝 이벤트 없이 끊겼을 때 — 이 PC 의 시전 잠금·미룬 브레이크를 버린다(남으면 이동 입력을 계속 무시한다).</summary>
|
|
public static void ResetFor(PCActor pc)
|
|
{
|
|
if (pc == null) return;
|
|
if (ReferenceEquals(pc, s_pc)) { s_active = false; s_lock = s_hold = s_lockFire = s_noSkip = s_heldAtCast = false; s_pendingBrake = -1; }
|
|
if (ReferenceEquals(pc, s_extPc)) { s_extPc = null; s_extPending = false; }
|
|
LastLog = "초기화(직업 전환) " + s_effect;
|
|
}
|
|
|
|
/// <summary>#912 seconds 동안 이 PC 의 이동 입력을 무시한다(스킬 상태일 때만 · 구르기는 그대로 = 스킵).</summary>
|
|
public static void HoldMove(PCActor pc, float seconds)
|
|
{
|
|
s_extPc = pc; s_extUntil = Time.time + Mathf.Max(0f, seconds); s_extPending = false;
|
|
}
|
|
|
|
/// <summary>#912 멈춤 끝 — apply 면 그동안 미룬 뗌(Set_AutoBraking(true) = 대기 · 브레이크 원복)을 지금 적용 · 끊김이면 버린다.</summary>
|
|
public static void EndHoldMove(PCActor pc, bool apply)
|
|
{
|
|
if (s_extPc == null || !ReferenceEquals(pc, s_extPc)) return;
|
|
s_extPc = null; bool pending = s_extPending; s_extPending = false;
|
|
if (apply && pending && !DSUtil.CheckNull(pc) && !pc.IsDead()) { pc.Set_AutoBraking(true); BrakeApplied++; }
|
|
}
|
|
|
|
static bool ExtBlocking(PCActor pc)
|
|
{
|
|
return s_extPc != null && ReferenceEquals(pc, s_extPc) && Time.time < s_extUntil && !pc.IsDead() && pc.Get_CurAnim() == eAnim.Skill;
|
|
}
|
|
|
|
static bool Blocking(PCActor pc)
|
|
{
|
|
if (ExtBlocking(pc)) return true;
|
|
if (s_hold && ReferenceEquals(pc, s_pc))
|
|
{
|
|
// #911 이펙트가 남아 있는 동안 — 스킬 동작이 끝나 대기로 돌아왔어도(이펙트가 더 길다) 이동 입력 무시 · 구르기로 끊긴 뒤엔 풀림
|
|
var cur = pc.Get_CurAnim();
|
|
if (cur == eAnim.Dodge || pc.IsDead()) { s_hold = false; return false; }
|
|
if ((s_active && cur == eAnim.Skill) || EffectActive(pc, s_effect)) return true;
|
|
s_hold = false;
|
|
}
|
|
if (!s_active || !ReferenceEquals(pc, s_pc) || pc.Get_CurAnim() != eAnim.Skill) return false;
|
|
if (s_lock || s_moveOnly) return true; // #983 moveLockOnly = 이동만(BlockDodge 는 보지 않음 = 구르기 허용)
|
|
var c = Cfg;
|
|
if (s_lockFire && !s_fired && Time.time - s_castAt < s_cap) return true; // #942 발동 전엔 새 이동 입력도 무시
|
|
return c.holdUntilFire != 0 && s_heldAtCast && !s_fired && Time.time - s_castAt < s_cap;
|
|
}
|
|
|
|
/// <summary>PCActor.Run_byJoystick 첫 줄 훅 — true = 이 이동 입력을 무시한다.</summary>
|
|
public static bool BlockMove(PCActor pc)
|
|
{
|
|
try { if (Blocking(pc)) { MoveBlocked++; return true; } }
|
|
catch (System.Exception ex) { Errors++; LastLog = "이동 확인 실패 — " + ex.Message; }
|
|
return false;
|
|
}
|
|
|
|
/// <summary>WLDoubleTapDodge 훅 — 이동 잠금 스킬(점프) 중엔 구르기도 무시.</summary>
|
|
public static bool BlockDodge(PCActor pc)
|
|
{
|
|
if (pc == null || !s_active || !ReferenceEquals(pc, s_pc) || pc.Get_CurAnim() != eAnim.Skill) return false;
|
|
if (!s_lock && !(s_lockFire && !s_fired && Time.time - s_castAt < s_cap)) return false; // #942 발동 전 구르기도 무시
|
|
DodgeBlocked++; return true;
|
|
}
|
|
|
|
/// <summary>PCActor.Set_AutoBraking 첫 줄 훅 — 입력을 무시하는 동안의 누름 · 뗌은 미뤘다가 잠금이 풀릴 때 적용(true = 원본은 지금 아무것도 안 함).</summary>
|
|
public static bool DeferBrake(PCActor pc, bool active)
|
|
{
|
|
try
|
|
{
|
|
if (ExtBlocking(pc))
|
|
{
|
|
// #912 멈춤 중 — 누름(구르기 시작 포함)은 그대로 · 뗌(= Play_Idle)만 멈춤이 끝날 때(EndHoldMove) 적용
|
|
if (!active) { s_extPending = false; return false; }
|
|
s_extPending = true; BrakeDeferred++; return true;
|
|
}
|
|
if (!Blocking(pc)) return false;
|
|
// #911 이펙트 끝까지 이동 잠금(holdMove)만 걸린 경우 누름(false)은 그대로 둔다 — 원본 구르기(Play_Dodge)가 시작할 때 부르는 Set_AutoBraking(false)까지 미루면
|
|
// 구르기가 곧바로 대기로 끊겼다(실측). 뗌(true = Play_Idle)만 미뤄 스킬 동작이 중간에 끊기지 않게 한다.
|
|
if (!active && s_hold && !s_lock && !(Cfg.holdUntilFire != 0 && s_heldAtCast && !s_fired)) return false;
|
|
s_pendingBrake = active ? 1 : 0;
|
|
if (active) s_heldAtCast = false; // 손을 뗐다 → 다음 누름은 새 입력
|
|
BrakeDeferred++;
|
|
return true;
|
|
}
|
|
catch (System.Exception ex) { Errors++; LastLog = "미루기 실패 — " + ex.Message; return false; }
|
|
}
|
|
|
|
static void ApplyBrake(PCActor pc)
|
|
{
|
|
int b = s_pendingBrake; s_pendingBrake = -1;
|
|
if (b < 0 || DSUtil.CheckNull(pc) || pc.IsDead()) return;
|
|
pc.Set_AutoBraking(b == 1);
|
|
BrakeApplied++;
|
|
}
|
|
|
|
internal static void Tick()
|
|
{
|
|
var pc = s_pc;
|
|
if (!s_active && s_hold && !DSUtil.CheckNull(pc) && !Blocking(pc) && s_pendingBrake >= 0) ApplyBrake(pc); // #911 이펙트가 끝나 이동 잠금이 풀리면 미룬 누름 · 뗌 적용
|
|
if (!s_active || DSUtil.CheckNull(pc)) return;
|
|
try
|
|
{
|
|
if (pc.IsDead()) { s_active = false; s_pendingBrake = -1; return; }
|
|
var cur = pc.Get_CurAnim();
|
|
if (cur == eAnim.Skill)
|
|
{
|
|
// ① 이 끝났는데(발동 · 상한) 이동 키가 아직 눌려 있으면 미룬 누름을 지금 적용 — 이어지는 이동 입력이 평소 규칙(스킵)대로 동작
|
|
if (s_pendingBrake == 0 && !s_lock && !Blocking(pc)) ApplyBrake(pc);
|
|
return;
|
|
}
|
|
s_active = false;
|
|
// 스킬 상태에서 바로 달리기 · 구르기 = 입력으로 끊김(자연 종료는 Event_Idle → 대기). 원본 구르기는 같은 프레임에 달리기로 넘어가 Dodge 를 못 볼 때가 있다(실측)
|
|
bool byInput = cur == eAnim.Run || cur == eAnim.Dodge;
|
|
var c = Cfg;
|
|
if (byInput && c.skipOnMove != 0 && !s_noSkip) Skip(pc, s_effect, cur == eAnim.Dodge ? "구르기" : "이동");
|
|
else if (cur == eAnim.Dodge && s_moveOnly && !s_fired) Skip(pc, s_effect, "발동 전 구르기"); // #983 회전 중 구르기로 캔슬 = 검기 · 시전 연출 없음
|
|
else if (byInput && s_noSkip) { SkipsAvoided++; LastLog = "유지 " + s_effect + " · " + (cur == eAnim.Dodge ? "구르기" : "이동") + (s_fired ? " · 발동 뒤" : " · 발동 전"); }
|
|
else if (!byInput && Has(c.endWithMotion, s_effect)) { MotionEnds++; Skip(pc, s_effect, "동작 끝"); } // #914 동작 끝 = 이펙트 끝(이동 잠금 holdMove 도 함께 풀림)
|
|
if (s_pendingBrake >= 0 && !Blocking(pc)) ApplyBrake(pc);
|
|
}
|
|
catch (System.Exception ex) { Errors++; LastLog = "틱 실패 — " + ex.Message; s_active = false; }
|
|
}
|
|
|
|
/// <summary>#911 이 PC 가 쏜 그 스킬의 원본 투사체(InGameInfo 스킬 풀)가 아직 켜져 있나.</summary>
|
|
static bool EffectActive(PCActor pc, string effect)
|
|
{
|
|
eEffect ef;
|
|
if (InGameInfo.Ins == null || !System.Enum.TryParse(effect, out ef)) return false;
|
|
if (s_fDicSkill == null) s_fDicSkill = typeof(InGameInfo).GetField("dic_Skill", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
|
var dic = s_fDicSkill != null ? s_fDicSkill.GetValue(InGameInfo.Ins) as IDictionary : null;
|
|
var list = dic != null && dic.Contains(ef) ? dic[ef] as IList : null;
|
|
if (list == null) return false;
|
|
foreach (var o in list)
|
|
{
|
|
var go = o as GameObject;
|
|
if (go == null || !go.activeInHierarchy) continue;
|
|
var pb = go.GetComponent<ProjectileBase>(); var data = pb != null ? pb.Get_ProjectTileData() : null;
|
|
if (data != null && ReferenceEquals(data.shooter, pc)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>스킵 — 그 스킬의 원본 투사체(이 PC 가 쏜 켜진 것)와 스킬 연출(차지 · 본체)을 끈다.</summary>
|
|
static void Skip(PCActor pc, string effect, string why)
|
|
{
|
|
int n = 0;
|
|
eEffect ef;
|
|
if (InGameInfo.Ins != null && System.Enum.TryParse(effect, out ef))
|
|
{
|
|
if (s_fDicSkill == null) s_fDicSkill = typeof(InGameInfo).GetField("dic_Skill", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
|
var dic = s_fDicSkill != null ? s_fDicSkill.GetValue(InGameInfo.Ins) as IDictionary : null;
|
|
var list = dic != null && dic.Contains(ef) ? dic[ef] as IList : null;
|
|
if (list != null)
|
|
foreach (var o in list)
|
|
{
|
|
var go = o as GameObject;
|
|
if (go == null || !go.activeInHierarchy) continue;
|
|
var pb = go.GetComponent<ProjectileBase>();
|
|
var data = pb != null ? pb.Get_ProjectTileData() : null;
|
|
if (data == null || !ReferenceEquals(data.shooter, pc)) continue;
|
|
WLSlashFlurry.Off(pb); n++;
|
|
}
|
|
}
|
|
n += WL.Combat.Reaction.SkillSpectacle.CancelOwner(pc);
|
|
Skips++; SkippedObjects += n;
|
|
LastLog = "스킵 #" + Skips + " " + effect + " · " + why + " · 끈 것 " + n;
|
|
}
|
|
}
|
|
|
|
internal sealed class WLSkillCancelRunner : MonoBehaviour
|
|
{
|
|
void LateUpdate() { WLSkillCancel.Tick(); }
|
|
}
|
|
}
|