// WL-813y 프로브 — UI 핫픽스 묶음 실측(에디트 모드 · Play 0). // run_script --file AgentScripts/WL813y_Probe.cs --entry WL813y_Probe. // Nodes · Bridge · Stat · Text · BossLate · Hud // 로그는 /Logs/WL813y_*.txt (Assets 밖 · 미추적). // 🔴 프리팹은 LoadPrefabContents 로 열고 **저장하지 않는다**. 구독·임시 노드는 전부 되돌린다. using System; using System.Collections.Generic; using System.IO; using System.Reflection; using System.Text; using UnityEngine; using UnityEngine.UI; using TMPro; using UnityEditor; using WL.UI; public static class WL813y_Probe { const string kPrefab = "Assets/Res_Addr/MainUI/NewGameUI.prefab"; const string kHud = "IngameUIs/WL_HUD"; const string kPad = "IngameUIs/BattleUI"; const BindingFlags NP = BindingFlags.Instance | BindingFlags.NonPublic; static string LogDir { get { string d = Path.Combine(Directory.GetParent(Application.dataPath).FullName, "Logs"); if (!Directory.Exists(d)) Directory.CreateDirectory(d); return d; } } static string Write(string name, string body) { string p = Path.Combine(LogDir, name); File.WriteAllText(p, body, new UTF8Encoding(false)); return p; } static Transform Find(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; } static string PathOf(Transform t, Transform root) { var sb = new StringBuilder(t.name); var c = t.parent; while (c != null && c != root) { sb.Insert(0, c.name + "/"); c = c.parent; } return sb.ToString(); } // ── ① 프리팹 노드 탐색 — HUD_BottomMenu 경로 · WL_HUD 자식 · 패드 좌표 ────── public static string Nodes() { var sb = new StringBuilder(); var root = PrefabUtility.LoadPrefabContents(kPrefab); try { var rt = root.transform; sb.AppendLine("[1] 루트 자식 " + rt.childCount + "개"); foreach (Transform c in rt) sb.AppendLine(" - " + c.name + " active=" + c.gameObject.activeSelf); sb.AppendLine(); sb.AppendLine("[2] 이름에 'Bottom' / 'Menu' 가 든 노드 (전수 탐색)"); var all = root.GetComponentsInChildren(true); sb.AppendLine(" 총 Transform = " + all.Length); foreach (var t in all) { if (t.name.IndexOf("Bottom", StringComparison.OrdinalIgnoreCase) < 0 && t.name.IndexOf("Menu", StringComparison.OrdinalIgnoreCase) < 0) continue; var r = t as RectTransform; sb.AppendLine(" - " + PathOf(t, rt) + " active=" + t.gameObject.activeSelf + " children=" + t.childCount + (r != null ? " pos" + r.anchoredPosition + " size" + r.sizeDelta + " aMin" + r.anchorMin + " aMax" + r.anchorMax + " pivot" + r.pivot : "")); foreach (Transform cc in t) sb.AppendLine(" · " + cc.name + " active=" + cc.gameObject.activeSelf); } sb.AppendLine(); sb.AppendLine("[3] IngameUIs 아래 1단계"); var ing = WLVignetteUtil.FindUiPath(rt, "IngameUIs"); if (ing == null) sb.AppendLine(" IngameUIs 없음"); else foreach (Transform c in ing) sb.AppendLine(" - " + c.name + " active=" + c.gameObject.activeSelf + " children=" + c.childCount); sb.AppendLine(); sb.AppendLine("[4] WL_HUD 자식"); var hud = WLVignetteUtil.FindUiPath(rt, kHud); if (hud == null) sb.AppendLine(" WL_HUD 없음"); else { sb.AppendLine(" WL_HUD SafeAreaFitter=" + (hud.GetComponent() != null)); foreach (Transform c in hud) { var comps = c.GetComponents(); var names = new List(); foreach (var cp in comps) names.Add(cp == null ? "(missing)" : cp.GetType().Name); sb.AppendLine(" - " + c.name + " [" + string.Join(", ", names.ToArray()) + "] children=" + c.childCount); } } sb.AppendLine(); sb.AppendLine("[5] BattleUI 자식"); var bui = WLVignetteUtil.FindUiPath(rt, kPad); if (bui == null) sb.AppendLine(" BattleUI 없음"); else foreach (Transform c in bui) { var r = c as RectTransform; sb.AppendLine(" - " + c.name + " active=" + c.gameObject.activeSelf + (r != null ? " pos" + r.anchoredPosition + " size" + r.sizeDelta + " aMin" + r.anchorMin : "")); } } finally { PrefabUtility.UnloadPrefabContents(root); } string p = Write("WL813y_nodes.txt", sb.ToString()); return sb.ToString() + "\n→ " + p; } // ═══ ⓑ ① 813i 연결 6줄 (발주서 §1-1) ═══════════════════════════════════ public static string Bridge() { var sb = new StringBuilder(); var root = PrefabUtility.LoadPrefabContents(kPrefab); int ov0 = WL.Combat.Survival.WLSurvivalSettings.RuntimeOverride; float ts0 = Time.timeScale; try { var hud = Find(root.transform, kHud); var pad = Find(root.transform, kPad); var dlgT = hud != null ? hud.Find("WL_ReviveDialog") : null; var dlg = dlgT != null ? dlgT.GetComponent() : null; var potion = pad != null ? pad.GetComponentInChildren(true) : null; if (dlg == null) return "🔴 WL_ReviveDialog 없음"; if (potion == null) return "🔴 WL_PotionButton 없음"; dlg.Initialize(); potion.Initialize(); sb.AppendLine("[0] 연결 전 (813tj 상태)"); WLSurvivalUiBridge.Uninstall(); WLSurvivalUiBridge.ResetDiagnostics(); sb.AppendLine(" " + WLSurvivalUiBridge.Dump().TrimEnd()); sb.AppendLine(); sb.AppendLine("[1] Install() — 6줄 연결"); sb.AppendLine(" " + WLSurvivalUiBridge.Install()); sb.Append(WLSurvivalUiBridge.Dump()); sb.AppendLine(); sb.AppendLine("[2] 물약 4줄 — 스위치 off(에셋 enabled=false · 병합 전) → 813tj 표시용 폴백"); WL.Combat.Survival.WLSurvivalSettings.RuntimeOverride = -1; sb.AppendLine(" survivalEnabled=" + WL.Combat.Survival.WLSurvivalSettings.Enabled + " · UI 가 읽는 count=" + PotionButton.CountProvider() + " cdRemain=" + PotionButton.CooldownRemainProvider().ToString("F2") + " cdTotal=" + PotionButton.CooldownTotalProvider().ToString("F2") + " · 버튼 ReadCount=" + potion.ReadCount() + " fill=" + potion.ReadCooldownFill().ToString("F3")); sb.AppendLine(" 클릭 → " + potion.OnClickPotion()); sb.AppendLine(); sb.AppendLine("[3] 물약 4줄 — 스위치 on(RuntimeOverride=1 · Lead 가 병합 시 켜는 상태) → 813i PotionUse"); WL.Combat.Survival.WLSurvivalSettings.RuntimeOverride = 1; WL.Combat.Survival.PotionUse.ResetRun("probe"); WL.Combat.Survival.PotionUse.ResetDiagnostics(); sb.AppendLine(" survivalEnabled=" + WL.Combat.Survival.WLSurvivalSettings.Enabled + " · 813i Remaining=" + WL.Combat.Survival.PotionUse.Remaining + "/" + WL.Combat.Survival.PotionUse.Max + " cdTotal=" + WL.Combat.Survival.PotionUse.CooldownTotal.ToString("F2") + "s"); sb.AppendLine(" UI 가 읽는 count=" + PotionButton.CountProvider() + " cdRemain=" + PotionButton.CooldownRemainProvider().ToString("F2") + " cdTotal=" + PotionButton.CooldownTotalProvider().ToString("F2") + " ← ① ② ③ 이 813i 값을 그대로 읽는다"); int chg0 = WLSurvivalUiBridge.PotionChangedSeen; sb.AppendLine(" ④ UseHandler() → " + PotionButton.UseHandler() + " (PC 없는 에디트 모드라 813i 가 " + WL.Combat.Survival.PotionUse.LastResult + " 로 거절 · 호출은 도달) · Changed 수신 " + chg0 + "→" + WLSurvivalUiBridge.PotionChangedSeen); sb.AppendLine(" 버튼 클릭 경로 → " + potion.OnClickPotion()); sb.AppendLine(); sb.AppendLine("[4] 부활 2줄 — 813i DeathFlow 왕복(가짜 메인 PC · ReviveOverride 로 원본 회복 경로 차단)"); var deathFlowOverride = WL.Combat.Survival.DeathFlow.ReviveOverride; int overrideCalls = 0; WL.Combat.Survival.DeathFlow.ReviveOverride = (p, r) => { overrideCalls++; }; var pc = MakePC(); try { WL.Combat.Survival.DeathFlow.ResetDiagnostics(); WLSurvivalUiBridge.ResetDiagnostics(); dlg.HideNow(); float t = 100f; WL.Combat.Survival.DeathFlow.Tick(t); sb.AppendLine(" ⑤ OnPCDied → " + "state=" + WL.Combat.Survival.DeathFlow.State); WL.Combat.Survival.DeathFlow.OnPCDied(pc); sb.AppendLine(" Died 수신=" + WLSurvivalUiBridge.DiedSeen + " state=" + WL.Combat.Survival.DeathFlow.State + " · 팝업 표시=" + dlg.FallbackVisible + "(사망 연출 대기 · 예약=" + dlg.Pending + ")"); var st = WL.Combat.Survival.WLSurvivalSettings.Instance; float disp = st != null ? st.deathDisplaySeconds : 2f; WL.Combat.Survival.DeathFlow.Tick(t + disp + 0.01f); sb.AppendLine(" +" + disp.ToString("F2") + "s → ReviveRequested 수신=" + WLSurvivalUiBridge.ReviveRequestedSeen + " state=" + WL.Combat.Survival.DeathFlow.State + " · 팝업 표시=" + dlg.FallbackVisible + " shows=" + dlg.ShownCount + " 본문=\"" + dlg.LastMessage.Replace("\n", " / ") + "\""); sb.AppendLine(" autoReviveWhenNoListener 무력화 = ReviveRequested 구독자 " + WL.Combat.Survival.DeathFlow.ReviveRequested.Count + "(0 이면 813i 가 자동 부활한다)"); sb.AppendLine(" ⑥ 팝업 확인 버튼 → " + dlg.OnConfirm()); sb.AppendLine(" DeathFlow.Revive() 호출=" + WLSurvivalUiBridge.ReviveCalls + " state=" + WL.Combat.Survival.DeathFlow.State + " reviveCount=" + WL.Combat.Survival.DeathFlow.ReviveCount + " (ReviveOverride 호출 " + overrideCalls + ")"); sb.AppendLine(" Revived 수신=" + WLSurvivalUiBridge.RevivedSeen + " → 팝업 닫힘=" + !dlg.FallbackVisible); sb.AppendLine(" 최종 " + WLSurvivalUiBridge.Dump().Split('\n')[0].Trim()); } finally { WL.Combat.Survival.DeathFlow.ReviveOverride = deathFlowOverride; if (pc != null) UnityEngine.Object.DestroyImmediate(pc.gameObject); } } finally { WLSurvivalUiBridge.Uninstall(); WL.Combat.Survival.WLSurvivalSettings.RuntimeOverride = ov0; WL.Combat.Survival.DeathFlow.ResetDiagnostics(); Time.timeScale = ts0; KillRunners(); PrefabUtility.UnloadPrefabContents(root); } sb.AppendLine(); sb.AppendLine("정리: 브리지 해제=" + !WLSurvivalUiBridge.Installed + " · DeathFlow 구독(Died/Req/Revived)=" + WL.Combat.Survival.DeathFlow.Died.Count + "/" + WL.Combat.Survival.DeathFlow.ReviveRequested.Count + "/" + WL.Combat.Survival.DeathFlow.Revived.Count + " · PotionButton.HasProvider=" + PotionButton.HasProvider + " · timeScale=" + Time.timeScale + " · SurvivalRunner=" + WL.Combat.Survival.SurvivalRunner.Exists); var body = sb.ToString(); return body + "\n→ " + Write("WL813y_bridge.txt", body); } static PCActor MakePC() { var go = new GameObject("__WL813y_PC"); var pc = go.AddComponent(); pc.m_Role = eRole.PC; pc.m_SubRole = eSubRol.None; var stat = new ActorStatInfo(eRole.PC); stat.Set_Stat(eStat.MaxHP, 1000d); stat.Set_Stat(eStat.HP, 0d); typeof(Actor).GetField("m_Stat", NP).SetValue(pc, stat); typeof(Actor).GetField("m_Enemy", NP).SetValue(pc, false); typeof(Actor).GetField("DeadStatus", NP).SetValue(pc, true); return pc; } static void KillRunners() { var all = UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None); foreach (var m in all) { if (m == null) continue; string n = m.GetType().Name; if (n == "SurvivalRunner" || n == "HudCombatVisibilityRunner") UnityEngine.Object.DestroyImmediate(m.gameObject); } var leftovers = UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None); foreach (var a in leftovers) if (a != null && a.name.StartsWith("__WL813y_")) UnityEngine.Object.DestroyImmediate(a.gameObject); } // ═══ ⓑ ② 스탯 팝업 3줄 (발주서 §1-2) ═══════════════════════════════════ public static string Stat() { var sb = new StringBuilder(); var root = PrefabUtility.LoadPrefabContents(kPrefab); StatPopup sp = null; try { var hud = Find(root.transform, kHud); var node = hud != null ? hud.Find("WL_StatPopup") : null; sp = node != null ? node.GetComponent() : null; if (sp == null) return "🔴 WL_StatPopup 없음"; var s = WLCombatTextSettings.Instance; if (s == null) return "🔴 WLCombatTextSettings 에셋 없음"; sb.AppendLine("[0] 구독 축 선택 (statPopupUseGrowthEvents=" + s.statPopupUseGrowthEvents + ")"); sb.AppendLine(" 구독 전 growth=" + WL.Combat.Growth.GrowthEvents.LevelUp.Count + " core=" + WL.Combat.Core.CombatEvents.LevelUp.Count); sb.AppendLine(" " + sp.Initialize()); sb.AppendLine(" 구독 후 growth=" + WL.Combat.Growth.GrowthEvents.LevelUp.Count + " core=" + WL.Combat.Core.CombatEvents.LevelUp.Count + " · UsingGrowthEvents=" + sp.UsingGrowthEvents + " ← 813k 축 하나만 문다(이중 팝업 방지)"); sb.AppendLine(); sb.AppendLine("[1] 813k 실제 Dispatch — 스탯이 오른 경우(statDelta=+12) → 3줄"); sb.AppendLine(" " + StatPopup.RaiseFakeGrowthLevelUp(7, 30, 12d)); sb.AppendLine(" rows=" + sp.LastRowCount + " 포인트줄=" + sp.UsedPointLine + " visible=" + sp.Visible + " level=" + sp.LastLevel); sb.AppendLine(" text = " + sp.LastText); sb.AppendLine(); sb.AppendLine("[2] 813k 실제 Dispatch — 스탯 변화 0(원본 기본값 · 813k 보고 ①) → \"포인트\" 줄"); sb.AppendLine(" " + StatPopup.RaiseFakeGrowthLevelUp(8, 35, 0d)); sb.AppendLine(" rows=" + sp.LastRowCount + " 포인트줄=" + sp.UsedPointLine + " statPointTotal=" + sp.LastStatPointTotal + " levelsGained=" + sp.LastLevelsGained); sb.AppendLine(" text = " + sp.LastText); sb.AppendLine(" 포맷 = \"" + s.statPopupPointFormat + "\" (SO · C8)"); sb.AppendLine(); sb.AppendLine("[3] 811b 코어 축은 이제 안 문다 — CombatEvents.LevelUp 을 쏴도 무반응(이중 팝업 0)"); int before = sp.LevelUpSeen; sb.AppendLine(" " + StatPopup.RaiseFakeLevelUp(9)); sb.AppendLine(" levelUpSeen " + before + "→" + sp.LevelUpSeen + " lastLevel=" + sp.LastLevel + "(8 이어야 한다)"); sb.AppendLine(); sb.AppendLine("[4] 최종 덤프"); sb.Append(sp.Dump()); } finally { if (sp != null) sp.Subscribe(false); PrefabUtility.UnloadPrefabContents(root); } sb.AppendLine(); sb.AppendLine("해제 후 구독자 growth=" + WL.Combat.Growth.GrowthEvents.LevelUp.Count + " core=" + WL.Combat.Core.CombatEvents.LevelUp.Count + " (0/0 이어야 한다)"); var body = sb.ToString(); return body + "\n→ " + Write("WL813y_stat.txt", body); } // ═══ ⓑ ③④ 토스트 가시성 · 텍스트 아웃라인 (발주서 §1-3 · §1-4) ═════════ public static string Text() { var sb = new StringBuilder(); var root = PrefabUtility.LoadPrefabContents(kPrefab); LootToast lt = null; KillChainText kc = null; StatPopup sp = null; try { var hud = Find(root.transform, kHud); var s = WLCombatTextSettings.Instance; if (s == null) return "🔴 WLCombatTextSettings 에셋 없음"; lt = hud.Find("WL_LootToast").GetComponent(); kc = hud.Find("WL_KillChainText").GetComponent(); sp = hud.Find("WL_StatPopup").GetComponent(); float u = WLVignetteUtil.UnitsPerPx(hud.GetComponentInParent()); sb.AppendLine("[0] 기준 — 설계 " + s.designWidthPx + "×" + WLHudLayoutSettings.Instance.designHeightPx + " · unitsPerPx=" + u.ToString("F4") + " · textEdgeEnabled=" + s.textEdgeEnabled); sb.AppendLine(); sb.AppendLine("[1] 토스트 배치 (Q3 결함 D-6 ② · 15회 발생 · 육안 실패)"); sb.AppendLine(" 지속 " + s.lootToastSeconds + "s (발주서 1.2 → 2.0) · 글자 " + s.lootToastFontPx + "px · 줄높이 " + s.lootToastLineHeightPx + "px · 폭 " + s.lootToastWidthPx + "px · 우측여백 " + s.lootToastRightMarginPx + "px · 중심오프셋 " + s.lootToastCenterOffsetPx + "px"); sb.AppendLine(" " + lt.Initialize()); sb.AppendLine(" " + lt.ApplyLayout()); var ltRt = lt.GetComponent(); sb.AppendLine(" 루트 pos" + ltRt.anchoredPosition + " size" + ltRt.sizeDelta + " aMin" + ltRt.anchorMin + " aMax" + ltRt.anchorMax + " · SafeArea 안(부모 WL_HUD SafeAreaFitter=" + (hud.GetComponent() != null) + ")"); sb.AppendLine(" 등급 색 보정: 등급1 " + s.GradeColorTag(1) + " → " + s.GradeColorTagBright(1) + " · 등급2 " + s.GradeColorTag(2) + " → " + s.GradeColorTagBright(2) + " (밝기하한 " + s.lootToastMinBrightness + ")"); sb.AppendLine(" " + lt.Push(1, 3)); sb.AppendLine(" " + lt.Push(2, 1)); sb.Append(lt.Dump()); sb.AppendLine(); sb.AppendLine("[2] 아웃라인 · 그림자 — TMP 머티리얼 실측값(uGUI Outline/Shadow 는 TMP 에 안 먹는다)"); sb.AppendLine(" [연쇄] 값 " + s.Edge(s.killChainEdge)); sb.AppendLine(" " + kc.Initialize()); sb.AppendLine(" 실측 " + kc.LastEdge); sb.AppendLine(" [스탯] 값 " + s.Edge(s.statPopupEdge)); sb.AppendLine(" " + sp.Initialize()); sb.AppendLine(" 실측 " + sp.LastEdge); sb.AppendLine(" [토스트] 값 " + s.Edge(s.lootToastEdge)); sb.AppendLine(" 실측 " + lt.LastEdge); sb.AppendLine(" [데미지] 값 " + s.Edge(s.damageEdge)); sb.AppendLine(" 풀 인스턴스 없음: " + HUDDMGUI.RefreshEdges()); // 데미지 숫자는 InGameInfo.cs:110 이 Addressables 로 Res_Addr/Ingame/HUDDMGUI.prefab 을 12개 복제한다 // (Systems 소유 · **읽기만** — 프로브에서 1개만 인스턴스화해 머티리얼 경로를 확인하고 즉시 파기한다). var dmgSrc = AssetDatabase.LoadAssetAtPath("Assets/Res_Addr/Ingame/HUDDMGUI.prefab"); if (dmgSrc == null) sb.AppendLine(" 🔴 HUDDMGUI.prefab 없음"); else { var inst = (GameObject)PrefabUtility.InstantiatePrefab(dmgSrc); try { var d = inst.GetComponent(); sb.AppendLine(" prefab 인스턴스 1개(dmgs=" + (d != null && d.dmgs != null ? d.dmgs.Length : 0) + "라벨) → " + HUDDMGUI.RefreshEdges()); if (d != null && d.dmgs != null) for (int i = 0; i < d.dmgs.Length; i++) if (d.dmgs[i] != null) sb.AppendLine(" dmgs[" + i + "] mat=" + d.dmgs[i].fontSharedMaterial.name + " outlineW=" + d.dmgs[i].fontSharedMaterial.GetFloat("_OutlineWidth").ToString("F3") + " UNDERLAY_ON=" + d.dmgs[i].fontSharedMaterial.IsKeywordEnabled("UNDERLAY_ON")); } finally { UnityEngine.Object.DestroyImmediate(inst); } sb.AppendLine(" 🔴 HUDDMGUI.prefab 은 **읽기 전용** — 인스턴스만 만들고 파기(에셋 저장 0 · Systems 소유)"); } sb.AppendLine(); sb.AppendLine("[3] 배칭 — 변형 머티리얼은 (원본 머티리얼 + 값) 조합마다 1장 공유(라벨별 인스턴스 0)"); var mats = new Dictionary(); var matName = new Dictionary(); foreach (var t in root.GetComponentsInChildren(true)) { var m = t.fontSharedMaterial; if (m == null || m.name.IndexOf("WL813y") < 0) continue; int id = m.GetInstanceID(); int n; mats.TryGetValue(id, out n); mats[id] = n + 1; matName[id] = m.name + " outlineW=" + m.GetFloat("_OutlineWidth").ToString("F3") + " underlayOff(" + m.GetFloat("_UnderlayOffsetX").ToString("F2") + ")"; } if (mats.Count == 0) sb.AppendLine(" 변형 머티리얼 0 (edge off)"); sb.AppendLine(" 서로 다른 머티리얼 " + mats.Count + "장 (값 조합 3종 = 연쇄 0.30 / 스탯 0.26 / 토스트 0.24)"); foreach (var kv in mats) sb.AppendLine(" · " + matName[kv.Key] + " ← 라벨 " + kv.Value + "개 공유"); } finally { if (lt != null) lt.Subscribe(false); if (kc != null) kc.Subscribe(false); if (sp != null) sp.Subscribe(false); PrefabUtility.UnloadPrefabContents(root); } var body = sb.ToString(); return body + "\n→ " + Write("WL813y_text.txt", body); } // ═══ ⓑ ⑤ 보스 HP 바 · 배너 늦은 구독 보정 (발주서 §1-5) ════════════════ public static string BossLate() { var sb = new StringBuilder(); var root = PrefabUtility.LoadPrefabContents(kPrefab); BossHpBar bar = null; BossBanner ban = null; MobActor boss = null; try { var hud = Find(root.transform, kHud); bar = hud.Find("WL_BossHpBar").GetComponent(); ban = hud.Find("WL_BossBanner").GetComponent(); sb.AppendLine("[0] Q3 결함 D-3 재현 — 보스가 **먼저** 스폰되고 HUD 가 **나중에** 켜진다"); sb.AppendLine(" (Q3 실측: SpawnedSeen=232 · _bound=False — Spawned(isBoss) 가 구독 전에 지나갔다)"); boss = MakeBoss(); sb.AppendLine(" 가짜 보스 = " + boss.name + " m_SubRole=" + boss.m_SubRole + " IsSubRole(Boss)=" + boss.IsSubRole(eSubRol.Boss) + " IsDead=" + boss.IsDead() + " activeInHierarchy=" + boss.gameObject.activeInHierarchy); sb.AppendLine(); sb.AppendLine("[1] 경로 ① BossArena.Boss (813d 게이트가 잡아 둔 보스)"); WL.Combat.Boss.BossArena.SetBossForProbe(boss); sb.AppendLine(" BossArena.Boss=" + (WL.Combat.Boss.BossArena.Boss != null ? WL.Combat.Boss.BossArena.Boss.name : "없음")); sb.AppendLine(" 보스바 " + bar.Initialize()); sb.AppendLine(" → visible=" + bar.Visible + " ratio=" + bar.LastRatio.ToString("F3") + " 늦은구독=" + bar.LateBound + " (기대 True)"); sb.AppendLine(" 배너 " + ban.Initialize()); ban.Tick(Time.unscaledTime + 0.2f); sb.AppendLine(" → visible=" + ban.Visible + " shows=" + ban.ShowCount + " 늦은구독=" + ban.LateTriggered + " α=" + ban.BannerAlpha.ToString("F3") + " (기대 True)"); sb.AppendLine(); sb.AppendLine("[2] 경로 ② 씬 1회 스캔 (게이트 이전 = 맵 로드 시 스폰 · BossArena 비어 있음)"); bar.Subscribe(false); ban.Subscribe(false); WL.Combat.Boss.BossArena.SetBossForProbe(null); var bar2 = hud.Find("WL_BossHpBar").GetComponent(); typeof(BossHpBar).GetField("_bound", NP).SetValue(bar2, false); sb.AppendLine(" BossArena.Boss=없음 · " + bar2.BindExistingBoss()); sb.AppendLine(" → visible=" + bar2.Visible + " 늦은구독=" + bar2.LateBound + " 스캔=" + bar2.LateScanCount); sb.AppendLine(" 배너 " + ban.TriggerExistingBoss()); sb.AppendLine(); sb.AppendLine("[3] 살아 있는 보스가 없으면 아무 일도 없어야 한다(오탐 0)"); boss.gameObject.SetActive(false); var bar3 = hud.Find("WL_BossHpBar").GetComponent(); typeof(BossHpBar).GetField("_bound", NP).SetValue(bar3, false); sb.AppendLine(" 보스 비활성 → " + bar3.BindExistingBoss()); boss.gameObject.SetActive(true); sb.AppendLine(); sb.AppendLine("[4] C8 롤백 — bossBarBindExisting / bannerTriggerExisting = false 면 생략"); var hs = WLHudLayoutSettings.Instance; var bs = WLBossUiSettings.Instance; bool h0 = hs.bossBarBindExisting, b0 = bs.bannerTriggerExisting; hs.bossBarBindExisting = false; bs.bannerTriggerExisting = false; var bar4 = hud.Find("WL_BossHpBar").GetComponent(); typeof(BossHpBar).GetField("_bound", NP).SetValue(bar4, false); sb.AppendLine(" " + bar4.BindExistingBoss()); sb.AppendLine(" " + ban.TriggerExistingBoss()); hs.bossBarBindExisting = h0; bs.bannerTriggerExisting = b0; sb.AppendLine(); sb.AppendLine("[5] 최종 덤프"); sb.Append(bar.Dump()); sb.Append(ban.Dump()); } finally { if (bar != null) bar.Subscribe(false); if (ban != null) ban.Subscribe(false); WL.Combat.Boss.BossArena.SetBossForProbe(null); if (boss != null) UnityEngine.Object.DestroyImmediate(boss.gameObject); PrefabUtility.UnloadPrefabContents(root); } sb.AppendLine(); sb.AppendLine("해제 후 구독자 Spawned=" + WL.Combat.Core.CombatEvents.Spawned.Count + " BossPhase=" + WL.Combat.Boss.BossEvents.BossPhase.Count + " (0/0 이어야 한다)" + " · BossArena.Boss=" + (WL.Combat.Boss.BossArena.Boss != null ? "남음🔴" : "없음")); var body = sb.ToString(); return body + "\n→ " + Write("WL813y_bosslate.txt", body); } static string MenuBlocks(Transform root, WLHudLayoutSettings s) { var m = WLVignetteUtil.FindUiPath(root, s.bottomMenuPath); if (m == null) return "메뉴 없음"; var cg = m.GetComponent(); return cg == null ? "CanvasGroup 없음🔴" : cg.blocksRaycasts.ToString(); } static MobActor MakeBoss() { var go = new GameObject("__WL813y_Boss"); var m = go.AddComponent(); m.m_Role = eRole.Mob; m.m_SubRole = eSubRol.Boss; var stat = new ActorStatInfo(eRole.Mob); stat.Set_Stat(eStat.MaxHP, 50000d); stat.Set_Stat(eStat.HP, 50000d); typeof(Actor).GetField("m_Stat", NP).SetValue(m, stat); typeof(Actor).GetField("m_Enemy", NP).SetValue(m, true); return m; } // ═══ ⓑ ⑥ 전투 중 하단 메뉴 숨김 + 패드 원점 복귀 (발주서 §1-6) ══════════ public static string Hud() { var sb = new StringBuilder(); var root = PrefabUtility.LoadPrefabContents(kPrefab); try { var s = WLHudLayoutSettings.Instance; if (s == null) return "🔴 WLHudLayoutSettings 에셋 없음"; var padT = Find(root.transform, kPad); var pad = padT != null ? padT.GetComponent() : null; if (pad == null) pad = root.GetComponentInChildren(true); if (pad == null) return "🔴 WLBattlePadLayout 없음"; sb.AppendLine("[0] 에셋 값 (C8/C45 · 코드 상수 0)"); sb.AppendLine(" 전투(메뉴 숨김) fanOrigin=" + s.fanOriginPx + " attackCenter=" + s.attackCenterPx); sb.AppendLine(" idle(메뉴 보임) fanOrigin=" + s.idleFanOriginPx + " attackCenter=" + s.idleAttackCenterPx); sb.AppendLine(" fanRadius=" + s.fanRadiusPx + " slot⌀=" + s.slotDiameterPx + " reserveRadius=" + s.reserveRadiusPx + " reserve⌀=" + s.reserveDiameterPx + " attack⌀=" + s.attackDiameterPx + " 목표반경=" + s.thumbReachRadiusPx); sb.AppendLine(" combatHideBottomMenu=" + s.combatHideBottomMenu + " path=\"" + s.bottomMenuPath + "\" exit=" + s.combatExitSeconds + "s enter=" + s.combatEnterDelaySeconds + "s fade=" + s.bottomMenuFadeSeconds + "s canvasGroup=" + s.bottomMenuUseCanvasGroup); sb.AppendLine(); sb.AppendLine("[1] 하단 메뉴 연결 + 복귀 버튼 생성(런타임 · 프리팹 diff 0)"); HudCombatVisibility.Teardown(); HudCombatVisibility.ResetState(); sb.AppendLine(" " + HudCombatVisibility.Bind(root.transform)); sb.AppendLine(" " + HudCombatVisibility.Subscribe(true)); sb.AppendLine(); sb.AppendLine("[2] idle(메뉴 보임) → 전투(메뉴 숨김) → 다시 idle · 도달 px 실측"); // 🔴 Signal() 은 Time.unscaledTime 을 쓴다 — 프로브 클록도 같은 축이어야 한다(임의 t 를 넣으면 since 가 수천 초로 뜬다). float t = Time.unscaledTime; sb.AppendLine(" ⓪ idle " + HudCombatVisibility.Tick(t)); sb.AppendLine(" pad " + pad.Apply()); float idleReach = pad.LastActionReachPx, idleMax = pad.LastMaxReachPx; sb.AppendLine(" ① 전투 신호 " + HudCombatVisibility.Signal("probe:attack")); t = Time.unscaledTime; sb.AppendLine(" tick " + HudCombatVisibility.Tick(t)); sb.AppendLine(" pad " + pad.Apply()); float fightReach = pad.LastActionReachPx, fightMax = pad.LastMaxReachPx; sb.AppendLine(" 메뉴 α=" + HudCombatVisibility.MenuAlpha.ToString("F2") + "(fade " + s.bottomMenuFadeSeconds + "s 시작)" + " 복귀버튼 표시=" + HudCombatVisibility.RestoreButtonVisible + " 메뉴 blocksRaycasts=" + MenuBlocks(root.transform, s)); HudCombatVisibility.Tick(t + s.bottomMenuFadeSeconds + 0.01f); sb.AppendLine(" +" + s.bottomMenuFadeSeconds + "s → 메뉴 α=" + HudCombatVisibility.MenuAlpha.ToString("F2") + "(기대 0.00) blocksRaycasts=" + MenuBlocks(root.transform, s)); sb.AppendLine(" ② 전투 종료(" + s.combatExitSeconds + "s 무신호) " + HudCombatVisibility.Tick(t + s.combatExitSeconds + 0.01f)); HudCombatVisibility.Tick(t + s.combatExitSeconds + s.bottomMenuFadeSeconds + 0.02f); sb.AppendLine(" 메뉴 α=" + HudCombatVisibility.MenuAlpha.ToString("F2") + "(기대 1.00) menuVisible=" + HudCombatVisibility.BottomMenuVisible); sb.AppendLine(" pad " + pad.Apply()); sb.AppendLine(" ③ 복귀 버튼 클릭(전투 중이어도 되돌린다)"); HudCombatVisibility.Signal("probe:attack2"); t = Time.unscaledTime; HudCombatVisibility.Tick(t); sb.AppendLine(" 숨김 확인 menuVisible=" + HudCombatVisibility.BottomMenuVisible + "(기대 False) 복귀버튼 표시=" + HudCombatVisibility.RestoreButtonVisible); sb.AppendLine(" " + HudCombatVisibility.OnRestoreClicked()); sb.AppendLine(" menuVisible=" + HudCombatVisibility.BottomMenuVisible + " (기대 True)"); HudCombatVisibility.Tick(t + 0.01f); sb.AppendLine(" hold " + s.restoreHoldSeconds + "s 안에는 전투여도 유지 → menuVisible=" + HudCombatVisibility.BottomMenuVisible); sb.AppendLine(); sb.AppendLine("[3] 엄지 도달 실측 (발주서 §1-6 목표 ≤ 490 px · 813m 오버레이 747 px 대비)"); sb.AppendLine(" idle(메뉴 보임) actionReach=" + idleReach.ToString("F1") + " px · maxReach(자동토글 포함)=" + idleMax.ToString("F1") + " px"); sb.AppendLine(" 전투(메뉴 숨김) actionReach=" + fightReach.ToString("F1") + " px · maxReach(자동토글 포함)=" + fightMax.ToString("F1") + " px"); sb.AppendLine(" 판정: 전투 중 " + fightReach.ToString("F1") + " ≤ 490 → " + (fightReach <= 490f ? "PASS" : "FAIL") + " (813m 실측 747 px 대비 " + (747f - fightReach).ToString("F1") + " px 감소)"); sb.AppendLine(); sb.AppendLine("[4] C8 롤백 — combatHideBottomMenu=false 면 메뉴를 건드리지 않고 패드도 idle 자리(813c+Lead 핫픽스 상태)"); bool k0 = s.combatHideBottomMenu; s.combatHideBottomMenu = false; HudCombatVisibility.Signal("probe:rollback"); sb.AppendLine(" " + HudCombatVisibility.Tick(Time.unscaledTime)); sb.AppendLine(" " + pad.Apply()); sb.AppendLine(" menuVisible=" + HudCombatVisibility.BottomMenuVisible + "(기대 True · 전투 신호가 있어도 안 숨긴다)" + " 메뉴 α=" + HudCombatVisibility.MenuAlpha.ToString("F2") + " actionReach=" + pad.LastActionReachPx.ToString("F1") + " px(= 롤백 전 746.8)"); s.combatHideBottomMenu = k0; sb.AppendLine(); sb.AppendLine("[5] 최종 덤프"); sb.Append(HudCombatVisibility.Dump()); sb.Append(pad.Dump()); } finally { HudCombatVisibility.Teardown(); KillRunners(); PrefabUtility.UnloadPrefabContents(root); } sb.AppendLine(); sb.AppendLine("정리: 구독=" + HudCombatVisibility.Subscribed + " 연결=" + HudCombatVisibility.Bound + " · AttackStarted 구독자=" + WL.Combat.Core.CombatEvents.AttackStarted.Count + " Damaged=" + WL.Combat.Core.CombatEvents.Damaged.Count + " (0 이어야 한다)"); var body = sb.ToString(); return body + "\n→ " + Write("WL813y_hud.txt", body); } }