189 lines
10 KiB
C#
189 lines
10 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// MobKnockbackScale.cs — 몹 넉백 이동 거리 배율 · 원본 넉백 코루틴 0줄 · 코루틴 0 · GC 0(추적 목록은 풀 크기 상한)
|
||
//
|
||
// 2026-09-18 PD 「몬스터 넉백 거리가 너무 멀어. 우선은 절반 정도로 줄이고,
|
||
// 추후에는 캐릭터 테이블에서 공격 타수 별 넉백 거리를 조절할 수 있도록 설계해줘.」
|
||
//
|
||
// ■ 원본 넉백 2종(Actor.cs)
|
||
// KnockBack(time, speed 4) → Co_KnockBack : eCC.KnockBack 의 CCTime 이 0 이 될 때까지 매 프레임 뒤로 speed × dt
|
||
// Knockback_byHitter(pos, time, 10) → Co_Knockback_byHitter : eCC.KnockBack_byHitter 의 CCTime 동안 때린 쪽 반대로 speed × dt
|
||
// 둘 다 「남은 시간 × 속도」만큼 움직이므로, 새 넉백이 시작된 직후 남은 시간을 줄이면 총 거리가 같은 비율로 준다.
|
||
// ■ 검출 = 피격(HitConfirmed)된 몹만 추적 · 매 틱 CCTime 이 직전 값보다 커지면 「새 넉백 시작」.
|
||
// 그 사이 원본이 이미 한 프레임(dt) 밀었으므로 남은 시간 = (현재 + dt) × 배율 − dt → 총 거리 = 원래 × 배율.
|
||
// ■ 값 = WLHitFeelSettings.mobKnockbackDistanceScale(0.5) · mobKnockbackScaleByHitter · 0 이하 = 기능 끔(원본 그대로).
|
||
// #915 스킬별 배율 = WLSkillHitFeel(그 스킬 피격이 건 넉백은 knockbackScale 로 · 몹 넉백 배율 대신).
|
||
// ■ 진단: 종류별 배율 적용 수 · 실제 이동 거리(넉백 직전 위치 → 끝 위치 수평 거리) 합·최근값. 원래 거리는 같은 세션에서 배율 1 로 재서 비교한다.
|
||
// 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다(813z 규칙).
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
using System.Collections.Generic;
|
||
using System.Reflection;
|
||
using UnityEngine;
|
||
using WL.Combat.Core;
|
||
|
||
namespace WL.Combat.Reaction
|
||
{
|
||
public static class MobKnockbackScale
|
||
{
|
||
sealed class Track
|
||
{
|
||
public Actor actor;
|
||
public Dictionary<eCC, CCData> cc;
|
||
public double lastBack, lastHitter;
|
||
public bool movingBack, movingHitter;
|
||
public int stillBack, stillHitter;
|
||
public Vector3 restPos, startBack, startHitter;
|
||
public float lastActiveAt;
|
||
public float ovScale = -1f, ovUntil; // #915 이 피격으로 시작될 넉백 = 스킬별 배율(WLSkillHitFeel) · ovUntil 까지
|
||
}
|
||
|
||
/// <summary>넉백 값이 이 틱 수 동안(시간이 흐르는 프레임 기준) 전혀 줄지 않으면 원본 코루틴이 없는 것 = 몹이 영영 묶인다 → 0 으로 풀어 준다.</summary>
|
||
const int StuckTicks = 10;
|
||
/// <summary>배율 계산에 쓰는 프레임 시간 상한(판 재시작·로딩 순간의 긴 프레임이 계산을 음수로 만들지 않게).</summary>
|
||
const float MaxStepSeconds = 0.05f;
|
||
|
||
static readonly Dictionary<Actor, Track> s_tracks = new Dictionary<Actor, Track>(64);
|
||
static readonly List<Track> s_list = new List<Track>(64);
|
||
static FieldInfo s_ccField;
|
||
static bool s_ccLooked;
|
||
|
||
// ── 진단(프로브가 읽는다)
|
||
public static bool Subscribed;
|
||
public static int ScaledBack, ScaledHitter, EndedBack, EndedHitter, StuckCleared, SkillScaled;
|
||
public static float SumMovedBack, SumMovedHitter, LastMovedBack, LastMovedHitter, LastScale = -1f;
|
||
public static int TrackCount { get { return s_list.Count; } }
|
||
|
||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)]
|
||
static void Register()
|
||
{
|
||
CombatEvents.HitConfirmed.Add(OnHitConfirmed);
|
||
Subscribed = true;
|
||
}
|
||
|
||
static WLHitFeelSettings St { get { return WL.Feel.WLHitFeel.Settings; } }
|
||
|
||
public static bool Enabled
|
||
{
|
||
get { var st = St; return st != null && st.enabled && st.mobKnockbackDistanceScale > 0f && !WL.Feel.WLHitFeel.RuntimeDisabled; }
|
||
}
|
||
|
||
static void OnHitConfirmed(in HitConfirmedEvent e)
|
||
{
|
||
if (!Enabled) return;
|
||
var victim = e.victim;
|
||
if (victim == null || !(victim is MobActor) || victim.IsDead()) return;
|
||
Track t;
|
||
if (!s_tracks.TryGetValue(victim, out t))
|
||
{
|
||
if (!s_ccLooked)
|
||
{
|
||
s_ccLooked = true;
|
||
s_ccField = typeof(Actor).GetField("dic_ccData", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||
}
|
||
var dict = s_ccField != null ? s_ccField.GetValue(victim) as Dictionary<eCC, CCData> : null;
|
||
if (dict == null) return;
|
||
// 직전 값은 0(넉백 아님)으로 시작한다 — 이 피격이 방금 건 넉백도 다음 틱에 「새 넉백」으로 잡힌다
|
||
t = new Track { actor = victim, cc = dict, restPos = victim.transform.position };
|
||
s_tracks.Add(victim, t);
|
||
s_list.Add(t);
|
||
}
|
||
t.lastActiveAt = Time.unscaledTime;
|
||
var sr = WLSkillHitFeel.For(e); // #915 검 각성 등 — 이 피격이 건 넉백은 스킬 배율로(몹 넉백 배율 대신)
|
||
if (sr != null) { t.ovScale = Mathf.Max(0f, sr.knockbackScale); t.ovUntil = Time.unscaledTime + 0.2f; SkillScaled++; }
|
||
WLReactionRunner.Ensure();
|
||
}
|
||
|
||
internal static void Tick(float now)
|
||
{
|
||
if (s_list.Count == 0) return;
|
||
var st = St;
|
||
bool on = Enabled;
|
||
float scale = on ? Mathf.Clamp01(st.mobKnockbackDistanceScale) : 1f;
|
||
bool byHitter = on && st.mobKnockbackScaleByHitter;
|
||
float dt = Time.deltaTime;
|
||
LastScale = scale;
|
||
|
||
for (int i = s_list.Count - 1; i >= 0; i--)
|
||
{
|
||
var t = s_list[i];
|
||
var a = t.actor;
|
||
if (a == null || !a.isActiveAndEnabled || a.IsDead() || (now - t.lastActiveAt > 5f && !t.movingBack && !t.movingHitter))
|
||
{
|
||
if (!ReferenceEquals(a, null)) s_tracks.Remove(a);
|
||
s_list.RemoveAt(i);
|
||
continue;
|
||
}
|
||
var tr = a.transform;
|
||
CCData d;
|
||
bool idle = true;
|
||
bool ov = t.ovScale >= 0f && now <= t.ovUntil; // #915 스킬별 넉백 배율
|
||
if (t.cc.TryGetValue(eCC.KnockBack, out d))
|
||
{
|
||
Step(t, d, ref t.lastBack, ref t.movingBack, ref t.startBack, ref t.stillBack, tr, ov ? t.ovScale : scale, dt, ref ScaledBack, ref EndedBack, ref SumMovedBack, ref LastMovedBack);
|
||
if (t.movingBack) idle = false;
|
||
}
|
||
if (t.cc.TryGetValue(eCC.KnockBack_byHitter, out d))
|
||
{
|
||
Step(t, d, ref t.lastHitter, ref t.movingHitter, ref t.startHitter, ref t.stillHitter, tr, ov ? t.ovScale : (byHitter ? scale : 1f), dt, ref ScaledHitter, ref EndedHitter, ref SumMovedHitter, ref LastMovedHitter);
|
||
if (t.movingHitter) idle = false;
|
||
}
|
||
if (idle) t.restPos = tr.position; // 넉백 직전 위치(다음 넉백의 시작점)
|
||
else t.lastActiveAt = now;
|
||
}
|
||
}
|
||
|
||
static void Step(Track t, CCData d, ref double last, ref bool moving, ref Vector3 start, ref int still, Transform tr, float scale, float dt,
|
||
ref int scaledCount, ref int endedCount, ref float sumMoved, ref float lastMoved)
|
||
{
|
||
double cc = d.CCTime;
|
||
if (cc > last + 0.0001)
|
||
{
|
||
// 새 넉백 시작(또는 진행 중 재시작) — 진행 중이었다면 지금까지 움직인 거리를 먼저 마감
|
||
if (moving) { lastMoved = Flat(start, tr.position); sumMoved += lastMoved; endedCount++; start = tr.position; }
|
||
else start = t.restPos;
|
||
moving = true;
|
||
still = 0;
|
||
// 🔴 직전 틱에 넉백이 아니었을 때만(= 원본 코루틴이 방금 시작) 줄인다. 계산이 0 이하면 0 — 양수 최솟값을 남기면
|
||
// 코루틴이 없는 경우(판 재시작으로 끊긴 넉백 등) 그 값이 영영 안 줄어 몹이 넉백 상태에 묶였다(2026-09-18 실측 · 0.0001 고정).
|
||
if (scale < 0.999f && last <= 0.0)
|
||
{
|
||
float step = Mathf.Min(dt, MaxStepSeconds);
|
||
double adj = (cc + step) * scale - step;
|
||
d.CCTime = adj > 0.0 ? adj : 0.0;
|
||
cc = d.CCTime;
|
||
scaledCount++;
|
||
}
|
||
}
|
||
else if (cc > 0.0)
|
||
{
|
||
// 줄지 않는 넉백 = 원본 코루틴이 없다(끊긴 채 값만 남음) → 몹이 영영 묶이지 않게 풀어 준다(시간이 멈춘 히트스톱 프레임은 세지 않는다)
|
||
if (dt > 0f && System.Math.Abs(cc - last) < 1e-9)
|
||
{
|
||
if (++still >= StuckTicks) { d.CCTime = 0.0; cc = 0.0; still = 0; StuckCleared++; }
|
||
}
|
||
else still = 0;
|
||
}
|
||
if (moving && cc <= 0.0)
|
||
{
|
||
lastMoved = Flat(start, tr.position);
|
||
sumMoved += lastMoved;
|
||
endedCount++;
|
||
moving = false;
|
||
}
|
||
last = cc;
|
||
}
|
||
|
||
static float Flat(Vector3 a, Vector3 b) { a.y = 0f; b.y = 0f; return Vector3.Distance(a, b); }
|
||
|
||
/// <summary>씬 전환: 추적 목록(파괴된 Actor 참조)을 버린다.</summary>
|
||
public static void ClearAll() { s_tracks.Clear(); s_list.Clear(); }
|
||
|
||
/// <summary>프로브용 카운터 초기화.</summary>
|
||
public static void ResetDiagnostics()
|
||
{
|
||
ScaledBack = ScaledHitter = EndedBack = EndedHitter = StuckCleared = 0;
|
||
SumMovedBack = SumMovedHitter = LastMovedBack = LastMovedHitter = 0f;
|
||
}
|
||
}
|
||
}
|