[WL-813q2] 결과 화면 핫픽스: 부활 겹침 · 다음 런 리셋 · 행 보강 (#813)
This commit is contained in:
parent
7494047e50
commit
60b74c1221
|
|
@ -0,0 +1,177 @@
|
|||
=== WL-813q2 ① D-18 결과 화면 위 부활 팝업 겹침 (에디트 모드 · Play 0) ===
|
||||
근거: Q5 캡처 PD2_07 — 제목 "시간 종료"(= 결과가 **먼저** 떴다) 위에 부활 팝업이 겹쳐 4행이 가려졌다.
|
||||
실측 전제: 813z 부활 팝업 sortingOrder = 60 · 결과 화면 SO sortingOrder = 40(부활 중 20)
|
||||
|
||||
Bind: 연결 IngameUIs/WL_HUD · box size(1528.89, 1920.00) 행틀=10 unitsPerPx=1.7778 · sortingOrder=40
|
||||
PASS 결과 화면 노드 조립됨
|
||||
PASS 전용 Canvas sortingOrder = 40 (SO 40) < 부활 60
|
||||
|
||||
A. 캡처 순서 재현 — RunEnded → 결과 표시 → **그 뒤** 사망(런 밖)
|
||||
PASS RunEnded 로 결과 화면이 떴다(open=True 제목 "시간 종료")
|
||||
PASS 런 밖 상태(IsRunning=False)
|
||||
사망 직후: DeathFlow=Dying · 즉시부활 호출 1회 · Died — 런 밖 사망 → 팝업 생략 즉시 부활(True)
|
||||
PASS 런 밖 사망 → 팝업 생략 즉시 부활을 불렀다(skipRevivePopupAfterRunEnd=1)
|
||||
연출 끝: DeathFlow=None · ReviveRequested 1회(팝업이 1프레임 안에 닫힌다) · Revived=1
|
||||
PASS 부활 완료 — DeathFlow.State=None(부활 대기 0 = 팝업이 남지 않는다)
|
||||
PASS DeathFlow.ReviveCount=1
|
||||
PASS 결과 화면은 그대로 · 재정렬 없음(open=True lowered=False)
|
||||
|
||||
B. skipRevivePopupAfterRunEnd=0 (C8 롤백) — 팝업이 뜨는 경우 결과를 부활 **뒤로 재정렬**
|
||||
PASS 결과 화면 재표시(open=True)
|
||||
사망 후: Died — 런 중 사망(재정렬=True)
|
||||
PASS 즉시 부활 호출 0(스위치 off = 813q 동작)
|
||||
PASS 결과를 부활 뒤로 재정렬 sortingOrder 40 → 20 (부활 60 아래)
|
||||
PASS 형제 순서 2 → 0 (SetAsFirstSibling)
|
||||
PASS 부활 대기 상태(DeathFlow=AwaitingRevive)
|
||||
PASS 「부활 우선」 판정 ON — 이유 = DeathFlow.AwaitingRevive
|
||||
PASS 부활 완료 → 결과 화면 복귀 sortingOrder=40 형제=2
|
||||
|
||||
C. 왜 813q 판정이 못 잡았나 — RunEnded 시점에는 아무도 죽지 않아 Busy=false 였다
|
||||
PASS RunEnded 직전 실측: ReviveDialog.Busy=False · DeathFlow=None → 813q 의 미룸 조건은 어느 쪽으로도 걸리지 않는다
|
||||
|
||||
D. 반대 순서 — 사망(런 중) → RunEnded → 부활 뒤에 결과가 뜬다
|
||||
PASS 사망 연출 중(DeathFlow=Dying)
|
||||
PASS 「부활 우선」 ON — DeathFlow.Dying
|
||||
PASS 결과 화면 미룸(open=False pending=True defer=1)
|
||||
PASS 부활 뒤 결과 화면 표시(open=True shows=1)
|
||||
|
||||
[RunResultUI] 구독=True bound=True open=True pending=False · ended=1 shows=1 defer=1 next=0 close=0 abandon생략=0 버퍼증설=0 공용팝업=False
|
||||
제목 "시간 종료" · 행 10개 · 부활팝업Busy=False
|
||||
[813q2] 부활우선=False(없음) · DeathFlow=None 런=Ended(running=False) · died=1 request=1 revived=1 팝업생략부활=1 · 재정렬=False(내림 0/복귀 0) sortingOrder=40 sibling=2
|
||||
[813q2] 엘리트 런중=0 종료스냅=0 전체본것=0 · 보스HP(UI 실측)=없음 · 마지막 사망 로그 = ReviveRequested — 런 밖 사망 → 팝업 생략 즉시 부활(True)
|
||||
- TotalTime : "04:01"
|
||||
- BossClear : "미처치"
|
||||
- Kills : "29 (보스 0)"
|
||||
- EliteKills : "0"
|
||||
- MaxChain : "7"
|
||||
- MaxDamage : "8889"
|
||||
- Level : "11 → 12"
|
||||
- LootByGrade : "<color=#F2F5FA>5</color> <color=#cd835e>2</color>"
|
||||
- Gold : "777"
|
||||
- DeathsPotions : "1 / 2"
|
||||
복사본 outcome=Timeout runIndex=1 total=241.0s bossClear=-1.0s kills=29(보스 0) chain=7 dmg=1234.4/8888.5 lv=11→12 lootTotal=7 gold=777 deaths=1 potions=2
|
||||
복사 배열 zone(2): [0]=40.0/24 [1]=-1.0/11 · loot(10): 0 5 2 0 0 0 0 0 0 0
|
||||
본문 = 시간 종료 | 총 시간 : 04:01 | 보스 처치 : 미처치 | 처치 수 : 29 (보스 0) | 엘리트 처치 : 0 | 최대 연쇄 : 7 | 최대 피해 : 8889 | 레벨 : 11 → 12 | 획득 : <color=#F2F5FA>5</color> <color=#cd835e>2</color> | 골드 : 777 | 사망 / 물약 : 1 / 2 |
|
||||
설정 enabled=True result=True 공용팝업=False abandon표시=False 부활우선=True box=(860.00, 1080.00) 행수=10 합산피해=True · [813q2] 사망중미룸=True 런밖즉시부활=True 전용캔버스=True(40→20) 재정렬=True/True 엘리트UI집계=True 보스HP UI=True
|
||||
|
||||
|
||||
① 결과 = 20 PASS / 0 FAIL
|
||||
|
||||
→ E:\NerdNavis\WL_wt\gameplay2\Logs\WL813q2_overlap.txt
|
||||
|
||||
=== WL-813q2 ② 워치독 자동 부활 경로에서도 「부활 우선」이 걸리는가 (813z §1) ===
|
||||
워치독 SO 실측: reviveWatchdogSeconds=20.0s
|
||||
PASS 부활 대기(DeathFlow=AwaitingRevive)
|
||||
PASS 결과 미룸 — 이유 = DeathFlow.AwaitingRevive (ReviveDialog.Busy=False 이라 813q 판정만으로는 못 막는다)
|
||||
PASS 워치독 직전까지 계속 미룸(defer=2)
|
||||
워치독: WatchdogCount=1 대기 20.02s · auto 부활=1 · RunResultUI.Revived=1
|
||||
PASS 워치독 자동 부활 1회 · 상태 None
|
||||
PASS RunResultUI 가 Revived 를 받았다(워치독 경로 포함)
|
||||
PASS 워치독 부활 뒤 결과 화면 표시(shows=1)
|
||||
|
||||
C8 롤백 — resultDeferWhileDying=0 (813q 동작 복귀)
|
||||
PASS DeathFlow=AwaitingRevive 인데 부활우선=False (스위치 off = 813q 판정만)
|
||||
|
||||
② 결과 = 7 PASS / 0 FAIL
|
||||
|
||||
→ E:\NerdNavis\WL_wt\gameplay2\Logs\WL813q2_watchdog.txt
|
||||
|
||||
=== WL-813q2 ③ D-23 「다음 런」 뒤 타이머 색 · 보스 바 · 배너 리셋 ===
|
||||
RunHud Bind: 연결 IngameUIs/WL_HUD · pos(0.00, -483.56) size(995.56, 167.11) 칸=0 unitsPerPx=1.7778
|
||||
SO 색 실측: 평상 #F2F5FA · 경고 #FF4A47 (경고 구간 30s)
|
||||
PASS 런 1 시작 타이머 색 #F2F5FA = 평상색 · 표시 "04:00"
|
||||
PASS 남은 25 s → 경고색 #FF4A47 · 표시 "00:25"
|
||||
PASS 🔴 D-23 — 다음 런 시작 타이머 색 #F2F5FA = 평상색 · 표시 "04:00" (색 쓰기 3회)
|
||||
PASS 존 게이지 리셋 — 칸 2개 전부 fill 0
|
||||
|
||||
C8 롤백 — timerResetColorOnRunStart=0 (813q 동작 = D-23 재현)
|
||||
PASS 스위치 off → 이전 런의 경고색 #FF4A47 이 그대로 남는다 = D-23 원인 지점 확정
|
||||
PASS 스위치 on 복귀 → 평상색
|
||||
|
||||
보스 HP 바 — 이전 런의 바가 남는가
|
||||
initialized subscribed=True · layout size=(1564.44, 117.33) unitsPerPx=1.7778 ticks=2 · 늦은구독: 살아 있는 보스 없음(스캔 0)
|
||||
bound name=테스트 보스 maxHp=200 ratio=1.000
|
||||
PASS 바 표시 · ratio=1.000
|
||||
PASS 런 단위 보스 HP 실측값 = 37.0% (결과 화면이 읽는 값)
|
||||
RunStarted 뒤: 리셋 1회 · visible=False · 런 HP=-1.00 · 늦은구독 스캔=0
|
||||
PASS RunStarted 를 받아 리셋했다
|
||||
PASS 🔴 D-23 — 이전 런의 보스 바가 사라졌다(visible=False)
|
||||
PASS 런 단위 보스 HP 값도 비워졌다(-1.00)
|
||||
PASS C8 롤백 bossResetOnRunStart=0 → 813q 동작(바가 남는다 · visible=True)
|
||||
|
||||
보스 등장 배너 — 재트리거 가드가 다음 런에서 풀리는가
|
||||
initialized subscribed=True · layout banner pos(0.00, -533.33) size(1528.89, 266.67) unitsPerPx=1.7778 warnThickness=320.0 · 늦은구독: 살아 있는 보스 없음(스캔 0)
|
||||
shown name=테스트 보스 banner=1.20s warn=0.80s
|
||||
같은 런에서 재트리거: 재트리거 억제(guard 3.0s) name=테스트 보스
|
||||
PASS 같은 런에서는 가드가 막는다(shows=1)
|
||||
PASS 다음 런 시작 → 배너 숨김(alpha=0.00)
|
||||
다음 런에서 재트리거: shown name=테스트 보스 banner=1.20s warn=0.80s
|
||||
PASS 다음 런에서는 다시 뜬다 = 가드 해제됨(shows=2)
|
||||
|
||||
[RunHud] 구독=True bound=True visible=True · started=7 tick=2 zoneCleared=0 gate=0 boss=0 ended=1
|
||||
타이머 "04:00" (남은 240.0s · 경고=False · 쓰기 9회) · 단계 "존 1" (쓰기 9회)
|
||||
[813q2] 타이머 색 = F2F5FA (색 쓰기 8회 · 다음런리셋대기=False · 기본 F2F5FA / 경고 FF4A47)
|
||||
존 칸 2 (fill 쓰기 18회): [0]=0.00×1.00 [1]=0.00×1.00
|
||||
노드 pos(0.00, -483.56) size(995.56, 216.89) active=True 부모=WL_HUD SafeArea=False
|
||||
edge = LiberationSans SDF Material (WL813y Edge) shader=TextMeshPro/Mobile/Distance Field outlineW=0.280 outlineC=0D0805FF underlay=000000CC off(0.50,-0.50) soft=0.22 UNDERLAY_ON=True
|
||||
설정 enabled=True hud=True topMargin=272px width=560px timerPx=56 warn=30s pulse=1.35×0.3s
|
||||
|
||||
|
||||
③ 결과 = 15 PASS / 0 FAIL
|
||||
|
||||
→ E:\NerdNavis\WL_wt\gameplay2\Logs\WL813q2_reset.txt
|
||||
|
||||
=== WL-813q2 ④ 결과 화면 행 — 보스 미처치 문구 · 엘리트 처치 수 ===
|
||||
SO 행 배열 = 10개 · 엘리트 행 포함=True
|
||||
PASS 엘리트 처치 행이 SO 배열에 있다
|
||||
PASS 런 중 엘리트 처치 3건(일반 5 · 보스 1 은 세지 않는다)
|
||||
PASS 런 밖 엘리트 처치는 무시(3)
|
||||
보스 처치 행 = "미처치" · 엘리트 처치 행 = "3"
|
||||
PASS 보스 미처치(HP 모름) → "미처치"
|
||||
PASS 엘리트 처치 행 = "3" (RunEnded 순간 스냅샷)
|
||||
바 실측 HP 43.6% 일 때 보스 처치 행 = "미처치 (HP 남음 44 %)"
|
||||
PASS 보스 미처치 + HP 남음 44 % 표기 — "미처치 (HP 남음 44 %)"
|
||||
PASS 보스 처치 런 = "02:34" (mm:ss 그대로 · 813q 동작 불변)
|
||||
PASS C8 롤백 — 보스 행 "미처치" · 엘리트 행 "0"
|
||||
|
||||
[RunResultUI] 구독=True bound=True open=True pending=False · ended=1 shows=1 defer=0 next=0 close=0 abandon생략=0 버퍼증설=0 공용팝업=False
|
||||
제목 "시간 종료" · 행 10개 · 부활팝업Busy=False
|
||||
[813q2] 부활우선=False(없음) · DeathFlow=None 런=Ended(running=False) · died=0 request=0 revived=0 팝업생략부활=0 · 재정렬=False(내림 0/복귀 0) sortingOrder=40 sibling=2
|
||||
[813q2] 엘리트 런중=0 종료스냅=0 전체본것=0 · 보스HP(UI 실측)=43.6% · 마지막 사망 로그 =
|
||||
- TotalTime : "04:00"
|
||||
- BossClear : "미처치"
|
||||
- Kills : "29 (보스 0)"
|
||||
- EliteKills : "0"
|
||||
- MaxChain : "7"
|
||||
- MaxDamage : "8889"
|
||||
- Level : "11 → 12"
|
||||
- LootByGrade : "<color=#F2F5FA>5</color> <color=#cd835e>2</color>"
|
||||
- Gold : "777"
|
||||
- DeathsPotions : "1 / 2"
|
||||
복사본 outcome=Timeout runIndex=1 total=240.0s bossClear=-1.0s kills=29(보스 0) chain=7 dmg=1234.4/8888.5 lv=11→12 lootTotal=7 gold=777 deaths=1 potions=2
|
||||
복사 배열 zone(2): [0]=40.0/24 [1]=-1.0/11 · loot(10): 0 5 2 0 0 0 0 0 0 0
|
||||
본문 = 시간 종료 | 총 시간 : 04:00 | 보스 처치 : 미처치 | 처치 수 : 29 (보스 0) | 엘리트 처치 : 0 | 최대 연쇄 : 7 | 최대 피해 : 8889 | 레벨 : 11 → 12 | 획득 : <color=#F2F5FA>5</color> <color=#cd835e>2</color> | 골드 : 777 | 사망 / 물약 : 1 / 2 |
|
||||
설정 enabled=True result=True 공용팝업=False abandon표시=False 부활우선=True box=(860.00, 1080.00) 행수=10 합산피해=True · [813q2] 사망중미룸=True 런밖즉시부활=True 전용캔버스=True(40→20) 재정렬=True/True 엘리트UI집계=True 보스HP UI=True
|
||||
|
||||
|
||||
④ 결과 = 8 PASS / 0 FAIL
|
||||
|
||||
→ E:\NerdNavis\WL_wt\gameplay2\Logs\WL813q2_rows.txt
|
||||
|
||||
=== WL-813q2 ⑤ GC 0 · C8 ===
|
||||
PASS 같은 초 100틱 — 타이머 쓰기 0회 · 색 쓰기 0회
|
||||
PASS Δ GetTotalMemory = 0 B
|
||||
PASS Killed 100건(엘리트 50) Δ = 0 B · 집계=50
|
||||
PASS 재정렬 왕복 100회(내림 100 / 복귀 100) · Δ = 0 B(진단 문자열분)
|
||||
PASS 노드 재생성 0 — Canvas 1개 · GraphicRaycaster 1개
|
||||
|
||||
PASS C8 RuntimeDisabled — 결과 화면 표시 0(open=False)
|
||||
PASS C8 중 부활우선 판정도 무동작(없음)
|
||||
PASS C8 복구 — Enabled=True
|
||||
|
||||
⑤ 결과 = 8 PASS / 0 FAIL
|
||||
|
||||
→ E:\NerdNavis\WL_wt\gameplay2\Logs\WL813q2_gc.txt
|
||||
|
||||
======================================================
|
||||
WL-813q2 전체 = 58 PASS / 0 FAIL
|
||||
|
|
@ -0,0 +1,700 @@
|
|||
// WL-813q2 프로브 — 결과 화면 핫픽스 실측(에디트 모드 · Play 0 · #813).
|
||||
// run_script --file AgentScripts/WL813q2_Probe.cs --entry WL813q2_Probe.<Entry>
|
||||
// Overlap · Watchdog · Reset · Rows · Gc · All
|
||||
// 로그는 <worktree>/Logs/WL813q2_*.txt (Assets 밖 · 미추적).
|
||||
//
|
||||
// 🔴 에디트 모드에서 `Time.timeScale` 에 쓰지 않는다(ProjectSettings/TimeManager.asset 오염 · 811b-fix).
|
||||
// DeathFlow.HoldWorld 도 `Application.isPlaying` 가드가 있어 에디트 모드에선 대입하지 않는다.
|
||||
// 🔴 씬·프리팹을 저장하지 않는다. 만든 노드는 finally 에서 전부 DestroyImmediate.
|
||||
// 🔴 SO 값은 메모리에서만 바꾸고 finally 에서 원복한다(AssetDatabase.SaveAssets 호출 0).
|
||||
// 🔴 Gameplay 는 읽기·구독·공개 API 호출만 한다(파일 수정 0). `RunDirector.s_phase` 는 리플렉션 **읽기/쓰기**로
|
||||
// "런 중/런 밖"만 흉내 낸다(런 시작·종료 부작용을 피하려고 StartRun/EndRun 을 쓰지 않는다).
|
||||
|
||||
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 WL813q2_Probe
|
||||
{
|
||||
const BindingFlags NP = BindingFlags.Instance | BindingFlags.NonPublic;
|
||||
const BindingFlags SP = BindingFlags.Static | BindingFlags.NonPublic;
|
||||
|
||||
static int s_pass, s_fail;
|
||||
|
||||
// ─────────────────────────────────────────────────────────── 공통
|
||||
|
||||
static string LogDir
|
||||
{
|
||||
get
|
||||
{
|
||||
string d = Path.Combine(Directory.GetParent(Application.dataPath).FullName, "Logs");
|
||||
if (!Directory.Exists(d)) Directory.CreateDirectory(d);
|
||||
return d;
|
||||
}
|
||||
}
|
||||
|
||||
static string Write(string name, string body)
|
||||
{
|
||||
string p = Path.Combine(LogDir, name);
|
||||
File.WriteAllText(p, body, new UTF8Encoding(false));
|
||||
return p;
|
||||
}
|
||||
|
||||
static string Chk(StringBuilder sb, bool ok, string what)
|
||||
{
|
||||
if (ok) s_pass++; else s_fail++;
|
||||
sb.AppendLine((ok ? " PASS " : " **FAIL** ") + what);
|
||||
return what;
|
||||
}
|
||||
|
||||
/// <summary>세로 1080×1920 기준 합성 캔버스 + `IngameUIs/WL_HUD` 대역(SO 경로와 같게).</summary>
|
||||
static Transform BuildUiRoot(out GameObject root)
|
||||
{
|
||||
root = new GameObject("__WL813q2_Canvas", typeof(RectTransform));
|
||||
root.hideFlags = HideFlags.DontSave;
|
||||
var canvas = root.AddComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
var scaler = root.AddComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1920f, 1080f);
|
||||
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.Expand;
|
||||
root.AddComponent<GraphicRaycaster>();
|
||||
|
||||
var ing = new GameObject("IngameUIs", typeof(RectTransform)).GetComponent<RectTransform>();
|
||||
ing.gameObject.hideFlags = HideFlags.DontSave;
|
||||
ing.SetParent(root.transform, false);
|
||||
var hud = new GameObject("WL_HUD", typeof(RectTransform)).GetComponent<RectTransform>();
|
||||
hud.gameObject.hideFlags = HideFlags.DontSave;
|
||||
hud.SetParent(ing, false);
|
||||
hud.anchorMin = hud.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
hud.pivot = new Vector2(0.5f, 0.5f);
|
||||
hud.sizeDelta = new Vector2(1080f, 1920f);
|
||||
// 결과 패널이 형제 맨 뒤/맨 앞으로 움직이는 것을 볼 수 있게 미끼 형제 2개
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
var d = new GameObject("Decoy" + i, typeof(RectTransform)).GetComponent<RectTransform>();
|
||||
d.gameObject.hideFlags = HideFlags.DontSave;
|
||||
d.SetParent(hud, false);
|
||||
}
|
||||
return root.transform;
|
||||
}
|
||||
|
||||
/// <summary>813z 프로브와 같은 가짜 메인 PC(사망 상태).</summary>
|
||||
static PCActor MakePC()
|
||||
{
|
||||
var go = new GameObject("__WL813q2_PC");
|
||||
go.hideFlags = HideFlags.DontSave;
|
||||
var pc = go.AddComponent<PCActor>();
|
||||
pc.m_Role = eRole.PC; pc.m_SubRole = eSubRol.None;
|
||||
var stat = new ActorStatInfo(eRole.PC);
|
||||
stat.Set_Stat(eStat.MaxHP, 1000d); stat.Set_Stat(eStat.HP, 0d);
|
||||
typeof(Actor).GetField("m_Stat", NP).SetValue(pc, stat);
|
||||
typeof(Actor).GetField("m_Enemy", NP).SetValue(pc, false);
|
||||
typeof(Actor).GetField("DeadStatus", NP).SetValue(pc, true);
|
||||
return pc;
|
||||
}
|
||||
|
||||
static void SetPhase(RunPhase p)
|
||||
{
|
||||
typeof(RunDirector).GetField("s_phase", SP).SetValue(null, p);
|
||||
}
|
||||
|
||||
static RunPhase GetPhase()
|
||||
{
|
||||
return (RunPhase)typeof(RunDirector).GetField("s_phase", SP).GetValue(null);
|
||||
}
|
||||
|
||||
static void OnReq(in ReviveRequestEvent e) { s_reqSeen++; }
|
||||
static int s_reqSeen;
|
||||
|
||||
static void Kill(GameObject go) { if (go != null) UnityEngine.Object.DestroyImmediate(go); }
|
||||
|
||||
static RunResult FakeResult(RunOutcome outcome, float total, float bossClear, int kills, int bossKills)
|
||||
{
|
||||
return new RunResult
|
||||
{
|
||||
outcome = outcome, runIndex = 1, totalSec = total, runSeconds = 240f,
|
||||
zoneClearSec = new float[] { 40f, -1f }, zoneKills = new int[] { 24, 11 },
|
||||
zoneCount = 2, zonesCleared = 1,
|
||||
bossGateSec = -1f, bossStartSec = -1f, bossClearSec = bossClear,
|
||||
kills = kills, bossKills = bossKills, maxChain = 7,
|
||||
maxDamage = 1234.4, maxDamageBurst = 8888.5,
|
||||
levelFrom = 11, levelTo = 12,
|
||||
lootByGrade = new int[] { 0, 5, 2, 0, 0, 0, 0, 0, 0, 0 }, lootTotal = 7, gold = 777,
|
||||
deaths = 1, potionsUsed = 2, skillCasts = 9,
|
||||
time = Time.unscaledTime, frame = Time.frameCount,
|
||||
};
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────── ① D-18 결과 위 부활 겹침
|
||||
|
||||
public static string Overlap()
|
||||
{
|
||||
s_pass = s_fail = 0;
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("=== WL-813q2 ① D-18 결과 화면 위 부활 팝업 겹침 (에디트 모드 · Play 0) ===");
|
||||
|
||||
var ui = WLRunUiSettings.Instance;
|
||||
var sv = WLSurvivalSettings.Instance;
|
||||
if (ui == null || sv == null) { string b = sb.AppendLine("**FAIL** 설정 에셋 없음").ToString(); return b + "\n→ " + Write("WL813q2_overlap.txt", b); }
|
||||
|
||||
GameObject root = null; PCActor pc = null;
|
||||
var phase0 = GetPhase();
|
||||
int ov0 = WLSurvivalSettings.RuntimeOverride;
|
||||
var ovr0 = DeathFlow.ReviveOverride;
|
||||
bool skip0 = ui.skipRevivePopupAfterRunEnd, dying0 = ui.resultDeferWhileDying, lower0 = ui.resultLowerWhileRevive;
|
||||
|
||||
sb.AppendLine("근거: Q5 캡처 PD2_07 — 제목 \"시간 종료\"(= 결과가 **먼저** 떴다) 위에 부활 팝업이 겹쳐 4행이 가려졌다.");
|
||||
sb.AppendLine("실측 전제: 813z 부활 팝업 sortingOrder = " + sv.reviveSortingOrder +
|
||||
" · 결과 화면 SO sortingOrder = " + ui.resultSortingOrder + "(부활 중 " + ui.resultSortingOrderWhileRevive + ")");
|
||||
sb.AppendLine();
|
||||
|
||||
try
|
||||
{
|
||||
WLSurvivalSettings.RuntimeOverride = 1;
|
||||
DeathFlow.ReviveOverride = (p, r) => { };
|
||||
DeathFlow.ReviveRequested.Add(OnReq);
|
||||
s_reqSeen = 0;
|
||||
|
||||
var uiRoot = BuildUiRoot(out root);
|
||||
sb.AppendLine("Bind: " + RunResultUI.Bind(uiRoot));
|
||||
RunResultUI.Subscribe(true);
|
||||
RunResultUI.ResetState();
|
||||
DeathFlow.ResetDiagnostics();
|
||||
pc = MakePC();
|
||||
|
||||
Chk(sb, RunResultUI.Bound, "결과 화면 노드 조립됨");
|
||||
Chk(sb, RunResultUI.SortingOrder == ui.resultSortingOrder,
|
||||
"전용 Canvas sortingOrder = " + RunResultUI.SortingOrder + " (SO " + ui.resultSortingOrder +
|
||||
") < 부활 " + sv.reviveSortingOrder);
|
||||
|
||||
// ── A. 캡처와 같은 순서: RunEnded(결과 표시) → 그 뒤 사망 ────────
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("A. 캡처 순서 재현 — RunEnded → 결과 표시 → **그 뒤** 사망(런 밖)");
|
||||
SetPhase(RunPhase.Zones);
|
||||
var r = FakeResult(RunOutcome.Timeout, 241f, -1f, 29, 0);
|
||||
SetPhase(RunPhase.Ended); // EndRun 과 같은 상태(IsRunning=false)
|
||||
WLRunUiProbeHooks.RaiseRunEnded(in r);
|
||||
Chk(sb, RunResultUI.IsOpen, "RunEnded 로 결과 화면이 떴다(open=" + RunResultUI.IsOpen + " 제목 \"" + RunResultUI.TitleText + "\")");
|
||||
Chk(sb, !RunDirector.IsRunning, "런 밖 상태(IsRunning=" + RunDirector.IsRunning + ")");
|
||||
|
||||
float t = 1000f;
|
||||
DeathFlow.Tick(t);
|
||||
DeathFlow.OnPCDied(pc); // ← 결과가 떠 있는 상태에서 사망
|
||||
sb.AppendLine(" 사망 직후: DeathFlow=" + DeathFlow.State + " · 즉시부활 호출 " + RunResultUI.SkipPopupReviveCount +
|
||||
"회 · " + RunResultUI.LastDeathLog);
|
||||
Chk(sb, RunResultUI.SkipPopupReviveCount >= 1, "런 밖 사망 → 팝업 생략 즉시 부활을 불렀다(skipRevivePopupAfterRunEnd=1)");
|
||||
|
||||
int reqBefore = s_reqSeen;
|
||||
DeathFlow.Tick(t + sv.deathDisplaySeconds + 0.01f); // 연출 끝 → acceptEarly 로 바로 부활
|
||||
sb.AppendLine(" 연출 끝: DeathFlow=" + DeathFlow.State + " · ReviveRequested " + (s_reqSeen - reqBefore) +
|
||||
"회(팝업이 1프레임 안에 닫힌다) · Revived=" + RunResultUI.RevivedSeen);
|
||||
Chk(sb, DeathFlow.State == DeathState.None, "부활 완료 — DeathFlow.State=None(부활 대기 0 = 팝업이 남지 않는다)");
|
||||
Chk(sb, DeathFlow.ReviveCount >= 1, "DeathFlow.ReviveCount=" + DeathFlow.ReviveCount);
|
||||
Chk(sb, RunResultUI.IsOpen && !RunResultUI.Lowered,
|
||||
"결과 화면은 그대로 · 재정렬 없음(open=" + RunResultUI.IsOpen + " lowered=" + RunResultUI.Lowered + ")");
|
||||
|
||||
// ── B. 스위치를 끄면 813q 동작(팝업이 뜬다) → 재정렬로 막는다 ──
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("B. skipRevivePopupAfterRunEnd=0 (C8 롤백) — 팝업이 뜨는 경우 결과를 부활 **뒤로 재정렬**");
|
||||
ui.skipRevivePopupAfterRunEnd = false;
|
||||
DeathFlow.ResetDiagnostics();
|
||||
RunResultUI.ResetState();
|
||||
SetPhase(RunPhase.Ended);
|
||||
WLRunUiProbeHooks.RaiseRunEnded(in r);
|
||||
Chk(sb, RunResultUI.IsOpen, "결과 화면 재표시(open=" + RunResultUI.IsOpen + ")");
|
||||
int sib0 = RunResultUI.SiblingIndex;
|
||||
|
||||
t = 2000f; DeathFlow.Tick(t); DeathFlow.OnPCDied(pc);
|
||||
sb.AppendLine(" 사망 후: " + RunResultUI.LastDeathLog);
|
||||
Chk(sb, RunResultUI.SkipPopupReviveCount == 0, "즉시 부활 호출 0(스위치 off = 813q 동작)");
|
||||
Chk(sb, RunResultUI.Lowered && RunResultUI.SortingOrder == ui.resultSortingOrderWhileRevive,
|
||||
"결과를 부활 뒤로 재정렬 sortingOrder " + ui.resultSortingOrder + " → " + RunResultUI.SortingOrder +
|
||||
" (부활 " + sv.reviveSortingOrder + " 아래)");
|
||||
Chk(sb, RunResultUI.SiblingIndex == 0 && sib0 != 0,
|
||||
"형제 순서 " + sib0 + " → " + RunResultUI.SiblingIndex + " (SetAsFirstSibling)");
|
||||
|
||||
DeathFlow.Tick(t + sv.deathDisplaySeconds + 0.01f); // AwaitingRevive
|
||||
Chk(sb, DeathFlow.State == DeathState.AwaitingRevive, "부활 대기 상태(DeathFlow=" + DeathFlow.State + ")");
|
||||
Chk(sb, RunResultUI.ReviveBusy, "「부활 우선」 판정 ON — 이유 = " + RunResultUI.ReviveBusyReason);
|
||||
|
||||
DeathFlow.Revive();
|
||||
Chk(sb, !RunResultUI.Lowered && RunResultUI.SortingOrder == ui.resultSortingOrder,
|
||||
"부활 완료 → 결과 화면 복귀 sortingOrder=" + RunResultUI.SortingOrder + " 형제=" + RunResultUI.SiblingIndex);
|
||||
|
||||
// ── C. 813q 원래 판정(ReviveDialog.Busy)만으로는 못 잡는다는 증명 ──
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("C. 왜 813q 판정이 못 잡았나 — RunEnded 시점에는 아무도 죽지 않아 Busy=false 였다");
|
||||
DeathFlow.ResetDiagnostics();
|
||||
RunResultUI.ResetState();
|
||||
SetPhase(RunPhase.Ended);
|
||||
Chk(sb, !ReviveDialog.Busy && DeathFlow.State == DeathState.None,
|
||||
"RunEnded 직전 실측: ReviveDialog.Busy=" + ReviveDialog.Busy + " · DeathFlow=" + DeathFlow.State +
|
||||
" → 813q 의 미룸 조건은 어느 쪽으로도 걸리지 않는다");
|
||||
ui.skipRevivePopupAfterRunEnd = true;
|
||||
|
||||
// ── D. 반대 순서(사망 먼저 → RunEnded)는 813q2 에서도 미뤄진다 ──
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("D. 반대 순서 — 사망(런 중) → RunEnded → 부활 뒤에 결과가 뜬다");
|
||||
RunResultUI.ResetState();
|
||||
DeathFlow.ResetDiagnostics();
|
||||
SetPhase(RunPhase.Zones);
|
||||
t = 3000f; DeathFlow.Tick(t); DeathFlow.OnPCDied(pc);
|
||||
Chk(sb, DeathFlow.State == DeathState.Dying, "사망 연출 중(DeathFlow=" + DeathFlow.State + ")");
|
||||
Chk(sb, RunResultUI.ReviveBusy, "「부활 우선」 ON — " + RunResultUI.ReviveBusyReason);
|
||||
SetPhase(RunPhase.Ended);
|
||||
WLRunUiProbeHooks.RaiseRunEnded(in r);
|
||||
Chk(sb, !RunResultUI.IsOpen && RunResultUI.Pending,
|
||||
"결과 화면 미룸(open=" + RunResultUI.IsOpen + " pending=" + RunResultUI.Pending +
|
||||
" defer=" + RunResultUI.DeferredCount + ")");
|
||||
DeathFlow.Tick(t + sv.deathDisplaySeconds + 0.01f);
|
||||
DeathFlow.Revive();
|
||||
RunResultUI.Tick(Time.unscaledTime);
|
||||
Chk(sb, RunResultUI.IsOpen && !RunResultUI.Pending,
|
||||
"부활 뒤 결과 화면 표시(open=" + RunResultUI.IsOpen + " shows=" + RunResultUI.ShownCount + ")");
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(RunResultUI.Dump());
|
||||
}
|
||||
catch (Exception ex) { s_fail++; sb.AppendLine("**FAIL** 예외 " + ex.GetType().Name + " — " + ex.Message + "\n" + ex.StackTrace); }
|
||||
finally
|
||||
{
|
||||
ui.skipRevivePopupAfterRunEnd = skip0; ui.resultDeferWhileDying = dying0; ui.resultLowerWhileRevive = lower0;
|
||||
DeathFlow.ReviveRequested.Remove(OnReq);
|
||||
DeathFlow.ReviveOverride = ovr0;
|
||||
DeathFlow.Abort("probe cleanup");
|
||||
WLSurvivalSettings.RuntimeOverride = ov0;
|
||||
RunResultUI.Teardown();
|
||||
Kill(pc != null ? pc.gameObject : null);
|
||||
Kill(root);
|
||||
SetPhase(phase0);
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("① 결과 = " + s_pass + " PASS / " + s_fail + " FAIL");
|
||||
string body = sb.ToString();
|
||||
return body + "\n→ " + Write("WL813q2_overlap.txt", body);
|
||||
}
|
||||
|
||||
// ────────────────────────────────────── ② 워치독 자동 부활 경로(813z)
|
||||
|
||||
public static string Watchdog()
|
||||
{
|
||||
s_pass = s_fail = 0;
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("=== WL-813q2 ② 워치독 자동 부활 경로에서도 「부활 우선」이 걸리는가 (813z §1) ===");
|
||||
|
||||
var ui = WLRunUiSettings.Instance;
|
||||
var sv = WLSurvivalSettings.Instance;
|
||||
if (ui == null || sv == null) { string b = sb.AppendLine("**FAIL** 설정 에셋 없음").ToString(); return b + "\n→ " + Write("WL813q2_watchdog.txt", b); }
|
||||
|
||||
GameObject root = null; PCActor pc = null;
|
||||
var phase0 = GetPhase();
|
||||
int ov0 = WLSurvivalSettings.RuntimeOverride;
|
||||
var ovr0 = DeathFlow.ReviveOverride;
|
||||
bool dying0 = ui.resultDeferWhileDying;
|
||||
|
||||
try
|
||||
{
|
||||
WLSurvivalSettings.RuntimeOverride = 1;
|
||||
DeathFlow.ReviveOverride = (p, r) => { };
|
||||
DeathFlow.ReviveRequested.Add(OnReq);
|
||||
s_reqSeen = 0;
|
||||
|
||||
var uiRoot = BuildUiRoot(out root);
|
||||
RunResultUI.Bind(uiRoot);
|
||||
RunResultUI.Subscribe(true);
|
||||
RunResultUI.ResetState();
|
||||
DeathFlow.ResetDiagnostics();
|
||||
pc = MakePC();
|
||||
|
||||
sb.AppendLine("워치독 SO 실측: reviveWatchdogSeconds=" + sv.reviveWatchdogSeconds.ToString("F1") + "s");
|
||||
SetPhase(RunPhase.Zones);
|
||||
float t = 5000f;
|
||||
DeathFlow.Tick(t); DeathFlow.OnPCDied(pc);
|
||||
float req = t + sv.deathDisplaySeconds + 0.01f;
|
||||
DeathFlow.Tick(req);
|
||||
Chk(sb, DeathFlow.State == DeathState.AwaitingRevive, "부활 대기(DeathFlow=" + DeathFlow.State + ")");
|
||||
|
||||
// 런 종료가 부활 대기 중에 온다 → 결과는 미뤄진다(813q 는 Busy 로만 봤다)
|
||||
SetPhase(RunPhase.Ended);
|
||||
var r = FakeResult(RunOutcome.Timeout, 240f, -1f, 31, 0);
|
||||
WLRunUiProbeHooks.RaiseRunEnded(in r);
|
||||
Chk(sb, !RunResultUI.IsOpen && RunResultUI.Pending,
|
||||
"결과 미룸 — 이유 = " + RunResultUI.ReviveBusyReason + " (ReviveDialog.Busy=" + ReviveDialog.Busy +
|
||||
" 이라 813q 판정만으로는 못 막는다)");
|
||||
|
||||
// 워치독 시각 직전/직후
|
||||
DeathFlow.Tick(req + sv.reviveWatchdogSeconds - 0.1f);
|
||||
RunResultUI.Tick(Time.unscaledTime);
|
||||
Chk(sb, !RunResultUI.IsOpen, "워치독 직전까지 계속 미룸(defer=" + RunResultUI.DeferredCount + ")");
|
||||
|
||||
DeathFlow.Tick(req + sv.reviveWatchdogSeconds + 0.02f);
|
||||
sb.AppendLine(" 워치독: WatchdogCount=" + DeathFlow.WatchdogCount + " 대기 " + DeathFlow.LastWatchdogSeconds.ToString("F2") +
|
||||
"s · auto 부활=" + DeathFlow.AutoReviveCount + " · RunResultUI.Revived=" + RunResultUI.RevivedSeen);
|
||||
Chk(sb, DeathFlow.WatchdogCount == 1 && DeathFlow.State == DeathState.None, "워치독 자동 부활 1회 · 상태 None");
|
||||
Chk(sb, RunResultUI.RevivedSeen >= 1, "RunResultUI 가 Revived 를 받았다(워치독 경로 포함)");
|
||||
RunResultUI.Tick(Time.unscaledTime);
|
||||
Chk(sb, RunResultUI.IsOpen && !RunResultUI.Pending, "워치독 부활 뒤 결과 화면 표시(shows=" + RunResultUI.ShownCount + ")");
|
||||
|
||||
// C8 — resultDeferWhileDying=0 이면 813q 동작(미룸 없음)
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("C8 롤백 — resultDeferWhileDying=0 (813q 동작 복귀)");
|
||||
ui.resultDeferWhileDying = false;
|
||||
RunResultUI.ResetState(); DeathFlow.ResetDiagnostics();
|
||||
SetPhase(RunPhase.Zones);
|
||||
t = 6000f; DeathFlow.Tick(t); DeathFlow.OnPCDied(pc);
|
||||
DeathFlow.Tick(t + sv.deathDisplaySeconds + 0.01f);
|
||||
Chk(sb, DeathFlow.State == DeathState.AwaitingRevive && !RunResultUI.ReviveBusy,
|
||||
"DeathFlow=" + DeathFlow.State + " 인데 부활우선=" + RunResultUI.ReviveBusy + " (스위치 off = 813q 판정만)");
|
||||
DeathFlow.Revive();
|
||||
}
|
||||
catch (Exception ex) { s_fail++; sb.AppendLine("**FAIL** 예외 " + ex.GetType().Name + " — " + ex.Message + "\n" + ex.StackTrace); }
|
||||
finally
|
||||
{
|
||||
ui.resultDeferWhileDying = dying0;
|
||||
DeathFlow.ReviveRequested.Remove(OnReq);
|
||||
DeathFlow.ReviveOverride = ovr0;
|
||||
DeathFlow.Abort("probe cleanup");
|
||||
WLSurvivalSettings.RuntimeOverride = ov0;
|
||||
RunResultUI.Teardown();
|
||||
Kill(pc != null ? pc.gameObject : null);
|
||||
Kill(root);
|
||||
SetPhase(phase0);
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("② 결과 = " + s_pass + " PASS / " + s_fail + " FAIL");
|
||||
string body2 = sb.ToString();
|
||||
return body2 + "\n→ " + Write("WL813q2_watchdog.txt", body2);
|
||||
}
|
||||
|
||||
// ─────────────────────────────── ③ D-23 「다음 런」 뒤 상태 리셋
|
||||
|
||||
public static string Reset()
|
||||
{
|
||||
s_pass = s_fail = 0;
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("=== WL-813q2 ③ D-23 「다음 런」 뒤 타이머 색 · 보스 바 · 배너 리셋 ===");
|
||||
|
||||
var ui = WLRunUiSettings.Instance;
|
||||
if (ui == null) { string b = sb.AppendLine("**FAIL** WLRunUiSettings 없음").ToString(); return b + "\n→ " + Write("WL813q2_reset.txt", b); }
|
||||
|
||||
GameObject root = null, barGo = null, bannerGo = null;
|
||||
bool reset0 = ui.timerResetColorOnRunStart;
|
||||
var phase0 = GetPhase();
|
||||
|
||||
try
|
||||
{
|
||||
var uiRoot = BuildUiRoot(out root);
|
||||
sb.AppendLine("RunHud Bind: " + RunHud.Bind(uiRoot));
|
||||
RunHud.Subscribe(true);
|
||||
RunHud.ResetState();
|
||||
|
||||
string warnHex = ColorUtility.ToHtmlStringRGB(ui.timerWarnColor);
|
||||
string normHex = ColorUtility.ToHtmlStringRGB(ui.timerColor);
|
||||
sb.AppendLine("SO 색 실측: 평상 #" + normHex + " · 경고 #" + warnHex + " (경고 구간 " + ui.timerWarnSeconds + "s)");
|
||||
|
||||
// 런 1 — 시작 → 마지막 30 s 경고색
|
||||
WLRunUiProbeHooks.RaiseRunStarted(1, 240f, 2, "probe");
|
||||
Chk(sb, ColorUtility.ToHtmlStringRGB(RunHud.TimerColor) == normHex,
|
||||
"런 1 시작 타이머 색 #" + ColorUtility.ToHtmlStringRGB(RunHud.TimerColor) + " = 평상색 · 표시 \"" + RunHud.TimerText + "\"");
|
||||
WLRunUiProbeHooks.RaiseRunTick(215f, 25f, 1, 3, 14, 1, 2, RunPhase.Zones, 1);
|
||||
Chk(sb, ColorUtility.ToHtmlStringRGB(RunHud.TimerColor) == warnHex && RunHud.TimerWarn,
|
||||
"남은 25 s → 경고색 #" + ColorUtility.ToHtmlStringRGB(RunHud.TimerColor) + " · 표시 \"" + RunHud.TimerText + "\"");
|
||||
|
||||
var r = FakeResult(RunOutcome.Timeout, 240f, -1f, 29, 0);
|
||||
WLRunUiProbeHooks.RaiseRunEnded(in r);
|
||||
|
||||
// 런 2 (= 「다음 런」) — 색이 평상색으로 돌아와야 한다
|
||||
WLRunUiProbeHooks.RaiseRunStarted(2, 240f, 2, "restart");
|
||||
Chk(sb, ColorUtility.ToHtmlStringRGB(RunHud.TimerColor) == normHex,
|
||||
"🔴 D-23 — 다음 런 시작 타이머 색 #" + ColorUtility.ToHtmlStringRGB(RunHud.TimerColor) +
|
||||
" = 평상색 · 표시 \"" + RunHud.TimerText + "\" (색 쓰기 " + RunHud.TimerColorWrites + "회)");
|
||||
bool zonesZero = true;
|
||||
for (int i = 0; i < RunHud.ZoneCells; i++) if (RunHud.ZoneFill(i) > 0.0005f) zonesZero = false;
|
||||
Chk(sb, zonesZero && RunHud.ZoneCells == 2, "존 게이지 리셋 — 칸 " + RunHud.ZoneCells + "개 전부 fill 0");
|
||||
|
||||
// C8 — 스위치를 끄면 813q 버그가 그대로 재현된다(= 이 스위치가 원인 지점이다)
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("C8 롤백 — timerResetColorOnRunStart=0 (813q 동작 = D-23 재현)");
|
||||
ui.timerResetColorOnRunStart = false;
|
||||
WLRunUiProbeHooks.RaiseRunTick(215f, 25f, 1, 3, 14, 1, 2, RunPhase.Zones, 2);
|
||||
WLRunUiProbeHooks.RaiseRunStarted(3, 240f, 2, "restart");
|
||||
Chk(sb, ColorUtility.ToHtmlStringRGB(RunHud.TimerColor) == warnHex,
|
||||
"스위치 off → 이전 런의 경고색 #" + ColorUtility.ToHtmlStringRGB(RunHud.TimerColor) +
|
||||
" 이 그대로 남는다 = D-23 원인 지점 확정");
|
||||
ui.timerResetColorOnRunStart = true;
|
||||
WLRunUiProbeHooks.RaiseRunStarted(4, 240f, 2, "restart");
|
||||
Chk(sb, ColorUtility.ToHtmlStringRGB(RunHud.TimerColor) == normHex, "스위치 on 복귀 → 평상색");
|
||||
|
||||
// ── 보스 HP 바
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("보스 HP 바 — 이전 런의 바가 남는가");
|
||||
barGo = new GameObject("__WL813q2_BossBar", typeof(RectTransform));
|
||||
barGo.hideFlags = HideFlags.DontSave;
|
||||
barGo.transform.SetParent(root.transform, false);
|
||||
var bar = barGo.AddComponent<BossHpBar>();
|
||||
sb.AppendLine(" " + bar.Initialize());
|
||||
sb.AppendLine(" " + bar.Bind(null, "테스트 보스", 200f)); // 합성(actor=null) 경로
|
||||
Chk(sb, bar.Visible && bar.LastRatio > 0.99f, "바 표시 · ratio=" + bar.LastRatio.ToString("F3"));
|
||||
bar.SetRatio(0.37f);
|
||||
Chk(sb, Mathf.Abs(BossHpBar.RunBossHpRatio - 0.37f) < 0.001f,
|
||||
"런 단위 보스 HP 실측값 = " + (BossHpBar.RunBossHpRatio * 100f).ToString("F1") + "% (결과 화면이 읽는 값)");
|
||||
|
||||
int reset1 = BossHpBar.RunResetCount;
|
||||
WLRunUiProbeHooks.RaiseRunStarted(5, 240f, 2, "restart");
|
||||
sb.AppendLine(" RunStarted 뒤: 리셋 " + (BossHpBar.RunResetCount - reset1) + "회 · visible=" + bar.Visible +
|
||||
" · 런 HP=" + BossHpBar.RunBossHpRatio.ToString("F2") + " · 늦은구독 스캔=" + bar.LateScanCount);
|
||||
Chk(sb, BossHpBar.RunResetCount == reset1 + 1, "RunStarted 를 받아 리셋했다");
|
||||
Chk(sb, !bar.Visible, "🔴 D-23 — 이전 런의 보스 바가 사라졌다(visible=" + bar.Visible + ")");
|
||||
Chk(sb, BossHpBar.RunBossHpRatio < 0f, "런 단위 보스 HP 값도 비워졌다(" + BossHpBar.RunBossHpRatio.ToString("F2") + ")");
|
||||
|
||||
ui.bossResetOnRunStart = false;
|
||||
bar.Bind(null, "테스트 보스", 200f);
|
||||
WLRunUiProbeHooks.RaiseRunStarted(6, 240f, 2, "restart");
|
||||
Chk(sb, bar.Visible, "C8 롤백 bossResetOnRunStart=0 → 813q 동작(바가 남는다 · visible=" + bar.Visible + ")");
|
||||
ui.bossResetOnRunStart = true;
|
||||
|
||||
// ── 보스 배너(재트리거 가드)
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("보스 등장 배너 — 재트리거 가드가 다음 런에서 풀리는가");
|
||||
bannerGo = new GameObject("__WL813q2_BossBanner", typeof(RectTransform));
|
||||
bannerGo.hideFlags = HideFlags.DontSave;
|
||||
bannerGo.transform.SetParent(root.transform, false);
|
||||
var banner = bannerGo.AddComponent<BossBanner>();
|
||||
sb.AppendLine(" " + banner.Initialize());
|
||||
sb.AppendLine(" " + banner.Trigger("테스트 보스", true));
|
||||
int show1 = banner.ShowCount;
|
||||
sb.AppendLine(" 같은 런에서 재트리거: " + banner.Trigger("테스트 보스", true));
|
||||
Chk(sb, banner.ShowCount == show1, "같은 런에서는 가드가 막는다(shows=" + banner.ShowCount + ")");
|
||||
WLRunUiProbeHooks.RaiseRunStarted(7, 240f, 2, "restart");
|
||||
Chk(sb, !banner.Visible, "다음 런 시작 → 배너 숨김(alpha=" + banner.BannerAlpha.ToString("F2") + ")");
|
||||
sb.AppendLine(" 다음 런에서 재트리거: " + banner.Trigger("테스트 보스", true));
|
||||
Chk(sb, banner.ShowCount == show1 + 1, "다음 런에서는 다시 뜬다 = 가드 해제됨(shows=" + banner.ShowCount + ")");
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(RunHud.Dump());
|
||||
}
|
||||
catch (Exception ex) { s_fail++; sb.AppendLine("**FAIL** 예외 " + ex.GetType().Name + " — " + ex.Message + "\n" + ex.StackTrace); }
|
||||
finally
|
||||
{
|
||||
ui.timerResetColorOnRunStart = reset0;
|
||||
ui.bossResetOnRunStart = true;
|
||||
BossHpBar.RunBossHpRatio = -1f; BossHpBar.RunBossSeen = false;
|
||||
RunHud.Teardown();
|
||||
Kill(barGo); Kill(bannerGo); Kill(root);
|
||||
SetPhase(phase0);
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("③ 결과 = " + s_pass + " PASS / " + s_fail + " FAIL");
|
||||
string body3 = sb.ToString();
|
||||
return body3 + "\n→ " + Write("WL813q2_reset.txt", body3);
|
||||
}
|
||||
|
||||
// ─────────────────────────────── ④ 결과 화면 행 보강(보스 미처치 · 엘리트)
|
||||
|
||||
public static string Rows()
|
||||
{
|
||||
s_pass = s_fail = 0;
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("=== WL-813q2 ④ 결과 화면 행 — 보스 미처치 문구 · 엘리트 처치 수 ===");
|
||||
|
||||
var ui = WLRunUiSettings.Instance;
|
||||
if (ui == null) { string b = sb.AppendLine("**FAIL** WLRunUiSettings 없음").ToString(); return b + "\n→ " + Write("WL813q2_rows.txt", b); }
|
||||
|
||||
GameObject root = null, barGo = null;
|
||||
var phase0 = GetPhase();
|
||||
|
||||
try
|
||||
{
|
||||
var uiRoot = BuildUiRoot(out root);
|
||||
RunResultUI.Bind(uiRoot);
|
||||
RunResultUI.Subscribe(true);
|
||||
RunResultUI.ResetState();
|
||||
BossHpBar.RunBossHpRatio = -1f; BossHpBar.RunBossSeen = false; // 앞 항목의 잔값 제거
|
||||
|
||||
sb.AppendLine("SO 행 배열 = " + ui.resultRows.Length + "개 · 엘리트 행 포함=" + ui.HasRow(WLRunResultRow.EliteKills));
|
||||
Chk(sb, ui.HasRow(WLRunResultRow.EliteKills), "엘리트 처치 행이 SO 배열에 있다");
|
||||
|
||||
// 엘리트 집계 — 런 중에만 센다
|
||||
SetPhase(RunPhase.Zones);
|
||||
WLRunUiProbeHooks.RaiseRunStarted(1, 240f, 2, "probe");
|
||||
for (int i = 0; i < 3; i++) WLRunUiProbeHooks.RaiseKilled(eSubRol.Elite);
|
||||
for (int i = 0; i < 5; i++) WLRunUiProbeHooks.RaiseKilled(eSubRol.None);
|
||||
WLRunUiProbeHooks.RaiseKilled(eSubRol.Boss);
|
||||
Chk(sb, RunResultUI.EliteKills == 3, "런 중 엘리트 처치 " + RunResultUI.EliteKills + "건(일반 5 · 보스 1 은 세지 않는다)");
|
||||
|
||||
SetPhase(RunPhase.Ended);
|
||||
WLRunUiProbeHooks.RaiseKilled(eSubRol.Elite); // 런 밖 처치는 세지 않는다
|
||||
Chk(sb, RunResultUI.EliteKills == 3, "런 밖 엘리트 처치는 무시(" + RunResultUI.EliteKills + ")");
|
||||
|
||||
// 보스 미처치 + HP 를 모르는 경우
|
||||
var r = FakeResult(RunOutcome.Timeout, 240f, -1f, 29, 0);
|
||||
WLRunUiProbeHooks.RaiseRunEnded(in r);
|
||||
string boss1 = RunResultUI.RowValueText(WLRunResultRow.BossClear);
|
||||
string elite1 = RunResultUI.RowValueText(WLRunResultRow.EliteKills);
|
||||
sb.AppendLine(" 보스 처치 행 = \"" + boss1 + "\" · 엘리트 처치 행 = \"" + elite1 + "\"");
|
||||
Chk(sb, boss1 == ui.resultBossNotClearedText, "보스 미처치(HP 모름) → \"" + boss1 + "\"");
|
||||
Chk(sb, elite1 == "3", "엘리트 처치 행 = \"" + elite1 + "\" (RunEnded 순간 스냅샷)");
|
||||
|
||||
// 보스 미처치 + 813c 바가 남긴 HP 실측값
|
||||
barGo = new GameObject("__WL813q2_BossBar2", typeof(RectTransform));
|
||||
barGo.hideFlags = HideFlags.DontSave;
|
||||
barGo.transform.SetParent(root.transform, false);
|
||||
var bar = barGo.AddComponent<BossHpBar>();
|
||||
bar.Initialize();
|
||||
bar.Bind(null, "테스트 보스", 200f);
|
||||
bar.SetRatio(0.436f);
|
||||
RunResultUI.ResetState();
|
||||
WLRunUiProbeHooks.RaiseRunEnded(in r);
|
||||
string boss2 = RunResultUI.RowValueText(WLRunResultRow.BossClear);
|
||||
sb.AppendLine(" 바 실측 HP 43.6% 일 때 보스 처치 행 = \"" + boss2 + "\"");
|
||||
Chk(sb, boss2.Contains("44"), "보스 미처치 + HP 남음 44 % 표기 — \"" + boss2 + "\"");
|
||||
|
||||
// 보스 처치한 런은 시간이 그대로 나온다
|
||||
var r2 = FakeResult(RunOutcome.Win, 154f, 154f, 99, 1);
|
||||
RunResultUI.ResetState();
|
||||
WLRunUiProbeHooks.RaiseRunEnded(in r2);
|
||||
string boss3 = RunResultUI.RowValueText(WLRunResultRow.BossClear);
|
||||
Chk(sb, boss3 == "02:34", "보스 처치 런 = \"" + boss3 + "\" (mm:ss 그대로 · 813q 동작 불변)");
|
||||
|
||||
// C8 — UI 집계·UI HP 를 끄면 이전 동작
|
||||
ui.resultBossHpFromUi = false; ui.resultCountElitesInUi = false;
|
||||
RunResultUI.ResetState();
|
||||
WLRunUiProbeHooks.RaiseRunEnded(in r);
|
||||
Chk(sb, RunResultUI.RowValueText(WLRunResultRow.BossClear) == ui.resultBossNotClearedText &&
|
||||
RunResultUI.RowValueText(WLRunResultRow.EliteKills) == "0",
|
||||
"C8 롤백 — 보스 행 \"" + RunResultUI.RowValueText(WLRunResultRow.BossClear) + "\" · 엘리트 행 \"" +
|
||||
RunResultUI.RowValueText(WLRunResultRow.EliteKills) + "\"");
|
||||
ui.resultBossHpFromUi = true; ui.resultCountElitesInUi = true;
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(RunResultUI.Dump());
|
||||
}
|
||||
catch (Exception ex) { s_fail++; sb.AppendLine("**FAIL** 예외 " + ex.GetType().Name + " — " + ex.Message + "\n" + ex.StackTrace); }
|
||||
finally
|
||||
{
|
||||
ui.resultBossHpFromUi = true; ui.resultCountElitesInUi = true;
|
||||
RunResultUI.Teardown();
|
||||
Kill(barGo); Kill(root);
|
||||
SetPhase(phase0);
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("④ 결과 = " + s_pass + " PASS / " + s_fail + " FAIL");
|
||||
string body4 = sb.ToString();
|
||||
return body4 + "\n→ " + Write("WL813q2_rows.txt", body4);
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────── ⑤ GC 0 · C8
|
||||
|
||||
public static string Gc()
|
||||
{
|
||||
s_pass = s_fail = 0;
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("=== WL-813q2 ⑤ GC 0 · C8 ===");
|
||||
|
||||
var ui = WLRunUiSettings.Instance;
|
||||
if (ui == null) { string b = sb.AppendLine("**FAIL** WLRunUiSettings 없음").ToString(); return b + "\n→ " + Write("WL813q2_gc.txt", b); }
|
||||
|
||||
GameObject root = null;
|
||||
var phase0 = GetPhase();
|
||||
|
||||
try
|
||||
{
|
||||
var uiRoot = BuildUiRoot(out root);
|
||||
RunHud.Bind(uiRoot); RunHud.Subscribe(true); RunHud.ResetState();
|
||||
RunResultUI.Bind(uiRoot); RunResultUI.Subscribe(true); RunResultUI.ResetState();
|
||||
|
||||
SetPhase(RunPhase.Zones);
|
||||
WLRunUiProbeHooks.RaiseRunStarted(1, 240f, 2, "probe");
|
||||
WLRunUiProbeHooks.RaiseRunTick(60f, 180f, 0, 5, 24, 0, 2, RunPhase.Zones, 1);
|
||||
|
||||
// 같은 초 100틱 = 표시 쓰기 0회 · 델타 0 B
|
||||
GC.Collect(); GC.WaitForPendingFinalizers();
|
||||
long m0 = GC.GetTotalMemory(true);
|
||||
int tw0 = RunHud.TimerWrites, cw0 = RunHud.TimerColorWrites;
|
||||
for (int i = 0; i < 100; i++) WLRunUiProbeHooks.RaiseRunTick(60f, 180f, 0, 5, 24, 0, 2, RunPhase.Zones, 1);
|
||||
long m1 = GC.GetTotalMemory(false);
|
||||
Chk(sb, RunHud.TimerWrites == tw0 && RunHud.TimerColorWrites == cw0,
|
||||
"같은 초 100틱 — 타이머 쓰기 " + (RunHud.TimerWrites - tw0) + "회 · 색 쓰기 " + (RunHud.TimerColorWrites - cw0) + "회");
|
||||
Chk(sb, m1 - m0 == 0, "Δ GetTotalMemory = " + (m1 - m0) + " B");
|
||||
|
||||
// Killed 100건(엘리트 집계 경로) — 문자열 0
|
||||
GC.Collect(); GC.WaitForPendingFinalizers();
|
||||
long k0 = GC.GetTotalMemory(true);
|
||||
for (int i = 0; i < 100; i++) WLRunUiProbeHooks.RaiseKilled(i % 2 == 0 ? eSubRol.Elite : eSubRol.None);
|
||||
long k1 = GC.GetTotalMemory(false);
|
||||
Chk(sb, k1 - k0 == 0, "Killed 100건(엘리트 50) Δ = " + (k1 - k0) + " B · 집계=" + RunResultUI.EliteKills);
|
||||
|
||||
// 재정렬 100회 왕복 — 노드 재생성 0(Canvas·Raycaster 는 1개씩 재사용)
|
||||
// 🔴 재정렬 함수는 진단 문자열을 돌려주므로 그 자체는 할당이 있다. 다만 **매 프레임 경로가 아니다**
|
||||
// (사망 1회 · 부활 1회). 여기서는 "노드를 다시 만들지 않는다"만 PASS 기준으로 둔다.
|
||||
long l0 = GC.GetTotalMemory(true);
|
||||
for (int i = 0; i < 100; i++) { RunResultUI.LowerBehindRevive(); RunResultUI.RaiseToFront(); }
|
||||
long l1 = GC.GetTotalMemory(false);
|
||||
var resRoot = root.transform.Find("IngameUIs/WL_HUD/WL_RunResult");
|
||||
int canvasN = resRoot != null ? resRoot.GetComponents<Canvas>().Length : -1;
|
||||
int rayN = resRoot != null ? resRoot.GetComponents<GraphicRaycaster>().Length : -1;
|
||||
Chk(sb, RunResultUI.LowerCount == 100 && RunResultUI.RaiseCount == 100, "재정렬 왕복 100회(내림 " +
|
||||
RunResultUI.LowerCount + " / 복귀 " + RunResultUI.RaiseCount + ") · Δ = " + (l1 - l0) + " B(진단 문자열분)");
|
||||
Chk(sb, canvasN == 1 && rayN == 1, "노드 재생성 0 — Canvas " + canvasN + "개 · GraphicRaycaster " + rayN + "개");
|
||||
|
||||
// C8 — 전체 off
|
||||
sb.AppendLine();
|
||||
WLRunUiSettings.RuntimeDisabled = true;
|
||||
RunResultUI.ResetState();
|
||||
var r = FakeResult(RunOutcome.Timeout, 240f, -1f, 29, 0);
|
||||
SetPhase(RunPhase.Ended);
|
||||
WLRunUiProbeHooks.RaiseRunEnded(in r);
|
||||
RunResultUI.Tick(Time.unscaledTime);
|
||||
Chk(sb, !RunResultUI.IsOpen, "C8 RuntimeDisabled — 결과 화면 표시 0(open=" + RunResultUI.IsOpen + ")");
|
||||
Chk(sb, !RunResultUI.ReviveBusy, "C8 중 부활우선 판정도 무동작(" + RunResultUI.ReviveBusyReason + ")");
|
||||
WLRunUiSettings.RuntimeDisabled = false;
|
||||
Chk(sb, WLRunUiSettings.Enabled, "C8 복구 — Enabled=" + WLRunUiSettings.Enabled);
|
||||
}
|
||||
catch (Exception ex) { s_fail++; sb.AppendLine("**FAIL** 예외 " + ex.GetType().Name + " — " + ex.Message + "\n" + ex.StackTrace); }
|
||||
finally
|
||||
{
|
||||
WLRunUiSettings.RuntimeDisabled = false;
|
||||
RunHud.Teardown(); RunResultUI.Teardown();
|
||||
Kill(root);
|
||||
SetPhase(phase0);
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("⑤ 결과 = " + s_pass + " PASS / " + s_fail + " FAIL");
|
||||
string body5 = sb.ToString();
|
||||
return body5 + "\n→ " + Write("WL813q2_gc.txt", body5);
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────────────────── 전체
|
||||
|
||||
public static string All()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
int pass = 0, fail = 0;
|
||||
string[] parts = new string[5];
|
||||
parts[0] = Overlap(); pass += s_pass; fail += s_fail;
|
||||
parts[1] = Watchdog(); pass += s_pass; fail += s_fail;
|
||||
parts[2] = Reset(); pass += s_pass; fail += s_fail;
|
||||
parts[3] = Rows(); pass += s_pass; fail += s_fail;
|
||||
parts[4] = Gc(); pass += s_pass; fail += s_fail;
|
||||
for (int i = 0; i < parts.Length; i++) sb.AppendLine(parts[i]).AppendLine();
|
||||
sb.AppendLine("======================================================");
|
||||
sb.AppendLine("WL-813q2 전체 = " + pass + " PASS / " + fail + " FAIL");
|
||||
string body = sb.ToString();
|
||||
return "WL-813q2 전체 = " + pass + " PASS / " + fail + " FAIL\n→ " + Write("WL813q2_all.txt", body);
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ using UnityEngine;
|
|||
using UnityEngine.UI;
|
||||
using WL.Combat.Boss;
|
||||
using WL.Combat.Core;
|
||||
using WL.Combat.Run;
|
||||
|
||||
namespace WL.UI
|
||||
{
|
||||
|
|
@ -137,14 +138,37 @@ namespace WL.UI
|
|||
{
|
||||
CombatEvents.Spawned.Add(OnSpawned);
|
||||
BossEvents.BossPhase.Add(OnBossPhase);
|
||||
RunEvents.RunStarted.Add(OnRunStarted); // WL-813q2(D-23) — 다음 런 리셋
|
||||
}
|
||||
else
|
||||
{
|
||||
CombatEvents.Spawned.Remove(OnSpawned);
|
||||
BossEvents.BossPhase.Remove(OnBossPhase);
|
||||
RunEvents.RunStarted.Remove(OnRunStarted);
|
||||
}
|
||||
}
|
||||
|
||||
// ── WL-813q2(D-23) — 「다음 런」 뒤 배너 상태(재트리거 가드·이름) 리셋 ──
|
||||
private void OnRunStarted(in RunStartedEvent e) { ResetForNewRun(); }
|
||||
|
||||
/// <summary>런 시작 리셋 — 배너를 숨기고 재트리거 가드·이름을 지운다(같은 보스라도 새 런에서는 다시 뜬다).</summary>
|
||||
public string ResetForNewRun()
|
||||
{
|
||||
var ui = WLRunUiSettings.Instance;
|
||||
if (ui != null && !ui.bossResetOnRunStart) return "런시작리셋: bossResetOnRunStart=false — 생략";
|
||||
|
||||
RunResetCount++;
|
||||
_lastTrigger = -999f; _shownName = ""; _nameKnown = false;
|
||||
LateTriggered = false;
|
||||
HideNow();
|
||||
|
||||
if (ui == null || !ui.bossBannerRetriggerOnRunStart) return "런시작리셋: 숨김·가드 해제만";
|
||||
return "런시작리셋 → " + TriggerExistingBoss();
|
||||
}
|
||||
|
||||
/// <summary>런 시작 리셋 횟수(진단 · D-23).</summary>
|
||||
public static int RunResetCount;
|
||||
|
||||
// ── 구독 ──────────────────────────────────────────────────────────────
|
||||
private void OnSpawned(in SpawnedEvent e)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ using TMPro;
|
|||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using WL.Combat.Core;
|
||||
using WL.Combat.Run;
|
||||
|
||||
namespace WL.UI
|
||||
{
|
||||
|
|
@ -54,6 +55,15 @@ namespace WL.UI
|
|||
private float _hideAt = -1f;
|
||||
private bool _subscribed;
|
||||
|
||||
// ── WL-813q2 — 이번 런에서 바가 실제로 본 보스 HP 비율(결과 화면 「보스 처치」 행이 읽는다) ──
|
||||
// 🔴 813p `RunResult` 에는 보스 HP 필드가 없다(구조체 실측) → 이 값이 UI 자체 실측 원천이다.
|
||||
// −1 = 이번 런에 보스를 한 번도 물지 않았다.
|
||||
public static float RunBossHpRatio = -1f;
|
||||
/// <summary>이번 런에 보스를 한 번이라도 물었는가(진단).</summary>
|
||||
public static bool RunBossSeen;
|
||||
/// <summary>런 시작 리셋 횟수(진단 · D-23).</summary>
|
||||
public static int RunResetCount;
|
||||
|
||||
/// <summary>마지막 갱신 결과 — 프로브(에디터)가 읽는 진단 값.</summary>
|
||||
public float LastRatio { get; private set; }
|
||||
public int SpawnedSeen { get; private set; }
|
||||
|
|
@ -145,15 +155,40 @@ namespace WL.UI
|
|||
CombatEvents.Spawned.Add(OnSpawned);
|
||||
CombatEvents.HitConfirmed.Add(OnHitConfirmed);
|
||||
CombatEvents.Killed.Add(OnKilled);
|
||||
RunEvents.RunStarted.Add(OnRunStarted); // WL-813q2(D-23) — 다음 런 리셋
|
||||
}
|
||||
else
|
||||
{
|
||||
CombatEvents.Spawned.Remove(OnSpawned);
|
||||
CombatEvents.HitConfirmed.Remove(OnHitConfirmed);
|
||||
CombatEvents.Killed.Remove(OnKilled);
|
||||
RunEvents.RunStarted.Remove(OnRunStarted);
|
||||
}
|
||||
}
|
||||
|
||||
// ── WL-813q2(D-23) — 「다음 런」 뒤 이전 런의 보스 바가 남는다 ─────────
|
||||
// `RunDirector.Restart()` 는 존 카운터·게이트만 되돌리고 UI 는 건드리지 않는다(Gameplay 소유).
|
||||
// 그래서 UI 쪽에서 `RunStarted` 를 받아 스스로 리셋하고, 보스가 아직 살아 있으면
|
||||
// 813y ⑤ 늦은 구독 경로(`BindExistingBoss`)를 그대로 다시 태워 물린다.
|
||||
private void OnRunStarted(in RunStartedEvent e) { ResetForNewRun(); }
|
||||
|
||||
/// <summary>런 시작 리셋 — 바를 숨기고 물림을 푼 뒤, 살아 있는 보스가 있으면 다시 물린다.</summary>
|
||||
public string ResetForNewRun()
|
||||
{
|
||||
var ui = WLRunUiSettings.Instance;
|
||||
if (ui != null && !ui.bossResetOnRunStart) return "런시작리셋: bossResetOnRunStart=false — 생략";
|
||||
|
||||
RunResetCount++;
|
||||
RunBossHpRatio = -1f; // 결과 화면이 읽는 런 단위 값도 여기서 비운다
|
||||
RunBossSeen = false;
|
||||
LateBound = false;
|
||||
HideNow(); // _bound=false · _boss=null · alpha 0
|
||||
SetRatio(0f);
|
||||
|
||||
if (ui != null && !ui.bossBarRebindOnRunStart) return "런시작리셋: 숨김만(재바인드 off)";
|
||||
return "런시작리셋 → " + BindExistingBoss();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_hideAt >= 0f && Time.unscaledTime >= _hideAt) HideNow();
|
||||
|
|
@ -308,6 +343,8 @@ namespace WL.UI
|
|||
{
|
||||
LastRatio = Mathf.Clamp01(ratio);
|
||||
if (fill != null) fill.fillAmount = LastRatio;
|
||||
// WL-813q2 — 물고 있는 동안만 런 단위 값을 남긴다(숨김·리셋 뒤의 0 은 반영하지 않는다).
|
||||
if (_bound) { RunBossHpRatio = LastRatio; RunBossSeen = true; }
|
||||
}
|
||||
|
||||
private void ShowNow()
|
||||
|
|
@ -534,6 +571,9 @@ 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(" [813q2] 런 리셋 " + RunResetCount + "회 · 런 보스 HP=" +
|
||||
(RunBossHpRatio >= 0f ? (RunBossHpRatio * 100f).ToString("F1") + "%" : "없음") +
|
||||
" (본적=" + RunBossSeen + ") · RunStarted 구독=" + RunEvents.RunStarted.Count);
|
||||
sb.AppendLine(" CombatEvents subscribers Spawned=" + CombatEvents.Spawned.Count +
|
||||
" HitConfirmed=" + CombatEvents.HitConfirmed.Count +
|
||||
" Killed=" + CombatEvents.Killed.Count + " Enabled=" + CombatEvents.Enabled);
|
||||
|
|
|
|||
|
|
@ -56,6 +56,10 @@ namespace WL.UI
|
|||
private static bool s_visible;
|
||||
private static int s_shownSec = int.MinValue;
|
||||
private static bool s_shownWarn;
|
||||
// WL-813q2(D-23) — 다음 런에서 타이머 색을 **반드시 한 번** 다시 쓰게 하는 플래그.
|
||||
// 813q 버그: RunStarted 가 s_shownWarn 을 false 로 되돌리면 WriteTimer 의
|
||||
// `warn != s_shownWarn` 이 false 가 되어 색 대입이 통째로 생략된다 → 이전 런의 경고색(붉은색) 잔존.
|
||||
private static bool s_timerColorDirty;
|
||||
private static int s_shownPhaseKind = -1; // 0 = 존 · 1 = 보스
|
||||
private static int s_shownPhaseZone = -1;
|
||||
private static float s_runSeconds;
|
||||
|
|
@ -66,7 +70,12 @@ namespace WL.UI
|
|||
// ── 진단(프로브가 읽는다 · 실측만) ────────────────────────────────────
|
||||
public static int StartedSeen, TickSeen, ZoneClearedSeen, GateSeen, BossSeen, EndedSeen;
|
||||
public static int TimerWrites, PhaseWrites, FillWrites;
|
||||
public static int TimerColorWrites; // WL-813q2(D-23) — 타이머 색을 실제로 쓴 횟수
|
||||
public static string LastEdge = "";
|
||||
/// <summary>지금 타이머 라벨에 들어 있는 색(실측 덤프용 · D-23 검증).</summary>
|
||||
public static Color TimerColor { get { return s_timer != null ? s_timer.color : new Color(0f, 0f, 0f, 0f); } }
|
||||
/// <summary>지금 경고색으로 보이는가(실측 덤프용).</summary>
|
||||
public static bool TimerWarn { get { return s_shownWarn; } }
|
||||
public static bool Subscribed { get { return s_subscribed; } }
|
||||
public static bool Bound { get { return s_root != null; } }
|
||||
public static bool Visible { get { return s_visible; } }
|
||||
|
|
@ -123,6 +132,8 @@ namespace WL.UI
|
|||
s_bossPhase = false;
|
||||
s_shownSec = int.MinValue; s_shownWarn = false;
|
||||
s_shownPhaseKind = -1; s_shownPhaseZone = -1;
|
||||
var st0 = St;
|
||||
if (st0 == null || st0.timerResetColorOnRunStart) s_timerColorDirty = true; // WL-813q2(D-23)
|
||||
BuildZoneCells(e.zoneCount);
|
||||
ResetZoneCells();
|
||||
SetVisible(true);
|
||||
|
|
@ -208,9 +219,18 @@ namespace WL.UI
|
|||
if (s_timer == null || s == null || !s.timerEnabled) return;
|
||||
int t = remaining <= 0f ? 0 : Mathf.CeilToInt(remaining);
|
||||
bool warn = s.timerWarnSeconds > 0f && remaining <= s.timerWarnSeconds;
|
||||
// WL-813q2(D-23) — 런 시작 직후 1회는 값이 같아도 색을 반드시 다시 쓴다(플래그는 여기서 바로 꺼진다
|
||||
// → 같은 초 100틱에서 색 쓰기 0회 = GC 0 규약 유지).
|
||||
if (s_timerColorDirty)
|
||||
{
|
||||
s_timerColorDirty = false;
|
||||
s_shownWarn = warn;
|
||||
s_timer.color = warn ? s.timerWarnColor : s.timerColor;
|
||||
TimerColorWrites++;
|
||||
}
|
||||
if (t == s_shownSec && warn == s_shownWarn) return; // 같은 초 = 아무 것도 하지 않는다(GC 0)
|
||||
s_shownSec = t;
|
||||
if (warn != s_shownWarn) { s_shownWarn = warn; s_timer.color = warn ? s.timerWarnColor : s.timerColor; }
|
||||
if (warn != s_shownWarn) { s_shownWarn = warn; s_timer.color = warn ? s.timerWarnColor : s.timerColor; TimerColorWrites++; }
|
||||
int n = FormatMMSS(s_timeBuf, remaining);
|
||||
s_timer.SetText(s_timeBuf, 0, n);
|
||||
TimerWrites++;
|
||||
|
|
@ -477,8 +497,8 @@ namespace WL.UI
|
|||
public static string ResetState()
|
||||
{
|
||||
StartedSeen = TickSeen = ZoneClearedSeen = GateSeen = BossSeen = EndedSeen = 0;
|
||||
TimerWrites = PhaseWrites = FillWrites = 0;
|
||||
s_shownSec = int.MinValue; s_shownWarn = false;
|
||||
TimerWrites = PhaseWrites = FillWrites = TimerColorWrites = 0;
|
||||
s_shownSec = int.MinValue; s_shownWarn = false; s_timerColorDirty = false;
|
||||
s_shownPhaseKind = -1; s_shownPhaseZone = -1;
|
||||
s_bossPhase = false; s_remaining = 0f;
|
||||
ResetZoneCells();
|
||||
|
|
@ -516,6 +536,10 @@ namespace WL.UI
|
|||
" gate=" + GateSeen + " boss=" + BossSeen + " ended=" + EndedSeen);
|
||||
sb.AppendLine(" 타이머 \"" + TimerText + "\" (남은 " + s_remaining.ToString("F1") + "s · 경고=" + s_shownWarn +
|
||||
" · 쓰기 " + TimerWrites + "회) · 단계 \"" + PhaseText + "\" (쓰기 " + PhaseWrites + "회)");
|
||||
sb.AppendLine(" [813q2] 타이머 색 = " + ColorUtility.ToHtmlStringRGB(TimerColor) +
|
||||
" (색 쓰기 " + TimerColorWrites + "회 · 다음런리셋대기=" + s_timerColorDirty +
|
||||
" · 기본 " + (s != null ? ColorUtility.ToHtmlStringRGB(s.timerColor) : "-") +
|
||||
" / 경고 " + (s != null ? ColorUtility.ToHtmlStringRGB(s.timerWarnColor) : "-") + ")");
|
||||
sb.Append(" 존 칸 " + s_zoneCells + " (fill 쓰기 " + FillWrites + "회):");
|
||||
for (int i = 0; i < s_zoneCells; i++)
|
||||
sb.Append(" [").Append(i).Append("]=").Append(ZoneFill(i).ToString("F2"))
|
||||
|
|
@ -601,6 +625,18 @@ namespace WL.UI
|
|||
}
|
||||
|
||||
public static void RaiseRunEnded(in RunResult r) { RunEvents.RunEnded.Dispatch(in r); }
|
||||
|
||||
/// <summary>WL-813q2 — 엘리트 집계 검증용 처치 이벤트(victim/killer 는 null · 같은 Dispatch 경로).</summary>
|
||||
public static void RaiseKilled(eSubRol subRole)
|
||||
{
|
||||
var e = new WL.Combat.Core.KilledEvent
|
||||
{
|
||||
victim = null, killer = null, subRole = subRole, id = 0,
|
||||
position = Vector3.zero, byDirectHit = true,
|
||||
time = Time.unscaledTime, frame = Time.frameCount,
|
||||
};
|
||||
WL.Combat.Core.CombatEvents.Killed.Dispatch(in e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>런 UI(HUD · 결과 화면)를 매 프레임 돌리는 런타임 전용 러너(씬에 굽지 않는다).</summary>
|
||||
|
|
|
|||
|
|
@ -22,6 +22,19 @@
|
|||
// 결과 화면은 미뤘다가(`resultDeferWhileRevive`) 부활 팝업이 닫힌 뒤에 뜬다.
|
||||
// `TimeScaleArbiter` 는 **건드리지 않는다**(발주서 §1-2).
|
||||
//
|
||||
// ■ WL-813q2(D-18) — 위 순서에 구멍이 있었다.
|
||||
// Q5 캡처 `PD2_07_result_revive_overlap.png` 실측: 제목이 **"시간 종료"** = 결과 화면이 **먼저** 떴고,
|
||||
// 그 뒤에 죽어서 부활 팝업(813z 전용 Canvas `sortingOrder=60`)이 **위에** 겹쳤다 → 9행 중 4행이 가려진다.
|
||||
// `RunEnded` 시점에는 아직 아무도 죽지 않았으니 `ReviveDialog.Busy` 가 false 라 미룸이 걸릴 수 없다.
|
||||
// 3층으로 막는다(전부 SO · 각각 끄면 813q 동작):
|
||||
// ① `resultDeferWhileDying` — 미룸 판정을 813i `DeathFlow.State`(Dying/AwaitingRevive)로 바꾼다
|
||||
// (`ReviveDialog.Busy` 는 보조 · 813z 워치독 부활 경로처럼 팝업이 안 잡히는 경우까지 덮는다).
|
||||
// ② `skipRevivePopupAfterRunEnd` — 런이 끝난 뒤(`RunDirector.IsRunning=false`) 사망은 런 밖이므로
|
||||
// `DeathFlow.Revive()`(813i 공개 API)를 바로 불러 **팝업 자체를 생략**한다. = 겹칠 일이 없다.
|
||||
// ③ `resultLowerWhileRevive` — 그래도 겹치면 결과 화면을 **부활 뒤로 재정렬**
|
||||
// (전용 Canvas `sortingOrder` 40 → 20 · `SetAsFirstSibling`), 부활이 끝나면 되돌린다.
|
||||
// 🔴 Gameplay 는 **읽기·구독·공개 API 호출만** 한다(파일 수정 0).
|
||||
//
|
||||
// ■ 프리팹 diff 0 — 노드는 전부 런타임 생성(`HideFlags.DontSave`) · 1회 만들고 재사용.
|
||||
// ■ C8 롤백 — `WLRunUiSettings.enabled_ = 0`(또는 `resultEnabled = 0`) → 구독 0 · 노드 0 · 표시 0.
|
||||
// 🔴 어셈블리 주의: Assets/WL/UI/ 에 .asmdef 를 만들지 말 것(Assembly-CSharp 단일).
|
||||
|
|
@ -31,7 +44,9 @@ using System.Text;
|
|||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using WL.Combat.Core;
|
||||
using WL.Combat.Run;
|
||||
using WL.Combat.Survival;
|
||||
|
||||
namespace WL.UI
|
||||
{
|
||||
|
|
@ -54,6 +69,8 @@ namespace WL.UI
|
|||
private static TextMeshProUGUI s_nextLabel, s_closeLabel;
|
||||
private static Image s_nextBg, s_closeBg;
|
||||
private static TMP_FontAsset s_font;
|
||||
private static Canvas s_canvas; // WL-813q2 — 전용 Canvas(부활 팝업 60 아래)
|
||||
private static GraphicRaycaster s_raycaster;
|
||||
|
||||
// ── 복사 버퍼 (🔴 813p 배열 재사용 계약) ──────────────────────────────
|
||||
private static RunResult s_result;
|
||||
|
|
@ -66,6 +83,9 @@ namespace WL.UI
|
|||
private static bool s_subscribed;
|
||||
private static bool s_open;
|
||||
private static bool s_pending; // 부활 팝업 때문에 미뤄 둔 결과가 있는가
|
||||
private static bool s_lowered; // WL-813q2 — 부활 뒤로 재정렬해 둔 상태인가
|
||||
private static int s_eliteKills; // WL-813q2 — 런 중 엘리트 처치(UI 자체 집계)
|
||||
private static int s_eliteKillsAtEnd; // RunEnded 순간의 스냅샷(결과 화면이 쓰는 값)
|
||||
private static readonly StringBuilder s_sb = new StringBuilder(128);
|
||||
private static readonly char[] s_timeBuf = new char[8];
|
||||
|
||||
|
|
@ -74,10 +94,21 @@ namespace WL.UI
|
|||
public static int BufferGrowCount; // 복사 버퍼를 새로 잡은 횟수(2번째 런부터 0 = GC 0)
|
||||
public static bool UsedCommonPopup;
|
||||
public static string LastShowLog = "";
|
||||
// ── WL-813q2(D-18) 진단 ──────────────────────────────────────────────
|
||||
public static int DiedSeen, ReviveRequestSeen, RevivedSeen;
|
||||
public static int SkipPopupReviveCount; // 런 밖 사망 → 팝업 생략 즉시 부활을 부른 횟수
|
||||
public static int LowerCount, RaiseCount; // 결과 화면을 부활 뒤로 내린/되돌린 횟수
|
||||
public static int EliteKilledSeen; // 런 중 본 엘리트 처치(UI 자체 집계)
|
||||
public static string LastDeathLog = "";
|
||||
public static bool Subscribed { get { return s_subscribed; } }
|
||||
public static bool Bound { get { return s_root != null; } }
|
||||
public static bool IsOpen { get { return s_open; } }
|
||||
public static bool Pending { get { return s_pending; } }
|
||||
public static bool Lowered { get { return s_lowered; } }
|
||||
public static int SortingOrder { get { return s_canvas != null && s_canvas.overrideSorting ? s_canvas.sortingOrder : -1; } }
|
||||
public static int SiblingIndex { get { return s_root != null ? s_root.GetSiblingIndex() : -1; } }
|
||||
public static int EliteKills { get { return s_eliteKills; } }
|
||||
public static int EliteKillsAtEnd { get { return s_eliteKillsAtEnd; } }
|
||||
public static int VisibleRows { get; private set; }
|
||||
/// <summary>복사해 둔 결과(배열 3종은 **자기 버퍼**를 가리킨다).</summary>
|
||||
public static RunResult Copied { get { return s_result; } }
|
||||
|
|
@ -101,22 +132,95 @@ namespace WL.UI
|
|||
public static string Subscribe(bool on)
|
||||
{
|
||||
if (on == s_subscribed) return "구독 변화 없음(" + s_subscribed + ")";
|
||||
if (on) { RunEvents.RunEnded.Add(OnRunEnded); RunEvents.RunStarted.Add(OnRunStarted); }
|
||||
else { RunEvents.RunEnded.Remove(OnRunEnded); RunEvents.RunStarted.Remove(OnRunStarted); }
|
||||
if (on)
|
||||
{
|
||||
RunEvents.RunEnded.Add(OnRunEnded); RunEvents.RunStarted.Add(OnRunStarted);
|
||||
// WL-813q2 — 813i DeathFlow 는 **읽기·구독만**(Gameplay 파일 수정 0)
|
||||
DeathFlow.Died.Add(OnDied);
|
||||
DeathFlow.ReviveRequested.Add(OnReviveRequested);
|
||||
DeathFlow.Revived.Add(OnRevived);
|
||||
CombatEvents.Killed.Add(OnKilled); // 엘리트 집계(RunResult 에 없다)
|
||||
}
|
||||
else
|
||||
{
|
||||
RunEvents.RunEnded.Remove(OnRunEnded); RunEvents.RunStarted.Remove(OnRunStarted);
|
||||
DeathFlow.Died.Remove(OnDied);
|
||||
DeathFlow.ReviveRequested.Remove(OnReviveRequested);
|
||||
DeathFlow.Revived.Remove(OnRevived);
|
||||
CombatEvents.Killed.Remove(OnKilled);
|
||||
}
|
||||
s_subscribed = on;
|
||||
return "구독=" + on + " (RunEnded/RunStarted)";
|
||||
return "구독=" + on + " (RunEnded/RunStarted + DeathFlow 3종 + Killed)";
|
||||
}
|
||||
|
||||
// ── 이벤트 ────────────────────────────────────────────────────────────
|
||||
private static void OnRunStarted(in RunStartedEvent e)
|
||||
{
|
||||
s_pending = false;
|
||||
s_eliteKills = 0; // WL-813q2 — 런마다 엘리트 집계 리셋
|
||||
if (s_open) Hide(); // 다음 런이 시작되면 결과 화면은 닫힌다
|
||||
RaiseToFront(); // 다음 런에서 재정렬이 남지 않게 되돌린다
|
||||
}
|
||||
|
||||
/// <summary>엘리트 처치 집계(🔴 813p RunResult 에 엘리트 필드가 없다 · 문자열 0 = GC 0).</summary>
|
||||
private static void OnKilled(in KilledEvent e)
|
||||
{
|
||||
if (e.subRole != eSubRol.Elite) return;
|
||||
EliteKilledSeen++;
|
||||
if (!RunDirector.IsRunning) return; // 런 밖 처치는 이번 런 값이 아니다
|
||||
s_eliteKills++;
|
||||
}
|
||||
|
||||
// ── WL-813q2(D-18) 사망/부활 ──────────────────────────────────────────
|
||||
/// <summary>사망 시작(813i `DeathFlow.Died`) — 런 밖이면 팝업을 생략하고 즉시 부활시킨다.</summary>
|
||||
private static void OnDied(in DeathEvent e)
|
||||
{
|
||||
DiedSeen++;
|
||||
HandleDeathStart("Died");
|
||||
}
|
||||
|
||||
/// <summary>부활 팝업 요청(813i `DeathFlow.ReviveRequested`) — Died 를 놓친 경우의 2차 방어.</summary>
|
||||
private static void OnReviveRequested(in ReviveRequestEvent e)
|
||||
{
|
||||
ReviveRequestSeen++;
|
||||
HandleDeathStart("ReviveRequested");
|
||||
}
|
||||
|
||||
/// <summary>런 밖 사망이면 즉시 부활 · 아니면 이미 떠 있는 결과 화면을 부활 뒤로 재정렬.</summary>
|
||||
private static string HandleDeathStart(string from)
|
||||
{
|
||||
var s = St;
|
||||
if (s == null || !WLRunUiSettings.Enabled || !s.resultEnabled) return "off";
|
||||
|
||||
// ② 런이 **끝난 뒤**(RunPhase.Ended = EndRun ~ 다음 StartRun 사이)의 사망은 런 밖이다 —
|
||||
// 부활을 물을 이유가 없으니 팝업을 생략하고 바로 살린다.
|
||||
// 🔴 Idle(런을 아직 시작도 안 한 로비 등)은 건드리지 않는다 = 813i 원래 흐름 그대로.
|
||||
if (s.skipRevivePopupAfterRunEnd && RunDirector.Phase == RunPhase.Ended)
|
||||
{
|
||||
bool ok = DeathFlow.Revive(); // 813i 공개 API(Dying = 연출 끝에 · AwaitingRevive = 즉시)
|
||||
if (ok) SkipPopupReviveCount++;
|
||||
LastDeathLog = from + " — 런 밖 사망 → 팝업 생략 즉시 부활(" + ok + ")";
|
||||
if (s.verboseLog) Debug.Log("[RunResultUI] " + LastDeathLog);
|
||||
return LastDeathLog;
|
||||
}
|
||||
|
||||
// ③ 그래도 겹치면(런 중 사망 + 결과 화면이 이미 떠 있음) 결과를 부활 뒤로 내린다.
|
||||
if (s_open && s.resultLowerWhileRevive) LowerBehindRevive();
|
||||
LastDeathLog = from + " — 런 중 사망(재정렬=" + s_lowered + ")";
|
||||
return LastDeathLog;
|
||||
}
|
||||
|
||||
/// <summary>부활 완료(813i `DeathFlow.Revived` · 워치독 자동 부활 포함) — 결과를 다시 앞으로.</summary>
|
||||
private static void OnRevived(in RevivedEvent e)
|
||||
{
|
||||
RevivedSeen++;
|
||||
RaiseToFront(); // 미뤄 둔 결과는 러너 Tick 이 곧 띄운다
|
||||
}
|
||||
|
||||
private static void OnRunEnded(in RunResult e)
|
||||
{
|
||||
EndedSeen++;
|
||||
s_eliteKillsAtEnd = s_eliteKills; // WL-813q2 — 결과 화면이 쓰는 값은 이 순간의 스냅샷
|
||||
CopyResult(in e); // 🔴 배열 3종을 즉시 자기 버퍼로 복사
|
||||
var s = St;
|
||||
if (s == null || !WLRunUiSettings.Enabled || !s.resultEnabled) return;
|
||||
|
|
@ -175,22 +279,98 @@ namespace WL.UI
|
|||
}
|
||||
|
||||
// ── 표시 ──────────────────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// WL-813q2(D-18) — 「부활 우선」 판정. **주 판정 = 813i `DeathFlow.State`**(Dying/AwaitingRevive),
|
||||
/// `ReviveDialog.Busy`(공용 팝업 경로 · 813q)는 보조. 둘 다 SO 로 끌 수 있다.
|
||||
/// </summary>
|
||||
public static bool ReviveBusy
|
||||
{
|
||||
get
|
||||
{
|
||||
var s = St;
|
||||
if (s == null) return false;
|
||||
if (s.resultDeferWhileDying && DeathFlow.State != DeathState.None) return true;
|
||||
if (s.resultDeferWhileRevive && ReviveDialog.Busy) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>「부활 우선」이 걸린 이유(실측 덤프용 · 표시 경로가 아니다).</summary>
|
||||
public static string ReviveBusyReason
|
||||
{
|
||||
get
|
||||
{
|
||||
var s = St;
|
||||
if (s == null) return "설정 없음";
|
||||
if (s.resultDeferWhileDying && DeathFlow.State != DeathState.None) return "DeathFlow." + DeathFlow.State;
|
||||
if (s.resultDeferWhileRevive && ReviveDialog.Busy) return "ReviveDialog.Busy";
|
||||
return "없음";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>부활 팝업이 비면 띄운다. 아직 떠 있으면 미뤄 둔다(러너가 다시 부른다).</summary>
|
||||
public static string TryShow(float now)
|
||||
{
|
||||
var s = St;
|
||||
if (s == null) return "WLRunUiSettings 에셋 없음";
|
||||
if (!s_pending) return "대기 중인 결과 없음";
|
||||
if (s.resultDeferWhileRevive && ReviveDialog.Busy)
|
||||
if (ReviveBusy)
|
||||
{
|
||||
DeferredCount++;
|
||||
LastShowLog = "부활 팝업 우선 — 결과 화면 대기";
|
||||
LastShowLog = "부활 우선 — 결과 화면 대기(" + ReviveBusyReason + ")";
|
||||
return LastShowLog;
|
||||
}
|
||||
s_pending = false;
|
||||
return Show();
|
||||
}
|
||||
|
||||
// ── WL-813q2(D-18) 재정렬 — 813z 부활 팝업(sortingOrder 60)보다 아래로 ───
|
||||
/// <summary>결과 화면을 부활 팝업 뒤로 내린다(sortingOrder 낮춤 + 형제 맨 앞).</summary>
|
||||
public static string LowerBehindRevive()
|
||||
{
|
||||
var s = St;
|
||||
if (s == null || s_root == null) return "노드 없음";
|
||||
if (s_lowered) return "이미 재정렬됨";
|
||||
s_lowered = true;
|
||||
LowerCount++;
|
||||
EnsureCanvas(s);
|
||||
if (s_canvas != null) s_canvas.sortingOrder = s.resultSortingOrderWhileRevive;
|
||||
s_root.SetAsFirstSibling();
|
||||
return "부활 뒤로 재정렬 sortingOrder=" + SortingOrder + " sibling=" + SiblingIndex;
|
||||
}
|
||||
|
||||
/// <summary>재정렬을 되돌린다(부활 완료·다음 런).</summary>
|
||||
public static string RaiseToFront()
|
||||
{
|
||||
var s = St;
|
||||
if (s == null || s_root == null) { s_lowered = false; return "노드 없음"; }
|
||||
if (!s_lowered) return "재정렬 없음";
|
||||
s_lowered = false;
|
||||
if (!s.resultRaiseAfterRevive) return "resultRaiseAfterRevive=0 — 되돌리지 않음";
|
||||
RaiseCount++;
|
||||
EnsureCanvas(s);
|
||||
if (s_canvas != null) s_canvas.sortingOrder = s.resultSortingOrder;
|
||||
s_root.SetAsLastSibling();
|
||||
return "앞으로 복귀 sortingOrder=" + SortingOrder + " sibling=" + SiblingIndex;
|
||||
}
|
||||
|
||||
/// <summary>전용 Canvas(overrideSorting) 보장 — 813z 부활 팝업과 같은 방식.</summary>
|
||||
private static void EnsureCanvas(WLRunUiSettings s)
|
||||
{
|
||||
if (s == null || s_root == null || !s.resultOwnCanvas) 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.overrideSorting) s_canvas.overrideSorting = true;
|
||||
if (s_raycaster == null)
|
||||
{
|
||||
s_raycaster = s_root.GetComponent<GraphicRaycaster>();
|
||||
if (s_raycaster == null) s_raycaster = s_root.gameObject.AddComponent<GraphicRaycaster>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>지금 바로 결과 화면을 띄운다(공용 팝업 스위치 → 런타임 패널).</summary>
|
||||
public static string Show()
|
||||
{
|
||||
|
|
@ -213,7 +393,11 @@ namespace WL.UI
|
|||
if (s_group != null) { s_group.alpha = 1f; s_group.blocksRaycasts = true; s_group.interactable = true; }
|
||||
if (s_root.gameObject.activeSelf == false) s_root.gameObject.SetActive(true);
|
||||
s_root.SetAsLastSibling(); // 형제 순서 = 그리는 순서(813tj 교훈) — HUD 위로
|
||||
LastShowLog = "런타임 패널 표시 outcome=" + s_result.outcome + " 행=" + VisibleRows;
|
||||
s_lowered = false;
|
||||
EnsureCanvas(s); // WL-813q2 — 부활 팝업(60)보다 아래 고정
|
||||
if (s_canvas != null) s_canvas.sortingOrder = s.resultSortingOrder;
|
||||
LastShowLog = "런타임 패널 표시 outcome=" + s_result.outcome + " 행=" + VisibleRows +
|
||||
" sortingOrder=" + SortingOrder;
|
||||
if (s.verboseLog) Debug.Log("[RunResultUI] " + LastShowLog);
|
||||
return LastShowLog;
|
||||
}
|
||||
|
|
@ -352,6 +536,7 @@ namespace WL.UI
|
|||
case WLRunResultRow.Level: return s.rowLevelLabel;
|
||||
case WLRunResultRow.LootByGrade: return s.rowLootLabel;
|
||||
case WLRunResultRow.Gold: return s.rowGoldLabel;
|
||||
case WLRunResultRow.EliteKills: return s.rowEliteKillsLabel;
|
||||
default: return s.rowDeathsLabel;
|
||||
}
|
||||
}
|
||||
|
|
@ -364,7 +549,7 @@ namespace WL.UI
|
|||
AppendTime(sb, s_result.totalSec, s);
|
||||
break;
|
||||
case WLRunResultRow.BossClear:
|
||||
AppendTime(sb, s_result.bossClearSec, s);
|
||||
AppendBossClear(sb, s); // WL-813q2 — 미처치면 "미처치 (HP 남음 N %)"
|
||||
break;
|
||||
case WLRunResultRow.Kills:
|
||||
AppendFormat2(sb, s.rowKillsFormat, s_result.kills, s_result.bossKills);
|
||||
|
|
@ -387,12 +572,37 @@ namespace WL.UI
|
|||
case WLRunResultRow.Gold:
|
||||
sb.Append(s_result.gold);
|
||||
break;
|
||||
case WLRunResultRow.EliteKills:
|
||||
sb.Append(s.resultCountElitesInUi ? s_eliteKillsAtEnd : 0);
|
||||
break;
|
||||
default:
|
||||
AppendFormat2(sb, s.rowDeathsFormat, s_result.deaths, s_result.potionsUsed);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WL-813q2 §1-3 — 보스 처치 행. 처치했으면 mm:ss, 아니면 "미처치".
|
||||
/// 🔴 813p `RunResult` 에는 **보스 HP 필드가 없다**(구조체 실측) → 남은 HP 는 813c `BossHpBar` 가
|
||||
/// 이번 런에 실제로 물고 있던 마지막 비율(UI 자체 실측)을 쓴다(`resultBossHpFromUi`).
|
||||
/// 바가 보스를 한 번도 물지 못했으면 퍼센트 없이 "미처치"만 쓴다.
|
||||
/// </summary>
|
||||
private static void AppendBossClear(StringBuilder sb, WLRunUiSettings s)
|
||||
{
|
||||
if (s_result.bossClearSec >= 0f) { AppendTime(sb, s_result.bossClearSec, s); return; }
|
||||
|
||||
float ratio = s.resultBossHpFromUi ? BossHpBar.RunBossHpRatio : -1f;
|
||||
if (ratio >= 0f && !string.IsNullOrEmpty(s.resultBossNotClearedHpFormat))
|
||||
{
|
||||
// 0 % 는 "죽었는데 처치 기록이 없는" 경우라 올림하지 않는다(살아 있으면 최소 1 %).
|
||||
int pct = (int)System.Math.Round(Mathf.Clamp01(ratio) * 100f, System.MidpointRounding.AwayFromZero);
|
||||
if (pct <= 0 && ratio > 0f) pct = 1;
|
||||
AppendFormat2(sb, s.resultBossNotClearedHpFormat, pct, 0);
|
||||
return;
|
||||
}
|
||||
sb.Append(string.IsNullOrEmpty(s.resultBossNotClearedText) ? s.resultDashText : s.resultBossNotClearedText);
|
||||
}
|
||||
|
||||
/// <summary>초 → mm:ss (음수 = 미발생 → 대시 문구).</summary>
|
||||
private static void AppendTime(StringBuilder sb, float sec, WLRunUiSettings s)
|
||||
{
|
||||
|
|
@ -475,9 +685,12 @@ namespace WL.UI
|
|||
s_closeButton = NewButton(s_box, "btn_close", out s_closeBg, out s_closeLabel, OnCloseListener);
|
||||
}
|
||||
EnsureRows(s);
|
||||
EnsureCanvas(s); // WL-813q2 — 전용 Canvas(overrideSorting)
|
||||
if (s_canvas != null) s_canvas.sortingOrder = s.resultSortingOrder;
|
||||
s_lowered = false;
|
||||
string layout = ApplyLayout();
|
||||
Hide();
|
||||
return "연결 " + s.resultParentPath + " · " + layout;
|
||||
return "연결 " + s.resultParentPath + " · " + layout + " · sortingOrder=" + SortingOrder;
|
||||
}
|
||||
|
||||
private static Button NewButton(RectTransform parent, string name, out Image bg, out TextMeshProUGUI label,
|
||||
|
|
@ -636,6 +849,10 @@ namespace WL.UI
|
|||
{
|
||||
EndedSeen = ShownCount = DeferredCount = NextRunCount = CloseCount = SkipAbandonCount = 0;
|
||||
BufferGrowCount = 0;
|
||||
DiedSeen = ReviveRequestSeen = RevivedSeen = SkipPopupReviveCount = 0;
|
||||
LowerCount = RaiseCount = EliteKilledSeen = 0;
|
||||
s_eliteKills = s_eliteKillsAtEnd = 0;
|
||||
s_lowered = false; LastDeathLog = "";
|
||||
s_pending = false; UsedCommonPopup = false; LastShowLog = "";
|
||||
Hide();
|
||||
return "상태 초기화";
|
||||
|
|
@ -649,7 +866,7 @@ namespace WL.UI
|
|||
if (Application.isPlaying) Object.Destroy(s_root.gameObject);
|
||||
else Object.DestroyImmediate(s_root.gameObject);
|
||||
}
|
||||
s_root = null; s_group = null; s_dim = null; s_box = null; s_boxBg = null;
|
||||
s_root = null; s_canvas = null; s_raycaster = null; s_group = null; s_dim = null; s_box = null; s_boxBg = null;
|
||||
s_title = null; s_rowRoot = null; s_nextButton = null; s_closeButton = null;
|
||||
s_nextLabel = null; s_closeLabel = null; s_nextBg = null; s_closeBg = null;
|
||||
s_row = new RectTransform[0]; s_rowLabel = new TextMeshProUGUI[0]; s_rowValue = new TextMeshProUGUI[0];
|
||||
|
|
@ -668,6 +885,15 @@ namespace WL.UI
|
|||
" abandon생략=" + SkipAbandonCount + " 버퍼증설=" + BufferGrowCount +
|
||||
" 공용팝업=" + UsedCommonPopup);
|
||||
sb.AppendLine(" 제목 \"" + TitleText + "\" · 행 " + VisibleRows + "개 · 부활팝업Busy=" + ReviveDialog.Busy);
|
||||
sb.AppendLine(" [813q2] 부활우선=" + ReviveBusy + "(" + ReviveBusyReason + ") · DeathFlow=" + DeathFlow.State +
|
||||
" 런=" + RunDirector.Phase + "(running=" + RunDirector.IsRunning + ") · died=" + DiedSeen + " request=" + ReviveRequestSeen +
|
||||
" revived=" + RevivedSeen + " 팝업생략부활=" + SkipPopupReviveCount +
|
||||
" · 재정렬=" + s_lowered + "(내림 " + LowerCount + "/복귀 " + RaiseCount + ")" +
|
||||
" sortingOrder=" + SortingOrder + " sibling=" + SiblingIndex);
|
||||
sb.AppendLine(" [813q2] 엘리트 런중=" + s_eliteKills + " 종료스냅=" + s_eliteKillsAtEnd +
|
||||
" 전체본것=" + EliteKilledSeen + " · 보스HP(UI 실측)=" +
|
||||
(BossHpBar.RunBossHpRatio >= 0f ? (BossHpBar.RunBossHpRatio * 100f).ToString("F1") + "%" : "없음") +
|
||||
" · 마지막 사망 로그 = " + LastDeathLog);
|
||||
if (s != null && s.resultRows != null)
|
||||
for (int i = 0; i < s.resultRows.Length; i++)
|
||||
{
|
||||
|
|
@ -691,7 +917,11 @@ namespace WL.UI
|
|||
sb.AppendLine(" 설정 enabled=" + s.enabled_ + " result=" + s.resultEnabled + " 공용팝업=" + s.resultUseCommonPopup +
|
||||
" abandon표시=" + s.resultShowOnAbandon + " 부활우선=" + s.resultDeferWhileRevive +
|
||||
" box=" + s.resultBoxSizePx + " 행수=" + (s.resultRows != null ? s.resultRows.Length : 0) +
|
||||
" 합산피해=" + s.resultMaxDamageUseBurst);
|
||||
" 합산피해=" + s.resultMaxDamageUseBurst +
|
||||
" · [813q2] 사망중미룸=" + s.resultDeferWhileDying + " 런밖즉시부활=" + s.skipRevivePopupAfterRunEnd +
|
||||
" 전용캔버스=" + s.resultOwnCanvas + "(" + s.resultSortingOrder + "→" + s.resultSortingOrderWhileRevive + ")" +
|
||||
" 재정렬=" + s.resultLowerWhileRevive + "/" + s.resultRaiseAfterRevive +
|
||||
" 엘리트UI집계=" + s.resultCountElitesInUi + " 보스HP UI=" + s.resultBossHpFromUi);
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,13 @@ MonoBehaviour:
|
|||
resultUseCommonPopup: 0
|
||||
resultShowOnAbandon: 0
|
||||
resultDeferWhileRevive: 1
|
||||
resultDeferWhileDying: 1
|
||||
skipRevivePopupAfterRunEnd: 1
|
||||
resultOwnCanvas: 1
|
||||
resultSortingOrder: 40
|
||||
resultSortingOrderWhileRevive: 20
|
||||
resultLowerWhileRevive: 1
|
||||
resultRaiseAfterRevive: 1
|
||||
resultWinTitle: "\uB7F0 \uD074\uB9AC\uC5B4!"
|
||||
resultTimeoutTitle: "\uC2DC\uAC04 \uC885\uB8CC"
|
||||
resultAbandonTitle: "\uB7F0 \uC911\uB2E8"
|
||||
|
|
@ -91,6 +98,7 @@ MonoBehaviour:
|
|||
- 0
|
||||
- 1
|
||||
- 2
|
||||
- 9
|
||||
- 3
|
||||
- 4
|
||||
- 5
|
||||
|
|
@ -106,6 +114,7 @@ MonoBehaviour:
|
|||
rowLootLabel: "\uD68D\uB4DD"
|
||||
rowGoldLabel: "\uACE8\uB4DC"
|
||||
rowDeathsLabel: "\uC0AC\uB9DD / \uBB3C\uC57D"
|
||||
rowEliteKillsLabel: "\uC5D8\uB9AC\uD2B8 \uCC98\uCE58"
|
||||
rowKillsFormat: "{0} (\uBCF4\uC2A4 {1})"
|
||||
rowLevelFormat: "{0} \u2192 {1}"
|
||||
rowDeathsFormat: '{0} / {1}'
|
||||
|
|
@ -114,6 +123,10 @@ MonoBehaviour:
|
|||
rowLootEmptyText: '0'
|
||||
resultMaxDamageUseBurst: 1
|
||||
resultHideLevelWhenZero: 1
|
||||
resultBossNotClearedText: "미처치"
|
||||
resultBossNotClearedHpFormat: "미처치 (HP 남음 {0} %)"
|
||||
resultBossHpFromUi: 1
|
||||
resultCountElitesInUi: 1
|
||||
resultNextRunText: "\uB2E4\uC74C \uB7F0"
|
||||
resultCloseText: "\uB2EB\uAE30"
|
||||
resultButtonSizePx: {x: 340, y: 104}
|
||||
|
|
@ -124,3 +137,7 @@ MonoBehaviour:
|
|||
resultNextLabelColor: {r: 0.04, g: 0.09, b: 0.06, a: 1}
|
||||
resultCloseBgColor: {r: 0.16, g: 0.18, b: 0.22, a: 1}
|
||||
resultCloseLabelColor: {r: 0.9, g: 0.92, b: 0.95, a: 1}
|
||||
timerResetColorOnRunStart: 1
|
||||
bossResetOnRunStart: 1
|
||||
bossBarRebindOnRunStart: 1
|
||||
bossBannerRetriggerOnRunStart: 0
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@ namespace WL.UI
|
|||
Gold = 7,
|
||||
/// <summary>사망 / 물약.</summary>
|
||||
DeathsPotions = 8,
|
||||
/// <summary>엘리트 처치 수(WL-813q2 · 813p RunResult 에 없어 UI 가 직접 센다).</summary>
|
||||
EliteKills = 9,
|
||||
}
|
||||
|
||||
[CreateAssetMenu(fileName = "WLRunUiSettings", menuName = "WL/Run UI Settings", order = 33)]
|
||||
|
|
@ -226,6 +228,34 @@ namespace WL.UI
|
|||
[Tooltip("부활 팝업(813i/813tj)이 떠 있거나 예약 중이면 결과 화면을 미뤘다가 닫힌 뒤 띄운다.")]
|
||||
public bool resultDeferWhileRevive = true;
|
||||
|
||||
// ── D-18 (WL-813q2) ──────────────────────────────────────────────────
|
||||
// Q5 캡처 `PD2_07` 실측: 결과 화면("시간 종료")이 **먼저** 뜬 뒤 사망 → 부활 팝업이 그 위에 겹쳐
|
||||
// 9행 중 4행을 가린다. 813q 는 `ReviveDialog.Busy` 만 봤는데 RunEnded 시점에는 아직 아무도
|
||||
// 죽지 않아 Busy=false 였다 → 미룸이 걸리지 않는다. 아래 3개가 그 구멍을 막는다.
|
||||
[Header("결과 화면 — 부활 겹침 (WL-813q2 · D-18)")]
|
||||
[Tooltip("813i DeathFlow 상태(Dying/AwaitingRevive)를 **주 판정**으로 쓴다. " +
|
||||
"ReviveDialog.Busy(공용 팝업 경로)는 보조로 남는다. 끄면 813q 동작(Busy 만).")]
|
||||
public bool resultDeferWhileDying = true;
|
||||
|
||||
[Tooltip("런이 끝난 뒤(RunDirector.Phase == Ended · EndRun ~ 다음 StartRun 사이) 죽으면 부활 팝업을 " +
|
||||
"생략하고 즉시 부활한다(813i DeathFlow.Revive() 공개 API). Idle(로비)은 건드리지 않는다.")]
|
||||
public bool skipRevivePopupAfterRunEnd = true;
|
||||
|
||||
[Tooltip("결과 화면에 전용 Canvas(overrideSorting)를 붙인다. 끄면 형제 순서만으로 겹친다(813q 동작).")]
|
||||
public bool resultOwnCanvas = true;
|
||||
|
||||
[Tooltip("결과 화면 평상시 sortingOrder. 813z 부활 팝업(reviveSortingOrder=60)보다 **아래**여야 한다.")]
|
||||
public int resultSortingOrder = 40;
|
||||
|
||||
[Tooltip("부활 진행 중 결과 화면 sortingOrder(더 아래로 재정렬).")]
|
||||
public int resultSortingOrderWhileRevive = 20;
|
||||
|
||||
[Tooltip("결과 화면이 이미 떠 있는데 사망하면 부활 뒤로 재정렬한다(sortingOrder 낮춤 + SetAsFirstSibling).")]
|
||||
public bool resultLowerWhileRevive = true;
|
||||
|
||||
[Tooltip("부활이 끝나면 결과 화면을 다시 앞으로 되돌린다(sortingOrder 복귀 + SetAsLastSibling).")]
|
||||
public bool resultRaiseAfterRevive = true;
|
||||
|
||||
[Tooltip("승리 제목.")]
|
||||
public string resultWinTitle = "런 클리어!";
|
||||
|
||||
|
|
@ -315,6 +345,7 @@ namespace WL.UI
|
|||
public string rowLootLabel = "획득";
|
||||
public string rowGoldLabel = "골드";
|
||||
public string rowDeathsLabel = "사망 / 물약";
|
||||
public string rowEliteKillsLabel = "엘리트 처치";
|
||||
|
||||
[Tooltip("처치 수 값 문구 — {0} = 전체 처치, {1} = 그중 보스.")]
|
||||
public string rowKillsFormat = "{0} (보스 {1})";
|
||||
|
|
@ -340,6 +371,22 @@ namespace WL.UI
|
|||
[Tooltip("레벨 행은 levelFrom·levelTo 가 둘 다 0 이면(에디트 모드 등) 감춘다.")]
|
||||
public bool resultHideLevelWhenZero = true;
|
||||
|
||||
// ── 행 보강 (WL-813q2 §1-3) ───────────────────────────────────────────
|
||||
[Header("결과 화면 — 행 보강 (WL-813q2)")]
|
||||
[Tooltip("보스 미처치 행 문구(HP 를 모를 때). 비우면 resultDashText(—)를 그대로 쓴다.")]
|
||||
public string resultBossNotClearedText = "미처치";
|
||||
|
||||
[Tooltip("보스 미처치 + 남은 HP 를 아는 경우의 문구 — {0} = 남은 HP 퍼센트(정수).")]
|
||||
public string resultBossNotClearedHpFormat = "미처치 (HP 남음 {0} %)";
|
||||
|
||||
[Tooltip("🔴 813p RunResult 에는 보스 HP 필드가 없다(실측). 켜면 813c 보스 HP 바가 이번 런에 " +
|
||||
"실제로 물었던 마지막 비율을 쓴다(UI 자체 실측). 끄면 남은 HP 를 쓰지 않는다.")]
|
||||
public bool resultBossHpFromUi = true;
|
||||
|
||||
[Tooltip("🔴 813p RunResult 에는 엘리트 집계가 없다(실측). 켜면 UI 가 CombatEvents.Killed 의 " +
|
||||
"subRole==Elite 를 런 중에 직접 센다. 끄면 엘리트 행은 0 으로 나온다.")]
|
||||
public bool resultCountElitesInUi = true;
|
||||
|
||||
[Header("결과 화면 — 버튼")]
|
||||
[Tooltip("\"다음 런\" 버튼 문구 — 누르면 RunDirector.Restart() 하나만 부른다(813p 공개 API).")]
|
||||
public string resultNextRunText = "다음 런";
|
||||
|
|
@ -371,6 +418,23 @@ namespace WL.UI
|
|||
[Tooltip("\"닫기\" 버튼 글자 색.")]
|
||||
public Color resultCloseLabelColor = new Color(0.90f, 0.92f, 0.95f, 1f);
|
||||
|
||||
// ── 다음 런 리셋 (WL-813q2 · D-23) ────────────────────────────────────
|
||||
// Q5 `R3_30_after_next` 실측: 「다음 런」 뒤 이전 런의 보스 HP 바가 남고 타이머가 붉게 시작한다.
|
||||
// 타이머는 RunStarted 에서 s_shownWarn 을 false 로 되돌리지만 **색을 다시 쓰지 않아서**
|
||||
// (warn != s_shownWarn 이 false → 색 대입 생략) 이전 런의 경고색이 그대로 남는다.
|
||||
[Header("다음 런 리셋 (WL-813q2 · D-23)")]
|
||||
[Tooltip("RunStarted 마다 타이머 색을 기본색으로 되돌린다(경고색 잔존 버그).")]
|
||||
public bool timerResetColorOnRunStart = true;
|
||||
|
||||
[Tooltip("RunStarted 마다 보스 HP 바·등장 배너 상태를 리셋한다(이전 런의 바 잔존).")]
|
||||
public bool bossResetOnRunStart = true;
|
||||
|
||||
[Tooltip("리셋 뒤 살아 있는 보스가 있으면 보스 HP 바를 다시 물린다(813y ⑤ 늦은 구독 경로 재사용).")]
|
||||
public bool bossBarRebindOnRunStart = true;
|
||||
|
||||
[Tooltip("리셋 뒤 살아 있는 보스가 있으면 등장 배너도 다시 띄운다(기본 끔 — 새 런 시작에 배너는 요란하다).")]
|
||||
public bool bossBannerRetriggerOnRunStart = false;
|
||||
|
||||
// ── 파생 헬퍼 (코드 상수 0) ───────────────────────────────────────────
|
||||
/// <summary>px → 캔버스 유닛 계수. 813c 의 산식 하나만 쓴다(중복 0).</summary>
|
||||
public float UnitsPerPx(Canvas canvas)
|
||||
|
|
|
|||
Loading…
Reference in New Issue