// WL-813tj (#813) — ① 설정 에셋 2종 생성 ② NewGameUI Safe Area 실효화(5레이어 재부모화) // ③ 보스 배너 · 피격 비네트 · 부활 팝업 · 물약 버튼 노드 추가 // 실행: // unity command run_script --file AgentScripts/WL813tj_Apply.cs --entry WL813tj_Apply.CreateSettings // unity command run_script --file AgentScripts/WL813tj_Apply.cs --entry WL813tj_Apply.Resave // unity command run_script --file AgentScripts/WL813tj_Apply.cs --entry WL813tj_Apply.ApplySafeArea // unity command run_script --file AgentScripts/WL813tj_Apply.cs --entry WL813tj_Apply.ApplyUi // unity command run_script --file AgentScripts/WL813tj_Apply.cs --entry WL813tj_Apply.Revert // 🔴 run_script 파일은 파일마다 독립 어셈블리 — 다른 AgentScript 상수 참조 불가(813c 교훈). // // ■ Safe Area 를 패널 **2개**로 나누는 이유 (실측 근거 · 완료보고 ⓒ) // NewGameUI 캔버스 직속 12레이어에는 Canvas/sortingOrder 가 하나도 없다(실측: 5레이어 전부 컴포넌트 1~2개, // Canvas 0개) → **그리는 순서 = 형제 순서**다. 대상 5레이어는 원본에서 // Common(4) · LobbyUIs(5) < IngameUIs(6) · FarmUIs(7) < MessageInfo(8) · Center(9) · PopupMgr(10) < TestMapUIMgr(11) // 로 **IngameUIs/FarmUIs 를 사이에 끼고 앞뒤로 갈라져 있다**. 5개를 패널 하나에 몰면 앞뒤 관계가 뒤집힌다 // (로비 UI 가 인게임 UI 위로 올라오거나, 팝업이 인게임 UI 아래로 내려간다). // 그래서 Back(Common·LobbyUIs) / Front(MessageInfo·Center·PopupMgr) 두 패널을 원래 자리에 끼워 // **평탄화하면 원본과 완전히 같은 순서**가 되게 했다. using System.Text; using UnityEditor; using UnityEngine; public static class WL813tj_Apply { public const string NewGameUIPath = "Assets/Res_Addr/MainUI/NewGameUI.prefab"; public const string SettingsDir = "Assets/WL/UI/Settings/Resources/WL"; public const string BossSettingsPath = SettingsDir + "/WLBossUiSettings.asset"; public const string SurvivalSettingsPath = SettingsDir + "/WLSurvivalUiSettings.asset"; public const string BackPanel = "WL_SafeArea_Back"; public const string FrontPanel = "WL_SafeArea_Front"; public static readonly string[] BackLayers = { "Common", "LobbyUIs" }; public static readonly string[] FrontLayers = { "MessageInfo", "Center", "PopupMgr" }; public const string HudPath = "IngameUIs/WL_HUD"; public const string PadPath = "IngameUIs/BattleUI"; public const string BannerName = "WL_BossBanner"; public const string HitName = "WL_HitVignette"; public const string ReviveName = "WL_ReviveDialog"; public const string PotionName = "WL_PotionButton"; const string TmpFontGuid = "a387e325271126742ad15569dba1ac57"; // 813c/813g 가 실측한 원본 TMP 폰트 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 Transform FindAnywhere(Transform root, string name) { for (int i = 0; i < root.childCount; i++) { var c = root.GetChild(i); if (c.name == name) return c; for (int j = 0; j < c.childCount; j++) if (c.GetChild(j).name == name) return c.GetChild(j); } return null; } 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; } } static RectTransform EnsureNode(Transform parent, string name, StringBuilder sb) { var t = parent.Find(name) as RectTransform; if (t == null) { var go = new GameObject(name, typeof(RectTransform)); go.layer = UILayer; t = (RectTransform)go.transform; t.SetParent((RectTransform)parent, false); sb.AppendLine(" " + parent.name + "/" + name + " : 노드 추가"); } else sb.AppendLine(" " + parent.name + "/" + name + " : 이미 있음"); return t; } static TMPro.TMP_FontAsset LoadFont(StringBuilder sb) { var p = AssetDatabase.GUIDToAssetPath(TmpFontGuid); var f = string.IsNullOrEmpty(p) ? null : AssetDatabase.LoadAssetAtPath(p); sb.AppendLine("폰트: " + (f != null ? p : "「미확인」 — TMP 기본 폰트로 떨어진다")); return f; } // ── ① 설정 에셋 ────────────────────────────────────────────────────────── /// 설정 에셋 2종 생성(이미 있으면 값 유지 · 덮어쓰지 않는다). public static object CreateSettings() { var sb = new StringBuilder(); EnsureFolder(SettingsDir); var boss = AssetDatabase.LoadAssetAtPath(BossSettingsPath); if (boss == null) { boss = ScriptableObject.CreateInstance(); AssetDatabase.CreateAsset(boss, BossSettingsPath); sb.AppendLine("생성: " + BossSettingsPath); } else sb.AppendLine("이미 있음(값 유지): " + BossSettingsPath); var sur = AssetDatabase.LoadAssetAtPath(SurvivalSettingsPath); if (sur == null) { sur = ScriptableObject.CreateInstance(); AssetDatabase.CreateAsset(sur, SurvivalSettingsPath); sb.AppendLine("생성: " + SurvivalSettingsPath); } else sb.AppendLine("이미 있음(값 유지): " + SurvivalSettingsPath); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); WL.UI.WLBossUiSettings.ClearCache(); WL.UI.WLSurvivalUiSettings.ClearCache(); sb.AppendLine("── 813t 보스 배너(기준서 §D-1 813t)"); sb.AppendLine(" banner=" + boss.bannerSeconds + "s topPx=" + boss.bannerTopMarginPx + " sizePx=" + boss.bannerSizePx + " title=\"" + boss.bannerTitleText + "\" punch=" + boss.bannerPunchScale); sb.AppendLine(" warn=" + boss.warnVignetteSeconds + "s thickPx=" + boss.warnVignetteThicknessPx + " pulses=" + boss.warnVignettePulses + " color=" + boss.warnVignetteColor); sb.AppendLine("── 813j 생존 UI(기준서 §D-1 813j · §B 요소 8)"); sb.AppendLine(" 물약 slot=" + sur.potionReserveSlotIndex + " fallback(count=" + sur.potionFallbackCount + " cd=" + sur.potionFallbackCooldownSeconds + "s) requireProvider=" + sur.potionRequireProvider); sb.AppendLine(" 부활 delay=" + sur.reviveDelaySeconds + "s 공용팝업=" + sur.reviveUseCommonPopup + " msg=\"" + sur.reviveMessage + "\" 비용=\"" + sur.reviveCostText + "\""); sb.AppendLine(" 피격 border=" + sur.hitBorderSeconds + "s/" + sur.hitBorderThicknessPx + "px target=" + sur.hitBorderTargetPath + " 무적무시=" + sur.ignoreInvincibleHits + " 스로틀=" + sur.hitMinIntervalSeconds + "s"); return sb.ToString(); } /// 무변경 재저장 — 프리팹 편집 전에 재직렬화 diff 가 생기는지 먼저 잰다(802b·813c 교훈). public static object Resave() { var root = PrefabUtility.LoadPrefabContents(NewGameUIPath); try { PrefabUtility.SaveAsPrefabAsset(root, NewGameUIPath); } finally { PrefabUtility.UnloadPrefabContents(root); } AssetDatabase.Refresh(); return NewGameUIPath + " : 무변경 재저장 (git diff 로 재직렬화 여부 확인)"; } // ── ② Safe Area 실효화 ─────────────────────────────────────────────────── static RectTransform MakePanel(Transform root, string name, int siblingIndex, StringBuilder sb) { var t = root.Find(name) as RectTransform; if (t == null) { var go = new GameObject(name, typeof(RectTransform)); go.layer = UILayer; t = (RectTransform)go.transform; t.SetParent((RectTransform)root, false); sb.AppendLine(name + " : 패널 추가"); } else sb.AppendLine(name + " : 이미 있음"); t.anchorMin = Vector2.zero; t.anchorMax = Vector2.one; t.offsetMin = Vector2.zero; t.offsetMax = Vector2.zero; t.pivot = new Vector2(0.5f, 0.5f); t.localScale = Vector3.one; t.localPosition = new Vector3(0f, 0f, 0f); if (t.GetComponent() == null) { t.gameObject.AddComponent(); sb.AppendLine(" SafeAreaFitter 부착"); } if (siblingIndex >= 0) t.SetSiblingIndex(Mathf.Min(siblingIndex, root.childCount - 1)); return t; } public static object ApplySafeArea() { var sb = new StringBuilder(); var root = PrefabUtility.LoadPrefabContents(NewGameUIPath); try { var rt = root.transform; sb.AppendLine("── 적용 전 캔버스 루트 순서"); sb.AppendLine(" " + Order(rt)); // Back 패널 = 원래 Common 이 있던 자리 int backAt = IndexOf(rt, BackLayers[0]); if (backAt < 0) return "🔴 " + BackLayers[0] + " 없음 — 중단"; var back = MakePanel(rt, BackPanel, backAt, sb); foreach (var n in BackLayers) Reparent(rt, n, back, sb); // Front 패널 = 원래 MessageInfo 가 있던 자리(Back 이동으로 인덱스가 당겨진 뒤 다시 잰다) int frontAt = IndexOf(rt, FrontLayers[0]); if (frontAt < 0) return "🔴 " + FrontLayers[0] + " 없음 — 중단"; var front = MakePanel(rt, FrontPanel, frontAt, sb); foreach (var n in FrontLayers) Reparent(rt, n, front, sb); sb.AppendLine("── 적용 후 캔버스 루트 순서(children=" + rt.childCount + ")"); sb.AppendLine(" " + Order(rt)); sb.AppendLine("── 평탄화(패널을 펼친 순서) = 원본과 같아야 한다"); sb.AppendLine(" " + Flatten(rt)); PrefabUtility.SaveAsPrefabAsset(root, NewGameUIPath); } finally { PrefabUtility.UnloadPrefabContents(root); } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); return sb.ToString(); } static int IndexOf(Transform parent, string name) { for (int i = 0; i < parent.childCount; i++) if (parent.GetChild(i).name == name) return i; return -1; } static void Reparent(Transform root, string name, RectTransform panel, StringBuilder sb) { var t = root.Find(name) as RectTransform; if (t == null) { if (panel.Find(name) != null) { sb.AppendLine(" " + name + " : 이미 " + panel.name + " 아래"); return; } sb.AppendLine(" 🔴 " + name + " 없음"); return; } var before = new { a0 = t.anchorMin, a1 = t.anchorMax, p = t.anchoredPosition, s = t.sizeDelta, sc = t.localScale, z = t.localPosition.z, pv = t.pivot }; t.SetParent(panel, false); // worldPositionStays=false → 로컬 값 그대로 보존 t.SetAsLastSibling(); bool same = before.a0 == t.anchorMin && before.a1 == t.anchorMax && before.p == t.anchoredPosition && before.s == t.sizeDelta && before.sc == t.localScale && Mathf.Approximately(before.z, t.localPosition.z) && before.pv == t.pivot; sb.AppendLine(" " + name + " → " + panel.name + " (보존=" + same + " aMin" + t.anchorMin + " aMax" + t.anchorMax + " off" + t.offsetMin + t.offsetMax + " scale" + t.localScale + " z=" + t.localPosition.z + ")"); } static string Order(Transform t) { var sb = new StringBuilder(); for (int i = 0; i < t.childCount; i++) { if (i > 0) sb.Append(" · "); sb.Append(i).Append(':').Append(t.GetChild(i).name); } return sb.ToString(); } static string Flatten(Transform t) { var sb = new StringBuilder(); int k = 0; for (int i = 0; i < t.childCount; i++) { var c = t.GetChild(i); if (c.name.StartsWith("WL_SafeArea")) { for (int j = 0; j < c.childCount; j++) { if (k++ > 0) sb.Append(" · "); sb.Append(c.GetChild(j).name); } } else { if (k++ > 0) sb.Append(" · "); sb.Append(c.name); } } return sb.ToString(); } // ── ③ UI 노드 ──────────────────────────────────────────────────────────── public static object ApplyUi() { var sb = new StringBuilder(); var boss = AssetDatabase.LoadAssetAtPath(BossSettingsPath); var sur = AssetDatabase.LoadAssetAtPath(SurvivalSettingsPath); if (boss == null || sur == null) return "🔴 설정 에셋 없음 — CreateSettings 먼저"; var font = LoadFont(sb); var root = PrefabUtility.LoadPrefabContents(NewGameUIPath); try { // ── 813t + 813j 화면 연출 = IngameUIs/WL_HUD (802b SafeAreaFitter 갈래 · 813c/813g 규칙) var hud = FindByPath(root.transform, HudPath); if (hud == null) sb.AppendLine("🔴 " + HudPath + " 없음"); else { var banner = EnsureNode(hud, BannerName, sb); var bc = banner.GetComponent(); if (bc == null) { bc = banner.gameObject.AddComponent(); sb.AppendLine(" BossBanner 추가"); } if (font != null) bc.SetFont(font); sb.AppendLine(" build made=" + bc.BuildIfNeeded() + " · " + bc.ApplyLayout()); bc.HideNow(); var hit = EnsureNode(hud, HitName, sb); var hc = hit.GetComponent(); if (hc == null) { hc = hit.gameObject.AddComponent(); sb.AppendLine(" HitVignette 추가"); } sb.AppendLine(" build made=" + hc.BuildIfNeeded() + " · " + hc.ApplyLayout()); hc.HideNow(); // HP 바 테두리는 런타임(EnsureBorder)에서 만든다 = 프리팹 diff 0 var rev = EnsureNode(hud, ReviveName, sb); var rc = rev.GetComponent(); if (rc == null) { rc = rev.gameObject.AddComponent(); sb.AppendLine(" ReviveDialog 추가"); } if (font != null) rc.SetFont(font); sb.AppendLine(" build made=" + rc.BuildIfNeeded() + " · " + rc.ApplyLayout()); rc.HideNow(); rev.SetAsLastSibling(); // 폴백 팝업은 HUD 최상단 } // ── 813j 물약 버튼 = IngameUIs/BattleUI 의 813c 예약 슬롯 자리 var pad = FindByPath(root.transform, PadPath); if (pad == null) sb.AppendLine("🔴 " + PadPath + " 없음"); else { var pot = EnsureNode(pad, PotionName, sb); var pc = pot.GetComponent(); if (pc == null) { pc = pot.gameObject.AddComponent(); sb.AppendLine(" PotionButton 추가"); } if (font != null) pc.SetFont(font); sb.AppendLine(" build made=" + pc.BuildIfNeeded() + " · " + pc.ApplyLayout()); sb.AppendLine(" refresh " + pc.Refresh()); } 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 { // UI 노드 var hud = FindByPath(root.transform, HudPath); if (hud != null) foreach (var n in new[] { BannerName, HitName, ReviveName }) { var t = hud.Find(n); if (t != null) { Object.DestroyImmediate(t.gameObject); sb.AppendLine(HudPath + "/" + n + " : 제거"); } } var pad = FindByPath(root.transform, PadPath); if (pad != null) { var t = pad.Find(PotionName); if (t != null) { Object.DestroyImmediate(t.gameObject); sb.AppendLine(PadPath + "/" + PotionName + " : 제거"); } } // Safe Area 패널 — 자식을 원래 자리로 되돌리고 패널을 지운다 var rt = root.transform; RestorePanel(rt, BackPanel, sb); RestorePanel(rt, FrontPanel, sb); sb.AppendLine("복원 후 순서: " + Order(rt)); PrefabUtility.SaveAsPrefabAsset(root, NewGameUIPath); } finally { PrefabUtility.UnloadPrefabContents(root); } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); return sb.ToString(); } static void RestorePanel(Transform root, string panelName, StringBuilder sb) { var panel = root.Find(panelName) as RectTransform; if (panel == null) { sb.AppendLine(panelName + " : 없음"); return; } int at = panel.GetSiblingIndex(); while (panel.childCount > 0) { var c = panel.GetChild(0) as RectTransform; c.SetParent(root as RectTransform, false); c.SetSiblingIndex(at++); sb.AppendLine(" " + c.name + " : 루트로 복원 @" + (at - 1)); } Object.DestroyImmediate(panel.gameObject); sb.AppendLine(panelName + " : 패널 제거"); } }