383 lines
22 KiB
C#
383 lines
22 KiB
C#
|
|
// PD 지시 #764 — 몬스터 스폰/이동 지형 실측 프로브 (읽기 전용)
|
|||
|
|
//
|
|||
|
|
// 실행 (CLAUDE.md §1: using 금지 · 네임스페이스 풀어 쓰기)
|
|||
|
|
// 에디트 모드 스폰 도달성 + NavMesh 섬 통계
|
|||
|
|
// unity command run_script --file AgentScripts/WL764_Probe.cs --entry WL764_Probe.Edit --timeout 300
|
|||
|
|
// Play 중 몹 이탈 감시 시작 / 상태 / 결과
|
|||
|
|
// unity command run_script --file AgentScripts/WL764_Probe.cs --entry WL764_Probe.Watch
|
|||
|
|
// unity command run_script --file AgentScripts/WL764_Probe.cs --entry WL764_Probe.Status
|
|||
|
|
// unity command run_script --file AgentScripts/WL764_Probe.cs --entry WL764_Probe.Dump
|
|||
|
|
//
|
|||
|
|
// 수치는 전부 프로젝트 데이터에서 읽는다 (C45):
|
|||
|
|
// 스포너 위치·MobGenRange = WL_Start.prefab / 플레이어 시작 지점 = MapData.m_MapZoneData[].go_portal (+Vector3.forward)
|
|||
|
|
// NavMesh = Assets/WL/Settings/WL_Start_NavMesh.asset
|
|||
|
|
// 프로브 자체의 격자 간격·표집 수는 "측정 해상도"이지 게임 데이터가 아니다.
|
|||
|
|
|
|||
|
|
public static class WL764_Probe
|
|||
|
|
{
|
|||
|
|
const string kTargetPrefab = "Assets/Res_Addr/Map/WL_Start.prefab";
|
|||
|
|
const string kNavAsset = "Assets/WL/Settings/WL_Start_NavMesh.asset";
|
|||
|
|
const string kEditLog = "AgentScripts/staging/WL_Spawn/EDIT_PROBE.txt";
|
|||
|
|
const string kRunLog = "AgentScripts/staging/WL_Spawn/RUNTIME_PROBE.txt";
|
|||
|
|
|
|||
|
|
// 측정 해상도 (게임 데이터 아님)
|
|||
|
|
const float kGridStep = 4f; // 섬 통계 격자
|
|||
|
|
const int kSamplesPerSpawner = 30;
|
|||
|
|
const int kSeed = 764;
|
|||
|
|
const float kOffMeshRadius = 0.3f; // 발주서 지정 — 이 반경 안에 NavMesh 가 없으면 "이탈"
|
|||
|
|
|
|||
|
|
static System.Text.StringBuilder s;
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────────────────
|
|||
|
|
// 1. 에디트 모드: 스포너별 도달률 + 섬 통계
|
|||
|
|
// ─────────────────────────────────────────────────────────────────────
|
|||
|
|
public static string Edit()
|
|||
|
|
{
|
|||
|
|
s = new System.Text.StringBuilder();
|
|||
|
|
L("===== WL764 에디트 모드 실측 =====");
|
|||
|
|
L("playMode=" + UnityEngine.Application.isPlaying);
|
|||
|
|
|
|||
|
|
var navData = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.AI.NavMeshData>(kNavAsset);
|
|||
|
|
if (navData == null) { L("[에러] NavMeshData 없음: " + kNavAsset); return Flush(kEditLog); }
|
|||
|
|
|
|||
|
|
var root = UnityEditor.PrefabUtility.LoadPrefabContents(kTargetPrefab);
|
|||
|
|
var navInst = UnityEngine.AI.NavMesh.AddNavMeshData(navData);
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// 플레이어 시작 지점 — MapData 의 포탈 (런타임 LoadMapMgr.Get_PortalPos 와 동일 식)
|
|||
|
|
var md = root.GetComponent<MapData>();
|
|||
|
|
UnityEngine.Vector3 pcSpawn;
|
|||
|
|
if (md != null && md.m_MapZoneData != null && md.m_MapZoneData.Count > 0 && md.m_MapZoneData[0].go_portal != null)
|
|||
|
|
{
|
|||
|
|
pcSpawn = md.m_MapZoneData[0].go_portal.transform.position + UnityEngine.Vector3.forward;
|
|||
|
|
L("플레이어 시작 = MapData.m_MapZoneData[0].go_portal(" + md.m_MapZoneData[0].go_portal.name + ") + forward = " + pcSpawn.ToString("F2"));
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
var p = root.transform.Find("Portal_Start");
|
|||
|
|
if (p == null) { L("[에러] 포탈을 찾지 못했다"); return Flush(kEditLog); }
|
|||
|
|
pcSpawn = p.position + UnityEngine.Vector3.forward;
|
|||
|
|
L("플레이어 시작 = Portal_Start + forward = " + pcSpawn.ToString("F2") + " (MapData 경로 실패 → 폴백)");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
UnityEngine.AI.NavMeshHit pcHit;
|
|||
|
|
if (!UnityEngine.AI.NavMesh.SamplePosition(pcSpawn, out pcHit, 5f, UnityEngine.AI.NavMesh.AllAreas))
|
|||
|
|
{ L("[에러] 시작 지점이 NavMesh 위가 아니다"); return Flush(kEditLog); }
|
|||
|
|
var pcOnNav = pcHit.position;
|
|||
|
|
L("시작 지점 NavMesh 스냅 = " + pcOnNav.ToString("F2"));
|
|||
|
|
|
|||
|
|
// ── (d) 섬 통계 ──────────────────────────────────────────────
|
|||
|
|
IslandStats(pcOnNav);
|
|||
|
|
|
|||
|
|
// ── (a) 스포너별 도달률 ──────────────────────────────────────
|
|||
|
|
var mgrs = root.GetComponentsInChildren<MobControlMgr>(true);
|
|||
|
|
L("");
|
|||
|
|
L("── (a) 스포너 " + mgrs.Length + "개 · 스포너당 " + kSamplesPerSpawner + "점 표집 ──");
|
|||
|
|
L(" ID | 스포너위치 | range | 스포너도달 | 현행표집 도달 | 신방식 도달 | 표집반경초과");
|
|||
|
|
System.Array.Sort(mgrs, delegate (MobControlMgr a, MobControlMgr b) { return a.SpawnerID.CompareTo(b.SpawnerID); });
|
|||
|
|
|
|||
|
|
int totBadOld = 0, totBadNew = 0, badSpawners = 0, overRange = 0, totFallback = 0; float sumSpread = 0f;
|
|||
|
|
for (int i = 0; i < mgrs.Length; i++)
|
|||
|
|
{
|
|||
|
|
var m = mgrs[i];
|
|||
|
|
var org = m.transform.position;
|
|||
|
|
bool spawnerReach = Reachable(pcOnNav, org);
|
|||
|
|
if (!spawnerReach) badSpawners++;
|
|||
|
|
|
|||
|
|
UnityEngine.Random.InitState(kSeed + m.SpawnerID);
|
|||
|
|
int okOld = 0, over = 0;
|
|||
|
|
for (int k = 0; k < kSamplesPerSpawner; k++)
|
|||
|
|
{
|
|||
|
|
var p = DSUtil.Get_RandomPos_onNavMesh(org, 0f, m.MobGenRange);
|
|||
|
|
if (Reachable(pcOnNav, p)) okOld++;
|
|||
|
|
if (UnityEngine.Vector3.Distance(org, p) > m.MobGenRange + 0.01f) over++;
|
|||
|
|
}
|
|||
|
|
overRange += over;
|
|||
|
|
|
|||
|
|
UnityEngine.Random.InitState(kSeed + m.SpawnerID);
|
|||
|
|
int okNew = 0, fallback = 0; float sumD = 0f;
|
|||
|
|
for (int k = 0; k < kSamplesPerSpawner; k++)
|
|||
|
|
{
|
|||
|
|
s_fellBack = false;
|
|||
|
|
var p = SampleStrict(org, m.MobGenRange, pcOnNav);
|
|||
|
|
if (s_fellBack) fallback++;
|
|||
|
|
sumD += UnityEngine.Vector3.Distance(org, p);
|
|||
|
|
if (Reachable(pcOnNav, p)) okNew++;
|
|||
|
|
}
|
|||
|
|
totFallback += fallback; sumSpread += sumD / kSamplesPerSpawner;
|
|||
|
|
|
|||
|
|
totBadOld += (kSamplesPerSpawner - okOld);
|
|||
|
|
totBadNew += (kSamplesPerSpawner - okNew);
|
|||
|
|
L(string.Format(" {0,2} | ({1,8:F1},{2,6:F1},{3,8:F1}) | {4,5:F1} | {5,-6} | {6,3}/{7} ({8,3}%) | {9,3}/{10} ({11,3}%) | {12,2}",
|
|||
|
|
m.SpawnerID, org.x, org.y, org.z, m.MobGenRange, spawnerReach ? "OK" : "🔴불가",
|
|||
|
|
okOld, kSamplesPerSpawner, okOld * 100 / kSamplesPerSpawner,
|
|||
|
|
okNew, kSamplesPerSpawner, okNew * 100 / kSamplesPerSpawner, over));
|
|||
|
|
}
|
|||
|
|
L("");
|
|||
|
|
L("합계: 스포너 원점 도달불가 " + badSpawners + "/" + mgrs.Length
|
|||
|
|
+ " · 현행 표집 도달불가 " + totBadOld + "/" + (mgrs.Length * kSamplesPerSpawner)
|
|||
|
|
+ " (" + Pct(totBadOld, mgrs.Length * kSamplesPerSpawner) + "%)"
|
|||
|
|
+ " · 신방식 도달불가 " + totBadNew + "/" + (mgrs.Length * kSamplesPerSpawner)
|
|||
|
|
+ " (" + Pct(totBadNew, mgrs.Length * kSamplesPerSpawner) + "%)");
|
|||
|
|
L("표집점이 MobGenRange 를 벗어난 횟수(현행) = " + overRange
|
|||
|
|
+ " — SamplePosition 의 스냅 반경이 MobGenRange 라 최대 2×range 까지 튄다");
|
|||
|
|
L("신방식 산포: 원점 폴백 " + totFallback + "/" + (mgrs.Length * kSamplesPerSpawner)
|
|||
|
|
+ " · 스포너별 평균 이격 " + (sumSpread / mgrs.Length).ToString("F2") + "m (MobGenRange 10 기준 · 몹이 원점에 뭉치지 않는지 확인)");
|
|||
|
|
}
|
|||
|
|
finally
|
|||
|
|
{
|
|||
|
|
UnityEngine.AI.NavMesh.RemoveNavMeshData(navInst);
|
|||
|
|
UnityEditor.PrefabUtility.UnloadPrefabContents(root);
|
|||
|
|
}
|
|||
|
|
return Flush(kEditLog);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static void IslandStats(UnityEngine.Vector3 pcOnNav)
|
|||
|
|
{
|
|||
|
|
var tri = UnityEngine.AI.NavMesh.CalculateTriangulation();
|
|||
|
|
if (tri.vertices.Length == 0) { L("[에러] NavMesh 가 비어 있다"); return; }
|
|||
|
|
var min = tri.vertices[0]; var max = tri.vertices[0];
|
|||
|
|
for (int i = 1; i < tri.vertices.Length; i++)
|
|||
|
|
{ min = UnityEngine.Vector3.Min(min, tri.vertices[i]); max = UnityEngine.Vector3.Max(max, tri.vertices[i]); }
|
|||
|
|
|
|||
|
|
int nGrid = 0, nOnNav = 0, nReach = 0;
|
|||
|
|
for (float x = min.x; x <= max.x; x += kGridStep)
|
|||
|
|
for (float z = min.z; z <= max.z; z += kGridStep)
|
|||
|
|
{
|
|||
|
|
nGrid++;
|
|||
|
|
UnityEngine.AI.NavMeshHit h;
|
|||
|
|
var probe = new UnityEngine.Vector3(x, (min.y + max.y) * 0.5f, z);
|
|||
|
|
if (!UnityEngine.AI.NavMesh.SamplePosition(probe, out h, (max.y - min.y) * 0.5f + kGridStep, UnityEngine.AI.NavMesh.AllAreas)) continue;
|
|||
|
|
if (UnityEngine.Mathf.Abs(h.position.x - x) > kGridStep || UnityEngine.Mathf.Abs(h.position.z - z) > kGridStep) continue;
|
|||
|
|
nOnNav++;
|
|||
|
|
if (Reachable(pcOnNav, h.position)) nReach++;
|
|||
|
|
}
|
|||
|
|
L("");
|
|||
|
|
L("── (d) NavMesh 섬 통계 (" + kGridStep + "m 격자) ──");
|
|||
|
|
L(" 삼각형 " + (tri.indices.Length / 3) + "개 · 범위 min=" + min.ToString("F0") + " max=" + max.ToString("F0"));
|
|||
|
|
L(" 격자 " + nGrid + "점 → NavMesh 위 " + nOnNav + "점 → 시작 지점에서 도달 가능 " + nReach
|
|||
|
|
+ "점 (" + Pct(nReach, nOnNav) + "%) · 도달 불가 " + (nOnNav - nReach) + "점 (" + Pct(nOnNav - nReach, nOnNav) + "%)");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static bool s_fellBack;
|
|||
|
|
|
|||
|
|
/// <summary>신방식 표집 — 스냅 반경을 좁히고 완전 경로(PathComplete)를 요구한다. MobControlMgr 에 넣을 로직과 동일.</summary>
|
|||
|
|
public static UnityEngine.Vector3 SampleStrict(UnityEngine.Vector3 origin, float range, UnityEngine.Vector3 reachFrom)
|
|||
|
|
{
|
|||
|
|
float snap = range * 0.2f; // Edit 프로브 기본 — 실제 값은 MapData.spawnSnapRadius
|
|||
|
|
for (int i = 0; i < 50; i++)
|
|||
|
|
{
|
|||
|
|
var dir = UnityEngine.Random.insideUnitSphere * range + origin;
|
|||
|
|
UnityEngine.AI.NavMeshHit hit;
|
|||
|
|
if (!UnityEngine.AI.NavMesh.SamplePosition(dir, out hit, snap, UnityEngine.AI.NavMesh.AllAreas)) continue;
|
|||
|
|
if (UnityEngine.Vector3.Distance(origin, hit.position) > range) continue;
|
|||
|
|
if (!Reachable(origin, hit.position)) continue;
|
|||
|
|
if (!Reachable(reachFrom, hit.position)) continue;
|
|||
|
|
return hit.position;
|
|||
|
|
}
|
|||
|
|
s_fellBack = true;
|
|||
|
|
return origin;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static bool Reachable(UnityEngine.Vector3 from, UnityEngine.Vector3 to)
|
|||
|
|
{
|
|||
|
|
var path = new UnityEngine.AI.NavMeshPath();
|
|||
|
|
if (!UnityEngine.AI.NavMesh.CalculatePath(from, to, UnityEngine.AI.NavMesh.AllAreas, path)) return false;
|
|||
|
|
return path.status == UnityEngine.AI.NavMeshPathStatus.PathComplete;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────────────────
|
|||
|
|
// 2. Play 모드: 몹 이탈 감시
|
|||
|
|
// ─────────────────────────────────────────────────────────────────────
|
|||
|
|
static bool s_watching;
|
|||
|
|
static float s_next, s_t0, s_dur;
|
|||
|
|
static int s_ticks;
|
|||
|
|
static System.Text.StringBuilder s_run;
|
|||
|
|
static System.Collections.Generic.Dictionary<int, int> s_offCount; // instanceID → 이탈 틱 수
|
|||
|
|
static System.Collections.Generic.Dictionary<int, string> s_offName;
|
|||
|
|
static int s_peakMobs, s_offEvents;
|
|||
|
|
|
|||
|
|
const float kWatchSeconds = 60f; // 발주서 지정 감시 시간(측정 조건)
|
|||
|
|
|
|||
|
|
public static string Watch()
|
|||
|
|
{
|
|||
|
|
float seconds = kWatchSeconds;
|
|||
|
|
if (!UnityEngine.Application.isPlaying) return "[에러] Play 중이 아니다";
|
|||
|
|
if (s_watching) return "이미 감시 중 — 남은 " + (s_dur - (UnityEngine.Time.realtimeSinceStartup - s_t0)).ToString("F0") + "s";
|
|||
|
|
s_run = new System.Text.StringBuilder();
|
|||
|
|
s_offCount = new System.Collections.Generic.Dictionary<int, int>();
|
|||
|
|
s_offName = new System.Collections.Generic.Dictionary<int, string>();
|
|||
|
|
s_peakMobs = 0; s_offEvents = 0; s_ticks = 0;
|
|||
|
|
s_t0 = UnityEngine.Time.realtimeSinceStartup; s_dur = seconds; s_next = 0f;
|
|||
|
|
s_watching = true;
|
|||
|
|
UnityEditor.EditorApplication.update -= Tick;
|
|||
|
|
UnityEditor.EditorApplication.update += Tick;
|
|||
|
|
R("===== 런타임 이탈 감시 시작 (" + seconds + "s · 1s 간격 · SamplePosition 반경 " + kOffMeshRadius + ") =====");
|
|||
|
|
ReachSweep("시작");
|
|||
|
|
return "감시 시작 " + seconds + "s";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static void Tick()
|
|||
|
|
{
|
|||
|
|
if (!s_watching) { UnityEditor.EditorApplication.update -= Tick; return; }
|
|||
|
|
if (!UnityEngine.Application.isPlaying) { R("[중단] Play 가 멈췄다"); StopWatch(); return; }
|
|||
|
|
float el = UnityEngine.Time.realtimeSinceStartup - s_t0;
|
|||
|
|
if (el < s_next) return;
|
|||
|
|
s_next = el + 1f;
|
|||
|
|
s_ticks++;
|
|||
|
|
|
|||
|
|
var mobs = UnityEngine.Object.FindObjectsByType<MobActor>(UnityEngine.FindObjectsInactive.Exclude, UnityEngine.FindObjectsSortMode.None);
|
|||
|
|
if (mobs.Length > s_peakMobs) s_peakMobs = mobs.Length;
|
|||
|
|
int off = 0;
|
|||
|
|
for (int i = 0; i < mobs.Length; i++)
|
|||
|
|
{
|
|||
|
|
var m = mobs[i];
|
|||
|
|
var p = m.transform.position;
|
|||
|
|
UnityEngine.AI.NavMeshHit h;
|
|||
|
|
if (UnityEngine.AI.NavMesh.SamplePosition(p, out h, kOffMeshRadius, UnityEngine.AI.NavMesh.AllAreas)) continue;
|
|||
|
|
off++; s_offEvents++;
|
|||
|
|
int id = m.GetInstanceID();
|
|||
|
|
if (!s_offCount.ContainsKey(id)) { s_offCount[id] = 0; s_offName[id] = m.name; }
|
|||
|
|
s_offCount[id]++;
|
|||
|
|
if (s_offCount[id] <= 3) // 몹당 3회까지만 상세 기록
|
|||
|
|
{
|
|||
|
|
var ag = m.GetComponent<UnityEngine.AI.NavMeshAgent>();
|
|||
|
|
R(string.Format(" [이탈] t={0,5:F1}s {1} pos=({2:F1},{3:F1},{4:F1}) agent.enabled={5} isOnNavMesh={6} CC={7}",
|
|||
|
|
el, m.name, p.x, p.y, p.z,
|
|||
|
|
ag == null ? "null" : ag.enabled.ToString(),
|
|||
|
|
ag == null ? "null" : (ag.enabled ? ag.isOnNavMesh.ToString() : "-"),
|
|||
|
|
m.IsCC()));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (s_ticks % 10 == 0) R(" t=" + el.ToString("F0") + "s 활성몹=" + mobs.Length + " 이탈=" + off);
|
|||
|
|
if (el >= s_dur) { ReachSweep("종료"); StopWatch(); }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static void ReachSweep(string tag)
|
|||
|
|
{
|
|||
|
|
var pc = MyValue.MyPC;
|
|||
|
|
if (pc == null) { R("[" + tag + "] PC 없음 — 도달성 스윕 생략"); return; }
|
|||
|
|
var pcPos = pc.transform.position;
|
|||
|
|
UnityEngine.AI.NavMeshHit ph;
|
|||
|
|
if (!UnityEngine.AI.NavMesh.SamplePosition(pcPos, out ph, 3f, UnityEngine.AI.NavMesh.AllAreas))
|
|||
|
|
{ R("[" + tag + "] PC 가 NavMesh 위가 아니다 pos=" + pcPos.ToString("F1")); return; }
|
|||
|
|
|
|||
|
|
var mobs = UnityEngine.Object.FindObjectsByType<MobActor>(UnityEngine.FindObjectsInactive.Exclude, UnityEngine.FindObjectsSortMode.None);
|
|||
|
|
int bad = 0; var names = new System.Text.StringBuilder();
|
|||
|
|
for (int i = 0; i < mobs.Length; i++)
|
|||
|
|
{
|
|||
|
|
if (Reachable(ph.position, mobs[i].transform.position)) continue;
|
|||
|
|
bad++;
|
|||
|
|
if (bad <= 15) { if (names.Length > 0) names.Append(", "); names.Append(mobs[i].name + mobs[i].transform.position.ToString("F0")); }
|
|||
|
|
}
|
|||
|
|
R("[" + tag + "] PC=" + ph.position.ToString("F1") + " 활성몹 " + mobs.Length
|
|||
|
|
+ " 중 도달 불가 " + bad + " (" + Pct(bad, mobs.Length) + "%)" + (bad > 0 ? " 예: " + names : ""));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static void StopWatch()
|
|||
|
|
{
|
|||
|
|
s_watching = false;
|
|||
|
|
UnityEditor.EditorApplication.update -= Tick;
|
|||
|
|
R("");
|
|||
|
|
R("===== 감시 종료 · 틱 " + s_ticks + " · 최대 활성몹 " + s_peakMobs
|
|||
|
|
+ " · 이탈 이벤트 " + s_offEvents + " · 이탈 몹 " + (s_offCount == null ? 0 : s_offCount.Count) + "종 =====");
|
|||
|
|
if (s_offCount != null)
|
|||
|
|
foreach (var kv in s_offCount) R(" 이탈몹 " + s_offName[kv.Key] + " ×" + kv.Value + "틱");
|
|||
|
|
WriteFile(kRunLog, s_run.ToString());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static string Stop() { if (s_watching) StopWatch(); return "중지"; }
|
|||
|
|
|
|||
|
|
/// <summary>즉시 1회 측정 — 활성 몹 전원의 (a) PC 로부터의 도달성 (b) NavMesh 이탈 여부.</summary>
|
|||
|
|
public static string Sweep()
|
|||
|
|
{
|
|||
|
|
if (!UnityEngine.Application.isPlaying) return "[에러] Play 중이 아니다";
|
|||
|
|
var pc = MyValue.MyPC;
|
|||
|
|
if (pc == null) return "[에러] PC 없음";
|
|||
|
|
UnityEngine.AI.NavMeshHit ph;
|
|||
|
|
if (!UnityEngine.AI.NavMesh.SamplePosition(pc.transform.position, out ph, 3f, UnityEngine.AI.NavMesh.AllAreas))
|
|||
|
|
return "[에러] PC 가 NavMesh 위가 아니다 " + pc.transform.position.ToString("F1");
|
|||
|
|
|
|||
|
|
var mobs = UnityEngine.Object.FindObjectsByType<MobActor>(UnityEngine.FindObjectsInactive.Exclude, UnityEngine.FindObjectsSortMode.None);
|
|||
|
|
int unreach = 0, off = 0;
|
|||
|
|
var bad = new System.Text.StringBuilder();
|
|||
|
|
for (int i = 0; i < mobs.Length; i++)
|
|||
|
|
{
|
|||
|
|
var p = mobs[i].transform.position;
|
|||
|
|
UnityEngine.AI.NavMeshHit h;
|
|||
|
|
if (!UnityEngine.AI.NavMesh.SamplePosition(p, out h, kOffMeshRadius, UnityEngine.AI.NavMesh.AllAreas)) off++;
|
|||
|
|
if (Reachable(ph.position, p)) continue;
|
|||
|
|
unreach++;
|
|||
|
|
if (unreach <= 12) { if (bad.Length > 0) bad.Append(", "); bad.Append(mobs[i].name + p.ToString("F0")); }
|
|||
|
|
}
|
|||
|
|
return "활성몹 " + mobs.Length + " · PC(" + ph.position.ToString("F1") + ") 에서 도달 불가 " + unreach
|
|||
|
|
+ " (" + Pct(unreach, mobs.Length) + "%) · NavMesh 이탈(" + kOffMeshRadius + "m) " + off
|
|||
|
|
+ (unreach > 0 ? " · 예: " + bad : "");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static string Status()
|
|||
|
|
{
|
|||
|
|
if (!s_watching) return "감시 중 아님 (틱 " + s_ticks + " · 이탈 " + s_offEvents + ")";
|
|||
|
|
float el = UnityEngine.Time.realtimeSinceStartup - s_t0;
|
|||
|
|
var mobs = UnityEngine.Object.FindObjectsByType<MobActor>(UnityEngine.FindObjectsInactive.Exclude, UnityEngine.FindObjectsSortMode.None);
|
|||
|
|
return "감시 중 " + el.ToString("F0") + "/" + s_dur + "s · 틱 " + s_ticks + " · 현재 활성몹 " + mobs.Length
|
|||
|
|
+ " · 누적 이탈 " + s_offEvents + " (" + (s_offCount == null ? 0 : s_offCount.Count) + "종)";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static string Dump()
|
|||
|
|
{
|
|||
|
|
if (s_run == null) return "기록 없음";
|
|||
|
|
WriteFile(kRunLog, s_run.ToString());
|
|||
|
|
return s_run.ToString();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>#763 펫 판정 — 활성 PetActor 수와 펫 카드 상태.</summary>
|
|||
|
|
public static string PetCheck()
|
|||
|
|
{
|
|||
|
|
var sb = new System.Text.StringBuilder();
|
|||
|
|
var all = UnityEngine.Object.FindObjectsByType<PetActor>(UnityEngine.FindObjectsInactive.Include, UnityEngine.FindObjectsSortMode.None);
|
|||
|
|
var act = UnityEngine.Object.FindObjectsByType<PetActor>(UnityEngine.FindObjectsInactive.Exclude, UnityEngine.FindObjectsSortMode.None);
|
|||
|
|
sb.Append("PetActor 전체=" + all.Length + " 활성=" + act.Length);
|
|||
|
|
for (int i = 0; i < all.Length; i++) sb.Append(" | " + all[i].name + " active=" + all[i].gameObject.activeInHierarchy);
|
|||
|
|
var pets = ActorInfo.Ins != null ? ActorInfo.Ins.Get_Actors(eRole.Pet, 0, true) : null;
|
|||
|
|
sb.Append(" || ActorInfo eRole.Pet(includeDie) = " + (pets == null ? "n/a" : pets.Count.ToString()));
|
|||
|
|
// 계정에 실제로 펫이 장착돼 있는지 — 장착 0 이면 이 검증은 무의미하다
|
|||
|
|
if (ServerInfo.Ins != null && ServerInfo.Ins.m_ServerData != null)
|
|||
|
|
{
|
|||
|
|
int equipped = ServerInfo.Ins.m_ServerData.Get_EqiupmentEquip(eItem.Pet);
|
|||
|
|
sb.Append(" || 장착 펫ID = " + equipped + (equipped > 0 ? " (장착됨 — 차단이 실제로 동작해야 함)" : " (미장착 🔴 검증 무의미)"));
|
|||
|
|
}
|
|||
|
|
// 펫 카드 UI 상태
|
|||
|
|
if (NewGameUI.isIns && NewGameUI.Ins.arr_PetCard != null && NewGameUI.Ins.arr_PetCard.Length > 0)
|
|||
|
|
{
|
|||
|
|
var card = NewGameUI.Ins.arr_PetCard[0];
|
|||
|
|
sb.Append(" || arr_PetCard[0] = " + (card == null ? "null" : card.name + " active=" + card.gameObject.activeInHierarchy));
|
|||
|
|
}
|
|||
|
|
else sb.Append(" || NewGameUI/arr_PetCard 없음");
|
|||
|
|
sb.Append(" || MyPC.Get_MyPet() = " + (MyValue.MyPC == null ? "PC없음" : (MyValue.MyPC.Get_MyPet() == null ? "null(OK)" : MyValue.MyPC.Get_MyPet().name)));
|
|||
|
|
// 설정 에셋은 리플렉션으로 읽는다 — 이 프로브가 WLGameplaySettings 존재 여부와 무관하게 컴파일되도록.
|
|||
|
|
var t = System.Type.GetType("WL.Settings.WLGameplaySettings, Assembly-CSharp");
|
|||
|
|
if (t == null) sb.Append(" || WLGameplaySettings 타입 없음");
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
var inst = t.GetProperty("Instance", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static).GetValue(null, null);
|
|||
|
|
if (inst == null) sb.Append(" || WLGameplaySettings 에셋 없음");
|
|||
|
|
else sb.Append(" || WLGameplaySettings.spawnPlayerPet=" + t.GetField("spawnPlayerPet").GetValue(inst));
|
|||
|
|
}
|
|||
|
|
return sb.ToString();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────────────────
|
|||
|
|
static string Pct(int a, int b) { return b == 0 ? "0" : (a * 100 / b).ToString(); }
|
|||
|
|
static void L(string t) { s.Append(t); s.Append("\n"); UnityEngine.Debug.Log("[WL764] " + t); }
|
|||
|
|
static void R(string t) { s_run.Append(t); s_run.Append("\n"); }
|
|||
|
|
static string Flush(string path) { WriteFile(path, s.ToString()); return s.ToString(); }
|
|||
|
|
static void WriteFile(string path, string body)
|
|||
|
|
{
|
|||
|
|
var full = System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), path);
|
|||
|
|
var dir = System.IO.Path.GetDirectoryName(full);
|
|||
|
|
if (!System.IO.Directory.Exists(dir)) System.IO.Directory.CreateDirectory(dir);
|
|||
|
|
System.IO.File.WriteAllText(full, body, new System.Text.UTF8Encoding(true));
|
|||
|
|
}
|
|||
|
|
}
|