// ───────────────────────────────────────────────────────────────────────────── // WLLaunch.cs — 맞은 적 띄우기 · 넉백 뒤 기절 (#902 · 2026-10-17) // // PD 원문: 「해머 3타에 맞으면 적이 뒤로 넉백되며 0.5초 스턴」 · 「(점프 내려치기) 충격 지점과 충격파에 피격 된 적을 공중에 1m가량 // 띄웠다가 떨어뜨린 후 스턴 효과를 1.5초 부여」 // ■ 띄우기 = 모델(애니메이터 트랜스폼)만 올렸다 내린다(사인 곡선) — 액터 루트 · NavMeshAgent 는 그대로라 지형 · 경로가 깨지지 않는다. // 그동안 원본 Stun(띄우는 시간 + 기절 시간, 별 표시)으로 행동을 묶는다. // ■ 넉백 = 원본 Actor.KnockBack(시간, 속도)(자기 뒤쪽으로 밀림) — 공격자를 보게 돌린 뒤 밀어 「공격자 반대 방향」이 되게 한다. 기절은 넉백 시간 뒤 원본 Stun. // 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것. // ───────────────────────────────────────────────────────────────────────────── using UnityEngine; namespace WL.Combat.Weapon { [DisallowMultipleComponent] public sealed class WLLaunch : MonoBehaviour { Actor _actor; UnityEngine.AI.NavMeshAgent _agent; float _base0, _t0, _dur, _h; bool _on; float _stunAt = -1f, _stunSec; // #906 직접 밀기(원본 KnockBack 의 반복 감소 · 거리 배율과 무관하게 정확한 거리) Vector3 _pushDir; float _pushSpeed, _pushUntil = -1f; bool _bounce; // #908 벽(이동 불가 경계)에 닿으면 반사해 남은 거리를 계속 날아감 public static int Launches, Knockbacks; /// 띄우기(height m · seconds 초) + 내려온 뒤 기절(stunAfter 초). 띄우는 동안도 기절로 묶는다. public static bool Apply(Actor victim, float height, float seconds, float stunAfter) { if (DSUtil.CheckNull(victim) || victim.IsDead()) return false; float air = height > 0f ? Mathf.Max(0.05f, seconds) : 0f; if (air + stunAfter > 0f) victim.Stun(air + Mathf.Max(0f, stunAfter), true); if (air <= 0f || victim.Get_Agent() == null) return true; var l = victim.GetComponent(); if (l == null) l = victim.gameObject.AddComponent(); l.Lift(victim, height, air); Launches++; return true; } /// 공격자 반대쪽으로 넉백(seconds × speed m) 뒤 stun 초 기절. 원본 KnockBack 이 거부해도(보스 · CC 중) 기절은 건다. public static void KnockbackStun(Actor victim, Actor attacker, float seconds, float speed, float stun) { if (DSUtil.CheckNull(victim) || victim.IsDead()) return; if (!DSUtil.CheckNull(attacker)) { var d = attacker.Get_position() - victim.Get_position(); d.y = 0f; if (d.sqrMagnitude > 1e-4f) victim.transform.rotation = Quaternion.LookRotation(d.normalized); } if (seconds > 0f) victim.KnockBack(seconds, speed); Knockbacks++; if (stun <= 0f) return; var l = victim.GetComponent(); if (l == null) l = victim.gameObject.AddComponent(); l._actor = victim; l._stunAt = Time.time + Mathf.Max(0f, EffectiveKnockbackSeconds(seconds)); l._stunSec = stun; } /// #906 PD 「해머 3타에 명중하면 적이 공중으로 살짝 뜨며 0.5m 3미터 정도 날아가도록」 — 공격자 반대쪽으로 distance m 를 seconds 초 동안 /// NavMeshAgent.Move 로 민다(지형 · NavMesh 를 넘지 않는다) · height m 포물선으로 띄움 · 착지 뒤 stun 초 기절. 밀리는 동안 경직(이동 · 공격 잠금)으로 붙잡는다. public static void Fling(Actor victim, Actor attacker, float distance, float seconds, float height, float stun) { Fling(victim, attacker, distance, seconds, height, stun, false); } /// #908 PD 「튕길 때 벽(이동 불가 영역)에 닿은 경우 벽에 튕겨서 날아가도록」 — bounce = NavMesh 경계에 닿으면 경계 법선으로 반사해 남은 거리를 계속 민다. public static void Fling(Actor victim, Actor attacker, float distance, float seconds, float height, float stun, bool bounce) { if (DSUtil.CheckNull(victim) || victim.IsDead() || victim.IsSubRole(eSubRol.Boss) || distance <= 0f || seconds <= 0f) return; var dir = !DSUtil.CheckNull(attacker) ? victim.Get_position() - attacker.Get_position() : -victim.transform.forward; dir.y = 0f; if (dir.sqrMagnitude < 1e-4f) dir = -victim.transform.forward; dir.Normalize(); victim.transform.rotation = Quaternion.LookRotation(-dir); var l = victim.GetComponent(); if (l == null) l = victim.gameObject.AddComponent(); l._actor = victim; l._pushDir = dir; l._pushSpeed = distance / seconds; l._pushUntil = Time.time + seconds; l._bounce = bounce; if (height > 0f) { l.Lift(victim, height, seconds); Launches++; } if (stun > 0f) { l._stunAt = Time.time + seconds; l._stunSec = stun; } WL.Combat.Reaction.MobStagger.Extend(victim, seconds); Flings++; } public static int Flings, Bounces; /// #906 기절 없이 모델만 띄웠다 내린다(넉백과 함께 — 포물선 비행). public static void LiftOnly(Actor victim, float height, float seconds) { if (DSUtil.CheckNull(victim) || victim.IsDead() || height <= 0f || seconds <= 0f || victim.Get_Agent() == null) return; var l = victim.GetComponent(); if (l == null) l = victim.gameObject.AddComponent(); l.Lift(victim, height, seconds); Launches++; } /// #906 원본 넉백 시간이 실제로 움직이는 시간 — 몹 넉백 거리 배율(WLHitFeelSettings.mobKnockbackDistanceScale · MobKnockbackScale)이 남은 시간을 줄인다. public static float EffectiveKnockbackSeconds(float seconds) { var st = WL.Feel.WLHitFeel.Settings; float k = st != null && st.mobKnockbackDistanceScale > 0f ? st.mobKnockbackDistanceScale : 1f; return seconds * k; } /// #908 이번 프레임 step m 밀기 — bounce 면 NavMesh 경계(벽)에 닿는 순간 법선으로 반사해 남은 만큼 반대쪽으로(한 프레임 최대 2 번). void Push(UnityEngine.AI.NavMeshAgent ag, float step) { if (!_bounce) { ag.Move(_pushDir * step); return; } for (int k = 0; k < 2 && step > 1e-4f; k++) { var from = ag.nextPosition; UnityEngine.AI.NavMeshHit hit; if (!UnityEngine.AI.NavMesh.Raycast(from, from + _pushDir * step, out hit, ag.areaMask)) { ag.Move(_pushDir * step); return; } float used = Mathf.Max(0f, hit.distance - 0.02f); if (used > 0f) ag.Move(_pushDir * used); var n = hit.normal; n.y = 0f; _pushDir = n.sqrMagnitude > 1e-4f ? Vector3.Reflect(_pushDir, n.normalized) : -_pushDir; _pushDir.y = 0f; _pushDir.Normalize(); step -= hit.distance; Bounces++; } } /// #906 NavMeshAgent.baseOffset(에이전트 위 몸 높이)을 올렸다 내린다 — 애니메이터가 몹 루트에 붙어 있어도(박쥐) 몸 전체가 뜬다 · 경로 · 지형은 그대로. void Lift(Actor a, float h, float dur) { // #911 공중에서 다시 맞으면(해머 각성 3연타) 바닥으로 되돌리지 않고 지금 높이에서 다시 h 까지 올렸다 내린다(전에는 순간 바닥으로 튀었다가 다시 떴다) float from = 0f; if (_on && _agent != null) { from = Mathf.Max(0f, (_agent.baseOffset - _base0) * Mathf.Max(Mathf.Abs(a.transform.lossyScale.y), 1e-3f)); } else { _actor = a; _agent = a.Get_Agent(); if (_agent == null) return; _base0 = _agent.baseOffset; } _from = from; _h = Mathf.Max(h, from); _dur = dur; _t0 = Time.time; _on = true; } float _from; void LateUpdate() { if (_pushUntil > 0f) { if (Time.time >= _pushUntil || DSUtil.CheckNull(_actor) || _actor.IsDead()) _pushUntil = -1f; else { var ag = _actor.Get_Agent(); if (ag != null && ag.enabled && ag.isOnNavMesh) Push(ag, _pushSpeed * Time.deltaTime); } } if (_stunAt >= 0f && Time.time >= _stunAt) { _stunAt = -1f; if (!DSUtil.CheckNull(_actor) && !_actor.IsDead()) _actor.Stun(_stunSec, true); } if (!_on) return; if (_agent == null || DSUtil.CheckNull(_actor) || _actor.IsDead()) { Restore(); return; } float t = (Time.time - _t0) / _dur; if (t >= 1f) { Restore(); return; } float sy = Mathf.Max(Mathf.Abs(_actor.transform.lossyScale.y), 1e-3f); // baseOffset = 로컬 단위 // 0 → 최고점(0.5) 구간은 _from(다시 맞은 높이)에서 _h 까지 · 최고점 → 끝은 _h 에서 0 까지(사인 반쪽씩) float y = t < 0.5f ? Mathf.Lerp(_from, _h, Mathf.Sin(t * Mathf.PI)) : _h * Mathf.Sin(t * Mathf.PI); _agent.baseOffset = _base0 + y / sy; } void Restore() { if (_on && _agent != null) _agent.baseOffset = _base0; _on = false; _from = 0f; } void OnDisable() { Restore(); _stunAt = -1f; _pushUntil = -1f; } } }