Project_WL/AgentScripts/WL813y_Probe.cs

637 lines
36 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// WL-813y 프로브 — UI 핫픽스 묶음 실측(에디트 모드 · Play 0).
// run_script --file AgentScripts/WL813y_Probe.cs --entry WL813y_Probe.<Entry>
// Nodes · Bridge · Stat · Text · BossLate · Hud
// 로그는 <worktree>/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<Transform>(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<SafeAreaFitter>() != null));
foreach (Transform c in hud)
{
var comps = c.GetComponents<Component>();
var names = new List<string>();
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<ReviveDialog>() : null;
var potion = pad != null ? pad.GetComponentInChildren<PotionButton>(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<PCActor>();
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<MonoBehaviour>(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<Actor>(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<StatPopup>() : 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<LootToast>();
kc = hud.Find("WL_KillChainText").GetComponent<KillChainText>();
sp = hud.Find("WL_StatPopup").GetComponent<StatPopup>();
float u = WLVignetteUtil.UnitsPerPx(hud.GetComponentInParent<Canvas>());
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<RectTransform>();
sb.AppendLine(" 루트 pos" + ltRt.anchoredPosition + " size" + ltRt.sizeDelta +
" aMin" + ltRt.anchorMin + " aMax" + ltRt.anchorMax +
" · SafeArea 안(부모 WL_HUD SafeAreaFitter=" +
(hud.GetComponent<SafeAreaFitter>() != 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<GameObject>("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<HUDDMGUI>();
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<int, int>();
var matName = new Dictionary<int, string>();
foreach (var t in root.GetComponentsInChildren<TMP_Text>(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<BossHpBar>();
ban = hud.Find("WL_BossBanner").GetComponent<BossBanner>();
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<BossHpBar>();
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<BossHpBar>();
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<BossHpBar>();
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<CanvasGroup>();
return cg == null ? "CanvasGroup 없음🔴" : cg.blocksRaycasts.ToString();
}
static MobActor MakeBoss()
{
var go = new GameObject("__WL813y_Boss");
var m = go.AddComponent<MobActor>();
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<WLBattlePadLayout>() : null;
if (pad == null) pad = root.GetComponentInChildren<WLBattlePadLayout>(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);
}
}