diff --git a/AgentScripts/WL813t4_MEASURE.txt b/AgentScripts/WL813t4_MEASURE.txt new file mode 100644 index 000000000..7250b5f5b --- /dev/null +++ b/AgentScripts/WL813t4_MEASURE.txt @@ -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 | 미달 | + +===== 끝 ===== diff --git a/AgentScripts/WL813t4_Measure.cs b/AgentScripts/WL813t4_Measure.cs new file mode 100644 index 000000000..37c0d5b45 --- /dev/null +++ b/AgentScripts/WL813t4_Measure.cs @@ -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(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; + } +} diff --git a/AgentScripts/WL813t4_PROBE.txt b/AgentScripts/WL813t4_PROBE.txt new file mode 100644 index 000000000..0d8bd936e --- /dev/null +++ b/AgentScripts/WL813t4_PROBE.txt @@ -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 diff --git a/AgentScripts/WL813t4_Probe.cs b/AgentScripts/WL813t4_Probe.cs new file mode 100644 index 000000000..9a891f571 --- /dev/null +++ b/AgentScripts/WL813t4_Probe.cs @@ -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/WLBossArenaSettings"); + var tbl = UnityEngine.Resources.Load("WL/WLStageTable"); + var guide = UnityEngine.Resources.Load("WL/WLRunGuideSettings"); + var stg = UnityEngine.Resources.Load("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(); + var deploys = new System.Collections.Generic.List(); + var recalls = new System.Collections.Generic.List(); + + 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/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") + ")"); + } +} diff --git a/AgentScripts/WL813t4_REFINE.txt b/AgentScripts/WL813t4_REFINE.txt new file mode 100644 index 000000000..6f7f47b97 --- /dev/null +++ b/AgentScripts/WL813t4_REFINE.txt @@ -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 +===== 끝 ===== diff --git a/Assets/WL/Combat/Settings/Resources/WL/WLBossArenaSettings.asset b/Assets/WL/Combat/Settings/Resources/WL/WLBossArenaSettings.asset index f6c698407..d2c09234a 100644 --- a/Assets/WL/Combat/Settings/Resources/WL/WLBossArenaSettings.asset +++ b/Assets/WL/Combat/Settings/Resources/WL/WLBossArenaSettings.asset @@ -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 diff --git a/Assets/WL/Combat/Settings/Resources/WL/WLStageTable.asset b/Assets/WL/Combat/Settings/Resources/WL/WLStageTable.asset index 01decc49b..89fc2c582 100644 --- a/Assets/WL/Combat/Settings/Resources/WL/WLStageTable.asset +++ b/Assets/WL/Combat/Settings/Resources/WL/WLStageTable.asset @@ -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 diff --git a/Assets/WL/Combat/Stage/StageDirector.cs b/Assets/WL/Combat/Stage/StageDirector.cs index 4e3a9c650..ee572e113 100644 --- a/Assets/WL/Combat/Stage/StageDirector.cs +++ b/Assets/WL/Combat/Stage/StageDirector.cs @@ -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() {