196 lines
11 KiB
C#
196 lines
11 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
|
// SightRangeGuard.cs — PC 시야(추적) 반경 제한 · 반경 밖 적 무시
|
|
//
|
|
// PD 지시 #813 추가 (2026-09-11 00:5x) · 발주서 WL-813t3 §1-1
|
|
// 「지금은 캐릭터의 시야 범위가 너무 넓어서 한참 동안 적을 추적하고 있어(보이지도 않는 위치까지 추적).
|
|
// 플레이어의 크기를 1 m 라고 가정할 때 반경 3 m 정도로 제한해줘.」
|
|
//
|
|
// ■ 왜 값 하나로는 안 되는가 (코드 실측)
|
|
// ① 「고르는 거리」 = `MyActor.Get_WLFindRange()`(MyActor.cs:21) → `WLTargetingSettings.RetargetSearchRange`.
|
|
// 0 이면 원본 `f_FindEnemyRange` = `table_GlobalValue.f_PC_FindEnemyRange`(**실측 6 m**) + `AttackRange_Up`.
|
|
// → 이 값을 3 으로 두면 **새로 고르는** 적은 3 m 안으로 제한된다.
|
|
// ② 그런데 **이미 잡은 타깃을 놓는 규칙이 원본에 없다.** `MyActor.Update`(:191~)는 타깃이 죽거나
|
|
// (펫 한정) 주인과 7 m 넘게 떨어질 때만 `Del_Target` 을 부르고, PC 는 그 외에는
|
|
// `Play_Run()` → `Set_Path(m_Target.Get_position())`(MyActor.cs:335)로 **거리 제한 없이 계속 쫓아간다**.
|
|
// → PD 가 본 「한참 동안 추적」이 이 경로다. 그래서 「놓는 거리」(chaseRange)가 따로 필요하다.
|
|
//
|
|
// ■ 이 파일이 하는 것 — 원본 훅 0 · 폴링만 (813x `MobNavRecovery` · 813t2 `TerrainGuard` 선례)
|
|
// 0.1 s 마다 PC 의 현재 타깃까지의 거리를 재서 `chaseRange` 를 넘으면 원본 `Actor.Del_Target`(Actor.cs:957)로
|
|
// 놓는다. 놓은 뒤 원본 `Check_Target`(MyActor.cs:357)이 같은 프레임에 다시 고르는데, 그때 쓰는 탐색 거리가
|
|
// ①(3 m)이므로 **3 m 밖 적은 다시 잡히지 않는다** = 「무시」.
|
|
//
|
|
// ■ 이동도 같이 멈춘다 (실측 근거)
|
|
// `Del_Target` 만으로는 **달리던 경로가 남는다** — `MyActor.Update` 의 타깃 없음 분기는
|
|
// `else if (CurAnim != eAnim.Run …) Play_Idle();`(MyActor.cs:258) 라서 이미 Run 이면 아무것도 하지 않고,
|
|
// `Play_Idle`(Actor.cs:1900)은 애니메이션만 바꾼다(에이전트 정지 아님).
|
|
// → 타깃을 놓는 순간 `Get_Agent().ResetPath()` + `Play_Idle()` 로 세운다(원본 공개 API 만).
|
|
// 🔴 단, **조이스틱 조작 중**(`PCActor.JoystickStatus > 0`)이거나 **813w 가이드가 이동을 쥐고 있을 때**
|
|
// (`RunGuide.Steering`)는 손대지 않는다 — 두 이동이 싸우면 존 이동이 끊긴다.
|
|
//
|
|
// ■ 락 타깃
|
|
// 813w `RunGuide` 는 보스에 원본 락(`Actor.m_IsLockTarget`)을 건다. 기본값(`chaseDropIgnoresLock = false`)은
|
|
// `Del_Target(false)` 를 쓰므로 **원본이 스스로 락을 지킨다**(Actor.cs:961). 락을 무시하려면 SO 로 켠다.
|
|
//
|
|
// ■ 값(C45) 전부 `WLTargetingSettings`. 게임 수치 상수 0. `chaseRange = 0` 이면 러너 자체가 뜨지 않는다(C8).
|
|
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것(Actor/PCActor 가 Assembly-CSharp 에 있다).
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
using UnityEngine;
|
|
|
|
namespace WL.Combat.Auto
|
|
{
|
|
/// <summary>PC 가 시야(추적) 반경 밖 적을 계속 쫓지 않게 타깃을 놓는다. 정적 · 폴링 · 원본 훅 0.</summary>
|
|
public static class SightRangeGuard
|
|
{
|
|
/// <summary>이 축이 살아 있는가. 반경 0 = 꺼짐(= 813t3 이전 동작 100 %).</summary>
|
|
public static bool Active
|
|
{
|
|
get { return !RuntimeDisabled && WL.Settings.WLTargetingSettings.ChaseRange > 0f; }
|
|
}
|
|
|
|
/// <summary>진단·A/B 전용 런타임 스위치. Play 종료 시 도메인 리로드로 자동 false.</summary>
|
|
public static bool RuntimeDisabled;
|
|
|
|
// ───────────────────────────────────────── 진단 카운터 (프로브가 읽는다)
|
|
public static int Ticks, Checks, Dropped, LockKept, MoveStopped, MoveSkipped;
|
|
public static float LastDistance;
|
|
public static string LastDroppedName = "";
|
|
public static string LastLog = "";
|
|
|
|
// ───────────────────────────────────────── 내부 상태 (GC 0 — 틱에서 새로 만들지 않는다)
|
|
static float s_nextCheck;
|
|
static bool s_faulted;
|
|
|
|
internal static void Tick()
|
|
{
|
|
if (s_faulted || !Active) return;
|
|
float now = Time.unscaledTime;
|
|
if (now < s_nextCheck) return;
|
|
s_nextCheck = now + Mathf.Max(0.02f, WL.Settings.WLTargetingSettings.ChaseCheckSeconds);
|
|
Ticks++;
|
|
|
|
// 🔴 Debug.LogException 을 쓰지 않는다 — ErrorLogHookManager 가 그것을 플레이어 모달로 띄운다(813x).
|
|
try { CheckOnce(); }
|
|
catch (System.Exception ex) { s_faulted = true; Log("중단(예외) — " + ex.Message); }
|
|
}
|
|
|
|
/// <summary>판정 결과 — 0 유지 · 1 놓는다 · 2 락 때문에 유지 · 3 축이 꺼져 있음.</summary>
|
|
public const int KeepTarget = 0, DropTarget = 1, KeepByLock = 2, KeepDisabled = 3;
|
|
|
|
/// <summary>
|
|
/// 놓을지 말지의 **순수 함수**(Actor 접근 0 · 프로브가 가짜 거리로 그대로 부른다).
|
|
/// range 이하면 유지 · 넘으면 놓는다. 단 락 타깃은 ignoreLock 이 아니면 유지한다.
|
|
/// </summary>
|
|
public static int Decide(float range, float distance, bool targetLocked, bool ignoreLock)
|
|
{
|
|
if (range <= 0f) return KeepDisabled;
|
|
if (distance <= range) return KeepTarget;
|
|
if (targetLocked && !ignoreLock) return KeepByLock;
|
|
return DropTarget;
|
|
}
|
|
|
|
/// <summary>1회 판정. 놓았으면 true.</summary>
|
|
public static bool CheckOnce()
|
|
{
|
|
float range = WL.Settings.WLTargetingSettings.ChaseRange;
|
|
if (range <= 0f) return false;
|
|
|
|
var pc = MyValue.MyPC;
|
|
if (DSUtil.CheckNull(pc) || pc.IsDead() || !pc.isTarget) return false;
|
|
|
|
var target = pc.Get_Target();
|
|
if (DSUtil.CheckNull(target))
|
|
{
|
|
// 참조 없이 플래그만 남은 상태 — Del_Target 은 여기서 m_Target 을 NRE 로 만진다(813w 선례).
|
|
pc.isTarget = false;
|
|
return false;
|
|
}
|
|
if (target.IsDead()) return false; // 죽은 타깃 교체는 원본(#770-2)이 한다
|
|
|
|
Checks++;
|
|
float d = Vector3.Distance(pc.Get_position(), target.Get_position());
|
|
LastDistance = d;
|
|
|
|
bool ignoreLock = WL.Settings.WLTargetingSettings.ChaseDropIgnoresLock;
|
|
int verdict = Decide(range, d, target.m_IsLockTarget, ignoreLock);
|
|
if (verdict == KeepByLock) { LockKept++; return false; }
|
|
if (verdict != DropTarget) return false;
|
|
|
|
string name = target.name;
|
|
pc.Del_Target(ignoreLock);
|
|
if (pc.isTarget) { LockKept++; return false; } // 원본이 락으로 거부했다
|
|
|
|
Dropped++;
|
|
LastDroppedName = name;
|
|
StopChase(pc);
|
|
Log("타깃 무시 #" + Dropped + " " + name + " " + d.ToString("F2") + " m > " + range.ToString("F2") + " m");
|
|
return true;
|
|
}
|
|
|
|
/// <summary>달려가던 경로를 끊고 세운다. 조이스틱·813w 가이드가 이동을 쥐고 있으면 손대지 않는다.</summary>
|
|
static void StopChase(PCActor pc)
|
|
{
|
|
if (!WL.Settings.WLTargetingSettings.StopChaseMoveOnDrop) { MoveSkipped++; return; }
|
|
if (pc.JoystickStatus > 0 || WL.Combat.Run.RunGuide.Steering) { MoveSkipped++; return; }
|
|
if (pc.Get_CurAnim() != eAnim.Run) { MoveSkipped++; return; } // 공격·스킬·회피 모션은 끊지 않는다
|
|
|
|
var agent = pc.Get_Agent();
|
|
if (agent != null && agent.enabled && agent.isOnNavMesh)
|
|
{
|
|
agent.ResetPath();
|
|
agent.velocity = Vector3.zero;
|
|
}
|
|
pc.Play_Idle();
|
|
MoveStopped++;
|
|
}
|
|
|
|
static void Log(string msg)
|
|
{
|
|
LastLog = msg;
|
|
if (WL.Settings.WLTargetingSettings.ChaseVerboseLog) Debug.Log("[SightRangeGuard] " + msg);
|
|
}
|
|
|
|
// ───────────────────────────────────────── 진단 · 초기화
|
|
/// <summary>프로브용: 카운터 초기화.</summary>
|
|
public static void ResetDiagnostics()
|
|
{
|
|
Ticks = Checks = Dropped = LockKept = MoveStopped = MoveSkipped = 0;
|
|
LastDistance = 0f; LastDroppedName = ""; LastLog = "";
|
|
s_nextCheck = 0f; s_faulted = false;
|
|
}
|
|
|
|
/// <summary>프로브용: 러너 없이 1회 틱(간격 무시).</summary>
|
|
public static void ForceTick() { s_nextCheck = 0f; Tick(); }
|
|
|
|
/// <summary>프로브용: 러너가 부르는 것과 똑같은 틱(타이머 초기화 0 — GC 측정용).</summary>
|
|
public static void ProbeTick() { Tick(); }
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
/// <summary>SightRangeGuard 의 시간 축. 숨김 GameObject 1개 · 코루틴 0.</summary>
|
|
internal sealed class SightRangeGuardRunner : MonoBehaviour
|
|
{
|
|
void Update() { SightRangeGuard.Tick(); }
|
|
}
|
|
|
|
/// <summary>러너 부팅(C8 — 반경이 0 이면 오브젝트 0).</summary>
|
|
public static class SightRangeGuardBoot
|
|
{
|
|
static SightRangeGuardRunner s_runner;
|
|
|
|
internal static void EnsureRunner()
|
|
{
|
|
if (!Application.isPlaying || !DSUtil.CheckNull(s_runner)) return;
|
|
var go = new GameObject("[WL813t3] SightRangeGuardRunner");
|
|
go.hideFlags = HideFlags.HideAndDontSave; // 씬 전환에도 살아남고 씬·프리팹에 저장되지 않는다
|
|
s_runner = go.AddComponent<SightRangeGuardRunner>();
|
|
}
|
|
|
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
|
static void Boot()
|
|
{
|
|
if (SightRangeGuard.Active) EnsureRunner();
|
|
}
|
|
}
|
|
}
|