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

131 lines
8.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─────────────────────────────────────────────────────────────────────────────
// WLSkillApproach.cs — 스킬 시작 때 거리로 분기: 사정거리 안 = 바로 시전 동작 · 밖 = 빠른 대쉬로 붙은 뒤 시전 동작 — 표 기반 (#905 · 2026-10-17)
//
// PD 원문: 「사정거리 밖에서 방패 치기를 시도할 경우 걷지 말고 빠르게 대쉬하는 동작 후 방패치기가 가능하도록 수정하고, 이미 사정거리에 적이 있는 경우
// (플레이어가 움직일 필요가 없는 경우)에는 이동 동작을 생략하고 방패치기 하도록」
// ■ 원리 — 원본 Actor.Play_Skill 의 동작 재생 줄 훅 1개가 스킬 동작 이름(예: shieldattack)을 넘기면 표 규칙을 찾아
// · 대상까지 수평 거리 ≤ reach(+ 대상 몸 반지름) → actState(예: shieldbash = 방패 치기 클립)를 바로 돌려준다(이동 0).
// · 더 멀면 dashState(일반 공격 대쉬와 같은 dash 상태)를 돌려주고, 러너가 NavMeshAgent.Move 로 stopDistance(+ 반지름)까지 dashSpeed 로 붙인 뒤
// (클립 속도를 대쉬 시간에 맞춤) actState 로 넘긴다. 상한 시간이 지나면 그 자리에서 넘긴다.
// 방패 치기 피해는 원본 Skill_ShieldAttack 이 대상에게 직접 준다(판정 상자 없음) — 거리 값은 보이는 접촉 기준.
// ■ 값 = Resources/WL/WLSkillApproach.json (C45) · 끄기 = enabled 0 → 원본 동작(shieldattack = 걷기 두 걸음 → 방패 치기 · #899/#900)
// 🔴 Debug.LogError 금지(원본 팝업).
// ─────────────────────────────────────────────────────────────────────────────
using UnityEngine;
namespace WL.Combat.Weapon
{
[System.Serializable]
public class WLSkillApproachRule
{
public string motion = ""; // 원본 스킬 동작 이름(s_Animation)
public int[] classIds = new int[0];
public string actState = ""; // 실제 시전 동작 상태
public float reach; // 이 거리(m · 몸 중심 사이) 이내면 바로 시전
public float stopDistance; // 대쉬로 붙는 거리(m)
public int addTargetRadius = 1; // 1 = 위 두 거리에 대상 몸 반지름(NavMeshAgent 반경 × 크기)을 더한다
public string dashState = "dash";
public string dashClipName = "Chase_Start";
public float dashSpeed = 10f; // m/s
public float dashMaxSeconds = 0.3f;
public float clipHoldSeconds = 0.2f; // #909 대쉬 클립을 대쉬 시간보다 이만큼 더 길게 재생(도착 전에 클립이 끝나지 않게)
public float crossFade = 0.06f;
}
[System.Serializable] public class WLSkillApproachJson { public int enabled; public WLSkillApproachRule[] rules = new WLSkillApproachRule[0]; public string note = ""; }
public static class WLSkillApproach
{
public const string ResourcesPath = "WL/WLSkillApproach";
public static int Immediate, Dashes, Arrivals, Timeouts, Broken; public static string LastLog = "";
static WLSkillApproachJson s_cfg;
static WLSkillApproachJson Cfg
{
get
{
if (s_cfg != null) return s_cfg;
s_cfg = new WLSkillApproachJson();
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; }
internal static float Radius(Actor a)
{
var ag = a != null ? a.Get_Agent() : null;
return ag != null ? ag.radius * Mathf.Max(a.transform.lossyScale.x, a.transform.lossyScale.z) : 0f;
}
/// <summary>원본 Actor.Play_Skill 훅 — 지금 재생할 상태 이름을 돌려준다(규칙 없음 = 원래 이름).</summary>
public static string Motion(Actor actor, string motion)
{
try
{
var c = Cfg;
var pc = actor as PCActor;
if (c.enabled == 0 || pc == null || !pc.IsMainPC() || string.IsNullOrEmpty(motion) || c.rules == null) return motion;
WLSkillApproachRule r = null; int cls = pc.Get_ID();
foreach (var x in c.rules) if (x != null && x.motion == motion && x.classIds != null && System.Array.IndexOf(x.classIds, cls) >= 0) { r = x; break; }
if (r == null || string.IsNullOrEmpty(r.actState)) return motion;
var tg = pc.Get_Target();
if (tg == null || tg.IsDead()) { Immediate++; return r.actState; }
float rad = r.addTargetRadius != 0 ? Radius(tg) : 0f;
var d = tg.Get_position() - pc.Get_position(); d.y = 0f;
float dist = d.magnitude;
if (dist <= r.reach + rad) { Immediate++; LastLog = "바로 " + r.actState + " · 거리 " + dist.ToString("0.##"); return r.actState; }
var run = pc.GetComponent<WLSkillApproachRunner>(); if (run == null) run = pc.gameObject.AddComponent<WLSkillApproachRunner>();
run.Begin(pc, tg, r, r.stopDistance + rad, dist);
Dashes++; LastLog = "대쉬 " + r.dashState + " · 거리 " + dist.ToString("0.##") + " → " + (r.stopDistance + rad).ToString("0.##");
return r.dashState;
}
catch (System.Exception ex) { LastLog = "실패 — " + ex.Message; return motion; }
}
}
/// <summary>스킬 대쉬 이동 → 도착(또는 상한 시간)에 시전 동작으로 넘긴다.</summary>
public sealed class WLSkillApproachRunner : MonoBehaviour
{
PCActor _pc; Actor _tg; WLSkillApproachRule _r; float _stop, _t0, _maxT; bool _on;
public void Begin(PCActor pc, Actor tg, WLSkillApproachRule r, float stop, float dist)
{
_pc = pc; _tg = tg; _r = r; _stop = stop; _t0 = Time.time; _on = true;
float dur = Mathf.Clamp(Mathf.Max(dist - stop, 0f) / Mathf.Max(r.dashSpeed, 0.1f), 0.05f, Mathf.Max(0.05f, r.dashMaxSeconds));
_maxT = dur + 0.15f;
// #909 대쉬 클립은 대쉬보다 조금 길게(clipHoldSeconds) — 클립이 먼저 끝나 대기로 넘어가면(Event_Idle) 스킬이 끊겨 방패 치기가 안 나갔다(실측 6 · 9 m)
var an = pc.m_animation;
if (an != null && an.runtimeAnimatorController != null && !string.IsNullOrEmpty(r.dashClipName))
foreach (var clip in an.runtimeAnimatorController.animationClips)
if (clip != null && clip.name.Contains(r.dashClipName)) { an.speed = Mathf.Clamp(clip.length / (dur + Mathf.Max(0f, r.clipHoldSeconds)), 0.3f, 2.5f); break; }
}
void Update()
{
if (!_on) return;
if (_pc == null || _pc.IsDead() || _pc.IsCC() || _pc.Get_CurAnim() != eAnim.Skill || _tg == null || _tg.IsDead()) { End(_pc != null && !_pc.IsDead() && _pc.Get_CurAnim() == eAnim.Skill); WLSkillApproach.Broken++; return; }
var to = _tg.Get_position() - _pc.Get_position(); to.y = 0f;
float dist = to.magnitude;
if (dist > 1e-3f) _pc.transform.rotation = Quaternion.LookRotation(to / dist);
if (dist <= _stop) { WLSkillApproach.Arrivals++; End(true); return; }
if (Time.time - _t0 > _maxT) { WLSkillApproach.Timeouts++; End(true); return; }
var ag = _pc.Get_Agent();
if (ag != null && ag.enabled && ag.isOnNavMesh) ag.Move(to / dist * Mathf.Min(_r.dashSpeed * Time.deltaTime, dist - _stop));
}
void End(bool act)
{
_on = false;
var an = _pc != null ? _pc.m_animation : null;
if (an != null) an.speed = 1f;
if (act && an != null) an.CrossFadeInFixedTime(_r.actState, Mathf.Max(0f, _r.crossFade), 0, 0f);
}
void OnDisable() { _on = false; }
}
}