Merge branch 'wl/ui/WL-813c2-bossbar-fix'
This commit is contained in:
commit
6137ff94fd
|
|
@ -0,0 +1,126 @@
|
|||
# WL813c2 Probe 2026-09-09 14:46:39 (edit mode · Play False)
|
||||
|
||||
── ⓪ 값 — 이번 작업으로 늘어난 SO 필드 실측(코드 상수 0)
|
||||
[PASS] WLHudLayoutSettings 에셋
|
||||
보스 바 : poll=True @10Hz · 아레나추종=True · 사망숨김=True · 재바인드조건(BossSpawned)=True · 눈금=0.70,0.40 · 숨김지연=1.2s
|
||||
[PASS] WLCombatTextSettings 에셋
|
||||
토스트 : 줄상한=3 · 표시=2s · 병합창=0.5s(보이는동안=True) · 넘침페이드=0.15s · 큐=6 · 골드포함=False
|
||||
[PASS] WLZonePickupSettings 에셋
|
||||
픽업카드: 부활중정지=True · 전용캔버스=True · sortingOrder=40 · 타임아웃=10s · 열기지연=0.35s
|
||||
부활 : reviveSortingOrder=60
|
||||
[PASS] 카드 sortingOrder 40 < 부활 60 (발주서 §1-3)
|
||||
런 UI : bossResetOnRunStart=True · bossBarRebindOnRunStart=True · bossBannerRetriggerOnRunStart=False
|
||||
|
||||
── ① 합성 캔버스(1080×1920 Expand) · IngameUIs/WL_HUD · 보스 바 · 토스트 · 부활 팝업
|
||||
BossHpBar.Initialize : initialized subscribed=True · layout size=(1564.44, 117.33) unitsPerPx=1.7778 ticks=2 · 늦은구독: 살아 있는 보스 없음(스캔 0)
|
||||
[PASS] 보스 바 조립
|
||||
LootToast.Initialize : 구독 ON · lines=3 root pos=(-71.11, -213.33) size=(962.67, 362.67) unitsPerPx=1.7778 · 부모=607.5px · 폭 560→541.5px(상한 560) · 우여백 40→40.0px · pad=26.0px · 슬라이드 60→14.0px · 우측 최대 침범 0.0…
|
||||
[PASS] 토스트 조립
|
||||
ReviveDialog.Initialize : initialized · 폴백 팝업 box size(1351.11, 675.56) unitsPerPx=1.7778
|
||||
[PASS] 부활 팝업은 닫힌 상태로 시작
|
||||
|
||||
── ② D-26 — 보스 HP 바가 실제 HP 를 따라가는가 (Q6 `PD3_05` = 프로브 869/1800 인데 바 3칸 만피)
|
||||
RaiseFakeBossSpawned(100) : dispatched Spawned(isBoss=true) subscribers=1
|
||||
[PASS] 물린 직후 ratio=1.000 (실측 1.000)
|
||||
[PASS] 폴링 off → 갱신 0(C8 롤백 = 813q2 동작)
|
||||
[PASS] 🔴 D-26 재현 — 실제 869/1800 = 0.483 인데 바는 1.000 (MaxHP 가 폴백 100 에 굳어 Clamp01(869/100)=1.000)
|
||||
[PASS] 폴링 on → 갱신 1회
|
||||
[PASS] MaxHP 보정 1회(100 → 1800)
|
||||
[PASS] 발주서 §1-5 — 1800→869 → fill 0.483 (기대 0.483)
|
||||
[PASS] Image.fillAmount 실측 = 0.483
|
||||
[PASS] HP 가 올라가도 따라간다(회복 경로) ratio=0.695
|
||||
[PASS] 10 % 구간 ratio=0.100
|
||||
[PASS] 페이즈 눈금 2개 = 3칸 분할 유지(0.70 / 0.40)
|
||||
눈금 위치 : Tick_0@0.70 Tick_1@0.40
|
||||
[PASS] HP 0 → fill 0.000
|
||||
RaiseFakeKilled : dispatched Killed subscribers=2
|
||||
[PASS] 처치 이벤트 수신 → 0 유지(숨김은 bossBarHideDelaySec 뒤)
|
||||
[PASS] 물림 해제 상태 + BossSpawned=false → 폴링이 바를 다시 켜지 않는다
|
||||
[BossHpBar] subscribed=True visible=False ratio=0.000 hp=0/1800 seen(Spawned/Hit/Killed)=1/0/1 늦은구독=False(스캔 0 · BossArena.Boss=없음)
|
||||
[813c2] 폴링 4회 · MaxHP 보정 1회 · 아레나 재바인드 0회 · hp=0/1800 · BossArena.BossSpawned=False · 설정 poll=True@10Hz 아레나추종=True 사망숨김=True 재바인드조건=True
|
||||
[813q2] 런 리셋 1회 · 런 보스 HP=없음 (본적=False) · RunStarted 구독=1
|
||||
CombatEvents subscribers Spawned=1 HitConfirmed=1 Killed=2 Enabled=True
|
||||
root pos(0.00, -337.78) size(1564.44, 117.33) aMin(0.50, 1.00) aMax(0.50, 1.00)
|
||||
bar size(0.00, 49.78) aMin(0.00, 0.00) aMax(1.00, 0.00) fill=0.000 ticks=2
|
||||
Tick_0 at=0.700 w=7.1
|
||||
Tick_1 at=0.400 w=7.1
|
||||
설정 sizePx=(880.00, 28.00) topMarginPx=190 ticks=0.70,0.40
|
||||
|
||||
|
||||
── ③ D-23 잔여 — 「다음 런」 뒤 이전 런 보스 바 잔존 (Q6 `PD3_07`)
|
||||
[PASS] BossArena.s_armed 필드 확보(리플렉션 · 읽기/쓰기 1회)
|
||||
[PASS] 아레나가 보스를 잡고 있다
|
||||
[PASS] 🔴 원인 — `BossArena.ResetGate()`(다음 런) 뒤에도 `BossArena.Boss` 가 **그대로 남는다** → 813q2 리셋 직후 `BindExistingBoss()` 가 이전 런 보스를 즉시 다시 문다(= 화면상 「잔존」)
|
||||
[PASS] 반면 `BossSpawned` 는 ResetGate 가 false 로 되돌린다 → 이 값을 재바인드 조건으로 쓴다
|
||||
RaiseFakeBossSpawned(1800) : dispatched Spawned(isBoss=true) subscribers=1
|
||||
[PASS] 런1 — 바 표시 중(ratio 0.483)
|
||||
RunStarted 구독자 = 1
|
||||
[PASS] RunStarted → 런 시작 리셋 1회
|
||||
[PASS] ✅ D-23 — 「다음 런」 직후 보스 바 **숨김**(visible=False)
|
||||
[PASS] 결과 화면이 읽는 런 단위 HP 값도 비워졌다(-1.00)
|
||||
[PASS] 그 뒤 폴링을 돌려도 다시 켜지지 않는다(재바인드 조건 미충족)
|
||||
조건 off 리셋 결과 : 런시작리셋 → 늦은구독: 살아 있는 보스 없음(스캔 0)
|
||||
[PASS] C8 롤백 — 조건을 끄면 곧바로 `BindExistingBoss()` 로 간다(813q2 동작)
|
||||
[PASS] C8 — bossResetOnRunStart=0 → 리셋 자체 생략(813q 동작)
|
||||
|
||||
── ④ D-31 — 획득 토스트 (Q6: 60 s 21건 · 「창 안 6줄」)
|
||||
🔴 실측 구분 — Q6 의 「창 안 6줄」은 프로브가 **표시 시간 창 안의 Picked 이벤트 수**를 센 값이다
|
||||
(`Assets/WL/Tools/WL813_ScoreProbe.cs:437~443`). 화면 동시 줄 수는 813g 때부터 lootToastLines 로 막혀 있다.
|
||||
[PASS] 서로 다른 21건 → 화면 동시 최대 3줄 ≤ 상한 3
|
||||
[PASS] 넘친 만큼 오래된 줄을 즉시 페이드시켰다(1회 · 0.15s)
|
||||
[PASS] 나머지는 큐에서 자리를 기다린다(대기 6 · 큐버림 12)
|
||||
LootToast subscribed=True killedSeen=1 push=3 merge=0 drop=1 visible=3
|
||||
[813c2] 상한=3 대기=6/6 큐버림=12 넘침페이드=1회 · 병합창=0.50s(보이는동안=True) · 페이드=0.15s
|
||||
[0] used=True item=9000 x1 alpha=1.00 pos=(0.00, 0.00) size=(962.67, 120.89) font=92.4 panel=#08080DAD off(-32.00, -10.67)(32.00, 10.67) text="<color=#F2F5FA>9000[-]"
|
||||
[1] used=True item=9001 x1 alpha=1.00 pos=(0.00, -120.89) size=(962.67, 120.89) font=92.4 panel=#08080DAD off(-32.00, -10.67)(32.00, 10.67) text="<color=#F2F5FA>9001[-]"
|
||||
[2] used=True item=9002 x1 alpha=1.00 pos=(0.00, -241.78) size=(962.67, 120.89) font=92.4 panel=#08080DAD off(-32.00, -10.67)(32.00, 10.67) text="<color=#F2F5FA>9002[-]"
|
||||
설정 seconds=2s fontPx=52 widthPx=560 lineHeightPx=68 rightMarginPx=40 centerOffsetPx=-120 panel=True 밝기하한=0.72 · 등급1 태그 <color=#a6a6a7> → <color=#F2F5FA>
|
||||
clamp = 부모=607.5px · 폭 560→541.5px(상한 560) · 우여백 40→40.0px · pad=26.0px · 슬라이드 60→14.0px · 우측 최대 침범 0.0px · clamp=True
|
||||
edge = LiberationSans SDF Material (WL813y Edge) shader=TextMeshPro/Mobile/Distance Field outlineW=0.240 outlineC=080505FF underlay=000000B2 off(0.40,-0.40) soft=0.18 UNDERLAY_ON=True
|
||||
|
||||
[PASS] 자리가 나자 큐에서 3건이 나갔다(대기 6 → 3)
|
||||
[PASS] 큐가 나가도 동시 줄 수는 상한 유지(실측 3)
|
||||
첫 배치 대기 6 · 두 번째 배치 대기 6
|
||||
[PASS] 같은 아이템 8건 → 줄 1개(병합)
|
||||
[PASS] 병합 문구 = "<color=#F2F5FA>2[-] x8" (형식 {0}[-] x{1} · 표 미로드라 이름은 ID)
|
||||
[PASS] 병합 창(0.5s) 밖이어도 보이는 동안이면 한 줄 — 실측 1
|
||||
[PASS] C8 — 스위치를 끄면 창 밖은 새 줄(813g 동작) 실측 2
|
||||
[PASS] C8 — 페이드 0 이면 큐를 쓰지 않는다(대기 0)
|
||||
[PASS] 그래도 동시 줄 수는 상한 3 유지(실측 3)
|
||||
[PASS] C8 — lootToastEnabled=0 → 표시 0
|
||||
|
||||
── ⑤ D-29 — 부활 팝업이 카드를 덮는 동안 카드 타이머 정지 (Q6 `R3_15`)
|
||||
[PASS] table_selectskill.Ins(원본 Awake 경로)
|
||||
ZonePickup.Bind : 연결 IngameUIs/WL_HUD · box(1742.22, 1280.00) 카드=3 unitsPerPx=1.7778
|
||||
[PASS] 카드 노드 조립
|
||||
[PASS] 카드 전용 Canvas sortingOrder = 40
|
||||
[PASS] ✅ 카드(40) < 부활 팝업(60) = 부활이 위로 온다
|
||||
[PASS] 카드 열림 — phase=Open open=True zone=0 picks=0 cards=3 sec=10 timerPaused=True worldHeld=False actorsStopped=True orig=False bound=True · [813c2] 부활정지=False(정지 0회 · 재개 0회 · 틱 0 · 누적 0.00s) sortingOrder=40(부활 60) raycaster=True
|
||||
3 s 경과 — 남은 초 표시 = 7초 후 자동 선택 (7)
|
||||
ReviveDialog.ShowNowOnce : 폴백 팝업 표시 msg="존 시작점에서 부활합니다.
|
||||
무료"
|
||||
[PASS] 부활 팝업 Busy = True
|
||||
[PASS] 카드는 닫히지 않는다(부활 아래에 그대로 있다)
|
||||
[PASS] 타이머 정지 상태 = True
|
||||
[PASS] ✅ D-29 — 부활 중 9 s 가 흘러도 자동 선택 0(열림=True timeout=0)
|
||||
[PASS] 남은 초 표시도 멈춰 있다 7 → 7 ("7초 후 자동 선택")
|
||||
[PASS] 누적 정지 9.00s (기대 9.00)
|
||||
[PASS] 정지 진입 1회 — 실측 1
|
||||
[PASS] 부활 팝업 닫힘
|
||||
[PASS] 재개 1회 — 실측 1
|
||||
[PASS] 재개 직후에도 아직 열려 있다(남은 7초부터)
|
||||
[PASS] 정지분(9 s)을 뺀 남은 시간이 다 지나면 그때 자동 선택(timeout=1)
|
||||
[PASS] C8 — 카드 다시 열림
|
||||
ReviveDialog.ShowNowOnce : 폴백 팝업 표시 msg="존 시작점에서 부활합니다.
|
||||
무료"
|
||||
[PASS] 🔴 C8 재현 — 스위치를 끄면 부활 팝업 아래에서 타이머가 흘러 자동 선택된다(= Q6 D-29 그대로)
|
||||
[PASS] C8 — ownCanvasEnabled=0 → overrideSorting 해제(형제 순서만 · 813o 동작)
|
||||
[PASS] 복구 sortingOrder=40
|
||||
|
||||
── ⑥ GC — 매 프레임 경로(보스 바 폴링 · 카드 틱 · 토스트 수명)에서 할당 0
|
||||
보스 바 폴링 20,000회 : 1185767424 → 1185767424 (0 B)
|
||||
[PASS] 폴링 경로 할당 ≈ 0 (실측 0 B / 20,000회)
|
||||
픽업 Tick(대기 상태) 20,000회 : 0 B
|
||||
[PASS] 카드 틱 경로 할당 ≈ 0 (실측 0 B / 20,000회)
|
||||
|
||||
== 합계 PASS 65 · FAIL 0 ==
|
||||
|
|
@ -0,0 +1,603 @@
|
|||
// 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -163,6 +163,13 @@ namespace WL.UI
|
|||
HideNow();
|
||||
|
||||
if (ui == null || !ui.bossBannerRetriggerOnRunStart) return "런시작리셋: 숨김·가드 해제만";
|
||||
|
||||
// WL-813c2 (Q6 D-23 잔여) — 813c 바와 같은 조건. `BossArena.ResetGate()`(`BossArena.cs:630`)는
|
||||
// `s_boss` 를 비우지 않아, 조건이 없으면 **이전 런의 살아 있는 보스**로 배너가 다시 뜬다.
|
||||
var hud = WLHudLayoutSettings.Instance;
|
||||
if (hud != null && hud.bossBarRebindRequiresArenaSpawn && !WL.Combat.Boss.BossArena.BossSpawned)
|
||||
return "런시작리셋: 숨김만(이번 런 보스 미등장 · BossArena.BossSpawned=false)";
|
||||
|
||||
return "런시작리셋 → " + TriggerExistingBoss();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -184,16 +184,126 @@ namespace WL.UI
|
|||
LateBound = false;
|
||||
HideNow(); // _bound=false · _boss=null · alpha 0
|
||||
SetRatio(0f);
|
||||
_nextPoll = 0f;
|
||||
|
||||
if (ui != null && !ui.bossBarRebindOnRunStart) return "런시작리셋: 숨김만(재바인드 off)";
|
||||
|
||||
// ── WL-813c2 (Q6 D-23 잔여) ────────────────────────────────────────
|
||||
// 813q2 의 리셋은 **돌고 있다**. 바가 그대로 남은 것처럼 보이는 실측 원인은 리셋 실패가 아니라
|
||||
// **곧바로 이어지는 재바인드**다 — `RunDirector.Restart()`(:497) 는 `BossArena.ResetGate()` 만 부르고,
|
||||
// `ResetGate()`(`BossArena.cs:630~638`)는 `s_boss` 를 **비우지 않는다**. 그래서 `BindExistingBoss()` 의
|
||||
// ① `BossArena.Boss` 가 **이전 런의 살아 있는 보스**를 그대로 돌려주고 바가 즉시 다시 뜬다.
|
||||
// → 이번 런의 보스가 실제로 등장(`BossArena.BossSpawned` · ResetGate 가 false 로 되돌린다)해야만 다시 문다.
|
||||
var hud = WLHudLayoutSettings.Instance;
|
||||
if (hud != null && hud.bossBarRebindRequiresArenaSpawn && !WL.Combat.Boss.BossArena.BossSpawned)
|
||||
return "런시작리셋: 숨김만(이번 런 보스 미등장 · BossArena.BossSpawned=false)";
|
||||
|
||||
return "런시작리셋 → " + BindExistingBoss();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
PollBoss(false);
|
||||
if (_hideAt >= 0f && Time.unscaledTime >= _hideAt) HideNow();
|
||||
}
|
||||
|
||||
// ── WL-813c2 (Q6 D-26) — 바가 실제 HP 를 안 따라간다 ────────────────────
|
||||
// 실측 원인(같은 파일):
|
||||
// ① `Bind()`(:311~) 가 `_maxHp` 를 **물릴 때 한 번만** 읽는다. `MyValue.Get_Stat`(`MyValue.cs:993`)
|
||||
// 은 키가 아직 없으면 **0** 을 돌려주므로, 몹 스탯이 채워지기(`MyValue.cs:371`) 전에 물면
|
||||
// `_maxHp = DebugFallbackMaxHp(100)` 로 굳는다 → `Clamp01(869/100) = 1.000` = **영원히 만피**.
|
||||
// ② 갱신 경로가 `OnHitConfirmed`(:205~) 하나뿐이라 `Actor.Get_Damage` 를 안 타는 HP 변화
|
||||
// (리쉬 회복 `BossArena.cs:507·512` · 리젠)는 바에 전혀 안 보인다.
|
||||
// ③ 늦은 구독 씬 스캔(:118~)이 **첫 번째** 살아 있는 보스를 물어, 실제로 싸우는 아레나 보스가
|
||||
// 아닐 수 있다(그러면 HitConfirmed victim 이 한 번도 일치하지 않아 갱신 0).
|
||||
// → 물고 있는 보스의 현재 HP/MaxHP 를 **직접 폴링**해 fill 을 갱신한다. 값·주기·스위치 전부 SO.
|
||||
// 🔴 GC 0 — 이 경로는 문자열을 만들지 않는다(진단은 전부 카운터).
|
||||
|
||||
/// <summary>폴링 실행 횟수(진단).</summary>
|
||||
public int PollCount { get; private set; }
|
||||
/// <summary>폴링이 아레나 보스로 갈아탄 횟수(진단 · 원인 ③).</summary>
|
||||
public int PollRebindCount { get; private set; }
|
||||
/// <summary>폴링이 굳어 있던 `_maxHp` 를 고친 횟수(진단 · 원인 ①).</summary>
|
||||
public int MaxHpFixCount { get; private set; }
|
||||
/// <summary>폴링이 마지막으로 읽은 현재 HP / 최대 HP(진단).</summary>
|
||||
public float PolledHp { get { return _curHp; } }
|
||||
public float PolledMaxHp { get { return _maxHp; } }
|
||||
|
||||
private float _nextPoll;
|
||||
|
||||
/// <summary>프로브 전용 — 합성(actor=null) 모드에서 폴링이 읽을 현재 HP(음수 = 쓰지 않음).</summary>
|
||||
public static float DebugFakeHp = -1f;
|
||||
/// <summary>프로브 전용 — 합성 모드에서 폴링이 읽을 최대 HP(0 이하 = 쓰지 않음).</summary>
|
||||
public static float DebugFakeMaxHp = -1f;
|
||||
|
||||
/// <summary>
|
||||
/// 물고 있는 보스의 HP 를 직접 읽어 fill 을 갱신한다. <paramref name="force"/> = 주기 무시(에디트 모드 프로브).
|
||||
/// 돌려주는 값 = 이번 호출에서 실제로 갱신했는가.
|
||||
/// </summary>
|
||||
public bool PollBoss(bool force)
|
||||
{
|
||||
var s = WLHudLayoutSettings.Instance;
|
||||
if (s == null || !s.bossBarEnabled || !s.bossBarPollEnabled) return false;
|
||||
|
||||
float now = Time.unscaledTime;
|
||||
if (!force && s.bossBarPollHz > 0f)
|
||||
{
|
||||
if (now < _nextPoll) return false;
|
||||
_nextPoll = now + 1f / s.bossBarPollHz;
|
||||
}
|
||||
|
||||
// ③ 아레나가 잡은 보스와 물고 있는 보스가 다르면 그쪽으로 갈아탄다.
|
||||
// 🔴 **물고 있지 않을 때**(= 런 시작 리셋으로 숨긴 상태)는 D-23 을 되살리지 않게
|
||||
// 이번 런의 보스가 실제로 등장했을 때만 새로 문다(ResetForNewRun 과 같은 조건).
|
||||
if (s.bossBarPollFollowsArena)
|
||||
{
|
||||
var arena = WL.Combat.Boss.BossArena.Boss;
|
||||
if (IsAliveBoss(arena) && !ReferenceEquals(arena, _boss) &&
|
||||
(_bound || !s.bossBarRebindRequiresArenaSpawn || WL.Combat.Boss.BossArena.BossSpawned))
|
||||
{
|
||||
PollRebindCount++;
|
||||
Bind(arena, null, DebugFallbackMaxHp); // Bind 안에서 MaxHP/HP 를 다시 읽는다
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!_bound) return false;
|
||||
if (ReferenceEquals(_boss, null)) return PollSynthetic(); // 합성 검증 모드(actor 없음)
|
||||
if (_boss == null) { HideNow(); return false; } // 파괴된 액터(Unity null)
|
||||
|
||||
PollCount++;
|
||||
|
||||
// ① MaxHP 를 매번 다시 읽는다 — 늦게 채워져도 따라간다.
|
||||
float max = ReadMaxHp(_boss);
|
||||
if (max > 0f && max != _maxHp) { _maxHp = max; MaxHpFixCount++; }
|
||||
// ② 현재 HP 도 매번 다시 읽는다 — 피해·회복·리젠 전부 반영된다.
|
||||
float cur = ReadHp(_boss);
|
||||
if (cur >= 0f) _curHp = Mathf.Max(0f, cur);
|
||||
|
||||
SetRatio(_maxHp > 0f ? _curHp / _maxHp : 0f);
|
||||
|
||||
if (s.bossBarPollHidesOnDeath && _boss.IsDead())
|
||||
{
|
||||
SetRatio(0f);
|
||||
if (_hideAt < 0f) _hideAt = now + s.bossBarHideDelaySec;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>합성 모드 폴링 — 프로브가 넣은 가짜 HP 로 같은 산식을 태운다(게임 경로는 −1 이라 무동작).</summary>
|
||||
private bool PollSynthetic()
|
||||
{
|
||||
if (DebugFakeHp < 0f && DebugFakeMaxHp <= 0f) return false;
|
||||
PollCount++;
|
||||
if (DebugFakeMaxHp > 0f && DebugFakeMaxHp != _maxHp) { _maxHp = DebugFakeMaxHp; MaxHpFixCount++; }
|
||||
if (DebugFakeHp >= 0f) _curHp = Mathf.Max(0f, DebugFakeHp);
|
||||
SetRatio(_maxHp > 0f ? _curHp / _maxHp : 0f);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>프로브 전용 — 합성 HP 주입을 지운다(게임 경로 오염 0).</summary>
|
||||
public static void ClearFakeHp() { DebugFakeHp = -1f; DebugFakeMaxHp = -1f; }
|
||||
|
||||
// ── 이벤트 구독 (811b CombatEvents) ───────────────────────────────────
|
||||
private void OnSpawned(in SpawnedEvent e)
|
||||
{
|
||||
|
|
@ -571,6 +681,14 @@ namespace WL.UI
|
|||
" seen(Spawned/Hit/Killed)=" + SpawnedSeen + "/" + HitSeen + "/" + KilledSeen +
|
||||
" 늦은구독=" + LateBound + "(스캔 " + LateScanCount + " · BossArena.Boss=" +
|
||||
(WL.Combat.Boss.BossArena.Boss != null ? WL.Combat.Boss.BossArena.Boss.name : "없음") + ")");
|
||||
sb.AppendLine(" [813c2] 폴링 " + PollCount + "회 · MaxHP 보정 " + MaxHpFixCount + "회 · 아레나 재바인드 " +
|
||||
PollRebindCount + "회 · hp=" + PolledHp.ToString("F0") + "/" + PolledMaxHp.ToString("F0") +
|
||||
" · BossArena.BossSpawned=" + WL.Combat.Boss.BossArena.BossSpawned +
|
||||
" · 설정 " + (s == null ? "(에셋 없음)" :
|
||||
"poll=" + s.bossBarPollEnabled + "@" + s.bossBarPollHz.ToString("F0") + "Hz" +
|
||||
" 아레나추종=" + s.bossBarPollFollowsArena +
|
||||
" 사망숨김=" + s.bossBarPollHidesOnDeath +
|
||||
" 재바인드조건=" + s.bossBarRebindRequiresArenaSpawn));
|
||||
sb.AppendLine(" [813q2] 런 리셋 " + RunResetCount + "회 · 런 보스 HP=" +
|
||||
(RunBossHpRatio >= 0f ? (RunBossHpRatio * 100f).ToString("F1") + "%" : "없음") +
|
||||
" (본적=" + RunBossSeen + ") · RunStarted 구독=" + RunEvents.RunStarted.Count);
|
||||
|
|
|
|||
|
|
@ -214,13 +214,92 @@ namespace WL.UI
|
|||
/// <summary>813f(후속) · 프로브 공용 입력 — 아이템 1건 획득을 줄로 띄운다.</summary>
|
||||
public string Push(int itemId, int count)
|
||||
{
|
||||
var s = WLCombatTextSettings.Instance;
|
||||
if (s == null || !WLCombatTextSettings.Enabled || !s.lootToastEnabled) return "설정 off — 표시 0";
|
||||
if (!s.lootToastIncludeGold && itemId == GoldItemId) return "골드 제외(설정) — 표시 0";
|
||||
int r = PushCore(itemId, count);
|
||||
switch (r)
|
||||
{
|
||||
case kOff: return "설정 off — 표시 0";
|
||||
case kGold: return "골드 제외(설정) — 표시 0";
|
||||
case kGrade: return "등급 하한 미만 — 표시 0";
|
||||
case kMerged: return "merge item=" + itemId + " count=" +
|
||||
(_lastPushLine >= 0 && _lastPushLine < _lines.Count ? _lines[_lastPushLine].count : count) +
|
||||
" line=" + _lastPushLine;
|
||||
case kQueued: return "queue item=" + itemId + " count=" + count + " 대기=" + _qCount + "(오래된 줄 페이드)";
|
||||
case kQueueDropped: return "queue full item=" + itemId + " — 버림(대기 " + _qCount + ")";
|
||||
default: return "push item=" + itemId + " count=" + count + " line=" + _lastPushLine;
|
||||
}
|
||||
}
|
||||
|
||||
int grade = 1;
|
||||
string name = itemId.ToString();
|
||||
// 표가 아직 안 올라온 시점(에디트 모드·로딩 중)에도 죽지 않게 감싼다 — 이름/등급이 없으면 ID·등급 1 로 뜬다.
|
||||
// ── WL-813c2 (Q6 D-31) — 큐 · 병합 · 넘침 페이드 ────────────────────────
|
||||
// 실측: 화면 동시 줄 수는 813g 때부터 `lootToastLines`(3)로 막혀 있었다. Q6 의 「창 안 6줄」은
|
||||
// 프로브가 **표시 시간 창 안의 Picked 이벤트 수**를 센 값이다(`WL813_ScoreProbe.cs:437~443`).
|
||||
// 실물 결함은 넘칠 때 오래된 줄이 **페이드 없이 즉시 다른 내용으로 교체**돼 깜빡이는 것이다.
|
||||
// → ① 같은 아이템은 보이는 동안 계속 수량 병합("금화 ×8") ② 넘치면 오래된 줄을 즉시 페이드시키고
|
||||
// 새 줄은 **고정 크기 링 큐**에서 자리를 기다린다(동시 줄 수 상한 유지 · 할당 0).
|
||||
// C8 롤백 = `lootToastOverflowFadeSec = 0` → 813g 의 즉시 한 칸 당김 그대로.
|
||||
|
||||
private const int kOff = 0, kGold = 1, kGrade = 2, kMerged = 3, kShown = 4, kQueued = 5, kQueueDropped = 6;
|
||||
private int _lastPushLine = -1;
|
||||
|
||||
private struct Pending { public int itemId, count; }
|
||||
private Pending[] _queue = new Pending[0];
|
||||
private int _qHead, _qCount;
|
||||
|
||||
/// <summary>지금 자리를 기다리는 건수(진단).</summary>
|
||||
public int QueuedCount { get { return _qCount; } }
|
||||
/// <summary>큐가 꽉 차 버린 건수(진단).</summary>
|
||||
public int QueueDropCount { get; private set; }
|
||||
/// <summary>넘침으로 즉시 페이드시킨 줄 수(진단).</summary>
|
||||
public int OverflowFadeCount { get; private set; }
|
||||
|
||||
/// <summary>문자열을 만들지 않는 입력 본체(GC 0). 돌려주는 값은 위 k* 코드.</summary>
|
||||
private int PushCore(int itemId, int count)
|
||||
{
|
||||
_lastPushLine = -1;
|
||||
var s = WLCombatTextSettings.Instance;
|
||||
if (s == null || !WLCombatTextSettings.Enabled || !s.lootToastEnabled) return kOff;
|
||||
if (!s.lootToastIncludeGold && itemId == GoldItemId) return kGold;
|
||||
|
||||
int grade; string name;
|
||||
ResolveItem(itemId, out grade, out name);
|
||||
if (grade < s.lootToastMinGrade) return kGrade;
|
||||
|
||||
BuildIfNeeded();
|
||||
int n = LineCap(s);
|
||||
|
||||
// ① 같은 아이템 병합 — 보이는 동안이면 창과 무관하게(설정) 수량만 올린다.
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
var l = _lines[i];
|
||||
if (!l.used || l.itemId != itemId) continue;
|
||||
if (!s.lootToastMergeWhileVisible && l.elapsed > s.lootToastMergeSeconds) continue;
|
||||
l.count += count;
|
||||
l.life = l.lifeMax; l.elapsed = 0f; l.group.alpha = 1f;
|
||||
Render(l, name, grade, s);
|
||||
MergeCount++; _lastPushLine = i;
|
||||
return kMerged;
|
||||
}
|
||||
|
||||
// ② 빈 줄이 있으면 바로 띄운다(지연 0).
|
||||
int slot = FreeSlot(s);
|
||||
if (slot >= 0) { Occupy(slot, itemId, count, name, grade, s); return kShown; }
|
||||
|
||||
// ③ 넘침 — 오래된 줄을 즉시 페이드시키고 새 줄은 큐에서 기다린다.
|
||||
if (s.lootToastOverflowFadeSec > 0f)
|
||||
{
|
||||
FadeOldest(s);
|
||||
return Enqueue(s, itemId, count) ? kQueued : kQueueDropped;
|
||||
}
|
||||
|
||||
// ④ C8 롤백 경로(페이드 0) — 813g 그대로 한 칸씩 당기고 마지막 자리를 쓴다.
|
||||
slot = ShiftUp(s);
|
||||
Occupy(slot, itemId, count, name, grade, s);
|
||||
return kShown;
|
||||
}
|
||||
|
||||
/// <summary>표가 아직 안 올라온 시점(에디트 모드·로딩 중)에도 죽지 않게 감싼다 — 없으면 ID·등급 1.</summary>
|
||||
private static void ResolveItem(int itemId, out int grade, out string name)
|
||||
{
|
||||
grade = 1; name = null;
|
||||
try
|
||||
{
|
||||
var data = table_itemlist.Ins != null ? table_itemlist.Ins.Get_Data(itemId) : null;
|
||||
|
|
@ -232,41 +311,113 @@ namespace WL.UI
|
|||
}
|
||||
}
|
||||
catch { /* 표 미로드 · ID 없음 — 기본값으로 표시 */ }
|
||||
if (grade < s.lootToastMinGrade) return "등급 " + grade + " < 최소 " + s.lootToastMinGrade + " — 표시 0";
|
||||
if (name == null) name = itemId.ToString();
|
||||
}
|
||||
|
||||
BuildIfNeeded();
|
||||
/// <summary>지금 쓸 수 있는 줄 수 = min(만들어 둔 줄, 설정 상한).</summary>
|
||||
private int LineCap(WLCombatTextSettings s)
|
||||
{
|
||||
return Mathf.Min(_lines.Count, Mathf.Max(1, s.lootToastLines));
|
||||
}
|
||||
|
||||
// 같은 아이템이 병합 창 안이면 기존 줄의 수량만 올린다(줄 폭주 방지)
|
||||
for (int i = 0; i < _lines.Count; i++)
|
||||
{
|
||||
var l = _lines[i];
|
||||
if (!l.used || l.itemId != itemId) continue;
|
||||
if (l.elapsed > s.lootToastMergeSeconds) continue;
|
||||
l.count += count;
|
||||
l.life = l.lifeMax; l.elapsed = 0f;
|
||||
Render(l, name, grade, s);
|
||||
MergeCount++;
|
||||
return "merge item=" + itemId + " count=" + l.count + " line=" + i;
|
||||
}
|
||||
private int FreeSlot(WLCombatTextSettings s)
|
||||
{
|
||||
int n = LineCap(s);
|
||||
for (int i = 0; i < n; i++) if (!_lines[i].used) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int slot = TakeSlot(s);
|
||||
private void Occupy(int slot, int itemId, int count, string name, int grade, WLCombatTextSettings s)
|
||||
{
|
||||
var line = _lines[slot];
|
||||
line.used = true; line.itemId = itemId; line.count = count;
|
||||
line.lifeMax = line.life = Mathf.Max(0.01f, s.lootToastSeconds);
|
||||
line.elapsed = 0f;
|
||||
line.group.alpha = 1f;
|
||||
Render(line, name, grade, s);
|
||||
PushCount++;
|
||||
return "push item=" + itemId + " \"" + name + "\" grade=" + grade + " count=" + count + " line=" + slot;
|
||||
PushCount++; _lastPushLine = slot;
|
||||
}
|
||||
|
||||
/// <summary>빈 줄을 찾는다. 없으면 가장 오래된 줄을 밀어내고(위로 한 칸씩 당김) 마지막 자리를 준다.</summary>
|
||||
private int TakeSlot(WLCombatTextSettings s)
|
||||
/// <summary>남은 수명이 가장 짧은 줄(= 가장 오래된 줄)을 페이드 시간으로 줄인다 — 「넘치면 오래된 줄 즉시 페이드」.</summary>
|
||||
private void FadeOldest(WLCombatTextSettings s)
|
||||
{
|
||||
int n = Mathf.Min(_lines.Count, Mathf.Max(1, s.lootToastLines));
|
||||
for (int i = 0; i < n; i++) if (!_lines[i].used) return i;
|
||||
int n = LineCap(s), pick = -1; float least = float.MaxValue;
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
var l = _lines[i];
|
||||
if (!l.used) continue;
|
||||
if (l.life < least) { least = l.life; pick = i; }
|
||||
}
|
||||
if (pick < 0) return;
|
||||
float f = Mathf.Max(0.01f, s.lootToastOverflowFadeSec);
|
||||
if (_lines[pick].life <= f) return; // 이미 그만큼만 남았으면 건드리지 않는다
|
||||
_lines[pick].life = f;
|
||||
_lines[pick].lifeMax = f; // Passed = 1 − life/lifeMax → 곧바로 페이드 구간
|
||||
OverflowFadeCount++; DropCount++;
|
||||
}
|
||||
|
||||
// 전부 차 있음 — 0번(가장 오래된 줄)을 버리고 한 칸씩 올린다(내용만 옮기고 RectTransform 은 그대로).
|
||||
private void EnsureQueue(WLCombatTextSettings s)
|
||||
{
|
||||
int want = Mathf.Max(0, s.lootToastQueueMax);
|
||||
if (_queue.Length == want) return;
|
||||
_queue = new Pending[want]; // 설정을 바꿀 때만 1회 — 폴링·프레임 경로에서는 할당 0
|
||||
_qHead = 0; _qCount = 0;
|
||||
}
|
||||
|
||||
private bool Enqueue(WLCombatTextSettings s, int itemId, int count)
|
||||
{
|
||||
EnsureQueue(s);
|
||||
if (_queue.Length == 0) return false;
|
||||
|
||||
for (int k = 0; k < _qCount; k++) // 큐 안에서도 같은 아이템은 수량만 더한다
|
||||
{
|
||||
int idx = (_qHead + k) % _queue.Length;
|
||||
if (_queue[idx].itemId != itemId) continue;
|
||||
_queue[idx].count += count; MergeCount++;
|
||||
return true;
|
||||
}
|
||||
if (_qCount >= _queue.Length) // 꽉 차면 가장 오래 기다린 항목을 버린다
|
||||
{
|
||||
_qHead = (_qHead + 1) % _queue.Length; _qCount--;
|
||||
QueueDropCount++;
|
||||
}
|
||||
int tail = (_qHead + _qCount) % _queue.Length;
|
||||
_queue[tail].itemId = itemId; _queue[tail].count = count;
|
||||
_qCount++;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>자리가 나면 큐에서 꺼내 띄운다(매 프레임 · 문자열 0).</summary>
|
||||
private void FlushQueue(WLCombatTextSettings s)
|
||||
{
|
||||
if (_qCount <= 0 || _queue.Length == 0) return;
|
||||
int guard = _queue.Length;
|
||||
while (_qCount > 0 && guard-- > 0)
|
||||
{
|
||||
int slot = FreeSlot(s);
|
||||
if (slot < 0) return;
|
||||
var p = _queue[_qHead];
|
||||
_qHead = (_qHead + 1) % _queue.Length; _qCount--;
|
||||
int grade; string name;
|
||||
ResolveItem(p.itemId, out grade, out name);
|
||||
Occupy(slot, p.itemId, p.count, name, grade, s);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>프로브 전용 — 자리가 난 만큼 큐를 밀어 넣고 띄운 건수를 돌려준다(런타임은 Update 가 매 프레임 부른다).</summary>
|
||||
public int FlushQueueNow()
|
||||
{
|
||||
var s = WLCombatTextSettings.Instance;
|
||||
if (s == null) return 0;
|
||||
int before = _qCount;
|
||||
FlushQueue(s);
|
||||
return before - _qCount;
|
||||
}
|
||||
|
||||
/// <summary>C8 롤백 경로 — 0번을 버리고 한 칸씩 올린다(내용만 옮기고 RectTransform 은 그대로).</summary>
|
||||
private int ShiftUp(WLCombatTextSettings s)
|
||||
{
|
||||
int n = LineCap(s);
|
||||
DropCount++;
|
||||
for (int i = 0; i < n - 1; i++)
|
||||
{
|
||||
|
|
@ -296,6 +447,7 @@ namespace WL.UI
|
|||
if (_lines[i].group != null) _lines[i].group.alpha = 0f;
|
||||
if (_lines[i].label != null) _lines[i].label.text = "";
|
||||
}
|
||||
_qHead = 0; _qCount = 0; // WL-813c2 — 대기 큐도 비운다
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────── 폴링 · 수명
|
||||
|
|
@ -309,12 +461,17 @@ namespace WL.UI
|
|||
if (s == null) return;
|
||||
|
||||
// ── 획득 집계 폴링(813f 이벤트가 오면 걷어낸다)
|
||||
// 🔴 WL-813c2 — 게임 경로는 **문자열을 만들지 않는 갈래**로 부른다(기존에는 폴링마다
|
||||
// `new StringBuilder()` + 진단 문자열이 초당 10회 났다 = GC). 진단용 문자열 갈래는 프로브 전용.
|
||||
if (_armed && Application.isPlaying && s.lootToastPollHz > 0f && Time.unscaledTime >= _nextPoll)
|
||||
{
|
||||
_nextPoll = Time.unscaledTime + 1f / Mathf.Max(0.1f, s.lootToastPollHz);
|
||||
PollStageDrop();
|
||||
PollStageDropCore(null);
|
||||
}
|
||||
|
||||
// ── WL-813c2(D-31) — 넘쳐서 기다리던 줄을 자리가 나는 대로 띄운다
|
||||
FlushQueue(s);
|
||||
|
||||
// ── 줄 수명
|
||||
float dt = Time.unscaledDeltaTime;
|
||||
float u = WLTextFxUtil.UnitsPerPx(this, s);
|
||||
|
|
@ -337,8 +494,11 @@ namespace WL.UI
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>StageDropData 의 아이템 누적 수량 델타 = 이번에 주운 것.</summary>
|
||||
public string PollStageDrop()
|
||||
/// <summary>StageDropData 의 아이템 누적 수량 델타 = 이번에 주운 것(진단 문자열 갈래 · 프로브 전용).</summary>
|
||||
public string PollStageDrop() { return PollStageDropCore(new StringBuilder()); }
|
||||
|
||||
/// <summary>폴링 본체. <paramref name="sb"/> 가 null 이면 문자열을 하나도 만들지 않는다(게임 경로 · GC 0).</summary>
|
||||
private string PollStageDropCore(StringBuilder sb)
|
||||
{
|
||||
if (!DropItemInfo.isIns || DropItemInfo.Ins == null) return "DropItemInfo 없음";
|
||||
var data = DropItemInfo.Ins.m_StageDropData;
|
||||
|
|
@ -346,8 +506,7 @@ namespace WL.UI
|
|||
var dic = data.Get_Item();
|
||||
if (dic == null) return "집계 없음";
|
||||
|
||||
var sb = new StringBuilder();
|
||||
foreach (var kv in dic)
|
||||
foreach (var kv in dic) // Dictionary 열거자는 구조체 = 할당 0
|
||||
{
|
||||
int id = kv.Key;
|
||||
int now = kv.Value;
|
||||
|
|
@ -356,18 +515,23 @@ namespace WL.UI
|
|||
if (now > last)
|
||||
{
|
||||
DropCountedItems++;
|
||||
sb.Append(Push(id, now - last)).Append(" | ");
|
||||
if (sb != null) sb.Append(Push(id, now - last)).Append(" | ");
|
||||
else PushCore(id, now - last);
|
||||
}
|
||||
_seen[id] = now;
|
||||
}
|
||||
return sb.Length > 0 ? sb.ToString() : "델타 0";
|
||||
return sb != null && sb.Length > 0 ? sb.ToString() : "델타 0";
|
||||
}
|
||||
|
||||
/// <summary>진단 — 폴링으로 잡아낸 획득 건수.</summary>
|
||||
public int DropCountedItems { get; private set; }
|
||||
|
||||
/// <summary>스테이지가 바뀌면 집계 기준을 초기화한다(StageDropData.Init 과 짝).</summary>
|
||||
public void ResetPollBaseline() { _seen.Clear(); _armed = false; DropCountedItems = 0; }
|
||||
public void ResetPollBaseline()
|
||||
{
|
||||
_seen.Clear(); _armed = false; DropCountedItems = 0;
|
||||
_qHead = 0; _qCount = 0; QueueDropCount = 0; OverflowFadeCount = 0; // WL-813c2
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────── 프로브(에디트 모드 · Play 0)
|
||||
|
||||
|
|
@ -389,6 +553,13 @@ namespace WL.UI
|
|||
sb.AppendLine("LootToast subscribed=" + _subscribed + " killedSeen=" + KilledSeen +
|
||||
" push=" + PushCount + " merge=" + MergeCount + " drop=" + DropCount +
|
||||
" visible=" + VisibleLines);
|
||||
var sq = WLCombatTextSettings.Instance;
|
||||
sb.AppendLine(" [813c2] 상한=" + (sq != null ? sq.lootToastLines : 0) +
|
||||
" 대기=" + _qCount + "/" + _queue.Length + " 큐버림=" + QueueDropCount +
|
||||
" 넘침페이드=" + OverflowFadeCount + "회 · 병합창=" +
|
||||
(sq != null ? sq.lootToastMergeSeconds.ToString("F2") : "-") + "s(보이는동안=" +
|
||||
(sq != null ? sq.lootToastMergeWhileVisible.ToString() : "-") + ") · 페이드=" +
|
||||
(sq != null ? sq.lootToastOverflowFadeSec.ToString("F2") : "-") + "s");
|
||||
for (int i = 0; i < _lines.Count; i++)
|
||||
{
|
||||
var l = _lines[i];
|
||||
|
|
|
|||
|
|
@ -61,6 +61,8 @@ namespace WL.UI
|
|||
private static Transform s_uiRoot;
|
||||
private static RectTransform s_root;
|
||||
private static CanvasGroup s_group;
|
||||
private static Canvas s_canvas; // WL-813c2(D-29) — 그리는 순서를 값으로 고정
|
||||
private static GraphicRaycaster s_raycaster; // overrideSorting 캔버스는 자기 레이캐스터가 있어야 눌린다
|
||||
private static Image s_dim;
|
||||
private static RectTransform s_box;
|
||||
private static Image s_boxBg;
|
||||
|
|
@ -87,6 +89,11 @@ namespace WL.UI
|
|||
private static bool s_shownWarn;
|
||||
private static bool s_usedOriginalUi;
|
||||
|
||||
// ── WL-813c2 (Q6 D-29) — 부활 팝업이 카드를 덮는 동안 타임아웃 정지 ────
|
||||
private static float s_pausedFor; // 부활 때문에 멈춰 있던 누적 시간(초)
|
||||
private static float s_lastTickNow = -1f;// 직전 틱 시각(정지 구간을 dt 로 적분한다)
|
||||
private static bool s_reviveHeld; // 지금 부활 때문에 멈춰 있는가
|
||||
|
||||
/// <summary>이번에 뽑힌 카드의 스킬 ID(0 = 빈 칸). 폴백 패널 경로에서만 채운다.</summary>
|
||||
private static readonly List<int> s_ids = new List<int>(8);
|
||||
private static readonly List<object> s_rows = new List<object>(8);
|
||||
|
|
@ -94,6 +101,8 @@ namespace WL.UI
|
|||
// ── 진단 (프로브가 읽는다) ────────────────────────────────────────────
|
||||
public static int ZoneClearedSeen, OpenCount, PickCount, TimeoutCount, SkipCount,
|
||||
GuideWaitCount, ReviveDeferCount, GiveUpCount, TimerWrites, PauseCalls;
|
||||
/// <summary>WL-813c2(D-29) — 열린 카드의 타이머를 부활 때문에 멈춘/재개한 횟수와 누적 정지 시간(진단).</summary>
|
||||
public static int ReviveHoldCount, ReviveResumeCount, ReviveHoldTicks;
|
||||
public static string LastLog = "";
|
||||
public static string LastSkipReason = "";
|
||||
|
||||
|
|
@ -106,6 +115,15 @@ namespace WL.UI
|
|||
public static bool TimerPaused { get { return s_timerPaused; } }
|
||||
public static bool WorldHeldByPickup { get { return s_worldHeld; } }
|
||||
public static bool ActorsStopped { get { return s_actorsStopped; } }
|
||||
/// <summary>WL-813c2(D-29) — 지금 부활 팝업 때문에 카드 타이머가 멈춰 있는가.</summary>
|
||||
public static bool TimeoutHeldByRevive { get { return s_reviveHeld; } }
|
||||
/// <summary>WL-813c2(D-29) — 이번 카드에서 부활 때문에 멈춘 누적 시간(초).</summary>
|
||||
public static float PausedSeconds { get { return s_pausedFor; } }
|
||||
/// <summary>WL-813c2(D-29) — 카드 전용 Canvas 의 sortingOrder(없으면 −1). 부활 팝업(60)보다 작아야 한다.</summary>
|
||||
public static int SortingOrder
|
||||
{
|
||||
get { return s_canvas != null && s_canvas.enabled && s_canvas.overrideSorting ? s_canvas.sortingOrder : -1; }
|
||||
}
|
||||
public static bool UsedOriginalUi { get { return s_usedOriginalUi; } }
|
||||
public static int CardCount { get { return s_ids.Count; } }
|
||||
public static int CardId(int i) { return i >= 0 && i < s_ids.Count ? s_ids[i] : 0; }
|
||||
|
|
@ -234,8 +252,26 @@ namespace WL.UI
|
|||
// 원본 노드 경로에서 원본이 스스로 닫았으면(클릭·원본 타임아웃) 정리한다.
|
||||
if (s_usedOriginalUi && !OriginalUiActive()) { Close("원본이 닫음"); return "원본이 닫음"; }
|
||||
|
||||
float left = s.pickTimeoutSeconds - (now - s_openAt);
|
||||
// ── WL-813c2 (Q6 D-29) — 부활 팝업이 카드를 덮는 동안 타임아웃 정지 ──────────────
|
||||
// 813o 의 `deferWhileRevive` 는 **열기 전(Pending)** 에서만 본다(TickPending). 카드가 **먼저 열린 뒤**
|
||||
// 죽으면 부활 팝업(sortingOrder 60)이 카드를 덮은 채 그 밑에서 타이머가 계속 흘러
|
||||
// 플레이어가 보지도 고르지도 못한 채 자동 선택된다(`R3_15` = 10초 → 2초 → 자동 선택).
|
||||
// → 열린 동안에도 부활 상태를 폴링해 **멈춘 시간만큼 마감을 뒤로 민다**(부활 뒤 남은 초부터 재개).
|
||||
// 🔴 GC 0 — 문자열은 상수만 돌려준다. 값·스위치는 전부 SO.
|
||||
float dt = s_lastTickNow >= 0f ? Mathf.Max(0f, now - s_lastTickNow) : 0f;
|
||||
s_lastTickNow = now;
|
||||
bool held = s.pauseTimeoutWhileRevive && ReviveBusy();
|
||||
if (held)
|
||||
{
|
||||
s_pausedFor += dt;
|
||||
ReviveHoldTicks++;
|
||||
if (!s_reviveHeld) { s_reviveHeld = true; ReviveHoldCount++; }
|
||||
}
|
||||
else if (s_reviveHeld) { s_reviveHeld = false; ReviveResumeCount++; }
|
||||
|
||||
float left = s.pickTimeoutSeconds - (now - s_openAt - s_pausedFor);
|
||||
WriteTimer(s, left);
|
||||
if (held) return "부활 우선 — 타이머 정지";
|
||||
if (left > 0f) return "열림";
|
||||
|
||||
TimeoutCount++;
|
||||
|
|
@ -270,6 +306,7 @@ namespace WL.UI
|
|||
s_phase = ZonePickupPhase.Open;
|
||||
s_openAt = now;
|
||||
s_shownSec = int.MinValue;
|
||||
s_pausedFor = 0f; s_lastTickNow = now; s_reviveHeld = false; // WL-813c2(D-29)
|
||||
OpenCount++;
|
||||
PauseTimer(s, true);
|
||||
HoldWorld(s, true);
|
||||
|
|
@ -417,6 +454,7 @@ namespace WL.UI
|
|||
s_phase = ZonePickupPhase.Idle;
|
||||
s_pendingZone = -1;
|
||||
s_shownSec = int.MinValue;
|
||||
s_pausedFor = 0f; s_lastTickNow = -1f; s_reviveHeld = false; // WL-813c2(D-29)
|
||||
|
||||
Hide();
|
||||
StopActors(s, false);
|
||||
|
|
@ -609,6 +647,7 @@ namespace WL.UI
|
|||
|
||||
WLVignetteUtil.Stretch(s_root);
|
||||
s_root.localScale = Vector3.one;
|
||||
ApplySorting(s);
|
||||
if (s_dim != null) { s_dim.color = s.dimColor; s_dim.raycastTarget = s.dimBlocksTouch; }
|
||||
|
||||
s_box.anchorMin = s_box.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
|
|
@ -696,6 +735,37 @@ namespace WL.UI
|
|||
return "box" + s_box.sizeDelta + " 카드=" + n + " unitsPerPx=" + u.ToString("F4");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WL-813c2 (Q6 D-29) — 카드가 **부활 팝업 아래**로 오게 그리는 순서를 값으로 고정한다.
|
||||
/// 813z 부활 팝업은 전용 Canvas `overrideSorting · sortingOrder = 60`(`WLSurvivalSettings.reviveSortingOrder` 실측)
|
||||
/// 이므로 카드는 그보다 작은 값(기본 40)을 쓴다. 전용 Canvas 는 자기 `GraphicRaycaster` 가 있어야 눌린다.
|
||||
/// </summary>
|
||||
private static void ApplySorting(WLZonePickupSettings s)
|
||||
{
|
||||
if (s_root == null) return;
|
||||
if (s == null || !s.ownCanvasEnabled)
|
||||
{
|
||||
// C8 롤백 = 전용 캔버스를 렌더 경로에서 뺀다(형제 순서만 = 813o 동작).
|
||||
if (s_canvas != null) { s_canvas.overrideSorting = false; if (s_canvas.enabled) s_canvas.enabled = false; }
|
||||
if (s_raycaster != null && s_raycaster.enabled) s_raycaster.enabled = false;
|
||||
return;
|
||||
}
|
||||
if (s_canvas == null)
|
||||
{
|
||||
s_canvas = s_root.GetComponent<Canvas>();
|
||||
if (s_canvas == null) s_canvas = s_root.gameObject.AddComponent<Canvas>();
|
||||
}
|
||||
if (!s_canvas.enabled) s_canvas.enabled = true;
|
||||
if (!s_canvas.overrideSorting) s_canvas.overrideSorting = true;
|
||||
if (s_canvas.sortingOrder != s.cardSortingOrder) s_canvas.sortingOrder = s.cardSortingOrder;
|
||||
if (s_raycaster == null)
|
||||
{
|
||||
s_raycaster = s_root.GetComponent<GraphicRaycaster>();
|
||||
if (s_raycaster == null) s_raycaster = s_root.gameObject.AddComponent<GraphicRaycaster>();
|
||||
}
|
||||
if (!s_raycaster.enabled) s_raycaster.enabled = true;
|
||||
}
|
||||
|
||||
private static void TopRow(RectTransform rt, WLZonePickupSettings s, float u, float top, float h)
|
||||
{
|
||||
rt.anchorMin = new Vector2(0f, 1f); rt.anchorMax = new Vector2(1f, 1f);
|
||||
|
|
@ -724,6 +794,7 @@ namespace WL.UI
|
|||
Close("리셋");
|
||||
ZoneClearedSeen = OpenCount = PickCount = TimeoutCount = SkipCount = 0;
|
||||
GuideWaitCount = ReviveDeferCount = GiveUpCount = TimerWrites = PauseCalls = 0;
|
||||
ReviveHoldCount = ReviveResumeCount = ReviveHoldTicks = 0;
|
||||
s_picksThisRun = 0;
|
||||
LastLog = ""; LastSkipReason = "";
|
||||
return "리셋";
|
||||
|
|
@ -734,7 +805,7 @@ namespace WL.UI
|
|||
Subscribe(false);
|
||||
Close("teardown");
|
||||
if (s_root != null) Object.DestroyImmediate(s_root.gameObject);
|
||||
s_root = null; s_group = null; s_dim = null; s_box = null; s_boxBg = null;
|
||||
s_root = null; s_group = null; s_canvas = null; s_raycaster = null; s_dim = null; s_box = null; s_boxBg = null;
|
||||
s_title = null; s_timer = null; s_cardRoot = null; s_uiRoot = null;
|
||||
s_card = new RectTransform[0]; s_cardBg = new Image[0];
|
||||
s_cardName = new TextMeshProUGUI[0]; s_cardDesc = new TextMeshProUGUI[0];
|
||||
|
|
@ -748,7 +819,13 @@ namespace WL.UI
|
|||
" picks=" + s_picksThisRun + " cards=" + s_ids.Count +
|
||||
" sec=" + s_shownSec + " timerPaused=" + s_timerPaused +
|
||||
" worldHeld=" + s_worldHeld + " actorsStopped=" + s_actorsStopped +
|
||||
" orig=" + s_usedOriginalUi + " bound=" + Bound;
|
||||
" orig=" + s_usedOriginalUi + " bound=" + Bound +
|
||||
" · [813c2] 부활정지=" + s_reviveHeld + "(정지 " + ReviveHoldCount + "회 · 재개 " + ReviveResumeCount +
|
||||
"회 · 틱 " + ReviveHoldTicks + " · 누적 " + s_pausedFor.ToString("F2") + "s)" +
|
||||
" sortingOrder=" + SortingOrder + "(부활 " +
|
||||
(WL.Combat.Survival.WLSurvivalSettings.Instance != null
|
||||
? WL.Combat.Survival.WLSurvivalSettings.Instance.reviveSortingOrder.ToString() : "-") +
|
||||
") raycaster=" + (s_raycaster != null);
|
||||
}
|
||||
|
||||
/// <summary>프로브·QA 전용 — 카드 버튼을 실제 클릭 경로로 누른다.</summary>
|
||||
|
|
|
|||
|
|
@ -54,7 +54,10 @@ MonoBehaviour:
|
|||
lootToastEnabled: 1
|
||||
lootToastSeconds: 2
|
||||
lootToastLines: 3
|
||||
lootToastMergeSeconds: 1
|
||||
lootToastMergeSeconds: 0.5
|
||||
lootToastMergeWhileVisible: 1
|
||||
lootToastOverflowFadeSec: 0.15
|
||||
lootToastQueueMax: 6
|
||||
lootToastRightMarginPx: 40
|
||||
lootToastCenterOffsetPx: -120
|
||||
lootToastLineHeightPx: 68
|
||||
|
|
|
|||
|
|
@ -49,6 +49,11 @@ MonoBehaviour:
|
|||
bossBarTickColor: {r: 1, g: 1, b: 1, a: 0.85}
|
||||
bossBarNameColor: {r: 1, g: 0.92, b: 0.6, a: 1}
|
||||
bossBarBindExisting: 1
|
||||
bossBarPollEnabled: 1
|
||||
bossBarPollHz: 10
|
||||
bossBarPollFollowsArena: 1
|
||||
bossBarPollHidesOnDeath: 1
|
||||
bossBarRebindRequiresArenaSpawn: 1
|
||||
combatHideBottomMenu: 1
|
||||
bottomMenuPath: IngameUIs/WL_HUD/HUD_BottomMenu
|
||||
bottomMenuUseCanvasGroup: 1
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ MonoBehaviour:
|
|||
maxGuideDelaySeconds: 8
|
||||
deferWhileRevive: 1
|
||||
maxDeferSeconds: 45
|
||||
pauseTimeoutWhileRevive: 1
|
||||
ownCanvasEnabled: 1
|
||||
cardSortingOrder: 40
|
||||
cardCount: 3
|
||||
preferOriginalUi: 1
|
||||
pickTimeoutSeconds: 10
|
||||
|
|
|
|||
|
|
@ -174,8 +174,23 @@ namespace WL.UI
|
|||
[Tooltip("동시에 보이는 줄 수. 기준서 §B 요소 4 = 3줄. 넘치면 가장 오래된 줄부터 밀려난다.")]
|
||||
public int lootToastLines = 3;
|
||||
|
||||
[Tooltip("같은 아이템이 이 시간(초) 안에 또 들어오면 새 줄 대신 기존 줄의 수량을 올린다(줄 폭주 방지).")]
|
||||
public float lootToastMergeSeconds = 1.0f;
|
||||
[Tooltip("같은 아이템이 이 시간(초) 안에 또 들어오면 새 줄 대신 기존 줄의 수량을 올린다(줄 폭주 방지). 발주서 WL-813c2 §1-4 = 0.5 s.")]
|
||||
public float lootToastMergeSeconds = 0.5f;
|
||||
|
||||
// ── WL-813c2 (Q6 D-31) — 획득 21건에 줄이 밀린다 ───────────────────────
|
||||
// 실측: 화면 줄 수는 이미 `lootToastLines`(3)로 막혀 있고, Q6 의 「창 안 6줄」은
|
||||
// 프로브가 **표시 시간 창 안의 Picked 이벤트 수**를 센 값이다(`WL813_ScoreProbe.cs:437~443`).
|
||||
// 실물 결함은 넘칠 때 오래된 줄이 **페이드 없이 즉시 교체**돼 깜빡이는 것 → 큐 + 즉시 페이드.
|
||||
[Tooltip("같은 아이템이 **아직 보이는 줄**로 남아 있으면 병합 창(lootToastMergeSeconds)과 무관하게 수량만 올린다. " +
|
||||
"끄면 병합 창 안일 때만 병합한다(813g 동작).")]
|
||||
public bool lootToastMergeWhileVisible = true;
|
||||
|
||||
[Tooltip("줄이 꽉 찼을 때 가장 오래된 줄을 이 시간(초) 안에 **페이드아웃**시키고 새 줄은 큐에서 기다린다. " +
|
||||
"0 이면 813g 처럼 즉시 한 칸씩 당겨 교체한다(= C8 롤백).")]
|
||||
public float lootToastOverflowFadeSec = 0.15f;
|
||||
|
||||
[Tooltip("넘쳐서 대기시킬 수 있는 최대 건수(고정 크기 링 · 할당 0). 넘으면 가장 오래 기다린 항목을 버린다.")]
|
||||
public int lootToastQueueMax = 6;
|
||||
|
||||
[Tooltip("우측 여백(px · Safe Area 우측 기준).")]
|
||||
public float lootToastRightMarginPx = 40f;
|
||||
|
|
|
|||
|
|
@ -146,6 +146,28 @@ namespace WL.UI
|
|||
[Tooltip("바가 켜질 때 이미 살아 있는 보스가 있으면 즉시 물린다(813d 게이트 이전 스폰 = Q3 결함 D-3). 끄면 Spawned 이벤트만 본다.")]
|
||||
public bool bossBarBindExisting = true;
|
||||
|
||||
// ── WL-813c2 (Q6 D-26) — 바가 실제 HP 를 안 따라간다 ───────────────────
|
||||
// 이벤트(HitConfirmed)만 보면 ① 물릴 때 MaxHP 가 아직 0 이면(`MyValue.Get_Stat` 은 키가 없으면 0)
|
||||
// _maxHp 가 폴백값에 고정돼 Clamp01(869/100)=1.000 = 영원히 만피 ② 리쉬 회복(BossArena.ResetBoss)·
|
||||
// 리젠처럼 Actor.Get_Damage 를 안 타는 HP 변화가 바에 안 보인다. → 물고 있는 보스를 직접 폴링한다.
|
||||
[Header("보스 HP 바 — 실제 HP 추종 (WL-813c2 · Q6 D-26)")]
|
||||
[Tooltip("끄면 폴링 0 = 813q2 상태(이벤트 갱신만) 그대로 = C8 롤백.")]
|
||||
public bool bossBarPollEnabled = true;
|
||||
|
||||
[Tooltip("초당 폴링 횟수. 0 이면 매 프레임. 발주서 §1-1 = 매 프레임 또는 0.1 s.")]
|
||||
public float bossBarPollHz = 10f;
|
||||
|
||||
[Tooltip("폴링 때 813d BossArena.Boss(이번 아레나가 잡은 보스)가 물고 있는 것과 다르면 그쪽으로 갈아탄다 " +
|
||||
"— 늦은 구독 씬 스캔이 엉뚱한 필드 보스를 물었을 때의 복구 경로.")]
|
||||
public bool bossBarPollFollowsArena = true;
|
||||
|
||||
[Tooltip("물고 있는 보스가 죽으면 바를 0 으로 내리고 bossBarHideDelaySec 뒤 숨긴다(Killed 이벤트를 못 받아도).")]
|
||||
public bool bossBarPollHidesOnDeath = true;
|
||||
|
||||
[Tooltip("「다음 런」 리셋 뒤 재바인드를 **이번 런의 보스가 실제로 등장했을 때만** 한다(Q6 D-23 잔여). " +
|
||||
"🔴 BossArena.ResetGate() 는 s_boss 를 비우지 않아, 끄면 이전 런의 살아 있는 보스를 다시 물어 바가 남은 것처럼 보인다.")]
|
||||
public bool bossBarRebindRequiresArenaSpawn = true;
|
||||
|
||||
|
||||
[Header("전투 중 하단 메뉴 숨김 · 패드 원점 복귀 (WL-813y · Q3 결함 D-5)")]
|
||||
[Tooltip("끄면 하단 메뉴를 건드리지 않고 패드도 항상 idle 원점(아래 값)을 쓴다 — 813c+Lead 핫픽스 상태로 복귀(C8 롤백).")]
|
||||
|
|
|
|||
|
|
@ -98,6 +98,20 @@ namespace WL.UI
|
|||
[Tooltip("부활 때문에 미룰 수 있는 최대 시간(초 · unscaled). 넘으면 이번 픽업을 버린다(무한 대기 금지).")]
|
||||
public float maxDeferSeconds = 45f;
|
||||
|
||||
// ── WL-813c2 (Q6 D-29) — 카드가 **먼저 열린 뒤** 죽는 경우 ─────────────
|
||||
// `deferWhileRevive` 는 열기 전(Pending)만 본다. 카드가 열린 뒤 사망하면 부활 팝업이 카드를 덮고,
|
||||
// 그 밑에서 타이머가 계속 흘러 플레이어가 보지도 못한 채 자동 선택된다(`R3_15` · 10초 → 2초 → 자동).
|
||||
[Tooltip("카드가 열려 있는 동안 부활 팝업이 뜨면 자동 선택 타임아웃을 **멈춘다**(부활 뒤 남은 시간부터 재개). " +
|
||||
"끄면 813o 동작 그대로 = C8 롤백.")]
|
||||
public bool pauseTimeoutWhileRevive = true;
|
||||
|
||||
[Tooltip("카드 루트에 전용 Canvas(overrideSorting)를 붙여 그리는 순서를 값으로 고정한다. 끄면 형제 순서만 쓴다(813o 동작).")]
|
||||
public bool ownCanvasEnabled = true;
|
||||
|
||||
[Tooltip("카드 전용 Canvas 의 sortingOrder. 🔴 813z 부활 팝업(`WLSurvivalSettings.reviveSortingOrder` = 60 실측)보다 " +
|
||||
"**작아야** 부활이 카드 위로 온다(발주서 §1-3). 공용 팝업 프리팹 SortOrder_5 = 5.")]
|
||||
public int cardSortingOrder = 40;
|
||||
|
||||
// ── 카드 ──────────────────────────────────────────────────────────────
|
||||
[Header("카드 — 원본 table_selectskill / InGameInfo.Add_SelectSkill")]
|
||||
[Tooltip("한 번에 보여 줄 카드 수(기획안 F-3 = 3장 중 1장).")]
|
||||
|
|
|
|||
Loading…
Reference in New Issue