Project_WL/AgentScripts/WL813tj_Apply.cs

385 lines
19 KiB
C#

// 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<TMPro.TMP_FontAsset>(p);
sb.AppendLine("폰트: " + (f != null ? p : "「미확인」 — TMP 기본 폰트로 떨어진다"));
return f;
}
// ── ① 설정 에셋 ──────────────────────────────────────────────────────────
/// <summary>설정 에셋 2종 생성(이미 있으면 값 유지 · 덮어쓰지 않는다).</summary>
public static object CreateSettings()
{
var sb = new StringBuilder();
EnsureFolder(SettingsDir);
var boss = AssetDatabase.LoadAssetAtPath<WL.UI.WLBossUiSettings>(BossSettingsPath);
if (boss == null)
{
boss = ScriptableObject.CreateInstance<WL.UI.WLBossUiSettings>();
AssetDatabase.CreateAsset(boss, BossSettingsPath);
sb.AppendLine("생성: " + BossSettingsPath);
}
else sb.AppendLine("이미 있음(값 유지): " + BossSettingsPath);
var sur = AssetDatabase.LoadAssetAtPath<WL.UI.WLSurvivalUiSettings>(SurvivalSettingsPath);
if (sur == null)
{
sur = ScriptableObject.CreateInstance<WL.UI.WLSurvivalUiSettings>();
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();
}
/// <summary>무변경 재저장 — 프리팹 편집 전에 재직렬화 diff 가 생기는지 먼저 잰다(802b·813c 교훈).</summary>
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<WL.UI.SafeAreaFitter>() == null)
{
t.gameObject.AddComponent<WL.UI.SafeAreaFitter>();
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<WL.UI.WLBossUiSettings>(BossSettingsPath);
var sur = AssetDatabase.LoadAssetAtPath<WL.UI.WLSurvivalUiSettings>(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<WL.UI.BossBanner>();
if (bc == null) { bc = banner.gameObject.AddComponent<WL.UI.BossBanner>(); 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<WL.UI.HitVignette>();
if (hc == null) { hc = hit.gameObject.AddComponent<WL.UI.HitVignette>(); 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<WL.UI.ReviveDialog>();
if (rc == null) { rc = rev.gameObject.AddComponent<WL.UI.ReviveDialog>(); 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<WL.UI.PotionButton>();
if (pc == null) { pc = pot.gameObject.AddComponent<WL.UI.PotionButton>(); 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 + " : 패널 제거");
}
}