131 lines
8.4 KiB
C#
131 lines
8.4 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 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; }
|
||
}
|
||
}
|