// PD 지시 #813 · 발주서 WL-813s — 엘리트 몹(존별 배치 · 크기 1.3× · 상시 색 · HP 8~12 s · 킬캠 · 드랍) 검증 프로브 // (에디트 모드 전용 · Play 0 · 로그인 0 · 에셋 무수정) // // 실행 (CLAUDE.md §1: using 금지 · 네임스페이스 풀어 쓰기) // unity command run_script --file AgentScripts/WL813s_Probe.cs --entry WL813s_Probe.Edit --timeout 900 // // 무엇을 재는가 // A. 데이터 — MonsterAppear.json 새 행 813021~813024 인식 · 몹 ID 가 MonsterList 의 Elite 인지 · // 실 HP(f_BaseHPRate × n_MonsterHP) → Q3 실측 DPS 15/s 기준 처치 시간 · 몹/투사체 프리팹 실존 · // WL_Nature.prefab 의 스포너 전수(존 5 + 엘리트 4 + 레거시 14) 연결 · text_lv null 0 // B. 마커 — 진짜 엘리트 프리팹으로 가짜 Spawned(Elite) → 스케일 1.3× · MPB 색 부착 · **멱등**(2회) · // 가짜 Killed(Elite) → 색 뗌 · 처치 시간(TTK) 측정 · RestoreAll 로 원본 스케일 복귀 // C. 연계 — 킬캠(811d): CombatCameraDirector 를 리플렉션으로 구독시키고 진짜 Killed 를 올려 Elite/일반 대조 · // 드랍(813f): LootBurst.BurstCount(SO, Elite) 실행 · 임팩트 티어(811gh): bossKillIncludesElite 실값 // D. GC — Spawned/Killed 각 100회의 GC.GetAllocatedBytesForCurrentThread() 증가분(0 B 목표) // E. C8 — WLEliteSettings.RuntimeDisabled = true(= 에셋 enabled 0 과 같은 Enabled 게이트) // → 크기 원본 · 색 0 · MPB 잔류 0 // F. 원본 — Assets/Script/** · Core · UI 에 813s 문자열 0 · MobHitFlash 합성 배선 확인 // // 에디트 모드 주의 // · [RuntimeInitializeOnLoadMethod] 는 Play 에서만 돈다 → EliteMarker.Probe* 헬퍼로 핸들러를 직접 부른다. // · Time.unscaledTime 은 한 호출 안에서 멈춰 있다 → TTK 는 ImpactRunner.ForceTick 으로 시뮬 시계를 민다(811gh 방식). // · 프로브가 만드는 오브젝트는 전부 HideAndDontSave + DestroyImmediate. 어떤 에셋도 쓰지 않는다. public static class WL813s_Probe { const string kOut = "AgentScripts/staging/WL813s/EDIT_PROBE.txt"; const string kAppear = "Assets/ResWork/Table/Export/MonsterAppear.json"; const string kList = "Assets/ResWork/Table/Export/MonsterList.json"; const string kMap = "Assets/Res_Addr/Map/WL_Nature.prefab"; const string kFlash = "Assets/WL/Combat/Reaction/MobHitFlash.cs"; const float kDps = 15f; // Q3 실측(보스 9000 → 6247 / 3분) · 813u 발주서 근거 static readonly int[] kEliteSpawners = new int[] { 813021, 813022, 813023, 813024 }; static readonly int[] kZoneSpawners = new int[] { 813001, 813002, 813003, 813004 }; static System.Collections.Generic.List s_log = new System.Collections.Generic.List(); static bool s_hooked; static void OnLog(string cond, string stack, UnityEngine.LogType t) { if (t == UnityEngine.LogType.Error || t == UnityEngine.LogType.Exception || t == UnityEngine.LogType.Assert) s_log.Add(t + " | " + cond); } static void Hook() { if (!s_hooked) { UnityEngine.Application.logMessageReceived += OnLog; s_hooked = true; } } static void Unhook() { if (s_hooked) { UnityEngine.Application.logMessageReceived -= OnLog; s_hooked = false; } } static UnityEngine.GameObject s_root, s_eliteGo, s_normalGo; static MobActor s_elite, s_normal; static float s_eliteBaseScale = -1f; public static string Edit() { var sb = new System.Text.StringBuilder(); sb.Append("# WL-813s 프로브 (에디트 모드 · Play 0 · 로그인 0)\n"); sb.Append("unity=").Append(UnityEngine.Application.unityVersion) .Append(" isPlaying=").Append(UnityEngine.Application.isPlaying) .Append(" utc=").Append(System.DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss")).Append("\n\n"); Hook(); s_log.Clear(); try { Section0(sb); SectionA(sb); BuildFakes(sb); SectionB(sb); SectionC(sb); SectionD(sb); SectionE(sb); SectionF(sb); } catch (System.Exception ex) { sb.Append("\n🔴 예외: ").Append(ex.ToString()).Append("\n"); } finally { Cleanup(sb); Unhook(); } sb.Append("\n## 프로브 중 Error/Exception: ").Append(s_log.Count).Append("\n"); for (int i = 0; i < s_log.Count && i < 30; i++) sb.Append(" ").Append(s_log[i]).Append("\n"); var dir = System.IO.Path.GetDirectoryName(kOut); if (!System.IO.Directory.Exists(dir)) System.IO.Directory.CreateDirectory(dir); System.IO.File.WriteAllText(kOut, sb.ToString(), new System.Text.UTF8Encoding(false)); return sb.ToString(); } // ═══════════════════════════════════════════════ 0. 환경 · SO 실값 static void Section0(System.Text.StringBuilder sb) { sb.Append("## 0. SO 실값 (전부 에셋에서 읽은 값 · 코드 상수 0)\n"); var el = WL.Combat.Reaction.WLEliteSettings.Instance; sb.Append("WLEliteSettings: ").Append(el == null ? "🔴 에셋 없음" : "로드 O").Append("\n"); if (el != null) sb.Append(" enabled=").Append(el.enabled) .Append(" scaleEnabled=").Append(el.scaleEnabled) .Append(" scaleMultiplier=").Append(el.scaleMultiplier.ToString("F2")) .Append(" scaleMaxAbsolute=").Append(el.scaleMaxAbsolute.ToString("F1")) .Append(" tintEnabled=").Append(el.tintEnabled) .Append("\n tintAlbedo=").Append(el.tintAlbedo.ToString("F2")) .Append(" tintEmission=").Append(el.tintEmission.ToString("F2")) .Append(" 목표 처치 ").Append(el.killTimeTargetMin.ToString("F0")).Append("~").Append(el.killTimeTargetMax.ToString("F0")).Append(" s\n"); var loot = WL.Combat.Loot.WLLootSettings.Instance; sb.Append("WLLootSettings: ").Append(loot == null ? "🔴 없음" : "burstEnabled=" + loot.burstEnabled + " elite=" + loot.eliteBurstMin + "~" + loot.eliteBurstMax + " normal=" + loot.normalBurstCount + " boss=" + loot.bossBurstMin + "~" + loot.bossBurstMax + " burstRollsBag=" + loot.burstRollsBag + " burstGoldShare=" + loot.burstGoldShare.ToString("F2")).Append("\n"); var cam = WL.CombatCamera.WLCombatCameraSettings.Instance; sb.Append("WLCombatCameraSettings: ").Append(cam == null ? "🔴 없음" : "killCamEnabled=" + cam.killCamEnabled + " onElite=" + cam.killCamOnElite + " onBoss=" + cam.killCamOnBoss + " onNormal=" + cam.killCamOnNormal + " directHitOnly=" + cam.killCamDirectHitOnly + " maxPerRun=" + cam.killCamMaxPerRun).Append("\n"); var tier = WL.Combat.Reaction.WLImpactTierSettings.Instance; sb.Append("WLImpactTierSettings: ").Append(tier == null ? "🔴 없음" : "tierEnabled=" + tier.tierEnabled + " bossKillIncludesElite=" + tier.bossKillIncludesElite).Append("\n"); var blink = WL.Combat.Reaction.WLHitBlinkSettings.Instance; sb.Append("WLHitBlinkSettings: ").Append(blink == null ? "🔴 없음" : "colorProperties=[" + string.Join(",", blink.colorProperties) + "] emissionProperties=[" + string.Join(",", blink.emissionProperties) + "] dissolveElite hdr=" + blink.dissolveEdgeHdrElite).Append("\n"); sb.Append("코어 CombatEvents.Enabled=").Append(WL.Combat.Core.CombatEvents.Enabled).Append("\n\n"); } // ═══════════════════════════════════════════════ A. 데이터 static void SectionA(System.Text.StringBuilder sb) { sb.Append("## A. 데이터 — MonsterAppear 새 행 · MonsterList 대조 · 자산 실존 · 스포너 연결\n"); string appear = System.IO.File.ReadAllText(kAppear); string list = System.IO.File.ReadAllText(kList); sb.Append("| 스포너 | Lv | ATK | HP(행) | 몹ID | e_MonsterType | f_BaseHPRate | 실 HP | DPS15 처치 s | f_DefaultScale | ×1.3 | 리젠 s | 동시 | 몹 프리팹 | 투사체 |\n"); sb.Append("|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|\n"); int ok = 0; var el = WL.Combat.Reaction.WLEliteSettings.Instance; float mul = el != null ? el.scaleMultiplier : 1.3f; for (int i = 0; i < kEliteSpawners.Length; i++) { string row = Row(appear, "n_SpawnerID", kEliteSpawners[i].ToString()); if (row == null) { sb.Append("| ").Append(kEliteSpawners[i]).Append(" | 🔴 행 없음 |\n"); continue; } string mobId = F(row, "s_AppearMonster"); string hp = F(row, "n_MonsterHP"); string mrow = Row(list, "n_MonsterID", mobId); string type = mrow != null ? F(mrow, "e_MonsterType") : "?"; float hpRate = mrow != null ? Num(F(mrow, "f_BaseHPRate")) : 0f; float scale = mrow != null ? Num(F(mrow, "f_DefaultScale")) : 0f; string prefab = mrow != null ? F(mrow, "s_MonsterPrefab") : ""; string proj = mrow != null ? F(mrow, "s_Porjectile1") : ""; float realHp = hpRate * Num(hp); bool mobOk = Exists("Assets/Res_Addr/Mobs/" + prefab + ".prefab"); bool projOk = Exists("Assets/Res_Addr/Projectile/" + proj + ".prefab"); if (type == "Elite" && mobOk && projOk) ok++; sb.Append("| ").Append(kEliteSpawners[i]).Append(" | ").Append(F(row, "n_AppearMonsterLv")) .Append(" | ").Append(F(row, "n_MonsterATK")).Append(" | ").Append(hp) .Append(" | ").Append(mobId).Append(" | ").Append(type) .Append(" | ").Append(hpRate.ToString("F0")).Append(" | ").Append(realHp.ToString("F0")) .Append(" | ").Append((realHp / kDps).ToString("F1")) .Append(" | ").Append(scale.ToString("F1")).Append(" | ").Append((scale * mul).ToString("F2")) .Append(" | ").Append(F(row, "f_SpawnerDelay")).Append(" | ").Append(F(row, "n_MaxMonsterCount")) .Append(" | ").Append(prefab).Append(mobOk ? " O" : " 🔴").Append(" | ").Append(proj).Append(projOk ? " O" : " 🔴") .Append(" |\n"); } sb.Append("새 행 정상 = ").Append(ok).Append(" / ").Append(kEliteSpawners.Length).Append("\n"); // 기존 존 행 무변경 확인(813u 와의 충돌 방지 규칙) sb.Append("기존 존 행(813001~813005 · 813010) 값 = "); for (int i = 0; i < kZoneSpawners.Length; i++) { string r = Row(appear, "n_SpawnerID", kZoneSpawners[i].ToString()); sb.Append(kZoneSpawners[i]).Append(":HP").Append(F(r, "n_MonsterHP")).Append("/몹").Append(F(r, "s_AppearMonster")).Append(" "); } sb.Append("\n"); // WL_Nature 스포너 전수 var map = UnityEditor.AssetDatabase.LoadAssetAtPath(kMap); if (map == null) sb.Append("🔴 ").Append(kMap).Append(" 로드 실패\n"); else { var mgrs = map.GetComponentsInChildren(true); int nullText = 0, eliteNodes = 0; sb.Append("WL_Nature 스포너 ").Append(mgrs.Length).Append("개 — "); for (int i = 0; i < mgrs.Length; i++) { if (mgrs[i].text_lv == null) nullText++; for (int j = 0; j < kEliteSpawners.Length; j++) if (mgrs[i].SpawnerID == kEliteSpawners[j]) eliteNodes++; } sb.Append("엘리트 노드 ").Append(eliteNodes).Append("/4 · text_lv null ").Append(nullText).Append("\n"); for (int i = 0; i < mgrs.Length; i++) { bool isElite = false; for (int j = 0; j < kEliteSpawners.Length; j++) if (mgrs[i].SpawnerID == kEliteSpawners[j]) isElite = true; if (!isElite) continue; var t = mgrs[i].transform; var rend = mgrs[i].GetComponentInChildren(true); sb.Append(" ").Append(Path(t)).Append(" id=").Append(mgrs[i].SpawnerID) .Append(" pos=").Append(t.position.ToString("F1")).Append(" range=").Append(mgrs[i].MobGenRange.ToString("F0")) .Append(" lv라벨렌더러=").Append(rend == null ? "없음" : (rend.enabled ? "켬" : "끔")).Append("\n"); } // 같은 좌표의 존 스포너와 대조 for (int j = 0; j < kZoneSpawners.Length; j++) { UnityEngine.Vector3 zp = UnityEngine.Vector3.zero, ep = UnityEngine.Vector3.zero; bool hz = false, he = false; for (int i = 0; i < mgrs.Length; i++) { if (mgrs[i].SpawnerID == kZoneSpawners[j]) { zp = mgrs[i].transform.position; hz = true; } if (mgrs[i].SpawnerID == kEliteSpawners[j]) { ep = mgrs[i].transform.position; he = true; } } if (hz && he) sb.Append(" 존 ").Append(kZoneSpawners[j]).Append(" ↔ 엘리트 ").Append(kEliteSpawners[j]) .Append(" 거리 ").Append(UnityEngine.Vector3.Distance(zp, ep).ToString("F2")).Append(" m\n"); } } sb.Append("\n"); } // ═══════════════════════════════════════════════ 준비 static void BuildFakes(System.Text.StringBuilder sb) { sb.Append("## 준비: 진짜 엘리트 프리팹으로 가짜 액터\n"); s_root = new UnityEngine.GameObject("[813s] root"); s_root.hideFlags = UnityEngine.HideFlags.HideAndDontSave; s_root.SetActive(false); string appear = System.IO.File.ReadAllText(kAppear); string list = System.IO.File.ReadAllText(kList); string mobId = F(Row(appear, "n_SpawnerID", "813021"), "s_AppearMonster"); string mrow = Row(list, "n_MonsterID", mobId); string prefabName = mrow != null ? F(mrow, "s_MonsterPrefab") : "Elite/Aspethis_D"; float defScale = mrow != null ? Num(F(mrow, "f_DefaultScale")) : 1f; var prefab = UnityEditor.AssetDatabase.LoadAssetAtPath("Assets/Res_Addr/Mobs/" + prefabName + ".prefab"); if (prefab == null) { sb.Append("🔴 엘리트 프리팹 로드 실패 ").Append(prefabName).Append("\n\n"); return; } s_eliteGo = UnityEngine.Object.Instantiate(prefab, s_root.transform); s_eliteGo.hideFlags = UnityEngine.HideFlags.HideAndDontSave; s_eliteGo.name = "[813s] elite " + prefabName; s_elite = s_eliteGo.GetComponent(); if (s_elite == null) s_elite = s_eliteGo.AddComponent(); s_elite.m_Role = eRole.Mob; s_elite.m_SubRole = eSubRol.Elite; // 원본 MobActor.Set(:82) 이 e_MonsterType 으로 넣는 값 s_elite.transform.localScale = defScale * UnityEngine.Vector3.one; // 원본 MobActor.Set(:87) 과 같은 대입 s_eliteBaseScale = defScale; var normalPrefab = UnityEditor.AssetDatabase.LoadAssetAtPath("Assets/Res_Addr/Mobs/Mob/Batty_A.prefab"); if (normalPrefab != null) { s_normalGo = UnityEngine.Object.Instantiate(normalPrefab, s_root.transform); s_normalGo.hideFlags = UnityEngine.HideFlags.HideAndDontSave; s_normalGo.name = "[813s] normal Batty_A"; s_normal = s_normalGo.GetComponent(); if (s_normal != null) { s_normal.m_Role = eRole.Mob; s_normal.m_SubRole = eSubRol.None; } } var rends = s_eliteGo.GetComponentsInChildren(true); sb.Append("엘리트 = ").Append(prefabName).Append(" · Renderer ").Append(rends.Length) .Append(" · 기본 스케일 ").Append(defScale.ToString("F2")).Append(" · IsSubRole(Elite)=").Append(s_elite.IsSubRole(eSubRol.Elite)).Append("\n"); if (rends.Length > 0 && rends[0].sharedMaterial != null && rends[0].sharedMaterial.shader != null) { var sh = rends[0].sharedMaterial.shader; sb.Append("셰이더[0] = ").Append(sh.name) .Append(" · _AlbedoColor=").Append(sh.FindPropertyIndex("_AlbedoColor") >= 0) .Append(" · _EmissionColor=").Append(sh.FindPropertyIndex("_EmissionColor") >= 0) .Append(" · _RimColor=").Append(sh.FindPropertyIndex("_RimColor") >= 0) .Append(" (🔴 _RimColor 는 _MK_RIM_* shader_feature 게이팅이라 MPB 로 못 켠다 → 쓰지 않는다)\n"); } sb.Append("\n"); } // ═══════════════════════════════════════════════ B. 마커 static void SectionB(System.Text.StringBuilder sb) { sb.Append("## B. 마커 — 크기 1.3× · 상시 색 · 멱등 · 처치 시간\n"); if (s_elite == null) { sb.Append("🔴 엘리트 액터 없음 — 건너뜀\n\n"); return; } WL.Combat.Reaction.WLEliteSettings.RuntimeDisabled = false; WL.Combat.Reaction.EliteMarker.ClearAll(); WL.Combat.Reaction.EliteMarker.ResetDiagnostics(); WL.Combat.Reaction.MobHitFlash.ResetDiagnostics(); WL.Combat.Reaction.ImpactRunner.ResetForceClock(); var el = WL.Combat.Reaction.WLEliteSettings.Instance; float before = s_elite.transform.localScale.x; WL.Combat.Reaction.EliteMarker.ProbeSpawned(s_elite); float after1 = s_elite.transform.localScale.x; bool tinted1 = WL.Combat.Reaction.MobHitFlash.HasEliteTint(s_elite); bool block1 = WL.Combat.Reaction.MobHitFlash.HasAnyBlock(s_elite); WL.Combat.Reaction.EliteMarker.ProbeSpawned(s_elite); // 멱등 확인(풀 재사용 재등장) WL.Combat.Reaction.EliteMarker.ProbeSpawned(s_elite); float after3 = s_elite.transform.localScale.x; sb.Append("B-1 크기: 기본 ").Append(before.ToString("F3")) .Append(" → 1회 ").Append(after1.ToString("F3")) .Append(" (기대 ").Append((before * (el != null ? el.scaleMultiplier : 1.3f)).ToString("F3")).Append(")") .Append(" → 3회 ").Append(after3.ToString("F3")) .Append(" · 멱등 ").Append(UnityEngine.Mathf.Abs(after1 - after3) < 0.0005f ? "PASS" : "🔴 FAIL") .Append(" · EliteMarker.BaseScaleOf=").Append(WL.Combat.Reaction.EliteMarker.BaseScaleOf(s_elite).ToString("F3")).Append("\n"); sb.Append("B-2 상시 색: HasEliteTint=").Append(tinted1) .Append(" · MPB 부착=").Append(block1) .Append(" · TintApplied=").Append(WL.Combat.Reaction.EliteMarker.TintApplied) .Append(" TintSkipped=").Append(WL.Combat.Reaction.EliteMarker.TintSkipped) .Append(" · MobHitFlash.StatesCreated=").Append(WL.Combat.Reaction.MobHitFlash.StatesCreated).Append("\n"); // 일반 몹은 마커 대상이 아니다 if (s_normal != null) { float nb = s_normal.transform.localScale.x; WL.Combat.Reaction.EliteMarker.ProbeSpawned(s_normal); sb.Append("B-3 일반 몹(Batty_A): 스케일 ").Append(nb.ToString("F3")).Append(" → ").Append(s_normal.transform.localScale.x.ToString("F3")) .Append(" · 색=").Append(WL.Combat.Reaction.MobHitFlash.HasEliteTint(s_normal)) .Append(" · SkippedNotElite=").Append(WL.Combat.Reaction.EliteMarker.SkippedNotElite) .Append(UnityEngine.Mathf.Abs(nb - s_normal.transform.localScale.x) < 0.0005f ? " → PASS" : " → 🔴 FAIL").Append("\n"); } // 처치 시간 — 시뮬 시계를 10 s 민다(811gh ImpactRunner.ForceTick) for (int i = 0; i < 20; i++) WL.Combat.Reaction.ImpactRunner.ForceTick(0.5f); WL.Combat.Reaction.EliteMarker.ProbeKilled(s_elite, eSubRol.Elite); sb.Append("B-4 처치: TTK=").Append(WL.Combat.Reaction.EliteMarker.LastKillTime.ToString("F2")).Append(" s (시뮬 10.00 s 주입)") .Append(" · 색 뗌=").Append(!WL.Combat.Reaction.MobHitFlash.HasEliteTint(s_elite)) .Append(" · 크기 유지=").Append(s_elite.transform.localScale.x.ToString("F3")) .Append(" · KillCount=").Append(WL.Combat.Reaction.EliteMarker.KillCount).Append("\n"); WL.Combat.Reaction.ImpactRunner.ResetForceClock(); WL.Combat.Reaction.EliteMarker.RestoreAll(); sb.Append("B-5 RestoreAll: 스케일 ").Append(s_elite.transform.localScale.x.ToString("F3")) .Append(" (기대 ").Append(s_eliteBaseScale.ToString("F3")).Append(")") .Append(" · MPB 잔류=").Append(WL.Combat.Reaction.MobHitFlash.HasAnyBlock(s_elite)) .Append(UnityEngine.Mathf.Abs(s_elite.transform.localScale.x - s_eliteBaseScale) < 0.0005f ? " → PASS" : " → 🔴 FAIL").Append("\n\n"); } // ═══════════════════════════════════════════════ C. 연계(킬캠 · 드랍 · 티어) static void SectionC(System.Text.StringBuilder sb) { sb.Append("## C. 연계 — 킬캠(811d) · 드랍(813f) · 임팩트 티어(811gh)\n"); // C-1 드랍 버스트 수 — 실제 함수 실행 var loot = WL.Combat.Loot.WLLootSettings.Instance; if (loot == null) sb.Append("C-1 🔴 WLLootSettings 없음\n"); else { int min = int.MaxValue, max = int.MinValue, zero = 0; for (int i = 0; i < 200; i++) { int c = WL.Combat.Loot.LootBurst.BurstCount(loot, eSubRol.Elite); if (c < min) min = c; if (c > max) max = c; if (c <= 0) zero++; } int nrm = WL.Combat.Loot.LootBurst.BurstCount(loot, eSubRol.None); int bos = WL.Combat.Loot.LootBurst.BurstCount(loot, eSubRol.Boss); sb.Append("C-1 LootBurst.BurstCount(Elite) 200회 → ").Append(min).Append("~").Append(max) .Append(" · 0건 ").Append(zero).Append("회 (0 이면 「드랍 100 %」 성립)") .Append(" · 일반=").Append(nrm).Append(" 보스=").Append(bos).Append("\n"); sb.Append(" 버스트 1건은 백(").Append(loot.burstRollsBag ? "8130001 추첨" : "추첨 안 함") .Append(") → 빈손이면 골드(비중 ").Append(loot.burstGoldShare.ToString("F2")).Append(")로 대체 = 무조건 무언가 떨어진다\n"); } // C-2 임팩트 티어 — bossKillIncludesElite 실값(소스 ImpactTier.cs:130 의 유일한 분기) var tier = WL.Combat.Reaction.WLImpactTierSettings.Instance; sb.Append("C-2 ImpactTier.bossKillIncludesElite = ").Append(tier == null ? "?" : tier.bossKillIncludesElite.ToString()) .Append(" (이 값이 1 이면 Elite 처치가 BossKill 티어 = 히트스톱 ") .Append(tier != null && tier.hitStopMs != null && tier.hitStopMs.Length > 3 ? tier.hitStopMs[3].ToString("F0") : "?") .Append(" ms · 셰이크 배수 ") .Append(tier != null && tier.shakeMultiplier != null && tier.shakeMultiplier.Length > 3 ? tier.shakeMultiplier[3].ToString("F2") : "?") .Append(") · 이번 작업에서 값 변경 없음(이미 1)\n"); // C-3 킬캠 — 리플렉션으로 구독시키고 진짜 Killed 를 올린다 float ts0 = UnityEngine.Time.timeScale; var camType = typeof(WL.CombatCamera.CombatCameraDirector); var reg = camType.GetMethod("Register", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); if (reg == null || s_elite == null) sb.Append("C-3 🔴 Register/엘리트 없음 — 건너뜀\n"); else { WL.Combat.Core.CombatEvents.Killed.Clear(); WL.Combat.Core.CombatEvents.Spawned.Clear(); WL.Combat.Core.CombatEvents.ComboStage.Clear(); reg.Invoke(null, null); WL.CombatCamera.CombatCameraDirector.ResetDiagnostics(); WL.CombatCamera.CombatCameraDirector.ResetRun(); WL.CombatCamera.CombatCameraDirector.ResetCooldown(); WL.Combat.Core.CombatEvents.RaiseKilled(s_elite); int eliteCam = WL.CombatCamera.CombatCameraDirector.KillCamCount; int eliteRole = WL.CombatCamera.CombatCameraDirector.KillCamSkippedRole; string eliteInfo = WL.CombatCamera.CombatCameraDirector.LastInfo; var slow = WL.CombatCamera.CombatCameraDirector.LastSlowMoResult; WL.CombatCamera.CombatCameraDirector.ResetCooldown(); int camBeforeNormal = WL.CombatCamera.CombatCameraDirector.KillCamCount; if (s_normal != null) WL.Combat.Core.CombatEvents.RaiseKilled(s_normal); int normalCam = WL.CombatCamera.CombatCameraDirector.KillCamCount - camBeforeNormal; int normalRole = WL.CombatCamera.CombatCameraDirector.KillCamSkippedRole - eliteRole; sb.Append("C-3 킬캠: Killed(Elite) → KillCamCount ").Append(eliteCam) .Append(" · 역할 스킵 ").Append(eliteRole) .Append(" · 슬로모 ").Append(slow).Append(" · info=\"").Append(eliteInfo).Append("\"\n"); sb.Append(" 대조 Killed(일반 몹) → KillCamCount +").Append(normalCam) .Append(" · 역할 스킵 +").Append(normalRole).Append(" (killCamOnNormal=0 이라 스킵이 정상)\n"); sb.Append(" 판정: 엘리트 킬캠 조건 ").Append(eliteCam >= 1 ? "열림 PASS" : "🔴 닫힘").Append("\n"); WL.CombatCamera.CombatCameraDirector.ReleaseAll(); WL.Combat.Core.TimeScaleArbiter.ReleaseAll(); WL.Combat.Core.CombatEvents.Killed.Clear(); WL.Combat.Core.CombatEvents.Spawned.Clear(); WL.Combat.Core.CombatEvents.ComboStage.Clear(); } UnityEngine.Time.timeScale = ts0; sb.Append(" Time.timeScale ").Append(ts0.ToString("F2")).Append(" → ").Append(UnityEngine.Time.timeScale.ToString("F2")).Append(" (복원)\n\n"); } // ═══════════════════════════════════════════════ D. GC static void SectionD(System.Text.StringBuilder sb) { sb.Append("## D. GC (0 B 목표)\n"); if (s_elite == null) { sb.Append("🔴 건너뜀\n\n"); return; } WL.Combat.Reaction.WLEliteSettings.RuntimeDisabled = false; // 상태·MPB 를 미리 만들어 두고(1회 할당은 풀 크기 상한) 반복분만 잰다 WL.Combat.Reaction.EliteMarker.ProbeSpawned(s_elite); WL.Combat.Reaction.EliteMarker.ProbeKilled(s_elite, eSubRol.Elite); long a0 = System.GC.GetAllocatedBytesForCurrentThread(); for (int i = 0; i < 100; i++) WL.Combat.Reaction.EliteMarker.ProbeSpawned(s_elite); long a1 = System.GC.GetAllocatedBytesForCurrentThread(); for (int i = 0; i < 100; i++) { WL.Combat.Reaction.EliteMarker.ProbeSpawned(s_elite); WL.Combat.Reaction.EliteMarker.ProbeKilled(s_elite, eSubRol.Elite); } long a2 = System.GC.GetAllocatedBytesForCurrentThread(); sb.Append("Spawned ×100 = ").Append(a1 - a0).Append(" B · Spawned+Killed ×100 = ").Append(a2 - a1).Append(" B\n"); sb.Append("(killTimeLogEnabled=").Append(WL.Combat.Reaction.WLEliteSettings.Instance != null && WL.Combat.Reaction.WLEliteSettings.Instance.killTimeLogEnabled) .Append(" · 켜져 있으면 Debug.Log 문자열이 잡힌다 = 값이 0 이 아니면 그 로그분)\n\n"); } // ═══════════════════════════════════════════════ E. C8 static void SectionE(System.Text.StringBuilder sb) { sb.Append("## E. C8 — WLEliteSettings 를 끄면 원본 동작 100 %\n"); if (s_elite == null) { sb.Append("🔴 건너뜀\n\n"); return; } WL.Combat.Reaction.EliteMarker.ClearAll(); s_elite.transform.localScale = s_eliteBaseScale * UnityEngine.Vector3.one; WL.Combat.Reaction.EliteMarker.ResetDiagnostics(); WL.Combat.Reaction.WLEliteSettings.RuntimeDisabled = true; sb.Append("Enabled=").Append(WL.Combat.Reaction.WLEliteSettings.Enabled).Append(" (기대 False)\n"); WL.Combat.Reaction.EliteMarker.ProbeSpawned(s_elite); bool sizeOk = UnityEngine.Mathf.Abs(s_elite.transform.localScale.x - s_eliteBaseScale) < 0.0005f; bool tintOff = !WL.Combat.Reaction.MobHitFlash.HasEliteTint(s_elite); bool blockOff = !WL.Combat.Reaction.MobHitFlash.HasAnyBlock(s_elite); sb.Append("크기 ").Append(s_elite.transform.localScale.x.ToString("F3")).Append(" (원본 ").Append(s_eliteBaseScale.ToString("F3")).Append(")") .Append(" · 색 ").Append(tintOff ? "0" : "🔴 남음").Append(" · MPB ").Append(blockOff ? "0" : "🔴 남음") .Append(" · SkippedDisabled=").Append(WL.Combat.Reaction.EliteMarker.SkippedDisabled) .Append(sizeOk && tintOff && blockOff ? " → PASS" : " → 🔴 FAIL").Append("\n"); WL.Combat.Reaction.WLEliteSettings.RuntimeDisabled = false; sb.Append("\n"); } // ═══════════════════════════════════════════════ F. 원본 · 배선 static void SectionF(System.Text.StringBuilder sb) { sb.Append("## F. 원본 무수정 · 배선\n"); string[] originals = new string[] { "Assets/Script/Character/Actor.cs", "Assets/Script/Character/Mob/MobActor.cs", "Assets/Script/Mgr/MobControlMgr.cs", "Assets/WL/Combat/Core/CombatEvents.cs", "Assets/WL/Combat/Core/WLCombatCoreSettings.cs", }; for (int i = 0; i < originals.Length; i++) { if (!System.IO.File.Exists(originals[i])) { sb.Append(" ").Append(originals[i]).Append(" 🔴 없음\n"); continue; } string t = System.IO.File.ReadAllText(originals[i]); sb.Append(" ").Append(originals[i]).Append(" — \"EliteMarker\" ").Append(t.Contains("EliteMarker") ? "🔴 있음" : "0") .Append(" · \"813s\" ").Append(t.Contains("813s") ? "🔴 있음" : "0").Append("\n"); } string flash = System.IO.File.Exists(kFlash) ? System.IO.File.ReadAllText(kFlash) : ""; sb.Append(" MobHitFlash 합성 배선 — SetEliteTint ").Append(flash.Contains("SetEliteTint") ? "O" : "🔴") .Append(" · Apply 안 s.elite ").Append(flash.Contains("if (s.elite)") ? "O" : "🔴") .Append(" · RestoreAll 포함 ").Append(flash.Contains("!s.tint && !s.elite") ? "O" : "🔴").Append("\n\n"); } // ═══════════════════════════════════════════════ 정리 · 헬퍼 static void Cleanup(System.Text.StringBuilder sb) { WL.Combat.Reaction.WLEliteSettings.RuntimeDisabled = false; try { WL.Combat.Reaction.EliteMarker.ClearAll(); } catch (System.Exception) { } try { WL.Combat.Reaction.MobHitFlash.ClearAll(); } catch (System.Exception) { } try { WL.Combat.Reaction.ImpactRunner.ResetForceClock(); } catch (System.Exception) { } if (s_root != null) { UnityEngine.Object.DestroyImmediate(s_root); s_root = null; } s_eliteGo = null; s_normalGo = null; s_elite = null; s_normal = null; sb.Append("\n## 정리: 임시 오브젝트 파괴 · 상태 비움 완료\n"); } static bool Exists(string path) { return UnityEditor.AssetDatabase.LoadAssetAtPath(path) != null; } static string Path(UnityEngine.Transform t) { string p = t.name; while (t.parent != null) { t = t.parent; p = t.name + "/" + p; } return p; } /// 단일 라인 JSON 에서 key == value 인 객체 하나를 문자열로 잘라 온다. static string Row(string json, string key, string value) { if (json == null || value == null) return null; string needle = "\"" + key + "\": \"" + value + "\""; int i = json.IndexOf(needle, System.StringComparison.Ordinal); if (i < 0) return null; int s = json.LastIndexOf('{', i); int e = json.IndexOf('}', i); return s < 0 || e < 0 ? null : json.Substring(s, e - s + 1); } static string F(string row, string key) { if (row == null) return "?"; string needle = "\"" + key + "\": \""; int i = row.IndexOf(needle, System.StringComparison.Ordinal); if (i < 0) return "?"; i += needle.Length; int e = row.IndexOf('"', i); return e < 0 ? "?" : row.Substring(i, e - i); } static float Num(string s) { float v; return float.TryParse(s, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out v) ? v : 0f; } }