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

298 lines
21 KiB
C#
Raw Permalink 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.

// ─────────────────────────────────────────────────────────────────────────────
// 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줄
// ■ #998 (2026-10-30) PD 「창 기본 스킬은 회전하며 점프하는게 아니라 제자리에서 점프했다가 내려치기만」 · 「창 기본 스킬 점프하며 내려치는 동안 짧게 기가 모이는 연출을 발생한 후 내리쳐」
// · 「대검 기본 스킬 = 점프하는 동안 몸을 회전하며 검기로 몸을 세로 방향으로 최대 3회전 하며 공격하고 내려치는 공격(회전하는 검기는 기본 궤적 검기)」
// — 규칙 classId(직업 행 먼저) · ownJump 1 = 원본 점프 이동(Co_JumpAttack · 대상 쪽 · 앞구르기 클립 jump)을 Actor 훅 1줄(OwnJump)로 끄고 제자리 점프:
// 오름 rise(내려찍기 클립을 처음부터 타격 시각 × holdAt 까지 = 무기를 든 자세) → 멈춤 hang(자세 고정 · hangFx) → 내림 fall(타격 시각이 착지에 오게 속도) · 높이 height
// spins = 오름 + 멈춤 동안 엉덩이 뼈를 몸 중심 · 오른쪽 축으로 세로 회전하는 바퀴 수 · spinFx = 바퀴마다 몸 중심에 띄우는 검기(PC 를 따라감)
// 🔴 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; // 넘어가는 시간(초)
public int classId; // #998 0 = 모든 직업 · > 0 = 이 직업만(직업이 맞는 행 먼저)
public int ownJump; // #998 1 = 제자리 점프(아래 값) · 0 = 원본 점프 이동 + slamAt · landAt
public float height = 1f, rise = 0.3f, hang = 0.25f, fall = 0.2f; // #998 높이(m) · 오름 · 멈춤 · 내림(초)
public float holdAt = 0.5f; // #998 멈춤 자세 = 내려찍기 클립 타격 시각의 이 비율 자리
public WLStaffEarthFx hangFx = new WLStaffEarthFx(); // #998 멈춤 동안 모으기 이펙트(시전자 기준 · follow 1 = 따라감)
public int spins; // #998 세로 회전 바퀴 수(0 = 안 돎)
public WLStaffEarthFx spinFx = new WLStaffEarthFx(); // #998 바퀴마다 몸 중심에 띄우는 검기(PC 를 따라감 · euler = 정면 기준)
// #998 PD 「대검 기본 스킬은 회전하는 동안 제자리에 있는게 아니라 1.5m 가량 전진하며 낙하까지 · 회전하는 동안 상승(↗)과 낙하(↘)하며 회전이 멈출 타이밍에 맞춰 내려치는 동작이 이어지도록」
// arc 1 = 높이가 착지까지 한 포물선(오름 · 멈춤 · 내림 구분 없이 ↗↘ · 회전은 rise + hang 동안 = 내려가는 중에 멈추고 바로 내림 fall 동안 내려치기) · forward = 착지까지 정면으로 가는 거리(m)
public int arc;
public float forward;
// #1000 PD 「대검 기본 스킬 … 마지막 내려칠 때는 기본 공격 마지막타와 동일하게 내려치기동작과 이펙트로」 — landFx = 착지(타격) 순간 PC 정면 기준 offset 자리에 띄우는 지면 이펙트(기본 4타 지면 충격과 같은 프리팹)
public WLStaffEarthFx landFx = new WLStaffEarthFx();
public WLStaffEarthFx landTrace = new WLStaffEarthFx(); // #1006 PD 「대검 기본 스킬 사용 후 지면에 충격 흔적이 남도록 (해머 공격에 사용 된 지면 충격 효과를 참고)」 — landFx 와 같은 순간 · 같은 기준(PC 정면 offset)에 남기는 흔적
public float impactTime; // #1001 0 보다 크면 내려찍기 타격 시각(클립 초)을 클립 SkillEvent 대신 이 값으로(대검 = SkillEvent 를 점프 시작으로 옮겨 회전 연타 판정을 WLStaffEarth 가 맡음 · 내려치기 동작 시각은 4타 그대로 0.80)
// #1008 PD 「해머 주력 스킬 시전 시 해머를 내려치기 전 딜레이를 추가하고, 딜레이 시간 동안 해머가 빛나게 해줘. (해머 각성 스킬의 50% 정도의 딜레이 및 빛나는 연출)」 —
// hangGlow > 0 = 멈춤(hang) 동안 무기 흰 빛 0 → hangGlow(해머 각성과 같은 덧그림 WLWeaponGlowFx) · 내림 동안 유지 · 착지(타격)부터 glowFade 초에 꺼짐
public float hangGlow;
public float[] glowColor = { 1.6f, 1.6f, 1.6f };
public float glowFill = 0.35f, glowRim = 2.5f, glowFade = 0.25f;
public string glowMaterial = "WL/Fx/WL_Mat_WeaponGlow";
}
[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<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;
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;
var r = Find(pc, e.skill.e_Skill.ToString()); if (r == null) return;
var run = pc.GetComponent<WLJumpSlamRunner>(); if (run == null) run = pc.gameObject.AddComponent<WLJumpSlamRunner>();
run.Begin(pc, r);
}
catch (System.Exception ex) { Errors++; LastLog = "시작 실패 — " + ex.Message; }
}
/// <summary>#998 직업이 맞는 행 먼저 · 없으면 classId 0 행.</summary>
static WLJumpSlamRule Find(PCActor pc, string skill)
{
WLJumpSlamRule any = null; int cls = pc.Get_ID();
foreach (var r in Cfg.rules)
{
if (r == null || r.skill != skill) continue;
if (r.classId == cls) return r;
if (r.classId == 0 && any == null) any = r;
}
return any;
}
/// <summary>#998 Actor.Play_Skill 훅(1줄) — 이번 시전을 제자리 점프 러너가 맡았으면 true(원본 점프 이동 Co_JumpAttack 을 건너뜀). SkillCast 이벤트(Begin)가 먼저 불린다.</summary>
public static bool OwnJump(Actor a)
{
try { var pc = a as PCActor; if (pc == null || Cfg.enabled == 0) return false; var run = pc.GetComponent<WLJumpSlamRunner>(); return run != null && run.OwnActive; }
catch (System.Exception ex) { Errors++; LastLog = "OwnJump 실패 — " + ex.Message; return false; }
}
public static int OwnJumps, SpinFxShown, HangFxShown, LandFxShown, LandTraces, Glows; public static float LastSpinDeg, LastPeak, LastGlow;
/// <summary>클립의 첫 SkillEvent 시각(초) · 없으면 -1.</summary>
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;
// #998 제자리 점프
bool _own, _started, _hung, _set, _landFx; float _hold, _y0, _peak, _lastAng; int _spun; Transform _bone; Vector3 _pivotLocal, _setPos, _start, _fwd; Quaternion _setRot;
// #1008 멈춤 동안 무기 흰 빛
readonly WLWeaponGlowFx _glow = new WLWeaponGlowFx(); WLJumpSlamRule _glowRule; float _glowFadeAt = -1f; Color _glowCol = Color.white;
public bool OwnActive { get { return _on && _own; } }
void GlowSet(float g) { if (_glowRule == null) return; _glow.Set(g, _glowCol, _glowRule.glowFill, _glowRule.glowRim); WLJumpSlam.LastGlow = g; }
void GlowFade() { if (_glow.Active && _glowFadeAt < 0f) _glowFadeAt = Time.time; }
/// <summary>#1008 착지 뒤 흰 빛 끄기 — 시전이 끝난 뒤(_on 꺼짐)에도 돈다.</summary>
void GlowTick()
{
if (_glowFadeAt < 0f || _glowRule == null) return;
float k = 1f - Mathf.Clamp01((Time.time - _glowFadeAt) / Mathf.Max(0.01f, _glowRule.glowFade));
if (k <= 0f) { _glow.End(); _glowFadeAt = -1f; return; }
GlowSet(_glowRule.hangGlow * k);
}
public void Begin(PCActor pc, WLJumpSlamRule r)
{
if (_on && _own) Restore();
_glow.End(); _glowFadeAt = -1f; _glowRule = null; // #1008
_pc = pc; _r = r; _t0 = Time.time; _slammed = false;
_impact = r.impactTime > 0f ? r.impactTime : WLJumpSlam.ImpactTime(pc.m_animation, r.slamClip);
_on = _impact > 0f;
_own = _on && r.ownJump != 0; _started = _hung = _set = _landFx = false; _spun = 0; _lastAng = 0f; _bone = null;
if (!_on) { WLJumpSlam.Missing++; WLJumpSlam.LastLog = "타격 이벤트 없음 " + r.slamClip; return; }
if (!_own) return;
_hold = _impact * Mathf.Clamp01(r.holdAt); _y0 = pc.transform.position.y; _peak = 0f; _start = pc.transform.position; _fwd = pc.transform.forward;
if (OptionInfo.Ins != null) OptionInfo.Ins.Set_PCMove(pc, false); // 원본 Co_JumpAttack 과 같게(조작 끔 · 착지에 되돌림)
if (r.spins > 0)
{
_bone = SpinBone(pc);
_pivotLocal = pc.transform.InverseTransformPoint(WL.Combat.Impact.WLImpactFx.BodyCenter(pc));
}
WLJumpSlam.OwnJumps++;
}
/// <summary>#956 과 같은 뼈 — Humanoid Hips · 없으면 루트 직계 중 이름이 Root 로 시작하는 것(이 PC = Root_M).</summary>
static Transform SpinBone(PCActor pc)
{
var an = pc.m_animation; Transform b = an != null && an.isHuman ? an.GetBoneTransform(HumanBodyBones.Hips) : null;
if (b == null) foreach (Transform c in pc.transform) if (c.name.StartsWith("Root")) { b = c; break; }
return b;
}
void Update()
{
GlowTick(); // #1008
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 (_own) { OwnTick(an, t); return; }
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);
}
/// <summary>#998 제자리 점프 — 오름(무기 드는 자세까지 감기) → 멈춤(자세 고정 · 모으기) → 내림(타격 = 착지) · 높이는 매 프레임 직접.</summary>
void OwnTick(Animator an, float t)
{
var r = _r; float rise = Mathf.Max(0.05f, r.rise), hang = Mathf.Max(0f, r.hang), fall = Mathf.Max(0.05f, r.fall), land = rise + hang + fall;
if (!_started) // 첫 프레임 — Play_Skill 이 시전 이벤트 뒤에 스킬 동작(jump)을 틀므로 여기서 내려찍기로 넘긴다
{
_started = true;
_start = _pc.transform.position; _fwd = _pc.transform.forward; _fwd.y = 0f; if (_fwd.sqrMagnitude < 1e-4f) _fwd = Vector3.forward; _fwd.Normalize(); // Play_Skill 이 대상 쪽으로 돌린 뒤
an.CrossFadeInFixedTime(r.slamState, Mathf.Max(0f, r.crossFade), 0, 0f);
an.speed = Mathf.Clamp(_hold / rise, 0.05f, 3f);
WLJumpSlam.Slams++;
WLJumpSlam.LastLog = "제자리 점프 " + r.slamState + " · 높이 " + r.height.ToString("0.00") + " · 오름 " + rise.ToString("0.00") + " · 멈춤 " + hang.ToString("0.00") + " · 내림 " + fall.ToString("0.00") + " · 회전 " + r.spins + " · 타격 예정 " + land.ToString("0.00") + " s";
}
if (!_hung && t >= rise)
{
_hung = true; an.speed = 0f;
if (hang > 0f && r.hangFx != null && !string.IsNullOrEmpty(r.hangFx.prefab))
{
var go = WLStaffEarth.SpawnAt(r.hangFx, _pc, _pc.Get_position(), _pc.transform.forward, hang);
if (go != null) { if (r.hangFx.follow != 0) go.transform.SetParent(_pc.transform, true); WLJumpSlam.HangFxShown++; }
}
if (r.hangGlow > 0f) // #1008 무기 흰 빛 덧그림(무기 소켓 = 끝이 가장 먼 소켓)
{
Transform sk; Vector3 tl;
if (WL.Combat.Impact.WLImpactRunner.SocketTip(_pc, out sk, out tl) && _glow.Begin(sk, r.glowMaterial) > 0)
{
_glowRule = r; WLJumpSlam.Glows++;
var gc = r.glowColor; _glowCol = gc != null && gc.Length >= 3 ? new Color(gc[0], gc[1], gc[2], 1f) : Color.white;
}
}
}
if (_glowRule != null && _glowFadeAt < 0f && t >= rise) GlowSet(r.hangGlow * Mathf.Clamp01((t - rise) / Mathf.Max(0.01f, hang))); // #1008 멈춤 동안 0 → hangGlow · 내림 동안 유지
if (!_slammed && t >= rise + hang)
{
_slammed = true; an.speed = Mathf.Clamp((_impact - _hold) / fall, 0.3f, 4f);
WLJumpSlam.LastLog += " · 내림 속도 " + an.speed.ToString("0.00");
}
float y;
if (r.arc != 0) y = r.height * Mathf.Sin(Mathf.Clamp01(t / land) * Mathf.PI);
else if (t < rise) y = r.height * Mathf.Sin(t / rise * Mathf.PI * 0.5f);
else if (t < rise + hang) y = r.height;
else { float k = Mathf.Clamp01((t - rise - hang) / fall); y = r.height * (1f - k * k); }
var p = _pc.transform.position; p.y = _y0 + y;
if (r.forward != 0f) { var h = _start + _fwd * (r.forward * Mathf.Clamp01(t / land)); p.x = h.x; p.z = h.z; }
_pc.transform.position = p; if (y > _peak) { _peak = y; WLJumpSlam.LastPeak = y; }
if (!_landFx && t >= land)
{
_landFx = true; var g = new Vector3(p.x, _y0, p.z);
if (r.landFx != null && !string.IsNullOrEmpty(r.landFx.prefab) && WLStaffEarth.SpawnAt(r.landFx, _pc, g, _pc.transform.forward, 0f) != null) WLJumpSlam.LandFxShown++;
if (r.landTrace != null && WLStaffEarth.SpawnAt(r.landTrace, _pc, g, _pc.transform.forward, 0f) != null) WLJumpSlam.LandTraces++; // #1006
GlowFade(); // #1008 타격부터 흰 빛 꺼짐
}
if (t >= land + 0.05f) End(an);
}
/// <summary>#998 세로 회전 — 애니메이터가 이번 프레임 자세를 쓴 뒤 엉덩이 뼈(아래 무기 포함)를 몸 중심 · 오른쪽 축으로 돌린다(+ = 머리가 앞으로) · 바퀴마다 검기 1개.</summary>
void LateUpdate()
{
if (!_on || !_own || _r.spins <= 0 || _pc == null) return;
float dur = Mathf.Max(0.05f, _r.rise + _r.hang), t = Time.time - _t0; if (t > dur + 0.1f) return;
float ang = 360f * _r.spins * Mathf.SmoothStep(0f, 1f, Mathf.Clamp01(t / dur));
var root = _pc.transform; Vector3 pivot = root.TransformPoint(_pivotLocal);
if (_bone != null)
{
bool rewritten = !_set || Quaternion.Angle(_bone.rotation, _setRot) > 0.01f || (_bone.position - _setPos).sqrMagnitude > 1e-8f; // 애니메이터가 안 쓴 프레임이면 차이만
float d = rewritten ? ang : ang - _lastAng;
if (Mathf.Abs(d) > 0.001f) _bone.RotateAround(pivot, root.right, d);
_setRot = _bone.rotation; _setPos = _bone.position; _set = true; WLJumpSlam.LastSpinDeg = ang;
}
_lastAng = ang;
if (_spun < _r.spins && ang >= 360f * _spun && _r.spinFx != null && !string.IsNullOrEmpty(_r.spinFx.prefab))
{
_spun++;
var go = WLStaffEarth.SpawnAt(_r.spinFx, _pc, pivot, root.forward, 0f);
if (go != null) { go.transform.SetParent(root, true); WLJumpSlam.SpinFxShown++; }
}
}
/// <summary>#998 끊김(구르기 · 사망 · 상태 바뀜) 때 발 높이 · 조작을 되돌린다.</summary>
void Restore()
{
if (_pc == null) return;
var p = _pc.transform.position; p.y = _y0;
var agent = _pc.GetComponent<UnityEngine.AI.NavMeshAgent>(); UnityEngine.AI.NavMeshHit hit; // 앞으로 간 경우 에이전트 자리도 맞춘다(원본 Co_JumpAttack 끝의 Snap_toNavMesh 와 같게)
if (agent != null && agent.enabled && UnityEngine.AI.NavMesh.SamplePosition(p, out hit, 1f, UnityEngine.AI.NavMesh.AllAreas)) agent.Warp(hit.position); else _pc.transform.position = p;
WLJumpSlam.LastLog += " · 착지 이동 " + Vector3.Distance(new Vector3(_start.x, 0f, _start.z), new Vector3(_pc.transform.position.x, 0f, _pc.transform.position.z)).ToString("0.00") + " m";
if (OptionInfo.Ins != null) OptionInfo.Ins.Set_PCMove(_pc, true);
}
void End(Animator an)
{
if (_on && (_slammed || _own) && an != null) an.speed = 1f;
if (_on && _own) Restore();
GlowFade(); // #1008 끊겨도 흰 빛은 서서히
_on = false;
}
void OnDisable() { if (_on && _own) Restore(); _on = false; _glow.End(); _glowFadeAt = -1f; }
}
}