// WL-813g (#813) — 설정 에셋 2종 생성 + NewGameUI.prefab 에 텍스트 연출 노드 3개 · 슬롯 오버라이드 컴포넌트 추가. // 실행: unity command run_script --file AgentScripts/WL813g_Apply.cs --entry WL813g_Apply.CreateSettings // unity command run_script --file AgentScripts/WL813g_Apply.cs --entry WL813g_Apply.Resave // unity command run_script --file AgentScripts/WL813g_Apply.cs --entry WL813g_Apply.Apply // unity command run_script --file AgentScripts/WL813g_Apply.cs --entry WL813g_Apply.Revert // 🔴 run_script 파일은 파일마다 독립 어셈블리 — 다른 AgentScript 상수 참조 불가(813c 교훈). using System.Text; using UnityEditor; using UnityEngine; public static class WL813g_Apply { public const string NewGameUIPath = "Assets/Res_Addr/MainUI/NewGameUI.prefab"; public const string SettingsDir = "Assets/WL/UI/Settings/Resources/WL"; public const string TextSettingsPath = SettingsDir + "/WLCombatTextSettings.asset"; public const string SlotPresetPath = SettingsDir + "/WLSkillSlotPreset.asset"; public const string HudPath = "IngameUIs/WL_HUD"; public const string PadPath = "IngameUIs/BattleUI"; public const string KillChainName = "WL_KillChainText"; public const string LootToastName = "WL_LootToast"; public const string StatPopupName = "WL_StatPopup"; const string TmpFontGuid = "a387e325271126742ad15569dba1ac57"; // 813c 가 실측한 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; } } /// 설정 에셋 2종 생성(이미 있으면 값 유지 · 덮어쓰지 않는다). public static object CreateSettings() { var sb = new StringBuilder(); EnsureFolder(SettingsDir); var text = AssetDatabase.LoadAssetAtPath(TextSettingsPath); if (text == null) { text = ScriptableObject.CreateInstance(); AssetDatabase.CreateAsset(text, TextSettingsPath); sb.AppendLine("생성: " + TextSettingsPath); } else sb.AppendLine("이미 있음(값 유지): " + TextSettingsPath); var slot = AssetDatabase.LoadAssetAtPath(SlotPresetPath); if (slot == null) { slot = ScriptableObject.CreateInstance(); AssetDatabase.CreateAsset(slot, SlotPresetPath); sb.AppendLine("생성: " + SlotPresetPath); } else sb.AppendLine("이미 있음(값 유지): " + SlotPresetPath); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); WL.UI.WLCombatTextSettings.ClearCache(); WL.UI.WLSkillSlotPreset.ClearCache(); sb.AppendLine("── 데미지 리듬(기준서 §B 요소 2)"); sb.AppendLine(" normal=" + text.damageNormalSeconds + "s crit=" + text.damageCritSeconds + "s critScale=" + text.damageCritScale + " maxConcurrent=" + text.damageMaxConcurrent); sb.AppendLine("── 연쇄 문구 / 토스트 / 스탯 팝업"); sb.AppendLine(" killChain=" + text.killChainSeconds + "s punch=" + text.killChainPunchScale + " topPx=" + text.killChainTopOffsetPx + " · loot=" + text.lootToastSeconds + "s lines=" + text.lootToastLines + " · stat=" + text.statPopupSeconds + "s rows=" + text.statPopupRows); sb.AppendLine("── 슬롯 오버라이드(813n)"); sb.Append(WL.UI.WLSkillSlotOverride.ResolveDump(slot)); 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 로 재직렬화 여부 확인)"; } static Transform EnsureNode(Transform parent, string name, StringBuilder sb) { var t = parent.Find(name); if (t == null) { var go = new GameObject(name, typeof(RectTransform)); go.layer = UILayer; t = go.transform; ((RectTransform)t).SetParent((RectTransform)parent, false); sb.AppendLine(parent.name + "/" + name + " : 노드 추가"); } else sb.AppendLine(parent.name + "/" + name + " : 이미 있음"); return t; } public static object Apply() { var sb = new StringBuilder(); var text = AssetDatabase.LoadAssetAtPath(TextSettingsPath); if (text == null) return "🔴 " + TextSettingsPath + " 없음 — CreateSettings 먼저"; TMPro.TMP_FontAsset font = null; var fontPath = AssetDatabase.GUIDToAssetPath(TmpFontGuid); if (!string.IsNullOrEmpty(fontPath)) font = AssetDatabase.LoadAssetAtPath(fontPath); sb.AppendLine("폰트: " + (font != null ? fontPath : "「미확인」 — TMP 기본 폰트로 떨어진다")); var root = PrefabUtility.LoadPrefabContents(NewGameUIPath); try { // ── ① 텍스트 연출 3종 = IngameUIs/WL_HUD 아래(802b SafeAreaFitter 레이어 · 813c 규칙) var hud = FindByPath(root.transform, HudPath); if (hud == null) sb.AppendLine("🔴 " + HudPath + " 없음"); else { var kc = EnsureNode(hud, KillChainName, sb); var kct = kc.GetComponent(); if (kct == null) { kct = kc.gameObject.AddComponent(); sb.AppendLine(" KillChainText 추가"); } if (font != null) kct.SetFont(font); sb.AppendLine(" build made=" + kct.BuildIfNeeded() + " · " + kct.ApplyLayout()); kct.Hide(); var lt = EnsureNode(hud, LootToastName, sb); var ltc = lt.GetComponent(); if (ltc == null) { ltc = lt.gameObject.AddComponent(); sb.AppendLine(" LootToast 추가"); } if (font != null) ltc.SetFont(font); sb.AppendLine(" build made=" + ltc.BuildIfNeeded() + " · " + ltc.ApplyLayout()); ltc.HideAll(); var sp = EnsureNode(hud, StatPopupName, sb); var spc = sp.GetComponent(); if (spc == null) { spc = sp.gameObject.AddComponent(); sb.AppendLine(" StatPopup 추가"); } if (font != null) spc.SetFont(font); sb.AppendLine(" build made=" + spc.BuildIfNeeded() + " · " + spc.ApplyLayout()); spc.Hide(); } // ── ② 슬롯 장착 로컬 오버라이드 = IngameUIs/BattleUI 인스턴스 전용 추가 컴포넌트(813c 방식) var pad = FindByPath(root.transform, PadPath); if (pad == null) sb.AppendLine("🔴 " + PadPath + " 없음"); else { var ov = pad.GetComponent(); if (ov == null) { ov = pad.gameObject.AddComponent(); sb.AppendLine(PadPath + " : WLSkillSlotOverride 추가"); } else sb.AppendLine(PadPath + " : WLSkillSlotOverride 이미 있음"); var so = new SerializedObject(ov); so.FindProperty("target").objectReferenceValue = pad.GetComponent(); so.ApplyModifiedPropertiesWithoutUndo(); sb.AppendLine(" target=BattleUI 주입 · BattleUI.prefab 원본 무수정(인스턴스 전용 추가 컴포넌트)"); } 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 hud = FindByPath(root.transform, HudPath); if (hud != null) foreach (var n in new[] { KillChainName, LootToastName, StatPopupName }) { 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 ov = pad.GetComponent(); if (ov != null) { Object.DestroyImmediate(ov, true); sb.AppendLine(PadPath + " : WLSkillSlotOverride 제거"); } } PrefabUtility.SaveAsPrefabAsset(root, NewGameUIPath); } finally { PrefabUtility.UnloadPrefabContents(root); } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); return sb.ToString(); } }