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

274 lines
17 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 「검 각성 … 연속 공격 모션을 끝나는 시점에 맞춰서 이펙트도 소멸」).
// ■ 원본 훅 = 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[] 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; // ① 의 상한(발동 이벤트가 없는 스킬 대비)
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;
public static string LastLog = "";
static WLSkillCancelJson s_cfg;
static PCActor s_pc;
static string s_effect = "";
static float s_castAt;
static bool s_active, s_fired, s_heldAtCast, s_lock, s_hold, s_lockFire; // s_lockFire = #942 발동까지 잠금
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_hold = Has(c.holdMove, s_effect);
s_lockFire = Has(c.lockUntilFire, s_effect);
Casts++;
LastLog = "시작 " + s_effect + (s_lock ? " · 이동 잠금" : "") + (s_lockFire ? " · 발동까지 잠금" : "") + (s_heldAtCast ? " · 이동 키 누른 채" : "");
}
static void OnFired(in SkillFiredEvent e) { if (s_active && ReferenceEquals(e.actor, s_pc)) s_fired = true; }
/// <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) return true;
var c = Cfg;
if (s_lockFire && !s_fired && Time.time - s_castAt < c.holdMaxSeconds) return true; // #942 발동 전엔 새 이동 입력도 무시
return c.holdUntilFire != 0 && s_heldAtCast && !s_fired && Time.time - s_castAt < c.holdMaxSeconds;
}
/// <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 < Cfg.holdMaxSeconds)) 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 && !Has(c.noSkip, s_effect)) Skip(pc, s_effect, cur == eAnim.Dodge ? "구르기" : "이동");
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(); }
}
}