From 7a42c50cc448ef66e7451e6be5330d6aad93f2d1 Mon Sep 17 00:00:00 2001 From: swrring Date: Wed, 9 Sep 2026 00:17:58 +0900 Subject: [PATCH] =?UTF-8?q?[WL-813c]=20=EC=A0=84=ED=88=AC=20=ED=8C=A8?= =?UTF-8?q?=EB=93=9C=204=EC=8A=AC=EB=A1=AF=20+=20=EB=B3=B4=EC=8A=A4=20HP?= =?UTF-8?q?=20=EB=B0=94=20(#813)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AgentScripts/WL813c_Apply.cs | 226 +++++++ AgentScripts/WL813c_Probe.cs | 248 +++++++ Assets/Res_Addr/MainUI/NewGameUI.prefab | 640 +++++++++++++++++- Assets/Script/UI/Ingame/BattleUI.cs | 3 + Assets/WL/UI/Scripts/BossHpBar.cs | 432 ++++++++++++ Assets/WL/UI/Scripts/BossHpBar.cs.meta | 2 + Assets/WL/UI/Scripts/WLBattlePadLayout.cs | 275 ++++++++ .../WL/UI/Scripts/WLBattlePadLayout.cs.meta | 2 + Assets/WL/UI/Settings.meta | 8 + Assets/WL/UI/Settings/Resources.meta | 8 + Assets/WL/UI/Settings/Resources/WL.meta | 8 + .../Resources/WL/WLHudLayoutSettings.asset | 50 ++ .../WL/WLHudLayoutSettings.asset.meta | 8 + Assets/WL/UI/Settings/WLHudLayoutSettings.cs | 199 ++++++ .../UI/Settings/WLHudLayoutSettings.cs.meta | 2 + 15 files changed, 2105 insertions(+), 6 deletions(-) create mode 100644 AgentScripts/WL813c_Apply.cs create mode 100644 AgentScripts/WL813c_Probe.cs create mode 100644 Assets/WL/UI/Scripts/BossHpBar.cs create mode 100644 Assets/WL/UI/Scripts/BossHpBar.cs.meta create mode 100644 Assets/WL/UI/Scripts/WLBattlePadLayout.cs create mode 100644 Assets/WL/UI/Scripts/WLBattlePadLayout.cs.meta create mode 100644 Assets/WL/UI/Settings.meta create mode 100644 Assets/WL/UI/Settings/Resources.meta create mode 100644 Assets/WL/UI/Settings/Resources/WL.meta create mode 100644 Assets/WL/UI/Settings/Resources/WL/WLHudLayoutSettings.asset create mode 100644 Assets/WL/UI/Settings/Resources/WL/WLHudLayoutSettings.asset.meta create mode 100644 Assets/WL/UI/Settings/WLHudLayoutSettings.cs create mode 100644 Assets/WL/UI/Settings/WLHudLayoutSettings.cs.meta diff --git a/AgentScripts/WL813c_Apply.cs b/AgentScripts/WL813c_Apply.cs new file mode 100644 index 000000000..a2e31d6b7 --- /dev/null +++ b/AgentScripts/WL813c_Apply.cs @@ -0,0 +1,226 @@ +// WL813c_Apply.cs — #813 바퀴 2: 전투 패드 4슬롯(우하단 엄지 부채꼴) + 상단 보스 HP 바 (에디트 모드 · Play 불필요) +// unity command run_script --file AgentScripts/WL813c_Apply.cs --entry WL813c_Apply.CreateSettings +// unity command run_script --file AgentScripts/WL813c_Apply.cs --entry WL813c_Apply.Resave (무변경 재저장 — 재직렬화 diff 측정 · 802b 교훈) +// unity command run_script --file AgentScripts/WL813c_Apply.cs --entry WL813c_Apply.Apply +// unity command run_script --file AgentScripts/WL813c_Apply.cs --entry WL813c_Apply.Revert (추가한 컴포넌트·노드 제거 = 원상) +// +// 방식(발주서 ⓓ "NewGameUI.prefab 변경 최소 · 노드 추가만"): +// ① IngameUIs/BattleUI (BattleUI.prefab 인스턴스) 에 **인스턴스 전용 추가 컴포넌트**로 +// WL.UI.SafeAreaFitter(802b 방식 · 전용 루트 · 스트레치 · offset 0 · 재부모화 0) + WL.UI.WLBattlePadLayout 을 붙인다. +// → BattleUI.prefab 원본은 손대지 않으므로 SkillUI/SkillEquipUI 의 다른 인스턴스는 회귀 0. +// ② IngameUIs/WL_HUD (이미 SafeAreaFitter 부착 = Safe Area 레이어) 아래에 WL_BossHpBar 노드 1개를 추가한다. +// ③ 슬롯 좌표는 프리팹에 굽지 않는다 — 런타임 WLBattlePadLayout 이 에셋 값으로 배치한다(코드 상수 0 · diff 최소). +// 롤백: Revert() 또는 git checkout -- Assets/Res_Addr/MainUI/NewGameUI.prefab +using System.Collections.Generic; +using System.IO; +using System.Text; +using UnityEditor; +using UnityEngine; + +public static class WL813c_Apply +{ + public const string NewGameUIPath = "Assets/Res_Addr/MainUI/NewGameUI.prefab"; + public const string SettingsDir = "Assets/WL/UI/Settings/Resources/WL"; + public const string SettingsPath = SettingsDir + "/WLHudLayoutSettings.asset"; + public const string BossBarName = "WL_BossHpBar"; + public const string PadPath = "IngameUIs/BattleUI"; + public const string HudPath = "IngameUIs/WL_HUD"; + const string TmpFontGuid = "a387e325271126742ad15569dba1ac57"; // SkillCard/t_cooltime 이 쓰는 폰트(실측) + const int UILayer = 5; + + 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; + } + + static void EnsureFolder(string dir) + { + var parts = dir.Split('/'); + var cur = parts[0]; + for (int i = 1; i < parts.Length; i++) + { + var next = cur + "/" + parts[i]; + if (!AssetDatabase.IsValidFolder(next)) AssetDatabase.CreateFolder(cur, parts[i]); + cur = next; + } + } + + /// WLHudLayoutSettings.asset 생성(이미 있으면 값 유지 · 덮어쓰지 않는다). + public static object CreateSettings() + { + var sb = new StringBuilder(); + var existing = AssetDatabase.LoadAssetAtPath(SettingsPath); + if (existing != null) { sb.AppendLine(SettingsPath + " : 이미 있음 — 값 유지"); } + else + { + EnsureFolder(SettingsDir); + var so = ScriptableObject.CreateInstance(); // 기본값 = 기준서 §B/§F-1 제안값 + AssetDatabase.CreateAsset(so, SettingsPath); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + sb.AppendLine(SettingsPath + " : 생성"); + } + WL.UI.WLHudLayoutSettings.ClearCache(); + var s = WL.UI.WLHudLayoutSettings.Instance; + if (s == null) { sb.AppendLine("🔴 Resources 로드 실패 — Resources/WL 경로 확인"); return sb.ToString(); } + + sb.AppendLine("Resources 로드 OK · design=" + s.designWidthPx + "x" + s.designHeightPx + + " activeSlots=" + s.activeSkillSlots + " slotDia=" + s.slotDiameterPx + + " fanR=" + s.fanRadiusPx + " start=" + s.fanStartAngleDeg + " step=" + s.fanStepAngleDeg); + // 기하 검증 — 모서리 기준 좌표 · 이웃 여백 · 최대 도달 반경 (기준서 §B 요소3/7) + var pts = new List(); + var dia = new List(); + for (int i = 0; i < s.activeSkillSlots; i++) { pts.Add(s.FanSlotPx(i)); dia.Add(s.slotDiameterPx); } + for (int i = 0; i < 6 - s.activeSkillSlots; i++) { pts.Add(s.ReserveSlotPx(i)); dia.Add(s.reserveDiameterPx); } + pts.Add(s.attackCenterPx); dia.Add(s.attackDiameterPx); + pts.Add(s.autoButtonCenterPx); dia.Add(s.autoButtonDiameterPx); + float minGap = float.MaxValue, maxReach = 0f, minEdgeX = float.MaxValue, minEdgeY = float.MaxValue, maxTopPx = 0f; + for (int i = 0; i < pts.Count; i++) + { + maxReach = Mathf.Max(maxReach, pts[i].magnitude + dia[i] * 0.5f); + minEdgeX = Mathf.Min(minEdgeX, pts[i].x - dia[i] * 0.5f); + minEdgeY = Mathf.Min(minEdgeY, pts[i].y - dia[i] * 0.5f); + maxTopPx = Mathf.Max(maxTopPx, pts[i].y + dia[i] * 0.5f); + for (int j = i + 1; j < pts.Count; j++) + minGap = Mathf.Min(minGap, Vector2.Distance(pts[i], pts[j]) - (dia[i] + dia[j]) * 0.5f); + sb.AppendLine(" pt" + i + " px(dx=" + pts[i].x.ToString("F1") + ", dy=" + pts[i].y.ToString("F1") + + ", d=" + dia[i].ToString("F1") + ") reach=" + (pts[i].magnitude + dia[i] * 0.5f).ToString("F1")); + } + sb.AppendLine(" 검증 minGapPx=" + minGap.ToString("F1") + "(기준 ≥" + s.slotMinGapPx + ")" + + " maxReachPx=" + maxReach.ToString("F1") + "(목표 ≤" + s.thumbReachRadiusPx + ")" + + " 화면밖여유 dx=" + minEdgeX.ToString("F1") + " dy=" + minEdgeY.ToString("F1")); + float bottom40 = s.designHeightPx * 0.4f; // 기준서 §F-1 "하단 40 %" — 설정 값에서 파생(상수 0) + sb.AppendLine(" 하단40% 경계=" + bottom40.ToString("F0") + "px · 최상단 버튼 dy+반지름=" + + maxTopPx.ToString("F1") + " → 안=" + (maxTopPx <= bottom40)); + return sb.ToString(); + } + + /// 무변경 재저장 — LoadPrefabContents→SaveAsPrefabAsset 이 diff 를 만드는지 먼저 잰다(802b 교훈). + public static object Resave() + { + var root = PrefabUtility.LoadPrefabContents(NewGameUIPath); + try { PrefabUtility.SaveAsPrefabAsset(root, NewGameUIPath); } + finally { PrefabUtility.UnloadPrefabContents(root); } + AssetDatabase.Refresh(); + return NewGameUIPath + " : 무변경 재저장 (git diff 로 재직렬화 여부 확인)"; + } + + public static object Apply() + { + var sb = new StringBuilder(); + var settings = AssetDatabase.LoadAssetAtPath(SettingsPath); + if (settings == null) return "🔴 " + SettingsPath + " 없음 — CreateSettings 먼저"; + + TMPro.TMP_FontAsset font = null; + var fontPath = AssetDatabase.GUIDToAssetPath(TmpFontGuid); + if (!string.IsNullOrEmpty(fontPath)) font = AssetDatabase.LoadAssetAtPath(fontPath); + sb.AppendLine("폰트: " + (font != null ? fontPath : "「미확인」 — 이름 라벨 폰트 미주입")); + + var root = PrefabUtility.LoadPrefabContents(NewGameUIPath); + try + { + // ── ① 전투 패드 루트 = IngameUIs/BattleUI (인스턴스 전용 추가 컴포넌트) ── + var pad = FindByPath(root.transform, PadPath); + if (pad == null) { sb.AppendLine("🔴 " + PadPath + " 없음"); } + else + { + var prt = (RectTransform)pad; + if (pad.GetComponent() == null) + { pad.gameObject.AddComponent(); sb.AppendLine(PadPath + " : SafeAreaFitter 추가"); } + else sb.AppendLine(PadPath + " : SafeAreaFitter 이미 있음"); + + var layout = pad.GetComponent(); + if (layout == null) { layout = pad.gameObject.AddComponent(); sb.AppendLine(PadPath + " : WLBattlePadLayout 추가"); } + else sb.AppendLine(PadPath + " : WLBattlePadLayout 이미 있음"); + + var so = new SerializedObject(layout); + so.FindProperty("target").objectReferenceValue = pad.GetComponent(); + so.ApplyModifiedPropertiesWithoutUndo(); + + // Safe Area 패널 규격(802b 방식): 스트레치 · offset 0 · pivot 중앙 · 재부모화 0 + sb.AppendLine(" before " + Describe(prt)); + prt.anchorMin = Vector2.zero; prt.anchorMax = Vector2.one; + prt.pivot = new Vector2(0.5f, 0.5f); + prt.offsetMin = Vector2.zero; prt.offsetMax = Vector2.zero; + prt.localScale = Vector3.one; prt.localRotation = Quaternion.identity; + sb.AppendLine(" after " + Describe(prt)); + } + + // ── ② 상단 보스 HP 바 = IngameUIs/WL_HUD/WL_BossHpBar (기존 Safe Area 레이어 아래) ── + var hud = FindByPath(root.transform, HudPath); + if (hud == null) { sb.AppendLine("🔴 " + HudPath + " 없음"); } + else + { + var barT = hud.Find(BossBarName); + if (barT == null) + { + var go = new GameObject(BossBarName, typeof(RectTransform)); + go.layer = UILayer; + barT = go.transform; + ((RectTransform)barT).SetParent((RectTransform)hud, false); + sb.AppendLine(HudPath + "/" + BossBarName + " : 노드 추가"); + } + else sb.AppendLine(HudPath + "/" + BossBarName + " : 이미 있음"); + + var bar = barT.GetComponent(); + if (bar == null) { bar = barT.gameObject.AddComponent(); sb.AppendLine(" BossHpBar 추가"); } + if (font != null) bar.SetNameFont(font); + bool made = bar.BuildIfNeeded(); + sb.AppendLine(" BuildIfNeeded made=" + made + " · " + bar.ApplyLayout()); + // 저장 상태의 기본은 **미표시** — 보스 Spawned 이벤트가 와야 보인다(발주서 §1-2 "이벤트 미도착 = 미표시"). + var cg = barT.GetComponent(); + if (cg != null) { cg.alpha = 0f; cg.interactable = false; cg.blocksRaycasts = false; sb.AppendLine(" CanvasGroup alpha=0 (기본 미표시)"); } + foreach (Transform c in barT) sb.AppendLine(" child " + c.name + " " + Describe(c as RectTransform)); + } + + PrefabUtility.SaveAsPrefabAsset(root, NewGameUIPath); + } + finally { PrefabUtility.UnloadPrefabContents(root); } + + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + return sb.ToString(); + } + + public static object Revert() + { + var sb = new StringBuilder(); + var root = PrefabUtility.LoadPrefabContents(NewGameUIPath); + try + { + var pad = FindByPath(root.transform, PadPath); + if (pad != null) + { + var l = pad.GetComponent(); + if (l != null) { Object.DestroyImmediate(l, true); sb.AppendLine(PadPath + " : WLBattlePadLayout 제거"); } + var f = pad.GetComponent(); + if (f != null) { Object.DestroyImmediate(f, true); sb.AppendLine(PadPath + " : SafeAreaFitter 제거"); } + sb.AppendLine(" ⚠ 루트 RectTransform 값은 되돌리지 않는다 — git checkout -- " + NewGameUIPath + " 를 쓸 것"); + } + var hud = FindByPath(root.transform, HudPath); + var bar = hud != null ? hud.Find(BossBarName) : null; + if (bar != null) { Object.DestroyImmediate(bar.gameObject); sb.AppendLine(HudPath + "/" + BossBarName + " : 제거"); } + PrefabUtility.SaveAsPrefabAsset(root, NewGameUIPath); + } + finally { PrefabUtility.UnloadPrefabContents(root); } + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + return sb.ToString(); + } + + static string Describe(RectTransform rt) + { + if (rt == null) return "(rt 없음)"; + return "aMin=" + rt.anchorMin + " aMax=" + rt.anchorMax + " offMin=" + rt.offsetMin + " offMax=" + rt.offsetMax + + " size=" + rt.sizeDelta + " pos=" + rt.anchoredPosition + " pivot=" + rt.pivot + " scale=" + rt.localScale; + } +} diff --git a/AgentScripts/WL813c_Probe.cs b/AgentScripts/WL813c_Probe.cs new file mode 100644 index 000000000..a12e25750 --- /dev/null +++ b/AgentScripts/WL813c_Probe.cs @@ -0,0 +1,248 @@ +// 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; + } +} diff --git a/Assets/Res_Addr/MainUI/NewGameUI.prefab b/Assets/Res_Addr/MainUI/NewGameUI.prefab index 0d689bd0c..94e4d8697 100644 --- a/Assets/Res_Addr/MainUI/NewGameUI.prefab +++ b/Assets/Res_Addr/MainUI/NewGameUI.prefab @@ -6345,6 +6345,43 @@ RectTransform: m_AnchoredPosition: {x: 0, y: 0} m_SizeDelta: {x: 0, y: 0} m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &410977187151152065 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 7627031014790560531} + m_Layer: 5 + m_Name: Ticks + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &7627031014790560531 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 410977187151152065} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 2628488089120278146} + - {fileID: 4234803563267772812} + m_Father: {fileID: 4263647952213832125} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} --- !u!1 &424934420149951930 GameObject: m_ObjectHideFlags: 0 @@ -7024,6 +7061,44 @@ MonoBehaviour: m_FillOrigin: 0 m_UseSpriteMesh: 0 m_PixelsPerUnitMultiplier: 1 +--- !u!1 &515739860467809776 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 4263647952213832125} + m_Layer: 5 + m_Name: Bar + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &4263647952213832125 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 515739860467809776} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 379739010395703442} + - {fileID: 5853486629667327400} + - {fileID: 7627031014790560531} + m_Father: {fileID: 4008587971485836497} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 49.77778} + m_Pivot: {x: 0.5, y: 0} --- !u!1 &527205017749727505 GameObject: m_ObjectHideFlags: 0 @@ -21595,6 +21670,77 @@ MonoBehaviour: texts: - {fileID: 3829523605195096373} - {fileID: 1207394060063698035} +--- !u!1 &1383167073221206107 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 4008587971485836497} + - component: {fileID: 1658727240416140832} + - component: {fileID: 3869902529055779347} + m_Layer: 5 + m_Name: WL_BossHpBar + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &4008587971485836497 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1383167073221206107} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 4263647952213832125} + - {fileID: 7756575112940689194} + m_Father: {fileID: 6746702601929578265} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 1} + m_AnchorMax: {x: 0.5, y: 1} + m_AnchoredPosition: {x: 0, y: -337.77777} + m_SizeDelta: {x: 1564.4445, y: 117.333336} + m_Pivot: {x: 0.5, y: 1} +--- !u!114 &1658727240416140832 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1383167073221206107} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5759d44fea68d204697e4b572af27bde, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::WL.UI.BossHpBar + group: {fileID: 3869902529055779347} + bar: {fileID: 4263647952213832125} + background: {fileID: 5562866952227585321} + fill: {fileID: 7034991675718591158} + tickRoot: {fileID: 7627031014790560531} + nameLabel: {fileID: 7683836662204136578} + nameFont: {fileID: 11400000, guid: a387e325271126742ad15569dba1ac57, type: 2} + showName: 1 +--- !u!225 &3869902529055779347 +CanvasGroup: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1383167073221206107} + m_Enabled: 1 + m_Alpha: 0 + m_Interactable: 0 + m_BlocksRaycasts: 0 + m_IgnoreParentGroups: 0 --- !u!1 &1390787552525075213 GameObject: m_ObjectHideFlags: 0 @@ -33812,6 +33958,144 @@ MonoBehaviour: m_Spacing: {x: 5, y: 5} m_Constraint: 1 m_ConstraintCount: 3 +--- !u!1 &2042806660445920432 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 7756575112940689194} + - component: {fileID: 5255818241730012330} + - component: {fileID: 7683836662204136578} + m_Layer: 5 + m_Name: Name + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &7756575112940689194 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2042806660445920432} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 4008587971485836497} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 53.333332} + m_Pivot: {x: 0.5, y: 1} +--- !u!222 &5255818241730012330 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2042806660445920432} + m_CullTransparentMesh: 1 +--- !u!114 &7683836662204136578 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2042806660445920432} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TextMeshProUGUI + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_text: + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: a387e325271126742ad15569dba1ac57, type: 2} + m_sharedMaterial: {fileID: -6459762427321423278, guid: a387e325271126742ad15569dba1ac57, + type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4294967295 + m_fontColor: {r: 1, g: 0.92, b: 0.6, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_StyleSheet: {fileID: 0} + m_TextStyleHashCode: 0 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_fontSize: 53.333332 + m_fontSizeBase: 53.333332 + m_fontWeight: 400 + m_enableAutoSizing: 0 + m_fontSizeMin: 0 + m_fontSizeMax: 0 + m_fontStyle: 0 + m_HorizontalAlignment: 2 + m_VerticalAlignment: 512 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_characterHorizontalScale: 1 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_TextWrappingMode: 0 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 0 + m_ActiveFontFeatures: 00000000 + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_EmojiFallbackSupport: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 0 + m_isCullingEnabled: 0 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_IsTextObjectScaleStatic: 0 + m_VertexBufferAutoSizeReduction: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_hasFontAssetChanged: 0 + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} --- !u!1 &2048073296754467902 GameObject: m_ObjectHideFlags: 0 @@ -36975,6 +37259,81 @@ MonoBehaviour: m_ChildScaleWidth: 0 m_ChildScaleHeight: 0 m_ReverseArrangement: 0 +--- !u!1 &2265900064724467381 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5853486629667327400} + - component: {fileID: 1579818030648339006} + - component: {fileID: 7034991675718591158} + m_Layer: 5 + m_Name: Fill + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5853486629667327400 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2265900064724467381} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 4263647952213832125} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &1579818030648339006 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2265900064724467381} + m_CullTransparentMesh: 1 +--- !u!114 &7034991675718591158 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2265900064724467381} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image + m_Material: {fileID: 0} + m_Color: {r: 0.99, g: 0.26, b: 0.28, a: 1} + m_RaycastTarget: 0 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 3 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 0 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 --- !u!1 &2273064814390488970 GameObject: m_ObjectHideFlags: 0 @@ -74309,6 +74668,81 @@ MonoBehaviour: m_OnValueChanged: m_PersistentCalls: m_Calls: [] +--- !u!1 &4818555733381499326 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 379739010395703442} + - component: {fileID: 36720904701419872} + - component: {fileID: 5562866952227585321} + m_Layer: 5 + m_Name: Bg + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &379739010395703442 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4818555733381499326} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 4263647952213832125} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &36720904701419872 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4818555733381499326} + m_CullTransparentMesh: 1 +--- !u!114 &5562866952227585321 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4818555733381499326} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image + m_Material: {fileID: 0} + m_Color: {r: 0.05, g: 0.05, b: 0.06, a: 0.78} + m_RaycastTarget: 0 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 --- !u!1 &4820055094452431178 GameObject: m_ObjectHideFlags: 0 @@ -90955,6 +91389,81 @@ RectTransform: m_AnchoredPosition: {x: 0, y: -69.6} m_SizeDelta: {x: 654, y: 40} m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &5965395721045599513 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 4234803563267772812} + - component: {fileID: 8812723386514772295} + - component: {fileID: 9111117998758797823} + m_Layer: 5 + m_Name: Tick_1 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &4234803563267772812 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5965395721045599513} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 7627031014790560531} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.4, y: 0} + m_AnchorMax: {x: 0.4, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 7.111111, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &8812723386514772295 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5965395721045599513} + m_CullTransparentMesh: 1 +--- !u!114 &9111117998758797823 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5965395721045599513} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.85} + m_RaycastTarget: 0 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 --- !u!1 &5989676975516823687 GameObject: m_ObjectHideFlags: 0 @@ -107736,6 +108245,7 @@ RectTransform: - {fileID: 8773291827388259920} - {fileID: 2856595641425040628} - {fileID: 2614694673119463930} + - {fileID: 4008587971485836497} m_Father: {fileID: 2647977513349308266} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} @@ -123348,6 +123858,81 @@ RectTransform: m_AnchoredPosition: {x: 5, y: -320} m_SizeDelta: {x: 490, y: 80} m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &8234802103131574368 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2628488089120278146} + - component: {fileID: 7886232442197427265} + - component: {fileID: 8342103284696050257} + m_Layer: 5 + m_Name: Tick_0 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2628488089120278146 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8234802103131574368} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 7627031014790560531} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.7, y: 0} + m_AnchorMax: {x: 0.7, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 7.111111, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &7886232442197427265 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8234802103131574368} + m_CullTransparentMesh: 1 +--- !u!114 &8342103284696050257 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8234802103131574368} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.85} + m_RaycastTarget: 0 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 --- !u!1 &8237958486829975046 GameObject: m_ObjectHideFlags: 0 @@ -168173,12 +168758,12 @@ PrefabInstance: - target: {fileID: 4460242486586314136, guid: c327e063c90438e419668019b3490455, type: 3} propertyPath: m_AnchorMax.y - value: 0 + value: 1 objectReference: {fileID: 0} - target: {fileID: 4460242486586314136, guid: c327e063c90438e419668019b3490455, type: 3} propertyPath: m_AnchorMin.x - value: 1 + value: 0 objectReference: {fileID: 0} - target: {fileID: 4460242486586314136, guid: c327e063c90438e419668019b3490455, type: 3} @@ -168233,12 +168818,12 @@ PrefabInstance: - target: {fileID: 4460242486586314136, guid: c327e063c90438e419668019b3490455, type: 3} propertyPath: m_AnchoredPosition.x - value: -398 + value: 0 objectReference: {fileID: 0} - target: {fileID: 4460242486586314136, guid: c327e063c90438e419668019b3490455, type: 3} propertyPath: m_AnchoredPosition.y - value: 218 + value: 0 objectReference: {fileID: 0} - target: {fileID: 4460242486586314136, guid: c327e063c90438e419668019b3490455, type: 3} @@ -168258,7 +168843,15 @@ PrefabInstance: m_RemovedComponents: [] m_RemovedGameObjects: [] m_AddedGameObjects: [] - m_AddedComponents: [] + m_AddedComponents: + - targetCorrespondingSourceObject: {fileID: 4055677999475420392, guid: c327e063c90438e419668019b3490455, + type: 3} + insertIndex: -1 + addedObject: {fileID: 4965720817739109040} + - targetCorrespondingSourceObject: {fileID: 4055677999475420392, guid: c327e063c90438e419668019b3490455, + type: 3} + insertIndex: -1 + addedObject: {fileID: 3187828139587058435} m_SourcePrefab: {fileID: 100100000, guid: c327e063c90438e419668019b3490455, type: 3} --- !u!114 &2351113276399051383 stripped MonoBehaviour: @@ -168266,7 +168859,7 @@ MonoBehaviour: type: 3} m_PrefabInstance: {fileID: 9156483218753586562} m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} + m_GameObject: {fileID: 5141691710815562090} m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 5334ce64f7c47af4ba3028b1af4e69e5, type: 3} @@ -168278,3 +168871,38 @@ RectTransform: type: 3} m_PrefabInstance: {fileID: 9156483218753586562} m_PrefabAsset: {fileID: 0} +--- !u!1 &5141691710815562090 stripped +GameObject: + m_CorrespondingSourceObject: {fileID: 4055677999475420392, guid: c327e063c90438e419668019b3490455, + type: 3} + m_PrefabInstance: {fileID: 9156483218753586562} + m_PrefabAsset: {fileID: 0} +--- !u!114 &4965720817739109040 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5141691710815562090} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4f4cdcc41055ee140ae3a9916e8563ce, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::WL.UI.SafeAreaFitter + applyHorizontal: 1 + applyVertical: 1 +--- !u!114 &3187828139587058435 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5141691710815562090} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d9504994cdfd4f44f9aa7b4a6eaeb36f, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::WL.UI.WLBattlePadLayout + target: {fileID: 2351113276399051383} + autoButtonName: btn_auto + attackButtonName: Attack diff --git a/Assets/Script/UI/Ingame/BattleUI.cs b/Assets/Script/UI/Ingame/BattleUI.cs index 51fc71adf..6177382b4 100644 --- a/Assets/Script/UI/Ingame/BattleUI.cs +++ b/Assets/Script/UI/Ingame/BattleUI.cs @@ -30,6 +30,9 @@ public class BattleUI : MonoBehaviour Set_Auto(); DSUtil.InActivateGameObjects(gos); + + WL.UI.WLBattlePadLayout.NotifySet(this); // WL-813c: 서버 프리셋 재적용 뒤 전투 패드 배치 복원(컴포넌트 없으면 무동작) + //switch (m_BattleUIType) //{ // case eUICardType.UI: gos[1].SetActive(true); break; diff --git a/Assets/WL/UI/Scripts/BossHpBar.cs b/Assets/WL/UI/Scripts/BossHpBar.cs new file mode 100644 index 000000000..d7a6771d7 --- /dev/null +++ b/Assets/WL/UI/Scripts/BossHpBar.cs @@ -0,0 +1,432 @@ +// ───────────────────────────────────────────────────────────────────────────── +// BossHpBar.cs — 상단 보스 HP 바 + 페이즈 눈금 (WL-813c · #813) +// +// 기준서 v1 §B 요소 6 "등장 · 처치 연출": 등장 배너 + 상단 HP 바 + 페이즈 눈금(임계 100/70/40). +// §C 요소 6 현행: "상단 보스 HP 바 없음(타겟 패널 슬라이더만)". +// +// ■ 데이터 출처 +// 811b 의 WL.Combat.Core.CombatEvents 를 **구독만** 한다 — Spawned(isBoss) / HitConfirmed / Killed. +// 발주서 §1-2: "이벤트 미도착 시 폴링 폴백 금지 대신 「미표시」" → 이벤트가 오지 않으면 바는 숨은 채로 둔다. +// (Update 에서 Actor 를 찾아다니는 폴백을 두지 않는다. 이벤트 배선 문제를 UI 가 가리면 안 된다.) +// +// ■ 값 +// 크기 · 여백 · 눈금 위치 · 색 · 사라짐 지연은 전부 WLHudLayoutSettings 에셋(C45). +// 페이즈 **전투 임계값** 자체는 813h(Gameplay)가 테이블로 갖는다 — 여기 값은 **표시용 눈금**이다. +// +// ■ 숨김 방식 +// GameObject 를 끄지 않는다(꺼지면 OnEnable 이 안 돌아 구독이 끊긴다). +// CanvasGroup alpha/blocksRaycasts 로만 숨긴다. +// +// 🔴 어셈블리 주의: Assets/WL/UI/ 에 .asmdef 를 만들지 말 것(Assembly-CSharp 단일). +// ───────────────────────────────────────────────────────────────────────────── + +using System.Text; +using TMPro; +using UnityEngine; +using UnityEngine.UI; +using WL.Combat.Core; + +namespace WL.UI +{ + [DisallowMultipleComponent] + [RequireComponent(typeof(RectTransform))] + public class BossHpBar : MonoBehaviour + { + [Header("구성 요소 (BuildIfNeeded 가 만든다)")] + [SerializeField] private CanvasGroup group; + [SerializeField] private RectTransform bar; + [SerializeField] private Image background; + [SerializeField] private Image fill; + [SerializeField] private RectTransform tickRoot; + [SerializeField] private TextMeshProUGUI nameLabel; + + [Header("이름 라벨 폰트 (에디터가 주입)")] + [SerializeField] private TMP_FontAsset nameFont; + + [Tooltip("이름 라벨을 표시한다")] + [SerializeField] private bool showName = true; + + private RectTransform _rt; + private Canvas _canvas; + private Actor _boss; + private bool _bound; + private float _maxHp, _curHp; + private float _hideAt = -1f; + private bool _subscribed; + + /// 마지막 갱신 결과 — 프로브(에디터)가 읽는 진단 값. + public float LastRatio { get; private set; } + public int SpawnedSeen { get; private set; } + public int HitSeen { get; private set; } + public int KilledSeen { get; private set; } + public bool Visible { get { return group != null && group.alpha > 0f; } } + + private void Awake() { _rt = GetComponent(); } + + private void OnEnable() { Initialize(); } + + private void OnDisable() { Subscribe(false); } + + /// + /// OnEnable 본체. 에디트 모드에서는 OnEnable 이 불리지 않으므로 검증 스크립트가 이걸 직접 부른다 + /// (런타임과 **완전히 같은 경로**를 타야 검증이 의미가 있다). + /// + public string Initialize() + { + if (_rt == null) _rt = GetComponent(); + BuildIfNeeded(); + string layout = ApplyLayout(); + HideNow(); + Subscribe(true); + return "initialized subscribed=" + _subscribed + " · " + layout; + } + + /// 구독 등록/해제. OnEnable/OnDisable 이 부르고, 에디터 검증도 같은 것을 쓴다. + public void Subscribe(bool on) + { + if (on == _subscribed) return; + _subscribed = on; + if (on) + { + CombatEvents.Spawned.Add(OnSpawned); + CombatEvents.HitConfirmed.Add(OnHitConfirmed); + CombatEvents.Killed.Add(OnKilled); + } + else + { + CombatEvents.Spawned.Remove(OnSpawned); + CombatEvents.HitConfirmed.Remove(OnHitConfirmed); + CombatEvents.Killed.Remove(OnKilled); + } + } + + private void Update() + { + if (_hideAt >= 0f && Time.unscaledTime >= _hideAt) HideNow(); + } + + // ── 이벤트 구독 (811b CombatEvents) ─────────────────────────────────── + private void OnSpawned(in SpawnedEvent e) + { + SpawnedSeen++; + if (!e.isBoss) return; + Bind(e.actor, null, DebugFallbackMaxHp); + } + + private void OnHitConfirmed(in HitConfirmedEvent e) + { + if (!IsTracked(e.victim)) return; + HitSeen++; + float hp = ReadHp(e.victim); + _curHp = hp >= 0f ? hp : Mathf.Max(0f, _curHp - (float)e.damage); + SetRatio(_maxHp > 0f ? _curHp / _maxHp : 0f); + } + + private void OnKilled(in KilledEvent e) + { + if (!IsTracked(e.victim)) return; + KilledSeen++; + _curHp = 0f; + SetRatio(0f); + var s = WLHudLayoutSettings.Instance; + _hideAt = Time.unscaledTime + (s != null ? s.bossBarHideDelaySec : 0f); + } + + // 합성(에디터 검증) 이벤트는 actor 가 null 이다 — null == null 로 같은 경로를 탄다. + private bool IsTracked(Actor a) { return _bound && ReferenceEquals(a, _boss); } + + private static float ReadHp(Actor a) + { + if (a == null) return -1f; + var st = a.Get_StatInfo(); + if (st == null) return -1f; + return (float)st.Get_Stat(eStat.HP); + } + + private static float ReadMaxHp(Actor a) + { + if (a == null) return -1f; + var st = a.Get_StatInfo(); + if (st == null) return -1f; + return (float)st.Get_Stat(eStat.MaxHP); + } + + /// 보스를 바에 물린다. actor 가 null 이면 합성 검증 모드(최대 HP 는 fallbackMaxHp). + public string Bind(Actor actor, string displayName = null, float fallbackMaxHp = 100f) + { + var s = WLHudLayoutSettings.Instance; + if (s != null && !s.bossBarEnabled) return "bossBarEnabled=false — 미표시"; + + _boss = actor; + _bound = true; + _hideAt = -1f; + + float max = ReadMaxHp(actor); + _maxHp = max > 0f ? max : Mathf.Max(1f, fallbackMaxHp); + float cur = ReadHp(actor); + _curHp = cur >= 0f ? cur : _maxHp; + + if (nameLabel != null) + { + string n = displayName; + if (string.IsNullOrEmpty(n)) n = actor != null ? actor.name : ""; + nameLabel.text = n; + nameLabel.gameObject.SetActive(showName && !string.IsNullOrEmpty(n)); + } + + ApplyLayout(); + ShowNow(); + SetRatio(_maxHp > 0f ? _curHp / _maxHp : 0f); + return "bound name=" + (nameLabel != null ? nameLabel.text : "-") + + " maxHp=" + _maxHp.ToString("F0") + " ratio=" + LastRatio.ToString("F3"); + } + + /// 바 채움 비율(0~1). 눈금은 고정이고 fill 만 움직인다. + public void SetRatio(float ratio) + { + LastRatio = Mathf.Clamp01(ratio); + if (fill != null) fill.fillAmount = LastRatio; + } + + private void ShowNow() + { + if (group == null) return; + group.alpha = 1f; group.blocksRaycasts = false; group.interactable = false; + } + + private void HideNow() + { + _bound = false; _boss = null; _hideAt = -1f; + if (group == null) return; + group.alpha = 0f; group.blocksRaycasts = false; group.interactable = false; + } + + // ── 구성 · 배치 ─────────────────────────────────────────────────────── + /// 없는 자식만 만든다(에디터 오소링과 런타임이 같은 코드를 쓴다). 이미 있으면 아무 것도 하지 않는다. + public bool BuildIfNeeded() + { + if (_rt == null) _rt = GetComponent(); + bool made = false; + + if (group == null) { group = GetComponent(); if (group == null) { group = gameObject.AddComponent(); made = true; } } + + if (bar == null) + { + bar = NewChild(_rt, "Bar"); + made = true; + } + if (background == null) + { + var bg = NewChild(bar, "Bg"); + Stretch(bg); + background = bg.gameObject.AddComponent(); + background.raycastTarget = false; + made = true; + } + if (fill == null) + { + var f = NewChild(bar, "Fill"); + Stretch(f); + fill = f.gameObject.AddComponent(); + fill.raycastTarget = false; + fill.type = Image.Type.Filled; + fill.fillMethod = Image.FillMethod.Horizontal; + fill.fillOrigin = (int)Image.OriginHorizontal.Left; + fill.fillAmount = 1f; + made = true; + } + if (tickRoot == null) + { + tickRoot = NewChild(bar, "Ticks"); + Stretch(tickRoot); + made = true; + } + if (nameLabel == null) + { + var n = NewChild(_rt, "Name"); + nameLabel = n.gameObject.AddComponent(); + nameLabel.raycastTarget = false; + nameLabel.alignment = TextAlignmentOptions.Center; + nameLabel.text = ""; + if (nameFont != null) nameLabel.font = nameFont; + made = true; + } + return made; + } + + /// 설정 값(px)을 캔버스 유닛으로 환산해 바·눈금·색을 맞춘다. + public string ApplyLayout() + { + if (_rt == null) _rt = GetComponent(); + var s = WLHudLayoutSettings.Instance; + if (s == null) return "WLHudLayoutSettings 에셋 없음 — 배치 건너뜀"; + if (bar == null) return "구성 요소 없음 — BuildIfNeeded 먼저"; + + float u = ComputeUnitsPerPx(s); + + // 루트 = Safe Area 상단 가로 스트레치 (부모는 SafeAreaFitter 가 붙은 WL_HUD) + _rt.anchorMin = new Vector2(0.5f, 1f); + _rt.anchorMax = new Vector2(0.5f, 1f); + _rt.pivot = new Vector2(0.5f, 1f); + _rt.anchoredPosition = new Vector2(0f, -s.bossBarTopMarginPx * u); + float nameH = s.bossBarNameFontPx + s.bossBarNameGapPx; + _rt.sizeDelta = new Vector2(s.bossBarSizePx.x * u, (s.bossBarSizePx.y + nameH) * u); + _rt.localScale = Vector3.one; + + if (nameLabel != null) + { + var nrt = nameLabel.rectTransform; + nrt.anchorMin = new Vector2(0f, 1f); nrt.anchorMax = new Vector2(1f, 1f); + nrt.pivot = new Vector2(0.5f, 1f); + nrt.offsetMin = new Vector2(0f, 0f); nrt.offsetMax = new Vector2(0f, 0f); + nrt.anchoredPosition = Vector2.zero; + nrt.sizeDelta = new Vector2(0f, s.bossBarNameFontPx * u); + nameLabel.fontSize = s.bossBarNameFontPx * u; + nameLabel.color = s.bossBarNameColor; + if (nameFont != null && nameLabel.font != nameFont) nameLabel.font = nameFont; + } + + bar.anchorMin = new Vector2(0f, 0f); bar.anchorMax = new Vector2(1f, 0f); + bar.pivot = new Vector2(0.5f, 0f); + bar.anchoredPosition = Vector2.zero; + bar.sizeDelta = new Vector2(0f, s.bossBarSizePx.y * u); + + if (background != null) background.color = s.bossBarBgColor; + if (fill != null) fill.color = s.bossBarFillColor; + + RebuildTicks(s, u); + return "layout size=" + _rt.sizeDelta + " unitsPerPx=" + u.ToString("F4") + + " ticks=" + (s.bossPhaseTicks != null ? s.bossPhaseTicks.Length : 0); + } + + private void RebuildTicks(WLHudLayoutSettings s, float u) + { + if (tickRoot == null) return; + int want = s.bossPhaseTicks != null ? s.bossPhaseTicks.Length : 0; + + // 남는 눈금은 지운다(설정에서 개수를 줄였을 때). + for (int i = tickRoot.childCount - 1; i >= want; i--) + { + var extra = tickRoot.GetChild(i).gameObject; + if (Application.isPlaying) Destroy(extra); else DestroyImmediate(extra); + } + for (int i = tickRoot.childCount; i < want; i++) + { + var t = NewChild(tickRoot, "Tick_" + i); + var img = t.gameObject.AddComponent(); + img.raycastTarget = false; + } + for (int i = 0; i < want; i++) + { + var t = tickRoot.GetChild(i) as RectTransform; + if (t == null) continue; + float at = Mathf.Clamp01(s.bossPhaseTicks[i]); + t.anchorMin = new Vector2(at, 0f); + t.anchorMax = new Vector2(at, 1f); + t.pivot = new Vector2(0.5f, 0.5f); + t.anchoredPosition = Vector2.zero; + t.sizeDelta = new Vector2(s.bossBarTickWidthPx * u, 0f); + t.localScale = Vector3.one; + var img = t.GetComponent(); + if (img != null) img.color = s.bossBarTickColor; + } + } + + /// px → 캔버스 유닛 계수. 산식은 WLHudLayoutSettings 가 갖는다(전투 패드와 같은 것을 쓴다). + public float ComputeUnitsPerPx(WLHudLayoutSettings s) + { + if (_canvas == null) _canvas = GetComponentInParent(true); + return s.UnitsPerPx(_canvas); + } + + private static RectTransform NewChild(RectTransform parent, string childName) + { + var go = new GameObject(childName, typeof(RectTransform)); + go.layer = parent != null ? parent.gameObject.layer : 5; + var rt = (RectTransform)go.transform; + rt.SetParent(parent, false); + rt.localScale = Vector3.one; + return rt; + } + + private static void Stretch(RectTransform rt) + { + rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one; + rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero; + rt.pivot = new Vector2(0.5f, 0.5f); + } + + /// 에디터가 폰트를 주입한다(런타임 로드 없음). + public void SetNameFont(TMP_FontAsset font) + { + nameFont = font; + if (nameLabel != null && font != null) nameLabel.font = font; + } + + // ── 검증 전용 (발주서 ⓒ) ───────────────────────────────────────────── + /// + /// 가짜 전투 이벤트를 **실제 CombatEvents 경로로** 발행한다(Assembly-CSharp 내부라 Dispatch 접근 가능). + /// Play 없이 구독 → 갱신을 확인하는 용도. actor 가 없으므로 페이로드의 actor/victim 은 null 이다. + /// + public static float DebugFallbackMaxHp = 100f; // 합성 이벤트 전용(actor 가 없을 때의 최대 HP) + + public static string RaiseFakeBossSpawned(float maxHp) + { + DebugFallbackMaxHp = Mathf.Max(1f, maxHp); + var e = new SpawnedEvent { actor = null, isBoss = true, isElite = false, position = Vector3.zero, time = Time.unscaledTime, frame = Time.frameCount }; + CombatEvents.Spawned.Dispatch(in e); + return "dispatched Spawned(isBoss=true) subscribers=" + CombatEvents.Spawned.Count; + } + + /// 비보스 스폰 — 바가 반응하지 않아야 한다(회귀 확인). + public static string RaiseFakeNonBossSpawned() + { + var e = new SpawnedEvent { actor = null, isBoss = false, isElite = false, position = Vector3.zero, time = Time.unscaledTime, frame = Time.frameCount }; + CombatEvents.Spawned.Dispatch(in e); + return "dispatched Spawned(isBoss=false) subscribers=" + CombatEvents.Spawned.Count; + } + + public static string RaiseFakeHit(double damage) + { + var e = new HitConfirmedEvent { victim = null, attacker = null, dinfo = null, damage = damage, critical = false, isKill = false, byMainPC = true, time = Time.unscaledTime, frame = Time.frameCount }; + CombatEvents.HitConfirmed.Dispatch(in e); + return "dispatched HitConfirmed(dmg=" + damage + ") subscribers=" + CombatEvents.HitConfirmed.Count; + } + + public static string RaiseFakeKilled() + { + var e = new KilledEvent { victim = null, killer = null, id = 0, position = Vector3.zero, byDirectHit = true, time = Time.unscaledTime, frame = Time.frameCount }; + CombatEvents.Killed.Dispatch(in e); + return "dispatched Killed subscribers=" + CombatEvents.Killed.Count; + } + + /// 현재 상태 덤프. 발주서 ⓒ 증거용. + public string Dump() + { + var sb = new StringBuilder(); + if (_rt == null) _rt = GetComponent(); + var s = WLHudLayoutSettings.Instance; + sb.AppendLine("[BossHpBar] subscribed=" + _subscribed + + " visible=" + Visible + " ratio=" + LastRatio.ToString("F3") + + " hp=" + _curHp.ToString("F0") + "/" + _maxHp.ToString("F0") + + " seen(Spawned/Hit/Killed)=" + SpawnedSeen + "/" + HitSeen + "/" + KilledSeen); + sb.AppendLine(" CombatEvents subscribers Spawned=" + CombatEvents.Spawned.Count + + " HitConfirmed=" + CombatEvents.HitConfirmed.Count + + " Killed=" + CombatEvents.Killed.Count + " Enabled=" + CombatEvents.Enabled); + sb.AppendLine(" root " + (_rt != null ? "pos" + _rt.anchoredPosition + " size" + _rt.sizeDelta + " aMin" + _rt.anchorMin + " aMax" + _rt.anchorMax : "(없음)")); + sb.AppendLine(" bar " + (bar != null ? "size" + bar.sizeDelta + " aMin" + bar.anchorMin + " aMax" + bar.anchorMax : "(없음)") + + " fill=" + (fill != null ? fill.fillAmount.ToString("F3") : "-") + + " ticks=" + (tickRoot != null ? tickRoot.childCount : 0)); + if (tickRoot != null) + for (int i = 0; i < tickRoot.childCount; i++) + { + var t = tickRoot.GetChild(i) as RectTransform; + if (t != null) sb.AppendLine(" " + t.name + " at=" + t.anchorMin.x.ToString("F3") + " w=" + t.sizeDelta.x.ToString("F1")); + } + sb.AppendLine(" 설정 " + (s == null ? "(에셋 없음)" : + "sizePx=" + s.bossBarSizePx + " topMarginPx=" + s.bossBarTopMarginPx + + " ticks=" + (s.bossPhaseTicks != null ? string.Join(",", System.Array.ConvertAll(s.bossPhaseTicks, v => v.ToString("F2"))) : "-"))); + return sb.ToString(); + } + } +} diff --git a/Assets/WL/UI/Scripts/BossHpBar.cs.meta b/Assets/WL/UI/Scripts/BossHpBar.cs.meta new file mode 100644 index 000000000..11ccb9e37 --- /dev/null +++ b/Assets/WL/UI/Scripts/BossHpBar.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5759d44fea68d204697e4b572af27bde \ No newline at end of file diff --git a/Assets/WL/UI/Scripts/WLBattlePadLayout.cs b/Assets/WL/UI/Scripts/WLBattlePadLayout.cs new file mode 100644 index 000000000..1de73d039 --- /dev/null +++ b/Assets/WL/UI/Scripts/WLBattlePadLayout.cs @@ -0,0 +1,275 @@ +// ───────────────────────────────────────────────────────────────────────────── +// WLBattlePadLayout.cs — 전투 패드(우하단 엄지 부채꼴) 배치 드라이버 (WL-813c · #813) +// +// 기준서 v1 §F-1 권고: 오른손 엄지 부채꼴 **4슬롯** + 물약 1 + 회피 1 · 좌우 미러 옵션. +// §B 요소 3: 버튼 지름 132~150 px · 중심 간격 ≥16 px · 요소 7: Safe Area 안 · 한 손 도달. +// +// ■ 설계 +// · 원본 BattleUI.prefab 의 **배열을 줄이지 않는다**(skillCards 6 그대로 · 원복 가능). +// 활성 슬롯 수만 설정으로 정하고, 남는 슬롯은 물약/회피(813j/813r) 자리로 **비활성**으로 둔다. +// · 값은 전부 WLHudLayoutSettings(에셋). 이 파일에 좌표·지름·각도 상수는 없다(C45). +// · 배치는 런타임에만 한다([ExecuteAlways] 아님) — 프리팹 YAML 에 좌표를 굽지 않아 +// NewGameUI.prefab diff 를 "노드/컴포넌트 추가"로만 유지한다(발주서 ⓓ). +// · 이 컴포넌트는 **NewGameUI 안 IngameUIs/BattleUI 인스턴스에만** 붙인다. +// 같은 BattleUI.prefab 을 쓰는 SkillUI/SkillEquipUI 인스턴스는 손대지 않는다(회귀 0). +// +// ■ Safe Area +// 같은 오브젝트의 SafeAreaFitter(802b)가 루트를 안전 영역에 맞춘다. 이 스크립트는 루트 rect 를 +// 건드리지 않고(피터가 주인) 자식만 **우하단(미러 시 좌하단) 모서리 앵커**로 배치한다. +// 피터가 없으면(단독 테스트) 루트를 스트레치로 정규화한다. +// +// ■ 롤백(C8) +// 설정 에셋이 없거나 battlePadEnabled=false 면 아무 것도 만지지 않는다 = 원본 배치 그대로. +// +// 🔴 어셈블리 주의: Assets/WL/UI/ 에 .asmdef 를 만들지 말 것(Assembly-CSharp 단일). +// ───────────────────────────────────────────────────────────────────────────── + +using System.Text; +using UnityEngine; +using UnityEngine.UI; + +namespace WL.UI +{ + [DisallowMultipleComponent] + [RequireComponent(typeof(RectTransform))] + public class WLBattlePadLayout : MonoBehaviour + { + [Tooltip("배치 대상 — 비우면 같은 오브젝트의 BattleUI 를 찾는다")] + [SerializeField] private BattleUI target; + + [Tooltip("자동전투 토글 오브젝트 이름(BattleUI 자식) — 못 찾으면 건너뛴다")] + [SerializeField] private string autoButtonName = "btn_auto"; + + [Tooltip("공격 버튼 오브젝트 이름(BattleUI 자식) — 못 찾으면 건너뛴다")] + [SerializeField] private string attackButtonName = "Attack"; + + private RectTransform _rt; + private Canvas _canvas; + private bool _lastMirror; + private Vector2Int _lastScreen; + + // 마지막 적용 결과 — 프로브(에디터)가 읽는 진단 값. 실측 외에는 채우지 않는다. + public float LastUnitsPerPx { get; private set; } + public float LastMaxReachPx { get; private set; } + public float LastMinGapPx { get; private set; } + public int LastActiveSlots { get; private set; } + public int LastReserveSlots { get; private set; } + + private void Awake() { _rt = GetComponent(); } + private void OnEnable() { Apply(); } + + private void Update() + { + // 해상도 전환·설정 토글에만 다시 배치한다(매 프레임 계산 없음). + var s = WLHudLayoutSettings.Instance; + if (s == null) return; + if (Screen.width != _lastScreen.x || Screen.height != _lastScreen.y || s.mirrorLeftHanded != _lastMirror) + Apply(); + } + + /// BattleUI.Set() 직후 훅 — 서버 프리셋이 슬롯을 다시 채운 뒤 배치를 되살린다. + public static void NotifySet(BattleUI ui) + { + if (ui == null) return; + var layout = ui.GetComponent(); + if (layout != null && layout.isActiveAndEnabled) layout.Apply(); + } + + /// 설정 값대로 슬롯·공격·자동 버튼을 재배치한다. 설정이 없으면 아무 것도 하지 않는다. + public string Apply() + { + if (_rt == null) _rt = GetComponent(); + if (target == null) target = GetComponent(); + + var s = WLHudLayoutSettings.Instance; + if (s == null) return "WLHudLayoutSettings 에셋 없음 — 배치 건너뜀(원본 유지)"; + if (!s.battlePadEnabled) return "battlePadEnabled=false — 배치 건너뜀(원본 유지)"; + if (target == null) return "BattleUI 없음 — 배치 건너뜀"; + + _lastScreen = new Vector2Int(Screen.width, Screen.height); + _lastMirror = s.mirrorLeftHanded; + + // Safe Area 는 SafeAreaFitter 가 주인 — 없을 때만 루트를 스트레치로 정규화한다. + if (GetComponent() == null) + { + _rt.anchorMin = Vector2.zero; _rt.anchorMax = Vector2.one; + _rt.offsetMin = Vector2.zero; _rt.offsetMax = Vector2.zero; + } + + float u = ComputeUnitsPerPx(s); + LastUnitsPerPx = u; + + var cards = target.skillCards; + int total = cards != null ? cards.Length : 0; + int active = Mathf.Clamp(s.activeSkillSlots, 0, total); + LastActiveSlots = active; + LastReserveSlots = Mathf.Max(0, total - active); + + float maxReach = 0f; + var centers = new Vector2[total]; + + for (int i = 0; i < total; i++) + { + var card = cards[i]; + if (card == null) continue; + var crt = card.transform as RectTransform; + if (crt == null) continue; + + bool isSkill = i < active; + Vector2 px = isSkill ? s.FanSlotPx(i) : s.ReserveSlotPx(i - active); + float dia = isSkill ? s.slotDiameterPx : s.reserveDiameterPx; + + PlaceFromCorner(crt, px, dia, u, s.mirrorLeftHanded); + centers[i] = px; + maxReach = Mathf.Max(maxReach, px.magnitude + dia * 0.5f); + + // 4 활성 · 나머지는 물약/회피(813j/813r) 예약 자리 = 비활성 + if (card.gameObject.activeSelf != isSkill) card.gameObject.SetActive(isSkill); + } + + // 공격 버튼 · 자동전투 토글 + var attack = FindChild(attackButtonName); + if (attack != null) + { + PlaceFromCorner(attack, s.attackCenterPx, s.attackDiameterPx, u, s.mirrorLeftHanded); + maxReach = Mathf.Max(maxReach, s.attackCenterPx.magnitude + s.attackDiameterPx * 0.5f); + } + var auto = FindChild(autoButtonName); + if (auto != null) + { + PlaceFromCorner(auto, s.autoButtonCenterPx, s.autoButtonDiameterPx, u, s.mirrorLeftHanded); + maxReach = Mathf.Max(maxReach, s.autoButtonCenterPx.magnitude + s.autoButtonDiameterPx * 0.5f); + } + + LastMaxReachPx = maxReach; + LastMinGapPx = MinGapPx(s, centers, active, total); + + return "applied slots=" + active + "/" + total + " unitsPerPx=" + u.ToString("F4") + + " maxReachPx=" + maxReach.ToString("F1") + " minGapPx=" + LastMinGapPx.ToString("F1") + + " mirror=" + s.mirrorLeftHanded; + } + + /// 모서리(우하단 · 미러면 좌하단) 기준 (dx, dy) px 로 배치한다. + private static void PlaceFromCorner(RectTransform rt, Vector2 px, float diameterPx, float unitsPerPx, bool mirror) + { + Vector2 anchor = new Vector2(mirror ? 0f : 1f, 0f); + rt.anchorMin = anchor; + rt.anchorMax = anchor; + rt.pivot = new Vector2(0.5f, 0.5f); + float sx = mirror ? px.x : -px.x; + rt.anchoredPosition = new Vector2(sx * unitsPerPx, px.y * unitsPerPx); + rt.sizeDelta = new Vector2(diameterPx * unitsPerPx, diameterPx * unitsPerPx); + rt.localScale = Vector3.one; + } + + /// 이웃 슬롯 중심 거리 − 지름 = 실제 여백(px). 기준서 §B 요소3 ≥16 검증용. + private static float MinGapPx(WLHudLayoutSettings s, Vector2[] centers, int active, int total) + { + float min = float.MaxValue; + for (int i = 0; i < total; i++) + { + for (int j = i + 1; j < total; j++) + { + float ri = (i < active ? s.slotDiameterPx : s.reserveDiameterPx) * 0.5f; + float rj = (j < active ? s.slotDiameterPx : s.reserveDiameterPx) * 0.5f; + float gap = Vector2.Distance(centers[i], centers[j]) - ri - rj; + if (gap < min) min = gap; + } + } + return min == float.MaxValue ? 0f : min; + } + + /// px → 캔버스 유닛 계수. 산식은 WLHudLayoutSettings 가 갖는다(보스 바와 같은 것을 쓴다). + public float ComputeUnitsPerPx(WLHudLayoutSettings s) + { + if (_canvas == null) _canvas = GetComponentInParent(true); + return s.UnitsPerPx(_canvas); + } + + private RectTransform FindChild(string childName) + { + if (string.IsNullOrEmpty(childName)) return null; + for (int i = 0; i < transform.childCount; i++) + { + var c = transform.GetChild(i); + if (c.name == childName) return c as RectTransform; + } + return null; + } + + // ── 검증·디버그 전용 ────────────────────────────────────────────────── + /// 런타임 미러 토글(로드된 설정 인스턴스만 바꾼다 — .asset 파일은 건드리지 않는다). + public string SetMirror(bool mirror) + { + var s = WLHudLayoutSettings.Instance; + if (s == null) return "WLHudLayoutSettings 에셋 없음"; + s.mirrorLeftHanded = mirror; + return Apply(); + } + + /// 현재 배치 덤프(슬롯별 앵커·좌표·크기 · px 환산). 발주서 ⓑ 증거용. + public string Dump() + { + var sb = new StringBuilder(); + var s = WLHudLayoutSettings.Instance; + float u = LastUnitsPerPx > 0f ? LastUnitsPerPx : (s != null ? ComputeUnitsPerPx(s) : 1f); + sb.AppendLine("[WLBattlePadLayout] unitsPerPx=" + u.ToString("F4") + + " mirror=" + (s != null ? s.mirrorLeftHanded.ToString() : "?") + + " activeSlots=" + LastActiveSlots + "/" + (LastActiveSlots + LastReserveSlots) + + " maxReachPx=" + LastMaxReachPx.ToString("F1") + + " (목표 " + (s != null ? s.thumbReachRadiusPx.ToString("F0") : "?") + ")" + + " minGapPx=" + LastMinGapPx.ToString("F1")); + sb.AppendLine(" root " + RectLine(_rt != null ? _rt : GetComponent()) + + " safeAreaFitter=" + (GetComponent() != null)); + + var ui = target != null ? target : GetComponent(); + if (ui != null && ui.skillCards != null) + { + for (int i = 0; i < ui.skillCards.Length; i++) + { + var c = ui.skillCards[i]; + if (c == null) { sb.AppendLine(" slot" + i + " : null"); continue; } + var crt = c.transform as RectTransform; + sb.AppendLine(" slot" + i + " " + c.name + " active=" + c.gameObject.activeSelf + + " " + RectLine(crt) + " " + PxLine(crt, u)); + sb.AppendLine(" 쿨링 " + CoolRingLine(c)); + } + } + foreach (var n in new[] { attackButtonName, autoButtonName, "Avoid", "Equip" }) + { + var t = FindChild(n); + sb.AppendLine(" " + n + (t == null ? " : 없음" + : " active=" + t.gameObject.activeSelf + " " + RectLine(t) + " " + PxLine(t, u))); + } + return sb.ToString(); + } + + /// 쿨 링 실측 — SkillCard 원본의 i_cooltime(Filled/Radial360)이 배치 뒤에도 유효한지(발주서 ⓑ "쿨 링"). + private static string CoolRingLine(SkillCard card) + { + var img = card != null ? card.sprite_cooltime : null; + if (img == null) return "sprite_cooltime 없음"; + var irt = img.rectTransform; + return img.gameObject.name + " type=" + img.type + " fillMethod=" + img.fillMethod + + " origin=" + img.fillOrigin + " cw=" + img.fillClockwise + " amount=" + img.fillAmount.ToString("F3") + + " active=" + img.gameObject.activeInHierarchy + + " aMin" + irt.anchorMin + " aMax" + irt.anchorMax + " size" + irt.sizeDelta + + " · 구동 = PCActor.cs:91 Set_CoolTime → CardCoolTimeBase.Set_CoolTimeText"; + } + + private static string RectLine(RectTransform rt) + { + if (rt == null) return "(rt 없음)"; + return "aMin" + rt.anchorMin + " aMax" + rt.anchorMax + " pos" + rt.anchoredPosition + + " size" + rt.sizeDelta + " piv" + rt.pivot + " scl" + (Vector2)(Vector3)rt.localScale; + } + + private static string PxLine(RectTransform rt, float u) + { + if (rt == null || u <= 0f) return ""; + return "→ px(dx=" + Mathf.Abs(rt.anchoredPosition.x / u).ToString("F1") + + ", dy=" + (rt.anchoredPosition.y / u).ToString("F1") + + ", d=" + (rt.sizeDelta.x / u).ToString("F1") + ")"; + } + } +} diff --git a/Assets/WL/UI/Scripts/WLBattlePadLayout.cs.meta b/Assets/WL/UI/Scripts/WLBattlePadLayout.cs.meta new file mode 100644 index 000000000..23f32d345 --- /dev/null +++ b/Assets/WL/UI/Scripts/WLBattlePadLayout.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d9504994cdfd4f44f9aa7b4a6eaeb36f \ No newline at end of file diff --git a/Assets/WL/UI/Settings.meta b/Assets/WL/UI/Settings.meta new file mode 100644 index 000000000..d46343443 --- /dev/null +++ b/Assets/WL/UI/Settings.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f4aeda4b4f3437e46a49beefb0fd6755 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/WL/UI/Settings/Resources.meta b/Assets/WL/UI/Settings/Resources.meta new file mode 100644 index 000000000..ef3d47031 --- /dev/null +++ b/Assets/WL/UI/Settings/Resources.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 71749f48b532a4a48b420ea15652dc92 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/WL/UI/Settings/Resources/WL.meta b/Assets/WL/UI/Settings/Resources/WL.meta new file mode 100644 index 000000000..7810b4b42 --- /dev/null +++ b/Assets/WL/UI/Settings/Resources/WL.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0c51828552a79704da68e37718b523ab +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/WL/UI/Settings/Resources/WL/WLHudLayoutSettings.asset b/Assets/WL/UI/Settings/Resources/WL/WLHudLayoutSettings.asset new file mode 100644 index 000000000..ba63ad407 --- /dev/null +++ b/Assets/WL/UI/Settings/Resources/WL/WLHudLayoutSettings.asset @@ -0,0 +1,50 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 55115957ebcbb1e498a857d9f9bb17f8, type: 3} + m_Name: WLHudLayoutSettings + m_EditorClassIdentifier: Assembly-CSharp::WL.UI.WLHudLayoutSettings + designWidthPx: 1080 + designHeightPx: 1920 + battlePadEnabled: 1 + mirrorLeftHanded: 0 + activeSkillSlots: 4 + slotDiameterPx: 140 + slotMinGapPx: 16 + thumbReachRadiusPx: 320 + fanOriginPx: {x: 0, y: 0} + fanRadiusPx: 420 + fanStartAngleDeg: 12.5 + fanStepAngleDeg: 22 + reserveDiameterPx: 110 + reserveRadiusPx: 270 + reserveStartAngleDeg: 25 + reserveStepAngleDeg: 40 + attackCenterPx: {x: 100, y: 100} + attackDiameterPx: 132 + autoButtonCenterPx: {x: 56, y: 545} + autoButtonDiameterPx: 34.3 + joystickCenterPx: {x: 180, y: 260} + joystickRadiusPx: 90 + bossBarEnabled: 1 + bossBarSizePx: {x: 880, y: 28} + bossBarTopMarginPx: 190 + bossBarNameFontPx: 30 + bossBarNameGapPx: 8 + bossBarTickWidthPx: 4 + bossPhaseTicks: + - 0.7 + - 0.4 + bossBarHideDelaySec: 1.2 + bossBarFillColor: {r: 0.99, g: 0.26, b: 0.28, a: 1} + bossBarBgColor: {r: 0.05, g: 0.05, b: 0.06, a: 0.78} + bossBarTickColor: {r: 1, g: 1, b: 1, a: 0.85} + bossBarNameColor: {r: 1, g: 0.92, b: 0.6, a: 1} diff --git a/Assets/WL/UI/Settings/Resources/WL/WLHudLayoutSettings.asset.meta b/Assets/WL/UI/Settings/Resources/WL/WLHudLayoutSettings.asset.meta new file mode 100644 index 000000000..59d58c568 --- /dev/null +++ b/Assets/WL/UI/Settings/Resources/WL/WLHudLayoutSettings.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e66cc0e7d91be4f48b570e1691776ece +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/WL/UI/Settings/WLHudLayoutSettings.cs b/Assets/WL/UI/Settings/WLHudLayoutSettings.cs new file mode 100644 index 000000000..fe32a5c4e --- /dev/null +++ b/Assets/WL/UI/Settings/WLHudLayoutSettings.cs @@ -0,0 +1,199 @@ +// ───────────────────────────────────────────────────────────────────────────── +// WLHudLayoutSettings.cs — 전투 HUD 배치 값의 단일 출처 (WL-813c · #813) +// +// 기준서 v1 §B 요소 3(슬롯 수·배치·지름·간격) · 요소 6(상단 보스 HP 바·페이즈) · 요소 7(한 손 도달성) +// + §F-1 권고(엄지 부채꼴 4슬롯 + 물약 1 + 회피 1 · 좌우 미러 옵션). +// +// ■ 왜 이 에셋인가 (C45) +// 거리·지름·각도·임계 같은 값은 코드 상수로 두지 않는다. 이 파일에는 "필드와 기본값"만 있고, +// 실제로 게임이 읽는 값은 Resources/WL/WLHudLayoutSettings.asset 이다(PD·기획이 인스펙터에서 바꾼다). +// +// ■ 좌표계 (px) +// 모든 px 값은 **세로 1080×1920 기준 화면 픽셀**이다(designWidth/HeightPx). +// 전투 패드는 **Safe Area 의 우하단 모서리**를 원점으로 삼고 dx = 왼쪽 방향, dy = 위쪽 방향으로 잰다. +// 좌우 미러(mirrorLeftHanded)를 켜면 원점이 좌하단으로 바뀌고 dx 는 오른쪽 방향이 된다. +// 각도 deg 는 0° = dx 방향(화면 안쪽 가로), 90° = dy 방향(위쪽) 인 반시계 회전이다. +// +// px → 캔버스 유닛 환산은 코드가 런타임에 캔버스 폭에서 구한다(802b 노트 §2 · 세로에서 1 px = 1.7778 유닛). +// 환산 계수를 필드로 박지 않는 이유 = 해상도가 바뀌어도 같은 px 규격이 유지되게 하기 위해서다. +// +// ■ 「미확인」 · 알려진 규격 충돌 (완료보고 WL-813c §3 에 산술 근거) +// §B 요소 7 "버튼 전부 우하단 모서리 반경 ≤ 320 px" 은 §B 요소 3 "버튼 지름 132~150 px · 6개" 와 +// 기하학적으로 양립하지 않는다(반경 320 px 사분원 넓이 80,425 px² < 지름 140 px 원 6개 92,363 px²). +// → thumbReachRadiusPx 는 **목표값**으로 두고 실제 도달 반경은 813m 오버레이가 잰다. +// ───────────────────────────────────────────────────────────────────────────── + +using UnityEngine; +using UnityEngine.UI; + +namespace WL.UI +{ + [CreateAssetMenu(fileName = "WLHudLayoutSettings", menuName = "WL/HUD Layout Settings", order = 30)] + public sealed class WLHudLayoutSettings : ScriptableObject + { + /// Resources 경로 — 런타임 로드 키. + public const string ResourcesPath = "WL/WLHudLayoutSettings"; + + private static WLHudLayoutSettings s_instance; + private static bool s_tried; + + /// 에셋이 없으면 null 을 돌려준다(호출부는 null 이면 배치를 건너뛴다 = C8 롤백). + public static WLHudLayoutSettings Instance + { + get + { + if (s_instance == null && !s_tried) + { + s_tried = true; + s_instance = Resources.Load(ResourcesPath); + } + return s_instance; + } + } + + /// 에디터에서 에셋을 새로 만들거나 값을 바꾼 뒤 캐시를 버린다. + public static void ClearCache() { s_instance = null; s_tried = false; } + + // ── 기준 해상도 (CLAUDE.md §9 · 세로 1080×1920) ──────────────────────── + [Header("기준 해상도 (px)")] + [Tooltip("모든 px 값의 기준이 되는 화면 폭")] + public float designWidthPx = 1080f; + [Tooltip("모든 px 값의 기준이 되는 화면 높이")] + public float designHeightPx = 1920f; + + // ── 전투 패드 공통 ──────────────────────────────────────────────────── + [Header("전투 패드 — 공통")] + [Tooltip("끄면 WLBattlePadLayout 이 아무 것도 만지지 않는다(원본 배치 유지 · C8 롤백)")] + public bool battlePadEnabled = true; + [Tooltip("좌우 미러(왼손잡이) — 패드 원점이 좌하단으로 간다")] + public bool mirrorLeftHanded = false; + [Tooltip("활성 스킬 슬롯 수 — 기준서 §F-1 권고 4 (프리팹 배열은 6 그대로 · 나머지는 물약/회피 예약)")] + public int activeSkillSlots = 4; + [Tooltip("스킬 버튼 지름 — 기준서 §B 요소3 권고 132~150")] + public float slotDiameterPx = 140f; + [Tooltip("버튼 사이 최소 여백 — 기준서 §B 요소3 ≥16 (검증용 · 배치는 아래 각도/반경이 만든다)")] + public float slotMinGapPx = 16f; + [Tooltip("엄지 도달 반경 목표 — 기준서 §B 요소7 ≤320. 실제 도달 반경은 813m 오버레이가 잰다")] + public float thumbReachRadiusPx = 320f; + + // ── 스킬 부채꼴 (우하단 모서리 원점) ────────────────────────────────── + [Header("전투 패드 — 스킬 부채꼴")] + [Tooltip("부채꼴 중심 (모서리 기준 dx, dy)")] + public Vector2 fanOriginPx = new Vector2(0f, 0f); + [Tooltip("부채꼴 중심 → 슬롯 중심 거리")] + public float fanRadiusPx = 420f; + [Tooltip("첫 슬롯 각도 (0° = 가로 안쪽 · 90° = 위)")] + public float fanStartAngleDeg = 12.5f; + [Tooltip("슬롯 사이 각도")] + public float fanStepAngleDeg = 22f; + + // ── 예약 슬롯 (물약 · 회피 · 813j/813r) ─────────────────────────────── + [Header("전투 패드 — 예약 슬롯(물약·회피 · 비활성)")] + [Tooltip("예약 슬롯 지름 — 스킬보다 작게 두어 부채꼴 안쪽에 자리를 만든다")] + public float reserveDiameterPx = 110f; + [Tooltip("예약 슬롯 반경 (부채꼴 중심 기준)")] + public float reserveRadiusPx = 270f; + [Tooltip("첫 예약 슬롯 각도")] + public float reserveStartAngleDeg = 25f; + [Tooltip("예약 슬롯 사이 각도")] + public float reserveStepAngleDeg = 40f; + + // ── 공격 · 자동 버튼 ────────────────────────────────────────────────── + [Header("전투 패드 — 공격 · 자동")] + [Tooltip("공격 버튼 중심 (모서리 기준)")] + public Vector2 attackCenterPx = new Vector2(100f, 100f); + [Tooltip("공격 버튼 지름 — 원본 235 유닛 = 132 px")] + public float attackDiameterPx = 132f; + [Tooltip("자동전투 토글 중심 (모서리 기준)")] + public Vector2 autoButtonCenterPx = new Vector2(56f, 545f); + [Tooltip("자동전투 토글 지름 — 원본 61 유닛 = 34.3 px")] + public float autoButtonDiameterPx = 34.3f; + + // ── 조이스틱 규격 (§B 요소7 · 값만 확정 · 배치는 별도 태스크) ───────── + [Header("조이스틱 규격 (값만 확정 · 813c 는 이동시키지 않는다)")] + [Tooltip("좌하단 모서리 기준 조이스틱 중심")] + public Vector2 joystickCenterPx = new Vector2(180f, 260f); + [Tooltip("조이스틱 반경")] + public float joystickRadiusPx = 90f; + + // ── 상단 보스 HP 바 ─────────────────────────────────────────────────── + [Header("보스 HP 바 (상단)")] + [Tooltip("끄면 바를 만들지도 켜지도 않는다")] + public bool bossBarEnabled = true; + [Tooltip("바 크기 (px)")] + public Vector2 bossBarSizePx = new Vector2(880f, 28f); + [Tooltip("Safe Area 상단에서 바 위쪽까지 여백 — 기존 WL_HUD/HUD_Currency(상단 −130.5~−176.1 px 실측) 아래에 둔다")] + public float bossBarTopMarginPx = 190f; + [Tooltip("이름 라벨 글자 크기")] + public float bossBarNameFontPx = 30f; + [Tooltip("이름 라벨과 바 사이 간격")] + public float bossBarNameGapPx = 8f; + [Tooltip("페이즈 눈금 굵기")] + public float bossBarTickWidthPx = 4f; + [Tooltip("페이즈 눈금 위치(HP 비율) — 기준서 §B 요소6 임계 100/70/40 중 전환점 2개. 전투 임계값 자체는 813h(Gameplay) 소유")] + public float[] bossPhaseTicks = new float[] { 0.7f, 0.4f }; + [Tooltip("처치 후 바가 사라지기까지 시간(초)")] + public float bossBarHideDelaySec = 1.2f; + [Tooltip("바 채움 색")] + public Color bossBarFillColor = new Color(0.99f, 0.26f, 0.28f, 1f); + [Tooltip("바 배경 색")] + public Color bossBarBgColor = new Color(0.05f, 0.05f, 0.06f, 0.78f); + [Tooltip("페이즈 눈금 색")] + public Color bossBarTickColor = new Color(1f, 1f, 1f, 0.85f); + [Tooltip("이름 라벨 색")] + public Color bossBarNameColor = new Color(1f, 0.92f, 0.6f, 1f); + + // ── 유틸 ────────────────────────────────────────────────────────────── + /// + /// px → 캔버스 유닛 환산 계수. + /// + /// 1순위 = CanvasScaler 산식 재현. 기준 해상도(designWidth/HeightPx)에서 이 캔버스가 갖는 폭(유닛)을 + /// 계산한다. 세로에서는 폭이 해상도와 무관하게 고정이라(1080×1920·Expand·참조 1920×1080 → 1920 유닛) + /// px 는 진짜 "설계 단위"가 되고, 에디트 모드(레이아웃 미실행)에서도 같은 값이 나온다. + /// 2순위 = 실제 캔버스 rect 실측(스케일러가 없거나 고정 픽셀 모드일 때). + /// 계수를 코드 상수로 박지 않는다 — 스케일러 설정이 바뀌면 따라간다(802b 노트: 세로에서 1.7778). + /// + public float UnitsPerPx(Canvas canvas) + { + if (designWidthPx <= 0f || designHeightPx <= 0f) return 1f; + + var scaler = canvas != null ? canvas.GetComponent() : null; + if (scaler != null && scaler.uiScaleMode == CanvasScaler.ScaleMode.ScaleWithScreenSize) + { + Vector2 r = scaler.referenceResolution; + if (r.x > 0f && r.y > 0f) + { + float w = designWidthPx, h = designHeightPx, scale; + switch (scaler.screenMatchMode) + { + case CanvasScaler.ScreenMatchMode.Expand: scale = Mathf.Min(w / r.x, h / r.y); break; + case CanvasScaler.ScreenMatchMode.Shrink: scale = Mathf.Max(w / r.x, h / r.y); break; + default: scale = Mathf.Pow(w / r.x, 1f - scaler.matchWidthOrHeight) * + Mathf.Pow(h / r.y, scaler.matchWidthOrHeight); break; + } + if (scale > 0f) return (w / scale) / designWidthPx; + } + } + + var canvasRect = canvas != null ? canvas.transform as RectTransform : null; + if (canvasRect != null && canvasRect.rect.width > 0f) return canvasRect.rect.width / designWidthPx; + return 1f; + } + + /// 부채꼴 각도 → 모서리 기준 (dx, dy) px. + public Vector2 FanSlotPx(int index) + { + float deg = fanStartAngleDeg + fanStepAngleDeg * index; + float rad = deg * Mathf.Deg2Rad; + return fanOriginPx + new Vector2(Mathf.Cos(rad), Mathf.Sin(rad)) * fanRadiusPx; + } + + /// 예약(물약·회피) 슬롯 각도 → 모서리 기준 (dx, dy) px. + public Vector2 ReserveSlotPx(int index) + { + float deg = reserveStartAngleDeg + reserveStepAngleDeg * index; + float rad = deg * Mathf.Deg2Rad; + return fanOriginPx + new Vector2(Mathf.Cos(rad), Mathf.Sin(rad)) * reserveRadiusPx; + } + } +} diff --git a/Assets/WL/UI/Settings/WLHudLayoutSettings.cs.meta b/Assets/WL/UI/Settings/WLHudLayoutSettings.cs.meta new file mode 100644 index 000000000..e71216260 --- /dev/null +++ b/Assets/WL/UI/Settings/WLHudLayoutSettings.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 55115957ebcbb1e498a857d9f9bb17f8 \ No newline at end of file