// 발주서 WL-813f — 전리품 연출(산포 · 등급 빔 · 자동 줍기 · 폭발 드랍) 검증 프로브 // // 실행 (CLAUDE.md §1: using 금지 · 네임스페이스 풀어 쓰기) // 에디트 모드 : unity command run_script --file AgentScripts/WL813f_Probe.cs --entry WL813f_Probe.Edit --timeout 300 // Play(인게임): unity command run_script --file AgentScripts/WL813f_Probe.cs --entry WL813f_Probe.Status // unity command run_script --file AgentScripts/WL813f_Probe.cs --entry WL813f_Probe.Watch --args '[12.0]' // // Edit() 가 재는 것 — 씬/프리팹/에셋 변경 0(가짜 오브젝트는 전부 DestroyImmediate 로 회수): // (a) WLLootSettings 로드 · 값 전수 · 등급 빔 프리팹 참조 해소(등급 1~3 null · 4~9 실물) // (b) 산포 표집 1000회 — 반경 min/max/mean(설정 0.6~1.2 준수) · 부채꼴 각 분포 // (c) 등급별 튀는 높이 1~9 // (d) 폭발 건수 분포 — 보스 3~5 · 엘리트 1~2 · 일반 0 (2000회) // (e) 가짜 Killed(비활성 MobActor) → 구독 동작 · BurstPlanned · C8 off 시 0 // (f) 자석 리플렉션 — 원본 private 필드 3종 + ProjectileData.Speed 를 찾았는가 · EngageMagnet 전/후 상태 // (g) 자동 줍기 반경 경계 — 1.5 m 안/밖 · 콜라이더 꺼짐(착지 전) 제외 // (h) 등급 빔 + EffectBudget.Heavy 상한(코어 값) — 초과분 스킵 · CleanupAll 후 슬롯 0 // (i) 동시 드랍 상한 · 원본 풀 상한(DropItemInfo: 코인 30 · 아이템 2) // (j) C8 — RuntimeDisabled 시 훅 2개가 원본 값을 그대로 두는가 // (k) 데이터 — RandomBag 새 백 8130001 5행 총확률 · MonsterAppear 813e 6행 참조 // // 수치는 전부 SO/테이블에서 읽는다(C45). 프로브 상수는 "어디를 보나" 뿐이다. public static class WL813f_Probe { const string kBagJson = "Assets/ResWork/Table/Export/RandomBag.json"; const string kAppearJson = "Assets/ResWork/Table/Export/MonsterAppear.json"; const string kOutDir = "AgentScripts/staging/WL813f"; const int kNewBagId = 8130001; static System.Text.StringBuilder s; static void L(string t) { s.AppendLine(t); UnityEngine.Debug.Log("[WL813f] " + t); } static string Abs(string rel) { return System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), rel.Replace('/', System.IO.Path.DirectorySeparatorChar)); } static string Flush(string name) { try { System.IO.Directory.CreateDirectory(Abs(kOutDir)); System.IO.File.WriteAllText(Abs(kOutDir + "/" + name), s.ToString(), new System.Text.UTF8Encoding(true)); } catch (System.Exception e) { UnityEngine.Debug.LogWarning("[WL813f] 로그 저장 실패: " + e.Message); } return s.ToString(); } // ── 아주 얕은 JSON 배열 파서(행 = {"k": "v", ...}) ───────────────────────── static System.Collections.Generic.List> ReadRows(string path) { var list = new System.Collections.Generic.List>(); string txt = System.IO.File.ReadAllText(Abs(path)); foreach (System.Text.RegularExpressions.Match m in System.Text.RegularExpressions.Regex.Matches(txt, "\\{[^{}]*\\}")) { var d = new System.Collections.Generic.Dictionary(); foreach (System.Text.RegularExpressions.Match kv in System.Text.RegularExpressions.Regex.Matches(m.Value, "\"([^\"]+)\"\\s*:\\s*\"([^\"]*)\"")) d[kv.Groups[1].Value] = kv.Groups[2].Value; list.Add(d); } return list; } // ── 가짜 드랍(비활성 GO → Awake 실행 안 됨 · 콜라이더는 직접 붙인다) ──────── static DropItem MakeFakeDrop(string name, UnityEngine.Vector3 pos, bool colliderOn) { var go = new UnityEngine.GameObject(name); go.hideFlags = UnityEngine.HideFlags.HideAndDontSave; go.SetActive(false); var di = go.AddComponent(); var col = go.AddComponent(); col.isTrigger = true; col.enabled = colliderOn; di.m_Collider = col; go.transform.position = pos; go.SetActive(true); // Awake 는 이미 지나갔다(비활성 상태로 AddComponent) return di; } static Actor MakeFakeMob(string name, eSubRol subRole, UnityEngine.Vector3 pos) { var go = new UnityEngine.GameObject(name); go.hideFlags = UnityEngine.HideFlags.HideAndDontSave; go.SetActive(false); // 비활성으로 만들어 Awake/Start 를 태우지 않는다 var a = go.AddComponent(); a.m_SubRole = subRole; go.transform.position = pos; return a; } static void Kill(UnityEngine.Object o) { if (o != null) UnityEngine.Object.DestroyImmediate(o); } // ═══════════════════════════════════════════════════════════════ Edit public static string Edit() { s = new System.Text.StringBuilder(); L("=== WL-813f 전리품 연출 프로브(에디트 모드) " + System.DateTime.Now.ToString("HH:mm:ss") + " ==="); WL.Combat.Loot.WLLootSettings.ClearCache(); var cfg = WL.Combat.Loot.WLLootSettings.Instance; if (cfg == null) { L("!! WLLootSettings.asset 로드 실패 — Resources/WL/WLLootSettings"); return Flush("EDIT.txt"); } WL.Combat.Loot.LootBurst.EnsureSubscribed(); WL.Combat.Loot.LootBurst.ResetDiagnostics(); WL.Combat.Core.EffectBudget.ResetAll(); // (a) 설정 · 빔 프리팹 참조 L(""); L("[a] WLLootSettings — Enabled=" + WL.Combat.Loot.WLLootSettings.Enabled + " verbose=" + cfg.verboseLog); L(" 산포 " + cfg.scatterRadiusMin + "~" + cfg.scatterRadiusMax + " m · navSnap " + cfg.scatterNavSnapRadius + " · 튀기 base " + cfg.bounceHeightBase + " +" + cfg.bounceHeightPerGrade + "/등급 · max " + cfg.bounceHeightMax); L(" 빔 minGrade " + cfg.beamMinGrade + " · scale " + cfg.beamScale + " · maxLife " + cfg.beamMaxLifetimeSeconds + " · budget " + cfg.beamUseEffectBudget + " · sound " + cfg.beamSound + " vol " + cfg.beamSoundVolume); L(" 줍기 " + cfg.autoPickupEnabled + " r=" + cfg.autoPickupRadius + " m · scan " + cfg.autoPickupScanIntervalSeconds + " s · magnet rise " + cfg.magnetRiseHeight + "/" + cfg.magnetRiseSpeed); L(" 폭발 elite " + cfg.eliteBurstMin + "~" + cfg.eliteBurstMax + " · boss " + cfg.bossBurstMin + "~" + cfg.bossBurstMax + " · normal " + cfg.normalBurstCount + " · 간격 " + cfg.burstIntervalSeconds + " s · bagRoll " + cfg.burstRollsBag + " · goldShare " + cfg.burstGoldShare + " · cluster " + cfg.burstUseClusterBudget); L(" 상한 maxConcurrentDrops " + cfg.maxConcurrentDrops + " · beamSuppressAbove " + cfg.beamSuppressAboveDrops); var sb = new System.Text.StringBuilder(); for (int g = 1; g <= WL.Combat.Loot.WLLootSettings.GradeSlots; g++) { var pf = WL.Combat.Loot.LootBurst.BeamPrefab(cfg, g); sb.Append(g).Append("=").Append(pf != null ? pf.name : "-").Append(" "); } L(" 등급별 빔: " + sb.ToString().Trim()); // (b) 산포 표집 L(""); var origin = new UnityEngine.Vector3(10f, 0f, 10f); float mn = 999f, mx = 0f, sum = 0f; int n = 1000; for (int i = 0; i < n; i++) { var p = WL.Combat.Loot.LootBurst.SampleScatter(cfg, origin); float d = UnityEngine.Vector3.Distance(new UnityEngine.Vector3(origin.x, 0f, origin.z), new UnityEngine.Vector3(p.x, 0f, p.z)); if (d < mn) mn = d; if (d > mx) mx = d; sum += d; } L("[b] 산포 " + n + "회 — min " + mn.ToString("F3") + " · max " + mx.ToString("F3") + " · mean " + (sum / n).ToString("F3") + " m (설정 " + cfg.scatterRadiusMin + "~" + cfg.scatterRadiusMax + ") · NavMesh 없음 → 링 좌표 그대로"); // (c) 등급별 튀는 높이 var hb = new System.Text.StringBuilder(); for (int g = 1; g <= 9; g++) { float h = UnityEngine.Mathf.Min(cfg.bounceHeightMax, cfg.bounceHeightBase + UnityEngine.Mathf.Max(0, g - 1) * cfg.bounceHeightPerGrade); hb.Append(g).Append(":").Append(h.ToString("F2")).Append(" "); } L("[c] 튀는 높이(m) — " + hb.ToString().Trim()); // (d) 폭발 건수 분포 int[] boss = new int[8], elite = new int[8]; int normal = 0; for (int i = 0; i < 2000; i++) { boss[UnityEngine.Mathf.Clamp(WL.Combat.Loot.LootBurst.BurstCount(cfg, eSubRol.Boss), 0, 7)]++; elite[UnityEngine.Mathf.Clamp(WL.Combat.Loot.LootBurst.BurstCount(cfg, eSubRol.Elite), 0, 7)]++; normal += WL.Combat.Loot.LootBurst.BurstCount(cfg, eSubRol.None); } L("[d] 폭발 건수 2000회 — boss " + Hist(boss) + " · elite " + Hist(elite) + " · normal 합계 " + normal); // (e) 가짜 Killed L(""); var mobB = MakeFakeMob("__fakeBoss", eSubRol.Boss, new UnityEngine.Vector3(5f, 0f, 5f)); var mobE = MakeFakeMob("__fakeElite", eSubRol.Elite, new UnityEngine.Vector3(6f, 0f, 5f)); var mobN = MakeFakeMob("__fakeNormal", eSubRol.None, new UnityEngine.Vector3(7f, 0f, 5f)); WL.Combat.Loot.LootBurst.ResetDiagnostics(); int coreOn = WL.Combat.Core.WLCombatCoreSettings.Enabled ? 1 : 0; WL.Combat.Core.CombatEvents.RaiseKilled(mobB); int p1 = WL.Combat.Loot.LootBurst.BurstPlanned; int c1 = WL.Combat.Loot.LootBurst.LastBurstCount; WL.Combat.Core.CombatEvents.RaiseKilled(mobE); int p2 = WL.Combat.Loot.LootBurst.BurstPlanned; int c2 = WL.Combat.Loot.LootBurst.LastBurstCount; WL.Combat.Core.CombatEvents.RaiseKilled(mobN); int p3 = WL.Combat.Loot.LootBurst.BurstPlanned; int c3 = WL.Combat.Loot.LootBurst.LastBurstCount; L("[e] 가짜 Killed — core Enabled=" + coreOn + " · 구독 " + WL.Combat.Loot.LootBurst.Subscribed + " · Killed 처리 " + WL.Combat.Loot.LootBurst.KilledHandled + " · boss 계획 " + c1 + "(누적 " + p1 + ") · elite " + c2 + "(누적 " + p2 + ") · normal " + c3 + "(누적 " + p3 + ")" + " · 실제 드랍 " + WL.Combat.Loot.LootBurst.BurstDropped + " (DropItemInfo.Ins=" + (DropItemInfo.Ins != null) + " · table_randombag.Ins=" + (table_randombag.Ins != null) + ")"); // (f) 자석 리플렉션 L(""); var d1 = MakeFakeDrop("__fakeDrop1", new UnityEngine.Vector3(0f, 0f, 0f), true); int before = WL.Combat.Loot.LootBurst.MagnetState(d1); bool eng = WL.Combat.Loot.LootBurst.EngageMagnet(d1); int after = WL.Combat.Loot.LootBurst.MagnetState(d1); bool again = WL.Combat.Loot.LootBurst.EngageMagnet(d1); L("[f] 자석 리플렉션 OK=" + WL.Combat.Loot.LootBurst.ReflectionOk + " · m_Magnet " + before + "→" + after + " (engage " + eng + " · 재호출 " + again + " = 중복 방지)" + " · MagnetEngaged " + WL.Combat.Loot.LootBurst.MagnetEngaged); // (g) 자동 줍기 반경 경계 var pc = new UnityEngine.Vector3(0f, 0f, 0f); float r = cfg.autoPickupRadius; var din = MakeFakeDrop("__in", new UnityEngine.Vector3(r - 0.01f, 0f, 0f), true); var dout = MakeFakeDrop("__out", new UnityEngine.Vector3(r + 0.01f, 0f, 0f), true); var dair = MakeFakeDrop("__air", new UnityEngine.Vector3(r - 0.5f, 0f, 0f), false); // 아직 착지 전(콜라이더 off) L("[g] 줍기 반경 " + r + " m — " + (r - 0.01f).ToString("F2") + "m " + WL.Combat.Loot.LootBurst.InPickupRadius(din, pc, cfg) + " · " + (r + 0.01f).ToString("F2") + "m " + WL.Combat.Loot.LootBurst.InPickupRadius(dout, pc, cfg) + " · 착지 전(콜라이더 off) " + WL.Combat.Loot.LootBurst.InPickupRadius(dair, pc, cfg)); // (h) 등급 빔 + EffectBudget // 에디트 모드에는 table_itemlist.Ins 가 없어 ResolveGrade 가 항상 1 이다(런타임 등급 판정은 QA). // 그래서 **메모리에서만** beamMinGrade 를 1 로 내리고 등급 1 칸에 등급 4 빔을 꽂아 스폰 경로를 실제로 태운다. // 에셋 파일은 저장하지 않는다(SetDirty·SaveAssets 호출 0) — 끝나면 원래 값으로 되돌리고 git status 로 확인한다. L(""); WL.Combat.Loot.LootBurst.ResetDiagnostics(); WL.Combat.Core.EffectBudget.ResetAll(); int heavyLimit = WL.Combat.Core.EffectBudget.Limit(WL.Combat.Core.EffectBudgetKind.Heavy); int probeItemId = FindItemIdOfGrade(cfg.beamMinGrade); L("[h] 등급 판정 — 아이템 " + probeItemId + " → ResolveGrade " + WL.Combat.Loot.LootBurst.ResolveGrade(probeItemId) + " (table_itemlist.Ins=" + (table_itemlist.Ins != null) + " · 에디트 모드는 테이블 미로드 → 항상 1)"); int savedMinGrade = cfg.beamMinGrade; var savedSlot0 = cfg.beamByGrade[0]; cfg.beamByGrade[0] = WL.Combat.Loot.LootBurst.BeamPrefab(cfg, savedMinGrade); // 등급 1 칸 = 등급 4 빔(임시) cfg.beamMinGrade = 1; var drops = new System.Collections.Generic.List(); for (int i = 0; i < heavyLimit + 2; i++) { var di = MakeFakeDrop("__beam" + i, new UnityEngine.Vector3(i, 0f, 0f), true); drops.Add(di); var sp = new UnityEngine.Vector3(i, 0f, 0f); var tp = sp; WL.Combat.Loot.LootBurst.OnDropSpawn(di, probeItemId, 1, ref sp, ref tp); } L(" 빔 강제(minGrade 1 · 프리팹 " + (cfg.beamByGrade[0] != null ? cfg.beamByGrade[0].name : "-") + ") " + (heavyLimit + 2) + "건 — Heavy 상한 " + heavyLimit + " · 스폰 " + WL.Combat.Loot.LootBurst.BeamSpawned + " · 예산 스킵 " + WL.Combat.Loot.LootBurst.BeamSkippedBudget + " · 프리팹 없음 " + WL.Combat.Loot.LootBurst.BeamSkippedNoPrefab + " · Heavy Count " + WL.Combat.Core.EffectBudget.Count(WL.Combat.Core.EffectBudgetKind.Heavy) + " · 사운드 " + WL.Combat.Loot.LootBurst.SoundPlayed + "(SoundInfo.Ins=" + (SoundInfo.Ins != null) + ")" + " · 등록 드랍 " + WL.Combat.Loot.LootBurst.ActiveDropCount() + " · 산포 " + WL.Combat.Loot.LootBurst.Scattered); int beamChildren = 0; for (int i = 0; i < drops.Count; i++) beamChildren += drops[i].transform.childCount; L(" 빔 자식 수 " + beamChildren + " (drop 에 붙었는가 · beamParentToDrop=" + cfg.beamParentToDrop + ")"); WL.Combat.Loot.LootBurst.CleanupAll(); int beamAfter = 0; for (int i = 0; i < drops.Count; i++) beamAfter += drops[i].transform.childCount; L(" CleanupAll 후 Heavy Count " + WL.Combat.Core.EffectBudget.Count(WL.Combat.Core.EffectBudgetKind.Heavy) + " · 등록 드랍 " + WL.Combat.Loot.LootBurst.ActiveDropCount() + " · 남은 빔 자식 " + beamAfter); cfg.beamMinGrade = savedMinGrade; cfg.beamByGrade[0] = savedSlot0; L(" 설정 복원 — beamMinGrade " + cfg.beamMinGrade + " · 등급 1 칸 " + (cfg.beamByGrade[0] == null ? "null(원본)" : cfg.beamByGrade[0].name) + " · 에셋 저장 0(SetDirty/SaveAssets 미호출)"); // (j) C8 — 꺼짐이면 원본 값 그대로 L(""); WL.Combat.Loot.LootBurst.ResetDiagnostics(); WL.Combat.Loot.WLLootSettings.RuntimeDisabled = true; var dC8 = MakeFakeDrop("__c8", new UnityEngine.Vector3(0f, 0f, 0f), true); var sp0 = new UnityEngine.Vector3(1f, 2f, 3f); var tp0 = new UnityEngine.Vector3(4f, 5f, 6f); var spK = sp0; var tpK = tp0; WL.Combat.Loot.LootBurst.OnDropSpawn(dC8, probeItemId, 1, ref spK, ref tpK); WL.Combat.Loot.LootBurst.OnDropPicked(dC8, probeItemId, 1, null); WL.Combat.Core.CombatEvents.RaiseKilled(mobB); L("[j] C8 RuntimeDisabled — start 불변 " + (spK == sp0) + " · target 불변 " + (tpK == tp0) + " · DropsSeen " + WL.Combat.Loot.LootBurst.DropsSeen + " · Picked " + WL.Combat.Loot.LootBurst.PickedCount + " · Killed 처리 " + WL.Combat.Loot.LootBurst.KilledHandled + " · 빔 " + WL.Combat.Loot.LootBurst.BeamSpawned + " · Heavy Count " + WL.Combat.Core.EffectBudget.Count(WL.Combat.Core.EffectBudgetKind.Heavy)); WL.Combat.Loot.WLLootSettings.RuntimeDisabled = false; var spR = sp0; var tpR = tp0; WL.Combat.Loot.LootBurst.OnDropSpawn(dC8, probeItemId, 1, ref spR, ref tpR); L(" 복구 후 — start 변경 " + (spR != sp0) + " · target 변경 " + (tpR != tp0) + " · DropsSeen " + WL.Combat.Loot.LootBurst.DropsSeen + " · 빔 " + WL.Combat.Loot.LootBurst.BeamSpawned); WL.Combat.Loot.LootBurst.CleanupAll(); // (i) 원본 풀 상한 L(""); L("[i] 원본 풀 상한(DropItemInfo.Start 고정) — 코인(itemid 2) 30 · 아이템(-1) 2 · 초과 요청은 조용히 무시" + " · 우리 상한 maxConcurrentDrops " + cfg.maxConcurrentDrops + " (폭발 추가분만 막고 기본 드랍은 막지 않는다)"); // (k) 데이터 L(""); var bagRows = ReadRows(kBagJson); double pMiss = 1.0; int newRows = 0; var bagDetail = new System.Text.StringBuilder(); for (int i = 0; i < bagRows.Count; i++) { if (bagRows[i]["n_DropIndex"] != kNewBagId.ToString()) continue; newRows++; double rate = double.Parse(bagRows[i]["f_DropRate"], System.Globalization.CultureInfo.InvariantCulture); pMiss *= (1.0 - rate); bagDetail.Append("g").Append(bagRows[i]["n_ItemGrade"]).Append("/").Append(bagRows[i]["n_ItemId"]) .Append("@").Append(bagRows[i]["f_DropRate"]).Append(" "); } var appearRows = ReadRows(kAppearJson); int pointed = 0, old = 0; for (int i = 0; i < appearRows.Count; i++) { if (appearRows[i]["n_DropReward"] == kNewBagId.ToString()) pointed++; else old++; } L("[k] 데이터 — RandomBag 총 " + bagRows.Count + "행 · 새 백 " + kNewBagId + " " + newRows + "행 " + bagDetail.ToString().Trim()); L(" 새 백 총 드랍 확률 = " + ((1.0 - pMiss) * 100.0).ToString("F2") + "% (기준서 15~25%) · 원본 순차 추첨(첫 성공에서 break) 기준"); L(" MonsterAppear " + appearRows.Count + "행 — 새 백 참조 " + pointed + "행(813e 존 5 + 보스 1) · 9700001 유지 " + old + "행"); // 정리 for (int i = 0; i < drops.Count; i++) Kill(drops[i] != null ? drops[i].gameObject : null); Kill(d1 != null ? d1.gameObject : null); Kill(din != null ? din.gameObject : null); Kill(dout != null ? dout.gameObject : null); Kill(dair != null ? dair.gameObject : null); Kill(dC8 != null ? dC8.gameObject : null); Kill(mobB != null ? mobB.gameObject : null); Kill(mobE != null ? mobE.gameObject : null); Kill(mobN != null ? mobN.gameObject : null); var runner = WL.Combat.Loot.LootBurst.RunnerOrNull; if (runner != null) Kill(runner.gameObject); WL.Combat.Core.EffectBudget.ResetAll(); L(""); L("정리 완료 — Heavy Count " + WL.Combat.Core.EffectBudget.Count(WL.Combat.Core.EffectBudgetKind.Heavy) + " · ClusterBurst Count " + WL.Combat.Core.EffectBudget.Count(WL.Combat.Core.EffectBudgetKind.ClusterBurst)); return Flush("EDIT.txt"); } static string Hist(int[] h) { var b = new System.Text.StringBuilder(); for (int i = 0; i < h.Length; i++) if (h[i] > 0) b.Append(i).Append(":").Append(h[i]).Append(" "); return b.ToString().Trim(); } /// 테이블에서 해당 등급의 아이템 ID 를 하나 찾는다(없으면 새 백의 첫 행 · 그것도 없으면 골드). static int FindItemIdOfGrade(int grade) { var rows = ReadRows(kBagJson); for (int i = 0; i < rows.Count; i++) if (rows[i]["n_DropIndex"] == kNewBagId.ToString() && rows[i]["n_ItemGrade"] == grade.ToString()) return int.Parse(rows[i]["n_ItemId"]); return 2; } // ═══════════════════════════════════════════════════════════════ Play /// Play 중 카운터 스냅샷(실전투 QA 용). public static string Status() { s = new System.Text.StringBuilder(); L("=== WL-813f Status " + System.DateTime.Now.ToString("HH:mm:ss") + " ==="); L("Killed 처리 " + WL.Combat.Loot.LootBurst.KilledHandled + " · 드랍 " + WL.Combat.Loot.LootBurst.DropsSeen + " · 산포 " + WL.Combat.Loot.LootBurst.Scattered + " · 획득 " + WL.Combat.Loot.LootBurst.PickedCount); L("빔 " + WL.Combat.Loot.LootBurst.BeamSpawned + " · 예산 스킵 " + WL.Combat.Loot.LootBurst.BeamSkippedBudget + " · 프리팹 없음 " + WL.Combat.Loot.LootBurst.BeamSkippedNoPrefab + " · 과밀 스킵 " + WL.Combat.Loot.LootBurst.BeamSkippedCrowded + " · 사운드 " + WL.Combat.Loot.LootBurst.SoundPlayed); L("자석 " + WL.Combat.Loot.LootBurst.MagnetEngaged + " · 폭발 계획 " + WL.Combat.Loot.LootBurst.BurstPlanned + " · 실드랍 " + WL.Combat.Loot.LootBurst.BurstDropped + " · 상한 스킵 " + WL.Combat.Loot.LootBurst.BurstSkippedCap); L("살아 있는 드랍 " + WL.Combat.Loot.LootBurst.ActiveDropCount() + " · Heavy " + WL.Combat.Core.EffectBudget.Count(WL.Combat.Core.EffectBudgetKind.Heavy) + "/" + WL.Combat.Core.EffectBudget.Limit(WL.Combat.Core.EffectBudgetKind.Heavy) + " · 마지막 등급 " + WL.Combat.Loot.LootBurst.LastGrade + " · 마지막 산포 " + WL.Combat.Loot.LootBurst.LastScatterDistance.ToString("F2") + " m"); return Flush("STATUS.txt"); } /// Play 중 N초 감시 — 드랍/획득 수와 줍기 완료 시간을 잰다. public static string Watch(double seconds) { s = new System.Text.StringBuilder(); if (!UnityEngine.Application.isPlaying) { L("Play 중에만 쓸 수 있다."); return Flush("WATCH.txt"); } L("=== WL-813f Watch " + seconds.ToString("F0") + "s ==="); int d0 = WL.Combat.Loot.LootBurst.DropsSeen, p0 = WL.Combat.Loot.LootBurst.PickedCount, k0 = WL.Combat.Loot.LootBurst.KilledHandled, b0 = WL.Combat.Loot.LootBurst.BeamSpawned, m0 = WL.Combat.Loot.LootBurst.MagnetEngaged, r0 = WL.Combat.Loot.LootBurst.BurstDropped; double t0 = UnityEngine.Time.realtimeSinceStartupAsDouble; while (UnityEngine.Time.realtimeSinceStartupAsDouble - t0 < seconds) System.Threading.Thread.Sleep(50); L("처치 " + (WL.Combat.Loot.LootBurst.KilledHandled - k0) + " · 드랍 " + (WL.Combat.Loot.LootBurst.DropsSeen - d0) + " · 획득 " + (WL.Combat.Loot.LootBurst.PickedCount - p0) + " · 빔 " + (WL.Combat.Loot.LootBurst.BeamSpawned - b0) + " · 자석 " + (WL.Combat.Loot.LootBurst.MagnetEngaged - m0) + " · 폭발 드랍 " + (WL.Combat.Loot.LootBurst.BurstDropped - r0)); return Flush("WATCH.txt"); } }