// WL813c_Probe.cs — #813 전투 패드 · 보스 HP 바 실측 덤프 (에디트 모드 · Play 불필요 · 저장하지 않는다) // unity command run_script --file AgentScripts/WL813c_Probe.cs --entry WL813c_Probe.DumpPad (발주서 ⓑ · 부채꼴 좌표 + 미러 2회) // unity command run_script --file AgentScripts/WL813c_Probe.cs --entry WL813c_Probe.DumpBossEvents (발주서 ⓒ · 가짜 이벤트 → 바 갱신) // unity command run_script --file AgentScripts/WL813c_Probe.cs --entry WL813c_Probe.DumpPrefab (발주서 ⓓ · 저장된 프리팹 노드 상태) // unity command run_script --file AgentScripts/WL813c_Probe.cs --entry WL813c_Probe.DumpOverride (발주서 ⓓ · WLIngameUiOverride 대상 경로 회귀) // 덤프는 Logs/WL813c_probe_*.txt 에도 남긴다(콘솔 잘림 대비). using System.IO; using System.Text; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.UI; public static class WL813c_Probe { // run_script 는 파일마다 독립 어셈블리로 컴파일된다 — WL813c_Apply 의 상수를 참조할 수 없어 여기 다시 둔다(값 동일). public const string NewGameUIPath = "Assets/Res_Addr/MainUI/NewGameUI.prefab"; public const string PadPath = "IngameUIs/BattleUI"; public const string HudPath = "IngameUIs/WL_HUD"; public const string BossBarName = "WL_BossHpBar"; const string LogDir = "Logs"; static string Save(string name, string body) { Directory.CreateDirectory(LogDir); var path = Path.Combine(LogDir, "WL813c_probe_" + name + ".txt"); File.WriteAllText(path, body, new System.Text.UTF8Encoding(false)); return body + "\n(저장: " + path + ")"; } static Transform FindByPath(Transform root, string path) { var cur = root; foreach (var part in path.Split('/')) { Transform next = null; for (int i = 0; i < cur.childCount; i++) if (cur.GetChild(i).name == part) { next = cur.GetChild(i); break; } if (next == null) return null; cur = next; } return cur; } /// 발주서 ⓑ — 실제 NewGameUI 프리팹 계층에서 배치를 적용하고 좌표를 덤프한다(저장 안 함 · 미러 2회). public static object DumpPad() { var sb = new StringBuilder(); var s = WL.UI.WLHudLayoutSettings.Instance; if (s == null) return "🔴 WLHudLayoutSettings 없음"; bool origMirror = s.mirrorLeftHanded; var root = PrefabUtility.LoadPrefabContents(NewGameUIPath); try { var pad = FindByPath(root.transform, PadPath); if (pad == null) return "🔴 " + PadPath + " 없음"; var layout = pad.GetComponent(); if (layout == null) return "🔴 WLBattlePadLayout 없음 — Apply 먼저"; var canvas = pad.GetComponentInParent(); var scaler = canvas != null ? canvas.GetComponent() : null; sb.AppendLine("Canvas=" + (canvas != null ? canvas.name : "없음") + " scaleMode=" + (scaler != null ? scaler.uiScaleMode.ToString() : "-") + " refRes=" + (scaler != null ? scaler.referenceResolution.ToString() : "-") + " matchMode=" + (scaler != null ? scaler.screenMatchMode.ToString() : "-") + " match=" + (scaler != null ? scaler.matchWidthOrHeight.ToString("F2") : "-")); sb.AppendLine("SafeAreaFitter on pad = " + (pad.GetComponent() != null) + " · WL_HUD 형제 fitter = " + (FindByPath(root.transform, HudPath) != null && FindByPath(root.transform, HudPath).GetComponent() != null)); sb.AppendLine(); s.mirrorLeftHanded = false; sb.AppendLine("── 1회차 mirror=false ──────────────────────────────"); sb.AppendLine(layout.Apply()); sb.AppendLine(layout.Dump()); s.mirrorLeftHanded = true; sb.AppendLine("── 2회차 mirror=true ───────────────────────────────"); sb.AppendLine(layout.Apply()); sb.AppendLine(layout.Dump()); } finally { PrefabUtility.UnloadPrefabContents(root); // 저장하지 않는다 = 프리팹 무변경 s.mirrorLeftHanded = origMirror; // 에셋 값 원복(파일 저장 안 함) } return Save("pad", sb.ToString()); } /// 발주서 ⓒ — 임시 씬에 살아 있는 BossHpBar 를 만들고 가짜 전투 이벤트를 실제 CombatEvents 경로로 발행한다. public static object DumpBossEvents() { var sb = new StringBuilder(); var s = WL.UI.WLHudLayoutSettings.Instance; if (s == null) return "🔴 WLHudLayoutSettings 없음"; // 배치모드 에디터는 "제목 없는 미저장 씬"이 열려 있어 Additive 새 씬을 못 만든다. // → 현재 씬에 임시 오브젝트를 만들고 끝나면 지운다(저장하지 않는다). GameObject cgo = null; try { // NewGameUI 와 같은 캔버스 규격(실측: Expand · 참조 1920x1080 · match 0) cgo = new GameObject("WL813cProbeCanvas", typeof(Canvas), typeof(CanvasScaler)); var canvas = cgo.GetComponent(); canvas.renderMode = RenderMode.ScreenSpaceOverlay; var sc = cgo.GetComponent(); sc.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; sc.referenceResolution = new Vector2(1920f, 1080f); sc.screenMatchMode = CanvasScaler.ScreenMatchMode.Expand; sc.matchWidthOrHeight = 0f; var hud = new GameObject("WL_HUD", typeof(RectTransform), typeof(WL.UI.SafeAreaFitter)); var hrt = (RectTransform)hud.transform; hrt.SetParent(cgo.transform, false); hrt.anchorMin = Vector2.zero; hrt.anchorMax = Vector2.one; hrt.offsetMin = Vector2.zero; hrt.offsetMax = Vector2.zero; int beforeSpawned = WL.Combat.Core.CombatEvents.Spawned.Count; var bgo = new GameObject(BossBarName, typeof(RectTransform)); ((RectTransform)bgo.transform).SetParent(hrt, false); var bar = bgo.AddComponent(); // 에디트 모드에서는 OnEnable 이 불리지 않는다 → OnEnable 본체(Initialize)를 직접 부른다(런타임과 같은 경로). sb.AppendLine("Initialize: " + bar.Initialize()); sb.AppendLine("구독 전/후 Spawned 구독자 = " + beforeSpawned + " → " + WL.Combat.Core.CombatEvents.Spawned.Count); sb.AppendLine("초기: " + bar.Dump()); // ⓪ 비보스 스폰은 바를 켜지 않아야 한다(발주서 §1-2 "이벤트 미도착/무관 = 미표시") sb.AppendLine("⓪ " + WL.UI.BossHpBar.RaiseFakeNonBossSpawned() + " → visible=" + bar.Visible + " (기대 False)"); sb.AppendLine("① " + WL.UI.BossHpBar.RaiseFakeBossSpawned(1000f)); sb.AppendLine(bar.Dump()); sb.AppendLine("② " + WL.UI.BossHpBar.RaiseFakeHit(300d)); sb.AppendLine(bar.Dump()); sb.AppendLine("③ " + WL.UI.BossHpBar.RaiseFakeHit(350d)); sb.AppendLine(bar.Dump()); sb.AppendLine("④ " + WL.UI.BossHpBar.RaiseFakeKilled()); sb.AppendLine(bar.Dump()); bar.Subscribe(false); // 에디트 모드에서는 OnDisable 이 불리지 않는다 — 구독을 명시적으로 뗀다 Object.DestroyImmediate(bgo); sb.AppendLine("정리 후 Spawned 구독자 = " + WL.Combat.Core.CombatEvents.Spawned.Count + " (기대 " + beforeSpawned + ")"); } finally { if (cgo != null) Object.DestroyImmediate(cgo); } return Save("boss", sb.ToString()); } /// 발주서 ⓓ — 저장된 프리팹의 추가 노드/컴포넌트 상태만 덤프한다(계층 전체 덤프 아님). public static object DumpPrefab() { var sb = new StringBuilder(); var root = PrefabUtility.LoadPrefabContents(NewGameUIPath); try { var pad = FindByPath(root.transform, PadPath); sb.AppendLine(PadPath + " : " + (pad == null ? "없음" : Comp(pad))); if (pad != null) { var prt = (RectTransform)pad; sb.AppendLine(" rect aMin=" + prt.anchorMin + " aMax=" + prt.anchorMax + " offMin=" + prt.offsetMin + " offMax=" + prt.offsetMax + " pivot=" + prt.pivot + " scale=" + prt.localScale + " activeSelf=" + pad.gameObject.activeSelf + " children=" + pad.childCount); for (int i = 0; i < pad.childCount; i++) { var c = pad.GetChild(i); sb.AppendLine(" - " + c.name + " active=" + c.gameObject.activeSelf + " " + Rect2(c as RectTransform)); } } var hud = FindByPath(root.transform, HudPath); sb.AppendLine(HudPath + " : " + (hud == null ? "없음" : Comp(hud) + " children=" + hud.childCount)); if (hud != null) for (int i = 0; i < hud.childCount; i++) sb.AppendLine(" - " + hud.GetChild(i).name); var bar = hud != null ? hud.Find(BossBarName) : null; if (bar != null) { sb.AppendLine(BossBarName + " : " + Comp(bar) + " " + Rect2(bar as RectTransform)); DumpTree(bar, 1, sb); var b = bar.GetComponent(); if (b != null) sb.AppendLine(b.Dump()); } } finally { PrefabUtility.UnloadPrefabContents(root); } return Save("prefab", sb.ToString()); } /// 발주서 ⓓ — WLIngameUiOverride 가 쓰는 경로가 여전히 유효한지(패드 숨김 회귀 0). public static object DumpOverride() { var sb = new StringBuilder(); var gs = WL.Settings.WLGameplaySettings.Instance; sb.AppendLine("WLGameplaySettings: hideBattlePad=" + (gs != null ? gs.hideBattlePad.ToString() : "(에셋없음)") + " (813c 는 읽기만 · 값 전환은 Lead)"); var root = PrefabUtility.LoadPrefabContents(NewGameUIPath); try { foreach (var p in (gs != null && gs.battlePadPaths != null ? gs.battlePadPaths : new string[0])) { var t = FindByPath(root.transform, p); sb.AppendLine(" battlePadPath \"" + p + "\" → " + (t == null ? "🔴 못 찾음(회귀!)" : "OK activeSelf=" + t.gameObject.activeSelf + " children=" + t.childCount)); } foreach (var p in (gs != null && gs.ingameHideCanvasGroupPaths != null ? gs.ingameHideCanvasGroupPaths : new string[0])) { var t = FindByPath(root.transform, p); sb.AppendLine(" hideCanvasGroupPath \"" + p + "\" → " + (t == null ? "🔴 못 찾음(회귀!)" : "OK")); } var common = FindByPath(root.transform, "Common"); foreach (var n in (gs != null && gs.ingameHideCommonObjectNames != null ? gs.ingameHideCommonObjectNames : new string[0])) { var t = common != null ? common.Find(n) : null; sb.AppendLine(" hideCommonName \"" + n + "\" → " + (t == null ? "「미확인」(NewGameUI.gos[0] 인덱스 기준이라 프리팹 경로와 다를 수 있음)" : "OK")); } } finally { PrefabUtility.UnloadPrefabContents(root); } return Save("override", sb.ToString()); } static void DumpTree(Transform t, int depth, StringBuilder sb) { for (int i = 0; i < t.childCount; i++) { var c = t.GetChild(i); sb.AppendLine(new string(' ', depth * 2) + "- " + c.name + " " + Comp(c) + " " + Rect2(c as RectTransform)); DumpTree(c, depth + 1, sb); } } static string Comp(Transform t) { var sb = new StringBuilder("["); var cs = t.GetComponents(); for (int i = 0; i < cs.Length; i++) { if (i > 0) sb.Append(','); sb.Append(cs[i] == null ? "MISSING" : cs[i].GetType().Name); } return sb.Append(']').ToString(); } static string Rect2(RectTransform rt) { if (rt == null) return ""; return "aMin" + rt.anchorMin + " aMax" + rt.anchorMax + " pos" + rt.anchoredPosition + " size" + rt.sizeDelta; } }