Merge branch 'wl/systems/WL-813t4-arena-flat'

This commit is contained in:
깃 관리자 2026-09-11 02:07:09 +09:00
commit eec40635ee
8 changed files with 959 additions and 4 deletions

View File

@ -0,0 +1,78 @@
===== WL-813t4 아레나 평지 실측 · 2026-09-11 01:56:48 =====
NavMesh 602396 B · Agent0 slope 30 · climb 0.75 · voxel 0.16667
현재 SO — arenaCenter (139.00, 0.80, -20.00) · useFieldBossPositionAsCenter 0 · arenaRadius 20 · maxWalkableY 3.50
현재 표 — 스테이지5 앵커 (139.00, 0.80, -20.00) r20 · 스테이지10 앵커 (139.00, 0.80, -20.00) r20
시작점 Portal_Start = (115.00, 0.00, 9.98)
──────── 후보 #1 (179.00, 0.01, 4.00) ────────
NavMesh 스냅 (179.00, 0.01, 4.00) · Δ0.00 m
[30° 초과] (163.00, 0.91, 10.00) · 67.9° · 중심에서 17.1 m · WL_Nature/Dressing/Vegetation/FlowerBush_a_m_07/Collider
[30° 초과] (163.00, 4.51, 14.00) · 35.9° · 중심에서 18.9 m · WL_Nature/Dressing/Trees/Acacia_Tree_NoLeaves_l_01_LOD/Acacia_Tree_NoLeaves_l_01_LOD0/Collider (2)
[30° 초과] (197.00, 0.23, -4.00) · 59.1° · 중심에서 19.7 m · WL_Nature/_Terrain/CPT_Island_L_b_01
① 평탄도 — 격자 317점 · 지면 y -0.29 ~ 4.51 (Δ 4.80 m) · 경사 평균 2.7° 최대 67.9° · **30° 초과 3/317** · 지면 없음 0
② NavMesh 커버 — **317/317**(≥300 필요) · y -0.24 ~ 0.74
④ y 컷 2.96 초과 NavMesh 점 — **0**
③ 연결성 — 시작 (115.00, 0.00, 9.98) → 중심 **PathComplete** · 원주 12점(r 16) → 중심 **12/12**(NavMesh 위 12)
③ 존 스포너(813001~813004) → 중심 : **4/4** #813001=PathComplete #813002=PathComplete #813003=PathComplete #813004=PathComplete
③ 전체 스포너 23기 → 중심 PathComplete 23
[큰 바위] WL_Nature/Dressing/Rocks/Rocks_Square_l_03 XZ 거리 19.0 m · 중심 (187.3, 1.4, -15.7) 크기 (4.1, 4.4, 3.5)
⑤ 반경 20 m 겹침(XZ 최근접) — 물 **0**(가장 가까운 물까지 79.0 m) · 큰 바위(최대변 ≥3 m) **1** · 최대 WL_Nature/Dressing/Rocks/Rocks_Square_l_03 (187.3, 1.4, -15.7) 크기 (4.1, 4.4, 3.5) · XZ 19.0 m
⑥ 배치 산포 슬롯 15개(황금각 · r 14.0) NavMesh+y컷 해결 **15/15**
▶ 판정 = 미달 (평탄 30°초과 3 · 커버 317 · 시작 PathComplete · 원주 12/12 · y컷초과 0 · 물 0 · 존 4/4)
──────── 후보 #2 (183.00, -0.07, 16.00) ────────
NavMesh 스냅 (183.00, -0.07, 16.00) · Δ0.00 m
[30° 초과] (177.00, 0.42, 30.00) · 55.8° · 중심에서 15.2 m · WL_Nature/Dressing/Trees/Acacia_Tree_NoLeaves_crk_m_03_LOD/Acacia_Tree_NoLeaves_crk_m_03_LOD0/Collider (1)
① 평탄도 — 격자 317점 · 지면 y -0.29 ~ 0.61 (Δ 0.90 m) · 경사 평균 2.6° 최대 55.8° · **30° 초과 1/317** · 지면 없음 10
② NavMesh 커버 — **316/317**(≥300 필요) · y -0.24 ~ 0.71
④ y 컷 2.96 초과 NavMesh 점 — **0**
③ 연결성 — 시작 (115.00, 0.00, 9.98) → 중심 **PathComplete** · 원주 12점(r 16) → 중심 **12/12**(NavMesh 위 12)
③ 존 스포너(813001~813004) → 중심 : **4/4** #813001=PathComplete #813002=PathComplete #813003=PathComplete #813004=PathComplete
③ 전체 스포너 23기 → 중심 PathComplete 23
⑤ 반경 20 m 겹침(XZ 최근접) — 물 **0**(가장 가까운 물까지 83.0 m) · 큰 바위(최대변 ≥3 m) **0**
⑥ 배치 산포 슬롯 15개(황금각 · r 14.0) NavMesh+y컷 해결 **15/15**
▶ 판정 = 미달 (평탄 30°초과 1 · 커버 316 · 시작 PathComplete · 원주 12/12 · y컷초과 0 · 물 0 · 존 4/4)
──────── 후보 #3 (185.00, -0.18, 14.00) ────────
NavMesh 스냅 (185.00, -0.18, 14.00) · Δ0.00 m
[30° 초과] (177.00, 0.42, 30.00) · 55.8° · 중심에서 17.9 m · WL_Nature/Dressing/Trees/Acacia_Tree_NoLeaves_crk_m_03_LOD/Acacia_Tree_NoLeaves_crk_m_03_LOD0/Collider (1)
① 평탄도 — 격자 317점 · 지면 y -0.29 ~ 0.49 (Δ 0.78 m) · 경사 평균 2.4° 최대 55.8° · **30° 초과 1/317** · 지면 없음 23
② NavMesh 커버 — **307/317**(≥300 필요) · y -0.24 ~ 0.63
④ y 컷 2.96 초과 NavMesh 점 — **0**
③ 연결성 — 시작 (115.00, 0.00, 9.98) → 중심 **PathComplete** · 원주 12점(r 16) → 중심 **12/12**(NavMesh 위 12)
③ 존 스포너(813001~813004) → 중심 : **4/4** #813001=PathComplete #813002=PathComplete #813003=PathComplete #813004=PathComplete
③ 전체 스포너 23기 → 중심 PathComplete 23
⑤ 반경 20 m 겹침(XZ 최근접) — 물 **0**(가장 가까운 물까지 85.0 m) · 큰 바위(최대변 ≥3 m) **0**
⑥ 배치 산포 슬롯 15개(황금각 · r 14.0) NavMesh+y컷 해결 **15/15**
▶ 판정 = 미달 (평탄 30°초과 1 · 커버 307 · 시작 PathComplete · 원주 12/12 · y컷초과 0 · 물 0 · 존 4/4)
──────── 후보 #4 (139.00, 0.80, -20.00) ────────
NavMesh 스냅 (139.00, 0.81, -20.00) · Δ0.01 m
[30° 초과] (121.00, 4.82, -28.00) · 35.2° · 중심에서 19.7 m · WL_Nature/_Terrain/CPT_Island_L_b_07
[30° 초과] (121.00, 2.95, -24.00) · 31.4° · 중심에서 18.4 m · WL_Nature/_Terrain/CPT_Island_L_b_07
[30° 초과] (121.00, 1.16, -22.00) · 51.2° · 중심에서 18.1 m · WL_Nature/_Terrain/CPT_Island_L_b_07
[30° 초과] (123.00, 8.50, -32.00) · 42.8° · 중심에서 20.0 m · WL_Nature/_Terrain/CPT_Island_L_b_07
[30° 초과] (123.00, 6.89, -30.00) · 42.8° · 중심에서 18.9 m · WL_Nature/_Terrain/CPT_Island_L_b_07
[30° 초과] (123.00, 5.53, -28.00) · 36.0° · 중심에서 17.9 m · WL_Nature/_Terrain/CPT_Island_L_b_07
① 평탄도 — 격자 317점 · 지면 y -0.09 ~ 18.92 (Δ 19.01 m) · 경사 평균 15.4° 최대 84.1° · **30° 초과 90/317** · 지면 없음 0
② NavMesh 커버 — **317/317**(≥300 필요) · y -0.04 ~ 1.42
④ y 컷 2.96 초과 NavMesh 점 — **0**
③ 연결성 — 시작 (115.00, 0.00, 9.98) → 중심 **PathComplete** · 원주 12점(r 16) → 중심 **8/12**(NavMesh 위 12)
③ 존 스포너(813001~813004) → 중심 : **4/4** #813001=PathComplete #813002=PathComplete #813003=PathComplete #813004=PathComplete
③ 전체 스포너 23기 → 중심 PathComplete 23
[큰 바위] WL_Nature/_Rocks/Rock_Sharp_m_06 XZ 거리 16.1 m · 중심 (154.6, 3.4, -28.1) 크기 (2.2, 9.2, 1.9)
[큰 바위] WL_Nature/_Rocks/Rock_Sharp_l_04 XZ 거리 12.6 m · 중심 (128.6, 11.9, -44.5) 크기 (22.7, 19.2, 23.8)
[큰 바위] WL_Nature/_Rocks/Rock_Sharp_l_02 (1) XZ 거리 13.0 m · 중심 (138.0, 12.9, -41.4) 크기 (15.0, 11.5, 16.8)
⑤ 반경 20 m 겹침(XZ 최근접) — 물 **0**(가장 가까운 물까지 39.0 m) · 큰 바위(최대변 ≥3 m) **3** · 최대 WL_Nature/_Rocks/Rock_Sharp_l_04 (128.6, 11.9, -44.5) 크기 (22.7, 19.2, 23.8) · XZ 12.6 m
⑥ 배치 산포 슬롯 15개(황금각 · r 14.0) NavMesh+y컷 해결 **15/15**
▶ 판정 = 미달 (평탄 30°초과 90 · 커버 317 · 시작 PathComplete · 원주 8/12 · y컷초과 0 · 물 0 · 존 4/4)
── 요약 표 (반경 20 m · 격자 2 m) ──
| 후보 | 지면 Δy | 최대경사 | 30°초과 | NavMesh 커버 | NavMesh y | y컷 2.96 초과 | 시작→중심 | 원주12→중심 | 존→중심 | 물 | 큰바위 | 판정 |
| (179.00, 0.01, 4.00) | 4.80 m | 67.9° | 3/317 | 317/317 | -0.24~0.74 | 0 | PathComplete | 12/12 | 4/4 | 0 | 1 | 미달 |
| (183.00, -0.07, 16.00) | 0.90 m | 55.8° | 1/317 | 316/317 | -0.24~0.71 | 0 | PathComplete | 12/12 | 4/4 | 0 | 0 | 미달 |
| (185.00, -0.18, 14.00) | 0.78 m | 55.8° | 1/317 | 307/317 | -0.24~0.63 | 0 | PathComplete | 12/12 | 4/4 | 0 | 0 | 미달 |
| (139.00, 0.80, -20.00) | 19.01 m | 84.1° | 90/317 | 317/317 | -0.04~1.42 | 0 | PathComplete | 8/12 | 4/4 | 0 | 3 | 미달 |
===== 끝 =====

View File

@ -0,0 +1,500 @@
// 발주서 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<UnityEngine.AI.NavMeshData>(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.Combat.Boss.WLBossArenaSettings>("WL/WLBossArenaSettings");
var tbl = UnityEngine.Resources.Load<WL.Combat.Stage.WLStageTable>("WL/WLStageTable");
var tg = UnityEngine.Resources.Load<WL.Combat.Nav.WLTerrainGuardSettings>("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<UnityEngine.GameObject>(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<UnityEngine.Vector3>();
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<string> s_rows = new System.Collections.Generic.List<string>();
static System.Collections.Generic.List<UnityEngine.Vector3> s_explicit;
/// <summary>결선 4곳 전수 평가(정밀 탐색 상위 + 813t3 최적지 + 현 아레나).</summary>
public static string Final()
{
s_explicit = new System.Collections.Generic.List<UnityEngine.Vector3>();
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;
}
/// <summary>후보 1곳 전수 평가.</summary>
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<MobControlMgr>(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**" : "미달") + " |");
}
/// <summary>813t3 WideFlatSearch 와 같은 잣대(±60 m · 4 m 격자 · 반경 20 m 전체 평가) 상위 후보 좌표.</summary>
static System.Collections.Generic.List<UnityEngine.Vector3> WideSearch()
{
L("");
L("── 차선 후보 탐색 (813t3 WL813t3_Measure.WideFlatSearch 잣대 재사용 · ±60 m · 4 m 격자) ──");
var list = new System.Collections.Generic.List<Cand>();
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<UnityEngine.Vector3>();
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<UnityEngine.Renderer>(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<UnityEngine.AI.NavMeshData>(kNavAsset);
var prefab = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(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<Row>();
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;
}
}

View File

@ -0,0 +1,48 @@
===== WL-813t4 프로브 · 2026-09-11 02:01:05 =====
── ① 값 실측 (F-28) ──────────────────────────────────────────────
[PASS] WLBossArenaSettings.arenaCenter — (179.00, 0.01, 4.00)
[PASS] useFieldBossPositionAsCenter 유지 0 — 0
[PASS] arenaRadius 20 유지 — 20.0
[PASS] BossArena.Center 가 새 좌표를 읽는다 — (179.00, 0.01, 4.00)
[PASS] WLStageTable 스테이지 5 anchor — (179.00, 0.01, 4.00) r20
[PASS] 스테이지 5 barrierRadius 20 유지 — 20.0
[PASS] WLStageTable 스테이지 10 anchor — (179.00, 0.01, 4.00) r20
[PASS] 스테이지 10 barrierRadius 20 유지 — 20.0
[PASS] 스테이지 5·10 외 앵커 무변경 — 8스테이지 그대로
[813w 아레나 참조 실측] WLRunGuideSettings 의 Vector3 계열 필드 전수:
· zoneAnchorFallbacks : Vector3[] = [(115.00, 0.39, -22.00), (147.00, 2.59, 30.00), (71.00, -0.29, 18.00), (159.00, 0.88, -22.00)]
[PASS] 813w 에 아레나 좌표 별도 필드 없음(= BossArena.Center 를 읽는다 · 동기 불필요) — Vector3 계열 1개 · 아레나/보스 좌표 필드 0 · 보스 유도는 반경만(bossArriveRadius 10 / bossHoldRadius 14)
[PASS] WLStageSettings.Enabled — 1
참고 — 스테이지 배치 산포 반경 = barrierRadius × spawnScatterFactor 0.70 = 14.0 m · recallExcess 1
── ② Telegraph.StageScale 연결 (StageDirector 2줄) ─────────────────
[PASS] 초기값 — 1.00
[PASS] 입장 스테이지 1 → StageScale 1.20 — 1.20 · 상태 Entering
[PASS] 이탈(Abort) → StageScale 1 — 1.00 · 상태 Idle
[PASS] 입장 스테이지 5 → StageScale 1.00 — 1.00 · 상태 Entering
[PASS] 입장 스테이지 10 → StageScale 0.85 — 0.85 · 상태 Entering
[PASS] 클리어 → StageScale 1 (전 1.20) — 1.00 · 상태 Cleared
[PASS] 실패 → StageScale 1 (전 1.20) — 1.00 · 상태 Failed
[PASS] 로비 이탈 → StageScale 1 (전 0.85) — 1.00 · 상태 Idle
[PASS] 재도전 → StageScale 다시 1.20 — 1.20
── ③ 리스폰 0 · 배치 · 회수 seam (815b 프로브 이음매 재사용) ────────
[PASS] 스테이지 10 스포너 5기 리젠 차단 1회씩 — 차단 호출 5회 · 스포너 5기 [813001, 813004, 813005, 813024, 813010]
[PASS] 고정 수 배치 = 표 합계 15 — 묶음 5개 [813001×6, 813004×6, 813005×1, 813024×1, 813010×1] · Deployed 15
[PASS] 초과분 회수 — 스포너 5기 1회씩 — 회수 호출 5회 · 스포너 5기 · 반환 5 [813001, 813004, 813005, 813024, 813010]
[PASS] 같은 스포너 2묶음 → 차단 1회(중복 방지) — 차단 1회 · allStopped True
🔴 실기(Play) 리젠 0 · On_Regen/Off 실제 동작은 이 세션에서 미확인(815b §7-1·2 한계 그대로 · 살아 있는 MobActor 필요).
── ④ C8 진리표 (값 원복 = 813d 동작) ──────────────────────────────
[PASS] arenaCenter 원복 → BossArena.Center 813d 값 — (139.00, 0.80, -20.00)
[PASS] 복구 — (179.00, 0.01, 4.00)
[PASS] arenaEnabled 0 → 아레나 off (값은 남는다) — arenaEnabled 0 · Center (179.00, 0.01, 4.00)
[PASS] WLStageSettings off → 입장 false · StageScale 1 유지 — Enter=False · StageScale 1.00
[PASS] 815f WLTelegraphSettings.enabled(0 이면 예고 전부 off = 코드 무수정 롤백) — enabled 1
[PASS] 표의 telegraphScale 1 인 스테이지(3) → 배수 없음 — 스테이지 3 telegraphScale 1.00
── ⑤ GC ──────────────────────────────────────────────────────────
[PASS] BossArena.Center + StageScale ×1000 GC Δ — Δ -4096 B (acc 179000)
RESULT PASS — PASS 31 / FAIL 0

View File

@ -0,0 +1,312 @@
// 발주서 WL-813t4 — 프로브 (에디트 모드 · Play 0 · 로그인 0)
//
// unity command --project-path E:\NerdNavis\WL_wt\systems run_script --file AgentScripts/WL813t4_Probe.cs --entry WL813t4_Probe.Run --timeout 900
//
// ① 값 실측 — arenaCenter · 스테이지 5·10 앵커 · 813w WLRunGuideSettings 의 아레나 참조
// ② Telegraph.StageScale 연결(입장 → def.telegraphScale · 이탈/실패/클리어/Abort → 1)
// ③ 리스폰 0 / 배치 / 회수 seam 재실측(815b StopRespawnOverride·DeployOverride·RecallOverride)
// ④ C8 진리표(값 원복 = 813d 동작)
// ⑤ GC Δ
public static class WL813t4_Probe
{
static System.Text.StringBuilder s;
static int s_pass, s_fail;
static void L(string t) { s.AppendLine(t); UnityEngine.Debug.Log("[WL813t4P] " + t); }
static void Chk(string name, bool ok, string got)
{
if (ok) { s_pass++; L(" [PASS] " + name + " — " + got); }
else { s_fail++; L(" [FAIL] " + name + " — " + got); }
}
static string Abs(string rel)
{
return System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(),
rel.Replace('/', System.IO.Path.DirectorySeparatorChar));
}
static readonly UnityEngine.Vector3 kNew = new UnityEngine.Vector3(179f, 0.01f, 4f);
static readonly UnityEngine.Vector3 kOld = new UnityEngine.Vector3(139f, 0.8f, -20f);
public static string Run()
{
s = new System.Text.StringBuilder(); s_pass = 0; s_fail = 0;
L("===== WL-813t4 프로브 · " + System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " =====");
var arena = UnityEngine.Resources.Load<WL.Combat.Boss.WLBossArenaSettings>("WL/WLBossArenaSettings");
var tbl = UnityEngine.Resources.Load<WL.Combat.Stage.WLStageTable>("WL/WLStageTable");
var guide = UnityEngine.Resources.Load<WL.Combat.Run.WLRunGuideSettings>("WL/WLRunGuideSettings");
var stg = UnityEngine.Resources.Load<WL.Combat.Stage.WLStageSettings>("WL/WLStageSettings");
if (arena == null || tbl == null || stg == null) { L("[에러] SO 로드 실패"); return Flush(); }
Section1_Values(arena, tbl, guide, stg);
Section2_StageScale(tbl);
Section3_Spawner(tbl);
Section4_C8(arena, stg, tbl);
Section5_GC();
L("");
L("RESULT " + (s_fail == 0 ? "PASS" : "FAIL") + " — PASS " + s_pass + " / FAIL " + s_fail);
return Flush();
}
static string Flush()
{
try { System.IO.File.WriteAllText(Abs("AgentScripts/WL813t4_PROBE.txt"), s.ToString(), new System.Text.UTF8Encoding(true)); }
catch (System.Exception e) { UnityEngine.Debug.LogWarning("[WL813t4P] 저장 실패: " + e.Message); }
return s.ToString();
}
static bool Near(UnityEngine.Vector3 a, UnityEngine.Vector3 b) { return UnityEngine.Vector3.Distance(a, b) < 0.005f; }
// ─────────────────────────────────────────────────────────────────────────
static void Section1_Values(WL.Combat.Boss.WLBossArenaSettings arena, WL.Combat.Stage.WLStageTable tbl,
WL.Combat.Run.WLRunGuideSettings guide, WL.Combat.Stage.WLStageSettings stg)
{
L("");
L("── ① 값 실측 (F-28) ──────────────────────────────────────────────");
Chk("WLBossArenaSettings.arenaCenter", Near(arena.arenaCenter, kNew), arena.arenaCenter.ToString("F2"));
Chk("useFieldBossPositionAsCenter 유지 0", !arena.useFieldBossPositionAsCenter, arena.useFieldBossPositionAsCenter ? "1" : "0");
Chk("arenaRadius 20 유지", UnityEngine.Mathf.Abs(arena.arenaRadius - 20f) < 0.01f, arena.arenaRadius.ToString("F1"));
Chk("BossArena.Center 가 새 좌표를 읽는다", Near(WL.Combat.Boss.BossArena.Center, kNew), WL.Combat.Boss.BossArena.Center.ToString("F2"));
var d5 = tbl.Get(4); var d10 = tbl.Get(9);
Chk("WLStageTable 스테이지 5 anchor", d5 != null && Near(d5.anchor, kNew), d5 != null ? d5.anchor.ToString("F2") + " r" + d5.barrierRadius.ToString("F0") : "null");
Chk("스테이지 5 barrierRadius 20 유지", d5 != null && UnityEngine.Mathf.Abs(d5.barrierRadius - 20f) < 0.01f, d5 != null ? d5.barrierRadius.ToString("F1") : "null");
Chk("WLStageTable 스테이지 10 anchor", d10 != null && Near(d10.anchor, kNew), d10 != null ? d10.anchor.ToString("F2") + " r" + d10.barrierRadius.ToString("F0") : "null");
Chk("스테이지 10 barrierRadius 20 유지", d10 != null && UnityEngine.Mathf.Abs(d10.barrierRadius - 20f) < 0.01f, d10 != null ? d10.barrierRadius.ToString("F1") : "null");
// 나머지 8스테이지 앵커는 손대지 않았다
bool others = true; string ol = "";
for (int i = 0; i < tbl.Count; i++)
{
if (i == 4 || i == 9) continue;
var d = tbl.Get(i);
if (d == null) continue;
if (Near(d.anchor, kNew)) { others = false; ol += " #" + d.id; }
}
Chk("스테이지 5·10 외 앵커 무변경", others, others ? "8스테이지 그대로" : "바뀐 것" + ol);
// 813w WLRunGuideSettings — 아레나 좌표 필드가 따로 있는가(있으면 동기 필요)
L(" [813w 아레나 참조 실측] WLRunGuideSettings 의 Vector3 계열 필드 전수:");
int v3 = 0; bool sep = false;
var ft = typeof(WL.Combat.Run.WLRunGuideSettings).GetFields(
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
for (int i = 0; i < ft.Length; i++)
{
var f = ft[i];
if (f.FieldType != typeof(UnityEngine.Vector3) && f.FieldType != typeof(UnityEngine.Vector3[])) continue;
v3++;
string val = "";
if (guide != null)
{
if (f.FieldType == typeof(UnityEngine.Vector3)) val = ((UnityEngine.Vector3)f.GetValue(guide)).ToString("F2");
else { var arr = f.GetValue(guide) as UnityEngine.Vector3[]; val = "["; if (arr != null) for (int k = 0; k < arr.Length; k++) val += (k > 0 ? ", " : "") + arr[k].ToString("F2"); val += "]"; }
}
L(" · " + f.Name + " : " + f.FieldType.Name + " = " + val);
if (f.Name.IndexOf("arena", System.StringComparison.OrdinalIgnoreCase) >= 0
|| f.Name.IndexOf("boss", System.StringComparison.OrdinalIgnoreCase) >= 0) sep = true;
}
Chk("813w 에 아레나 좌표 별도 필드 없음(= BossArena.Center 를 읽는다 · 동기 불필요)", !sep,
"Vector3 계열 " + v3 + "개 · 아레나/보스 좌표 필드 " + (sep ? "있음" : "0") + " · 보스 유도는 반경만(bossArriveRadius "
+ (guide != null ? guide.bossArriveRadius.ToString("F0") : "?") + " / bossHoldRadius " + (guide != null ? guide.bossHoldRadius.ToString("F0") : "?") + ")");
Chk("WLStageSettings.Enabled", WL.Combat.Stage.WLStageSettings.Enabled, WL.Combat.Stage.WLStageSettings.Enabled ? "1" : "0");
L(" 참고 — 스테이지 배치 산포 반경 = barrierRadius × spawnScatterFactor " + stg.spawnScatterFactor.ToString("F2")
+ " = " + (20f * stg.spawnScatterFactor).ToString("F1") + " m · recallExcess " + (stg.recallExcess ? "1" : "0"));
}
// ─────────────────────────────────────────────────────────────────────────
static void Section2_StageScale(WL.Combat.Stage.WLStageTable tbl)
{
L("");
L("── ② Telegraph.StageScale 연결 (StageDirector 2줄) ─────────────────");
WL.Combat.Stage.StageDirector.ResetAll();
WL.Combat.Telegraph.Telegraph.StageScale = 1f;
Chk("초기값", UnityEngine.Mathf.Abs(WL.Combat.Telegraph.Telegraph.StageScale - 1f) < 0.0001f,
WL.Combat.Telegraph.Telegraph.StageScale.ToString("F2"));
// 입장 → def.telegraphScale
int[] idx = { 0, 4, 9 };
for (int i = 0; i < idx.Length; i++)
{
var def = tbl.Get(idx[i]);
WL.Combat.Stage.StageDirector.Enter(idx[i], "probe");
float got = WL.Combat.Telegraph.Telegraph.StageScale;
Chk("입장 스테이지 " + (def != null ? def.id : 0) + " → StageScale " + (def != null ? def.telegraphScale.ToString("F2") : "?"),
def != null && UnityEngine.Mathf.Abs(got - def.telegraphScale) < 0.0001f, got.ToString("F2")
+ " · 상태 " + WL.Combat.Stage.StageDirector.State);
if (i == 0)
{
// 이탈(Abort) → 1
WL.Combat.Stage.StageDirector.Abort("probe 이탈");
Chk("이탈(Abort) → StageScale 1", UnityEngine.Mathf.Abs(WL.Combat.Telegraph.Telegraph.StageScale - 1f) < 0.0001f,
WL.Combat.Telegraph.Telegraph.StageScale.ToString("F2") + " · 상태 " + WL.Combat.Stage.StageDirector.State);
}
}
// 클리어 · 실패 — private 이라 리플렉션으로 같은 경로를 태운다(SetState 를 지난다)
var t = typeof(WL.Combat.Stage.StageDirector);
var bf = System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static;
WL.Combat.Stage.StageDirector.Enter(0, "probe clear");
float before = WL.Combat.Telegraph.Telegraph.StageScale;
var mClear = t.GetMethod("Clear", bf);
if (mClear != null) mClear.Invoke(null, null);
Chk("클리어 → StageScale 1 (전 " + before.ToString("F2") + ")",
UnityEngine.Mathf.Abs(WL.Combat.Telegraph.Telegraph.StageScale - 1f) < 0.0001f,
WL.Combat.Telegraph.Telegraph.StageScale.ToString("F2") + " · 상태 " + WL.Combat.Stage.StageDirector.State);
WL.Combat.Stage.StageDirector.Enter(0, "probe fail");
before = WL.Combat.Telegraph.Telegraph.StageScale;
var mFail = t.GetMethod("Fail", bf);
if (mFail != null) mFail.Invoke(null, new object[] { WL.Combat.Stage.StageFailReason.TimeLimit });
Chk("실패 → StageScale 1 (전 " + before.ToString("F2") + ")",
UnityEngine.Mathf.Abs(WL.Combat.Telegraph.Telegraph.StageScale - 1f) < 0.0001f,
WL.Combat.Telegraph.Telegraph.StageScale.ToString("F2") + " · 상태 " + WL.Combat.Stage.StageDirector.State);
// 로비 이탈
WL.Combat.Stage.StageDirector.Enter(9, "probe lobby");
before = WL.Combat.Telegraph.Telegraph.StageScale;
WL.Combat.Stage.StageDirector.ToLobby();
Chk("로비 이탈 → StageScale 1 (전 " + before.ToString("F2") + ")",
UnityEngine.Mathf.Abs(WL.Combat.Telegraph.Telegraph.StageScale - 1f) < 0.0001f,
WL.Combat.Telegraph.Telegraph.StageScale.ToString("F2") + " · 상태 " + WL.Combat.Stage.StageDirector.State);
// 재도전 → 다시 걸린다
WL.Combat.Stage.StageDirector.Enter(0, "probe retry");
WL.Combat.Telegraph.Telegraph.StageScale = 1f; // 일부러 흐트러뜨린다
WL.Combat.Stage.StageDirector.Retry();
var d1 = tbl.Get(0);
Chk("재도전 → StageScale 다시 " + (d1 != null ? d1.telegraphScale.ToString("F2") : "?"),
d1 != null && UnityEngine.Mathf.Abs(WL.Combat.Telegraph.Telegraph.StageScale - d1.telegraphScale) < 0.0001f,
WL.Combat.Telegraph.Telegraph.StageScale.ToString("F2"));
WL.Combat.Stage.StageDirector.Abort("probe 정리");
WL.Combat.Stage.StageDirector.ResetAll();
WL.Combat.Telegraph.Telegraph.StageScale = 1f;
}
// ─────────────────────────────────────────────────────────────────────────
static void Section3_Spawner(WL.Combat.Stage.WLStageTable tbl)
{
L("");
L("── ③ 리스폰 0 · 배치 · 회수 seam (815b 프로브 이음매 재사용) ────────");
var stops = new System.Collections.Generic.List<int>();
var deploys = new System.Collections.Generic.List<string>();
var recalls = new System.Collections.Generic.List<int>();
WL.Combat.Stage.StageSpawner.StopRespawnOverride = delegate (WL.Combat.Stage.StageDef d, int sid) { stops.Add(sid); return true; };
WL.Combat.Stage.StageSpawner.DeployOverride = delegate (WL.Combat.Stage.StageDef d, WL.Combat.Stage.StageWave w, int want) { deploys.Add(w.spawnerId + "×" + want); return want; };
WL.Combat.Stage.StageSpawner.RecallOverride = delegate (WL.Combat.Stage.StageDef d, int sid) { recalls.Add(sid); return 1; };
try
{
var def10 = tbl.Get(9);
stops.Clear(); deploys.Clear();
WL.Combat.Stage.StageDirector.ResetAll();
WL.Combat.Stage.StageDirector.Enter(9, "probe spawner");
string sl = ""; for (int i = 0; i < stops.Count; i++) sl += (i > 0 ? ", " : "") + stops[i];
int distinct = 0;
for (int i = 0; i < stops.Count; i++) { bool dup = false; for (int j = 0; j < i; j++) if (stops[j] == stops[i]) dup = true; if (!dup) distinct++; }
Chk("스테이지 10 스포너 5기 리젠 차단 1회씩", stops.Count == 5 && distinct == 5, "차단 호출 " + stops.Count + "회 · 스포너 " + distinct + "기 [" + sl + "]");
string dl = ""; int sum = 0;
for (int i = 0; i < deploys.Count; i++) dl += (i > 0 ? ", " : "") + deploys[i];
for (int i = 0; def10 != null && def10.waves != null && i < def10.waves.Length; i++) sum += def10.waves[i].count;
Chk("고정 수 배치 = 표 합계 " + (def10 != null ? def10.TotalCount : 0),
def10 != null && WL.Combat.Stage.StageSpawner.Deployed >= def10.TotalCount && sum == def10.TotalCount,
"묶음 " + deploys.Count + "개 [" + dl + "] · Deployed " + WL.Combat.Stage.StageSpawner.Deployed);
// 초과분 회수 경로 — Recall 이 스포너마다 1회
recalls.Clear();
int n = WL.Combat.Stage.StageSpawner.Recall(def10, "probe 초과분 회수");
int rd = 0;
for (int i = 0; i < recalls.Count; i++) { bool dup = false; for (int j = 0; j < i; j++) if (recalls[j] == recalls[i]) dup = true; if (!dup) rd++; }
string rl = ""; for (int i = 0; i < recalls.Count; i++) rl += (i > 0 ? ", " : "") + recalls[i];
Chk("초과분 회수 — 스포너 5기 1회씩", recalls.Count == 5 && rd == 5, "회수 호출 " + recalls.Count + "회 · 스포너 " + rd + "기 · 반환 " + n + " [" + rl + "]");
// 같은 스포너를 두 번 쓰는 표에서도 1회만 — AlreadyRequested
var dup2 = new WL.Combat.Stage.StageDef();
dup2.id = 999; dup2.anchor = kNew; dup2.barrierRadius = 20f;
var w1 = new WL.Combat.Stage.StageWave(); w1.spawnerId = 813001; w1.count = 3;
var w2 = new WL.Combat.Stage.StageWave(); w2.spawnerId = 813001; w2.count = 4;
dup2.waves = new WL.Combat.Stage.StageWave[] { w1, w2 };
stops.Clear();
bool all; WL.Combat.Stage.StageSpawner.StopRespawns(dup2, out all);
Chk("같은 스포너 2묶음 → 차단 1회(중복 방지)", stops.Count == 1, "차단 " + stops.Count + "회 · allStopped " + all);
WL.Combat.Stage.StageDirector.Abort("probe 정리");
WL.Combat.Stage.StageDirector.ResetAll();
}
finally
{
WL.Combat.Stage.StageSpawner.StopRespawnOverride = null;
WL.Combat.Stage.StageSpawner.DeployOverride = null;
WL.Combat.Stage.StageSpawner.RecallOverride = null;
}
L(" 🔴 실기(Play) 리젠 0 · On_Regen/Off 실제 동작은 이 세션에서 미확인(815b §7-1·2 한계 그대로 · 살아 있는 MobActor 필요).");
}
// ─────────────────────────────────────────────────────────────────────────
static void Section4_C8(WL.Combat.Boss.WLBossArenaSettings arena, WL.Combat.Stage.WLStageSettings stg,
WL.Combat.Stage.WLStageTable tbl)
{
L("");
L("── ④ C8 진리표 (값 원복 = 813d 동작) ──────────────────────────────");
WL.Combat.Telegraph.Telegraph.StageScale = 1f;
WL.Combat.Stage.StageDirector.ResetAll();
// ⓐ arenaCenter 를 옛 값으로 되돌리면 BossArena.Center 도 그대로 813d 동작
var keep = arena.arenaCenter;
arena.arenaCenter = kOld;
Chk("arenaCenter 원복 → BossArena.Center 813d 값", Near(WL.Combat.Boss.BossArena.Center, kOld), WL.Combat.Boss.BossArena.Center.ToString("F2"));
arena.arenaCenter = keep;
Chk("복구", Near(WL.Combat.Boss.BossArena.Center, kNew), WL.Combat.Boss.BossArena.Center.ToString("F2"));
// ⓑ arenaEnabled 0 → 813d 아레나 자체가 꺼진다(중심 값은 남는다)
bool ka = arena.arenaEnabled; arena.arenaEnabled = false;
Chk("arenaEnabled 0 → 아레나 off (값은 남는다)", !arena.arenaEnabled, "arenaEnabled 0 · Center " + WL.Combat.Boss.BossArena.Center.ToString("F2"));
arena.arenaEnabled = ka;
// ⓒ 스테이지 축 off → 입장 자체가 안 된다 = StageScale 무변경(815f 병합 전과 동일)
WL.Combat.Stage.WLStageSettings.RuntimeDisabled = true;
bool entered = WL.Combat.Stage.StageDirector.Enter(0, "probe c8");
Chk("WLStageSettings off → 입장 false · StageScale 1 유지",
!entered && UnityEngine.Mathf.Abs(WL.Combat.Telegraph.Telegraph.StageScale - 1f) < 0.0001f,
"Enter=" + entered + " · StageScale " + WL.Combat.Telegraph.Telegraph.StageScale.ToString("F2"));
WL.Combat.Stage.WLStageSettings.RuntimeDisabled = false;
// ⓓ 815f 예고 축 off → StageScale 이 걸려도 예고 자체가 안 붙는다
var tel = UnityEngine.Resources.Load<WL.Combat.Telegraph.WLTelegraphSettings>("WL/WLTelegraphSettings");
Chk("815f WLTelegraphSettings.enabled(0 이면 예고 전부 off = 코드 무수정 롤백)", tel != null, tel != null ? "enabled " + (tel.enabled ? "1" : "0") : "SO 없음");
// ⓔ telegraphScale 을 1 로 두면 배수 없음
var d = tbl.Get(2);
Chk("표의 telegraphScale 1 인 스테이지(3) → 배수 없음", d != null && UnityEngine.Mathf.Abs(d.telegraphScale - 1f) < 0.0001f,
d != null ? "스테이지 " + d.id + " telegraphScale " + d.telegraphScale.ToString("F2") : "null");
WL.Combat.Stage.StageDirector.ResetAll();
WL.Combat.Telegraph.Telegraph.StageScale = 1f;
}
// ─────────────────────────────────────────────────────────────────────────
static void Section5_GC()
{
L("");
L("── ⑤ GC ──────────────────────────────────────────────────────────");
var c = WL.Combat.Boss.BossArena.Center; if (c.x == 12345f) L("(warmup)");
System.GC.Collect();
long a = System.GC.GetTotalMemory(true);
float acc = 0f;
for (int i = 0; i < 1000; i++)
{
var p = WL.Combat.Boss.BossArena.Center;
WL.Combat.Telegraph.Telegraph.StageScale = 1f + (i % 3) * 0.1f;
acc += p.x;
}
WL.Combat.Telegraph.Telegraph.StageScale = 1f;
long b = System.GC.GetTotalMemory(true);
Chk("BossArena.Center + StageScale ×1000 GC Δ", b - a <= 0, "Δ " + (b - a) + " B (acc " + acc.ToString("F0") + ")");
}
}

View File

@ -0,0 +1,16 @@
===== WL-813t4 정밀 탐색 · 2026-09-11 01:55:56 =====
기준 (179.00, 0.01, 4.00) 둘레 ±12 m · 2 m 격자 · 각 후보 반경 20 m 를 2 m 격자로 평가
| 중심 | 지면 Δy | 최대경사 | 30°초과 | NavMesh 커버 | y컷초과 | 큰바위 | 물까지 | 원주12 | 기준에서 |
NavMesh 커버 ≥300 후보 146개 · 상위 10
| (185.00, -0.18, 14.00) | 0.78 m | 55.8° | 1/317 | 307/317 | 0 | 0 | 85.0 m | 12/12 | 11.7 m |
| (185.00, -0.08, 16.00) | 0.83 m | 55.8° | 1/317 | 307/317 | 0 | 0 | 85.0 m | 12/12 | 13.4 m |
| (183.00, -0.07, 16.00) | 0.90 m | 55.8° | 1/317 | 316/317 | 0 | 0 | 83.0 m | 12/12 | 12.6 m |
| (183.00, 0.00, 8.00) | 2.04 m | 59.1° | 1/317 | 316/317 | 0 | 0 | 83.0 m | 12/12 | 5.7 m |
| (183.00, -0.12, 11.67) | 2.55 m | 68.6° | 1/317 | 316/317 | 0 | 0 | 83.0 m | 12/12 | 8.6 m |
| (185.00, -0.11, 12.00) | 0.71 m | 59.1° | 2/317 | 307/317 | 0 | 0 | 85.0 m | 12/12 | 10.0 m |
| (183.00, 0.01, 10.00) | 1.89 m | 67.9° | 2/317 | 316/317 | 0 | 0 | 83.0 m | 12/12 | 7.2 m |
| (171.00, 0.10, 4.00) | 4.80 m | 67.9° | 2/317 | 317/317 | 0 | 0 | 71.0 m | 12/12 | 8.0 m |
| (175.00, 0.07, 4.00) | 4.80 m | 67.9° | 2/317 | 317/317 | 0 | 0 | 75.0 m | 12/12 | 4.0 m |
| (171.00, 0.11, 6.00) | 4.80 m | 67.9° | 2/317 | 317/317 | 0 | 0 | 71.0 m | 12/12 | 8.2 m |
(813t3 최적지 (179.00, 0.01, 4.00)) 순위 81/146 · 30° 초과 3 · 큰 바위 1
===== 끝 =====

View File

@ -32,7 +32,7 @@ MonoBehaviour:
gateOpenDelaySeconds: 0
arenaEnabled: 1
useFieldBossPositionAsCenter: 0
arenaCenter: {x: 139, y: 0.8, z: -20}
arenaCenter: {x: 179, y: 0.01, z: 4}
arenaRadius: 20
bossChaseRange: 20
restoreChaseRangeOnCleanup: 1

View File

@ -142,7 +142,7 @@ MonoBehaviour:
timeLimitOverrideSeconds: 0
- id: 5
mapId: -1
anchor: {x: 139, y: 0.8, z: -20}
anchor: {x: 179, y: 0.01, z: 4}
startRadius: 2
barrierRadius: 20
barrierShape: 0
@ -372,7 +372,7 @@ MonoBehaviour:
timeLimitOverrideSeconds: 0
- id: 10
mapId: -1
anchor: {x: 139, y: 0.8, z: -20}
anchor: {x: 179, y: 0.01, z: 4}
startRadius: 2
barrierRadius: 20
barrierShape: 0

View File

@ -268,6 +268,7 @@ namespace WL.Combat.Stage
countdownSeconds = t.countdownSeconds, retries = s_retries,
time = Now, frame = Time.frameCount
};
WL.Combat.Telegraph.Telegraph.StageScale = def.telegraphScale; // 815f 연결(1줄) — 스테이지별 예고 배수
StageEvents.Stamp("StageEntered");
StageEvents.StageEntered.Dispatch(in ev);
RaiseEnemyCount();
@ -561,7 +562,7 @@ namespace WL.Combat.Stage
// 이벤트 발행
// ─────────────────────────────────────────────────────────────────────
static void SetState(StageState s) { s_state = s; s_stateEnter = Now; }
static void SetState(StageState s) { s_state = s; s_stateEnter = Now; if (s == StageState.Idle || s == StageState.Cleared || s == StageState.Result || s == StageState.Failed) WL.Combat.Telegraph.Telegraph.StageScale = 1f; } // 815f 복원(1줄) — 이탈·Abort·클리어·실패
static void RaiseCountdown()
{