Project_WL/AgentScripts/WL813s_Probe.cs

522 lines
33 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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<string> s_log = new System.Collections.Generic.List<string>();
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<UnityEngine.GameObject>(kMap);
if (map == null) sb.Append("🔴 ").Append(kMap).Append(" 로드 실패\n");
else
{
var mgrs = map.GetComponentsInChildren<MobControlMgr>(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<UnityEngine.MeshRenderer>(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<UnityEngine.GameObject>("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<MobActor>();
if (s_elite == null) s_elite = s_eliteGo.AddComponent<MobActor>();
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<UnityEngine.GameObject>("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<MobActor>();
if (s_normal != null) { s_normal.m_Role = eRole.Mob; s_normal.m_SubRole = eSubRol.None; }
}
var rends = s_eliteGo.GetComponentsInChildren<UnityEngine.Renderer>(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<UnityEngine.Object>(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;
}
/// <summary>단일 라인 JSON 에서 key == value 인 객체 하나를 문자열로 잘라 온다.</summary>
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;
}
}