Project_WL/AgentScripts/WL813t3_Measure.cs

462 lines
29 KiB
C#

// 발주서 WL-813t3 — 실측 스크립트 (① 시야 경로 · ② 보스 스폰 지점 · ③ 언덕/계단)
//
// 실행 (CLAUDE.md §1: using 금지 · 네임스페이스 풀어 쓰기)
// 전(현행 NavMesh · 슬로프 45) : unity command --project-path E:\NerdNavis\WL_wt\data run_script --file AgentScripts/WL813t3_Measure.cs --entry WL813t3_Measure.Before --timeout 1800
// 후(재베이크 · 슬로프 30) : … --entry WL813t3_Measure.After --timeout 1800
//
// 🔴 임시 씬을 만들어 프리팹을 인스턴스화한다(저장 0). NavMesh 자산은 AddNavMeshData 로 잠깐 올렸다 내린다.
public static class WL813t3_Measure
{
const string kMapPrefab = "Assets/Res_Addr/Map/WL_Nature.prefab";
const string kNavAsset = "Assets/WL/Settings/WL_Nature_NavMesh.asset";
const string kOutDir = "AgentScripts/staging/WL813t3";
static System.Text.StringBuilder s;
static void L(string t) { s.AppendLine(t); UnityEngine.Debug.Log("[WL813t3] " + t); }
static string Abs(string rel)
{
return System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(),
rel.Replace('/', System.IO.Path.DirectorySeparatorChar));
}
static string Flush(string path)
{
try
{
System.IO.Directory.CreateDirectory(Abs(kOutDir));
System.IO.File.WriteAllText(Abs(path), s.ToString(), new System.Text.UTF8Encoding(true));
}
catch (System.Exception e) { UnityEngine.Debug.LogWarning("[WL813t3] 로그 저장 실패: " + e.Message); }
return s.ToString();
}
public static string Before() { return Run("전(슬로프 45 · 커밋본)", kOutDir + "/MEASURE_BEFORE.txt"); }
public static string After() { return Run("후(슬로프 30 · 재베이크)", kOutDir + "/MEASURE_AFTER.txt"); }
// ─────────────────────────────────────────────────────────────────────────
static string Run(string tag, string logPath)
{
s = new System.Text.StringBuilder();
L("===== WL-813t3 실측 " + tag + " · " + System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " =====");
Section1_Sight();
UnityEditor.AssetDatabase.ImportAsset(kNavAsset, UnityEditor.ImportAssetOptions.ForceUpdate);
var data = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.AI.NavMeshData>(kNavAsset);
if (data == null) { L("[에러] NavMesh 자산 로드 실패"); return Flush(logPath); }
long bytes = 0; try { bytes = new System.IO.FileInfo(Abs(kNavAsset)).Length; } catch { }
L("NavMesh 자산 " + kNavAsset + " · " + bytes + " bytes · GUID " + UnityEditor.AssetDatabase.AssetPathToGUID(kNavAsset));
// 베이크 설정(전역값) 실측
var bs = UnityEngine.AI.NavMesh.GetSettingsByID(0);
L("Agent0(Humanoid) 실측 — radius " + bs.agentRadius + " · height " + bs.agentHeight
+ " · **slope " + bs.agentSlope + "** · climb " + bs.agentClimb + " · minRegionArea " + bs.minRegionArea
+ " · voxel " + bs.voxelSize.ToString("F5"));
var prefab = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(kMapPrefab);
if (prefab == null) { L("[에러] 프리팹 로드 실패"); return Flush(logPath); }
var scene = UnityEditor.SceneManagement.EditorSceneManager.NewScene(
UnityEditor.SceneManagement.NewSceneSetup.EmptyScene, UnityEditor.SceneManagement.NewSceneMode.Single);
var go = UnityEditor.PrefabUtility.InstantiatePrefab(prefab) as UnityEngine.GameObject;
if (go == null) { L("[에러] 인스턴스화 실패"); return Flush(logPath); }
UnityEngine.Physics.SyncTransforms();
var inst = UnityEngine.AI.NavMesh.AddNavMeshData(data);
try
{
Section2_BossSpawn(go);
Section2b_FlatSearch();
Section3_Hill();
Section3b_Stairs(go);
Section3c_Connectivity(go);
}
finally
{
UnityEngine.AI.NavMesh.RemoveNavMeshData(inst);
UnityEngine.Object.DestroyImmediate(go);
}
L("씬 정리 완료(저장 0) · scene=" + scene.name);
return Flush(logPath);
}
// ─────────────────────────────────────────────────────────────────────────
// ① 시야 · 추적 경로 전수
// ─────────────────────────────────────────────────────────────────────────
static void Section1_Sight()
{
L("");
L("── ① 시야·추적 경로 (표 1) ──────────────────────────────────────────");
float tableRange = -1f;
try { tableRange = table_GlobalValue.Ins.Get_Float("f_PC_FindEnemyRange"); }
catch (System.Exception e) { L(" table_GlobalValue 미로드(" + e.GetType().Name + ") — JSON 직접 읽기로 대체"); }
if (tableRange < 0f) tableRange = ReadGlobalValueFromJson("f_PC_FindEnemyRange");
L(" 원본 탐색 거리 table_GlobalValue.f_PC_FindEnemyRange = " + tableRange.ToString("F2") + " m (+ AttackRange_Up)");
L(" 원본 펫 탐색 거리 f_Pet_FindEnemyRange = " + ReadGlobalValueFromJson("f_Pet_FindEnemyRange").ToString("F2") + " m (PC 무관)");
var t = UnityEngine.Resources.Load<WL.Settings.WLTargetingSettings>("WL/WLTargetingSettings");
if (t == null) L(" [에러] WLTargetingSettings 에셋 로드 실패");
else
L(" WLTargetingSettings 실값 — retargetSearchRange " + t.retargetSearchRange
+ " · chaseRange " + t.chaseRange + " · chaseDropIgnoresLock " + t.chaseDropIgnoresLock
+ " · stopChaseMoveOnDrop " + t.stopChaseMoveOnDrop + " · chaseCheckSeconds " + t.chaseCheckSeconds
+ " · frontConeDeg " + t.frontConeDeg + " · immediateAttackMargin " + t.immediateAttackMargin
+ " · autoEngage " + t.autoEngage + " · pcAggroOnHit " + t.pcAggroOnHit);
var g = UnityEngine.Resources.Load<WL.Combat.Run.WLRunGuideSettings>("WL/WLRunGuideSettings");
if (g == null) L(" [에러] WLRunGuideSettings 에셋 로드 실패");
else
L(" WLRunGuideSettings 실값 — bossLockTarget " + g.bossLockTarget + " · bossHoldRadius " + g.bossHoldRadius
+ " · bossPriorityEnabled " + g.bossPriorityEnabled + " · eliteLockTarget " + g.eliteLockTarget
+ " · eliteTargetRadius " + g.eliteTargetRadius + " · guideLeashRadius " + g.guideLeashRadius
+ " · zoneArriveRadius " + g.zoneArriveRadius + " · zoneHoldRadius " + g.zoneHoldRadius);
var b = UnityEngine.Resources.Load<WL.Combat.Boss.WLBossArenaSettings>("WL/WLBossArenaSettings");
if (b != null)
L(" WLBossArenaSettings 실값 — bossChaseRange " + b.bossChaseRange + "(MonsterList 10006 f_BaseChaseRange 를 등장 시 덮어쓴다) · arenaRadius " + b.arenaRadius
+ " · useFieldBossPositionAsCenter " + b.useFieldBossPositionAsCenter + " · arenaCenter " + b.arenaCenter.ToString("F2"));
L(" 스킬 자동 타깃 — Actor.cs:718 `if (!isTarget) Set_Target(true)` = forcefind → MyActor.cs:126 `float.MaxValue`(무제한).");
L(" · 자동 전투 경로는 여기 안 온다 — PCActor.cs:93 이 이미 `isTarget && dist <= f_SkillRange` 를 요구한다(실측).");
L(" · 수동 스킬 버튼만 이 경로다 → 「스킬 사거리 유지」(발주서 제안) = 무변경.");
L(" Actor.cs:187 `Get_EnemyActors(…, 100f)` — OnDestroy 에서 주변 적의 타깃 참조를 끊는 **정리 경로**(ForceDel). 시야와 무관(실측).");
L(" ProjectileBase.cs:348 `Get_NearstEnemy(20 m)` · :301 `Check_TargetBoss(8 m)` — 발사체가 표적을 잃었을 때의 보정. 시야와 무관(무변경).");
}
static float ReadGlobalValueFromJson(string id)
{
try
{
string txt = System.IO.File.ReadAllText(Abs("Assets/ResWork/Table/Export/GlobalValue.json"));
int i = txt.IndexOf("\"s_ID\": \"" + id + "\"");
if (i < 0) return -1f;
int j = txt.IndexOf("\"n_Value\": \"", i);
if (j < 0) return -1f;
j += 12;
int k = txt.IndexOf('"', j);
float v;
if (float.TryParse(txt.Substring(j, k - j), System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture, out v)) return v;
}
catch { }
return -1f;
}
// ─────────────────────────────────────────────────────────────────────────
// ② 보스 스폰 지점
// ─────────────────────────────────────────────────────────────────────────
static void Section2_BossSpawn(UnityEngine.GameObject root)
{
L("");
L("── ② 보스 스폰 지점 (표 2) ──────────────────────────────────────────");
var fbds = root.GetComponentsInChildren<FieldBossData>(true);
L(" FieldBossData " + fbds.Length + "기");
for (int i = 0; i < fbds.Length; i++)
{
var p = fbds[i].transform.position;
UnityEngine.AI.NavMeshHit h;
bool on = UnityEngine.AI.NavMesh.SamplePosition(p, out h, 2f, UnityEngine.AI.NavMesh.AllAreas);
L(" spawnerId " + fbds[i].n_SpawnerId + " monsterId " + fbds[i].n_MonsterID
+ " · 노드 " + p.ToString("F2") + " → " + (on ? "NavMesh " + h.position.ToString("F2") + " (Δ" + UnityEngine.Vector3.Distance(p, h.position).ToString("F2") + " m)" : "NavMesh 밖(2 m)"));
}
var cfg = UnityEngine.Resources.Load<WL.Combat.Boss.WLBossArenaSettings>("WL/WLBossArenaSettings");
UnityEngine.Vector3 arena = cfg != null ? cfg.arenaCenter : new UnityEngine.Vector3(139f, 0.8f, -20f);
L(" SO arenaCenter = " + arena.ToString("F2") + " · useFieldBossPositionAsCenter = " + (cfg != null ? cfg.useFieldBossPositionAsCenter.ToString() : "?"));
var tg = UnityEngine.Resources.Load<WL.Combat.Nav.WLTerrainGuardSettings>("WL/WLTerrainGuardSettings");
float cut = tg != null ? tg.maxWalkableY : 3.5f;
L(" 813t2 y 컷(maxWalkableY) = " + cut.ToString("F2") + " m");
Flatness("아레나 중심", arena, cut);
Flatness("보스 스포너 노드", fbds.Length > 0 ? fbds[0].transform.position : arena, cut);
// 「현재 언덕 좌표」 비교용 — 813t2 D-48 실측 좌표
L(" 참고(813t2 실측) D-48 현장 = PC 가 올라갔던 언덕 (139, 3.66, -20) · (139, 5.48, -20) · (128, 8, -58) · (147, 6, 30)");
var hillProbe = new UnityEngine.Vector3[] {
new UnityEngine.Vector3(139f, 3.66f, -20f), new UnityEngine.Vector3(139f, 5.48f, -20f),
new UnityEngine.Vector3(128f, 8f, -58f), new UnityEngine.Vector3(147f, 6f, 30f) };
for (int i = 0; i < hillProbe.Length; i++)
{
UnityEngine.AI.NavMeshHit h;
bool on = UnityEngine.AI.NavMesh.SamplePosition(hillProbe[i], out h, 1f, UnityEngine.AI.NavMesh.AllAreas);
L(" 언덕 " + hillProbe[i].ToString("F2") + " → " + (on ? "walkable " + h.position.ToString("F2") : "NavMesh 밖(1 m)"));
}
}
/// <summary>아레나 주변에서 「가장 평평하고 NavMesh 가 꽉 찬」 후보 지점을 찾는다(PD 「넓은 평지 중 한 장소」).</summary>
static void Section2b_FlatSearch()
{
L("");
L("── ②b 아레나 주변 평지 후보 탐색 (중심 139/-20 · ±20 m · 2 m 격자 · 각 후보 반경 6 m 평가) ──");
var best = new System.Collections.Generic.List<string>();
float bestScore = 1e9f; UnityEngine.Vector3 bestP = UnityEngine.Vector3.zero;
string bestLine = "없음";
int evaluated = 0;
for (int i = -10; i <= 10; i++)
for (int j = -10; j <= 10; j++)
{
var c = new UnityEngine.Vector3(139f + i * 2f, 0.8f, -20f + j * 2f);
UnityEngine.AI.NavMeshHit ch;
if (!UnityEngine.AI.NavMesh.SamplePosition(c, out ch, 2f, UnityEngine.AI.NavMesh.AllAreas)) continue;
evaluated++;
int n = 0, on = 0; float ymin = 1e9f, ymax = -1e9f, smax = 0f;
for (int a = -3; a <= 3; a++)
for (int b = -3; b <= 3; b++)
{
float dx = a * 2f, dz = b * 2f;
if (dx * dx + dz * dz > 36f) continue;
n++;
var p = new UnityEngine.Vector3(ch.position.x + dx, ch.position.y, ch.position.z + dz);
UnityEngine.RaycastHit rh;
if (UnityEngine.Physics.Raycast(new UnityEngine.Vector3(p.x, p.y + 60f, p.z), UnityEngine.Vector3.down, out rh, 200f))
{
if (rh.point.y < ymin) ymin = rh.point.y;
if (rh.point.y > ymax) ymax = rh.point.y;
float ang = UnityEngine.Vector3.Angle(rh.normal, UnityEngine.Vector3.up);
if (ang > smax) smax = ang;
}
UnityEngine.AI.NavMeshHit h2;
if (UnityEngine.AI.NavMesh.SamplePosition(p, out h2, 1.5f, UnityEngine.AI.NavMesh.AllAreas)) on++;
}
if (n == 0) continue;
float cover = (float)on / n;
float span = (ymax > -1e8f) ? (ymax - ymin) : 99f;
float score = span * 2f + smax * 0.1f + (1f - cover) * 20f;
string line = ch.position.ToString("F2") + " · 지면 Δy " + span.ToString("F2") + " m · 최대경사 " + smax.ToString("F1")
+ "° · NavMesh " + on + "/" + n + " · 점수 " + score.ToString("F2");
if (score < bestScore) { bestScore = score; bestP = ch.position; bestLine = line; }
if (UnityEngine.Mathf.Abs(i) <= 1 && UnityEngine.Mathf.Abs(j) <= 1) best.Add(" 중심 부근 " + line);
}
L(" 평가한 후보 " + evaluated + "개");
for (int i = 0; i < best.Count; i++) L(best[i]);
L(" **가장 평평한 후보 = " + bestLine + "** (아레나 중심에서 "
+ UnityEngine.Vector3.Distance(bestP, new UnityEngine.Vector3(139f, 0.8f, -20f)).ToString("F1") + " m)");
}
/// <summary>지점 주변 반경 r 의 평탄도 — 지면 raycast y 범위 · NavMesh 스냅 y 범위 · 경사각 분포.</summary>
static void Flatness(string label, UnityEngine.Vector3 center, float cut)
{
const float r = 6f;
int n = 0, onMesh = 0, aboveCut = 0, steep = 0, noGround = 0;
float gyMin = 1e9f, gyMax = -1e9f, myMin = 1e9f, myMax = -1e9f, slopeMax = 0f;
double slopeSum = 0.0;
for (int i = -6; i <= 6; i++)
for (int j = -6; j <= 6; j++)
{
float dx = i * (r / 6f), dz = j * (r / 6f);
if (dx * dx + dz * dz > r * r) continue;
n++;
var p = new UnityEngine.Vector3(center.x + dx, center.y, center.z + dz);
UnityEngine.RaycastHit rh;
if (UnityEngine.Physics.Raycast(new UnityEngine.Vector3(p.x, center.y + 60f, p.z), UnityEngine.Vector3.down, out rh, 200f))
{
if (rh.point.y < gyMin) gyMin = rh.point.y;
if (rh.point.y > gyMax) gyMax = rh.point.y;
float ang = UnityEngine.Vector3.Angle(rh.normal, UnityEngine.Vector3.up);
slopeSum += ang; if (ang > slopeMax) slopeMax = ang;
if (ang > 30f) steep++;
}
else noGround++;
UnityEngine.AI.NavMeshHit h;
if (UnityEngine.AI.NavMesh.SamplePosition(p, out h, 1.5f, UnityEngine.AI.NavMesh.AllAreas))
{
onMesh++;
if (h.position.y < myMin) myMin = h.position.y;
if (h.position.y > myMax) myMax = h.position.y;
if (h.position.y > cut) aboveCut++;
}
}
L(" [평탄도] " + label + " " + center.ToString("F2") + " · 반경 " + r + " m · 격자 " + n + "점");
L(" 지면 y " + (gyMin > 1e8f ? "없음" : gyMin.ToString("F2") + " ~ " + gyMax.ToString("F2") + " (Δ " + (gyMax - gyMin).ToString("F2") + " m)")
+ " · 경사 평균 " + (n > noGround ? (slopeSum / (n - noGround)).ToString("F1") : "?") + "° 최대 " + slopeMax.ToString("F1") + "° · 30° 초과 " + steep + "점 · 지면 없음 " + noGround);
L(" NavMesh 위 " + onMesh + "/" + n + " · y " + (myMin > 1e8f ? "없음" : myMin.ToString("F2") + " ~ " + myMax.ToString("F2")) + " · y 컷 초과 " + aboveCut);
}
// ─────────────────────────────────────────────────────────────────────────
// ③ 언덕 격자 · 옆면 경사
// ─────────────────────────────────────────────────────────────────────────
static void Section3_Hill()
{
L("");
L("── ③ 언덕 격자 1000점 (표 3 · 813t2 D-48 클러스터 X[96,160] Z[-100,-16]) ──");
// 32 x 32 = 1024 → 앞 1000점
int want = 1000, taken = 0;
int walkTotal = 0, walkAbove3 = 0, walkSteep = 0, steepTotal = 0, above3Total = 0, noGround = 0;
int band01 = 0, band13 = 0, band3p = 0;
float maxWalkY = -1e9f;
for (int i = 0; i < 32 && taken < want; i++)
for (int j = 0; j < 32 && taken < want; j++)
{
float x = 96f + (160f - 96f) * (i + 0.5f) / 32f;
float z = -100f + (-16f + 100f) * (j + 0.5f) / 32f;
taken++;
UnityEngine.RaycastHit rh;
if (!UnityEngine.Physics.Raycast(new UnityEngine.Vector3(x, 120f, z), UnityEngine.Vector3.down, out rh, 400f)) { noGround++; continue; }
float gy = rh.point.y;
float ang = UnityEngine.Vector3.Angle(rh.normal, UnityEngine.Vector3.up);
bool steep = ang > 30f;
bool above3 = gy > 3f;
if (steep) steepTotal++;
if (above3) above3Total++;
if (gy < 1f) band01++; else if (gy < 3f) band13++; else band3p++;
UnityEngine.AI.NavMeshHit h;
bool on = UnityEngine.AI.NavMesh.SamplePosition(rh.point, out h, 1f, UnityEngine.AI.NavMesh.AllAreas);
if (!on) continue;
walkTotal++;
if (h.position.y > maxWalkY) maxWalkY = h.position.y;
if (above3) walkAbove3++;
if (steep) walkSteep++;
}
L(" 격자 " + taken + "점 · 지면 없음 " + noGround);
L(" 지면 y 대역 — [0,1) " + band01 + " · [1,3) " + band13 + " · y>=3 " + band3p);
L(" 지면 경사 30° 초과 " + steepTotal + "점 · 지면 y>3 " + above3Total + "점");
L(" **walkable 스냅(1 m) " + walkTotal + "점** · 그 중 y>3 " + walkAbove3 + " · 경사 30° 초과 지점 " + walkSteep);
L(" walkable 최고 y = " + (maxWalkY > -1e8f ? maxWalkY.ToString("F2") : "없음"));
// 옆면(y 0~3) 전용 — 경사 30° 초과인 지점만 다시 본다
int sideN = 0, sideWalk = 0;
float sideMaxSlopeWalk = 0f;
for (int i = 0; i < 32; i++)
for (int j = 0; j < 32; j++)
{
float x = 96f + (160f - 96f) * (i + 0.5f) / 32f;
float z = -100f + (-16f + 100f) * (j + 0.5f) / 32f;
UnityEngine.RaycastHit rh;
if (!UnityEngine.Physics.Raycast(new UnityEngine.Vector3(x, 120f, z), UnityEngine.Vector3.down, out rh, 400f)) continue;
float gy = rh.point.y;
if (gy < 0f || gy > 3f) continue;
float ang = UnityEngine.Vector3.Angle(rh.normal, UnityEngine.Vector3.up);
if (ang <= 30f) continue;
sideN++;
UnityEngine.AI.NavMeshHit h;
if (UnityEngine.AI.NavMesh.SamplePosition(rh.point, out h, 1f, UnityEngine.AI.NavMesh.AllAreas))
{ sideWalk++; if (ang > sideMaxSlopeWalk) sideMaxSlopeWalk = ang; }
}
L(" **언덕 옆면(지면 y 0~3 · 경사 30° 초과) " + sideN + "점 · walkable " + sideWalk + "** · walkable 중 최대 경사 " + sideMaxSlopeWalk.ToString("F1") + "°");
}
// ─────────────────────────────────────────────────────────────────────────
// ③b 계단 · 경사로 메시 실측
// ─────────────────────────────────────────────────────────────────────────
static void Section3b_Stairs(UnityEngine.GameObject root)
{
L("");
L("── ③b 계단·경사로 (표 4) ────────────────────────────────────────────");
string[] keys = { "stair", "step", "ramp", "slope", "계단", "ladder", "bridge" };
var all = root.GetComponentsInChildren<UnityEngine.Transform>(true);
int found = 0;
for (int i = 0; i < all.Length; i++)
{
string n = all[i].name.ToLowerInvariant();
bool hit = false;
for (int k = 0; k < keys.Length; k++) if (n.Contains(keys[k])) { hit = true; break; }
if (!hit) continue;
var rend = all[i].GetComponentInChildren<UnityEngine.Renderer>(true);
var col = all[i].GetComponentInChildren<UnityEngine.Collider>(true);
var b = rend != null ? rend.bounds : (col != null ? col.bounds : new UnityEngine.Bounds(all[i].position, UnityEngine.Vector3.zero));
found++;
L(" [" + found + "] " + Path(all[i]) + " · pos " + all[i].position.ToString("F2")
+ " · bounds c" + b.center.ToString("F2") + " sz" + b.size.ToString("F2")
+ " · collider " + (col != null ? col.GetType().Name : "없음"));
StairPath(all[i].name, b);
}
if (found == 0) L(" 🔴 이름에 stair/step/ramp/계단 이 든 노드 **0개** — WL_Nature 에 명시적 계단 프리팹은 없다(실측).");
}
/// <summary>계단(또는 경사로) 바운즈의 아래쪽 끝 ↔ 위쪽 끝 사이 경로 연결성.</summary>
static void StairPath(string name, UnityEngine.Bounds b)
{
var lo = new UnityEngine.Vector3(b.center.x, b.min.y + 0.5f, b.center.z);
var hi = new UnityEngine.Vector3(b.center.x, b.max.y + 0.5f, b.center.z);
// 긴 축을 따라 아래/위 끝을 잡는다
if (b.size.x >= b.size.z)
{ lo.x = b.min.x + 0.5f; hi.x = b.max.x - 0.5f; }
else
{ lo.z = b.min.z + 0.5f; hi.z = b.max.z - 0.5f; }
UnityEngine.AI.NavMeshHit hl, hh;
bool onl = UnityEngine.AI.NavMesh.SamplePosition(lo, out hl, 3f, UnityEngine.AI.NavMesh.AllAreas);
bool onh = UnityEngine.AI.NavMesh.SamplePosition(hi, out hh, 3f, UnityEngine.AI.NavMesh.AllAreas);
if (!onl || !onh) { L(" 상↔하 경로 = 판정 불가(하 " + (onl ? "on" : "밖") + " · 상 " + (onh ? "on" : "밖") + ")"); return; }
var path = new UnityEngine.AI.NavMeshPath();
UnityEngine.AI.NavMesh.CalculatePath(hl.position, hh.position, UnityEngine.AI.NavMesh.AllAreas, path);
L(" 상↔하 경로 " + hl.position.ToString("F2") + " → " + hh.position.ToString("F2") + " = **" + path.status + "** · corners " + path.corners.Length);
}
// ─────────────────────────────────────────────────────────────────────────
// ③c 경로 연결성 (시작 → 스포너 23 · 아레나)
// ─────────────────────────────────────────────────────────────────────────
static void Section3c_Connectivity(UnityEngine.GameObject root)
{
L("");
L("── ③c 경로 연결성 ───────────────────────────────────────────────────");
var portal = FindByName(root.transform, "Portal_Start");
UnityEngine.Vector3 startPos = portal != null ? portal.position : new UnityEngine.Vector3(115f, 0f, 10f);
UnityEngine.AI.NavMeshHit sh;
if (!UnityEngine.AI.NavMesh.SamplePosition(startPos, out sh, 5f, UnityEngine.AI.NavMesh.AllAreas))
{ L(" [에러] 시작 지점이 NavMesh 밖 " + startPos.ToString("F2")); return; }
L(" 시작 " + startPos.ToString("F2") + " → 스냅 " + sh.position.ToString("F2"));
var mgrs = root.GetComponentsInChildren<MobControlMgr>(true);
int comp = 0, part = 0, none = 0;
for (int i = 0; i < mgrs.Length; i++)
{
var p = mgrs[i].transform.position;
UnityEngine.AI.NavMeshHit h;
if (!UnityEngine.AI.NavMesh.SamplePosition(p, out h, 5f, UnityEngine.AI.NavMesh.AllAreas))
{ none++; L(" #" + mgrs[i].SpawnerID + " NavMesh 밖 " + p.ToString("F2")); continue; }
var path = new UnityEngine.AI.NavMeshPath();
UnityEngine.AI.NavMesh.CalculatePath(sh.position, h.position, UnityEngine.AI.NavMesh.AllAreas, path);
if (path.status == UnityEngine.AI.NavMeshPathStatus.PathComplete) comp++;
else { part++; L(" #" + mgrs[i].SpawnerID + " " + path.status + " " + p.ToString("F2")); }
}
L(" **시작 → 스포너 " + mgrs.Length + "기 : PathComplete " + comp + " · Partial/Invalid " + part + " · NavMesh 밖 " + none + "**");
var cfg = UnityEngine.Resources.Load<WL.Combat.Boss.WLBossArenaSettings>("WL/WLBossArenaSettings");
UnityEngine.Vector3 arena = cfg != null ? cfg.arenaCenter : new UnityEngine.Vector3(139f, 0.8f, -20f);
UnityEngine.AI.NavMeshHit ah;
if (UnityEngine.AI.NavMesh.SamplePosition(arena, out ah, 5f, UnityEngine.AI.NavMesh.AllAreas))
{
var p2 = new UnityEngine.AI.NavMeshPath();
UnityEngine.AI.NavMesh.CalculatePath(sh.position, ah.position, UnityEngine.AI.NavMesh.AllAreas, p2);
L(" 시작 → 아레나 " + arena.ToString("F2") + " 스냅 " + ah.position.ToString("F2") + " = **" + p2.status + "**");
int okr = 0;
for (int i = 0; i < 12; i++)
{
float ang = i * UnityEngine.Mathf.PI * 2f / 12f;
var rp = arena + new UnityEngine.Vector3(UnityEngine.Mathf.Cos(ang), 0f, UnityEngine.Mathf.Sin(ang)) * (cfg != null ? cfg.arenaRadius * 0.8f : 16f);
UnityEngine.AI.NavMeshHit rh2;
if (!UnityEngine.AI.NavMesh.SamplePosition(rp, out rh2, 5f, UnityEngine.AI.NavMesh.AllAreas)) continue;
var p3 = new UnityEngine.AI.NavMeshPath();
UnityEngine.AI.NavMesh.CalculatePath(rh2.position, ah.position, UnityEngine.AI.NavMesh.AllAreas, p3);
if (p3.status == UnityEngine.AI.NavMeshPathStatus.PathComplete) okr++;
}
L(" 아레나 원주 12점 → 중심 PathComplete " + okr + "/12");
}
else L(" 아레나 " + arena.ToString("F2") + " NavMesh 밖(5 m)");
}
static string Path(UnityEngine.Transform t)
{
string p = t.name;
while (t.parent != null) { t = t.parent; p = t.name + "/" + p; }
return p;
}
static UnityEngine.Transform FindByName(UnityEngine.Transform t, string name)
{
if (t.name == name) return t;
for (int i = 0; i < t.childCount; i++)
{
var r = FindByName(t.GetChild(i), name);
if (r != null) return r;
}
return null;
}
}