// 발주서 WL-813t4 — 보스 아레나 평지 후보 실측 (에디트 모드 · Play 0) // // 실행 (CLAUDE.md §1: using 금지 · 네임스페이스 풀어 쓰기) // unity command --project-path E:\NerdNavis\WL_wt\systems run_script --file AgentScripts/WL813t4_Measure.cs --entry WL813t4_Measure.Arena --timeout 1800 // // 평가 항목(발주서 §1-1) // ① 반경 20 m 평탄도 — 지면 Δy · 최대경사 · 30° 초과 점수 // ② NavMesh 커버(격자 ≥ 300점 중 몇 점이 NavMesh 위인가) // ③ 연결성 — Portal_Start → 중심 PathComplete · 원주 12점 → 중심 ≥ 10/12 · 존1~4 스포너 → 중심 // ④ 813t2 y 컷 — 반경 20 m NavMesh 최고 y ≤ 2.96 // ⑤ 반경 20 m 안 물 렌더러 · 큰 바위 수 // ⑥ 스테이지 배치 산포(황금각) 슬롯이 전부 NavMesh 로 해결되는가 // // 🔴 임시 씬을 만들어 프리팹을 인스턴스화한다(저장 0). NavMesh 자산은 AddNavMeshData 로 잠깐 올렸다 내린다. public static class WL813t4_Measure { const string kMapPrefab = "Assets/Res_Addr/Map/WL_Nature.prefab"; const string kNavAsset = "Assets/WL/Settings/WL_Nature_NavMesh.asset"; const string kOut = "AgentScripts/WL813t4_MEASURE.txt"; const float kRadius = 20f; // 아레나 반경 const float kStep = 2f; // 격자 간격 → 반경 20 m 안 317점 const float kYCut = 2.96f; // 813t2 · 813t3 실측 walkable 최고 y static System.Text.StringBuilder s; static void L(string t) { s.AppendLine(t); UnityEngine.Debug.Log("[WL813t4] " + t); } static string Abs(string rel) { return System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), rel.Replace('/', System.IO.Path.DirectorySeparatorChar)); } static string Flush() { try { System.IO.File.WriteAllText(Abs(kOut), s.ToString(), new System.Text.UTF8Encoding(true)); } catch (System.Exception e) { UnityEngine.Debug.LogWarning("[WL813t4] 로그 저장 실패: " + e.Message); } return s.ToString(); } // ───────────────────────────────────────────────────────────────────────── public static string Arena() { s = new System.Text.StringBuilder(); s_rows.Clear(); L("===== WL-813t4 아레나 평지 실측 · " + System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " ====="); UnityEditor.AssetDatabase.ImportAsset(kNavAsset, UnityEditor.ImportAssetOptions.ForceUpdate); var data = UnityEditor.AssetDatabase.LoadAssetAtPath(kNavAsset); if (data == null) { L("[에러] NavMesh 자산 로드 실패"); return Flush(); } long bytes = 0; try { bytes = new System.IO.FileInfo(Abs(kNavAsset)).Length; } catch { } var bs = UnityEngine.AI.NavMesh.GetSettingsByID(0); L("NavMesh " + bytes + " B · Agent0 slope " + bs.agentSlope + " · climb " + bs.agentClimb + " · voxel " + bs.voxelSize.ToString("F5")); var cfg = UnityEngine.Resources.Load("WL/WLBossArenaSettings"); var tbl = UnityEngine.Resources.Load("WL/WLStageTable"); var tg = UnityEngine.Resources.Load("WL/WLTerrainGuardSettings"); L("현재 SO — arenaCenter " + (cfg != null ? cfg.arenaCenter.ToString("F2") : "?") + " · useFieldBossPositionAsCenter " + (cfg != null ? (cfg.useFieldBossPositionAsCenter ? "1" : "0") : "?") + " · arenaRadius " + (cfg != null ? cfg.arenaRadius.ToString("F0") : "?") + " · maxWalkableY " + (tg != null ? tg.maxWalkableY.ToString("F2") : "?")); if (tbl != null) { var d5 = tbl.Get(4); var d10 = tbl.Get(9); L("현재 표 — 스테이지5 앵커 " + (d5 != null ? d5.anchor.ToString("F2") + " r" + d5.barrierRadius.ToString("F0") : "?") + " · 스테이지10 앵커 " + (d10 != null ? d10.anchor.ToString("F2") + " r" + d10.barrierRadius.ToString("F0") : "?")); } var prefab = UnityEditor.AssetDatabase.LoadAssetAtPath(kMapPrefab); if (prefab == null) { L("[에러] 프리팹 로드 실패"); return Flush(); } 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(); } UnityEngine.Physics.SyncTransforms(); var inst = UnityEngine.AI.NavMesh.AddNavMeshData(data); try { var cands = new System.Collections.Generic.List(); if (s_explicit != null && s_explicit.Count > 0) cands.AddRange(s_explicit); else { cands.Add(new UnityEngine.Vector3(179f, 0.01f, 4f)); // 813t3 실측 최적지(1순위) cands.Add(new UnityEngine.Vector3(139f, 0.8f, -20f)); // 현재 아레나(대조) // 차선 후보 2곳 — 813t3 WideFlatSearch 와 같은 잣대로 다시 훑어 1순위 다음 2개를 뽑는다 var wide = WideSearch(); int added = 0; for (int i = 0; i < wide.Count && added < 2; i++) { bool dup = false; for (int j = 0; j < cands.Count; j++) if (UnityEngine.Vector3.Distance(wide[i], cands[j]) < 12f) { dup = true; break; } if (dup) continue; cands.Add(wide[i]); added++; } } var start = ResolveStart(go); for (int i = 0; i < cands.Count; i++) Evaluate(go, cands[i], start, i); L(""); L("── 요약 표 (반경 " + kRadius + " m · 격자 " + kStep + " m) ──"); L("| 후보 | 지면 Δy | 최대경사 | 30°초과 | NavMesh 커버 | NavMesh y | y컷 " + kYCut + " 초과 | 시작→중심 | 원주12→중심 | 존→중심 | 물 | 큰바위 | 판정 |"); for (int i = 0; i < s_rows.Count; i++) L(s_rows[i]); } finally { UnityEngine.AI.NavMesh.RemoveNavMeshData(inst); UnityEngine.Object.DestroyImmediate(go); } L(""); L("===== 끝 ====="); return Flush(); } static System.Collections.Generic.List s_rows = new System.Collections.Generic.List(); static System.Collections.Generic.List s_explicit; /// 결선 4곳 전수 평가(정밀 탐색 상위 + 813t3 최적지 + 현 아레나). public static string Final() { s_explicit = new System.Collections.Generic.List(); s_explicit.Add(new UnityEngine.Vector3(179f, 0.01f, 4f)); // 813t3 최적지 · 발주서 1순위 s_explicit.Add(new UnityEngine.Vector3(183f, -0.07f, 16f)); // 정밀 탐색 3위(커버 316/317) s_explicit.Add(new UnityEngine.Vector3(185f, -0.18f, 14f)); // 정밀 탐색 1위(Δy 최소) s_explicit.Add(new UnityEngine.Vector3(139f, 0.8f, -20f)); // 현 아레나(대조) try { return Arena(); } finally { s_explicit = null; } } // ───────────────────────────────────────────────────────────────────────── static UnityEngine.Vector3 ResolveStart(UnityEngine.GameObject root) { var portal = FindByName(root.transform, "Portal_Start"); var p = portal != null ? portal.position : new UnityEngine.Vector3(115f, 0f, 9.98f); L(""); L("시작점 Portal_Start = " + p.ToString("F2") + (portal != null ? "" : " (씬에 없어 발주서 좌표 사용)")); return p; } /// 후보 1곳 전수 평가. static void Evaluate(UnityEngine.GameObject root, UnityEngine.Vector3 want, UnityEngine.Vector3 start, int idx) { L(""); L("──────── 후보 #" + (idx + 1) + " " + want.ToString("F2") + " ────────"); UnityEngine.AI.NavMeshHit ch; if (!UnityEngine.AI.NavMesh.SamplePosition(want, out ch, 5f, UnityEngine.AI.NavMesh.AllAreas)) { L(" 🔴 NavMesh 밖(5 m) — 아레나 불가"); s_rows.Add("| " + want.ToString("F2") + " | — | — | — | **NavMesh 밖** | — | — | — | — | — | — | — | 🔴 불가 |"); return; } L(" NavMesh 스냅 " + ch.position.ToString("F2") + " · Δ" + UnityEngine.Vector3.Distance(want, ch.position).ToString("F2") + " m"); // ① 평탄도 + ② NavMesh 커버 + ④ y 컷 int n = 0, onMesh = 0, steep = 0, noGround = 0, aboveCut = 0; float gyMin = 1e9f, gyMax = -1e9f, myMin = 1e9f, myMax = -1e9f, slopeMax = 0f; double slopeSum = 0.0; int half = UnityEngine.Mathf.CeilToInt(kRadius / kStep); for (int a = -half; a <= half; a++) for (int b = -half; b <= half; b++) { float dx = a * kStep, dz = b * kStep; if (dx * dx + dz * dz > kRadius * kRadius) 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 + 80f, p.z), UnityEngine.Vector3.down, out rh, 240f)) { 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++; if (steep <= 6) L(" [30° 초과] " + rh.point.ToString("F2") + " · " + ang.ToString("F1") + "° · 중심에서 " + UnityEngine.Mathf.Sqrt(dx * dx + dz * dz).ToString("F1") + " m · " + (rh.collider != null ? FullName(rh.collider.transform) : "?")); } } else noGround++; UnityEngine.AI.NavMeshHit h; if (UnityEngine.AI.NavMesh.SamplePosition(p, out h, 2f, 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 > kYCut) aboveCut++; } } float span = (gyMax > -1e8f) ? (gyMax - gyMin) : 99f; L(" ① 평탄도 — 격자 " + n + "점 · 지면 y " + gyMin.ToString("F2") + " ~ " + gyMax.ToString("F2") + " (Δ " + span.ToString("F2") + " m) · 경사 평균 " + (n > noGround ? (slopeSum / (n - noGround)).ToString("F1") : "?") + "° 최대 " + slopeMax.ToString("F1") + "° · **30° 초과 " + steep + "/" + n + "** · 지면 없음 " + noGround); L(" ② NavMesh 커버 — **" + onMesh + "/" + n + "**(≥300 필요) · y " + (myMin > 1e8f ? "없음" : myMin.ToString("F2") + " ~ " + myMax.ToString("F2"))); L(" ④ y 컷 " + kYCut + " 초과 NavMesh 점 — **" + aboveCut + "**"); // ③ 연결성 UnityEngine.AI.NavMeshHit sh; string startPath = "시작점 NavMesh 밖"; if (UnityEngine.AI.NavMesh.SamplePosition(start, out sh, 5f, UnityEngine.AI.NavMesh.AllAreas)) { var p1 = new UnityEngine.AI.NavMeshPath(); UnityEngine.AI.NavMesh.CalculatePath(sh.position, ch.position, UnityEngine.AI.NavMesh.AllAreas, p1); startPath = p1.status.ToString(); } int okr = 0, tried = 0; for (int i = 0; i < 12; i++) { float ang = i * UnityEngine.Mathf.PI * 2f / 12f; var rp = ch.position + new UnityEngine.Vector3(UnityEngine.Mathf.Cos(ang), 0f, UnityEngine.Mathf.Sin(ang)) * (kRadius * 0.8f); UnityEngine.AI.NavMeshHit rh2; if (!UnityEngine.AI.NavMesh.SamplePosition(rp, out rh2, 5f, UnityEngine.AI.NavMesh.AllAreas)) continue; tried++; var p3 = new UnityEngine.AI.NavMeshPath(); UnityEngine.AI.NavMesh.CalculatePath(rh2.position, ch.position, UnityEngine.AI.NavMesh.AllAreas, p3); if (p3.status == UnityEngine.AI.NavMeshPathStatus.PathComplete) okr++; } L(" ③ 연결성 — 시작 " + start.ToString("F2") + " → 중심 **" + startPath + "** · 원주 12점(r " + (kRadius * 0.8f).ToString("F0") + ") → 중심 **" + okr + "/12**(NavMesh 위 " + tried + ")"); // 존 1~4(존 스포너 813001~813004) + 전체 스포너 var mgrs = root.GetComponentsInChildren(true); int zoneOk = 0, zoneTot = 0; string zoneDetail = ""; int allOk = 0, allTot = 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)) continue; var path = new UnityEngine.AI.NavMeshPath(); UnityEngine.AI.NavMesh.CalculatePath(h.position, ch.position, UnityEngine.AI.NavMesh.AllAreas, path); bool ok = path.status == UnityEngine.AI.NavMeshPathStatus.PathComplete; allTot++; if (ok) allOk++; int sid = mgrs[i].SpawnerID; if (sid >= 813001 && sid <= 813004) { zoneTot++; if (ok) zoneOk++; zoneDetail += " #" + sid + "=" + path.status; } } L(" ③ 존 스포너(813001~813004) → 중심 : **" + zoneOk + "/" + zoneTot + "**" + zoneDetail); L(" ③ 전체 스포너 " + allTot + "기 → 중심 PathComplete " + allOk); // ⑤ 물 · 큰 바위 — 바운즈 XZ 최근접점 거리로 판정(AABB 중심 거리 = 강처럼 긴 메시에서 오판) int water = 0, rocks = 0; string worst = ""; float rockMax = 0f, waterNear = 1e9f; Overlap(root, ch.position, kRadius, ref water, ref rocks, ref worst, ref rockMax, ref waterNear, true); L(" ⑤ 반경 " + kRadius + " m 겹침(XZ 최근접) — 물 **" + water + "**(가장 가까운 물까지 " + waterNear.ToString("F1") + " m) · 큰 바위(최대변 ≥3 m) **" + rocks + "**" + (rocks > 0 ? " · 최대 " + worst : "")); // ⑥ 스테이지 배치 산포 슬롯(황금각 · 스테이지 10 = 15마리 기준) NavMesh 해결률 int slots = 15, slotOk = 0; float rMax = kRadius * 0.7f; for (int slot = 0; slot < slots; slot++) { float ang = slot * 2.399963f; float rad = rMax * UnityEngine.Mathf.Sqrt((slot + 0.5f) / slots); var p = ch.position + new UnityEngine.Vector3(UnityEngine.Mathf.Cos(ang) * rad, 0f, UnityEngine.Mathf.Sin(ang) * rad); UnityEngine.AI.NavMeshHit h; if (UnityEngine.AI.NavMesh.SamplePosition(p, out h, 4f, UnityEngine.AI.NavMesh.AllAreas) && h.position.y <= kYCut) slotOk++; } L(" ⑥ 배치 산포 슬롯 15개(황금각 · r " + rMax.ToString("F1") + ") NavMesh+y컷 해결 **" + slotOk + "/15**"); bool pass = steep == 0 && onMesh >= 300 && startPath == "PathComplete" && okr >= 10 && aboveCut == 0 && water == 0 && rocks == 0 && zoneOk == zoneTot && zoneTot > 0; L(" ▶ 판정 = " + (pass ? "PASS" : "미달") + " (평탄 30°초과 " + steep + " · 커버 " + onMesh + " · 시작 " + startPath + " · 원주 " + okr + "/12 · y컷초과 " + aboveCut + " · 물 " + water + " · 존 " + zoneOk + "/" + zoneTot + ")"); s_rows.Add("| " + want.ToString("F2") + " | " + span.ToString("F2") + " m | " + slopeMax.ToString("F1") + "° | " + steep + "/" + n + " | " + onMesh + "/" + n + " | " + (myMin > 1e8f ? "—" : myMin.ToString("F2") + "~" + myMax.ToString("F2")) + " | " + aboveCut + " | " + startPath + " | " + okr + "/12 | " + zoneOk + "/" + zoneTot + " | " + water + " | " + rocks + " | " + (pass ? "**PASS**" : "미달") + " |"); } /// 813t3 WideFlatSearch 와 같은 잣대(±60 m · 4 m 격자 · 반경 20 m 전체 평가) 상위 후보 좌표. static System.Collections.Generic.List WideSearch() { L(""); L("── 차선 후보 탐색 (813t3 WL813t3_Measure.WideFlatSearch 잣대 재사용 · ±60 m · 4 m 격자) ──"); var list = new System.Collections.Generic.List(); for (int i = -15; i <= 15; i++) for (int j = -15; j <= 15; j++) { var c = new UnityEngine.Vector3(139f + i * 4f, 0.8f, -20f + j * 4f); UnityEngine.AI.NavMeshHit ch; if (!UnityEngine.AI.NavMesh.SamplePosition(c, out ch, 2f, UnityEngine.AI.NavMesh.AllAreas)) continue; int n = 0, on = 0, steep = 0; float ymin = 1e9f, ymax = -1e9f, smax = 0f; for (int a = -5; a <= 5; a++) for (int b = -5; b <= 5; b++) { float dx = a * 4f, dz = b * 4f; if (dx * dx + dz * dz > 400f) 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 + 80f, p.z), UnityEngine.Vector3.down, out rh, 240f)) { 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; if (ang > 30f) steep++; } UnityEngine.AI.NavMeshHit h2; if (UnityEngine.AI.NavMesh.SamplePosition(p, out h2, 2f, UnityEngine.AI.NavMesh.AllAreas)) on++; } if (n == 0 || on < n * 0.9f) continue; float span = (ymax > -1e8f) ? (ymax - ymin) : 99f; list.Add(new Cand { p = ch.position, span = span, smax = smax, steep = steep, n = n, on = on }); } list.Sort(delegate (Cand a, Cand b) { return (a.span * 2f + a.steep).CompareTo(b.span * 2f + b.steep); }); L(" 후보(NavMesh 90 % 이상) " + list.Count + "개 · 상위 6"); var outp = new System.Collections.Generic.List(); for (int i = 0; i < list.Count && i < 6; i++) { var c = list[i]; L(" " + (i + 1) + ") " + c.p.ToString("F2") + " · 지면 Δy " + c.span.ToString("F2") + " m · 최대경사 " + c.smax.ToString("F1") + "° · 30° 초과 " + c.steep + "/" + c.n + " · NavMesh " + c.on + "/" + c.n + " · 현 아레나에서 " + UnityEngine.Vector3.Distance(c.p, new UnityEngine.Vector3(139f, 0.8f, -20f)).ToString("F1") + " m"); outp.Add(c.p); } return outp; } struct Cand { public UnityEngine.Vector3 p; public float span, smax; public int steep, n, on; } // ───────────────────────────────────────────────────────────────────────── // 겹침 판정 — 렌더러 바운즈의 XZ 최근접점 거리 (강처럼 긴 메시의 오판을 없앤다) // ───────────────────────────────────────────────────────────────────────── static void Overlap(UnityEngine.GameObject root, UnityEngine.Vector3 c, float r, ref int water, ref int rocks, ref string worst, ref float rockMax, ref float waterNear, bool verbose) { var rends = root.GetComponentsInChildren(true); for (int i = 0; i < rends.Length; i++) { var rd = rends[i]; if (rd == null || !rd.enabled) continue; var b = rd.bounds; float cx = UnityEngine.Mathf.Clamp(c.x, b.min.x, b.max.x); float cz = UnityEngine.Mathf.Clamp(c.z, b.min.z, b.max.z); float d = UnityEngine.Mathf.Sqrt((cx - c.x) * (cx - c.x) + (cz - c.z) * (cz - c.z)); string nm = FullName(rd.transform); bool isWater = nm.IndexOf("water", System.StringComparison.OrdinalIgnoreCase) >= 0 || nm.IndexOf("river", System.StringComparison.OrdinalIgnoreCase) >= 0 || nm.IndexOf("lake", System.StringComparison.OrdinalIgnoreCase) >= 0; if (isWater) { if (d < waterNear) waterNear = d; if (d <= r) { water++; if (verbose && water <= 3) L(" [물] " + nm + " XZ 거리 " + d.ToString("F1") + " m · 바운즈 " + b.center.ToString("F1") + " 크기 " + b.size.ToString("F1")); } continue; } if (d > r) continue; bool isRock = nm.IndexOf("rock", System.StringComparison.OrdinalIgnoreCase) >= 0 || nm.IndexOf("stone", System.StringComparison.OrdinalIgnoreCase) >= 0 || nm.IndexOf("cliff", System.StringComparison.OrdinalIgnoreCase) >= 0; if (!isRock) continue; float big = UnityEngine.Mathf.Max(b.size.x, UnityEngine.Mathf.Max(b.size.y, b.size.z)); if (big < 3f) continue; rocks++; if (verbose && rocks <= 4) L(" [큰 바위] " + nm + " XZ 거리 " + d.ToString("F1") + " m · 중심 " + b.center.ToString("F1") + " 크기 " + b.size.ToString("F1")); if (big > rockMax) { rockMax = big; worst = nm + " " + b.center.ToString("F1") + " 크기 " + b.size.ToString("F1") + " · XZ " + d.ToString("F1") + " m"; } } } // ───────────────────────────────────────────────────────────────────────── // Refine — (179, 0.01, 4) 둘레 ±12 m 를 2 m 격자로 훑어 「30° 초과 0 · 큰 바위 0 · NavMesh ≥300」 을 찾는다 // unity command run_script … --entry WL813t4_Measure.Refine // ───────────────────────────────────────────────────────────────────────── public static string Refine() { s = new System.Text.StringBuilder(); L("===== WL-813t4 정밀 탐색 · " + System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " ====="); UnityEditor.AssetDatabase.ImportAsset(kNavAsset, UnityEditor.ImportAssetOptions.ForceUpdate); var data = UnityEditor.AssetDatabase.LoadAssetAtPath(kNavAsset); var prefab = UnityEditor.AssetDatabase.LoadAssetAtPath(kMapPrefab); if (data == null || prefab == null) { L("[에러] 자산 로드 실패"); return Flush2(); } UnityEditor.SceneManagement.EditorSceneManager.NewScene( UnityEditor.SceneManagement.NewSceneSetup.EmptyScene, UnityEditor.SceneManagement.NewSceneMode.Single); var go = UnityEditor.PrefabUtility.InstantiatePrefab(prefab) as UnityEngine.GameObject; UnityEngine.Physics.SyncTransforms(); var inst = UnityEngine.AI.NavMesh.AddNavMeshData(data); try { var baseP = new UnityEngine.Vector3(179f, 0.01f, 4f); L("기준 " + baseP.ToString("F2") + " 둘레 ±12 m · 2 m 격자 · 각 후보 반경 " + kRadius + " m 를 " + kStep + " m 격자로 평가"); L("| 중심 | 지면 Δy | 최대경사 | 30°초과 | NavMesh 커버 | y컷초과 | 큰바위 | 물까지 | 원주12 | 기준에서 |"); var best = new System.Collections.Generic.List(); for (int i = -6; i <= 6; i++) for (int j = -6; j <= 6; j++) { var want = new UnityEngine.Vector3(baseP.x + i * 2f, baseP.y, baseP.z + j * 2f); UnityEngine.AI.NavMeshHit ch; if (!UnityEngine.AI.NavMesh.SamplePosition(want, out ch, 3f, UnityEngine.AI.NavMesh.AllAreas)) continue; int n = 0, on = 0, steep = 0, above = 0; float ymin = 1e9f, ymax = -1e9f, smax = 0f; int half = UnityEngine.Mathf.CeilToInt(kRadius / kStep); for (int a = -half; a <= half; a++) for (int b = -half; b <= half; b++) { float dx = a * kStep, dz = b * kStep; if (dx * dx + dz * dz > kRadius * kRadius) 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 + 80f, p.z), UnityEngine.Vector3.down, out rh, 240f)) { 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; if (ang > 30f) steep++; } UnityEngine.AI.NavMeshHit h; if (UnityEngine.AI.NavMesh.SamplePosition(p, out h, 2f, UnityEngine.AI.NavMesh.AllAreas)) { on++; if (h.position.y > kYCut) above++; } } if (on < 300) continue; int water = 0, rocks = 0; string worst = ""; float rockMax = 0f, waterNear = 1e9f; Overlap(go, ch.position, kRadius, ref water, ref rocks, ref worst, ref rockMax, ref waterNear, false); int okr = 0; for (int k = 0; k < 12; k++) { float ang2 = k * UnityEngine.Mathf.PI * 2f / 12f; var rp = ch.position + new UnityEngine.Vector3(UnityEngine.Mathf.Cos(ang2), 0f, UnityEngine.Mathf.Sin(ang2)) * (kRadius * 0.8f); 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, ch.position, UnityEngine.AI.NavMesh.AllAreas, p3); if (p3.status == UnityEngine.AI.NavMeshPathStatus.PathComplete) okr++; } best.Add(new Row { p = ch.position, span = ymax - ymin, smax = smax, steep = steep, on = on, n = n, above = above, rocks = rocks, water = water, waterNear = waterNear, okr = okr }); } // 정렬 = 30° 초과 → 큰 바위 → 원주 실패 → Δy best.Sort(delegate (Row a, Row b) { if (a.steep != b.steep) return a.steep.CompareTo(b.steep); if (a.rocks != b.rocks) return a.rocks.CompareTo(b.rocks); if (a.okr != b.okr) return b.okr.CompareTo(a.okr); return a.span.CompareTo(b.span); }); L(" NavMesh 커버 ≥300 후보 " + best.Count + "개 · 상위 10"); for (int i = 0; i < best.Count && i < 10; i++) { var r = best[i]; L("| " + r.p.ToString("F2") + " | " + r.span.ToString("F2") + " m | " + r.smax.ToString("F1") + "° | " + r.steep + "/" + r.n + " | " + r.on + "/" + r.n + " | " + r.above + " | " + r.rocks + " | " + r.waterNear.ToString("F1") + " m | " + r.okr + "/12 | " + UnityEngine.Vector3.Distance(r.p, baseP).ToString("F1") + " m |"); } for (int i = 0; i < best.Count; i++) if (UnityEngine.Vector3.Distance(best[i].p, baseP) < 0.5f) { L(" (813t3 최적지 " + baseP.ToString("F2") + ") 순위 " + (i + 1) + "/" + best.Count + " · 30° 초과 " + best[i].steep + " · 큰 바위 " + best[i].rocks); break; } } finally { UnityEngine.AI.NavMesh.RemoveNavMeshData(inst); UnityEngine.Object.DestroyImmediate(go); } L("===== 끝 ====="); return Flush2(); } struct Row { public UnityEngine.Vector3 p; public float span, smax, waterNear; public int steep, on, n, above, rocks, water, okr; } static string Flush2() { try { System.IO.File.WriteAllText(Abs("AgentScripts/WL813t4_REFINE.txt"), s.ToString(), new System.Text.UTF8Encoding(true)); } catch (System.Exception e) { UnityEngine.Debug.LogWarning("[WL813t4] 저장 실패: " + e.Message); } return s.ToString(); } static string FullName(UnityEngine.Transform t) { string p = t.name; int guard = 0; while (t.parent != null && guard++ < 32) { 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; } }