604 lines
32 KiB
C#
604 lines
32 KiB
C#
// WL-813c2 프로브 — Q6 핫픽스 실측(에디트 모드 · Play 0 · 로그인 0 · #813).
|
||
// unity command run_script --file AgentScripts/WL813c2_Probe.cs --entry WL813c2_Probe.RunAll
|
||
// 산출물: <worktree>/AgentScripts/WL813c2_PROBE.txt
|
||
//
|
||
// 🔴 씬·프리팹을 저장하지 않는다. 만든 노드는 finally 에서 전부 DestroyImmediate(HideFlags.HideAndDontSave).
|
||
// 🔴 SO 값은 메모리에서만 바꾸고 finally 에서 원복한다(AssetDatabase.SaveAssets 0).
|
||
// 🔴 Gameplay 는 읽기·구독·공개 API 호출만. `BossArena.s_armed` 만 리플렉션으로 잠깐 세운다
|
||
// (ResetGate 가 s_boss 를 비우지 않는다는 D-23 근거를 코드가 아니라 **실행**으로 남기려고 · 813q2 선례).
|
||
// 🔴 batchmode 는 Time.unscaledTime 이 전진하지 않는다 → 시각은 전부 프로브가 넘기는 t 값으로 만든다.
|
||
// 🔴 프리팹을 열지 않는다(813o 는 NewGameUI.prefab 을 열었지만 여기서는 합성 캔버스면 충분하다).
|
||
|
||
using System;
|
||
using System.IO;
|
||
using System.Reflection;
|
||
using System.Text;
|
||
using TMPro;
|
||
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
using UnityEditor;
|
||
using WL.Combat.Core;
|
||
using WL.Combat.Run;
|
||
using WL.Combat.Survival;
|
||
using WL.UI;
|
||
|
||
public static class WL813c2_Probe
|
||
{
|
||
const string kSelectSkillTxt = "Assets/ResWork/Table/table_selectskill.txt";
|
||
|
||
static StringBuilder _o;
|
||
static int _pass, _fail;
|
||
static GameObject _canvas, _fakes;
|
||
static BossHpBar _bar;
|
||
static LootToast _toast;
|
||
static ReviveDialog _revive;
|
||
static Transform _uiRoot;
|
||
|
||
static void H(string s) { _o.AppendLine(); _o.AppendLine("── " + s); }
|
||
static void N(string s) { _o.AppendLine(" " + s); }
|
||
static bool Chk(bool ok, string what)
|
||
{
|
||
if (ok) { _pass++; _o.AppendLine(" [PASS] " + what); }
|
||
else { _fail++; _o.AppendLine(" [FAIL] " + what); }
|
||
return ok;
|
||
}
|
||
|
||
static string Root { get { return Directory.GetParent(Application.dataPath).FullName; } }
|
||
static string OutPath(string n) { return Path.Combine(Path.Combine(Root, "AgentScripts"), n); }
|
||
|
||
public static string RunAll()
|
||
{
|
||
_o = new StringBuilder();
|
||
_pass = _fail = 0;
|
||
_o.AppendLine("# WL813c2 Probe " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " (edit mode · Play " + Application.isPlaying + ")");
|
||
|
||
var hud = WLHudLayoutSettings.Instance;
|
||
var txt = WLCombatTextSettings.Instance;
|
||
var zp = WLZonePickupSettings.Instance;
|
||
var run = WLRunUiSettings.Instance;
|
||
|
||
// 원복용 스냅샷
|
||
bool sPoll = hud != null && hud.bossBarPollEnabled;
|
||
float sPollHz = hud != null ? hud.bossBarPollHz : 0f;
|
||
bool sArenaGate = hud != null && hud.bossBarRebindRequiresArenaSpawn;
|
||
bool sGold = txt != null && txt.lootToastIncludeGold;
|
||
float sFade = txt != null ? txt.lootToastOverflowFadeSec : 0f;
|
||
bool sMergeVis = txt != null && txt.lootToastMergeWhileVisible;
|
||
bool sPause = zp != null && zp.pauseTimeoutWhileRevive;
|
||
bool sBossSpawned = WL.Combat.Boss.BossArena.BossSpawned;
|
||
|
||
try
|
||
{
|
||
Step0_Values(hud, txt, zp, run);
|
||
BuildScene();
|
||
Step1_BarFollowsHp(hud);
|
||
Step2_RunStartReset(hud, run);
|
||
Step3_ToastQueue(txt);
|
||
Step4_CardRevivePause(zp);
|
||
Step5_Gc(hud, txt);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_fail++;
|
||
_o.AppendLine(" [EXCEPTION] " + ex);
|
||
}
|
||
finally
|
||
{
|
||
if (hud != null) { hud.bossBarPollEnabled = sPoll; hud.bossBarPollHz = sPollHz; hud.bossBarRebindRequiresArenaSpawn = sArenaGate; }
|
||
if (txt != null) { txt.lootToastIncludeGold = sGold; txt.lootToastOverflowFadeSec = sFade; txt.lootToastMergeWhileVisible = sMergeVis; }
|
||
if (zp != null) zp.pauseTimeoutWhileRevive = sPause;
|
||
WL.Combat.Boss.BossArena.BossSpawned = sBossSpawned;
|
||
WL.Combat.Boss.BossArena.SetBossForProbe(null);
|
||
BossHpBar.ClearFakeHp();
|
||
BossHpBar.DebugFallbackMaxHp = 100f;
|
||
try { ZonePickup.Teardown(); } catch { }
|
||
if (_toast != null) _toast.Subscribe(false);
|
||
if (_bar != null) _bar.Subscribe(false);
|
||
if (_canvas != null) UnityEngine.Object.DestroyImmediate(_canvas);
|
||
if (_fakes != null) UnityEngine.Object.DestroyImmediate(_fakes);
|
||
table_selectskill.Ins = null;
|
||
InGameInfo.Ins = null;
|
||
ActorInfo.Ins = null;
|
||
}
|
||
|
||
_o.AppendLine();
|
||
_o.AppendLine("== 합계 PASS " + _pass + " · FAIL " + _fail + " ==");
|
||
string p = OutPath("WL813c2_PROBE.txt");
|
||
File.WriteAllText(p, _o.ToString(), new UTF8Encoding(false));
|
||
return "PASS " + _pass + " · FAIL " + _fail + " → " + p;
|
||
}
|
||
|
||
// ───────────────────────────────────────────── ⓪ 값(SO)
|
||
|
||
static void Step0_Values(WLHudLayoutSettings hud, WLCombatTextSettings txt, WLZonePickupSettings zp, WLRunUiSettings run)
|
||
{
|
||
H("⓪ 값 — 이번 작업으로 늘어난 SO 필드 실측(코드 상수 0)");
|
||
Chk(hud != null, "WLHudLayoutSettings 에셋");
|
||
if (hud != null)
|
||
N("보스 바 : poll=" + hud.bossBarPollEnabled + " @" + hud.bossBarPollHz + "Hz · 아레나추종=" + hud.bossBarPollFollowsArena +
|
||
" · 사망숨김=" + hud.bossBarPollHidesOnDeath + " · 재바인드조건(BossSpawned)=" + hud.bossBarRebindRequiresArenaSpawn +
|
||
" · 눈금=" + (hud.bossPhaseTicks != null ? string.Join(",", Array.ConvertAll(hud.bossPhaseTicks, v => v.ToString("F2"))) : "-") +
|
||
" · 숨김지연=" + hud.bossBarHideDelaySec + "s");
|
||
Chk(txt != null, "WLCombatTextSettings 에셋");
|
||
if (txt != null)
|
||
N("토스트 : 줄상한=" + txt.lootToastLines + " · 표시=" + txt.lootToastSeconds + "s · 병합창=" + txt.lootToastMergeSeconds +
|
||
"s(보이는동안=" + txt.lootToastMergeWhileVisible + ") · 넘침페이드=" + txt.lootToastOverflowFadeSec +
|
||
"s · 큐=" + txt.lootToastQueueMax + " · 골드포함=" + txt.lootToastIncludeGold);
|
||
Chk(zp != null, "WLZonePickupSettings 에셋");
|
||
if (zp != null)
|
||
N("픽업카드: 부활중정지=" + zp.pauseTimeoutWhileRevive + " · 전용캔버스=" + zp.ownCanvasEnabled +
|
||
" · sortingOrder=" + zp.cardSortingOrder + " · 타임아웃=" + zp.pickTimeoutSeconds + "s · 열기지연=" + zp.openDelaySeconds + "s");
|
||
var sv = WLSurvivalSettings.Instance;
|
||
N("부활 : reviveSortingOrder=" + (sv != null ? sv.reviveSortingOrder.ToString() : "-"));
|
||
Chk(zp != null && sv != null && zp.cardSortingOrder < sv.reviveSortingOrder,
|
||
"카드 sortingOrder " + (zp != null ? zp.cardSortingOrder : -1) + " < 부활 " + (sv != null ? sv.reviveSortingOrder : -1) + " (발주서 §1-3)");
|
||
if (run != null)
|
||
N("런 UI : bossResetOnRunStart=" + run.bossResetOnRunStart + " · bossBarRebindOnRunStart=" + run.bossBarRebindOnRunStart +
|
||
" · bossBannerRetriggerOnRunStart=" + run.bossBannerRetriggerOnRunStart);
|
||
}
|
||
|
||
// ───────────────────────────────────────────── 합성 씬
|
||
|
||
static void BuildScene()
|
||
{
|
||
H("① 합성 캔버스(1080×1920 Expand) · IngameUIs/WL_HUD · 보스 바 · 토스트 · 부활 팝업");
|
||
_canvas = new GameObject("__WL813c2_Canvas", typeof(RectTransform));
|
||
_canvas.hideFlags = HideFlags.HideAndDontSave;
|
||
var c = _canvas.AddComponent<Canvas>();
|
||
c.renderMode = RenderMode.ScreenSpaceOverlay;
|
||
var sc = _canvas.AddComponent<CanvasScaler>();
|
||
sc.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||
sc.referenceResolution = new Vector2(1920f, 1080f);
|
||
sc.screenMatchMode = CanvasScaler.ScreenMatchMode.Expand;
|
||
_canvas.AddComponent<GraphicRaycaster>();
|
||
|
||
var ing = NewRt("IngameUIs", _canvas.transform);
|
||
var hud = NewRt("WL_HUD", ing);
|
||
hud.anchorMin = hud.anchorMax = new Vector2(0.5f, 0.5f);
|
||
hud.pivot = new Vector2(0.5f, 0.5f);
|
||
hud.sizeDelta = new Vector2(1080f, 1920f);
|
||
_uiRoot = _canvas.transform;
|
||
|
||
_bar = NewRt("WL_BossHpBar", hud).gameObject.AddComponent<BossHpBar>();
|
||
N("BossHpBar.Initialize : " + _bar.Initialize());
|
||
Chk(_bar != null, "보스 바 조립");
|
||
|
||
_toast = NewRt("WL_LootToast", hud).gameObject.AddComponent<LootToast>();
|
||
N("LootToast.Initialize : " + Trim(_toast.Initialize(), 180));
|
||
Chk(_toast != null, "토스트 조립");
|
||
|
||
_revive = NewRt("WL_ReviveDialog", hud).gameObject.AddComponent<ReviveDialog>();
|
||
N("ReviveDialog.Initialize : " + Trim(_revive.Initialize(), 180));
|
||
Chk(!ReviveDialog.Busy, "부활 팝업은 닫힌 상태로 시작");
|
||
}
|
||
|
||
static RectTransform NewRt(string name, Transform parent)
|
||
{
|
||
var go = new GameObject(name, typeof(RectTransform));
|
||
go.hideFlags = HideFlags.HideAndDontSave;
|
||
var rt = (RectTransform)go.transform;
|
||
rt.SetParent(parent, false);
|
||
return rt;
|
||
}
|
||
|
||
static string Trim(string s, int n) { return s != null && s.Length > n ? s.Substring(0, n) + "…" : s; }
|
||
|
||
// ───────────────────────────────────────────── ② D-26
|
||
|
||
static void Step1_BarFollowsHp(WLHudLayoutSettings hud)
|
||
{
|
||
H("② D-26 — 보스 HP 바가 실제 HP 를 따라가는가 (Q6 `PD3_05` = 프로브 869/1800 인데 바 3칸 만피)");
|
||
if (_bar == null || hud == null) { Chk(false, "전제 없음"); return; }
|
||
|
||
// (a) 결함 재현 — MaxHP 를 못 읽은 채로 물렸다(폴백 100 고정) + 폴링 off = 813q2 상태
|
||
hud.bossBarPollEnabled = false;
|
||
BossHpBar.ClearFakeHp();
|
||
N("RaiseFakeBossSpawned(100) : " + BossHpBar.RaiseFakeBossSpawned(100f));
|
||
Chk(Math.Abs(_bar.LastRatio - 1f) < 0.001f, "물린 직후 ratio=1.000 (실측 " + _bar.LastRatio.ToString("F3") + ")");
|
||
BossHpBar.DebugFakeMaxHp = 1800f; BossHpBar.DebugFakeHp = 869f;
|
||
bool did = _bar.PollBoss(true);
|
||
Chk(!did, "폴링 off → 갱신 0(C8 롤백 = 813q2 동작)");
|
||
Chk(Math.Abs(_bar.LastRatio - 1f) < 0.001f,
|
||
"🔴 D-26 재현 — 실제 869/1800 = 0.483 인데 바는 " + _bar.LastRatio.ToString("F3") + " (MaxHP 가 폴백 " +
|
||
_bar.PolledMaxHp.ToString("F0") + " 에 굳어 Clamp01(869/100)=1.000)");
|
||
|
||
// (b) 고침 — 폴링 on
|
||
hud.bossBarPollEnabled = true;
|
||
int fixBefore = _bar.MaxHpFixCount;
|
||
did = _bar.PollBoss(true);
|
||
Chk(did, "폴링 on → 갱신 1회");
|
||
Chk(_bar.MaxHpFixCount == fixBefore + 1, "MaxHP 보정 1회(100 → " + _bar.PolledMaxHp.ToString("F0") + ")");
|
||
Chk(Math.Abs(_bar.LastRatio - 0.483f) < 0.001f,
|
||
"발주서 §1-5 — 1800→869 → fill " + _bar.LastRatio.ToString("F3") + " (기대 0.483)");
|
||
Chk(Math.Abs(FillAmount() - _bar.LastRatio) < 0.0001f, "Image.fillAmount 실측 = " + FillAmount().ToString("F3"));
|
||
|
||
// (c) 계속 따라가는가 — 이벤트 없이 값만 바뀌어도(리쉬 회복·리젠 경로) 따라온다
|
||
BossHpBar.DebugFakeHp = 1251f; // 결과창 시점 69.5 %
|
||
_bar.PollBoss(true);
|
||
Chk(Math.Abs(_bar.LastRatio - 0.695f) < 0.002f, "HP 가 올라가도 따라간다(회복 경로) ratio=" + _bar.LastRatio.ToString("F3"));
|
||
BossHpBar.DebugFakeHp = 180f;
|
||
_bar.PollBoss(true);
|
||
Chk(Math.Abs(_bar.LastRatio - 0.1f) < 0.002f, "10 % 구간 ratio=" + _bar.LastRatio.ToString("F3"));
|
||
|
||
// (d) 3칸 분할 표시 유지
|
||
int ticks = TickCount();
|
||
Chk(ticks == 2, "페이즈 눈금 " + ticks + "개 = 3칸 분할 유지(0.70 / 0.40)");
|
||
N("눈금 위치 : " + TickPositions());
|
||
|
||
// (e) 사망 → 0 → 숨김
|
||
BossHpBar.DebugFakeHp = 0f;
|
||
_bar.PollBoss(true);
|
||
Chk(Math.Abs(_bar.LastRatio) < 0.0001f, "HP 0 → fill 0.000");
|
||
N("RaiseFakeKilled : " + BossHpBar.RaiseFakeKilled());
|
||
Chk(Math.Abs(_bar.LastRatio) < 0.0001f && _bar.KilledSeen > 0, "처치 이벤트 수신 → 0 유지(숨김은 bossBarHideDelaySec 뒤)");
|
||
|
||
// (f) 물지 않은 상태에서는 폴링이 아무 것도 켜지 않는다(D-23 과의 상호작용)
|
||
BossHpBar.ClearFakeHp();
|
||
_bar.ResetForNewRun();
|
||
WL.Combat.Boss.BossArena.BossSpawned = false;
|
||
bool before = _bar.Visible;
|
||
_bar.PollBoss(true);
|
||
Chk(!_bar.Visible && !before, "물림 해제 상태 + BossSpawned=false → 폴링이 바를 다시 켜지 않는다");
|
||
N(_bar.Dump());
|
||
}
|
||
|
||
static float FillAmount()
|
||
{
|
||
var f = _bar.GetComponentInChildren<Image>(true);
|
||
var all = _bar.GetComponentsInChildren<Image>(true);
|
||
for (int i = 0; i < all.Length; i++) if (all[i].name == "Fill") return all[i].fillAmount;
|
||
return f != null ? f.fillAmount : -1f;
|
||
}
|
||
|
||
static Transform Ticks()
|
||
{
|
||
var t = _bar.transform.Find("Bar/Ticks");
|
||
return t;
|
||
}
|
||
|
||
static int TickCount() { var t = Ticks(); return t != null ? t.childCount : -1; }
|
||
|
||
static string TickPositions()
|
||
{
|
||
var t = Ticks();
|
||
if (t == null) return "(없음)";
|
||
var sb = new StringBuilder();
|
||
for (int i = 0; i < t.childCount; i++)
|
||
{
|
||
var rt = t.GetChild(i) as RectTransform;
|
||
if (rt != null) sb.Append(rt.name).Append("@").Append(rt.anchorMin.x.ToString("F2")).Append(" ");
|
||
}
|
||
return sb.ToString();
|
||
}
|
||
|
||
// ───────────────────────────────────────────── ③ D-23
|
||
|
||
static void Step2_RunStartReset(WLHudLayoutSettings hud, WLRunUiSettings run)
|
||
{
|
||
H("③ D-23 잔여 — 「다음 런」 뒤 이전 런 보스 바 잔존 (Q6 `PD3_07`)");
|
||
if (_bar == null || hud == null) { Chk(false, "전제 없음"); return; }
|
||
|
||
// (a) 왜 813q2 리셋이 안 먹은 것처럼 보였나 — ResetGate 가 BossArena.s_boss 를 비우지 않는다(실행으로 확인)
|
||
var armed = typeof(WL.Combat.Boss.BossArena).GetField("s_armed", BindingFlags.Static | BindingFlags.NonPublic);
|
||
Chk(armed != null, "BossArena.s_armed 필드 확보(리플렉션 · 읽기/쓰기 1회)");
|
||
var probeGo = new GameObject("__WL813c2_FakeBoss");
|
||
probeGo.SetActive(false); // 비활성 = Awake 미실행 · IsAliveBoss 도 false
|
||
probeGo.hideFlags = HideFlags.HideAndDontSave;
|
||
MobActor fakeBoss = null;
|
||
try { fakeBoss = probeGo.AddComponent<MobActor>(); } catch (Exception ex) { N("MobActor 부착 실패 — " + ex.Message); }
|
||
if (fakeBoss != null && armed != null)
|
||
{
|
||
bool armedBefore = (bool)armed.GetValue(null);
|
||
armed.SetValue(null, true);
|
||
WL.Combat.Boss.BossArena.SetBossForProbe(fakeBoss);
|
||
WL.Combat.Boss.BossArena.BossSpawned = true;
|
||
Chk(WL.Combat.Boss.BossArena.Boss != null, "아레나가 보스를 잡고 있다");
|
||
WL.Combat.Boss.BossArena.ResetGate();
|
||
Chk(WL.Combat.Boss.BossArena.Boss != null,
|
||
"🔴 원인 — `BossArena.ResetGate()`(다음 런) 뒤에도 `BossArena.Boss` 가 **그대로 남는다** → 813q2 리셋 직후 " +
|
||
"`BindExistingBoss()` 가 이전 런 보스를 즉시 다시 문다(= 화면상 「잔존」)");
|
||
Chk(!WL.Combat.Boss.BossArena.BossSpawned, "반면 `BossSpawned` 는 ResetGate 가 false 로 되돌린다 → 이 값을 재바인드 조건으로 쓴다");
|
||
WL.Combat.Boss.BossArena.SetBossForProbe(null);
|
||
armed.SetValue(null, armedBefore);
|
||
}
|
||
UnityEngine.Object.DestroyImmediate(probeGo);
|
||
|
||
// (b) 고침 — RunStarted 로 숨김 + 이번 런 보스 미등장이면 재바인드 안 함
|
||
hud.bossBarPollEnabled = true;
|
||
hud.bossBarRebindRequiresArenaSpawn = true;
|
||
BossHpBar.DebugFakeMaxHp = 1800f; BossHpBar.DebugFakeHp = 869f;
|
||
N("RaiseFakeBossSpawned(1800) : " + BossHpBar.RaiseFakeBossSpawned(1800f));
|
||
_bar.PollBoss(true);
|
||
Chk(_bar.Visible, "런1 — 바 표시 중(ratio " + _bar.LastRatio.ToString("F3") + ")");
|
||
|
||
WL.Combat.Boss.BossArena.BossSpawned = false; // 「다음 런」 = ResetGate 직후 상태
|
||
int resetBefore = BossHpBar.RunResetCount;
|
||
int subs = RunEvents.RunStarted.Count;
|
||
N("RunStarted 구독자 = " + subs);
|
||
WLRunUiProbeHooks.RaiseRunStarted(2, 270f, 2, "probe-813c2");
|
||
Chk(BossHpBar.RunResetCount == resetBefore + 1, "RunStarted → 런 시작 리셋 1회");
|
||
Chk(!_bar.Visible, "✅ D-23 — 「다음 런」 직후 보스 바 **숨김**(visible=" + _bar.Visible + ")");
|
||
Chk(BossHpBar.RunBossHpRatio < 0f, "결과 화면이 읽는 런 단위 HP 값도 비워졌다(" + BossHpBar.RunBossHpRatio.ToString("F2") + ")");
|
||
_bar.PollBoss(true);
|
||
Chk(!_bar.Visible, "그 뒤 폴링을 돌려도 다시 켜지지 않는다(재바인드 조건 미충족)");
|
||
|
||
// (c) C8 롤백 — 조건을 끄면 예전 경로(즉시 재바인드 시도)로 돌아간다
|
||
hud.bossBarRebindRequiresArenaSpawn = false;
|
||
string r = _bar.ResetForNewRun();
|
||
N("조건 off 리셋 결과 : " + r);
|
||
Chk(r.Contains("늦은구독"), "C8 롤백 — 조건을 끄면 곧바로 `BindExistingBoss()` 로 간다(813q2 동작)");
|
||
hud.bossBarRebindRequiresArenaSpawn = true;
|
||
|
||
// (d) 스위치 자체 롤백
|
||
if (run != null)
|
||
{
|
||
bool sv = run.bossResetOnRunStart;
|
||
run.bossResetOnRunStart = false;
|
||
string r2 = _bar.ResetForNewRun();
|
||
Chk(r2.Contains("bossResetOnRunStart=false"), "C8 — bossResetOnRunStart=0 → 리셋 자체 생략(813q 동작)");
|
||
run.bossResetOnRunStart = sv;
|
||
}
|
||
BossHpBar.ClearFakeHp();
|
||
}
|
||
|
||
// ───────────────────────────────────────────── ④ D-31
|
||
|
||
static void Step3_ToastQueue(WLCombatTextSettings txt)
|
||
{
|
||
H("④ D-31 — 획득 토스트 (Q6: 60 s 21건 · 「창 안 6줄」)");
|
||
if (_toast == null || txt == null) { Chk(false, "전제 없음"); return; }
|
||
|
||
N("🔴 실측 구분 — Q6 의 「창 안 6줄」은 프로브가 **표시 시간 창 안의 Picked 이벤트 수**를 센 값이다");
|
||
N(" (`Assets/WL/Tools/WL813_ScoreProbe.cs:437~443`). 화면 동시 줄 수는 813g 때부터 lootToastLines 로 막혀 있다.");
|
||
|
||
_toast.HideAll();
|
||
_toast.ResetPollBaseline();
|
||
|
||
// (a) 서로 다른 아이템 21건 연속 — 동시 줄 수가 상한을 넘지 않는다
|
||
int maxSeen = 0;
|
||
for (int i = 0; i < 21; i++)
|
||
{
|
||
_toast.Push(9000 + i, 1);
|
||
if (_toast.VisibleLines > maxSeen) maxSeen = _toast.VisibleLines;
|
||
}
|
||
Chk(maxSeen <= txt.lootToastLines, "서로 다른 21건 → 화면 동시 최대 " + maxSeen + "줄 ≤ 상한 " + txt.lootToastLines);
|
||
Chk(_toast.OverflowFadeCount > 0, "넘친 만큼 오래된 줄을 즉시 페이드시켰다(" + _toast.OverflowFadeCount + "회 · " +
|
||
txt.lootToastOverflowFadeSec.ToString("F2") + "s)");
|
||
Chk(_toast.QueuedCount > 0 || _toast.QueueDropCount > 0, "나머지는 큐에서 자리를 기다린다(대기 " + _toast.QueuedCount +
|
||
" · 큐버림 " + _toast.QueueDropCount + ")");
|
||
N(_toast.Dump());
|
||
|
||
// 자리가 나면 큐가 순서대로 나간다(런타임은 Update 가 매 프레임 부른다)
|
||
int queued = _toast.QueuedCount;
|
||
_toast.HideAll(); // 줄 3개가 비었다고 가정
|
||
for (int i = 0; i < 21; i++) _toast.Push(9100 + i, 1);
|
||
int held = _toast.QueuedCount;
|
||
ForceExpireLines(); // 배치 모드는 Update 가 안 돌아 수명을 직접 만료시킨다
|
||
int flushed = _toast.FlushQueueNow();
|
||
Chk(flushed > 0, "자리가 나자 큐에서 " + flushed + "건이 나갔다(대기 " + held + " → " + _toast.QueuedCount + ")");
|
||
Chk(_toast.VisibleLines <= txt.lootToastLines, "큐가 나가도 동시 줄 수는 상한 유지(실측 " + _toast.VisibleLines + ")");
|
||
N("첫 배치 대기 " + queued + " · 두 번째 배치 대기 " + held);
|
||
|
||
// (b) 같은 아이템 연속 획득 = 한 줄 수량 병합
|
||
_toast.HideAll();
|
||
bool svGold = txt.lootToastIncludeGold;
|
||
txt.lootToastIncludeGold = true;
|
||
try
|
||
{
|
||
for (int i = 0; i < 8; i++) _toast.Push(LootToast.GoldItemId, 1);
|
||
Chk(_toast.VisibleLines == 1, "같은 아이템 8건 → 줄 " + _toast.VisibleLines + "개(병합)");
|
||
string line = FirstLineText();
|
||
Chk(line.Contains("x8"), "병합 문구 = \"" + line + "\" (형식 " + txt.lootToastFormat + " · 표 미로드라 이름은 ID)");
|
||
}
|
||
finally { txt.lootToastIncludeGold = svGold; }
|
||
|
||
// (c) 병합 창 밖이어도 보이는 동안이면 병합한다(줄 폭주 방지)
|
||
_toast.HideAll();
|
||
_toast.Push(9500, 3);
|
||
AdvanceLines(txt.lootToastMergeSeconds + 0.2f); // 병합 창은 지났지만 아직 보이는 상태
|
||
_toast.Push(9500, 2);
|
||
Chk(_toast.VisibleLines == 1, "병합 창(" + txt.lootToastMergeSeconds + "s) 밖이어도 보이는 동안이면 한 줄 — 실측 " + _toast.VisibleLines);
|
||
bool svVis = txt.lootToastMergeWhileVisible;
|
||
txt.lootToastMergeWhileVisible = false;
|
||
try
|
||
{
|
||
_toast.HideAll();
|
||
_toast.Push(9600, 1);
|
||
AdvanceLines(txt.lootToastMergeSeconds + 0.2f);
|
||
_toast.Push(9600, 1);
|
||
Chk(_toast.VisibleLines == 2, "C8 — 스위치를 끄면 창 밖은 새 줄(813g 동작) 실측 " + _toast.VisibleLines);
|
||
}
|
||
finally { txt.lootToastMergeWhileVisible = svVis; }
|
||
|
||
// (d) C8 — 넘침 페이드 0 이면 813g 의 즉시 한 칸 당김
|
||
_toast.HideAll();
|
||
float svFade = txt.lootToastOverflowFadeSec;
|
||
txt.lootToastOverflowFadeSec = 0f;
|
||
try
|
||
{
|
||
int q0 = _toast.QueuedCount;
|
||
for (int i = 0; i < 6; i++) _toast.Push(9700 + i, 1);
|
||
Chk(_toast.QueuedCount == q0, "C8 — 페이드 0 이면 큐를 쓰지 않는다(대기 " + _toast.QueuedCount + ")");
|
||
Chk(_toast.VisibleLines <= txt.lootToastLines, "그래도 동시 줄 수는 상한 " + txt.lootToastLines + " 유지(실측 " + _toast.VisibleLines + ")");
|
||
}
|
||
finally { txt.lootToastOverflowFadeSec = svFade; }
|
||
|
||
// (e) 전체 off
|
||
_toast.HideAll();
|
||
bool svOn = txt.lootToastEnabled;
|
||
txt.lootToastEnabled = false;
|
||
try
|
||
{
|
||
_toast.Push(9800, 1);
|
||
Chk(_toast.VisibleLines == 0, "C8 — lootToastEnabled=0 → 표시 0");
|
||
}
|
||
finally { txt.lootToastEnabled = svOn; }
|
||
_toast.HideAll();
|
||
}
|
||
|
||
static string FirstLineText()
|
||
{
|
||
var all = _toast.GetComponentsInChildren<TextMeshProUGUI>(true);
|
||
for (int i = 0; i < all.Length; i++)
|
||
if (!string.IsNullOrEmpty(all[i].text)) return all[i].text;
|
||
return "";
|
||
}
|
||
|
||
/// <summary>배치 모드는 시간이 안 흐른다 — 줄 수명(elapsed)만 리플렉션으로 밀어 「창 밖」을 만든다.</summary>
|
||
static void AdvanceLines(float sec)
|
||
{
|
||
foreach (var l in LinesOf())
|
||
{
|
||
var fe = l.GetType().GetField("elapsed");
|
||
if (fe != null) fe.SetValue(l, (float)fe.GetValue(l) + sec);
|
||
}
|
||
}
|
||
|
||
/// <summary>배치 모드에서 줄 수명이 다 된 상태를 만든다(런타임은 Update 가 한다).</summary>
|
||
static void ForceExpireLines()
|
||
{
|
||
foreach (var l in LinesOf())
|
||
{
|
||
var fu = l.GetType().GetField("used");
|
||
var fl = l.GetType().GetField("life");
|
||
if (fu != null) fu.SetValue(l, false);
|
||
if (fl != null) fl.SetValue(l, 0f);
|
||
}
|
||
}
|
||
|
||
static System.Collections.IEnumerable LinesOf()
|
||
{
|
||
var f = typeof(LootToast).GetField("_lines", BindingFlags.Instance | BindingFlags.NonPublic);
|
||
var list = f != null ? f.GetValue(_toast) as System.Collections.IEnumerable : null;
|
||
return list ?? new object[0];
|
||
}
|
||
|
||
// ───────────────────────────────────────────── ⑤ D-29
|
||
|
||
static void Step4_CardRevivePause(WLZonePickupSettings zp)
|
||
{
|
||
H("⑤ D-29 — 부활 팝업이 카드를 덮는 동안 카드 타이머 정지 (Q6 `R3_15`)");
|
||
if (zp == null || _uiRoot == null) { Chk(false, "전제 없음"); return; }
|
||
|
||
// 원본 데이터 경로 대역(813o 선례) — 카드 뽑기는 원본 table_selectskill / InGameInfo 를 그대로 쓴다
|
||
_fakes = new GameObject("__WL813c2_Infos");
|
||
_fakes.hideFlags = HideFlags.HideAndDontSave;
|
||
var tbl = _fakes.AddComponent<table_selectskill>();
|
||
tbl.m_json = AssetDatabase.LoadAssetAtPath<TextAsset>(kSelectSkillTxt);
|
||
var awake = typeof(table_selectskill).GetMethod("Awake", BindingFlags.NonPublic | BindingFlags.Instance);
|
||
if (awake != null) awake.Invoke(tbl, null);
|
||
Chk(table_selectskill.Ins != null, "table_selectskill.Ins(원본 Awake 경로)");
|
||
InGameInfo.Ins = _fakes.AddComponent<InGameInfo>();
|
||
ActorInfo.Ins = _fakes.AddComponent<ActorInfo>();
|
||
|
||
ZonePickup.Subscribe(true);
|
||
N("ZonePickup.Bind : " + ZonePickup.Bind(_uiRoot));
|
||
Chk(ZonePickup.Bound, "카드 노드 조립");
|
||
|
||
// (a) 정렬 — 카드가 부활 팝업 아래
|
||
var sv = WLSurvivalSettings.Instance;
|
||
Chk(ZonePickup.SortingOrder == zp.cardSortingOrder, "카드 전용 Canvas sortingOrder = " + ZonePickup.SortingOrder);
|
||
Chk(sv != null && ZonePickup.SortingOrder < sv.reviveSortingOrder,
|
||
"✅ 카드(" + ZonePickup.SortingOrder + ") < 부활 팝업(" + (sv != null ? sv.reviveSortingOrder : -1) + ") = 부활이 위로 온다");
|
||
|
||
// (b) 카드를 먼저 연다
|
||
ZonePickup.ResetState();
|
||
RunDirector.EnsureSubscribed();
|
||
RunDirector.StartRun("probe-813c2");
|
||
WLZonePickupProbeHooks.RaiseZoneCleared(0, 813001, 30f, 12, 12, false);
|
||
float t0 = ZonePickup.NowClock();
|
||
ZonePickup.Tick(t0 + zp.openDelaySeconds + 0.01f);
|
||
if (!Chk(ZonePickup.IsOpen, "카드 열림 — " + ZonePickup.Dump())) return;
|
||
float tOpen = t0 + zp.openDelaySeconds + 0.01f;
|
||
|
||
// (c) 3초 흐른 뒤 사망 → 부활 팝업이 카드 위로 온다
|
||
ZonePickup.Tick(tOpen + 3f);
|
||
int secBefore = ZonePickup.ShownSecond;
|
||
N("3 s 경과 — 남은 초 표시 = " + ZonePickup.TimerText + " (" + secBefore + ")");
|
||
|
||
N("ReviveDialog.ShowNowOnce : " + Trim(_revive.ShowNowOnce(), 160));
|
||
Chk(ReviveDialog.Busy, "부활 팝업 Busy = " + ReviveDialog.Busy);
|
||
Chk(ZonePickup.IsOpen, "카드는 닫히지 않는다(부활 아래에 그대로 있다)");
|
||
|
||
// (d) 부활 팝업이 떠 있는 동안 9초를 흘려도 자동 선택되지 않는다 = D-29 고침
|
||
for (int i = 1; i <= 9; i++) ZonePickup.Tick(tOpen + 3f + i);
|
||
Chk(ZonePickup.TimeoutHeldByRevive, "타이머 정지 상태 = " + ZonePickup.TimeoutHeldByRevive);
|
||
Chk(ZonePickup.IsOpen && ZonePickup.TimeoutCount == 0,
|
||
"✅ D-29 — 부활 중 9 s 가 흘러도 자동 선택 0(열림=" + ZonePickup.IsOpen + " timeout=" + ZonePickup.TimeoutCount + ")");
|
||
Chk(ZonePickup.ShownSecond == secBefore,
|
||
"남은 초 표시도 멈춰 있다 " + secBefore + " → " + ZonePickup.ShownSecond + " (\"" + ZonePickup.TimerText + "\")");
|
||
Chk(Math.Abs(ZonePickup.PausedSeconds - 9f) < 0.05f, "누적 정지 " + ZonePickup.PausedSeconds.ToString("F2") + "s (기대 9.00)");
|
||
Chk(ZonePickup.ReviveHoldCount == 1, "정지 진입 1회 — 실측 " + ZonePickup.ReviveHoldCount);
|
||
|
||
// (e) 부활이 닫히면 남은 초부터 재개
|
||
_revive.HideNow();
|
||
Chk(!ReviveDialog.Busy, "부활 팝업 닫힘");
|
||
ZonePickup.Tick(tOpen + 12f);
|
||
Chk(!ZonePickup.TimeoutHeldByRevive && ZonePickup.ReviveResumeCount == 1, "재개 1회 — 실측 " + ZonePickup.ReviveResumeCount);
|
||
Chk(ZonePickup.IsOpen, "재개 직후에도 아직 열려 있다(남은 " + (zp.pickTimeoutSeconds - 3f).ToString("F0") + "초부터)");
|
||
ZonePickup.Tick(tOpen + 3f + 9f + zp.pickTimeoutSeconds - 3f + 0.1f);
|
||
Chk(!ZonePickup.IsOpen && ZonePickup.TimeoutCount == 1,
|
||
"정지분(9 s)을 뺀 남은 시간이 다 지나면 그때 자동 선택(timeout=" + ZonePickup.TimeoutCount + ")");
|
||
|
||
// (f) C8 — 스위치를 끄면 D-29 가 그대로 재현된다
|
||
bool svPause = zp.pauseTimeoutWhileRevive;
|
||
zp.pauseTimeoutWhileRevive = false;
|
||
try
|
||
{
|
||
ZonePickup.ResetState();
|
||
WLZonePickupProbeHooks.RaiseZoneCleared(0, 813001, 30f, 12, 12, false);
|
||
float u0 = ZonePickup.NowClock();
|
||
ZonePickup.Tick(u0 + zp.openDelaySeconds + 0.01f);
|
||
float uOpen = u0 + zp.openDelaySeconds + 0.01f;
|
||
Chk(ZonePickup.IsOpen, "C8 — 카드 다시 열림");
|
||
N("ReviveDialog.ShowNowOnce : " + Trim(_revive.ShowNowOnce(), 120));
|
||
for (int i = 1; i <= 11; i++) ZonePickup.Tick(uOpen + i);
|
||
Chk(ZonePickup.TimeoutCount == 1 && !ZonePickup.IsOpen,
|
||
"🔴 C8 재현 — 스위치를 끄면 부활 팝업 아래에서 타이머가 흘러 자동 선택된다(= Q6 D-29 그대로)");
|
||
_revive.HideNow();
|
||
}
|
||
finally { zp.pauseTimeoutWhileRevive = svPause; ZonePickup.ResetState(); }
|
||
|
||
// (g) 전용 캔버스 롤백
|
||
bool svCanvas = zp.ownCanvasEnabled;
|
||
zp.ownCanvasEnabled = false;
|
||
try
|
||
{
|
||
ZonePickup.ApplyLayout();
|
||
Chk(ZonePickup.SortingOrder == -1, "C8 — ownCanvasEnabled=0 → overrideSorting 해제(형제 순서만 · 813o 동작)");
|
||
}
|
||
finally { zp.ownCanvasEnabled = svCanvas; ZonePickup.ApplyLayout(); }
|
||
Chk(ZonePickup.SortingOrder == zp.cardSortingOrder, "복구 sortingOrder=" + ZonePickup.SortingOrder);
|
||
}
|
||
|
||
// ───────────────────────────────────────────── ⑥ GC
|
||
|
||
static void Step5_Gc(WLHudLayoutSettings hud, WLCombatTextSettings txt)
|
||
{
|
||
H("⑥ GC — 매 프레임 경로(보스 바 폴링 · 카드 틱 · 토스트 수명)에서 할당 0");
|
||
if (_bar == null || hud == null) { Chk(false, "전제 없음"); return; }
|
||
|
||
hud.bossBarPollEnabled = true;
|
||
BossHpBar.DebugFakeMaxHp = 1800f; BossHpBar.DebugFakeHp = 869f;
|
||
BossHpBar.RaiseFakeBossSpawned(1800f);
|
||
_bar.PollBoss(true);
|
||
|
||
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
|
||
long a = GC.GetTotalMemory(true);
|
||
for (int i = 0; i < 20000; i++) _bar.PollBoss(true);
|
||
long b = GC.GetTotalMemory(false);
|
||
N("보스 바 폴링 20,000회 : " + a + " → " + b + " (" + (b - a) + " B)");
|
||
Chk(b - a < 4096, "폴링 경로 할당 ≈ 0 (실측 " + (b - a) + " B / 20,000회)");
|
||
|
||
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
|
||
a = GC.GetTotalMemory(true);
|
||
float t = ZonePickup.NowClock();
|
||
for (int i = 0; i < 20000; i++) ZonePickup.Tick(t);
|
||
b = GC.GetTotalMemory(false);
|
||
N("픽업 Tick(대기 상태) 20,000회 : " + (b - a) + " B");
|
||
Chk(b - a < 4096, "카드 틱 경로 할당 ≈ 0 (실측 " + (b - a) + " B / 20,000회)");
|
||
|
||
BossHpBar.ClearFakeHp();
|
||
}
|
||
}
|