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

149 lines
10 KiB
C#
Raw 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.

// ─────────────────────────────────────────────────────────────────────────────
// 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; }
}
}