Project_WL/Assets/WL/Combat/Reaction/MobKnockbackScale.cs

184 lines
9.7 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.

// ─────────────────────────────────────────────────────────────────────────────
// 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 이하 = 기능 끔(원본 그대로).
// ■ 진단: 종류별 배율 적용 수 · 실제 이동 거리(넉백 직전 위치 → 끝 위치 수평 거리) 합·최근값. 원래 거리는 같은 세션에서 배율 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;
}
/// <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;
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;
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;
if (t.cc.TryGetValue(eCC.KnockBack, out d))
{
Step(t, d, ref t.lastBack, ref t.movingBack, ref t.startBack, ref t.stillBack, tr, 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, 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;
}
}
}