Project_WL/AgentScripts/WL813c_Apply.cs

227 lines
13 KiB
C#
Raw Permalink Normal View History

// 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;
}
}
/// <summary>WLHudLayoutSettings.asset 생성(이미 있으면 값 유지 · 덮어쓰지 않는다).</summary>
public static object CreateSettings()
{
var sb = new StringBuilder();
var existing = AssetDatabase.LoadAssetAtPath<WL.UI.WLHudLayoutSettings>(SettingsPath);
if (existing != null) { sb.AppendLine(SettingsPath + " : 이미 있음 — 값 유지"); }
else
{
EnsureFolder(SettingsDir);
var so = ScriptableObject.CreateInstance<WL.UI.WLHudLayoutSettings>(); // 기본값 = 기준서 §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<Vector2>();
var dia = new List<float>();
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();
}
/// <summary>무변경 재저장 — LoadPrefabContents→SaveAsPrefabAsset 이 diff 를 만드는지 먼저 잰다(802b 교훈).</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 로 재직렬화 여부 확인)";
}
public static object Apply()
{
var sb = new StringBuilder();
var settings = AssetDatabase.LoadAssetAtPath<WL.UI.WLHudLayoutSettings>(SettingsPath);
if (settings == null) return "🔴 " + SettingsPath + " 없음 — CreateSettings 먼저";
TMPro.TMP_FontAsset font = null;
var fontPath = AssetDatabase.GUIDToAssetPath(TmpFontGuid);
if (!string.IsNullOrEmpty(fontPath)) font = AssetDatabase.LoadAssetAtPath<TMPro.TMP_FontAsset>(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<WL.UI.SafeAreaFitter>() == null)
{ pad.gameObject.AddComponent<WL.UI.SafeAreaFitter>(); sb.AppendLine(PadPath + " : SafeAreaFitter 추가"); }
else sb.AppendLine(PadPath + " : SafeAreaFitter 이미 있음");
var layout = pad.GetComponent<WL.UI.WLBattlePadLayout>();
if (layout == null) { layout = pad.gameObject.AddComponent<WL.UI.WLBattlePadLayout>(); sb.AppendLine(PadPath + " : WLBattlePadLayout 추가"); }
else sb.AppendLine(PadPath + " : WLBattlePadLayout 이미 있음");
var so = new SerializedObject(layout);
so.FindProperty("target").objectReferenceValue = pad.GetComponent<BattleUI>();
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<WL.UI.BossHpBar>();
if (bar == null) { bar = barT.gameObject.AddComponent<WL.UI.BossHpBar>(); 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<CanvasGroup>();
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<WL.UI.WLBattlePadLayout>();
if (l != null) { Object.DestroyImmediate(l, true); sb.AppendLine(PadPath + " : WLBattlePadLayout 제거"); }
var f = pad.GetComponent<WL.UI.SafeAreaFitter>();
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;
}
}