diff --git a/AgentScripts/WL813g_Apply.cs b/AgentScripts/WL813g_Apply.cs
new file mode 100644
index 000000000..3c3691c8c
--- /dev/null
+++ b/AgentScripts/WL813g_Apply.cs
@@ -0,0 +1,209 @@
+// 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();
+ }
+}
diff --git a/AgentScripts/WL813g_Probe.cs b/AgentScripts/WL813g_Probe.cs
new file mode 100644
index 000000000..950df9c90
--- /dev/null
+++ b/AgentScripts/WL813g_Probe.cs
@@ -0,0 +1,407 @@
+// WL-813g (#813) — 에디트 모드 실측 프로브(Play 0 · 합성 이벤트).
+// 실행: unity command run_script --file AgentScripts/WL813g_Probe.cs --entry WL813g_Probe.DumpSlots
+// unity command run_script --file AgentScripts/WL813g_Probe.cs --entry WL813g_Probe.DumpText
+// unity command run_script --file AgentScripts/WL813g_Probe.cs --entry WL813g_Probe.DumpDamage
+// unity command run_script --file AgentScripts/WL813g_Probe.cs --entry WL813g_Probe.DumpPrefab
+// 🔴 이 파일은 독립 어셈블리로 컴파일된다 — Assembly-CSharp 의 internal(Dispatch 등)은 못 쓴다.
+// 합성 이벤트는 Assembly-CSharp 안의 헬퍼(KillChainText.RaiseFakeTier 등)를 통해 낸다(813c 교훈).
+// 🔴 씬은 저장하지 않는다. 만든 임시 오브젝트는 전부 DestroyImmediate 로 회수한다.
+
+using System.Collections.Generic;
+using System.Text;
+using UnityEditor;
+using UnityEngine;
+
+public static class WL813g_Probe
+{
+ const string LogDir = "Logs";
+ public const string NewGameUIPath = "Assets/Res_Addr/MainUI/NewGameUI.prefab";
+ public const string DmgPrefabPath = "Assets/Res_Addr/Ingame/HUDDMGUI.prefab";
+ public const string SkillJson = "Assets/ResWork/Table/Export/_WL813n_SkillList.json";
+ public const string GlobalJson = "Assets/ResWork/Table/Export/GlobalValue.json";
+ const int UILayer = 5;
+
+ static void Write(string name, string body)
+ {
+ System.IO.Directory.CreateDirectory(LogDir);
+ System.IO.File.WriteAllText(System.IO.Path.Combine(LogDir, name), body, new System.Text.UTF8Encoding(true));
+ }
+
+ static string Desc(RectTransform rt)
+ {
+ if (rt == null) return "(rt 없음)";
+ return "aMin=" + rt.anchorMin + " aMax=" + rt.anchorMax + " pos=" + rt.anchoredPosition +
+ " size=" + rt.sizeDelta + " pivot=" + rt.pivot + " scale=" + rt.localScale;
+ }
+
+ // ─────────────────────────────────────────── 표 읽기(에디트 모드에서 table_* 싱글턴이 없어서 JSON 직독)
+
+ // 표는 단일 라인 JSON 배열이고 값은 전부 문자열이다(813n 실측) → 행 단위로 잘라 정규식으로 읽는다.
+ // (run_script 는 파일마다 독립 어셈블리라 Newtonsoft 참조를 가정하지 않는다.)
+ static List SplitRows(string path)
+ {
+ var rows = new List();
+ try
+ {
+ var txt = System.IO.File.ReadAllText(path, System.Text.Encoding.UTF8).TrimStart('');
+ foreach (System.Text.RegularExpressions.Match m in
+ System.Text.RegularExpressions.Regex.Matches(txt, "\\{[^{}]*\\}"))
+ rows.Add(m.Value);
+ }
+ catch (System.Exception e) { Debug.LogWarning("[813g] 표 읽기 실패 " + path + " : " + e.Message); }
+ return rows;
+ }
+
+ static string Field(string row, string key)
+ {
+ var m = System.Text.RegularExpressions.Regex.Match(row, "\"" + key + "\"\\s*:\\s*\"([^\"]*)\"");
+ return m.Success ? m.Groups[1].Value : null;
+ }
+
+ static Dictionary ReadSkillMp()
+ {
+ var map = new Dictionary();
+ foreach (var row in SplitRows(SkillJson))
+ {
+ var id = Field(row, "n_SkillID"); var mp = Field(row, "n_MP");
+ int i, v;
+ if (id != null && mp != null && int.TryParse(id, out i) && int.TryParse(mp, out v)) map[i] = v;
+ }
+ return map;
+ }
+
+ static Dictionary ReadSkillCool()
+ {
+ var map = new Dictionary();
+ foreach (var row in SplitRows(SkillJson))
+ {
+ var id = Field(row, "n_SkillID"); var cd = Field(row, "f_CoolTime");
+ int i; float v;
+ if (id != null && cd != null && int.TryParse(id, out i) && float.TryParse(cd, out v)) map[i] = v;
+ }
+ return map;
+ }
+
+ static float ReadGlobal(string id, float fallback)
+ {
+ foreach (var row in SplitRows(GlobalJson))
+ {
+ if (Field(row, "s_ID") != id) continue;
+ float v;
+ if (float.TryParse(Field(row, "n_Value"), out v)) return v;
+ }
+ return fallback;
+ }
+
+ // ─────────────────────────────────────────── ① 슬롯 오버라이드 (발주서 §3 검증 = 4×4 덤프)
+
+ public static object DumpSlots()
+ {
+ var sb = new StringBuilder();
+ var preset = WL.UI.WLSkillSlotPreset.Instance;
+ sb.AppendLine("=== WL-813g ① 슬롯 장착 로컬 오버라이드 (에디트 모드 · Play 0) ===");
+ if (preset == null) { var s0 = "🔴 WLSkillSlotPreset.asset 로드 실패"; Write("WL813g_probe_slots.txt", s0); return s0; }
+
+ sb.AppendLine("[해석 표 4×4]");
+ sb.Append(WL.UI.WLSkillSlotOverride.ResolveDump(preset));
+
+ var mp = ReadSkillMp();
+ var cool = ReadSkillCool();
+ float mpPool = ReadGlobal("MP", -1f);
+ float mpRegen = ReadGlobal("MP_REGEN", -1f);
+ sb.AppendLine();
+ sb.AppendLine("[MP 게이트 · Actor.cs:733-741 과 같은 식 · SKILL_COST=0 가정 · MP 풀=" + mpPool + " (GlobalValue 실측) · 회복=" + mpRegen + "/s]");
+ for (int r = 0; r < preset.rows.Length; r++)
+ {
+ var row = preset.rows[r];
+ int n = preset.SlotCountFor(row.classId);
+ sb.Append("class ").Append(row.classId).Append(" :");
+ for (int s = 0; s < n; s++)
+ {
+ int id = preset.SkillIdFor(row.classId, s);
+ int need = mp.ContainsKey(id) ? mp[id] : -1;
+ float cd = cool.ContainsKey(id) ? cool[id] : -1f;
+ bool dim = mpPool > 0f && need > mpPool;
+ sb.Append(" [").Append(s).Append("] id=").Append(id).Append(" mp=").Append(need)
+ .Append(" cool=").Append(cd).Append("s dim@만탱=").Append(dim);
+ }
+ sb.AppendLine();
+ }
+
+ // ── 합성 SD_Equip 에 실제 ApplyTo 를 태워 before→after 를 잰다(서버 데이터 무접촉)
+ sb.AppendLine();
+ sb.AppendLine("[합성 SD_Equip 적용 · 실제 WLSkillSlotOverride.ApplyTo 경로]");
+ foreach (var row in preset.rows)
+ {
+ var equip = new SD_Equip();
+ equip.Preset = 0;
+ equip.Equip = new Dictionary();
+ equip.Equip[eItem.PC] = row.classId;
+ equip.Skill = new Dictionary>();
+ var lst = new List();
+ for (int i = 0; i < 6; i++) lst.Add(0);
+ lst[0] = 700001; // 서버 프리셋 흉내(패시브·엉뚱한 값)
+ lst[1] = 0; lst[2] = 0; lst[3] = 0;
+ equip.Skill[0] = lst;
+
+ var restore = new List();
+ string log = WL.UI.WLSkillSlotOverride.ApplyTo(equip, preset, row.classId, restore);
+ sb.AppendLine(" " + log);
+ var after = new StringBuilder(" Equip.Skill[0] = ");
+ for (int i = 0; i < lst.Count; i++) after.Append(lst[i]).Append(i < lst.Count - 1 ? ", " : "");
+ after.Append(" (슬롯 4·5 = 물약/회피 예약 · 건드리지 않음)");
+ sb.AppendLine(after.ToString());
+ }
+
+ sb.AppendLine();
+ sb.AppendLine("[C8 롤백] overrideEnabled=false 또는 에셋 삭제 → ApplyTo 미호출 = 서버 프리셋 원본");
+ var s1 = sb.ToString();
+ Write("WL813g_probe_slots.txt", s1);
+ return s1;
+ }
+
+ // ─────────────────────────────────────────── ② 텍스트 연출 3종 (합성 이벤트)
+
+ static Canvas MakeCanvas(out GameObject go)
+ {
+ go = new GameObject("WL813g_TempCanvas", typeof(RectTransform), typeof(Canvas), typeof(UnityEngine.UI.CanvasScaler));
+ go.layer = UILayer;
+ var canvas = go.GetComponent