149 lines
10 KiB
C#
149 lines
10 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 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;
|
||
|
||
/// <summary>띄우기(height m · seconds 초) + 내려온 뒤 기절(stunAfter 초). 띄우는 동안도 기절로 묶는다.</summary>
|
||
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<WLLaunch>(); if (l == null) l = victim.gameObject.AddComponent<WLLaunch>();
|
||
l.Lift(victim, height, air);
|
||
Launches++;
|
||
return true;
|
||
}
|
||
|
||
/// <summary>공격자 반대쪽으로 넉백(seconds × speed m) 뒤 stun 초 기절. 원본 KnockBack 이 거부해도(보스 · CC 중) 기절은 건다.</summary>
|
||
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<WLLaunch>(); if (l == null) l = victim.gameObject.AddComponent<WLLaunch>();
|
||
l._actor = victim; l._stunAt = Time.time + Mathf.Max(0f, EffectiveKnockbackSeconds(seconds)); l._stunSec = stun;
|
||
}
|
||
|
||
/// <summary>#906 PD 「해머 3타에 명중하면 적이 공중으로 살짝 뜨며 0.5m 3미터 정도 날아가도록」 — 공격자 반대쪽으로 distance m 를 seconds 초 동안
|
||
/// NavMeshAgent.Move 로 민다(지형 · NavMesh 를 넘지 않는다) · height m 포물선으로 띄움 · 착지 뒤 stun 초 기절. 밀리는 동안 경직(이동 · 공격 잠금)으로 붙잡는다.</summary>
|
||
public static void Fling(Actor victim, Actor attacker, float distance, float seconds, float height, float stun) { Fling(victim, attacker, distance, seconds, height, stun, false); }
|
||
|
||
/// <summary>#908 PD 「튕길 때 벽(이동 불가 영역)에 닿은 경우 벽에 튕겨서 날아가도록」 — bounce = NavMesh 경계에 닿으면 경계 법선으로 반사해 남은 거리를 계속 민다.</summary>
|
||
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<WLLaunch>(); if (l == null) l = victim.gameObject.AddComponent<WLLaunch>();
|
||
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;
|
||
|
||
/// <summary>#906 기절 없이 모델만 띄웠다 내린다(넉백과 함께 — 포물선 비행).</summary>
|
||
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<WLLaunch>(); if (l == null) l = victim.gameObject.AddComponent<WLLaunch>();
|
||
l.Lift(victim, height, seconds); Launches++;
|
||
}
|
||
|
||
/// <summary>#906 원본 넉백 시간이 실제로 움직이는 시간 — 몹 넉백 거리 배율(WLHitFeelSettings.mobKnockbackDistanceScale · MobKnockbackScale)이 남은 시간을 줄인다.</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>#908 이번 프레임 step m 밀기 — bounce 면 NavMesh 경계(벽)에 닿는 순간 법선으로 반사해 남은 만큼 반대쪽으로(한 프레임 최대 2 번).</summary>
|
||
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++;
|
||
}
|
||
}
|
||
|
||
/// <summary>#906 NavMeshAgent.baseOffset(에이전트 위 몸 높이)을 올렸다 내린다 — 애니메이터가 몹 루트에 붙어 있어도(박쥐) 몸 전체가 뜬다 · 경로 · 지형은 그대로.</summary>
|
||
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; }
|
||
}
|
||
}
|