// ───────────────────────────────────────────────────────────────────────────── // WLJumpSlam.cs — 점프 내려찍기 타이밍: 점프 중(최고점 무렵)에 내려찍기 동작으로 넘어가 착지 순간에 지면을 찍게 (#909 · 2026-10-17) // // PD 원문: 「망치 주력 스킬 사용시 점프 최고점에서 내려찍기 모션 상태로 지면을 찍도록 해줘. (지금은 점프가 끝난 후 지면에서 내려치는 모션이 나오고 있어서 어색해)」 // ■ 원본 = 점프 이동은 Actor.Co_JumpAttack(높이 1 · landAt 초) · 동작은 컨트롤러 jump(Armed-Jump-Flip) → 끝 무렵 jumpattack(Mace-Attack-R2 · SkillEvent = 타격) // 으로 넘어가 내려치기 타격이 착지 뒤에 나왔다. // ■ 처리 — 스킬 시작(CombatEvents.SkillCast)부터 slamAt 초에 slamState 로 바로 넘기고, 그 클립의 SkillEvent(타격) 시각이 정확히 landAt 초(착지)에 오도록 // 애니메이터 속도를 맞춘다(타격 이벤트 시각은 클립에서 읽음). 착지 뒤 속도 1 로 되돌림. // ■ 값 = Resources/WL/WLJumpSlam.json (C45) · 끄기 = enabled 0 → 원본 동작 · 원본 수정 0줄 // 🔴 Debug.LogError 금지(원본 팝업). // ───────────────────────────────────────────────────────────────────────────── using UnityEngine; using WL.Combat.Core; namespace WL.Combat.Weapon { [System.Serializable] public class WLJumpSlamRule { public string skill = ""; // eEffect 이름(원본 스킬 클래스) public string slamState = ""; // 내려찍기 상태 이름 public string slamClip = ""; // 그 상태의 클립 이름(타격 SkillEvent 시각을 읽는다) public float slamAt = 0.3f; // 스킬 시작 뒤 내려찍기로 넘어가는 시각(초) public float landAt = 0.8f; // 착지 시각(초) = 원본 Co_JumpAttack 지속 시간 public float crossFade = 0.08f; // 넘어가는 시간(초) } [System.Serializable] public class WLJumpSlamJson { public int enabled; public WLJumpSlamRule[] rules = new WLJumpSlamRule[0]; public string note = ""; } public static class WLJumpSlam { public const string ResourcesPath = "WL/WLJumpSlam"; public static int Slams, Missing, Errors; public static string LastLog = ""; static WLJumpSlamJson s_cfg; static WLJumpSlamJson Cfg { get { if (s_cfg != null) return s_cfg; s_cfg = new WLJumpSlamJson(); var ta = Resources.Load(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; CombatEvents.SkillCast.Remove(OnCast); if (Cfg.enabled != 0) CombatEvents.SkillCast.Add(OnCast); } static void OnCast(in SkillCastEvent e) { try { var pc = e.actor as PCActor; if (pc == null || !pc.IsMainPC() || e.skill == null || Cfg.rules == null) return; string ef = e.skill.e_Skill.ToString(); foreach (var r in Cfg.rules) { if (r == null || r.skill != ef) continue; var run = pc.GetComponent(); if (run == null) run = pc.gameObject.AddComponent(); run.Begin(pc, r); return; } } catch (System.Exception ex) { Errors++; LastLog = "시작 실패 — " + ex.Message; } } /// 클립의 첫 SkillEvent 시각(초) · 없으면 -1. internal static float ImpactTime(Animator an, string clipName) { if (an == null || an.runtimeAnimatorController == null || string.IsNullOrEmpty(clipName)) return -1f; foreach (var c in an.runtimeAnimatorController.animationClips) { if (c == null || c.name != clipName) continue; foreach (var ev in c.events) if (ev.functionName == "SkillEvent") return ev.time; } return -1f; } } public sealed class WLJumpSlamRunner : MonoBehaviour { PCActor _pc; WLJumpSlamRule _r; float _t0, _impact; bool _on, _slammed; public void Begin(PCActor pc, WLJumpSlamRule r) { _pc = pc; _r = r; _t0 = Time.time; _slammed = false; _impact = WLJumpSlam.ImpactTime(pc.m_animation, r.slamClip); _on = _impact > 0f; if (!_on) { WLJumpSlam.Missing++; WLJumpSlam.LastLog = "타격 이벤트 없음 " + r.slamClip; } } void Update() { if (!_on) return; var an = _pc != null ? _pc.m_animation : null; if (an == null || _pc.IsDead() || _pc.Get_CurAnim() != eAnim.Skill) { End(an); return; } float t = Time.time - _t0; if (!_slammed && t >= _r.slamAt) { _slammed = true; float remain = Mathf.Max(0.05f, _r.landAt - t); an.CrossFadeInFixedTime(_r.slamState, Mathf.Max(0f, _r.crossFade), 0, 0f); an.speed = Mathf.Clamp(_impact / remain, 0.3f, 3f); WLJumpSlam.Slams++; WLJumpSlam.LastLog = "내려찍기 " + _r.slamState + " · " + t.ToString("0.00") + " s · 속도 " + an.speed.ToString("0.00") + " · 타격 예정 " + _r.landAt.ToString("0.00") + " s"; } if (_slammed && t >= _r.landAt + 0.05f) End(an); } void End(Animator an) { if (_on && _slammed && an != null) an.speed = 1f; _on = false; } void OnDisable() { _on = false; } } }