// WL813l_Probe.cs — #813l(성장 체감: 전투력 % 토스트 · 레벨업 팝업 전투력 줄 · 처치 타수 비교) 에디트 모드 검증 프로브 // 에디트 모드 전용 · Play 불필요 · 로그인 0 · 씬 무수정(임시 GameObject 는 즉시 파괴) // 유일한 에셋 쓰기 = `Assets/WL/UI/Settings/Resources/WL/WLPowerDeltaSettings.asset` **생성**(발주 산출물 · EnsureAsset) // // 상주 에디터: // unity command run_script --project-path --file AgentScripts/WL813l_Probe.cs --entry WL813l_Probe.RunAll --timeout 600 // 결과: Console + AgentScripts/WL813l_PROBE.txt (RESULT PASS / RESULT FAIL n) // // 검사 (발주서 WL-813l §1 · §2) // ⓪ 에셋 생성·로드 · 값 전수 // ① 전투력 산식(순수 함수 · 원본 스탯 규약 실측 대조) + 실제 ActorStatInfo 읽기 // ② 전투력 % 토스트 — 가짜 LevelUp · 값 변화 · 하락 무시 · 잡음 차단 · 쿨다운 병합 // ③ 레벨업 팝업 전투력 줄 — 813y StatPopup 에 실제로 한 줄이 붙는가(구독 순서 무관) // ④ 처치 타수 비교 — 성장 전후 창 · "이제 N방 → M방" · 813p ZoneCleared 에서 1회 // ⑤ GC 0 — 폴링 Tick × 1000 · 카운터 이벤트 × 1000 // ⑥ C8 — WLPowerDeltaSettings.enabled_ = 0 → 표시 0 · 팝업 줄 0 // // 에디트 모드 한계(보고서 「미확인」으로 올린다) // · 진짜 PC 는 ServerData 없이 m_Stat 이 안 만들어진다 → 산식은 손으로 채운 ActorStatInfo 로 검증한다. // · Canvas 가 없어 UnitsPerPx = 1(px == 유닛). 실제 배치 px 는 병합 후 QA 캡처로 본다. using System; using System.IO; using System.Text; using UnityEngine; using UnityEngine.UI; using TMPro; using WL.Combat.Core; using WL.Combat.Growth; using WL.Combat.Run; using WL.UI; using UnityEditor; public static class WL813l_Probe { const string OutCommit = "AgentScripts/WL813l_PROBE.txt"; const string AssetPath = "Assets/WL/UI/Settings/Resources/WL/WLPowerDeltaSettings.asset"; static StringBuilder sb; static int s_fail; static GameObject s_pcGo, s_mobGo, s_canvasGo; static PCActor s_pc; static Actor s_mob; static PCActor s_prevMyPc; static bool s_prevBMyPc; static StatPopup s_popup; public static object RunAll() { sb = new StringBuilder(); s_fail = 0; sb.AppendLine("# WL813l Probe " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " (edit mode · Play 0 · 로그인 0)"); sb.AppendLine("playMode=" + Application.isPlaying); try { Sec0_Asset(); Setup(); Sec1_Formula(); Sec2_Toast(); Sec3_PopupLine(); Sec3b_ToastNode(); Sec4_Hits(); Sec5_Gc(); Sec6_Rollback(); } catch (Exception ex) { s_fail++; L("EXCEPTION " + ex); } finally { Teardown(); } sb.AppendLine(); sb.AppendLine(s_fail == 0 ? "RESULT PASS" : "RESULT FAIL " + s_fail); string text = sb.ToString(); try { File.WriteAllText(Path.Combine(Directory.GetCurrentDirectory(), OutCommit), text, new UTF8Encoding(true)); } catch { } Debug.Log(text); return text; } static void L(string s) { sb.AppendLine(s); } static string P(bool ok) { if (!ok) s_fail++; return ok ? "[OK]" : "[FAIL]"; } // ───────────────────────────────────────── ⓪ 에셋 static void Sec0_Asset() { L(""); L("── ⓪ 설정 에셋 (C45 · 값 전부 SO)"); var s = EnsureAsset(); L(" " + P(s != null) + " 에셋 로드 " + AssetPath); if (s == null) return; WLPowerDeltaSettings.RuntimeDisabled = false; L(" enabled_=" + s.enabled_ + " toastEnabled=" + s.toastEnabled + " statPopupLineEnabled=" + s.statPopupLineEnabled + " hitsPerKillEnabled=" + s.hitsPerKillEnabled); L(" 산식 스탯: atk=" + s.atkStat + "(폴백 " + s.atkFallbackStat + ") cri=" + s.critRateStat + " criDmg=" + s.critDamageStat + "(useCritDamage=" + s.useCritDamage + ") aspd=" + s.attackSpeedStat + "(base " + s.attackSpeedBase + ") hp=" + s.hpStat); L(" 계수: critWeight=" + s.critWeight + " aspdWeight=" + s.attackSpeedWeight + " hpWeight=" + s.hpWeight + " powerScale=" + s.powerScale); L(" 토스트: offset=" + s.toastCenterOffsetPx + "px lines=" + s.toastLines + " font=" + s.toastFontPx + "px " + s.toastSeconds + "s poll=" + s.pollIntervalSeconds + "s 최소 " + s.minPercentToShow + "%"); L(" 타수: source=" + s.hitsSource + " window=" + s.windowSeconds + "s minKills=" + s.minKillsPerWindow + " 감소 하한 " + s.compareMinDropHits + "방 · 존 클리어 표시=" + s.showCompareOnZoneCleared); L(" 문구: \"" + s.powerUpFormat + "\" / \"" + s.statPopupLineFormat + "\" / \"" + s.hitsCompareFormat + "\""); L(" " + P(WLPowerDeltaSettings.Enabled) + " WLPowerDeltaSettings.Enabled"); // 813y 대역과 겹치지 않는지 실측 대조 (연쇄 +520 · 스탯 팝업 +180 · 전리품 −120) var t = WLCombatTextSettings.Instance; if (t != null) { // 🔴 전투력 줄이 켜지면 스탯 팝업 상자가 한 줄 커진다 — 그 높이로 대조해야 한다. int popupRows = t.statPopupRows + (s.statPopupLineEnabled ? 1 : 0); float statTop = t.statPopupCenterOffsetPx + (t.statPopupTitleFontPx + t.statPopupRowHeightPx * popupRows) * 0.5f; float mineLo = s.toastCenterOffsetPx - s.toastLineHeightPx * (s.toastLines - 0.5f); float mineHi = s.toastCenterOffsetPx + s.toastLineHeightPx * 0.5f; bool clear = mineLo > statTop && mineHi < t.killChainTopOffsetPx - t.killChainFontPx * 0.5f; L(" " + P(clear) + " 대역 회피 — 내 토스트 [" + mineLo.ToString("F0") + ", " + mineHi.ToString("F0") + "] · 스탯 팝업 위 " + statTop.ToString("F0") + " · 연쇄 아래 " + (t.killChainTopOffsetPx - t.killChainFontPx * 0.5f).ToString("F0") + " · 전리품 " + t.lootToastCenterOffsetPx); } } /// 에셋이 없으면 클래스 기본값으로 만든다(값은 전부 C# 기본값 = 코드에 적힌 그대로). public static WLPowerDeltaSettings EnsureAsset() { var existing = AssetDatabase.LoadAssetAtPath(AssetPath); if (existing == null) { var dir = Path.GetDirectoryName(AssetPath); if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); var so = ScriptableObject.CreateInstance(); AssetDatabase.CreateAsset(so, AssetPath); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); Debug.Log("[WL813l] 설정 에셋 생성 " + AssetPath); } WLPowerDeltaSettings.ClearCache(); return WLPowerDeltaSettings.Instance; } // ───────────────────────────────────────── 준비 static void Setup() { s_prevMyPc = MyValue.MyPC; s_prevBMyPc = MyValue.bMyPC; s_pcGo = new GameObject("__WL813l_PC"); s_pc = s_pcGo.AddComponent(); s_pc.m_Role = eRole.PC; s_pc.m_SubRole = eSubRol.None; MyValue.MyPC = s_pc; // setter 가 bMyPC 도 켠다 s_mobGo = new GameObject("__WL813l_Mob"); s_mob = s_mobGo.AddComponent(); // 813w 선례 — MobActor 는 테이블 없이 못 세운다 s_mob.m_Role = eRole.Mob; s_mob.m_SubRole = eSubRol.None; PowerDeltaToast.ResetDiagnostics(); PowerDeltaToast.Subscribe(false); PowerDeltaToast.Subscribe(true); L(""); L("── 준비"); L(" " + P(s_pc.IsMainPC()) + " 가짜 메인 PC IsMainPC()=true · MyValue.bMyPC=" + MyValue.bMyPC); L(" " + P(PowerDeltaToast.Subscribed) + " 구독 ON · PC 스탯=" + (s_pc.Get_StatInfo() == null ? "null(에디트 모드 — 산식은 §① 에서 손으로 채운 ActorStatInfo 로 잰다)" : "있음")); } static void Teardown() { try { PowerDeltaToast.Subscribe(false); PowerDeltaToast.ResetDiagnostics(); WLPowerDeltaSettings.RuntimeDisabled = false; MyValue.MyPC = s_prevMyPc; MyValue.bMyPC = s_prevBMyPc; if (s_popup != null) s_popup.Subscribe(false); Kill(s_pcGo, s_mobGo, s_canvasGo); } catch { } } static void Kill(params GameObject[] gos) { for (int i = 0; i < gos.Length; i++) if (gos[i] != null) UnityEngine.Object.DestroyImmediate(gos[i]); } // ───────────────────────────────────────── ① 산식 static void Sec1_Formula() { L(""); L("── ① 전투력 산식 (🔴 원본에 CP 없음 = 「추정」 · 발주서 §1-1 대안)"); var s = WLPowerDeltaSettings.Instance; // 순수 함수 — 손으로 계산한 기대값과 대조 double p1 = WLPowerMeter.Compute(100d, 0d, 0d, 1d, 1000d, s); // 100 × 1 × 1 + 1000×0.1 = 200 double p2 = WLPowerMeter.Compute(100d, 0.2d, 0.5d, 1d, 1000d, s); // 100 × 1.1 × 1 + 100 = 210 double p3 = WLPowerMeter.Compute(100d, 0d, 0d, 1.3d, 1000d, s); // 100 × 1 × 1.3 + 100 = 230 double p4 = WLPowerMeter.Compute(200d, 0.2d, 0.5d, 1.3d, 2000d, s); // 200 × 1.1 × 1.3 + 200 = 486 L(" " + P(Math.Abs(p1 - 200d) < 0.001d) + " atk100 cri0 aspd1 hp1000 → " + p1.ToString("F2") + " (기대 200.00)"); L(" " + P(Math.Abs(p2 - 210d) < 0.001d) + " 치명 20%×50% → " + p2.ToString("F2") + " (기대 210.00 · +5%)"); L(" " + P(Math.Abs(p3 - 230d) < 0.001d) + " 공속 1.3 → " + p3.ToString("F2") + " (기대 230.00 · +15%)"); L(" " + P(Math.Abs(p4 - 486d) < 0.001d) + " 전 스탯 2배급 → " + p4.ToString("F2") + " (기대 486.00)"); L(" " + P(WLPowerMeter.PercentDelta(200d, 230d) > 14.99d && WLPowerMeter.PercentDelta(200d, 230d) < 15.01d) + " 증가율 200→230 = " + WLPowerMeter.PercentDelta(200d, 230d).ToString("F2") + "% (기대 15.00)"); L(" " + P(WLPowerMeter.PercentDelta(0d, 100d) == 0d) + " 이전 0 → 0%(나눗셈 방지)"); // 실제 ActorStatInfo 읽기 (원본 API 만 · 쓰기 0) var stat = new ActorStatInfo(eRole.PC); stat.Set_Stat(eStat.BaseDmg, 500d); stat.Set_Stat(eStat.ATK, 400d); stat.Set_Stat(eStat.CRI, 0.15d); stat.Set_Stat(eStat.CRI_DMG_Multiplier, 0.6d); stat.Set_Stat(eStat.ATK_SPD_Multiplier, 1.2d); stat.Set_Stat(eStat.MaxHP, 8000d); WLPowerMeter.Sample sm; bool read = WLPowerMeter.TryRead(stat, s, out sm); double expect = 500d * (1d + 0.15d * 0.6d) * (1d + 0.2d) + 8000d * 0.1d; // 500×1.09×1.2 + 800 = 1454 L(" " + P(read && Math.Abs(sm.power - expect) < 0.01d) + " ActorStatInfo 읽기 → " + sm + " (기대 " + expect.ToString("F2") + ")"); L(" " + P(sm.atk == 500d) + " BaseDmg 우선(ATK 400 이 아니라 500)"); // BaseDmg 가 0 이면 ATK 로 폴백 var stat2 = new ActorStatInfo(eRole.PC); stat2.Set_Stat(eStat.ATK, 400d); stat2.Set_Stat(eStat.ATK_SPD_Multiplier, 1d); WLPowerMeter.Sample sm2; WLPowerMeter.TryRead(stat2, s, out sm2); L(" " + P(sm2.atk == 400d) + " BaseDmg 0 → ATK 폴백 " + sm2.atk.ToString("F0")); WLPowerMeter.Sample none; L(" " + P(!WLPowerMeter.TryRead(null, s, out none)) + " 스탯 null → false(로비 안전)"); } // ───────────────────────────────────────── ② 토스트 static void Sec2_Toast() { L(""); L("── ② 전투력 % 토스트"); var s = WLPowerDeltaSettings.Instance; PowerDeltaToast.ResetDiagnostics(); float t = 100f; L(" " + PowerDeltaToast.Observe(1000d, t, "probe") + " ← 기준값"); t += 10f; string r1 = PowerDeltaToast.Observe(1150d, t, "probe"); bool up = PowerDeltaToast.LastText.Contains("15.0") && PowerDeltaToast.PowerToastCount == 1; L(" " + P(up) + " 1000 → 1150 · 문구 \"" + PowerDeltaToast.LastText + "\" · " + r1); t += 10f; PowerDeltaToast.Observe(1155d, t, "probe"); // +0.43% < 0.5% 하한 L(" " + P(PowerDeltaToast.SkippedSmall == 1 && PowerDeltaToast.PowerToastCount == 1) + " 잡음 차단 — +0.43% 는 안 띄운다(SkippedSmall=" + PowerDeltaToast.SkippedSmall + ")"); t += 10f; PowerDeltaToast.Observe(900d, t, "probe"); // 하락 L(" " + P(PowerDeltaToast.SkippedDown == 1 && PowerDeltaToast.PowerToastCount == 1) + " 하락 무시(showOnPowerDown=" + s.showOnPowerDown + " · SkippedDown=" + PowerDeltaToast.SkippedDown + ")"); // 쿨다운 병합 — 마지막 표시 직후의 연속 변화는 한 줄로 합친다 PowerDeltaToast.ResetDiagnostics(); t = 200f; PowerDeltaToast.Observe(1000d, t, "probe"); PowerDeltaToast.Observe(1100d, t, "probe"); // 첫 표시 (+10%) int after1 = PowerDeltaToast.PowerToastCount; PowerDeltaToast.Observe(1210d, t + 0.3f, "probe"); // 쿨다운(1 s) 안 → 병합 L(" " + P(PowerDeltaToast.MergedByCooldown == 1 && PowerDeltaToast.PowerToastCount == after1) + " 쿨다운 병합 1건(연타해도 토스트가 도배되지 않는다)"); PowerDeltaToast.Tick(t + 2f, 0.016f); // 쿨다운 지나 마감 // 분모는 **마지막으로 표시한 값**(1100) — 이미 보여 준 +10% 를 두 번 세지 않는다. bool merged = PowerDeltaToast.LastText.Contains("10.0"); // 1100 → 1210 = +10% L(" " + P(merged && PowerDeltaToast.PowerToastCount == after1 + 1) + " 마감 문구 \"" + PowerDeltaToast.LastText + "\" (직전 표시값 1100 → 1210 = +10.0% · 중복 계상 0)"); // 하한 미만의 잔변화는 버리지 않고 **누적**된다 PowerDeltaToast.ResetDiagnostics(); t = 300f; PowerDeltaToast.Observe(1000d, t, "probe"); PowerDeltaToast.Observe(1004d, t + 5f, "probe"); // +0.40% → 대기 PowerDeltaToast.Observe(1008d, t + 10f, "probe"); // 누적 +0.80% → 표시 L(" " + P(PowerDeltaToast.PowerToastCount == 1 && PowerDeltaToast.LastText.Contains("0.8")) + " 잔변화 누적 — 0.40% 대기 후 0.80% 로 표시 \"" + PowerDeltaToast.LastText + "\""); // 레벨업 프레임에는 토스트를 팝업 줄에 양보한다 PowerDeltaToast.ResetDiagnostics(); PowerDeltaToast.Observe(1000d, 400f, "probe"); RaiseFakeLevelUp(8); // suppressToastOnLevelUp 프레임 표시 PowerDeltaToast.Observe(1200d, 401f, "probe"); // 같은 프레임 → 생략 L(" " + P(PowerDeltaToast.PowerToastCount == 0 && PowerDeltaToast.SuppressedByPopup == 1) + " 레벨업 프레임 토스트 생략(중복 표시 0 · Suppressed=" + PowerDeltaToast.SuppressedByPopup + " Toast=" + PowerDeltaToast.PowerToastCount + ")"); // 가짜 LevelUp — 813k 축을 그대로 쏜다 PowerDeltaToast.ResetDiagnostics(); int before = PowerDeltaToast.LevelUpSeen; RaiseFakeLevelUp(7); L(" " + P(PowerDeltaToast.LevelUpSeen == before + 1) + " GrowthEvents.LevelUp 수신 " + PowerDeltaToast.LevelUpSeen + "회(PC 스탯이 없어 전투력 값은 안 읽힌다 = 에디트 모드 한계)"); // 노드 없이도 문구는 난다(HUD 가 없는 에디트 모드) — 노드 배치는 §③ 에서 가짜 캔버스로 본다 L(" Bound=" + PowerDeltaToast.Bound + " (에디트 모드 · NewGameUI 없음 — 문구만 기록)"); } /// 813k 의 공개 발생기를 그대로 쓴다(data=null 은 813k 가 허용 — 로비 경로와 같은 모양). static void RaiseFakeLevelUp(int newLevel) { GrowthEvents.RaiseLevelUp(null, newLevel); } // ───────────────────────────────────────── ③ 팝업 전투력 줄 static void Sec3_PopupLine() { L(""); L("── ③ 레벨업 팝업 전투력 줄 (813y StatPopup · 1줄 추가)"); var s = WLPowerDeltaSettings.Instance; s_canvasGo = new GameObject("__WL813l_Canvas", typeof(Canvas), typeof(CanvasScaler)); s_canvasGo.layer = 5; var croot = (RectTransform)s_canvasGo.transform; croot.sizeDelta = new Vector2(1080f, 1920f); var prt = new GameObject("WL_StatPopup", typeof(RectTransform)); prt.layer = 5; prt.transform.SetParent(croot, false); s_popup = prt.AddComponent(); string init = s_popup.Initialize(); L(" init: " + init); L(" " + P(StatPopup.PowerLineOn) + " StatPopup.PowerLineOn(에셋 · enabled_ · statPopupLineEnabled)"); // 전투력 값을 읽을 수 있게 스탯이 살아 있는 PC 가 필요하다 — 에디트 모드에서는 못 만든다. // 대신 팝업 경로 자체(줄 노드 생성 · 배치 · 문구 주입)를 검증한다. string layout = s_popup.ApplyLayout(); bool hasPowerRow = layout.Contains("power=on"); L(" " + P(hasPowerRow) + " 배치 — " + layout); var box = prt.transform.Find("Box"); var powerRow = box != null ? box.Find("PowerRow") : null; L(" " + P(powerRow != null) + " PowerRow 노드 생성(스탯 줄 " + WLCombatTextSettings.Instance.statPopupRows + "개는 그대로)"); if (powerRow != null) { var lb = powerRow.GetComponent(); var rt = (RectTransform)powerRow; var row2 = box.Find("Row" + (WLCombatTextSettings.Instance.statPopupRows - 1)) as RectTransform; bool below = row2 != null && rt.anchoredPosition.y < row2.anchoredPosition.y; L(" " + P(below && lb != null) + " 마지막 스탯 줄 아래 y=" + rt.anchoredPosition.y.ToString("F1") + "(스탯 줄 " + (row2 != null ? row2.anchoredPosition.y.ToString("F1") : "?") + ") · font=" + (lb != null ? lb.fontSize.ToString("F1") : "?") + " color=" + (lb != null ? lb.color.ToString() : "?")); } // 문구 조립 — 813y 팝업 표시 경로를 그대로 탄다(전투력 줄은 PC 스탯이 없으면 빈 줄 = 안전) var deltas = new[] { new WLStatDelta { name = "MaxHP", prev = 100d, next = 112d } }; string shown = s_popup.Show(7, deltas, "+5 포인트"); L(" Show: " + shown); L(" " + P(s_popup.LastRowCount >= 1) + " 스탯 줄 " + s_popup.LastRowCount + "개(813y 동작 불변)"); L(" 전투력 줄=\"" + s_popup.LastPowerLine + "\" (PC 스탯 null → 빈 줄이 정상 · 실값은 인게임 QA)"); // 문구 포맷 자체는 순수 조립으로 확인한다(값 3개가 자리에 들어가는가) string sample = DSUtil.Format(s.statPopupLineFormat, WLPowerMeter.Format(12000d), WLPowerMeter.Format(13800d), "+15.0"); L(" " + P(sample.Contains("12,000") && sample.Contains("13,800") && sample.Contains("+15.0")) + " 문구 조립 \"" + sample + "\""); string sample2 = DSUtil.Format(s.statPopupLineFormatNoChange, WLPowerMeter.Format(12000d)); L(" " + P(sample2.Contains("12,000")) + " 변화 없을 때 \"" + sample2 + "\""); } // ───────────────────────────────────────── ③-b 토스트 노드 (가짜 NewGameUI 트리) static void Sec3b_ToastNode() { L(""); L("── ③-b 토스트 노드 배치 (프리팹 diff 0 · 런타임 생성)"); var s = WLPowerDeltaSettings.Instance; // 원본 계층과 같은 모양만 만든다 — NewGameUI/IngameUIs/WL_HUD var croot = (RectTransform)s_canvasGo.transform; var ing = new GameObject("IngameUIs", typeof(RectTransform)); ing.transform.SetParent(croot, false); var hud = new GameObject("WL_HUD", typeof(RectTransform)); hud.transform.SetParent(ing.transform, false); var hudRt = (RectTransform)hud.transform; hudRt.anchorMin = Vector2.zero; hudRt.anchorMax = Vector2.one; hudRt.offsetMin = Vector2.zero; hudRt.offsetMax = Vector2.zero; string bind = PowerDeltaToast.Bind(croot); L(" bind: " + bind); L(" " + P(PowerDeltaToast.Bound) + " 노드 연결(부모 경로 \"" + s.hudParentPath + "\")"); var root = hudRt.Find("WL_PowerDeltaToast") as RectTransform; L(" " + P(root != null) + " WL_PowerDeltaToast 노드"); if (root == null) return; L(" " + P(root.gameObject.hideFlags == HideFlags.DontSave) + " hideFlags=" + root.gameObject.hideFlags + "(씬·프리팹에 굽지 않는다) · layer=" + root.gameObject.layer); L(" " + P(root.childCount == s.toastLines) + " 줄 노드 " + root.childCount + "개(SO toastLines=" + s.toastLines + ")"); for (int i = 0; i < root.childCount; i++) { var lrt = (RectTransform)root.GetChild(i); var cg = lrt.GetComponent(); var lb = lrt.GetComponentInChildren(true); L(" Line" + i + " y=" + lrt.anchoredPosition.y.ToString("F1") + " size=" + lrt.sizeDelta + " alpha=" + (cg != null ? cg.alpha.ToString("F2") : "?") + " font=" + (lb != null ? lb.fontSize.ToString("F1") : "?")); } // 실제 표시 — 전투력 줄 PowerDeltaToast.ResetDiagnostics(); PowerDeltaToast.Observe(1000d, 600f, "probe"); PowerDeltaToast.Observe(1250d, 610f, "probe"); var l0 = (RectTransform)root.GetChild(0); var lab0 = l0.GetComponentInChildren(true); var cg0 = l0.GetComponent(); L(" " + P(PowerDeltaToast.VisibleLines == 1 && lab0 != null && lab0.text == "전투력 +25.0%" && cg0.alpha > 0.99f) + " 표시 \"" + (lab0 != null ? lab0.text : "?") + "\" alpha=" + cg0.alpha.ToString("F2") + " color=" + (lab0 != null ? lab0.color.ToString() : "?") + " scale=" + l0.localScale.x.ToString("F3") + "(펀치)"); // 타수 비교 줄이 같은 대역 둘째 줄에 쌓인다 PowerDeltaToast.RaiseFakeCombatForProbe(s_pc, s_mob, 30, 10); PowerDeltaToast.MarkGrowth(620f, "probe"); PowerDeltaToast.RaiseFakeCombatForProbe(s_pc, s_mob, 20, 10); PowerDeltaToast.CloseWindow(s, 620f + s.windowSeconds); PowerDeltaToast.RaiseFakeZoneClearedForProbe(0); var l1 = (RectTransform)root.GetChild(1); var lab1 = l1.GetComponentInChildren(true); L(" " + P(PowerDeltaToast.VisibleLines == 2 && lab1 != null && lab1.text.Contains("방")) + " 둘째 줄 \"" + (lab1 != null ? lab1.text : "?") + "\" y=" + l1.anchoredPosition.y.ToString("F1") + "(첫 줄 " + l0.anchoredPosition.y.ToString("F1") + " · 간격 " + s.toastLineHeightPx + "px)"); // 수명 — toastSeconds 뒤 사라진다 for (int i = 0; i < 200; i++) PowerDeltaToast.Tick(700f + i * 0.02f, 0.02f); L(" " + P(PowerDeltaToast.VisibleLines == 0 && cg0.alpha < 0.01f) + " " + s.toastSeconds + "s 뒤 전부 사라짐(alpha=" + cg0.alpha.ToString("F2") + " · 보이는 줄 " + PowerDeltaToast.VisibleLines + ")"); PowerDeltaToast.ResetDiagnostics(); } // ───────────────────────────────────────── ④ 처치 타수 static void Sec4_Hits() { L(""); L("── ④ 처치 타수 비교 (기준서 §E 5-3 「타수 −1」)"); var s = WLPowerDeltaSettings.Instance; PowerDeltaToast.ResetDiagnostics(); float t = 1000f; // 창 1 — 성장 전: 30타 / 10킬 = 3.0방 FeedCombat(30, 10); L(" 창1 누적 hits=" + PowerDeltaToast.WindowHits + " attacks=" + PowerDeltaToast.WindowAttacks + " kills=" + PowerDeltaToast.WindowKills); string mark = PowerDeltaToast.MarkGrowth(t, "probe 성장"); L(" " + P(PowerDeltaToast.HasBaseline && Math.Abs(PowerDeltaToast.BaselineRatio - 3f) < 0.001f) + " 성장 시점에 기준선 확정 — " + mark); L(" " + P(PowerDeltaToast.WindowKills == 0) + " 새 창 리셋(kills=" + PowerDeltaToast.WindowKills + ")"); // 창 2 — 성장 후: 20타 / 10킬 = 2.0방 → 3방 → 2방 FeedCombat(20, 10); string close = PowerDeltaToast.CloseWindow(s, t + s.windowSeconds); L(" " + P(PowerDeltaToast.Armed && PowerDeltaToast.ArmedFrom == 3 && PowerDeltaToast.ArmedTo == 2) + " 창2 마감 — " + close); // 813p ZoneCleared 에서 1회만 표시 RaiseFakeZoneCleared(0); bool shown = PowerDeltaToast.CompareToastCount == 1 && PowerDeltaToast.LastCompareText.Contains("3") && PowerDeltaToast.LastCompareText.Contains("2"); L(" " + P(shown) + " ZoneCleared → \"" + PowerDeltaToast.LastCompareText + "\" (" + PowerDeltaToast.CompareToastCount + "회)"); RaiseFakeZoneCleared(1); L(" " + P(PowerDeltaToast.CompareToastCount == 1) + " 다음 존에서 다시 뜨지 않는다(무장 1회 소모 · " + PowerDeltaToast.CompareToastCount + "회)"); // 표본 부족 창은 버린다 PowerDeltaToast.ResetDiagnostics(); FeedCombat(6, 2); string poor = PowerDeltaToast.CloseWindow(s, t + 500f); L(" " + P(!PowerDeltaToast.HasBaseline && poor.Contains("표본 부족")) + " " + poor + " (minKills=" + s.minKillsPerWindow + ")"); // 타수가 안 줄면 무장하지 않는다 PowerDeltaToast.ResetDiagnostics(); FeedCombat(30, 10); PowerDeltaToast.MarkGrowth(t, "probe"); FeedCombat(29, 10); // 2.9방 → 반올림 3방 = 변화 없음 string flat = PowerDeltaToast.CloseWindow(s, t + s.windowSeconds); L(" " + P(!PowerDeltaToast.Armed) + " 3.00 → 2.90 은 무장 안 함 — " + flat); // 메인 PC 가 아닌 처치는 안 센다 PowerDeltaToast.ResetDiagnostics(); PowerDeltaToast.RaiseFakeCombatForProbe(s_mob, s_mob, 0, 3); // killer = 몹 L(" " + P(PowerDeltaToast.WindowKills == 0) + " 몹이 죽인 것은 표본에서 제외(kills=" + PowerDeltaToast.WindowKills + ")"); } /// 실제 이벤트 축으로 타수·처치를 채운다(카운터 직접 조작 0 · 구독 경로 전체를 탄다). static void FeedCombat(int hits, int kills) { PowerDeltaToast.RaiseFakeCombatForProbe(s_pc, s_mob, hits, kills); } static void RaiseFakeZoneCleared(int zoneIndex) { PowerDeltaToast.RaiseFakeZoneClearedForProbe(zoneIndex); } // ───────────────────────────────────────── ⑤ GC static void Sec5_Gc() { L(""); L("── ⑤ GC 0 (폴링 · 카운터)"); PowerDeltaToast.ResetDiagnostics(); PowerDeltaToast.Observe(1000d, 0f, "base"); long a0 = Gc(), b0 = Prof(); for (int i = 0; i < 1000; i++) PowerDeltaToast.Tick(3000f + i * 0.001f, 0.001f); // 폴링·창·수명 long a1 = Gc(), b1 = Prof(); L(" " + P(a1 - a0 <= 0 && b1 - b0 <= 0) + " Tick × 1000 → GetTotalMemory " + (a1 - a0) + " B · MonoUsedSize " + (b1 - b0) + " B (폴링 " + PowerDeltaToast.PollCount + "회)"); long c0 = Gc(), d0 = Prof(); PowerDeltaToast.RaiseFakeCombatForProbe(s_pc, s_mob, 1000, 200); // 이벤트 2,200발 · 헬퍼 반환 문자열 1개만 할당 long c1 = Gc(), d1 = Prof(); L(" " + P(c1 - c0 < 512) + " AttackStarted+HitConfirmed+Killed 2,200발 → " + (c1 - c0) + " B · " + (d1 - d0) + " B (헬퍼 반환 문자열 1개 제외하면 카운터는 int 증가뿐 · hits=" + PowerDeltaToast.WindowHits + ")"); // 대조군 — 측정이 살아 있음을 보인다(813k 선례) long e0 = Gc(); var keep = new object[1000]; for (int i = 0; i < 1000; i++) keep[i] = new object(); long e1 = Gc(); L(" 대조군 new object() × 1000 = " + (e1 - e0) + " B (0 이 아니어야 측정이 살아 있다) · keep=" + keep.Length); PowerDeltaToast.ResetDiagnostics(); } static long Gc() { return GC.GetTotalMemory(false); } static long Prof() { return UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong(); } // ───────────────────────────────────────── ⑥ C8 static void Sec6_Rollback() { L(""); L("── ⑥ C8 롤백 (WLPowerDeltaSettings.enabled_ = 0 → 표시 0)"); PowerDeltaToast.ResetDiagnostics(); WLPowerDeltaSettings.RuntimeDisabled = true; L(" " + P(!WLPowerDeltaSettings.Enabled) + " Enabled=false"); PowerDeltaToast.Observe(1000d, 500f, "probe"); PowerDeltaToast.Observe(2000d, 510f, "probe"); L(" " + P(PowerDeltaToast.PowerToastCount == 0) + " 전투력 2배 변화에도 토스트 " + PowerDeltaToast.PowerToastCount + "회"); RaiseFakeLevelUp(10); L(" " + P(PowerDeltaToast.LevelUpSeen == 0) + " 레벨업 무시(LevelUpSeen=" + PowerDeltaToast.LevelUpSeen + ")"); L(" " + P(!StatPopup.PowerLineOn) + " StatPopup.PowerLineOn=false → 팝업 전투력 줄 0"); if (s_popup != null) { s_popup.Show(11, null, null); L(" " + P(string.IsNullOrEmpty(s_popup.LastPowerLine)) + " 팝업 전투력 줄 \"" + s_popup.LastPowerLine + "\"(빈 문자열)"); } L(" " + P(PowerDeltaToast.BuildStatPopupLine() == null) + " BuildStatPopupLine()=null"); WLPowerDeltaSettings.RuntimeDisabled = false; L(" " + P(WLPowerDeltaSettings.Enabled) + " 롤백 해제 — 다시 Enabled=true"); } }