Project_WL/AgentScripts/WL813f_Probe.cs

378 lines
24 KiB
C#
Raw Normal View History

[WL-813f] 전리품 연출 + 드랍률 (#813) CombatEvents.Killed 구독 → 드랍 산포(0.6~1.2 m · 등급별 튀는 높이 0.35+0.08/등급) · 등급 4 이상 빔(보유 FX_LootDrop_Blue/Purple/Gold) + s015_GoldPowerUp · 자동 줍기 반경 1.5 m(원본 자석 상태기를 리플렉션으로 켜기만) · 보스 3~5 · 엘리트 1~2 폭발 드랍(RandomBag 재추첨 · 빈손이면 골드 25%). 값은 전부 WLLootSettings.asset(C45) · 동시 빔은 EffectBudget.Heavy 등록. 원본 수정 = DropItem.cs 2줄(Set 산포 훅 · ItemTouch 획득 통지 훅) 뿐. Actor.cs · MobActor.cs · DropItemInfo.cs · Assets/WL/Combat/Core/** 0줄. 데이터(S): RandomBag.json 새 백 8130001 5행(총 드랍 20.95% · 기준서 15~25%) · MonsterAppear.json 813e 존 6행(813001~813005 · 813010)의 n_DropReward 만 새 백으로 · 나머지 46행 · RandomBag 기존 197행 바이트 무변경. ⚠ butler.xlsm 동기화 필요. 검증: 게이트 PASS(oneshot 1.4분 · error CS 0 / resident up_to_date · 콘솔 error 0) · 에디트 모드 프로브(AgentScripts/WL813f_Probe.cs · 덤프 WL813f_PROBE.txt): 산포 1000회 0.601~1.199 m · 폭발 2000회 boss 3~5 · elite 1~2 · normal 0 · 가짜 Killed 3건 구독 동작 · 자석 리플렉션 OK(0→1 · 중복 방지) · 줍기 경계 1.49 in / 1.51 out / 착지 전 제외 · 빔 4건 → Heavy 상한 2 스폰 · 2 스킵 → CleanupAll 후 슬롯 0 · C8(RuntimeDisabled) 시 산포/빔/획득/폭발 전부 0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 16:19:48 +00:00
// 발주서 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<System.Collections.Generic.Dictionary<string, string>> ReadRows(string path)
{
var list = new System.Collections.Generic.List<System.Collections.Generic.Dictionary<string, string>>();
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<string, string>();
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<DropItem>();
var col = go.AddComponent<UnityEngine.SphereCollider>();
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<MobActor>();
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<DropItem>();
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();
}
/// <summary>테이블에서 해당 등급의 아이템 ID 를 하나 찾는다(없으면 새 백의 첫 행 · 그것도 없으면 골드).</summary>
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
/// <summary>Play 중 카운터 스냅샷(실전투 QA 용).</summary>
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");
}
/// <summary>Play 중 N초 감시 — 드랍/획득 수와 줍기 완료 시간을 잰다.</summary>
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");
}
}