Project_WL/AgentScripts/WL813l_Probe.cs

552 lines
30 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.

// WL813l_Probe.cs — #813l(성장 체감: 전투력 % 토스트 · 레벨업 팝업 전투력 줄 · 처치 타수 비교) 에디트 모드 검증 프로브
// 에디트 모드 전용 · Play 불필요 · 로그인 0 · 씬 무수정(임시 GameObject 는 즉시 파괴)
// 유일한 에셋 쓰기 = `Assets/WL/UI/Settings/Resources/WL/WLPowerDeltaSettings.asset` **생성**(발주 산출물 · EnsureAsset)
//
// 상주 에디터:
// unity command run_script --project-path <wt> --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);
}
}
/// <summary>에셋이 없으면 클래스 기본값으로 만든다(값은 전부 C# 기본값 = 코드에 적힌 그대로).</summary>
public static WLPowerDeltaSettings EnsureAsset()
{
var existing = AssetDatabase.LoadAssetAtPath<WLPowerDeltaSettings>(AssetPath);
if (existing == null)
{
var dir = Path.GetDirectoryName(AssetPath);
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
var so = ScriptableObject.CreateInstance<WLPowerDeltaSettings>();
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<PCActor>();
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<Actor>(); // 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 없음 — 문구만 기록)");
}
/// <summary>813k 의 공개 발생기를 그대로 쓴다(data=null 은 813k 가 허용 — 로비 경로와 같은 모양).</summary>
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<StatPopup>();
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<TextMeshProUGUI>();
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<CanvasGroup>();
var lb = lrt.GetComponentInChildren<TextMeshProUGUI>(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<TextMeshProUGUI>(true);
var cg0 = l0.GetComponent<CanvasGroup>();
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<TextMeshProUGUI>(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 + ")");
}
/// <summary>실제 이벤트 축으로 타수·처치를 채운다(카운터 직접 조작 0 · 구독 경로 전체를 탄다).</summary>
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");
}
}