552 lines
30 KiB
C#
552 lines
30 KiB
C#
|
|
// 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");
|
|||
|
|
}
|
|||
|
|
}
|