[WL-815d] 스테이지 UI: HUD 최소화 · 클리어/실패 화면 · 장벽 훅 · 자동 입장 · 로비 최소안 (#815)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
d8174b77a5
commit
e94e230b3b
|
|
@ -0,0 +1,210 @@
|
|||
# WL815d Probe 2026-09-11 01:46:18 (edit mode · Play 0 · 로그인 0)
|
||||
playMode=False
|
||||
|
||||
── ⓪ 에셋 · SO 값 (전부 C45)
|
||||
원본 프리팹 기준선: Assets/Res_Addr/MainUI/NewGameUI.prefab 5361935B · 2026-09-08 17:05:53Z
|
||||
[PASS] Resources.Load("WL/WLStageUiSettings") — NEW 에셋
|
||||
[PASS] WLStageUiSettings.Enabled = True
|
||||
HUD: stageLabel="스테이지 {0}" enemy="남은 적 {0} / {1}" warn=10s countdown="준비" 존게이지숨김=True
|
||||
메뉴: hide=True 목록 11개 · 유지 표기 7개 · 복원=True · 룩토글유지=True
|
||||
결과: 제목="스테이지 {0} 클리어!" 행 6개 [ClearTime · Kills · EliteKills · MaxChain · HitsPotions · Retries] · 버튼 [다음 스테이지] [로비로] · 배지="무피격 클리어!"
|
||||
실패: [다시 도전] [로비로] · 사망="쓰러졌습니다. / 다시 도전하시겠습니까?" · 시간초과="시간 초과! / 남은 적 {0}마리" · 결과창억제=True
|
||||
로비: lobbyMode=ReturnToLobby titleScene="Title"
|
||||
[PASS] 타이머 경고 = 마지막 10 s(PD 60 s 규칙) — 실측 10
|
||||
[PASS] 815b WLStageSettings 존재
|
||||
[PASS] F-27 autoEnterOnMapEnter = 1 (맵 진입 = 스테이지 1) — 실측 True
|
||||
[PASS] bindReviveOkToRetry = 0 (815d 2버튼 팝업이 대신한다) — 실측 False
|
||||
[PASS] toLobbyCallsReturnToLobby = 1 (로비 ⓐ안) — 실측 True
|
||||
[PASS] 장벽 훅 사용 on/off = True/True
|
||||
기타: mirrorRunEnded=True mirrorRunHudEvents=True clearHoldScale=0 wrapToFirstStage=False
|
||||
[PASS] 자동 부활 0 (reviveWatchdogSeconds) — 실측 0
|
||||
815c 장벽: style=Props(0 None/1 Ring/2 Props/3 Fire) enabled=True showVisual=True blockEnabled=True
|
||||
[PASS] F-26 style = 2(Props · 울타리) — 실측 2
|
||||
|
||||
── ① 장벽 훅 — StageBarrierHook.Set/Clear = StageBarrier.Set/Clear
|
||||
장벽 훅 연결 — Set/Clear = WL.Combat.Stage.StageBarrier (bind 1회)
|
||||
[PASS] StageBarrierHook.Set 대입됨
|
||||
[PASS] StageBarrierHook.Clear 대입됨
|
||||
[PASS] 훅 Set 호출 1 — 실측 1
|
||||
[PASS] 815c StageBarrier.Set 실제 실행 +1 — 실측 1
|
||||
[PASS] 장벽 활성 = True 중심 (115.0, 0.4, -22.0) r=14.0
|
||||
[PASS] 815c StageBarrier.Clear 실제 실행 +1 — 실측 1
|
||||
[PASS] 장벽 해제 = True
|
||||
[PASS] 훅 예외 0 — 실측 0
|
||||
StageDirector.Enter(0) = True · 상태 Entering · 고정 수 8 · 제한 60s
|
||||
[PASS] 입장 시 훅 Set 호출 ≥1 — 실측 1 중심 (115.0, 0.4, -22.0) r=14.0
|
||||
[PASS] 이탈 시 훅 Clear 호출 ≥1 — 실측 1
|
||||
장벽 훅 해제 — no-op 복귀(unbind 1회)
|
||||
[PASS] Unbind 뒤 815c 실행 0 = no-op(C8 ①)
|
||||
장벽 훅 연결 — Set/Clear = WL.Combat.Stage.StageBarrier (bind 2회)
|
||||
[StageBarrierBinding] bound=True hasSet=True hasClear=True · 훅 호출 set=2 clear=1 faults=0 · 마지막 (115.0, 0.4, -22.0) r=14.0 · 815c 실행 set=2 clear=2 active=False featureEnabled=True
|
||||
|
||||
── ② 노드 — NewGameUI 프리팹(저장 0)에 HUD·결과·실패 팝업 조립
|
||||
[PASS] LoadPrefabContents(Assets/Res_Addr/MainUI/NewGameUI.prefab)
|
||||
Bind HUD : 연결 IngameUIs/WL_HUD · pos(0.00, -483.56) size(995.56, 167.11) 칸=0 스테이지모드=False 존게이지=True 남은적=False unitsPerPx=1.7778
|
||||
Bind 결과 : 연결 IngameUIs/WL_HUD · box size(79.63, 100.00) 행틀=11 스테이지모드=False 버튼=[다음 런](True) [닫기] 배지=False unitsPerPx=0.0926 · sortingOrder=-1
|
||||
[PASS] 813q 노드 조립(WL_RunHud · WL_RunResult · HideFlags.DontSave)
|
||||
[PASS] StageUiBridge 구독 on
|
||||
[PASS] StageEvents 구독 배선 — Entered 1 Countdown 1 Started 1 Enemy 1 Time 1 Cleared 1 Failed 1 Exited 1
|
||||
[PASS] 813z ReviveDialog 컴포넌트 발견(IngameUIs/WL_HUD/WL_ReviveDialog)
|
||||
ReviveDialog init: initialized · 폴백 팝업 box size(1351.11, 675.56) 버튼=1 [부활] unitsPerPx=1.7778
|
||||
[PASS] ReviveDialog.Active 세팅
|
||||
|
||||
── ③ 상단 3요소 — 스테이지 N · 남은 시간 mm:ss · 남은 적 M/T (+ 카운트다운)
|
||||
[PASS] StageEntered 수신 1
|
||||
[PASS] RunHud 스테이지 모드 on
|
||||
[PASS] ① 단계 = "스테이지 1" — 실측 "스테이지 1"
|
||||
[PASS] ② 타이머 = 01:00(제한 60 s) — 실측 "01:00"
|
||||
[PASS] ③ 남은 적 = "남은 적 8 / 8" — 실측 "남은 적 8 / 8"
|
||||
[PASS] 존 게이지 접힘(기준서 §F-2) — 실측 활성=False
|
||||
[PASS] 카운트다운 = "준비" 표시 — 실측 보임=True "준비"
|
||||
[PASS] 전투 시작 = 카운트다운 줄 접힘 — 실측 False
|
||||
[PASS] 처치 3 → "남은 적 5 / 8" — 실측 "남은 적 5 / 8"
|
||||
[PASS] 42 s → 00:42 · 평상색 — 실측 "00:42" warn=False
|
||||
[PASS] 마지막 10 s → 붉은색(경고) — 실측 "00:10" warn=True color=#FF4A47
|
||||
[PASS] 남은 적 2 → 강조색 — 실측 "남은 적 2 / 8"
|
||||
[PASS] 병행 RunEvents 2건 무시 — 실측 2
|
||||
[PASS] 🔴 병행 발행이 상단 3요소를 덮지 않는다 — "스테이지 1" "00:10" "남은 적 2 / 8"
|
||||
HUD 배치: pos(0.00, -483.56) size(995.56, 245.33) 칸=0 스테이지모드=True 존게이지=False 남은적=True unitsPerPx=1.7778
|
||||
[RunHud] 구독=True bound=True visible=True · started=1 tick=1 zoneCleared=0 gate=0 boss=0 ended=0
|
||||
타이머 "00:10" (남은 9.4s · 경고=True · 쓰기 3회) · 단계 "스테이지 1" (쓰기 0회)
|
||||
[813q2] 타이머 색 = FF4A47 (색 쓰기 2회 · 다음런리셋대기=False · 기본 F2F5FA / 경고 FF4A47)
|
||||
존 칸 0 (fill 쓰기 0회):
|
||||
노드 pos(0.00, -483.56) size(995.56, 245.33) active=True 부모=WL_HUD SafeArea=True
|
||||
[815d] 스테이지모드=True 단계="스테이지 1"(쓰기 1) 남은적="남은 적 2 / 8"(쓰기 3) 알림="준비"(보임 False · 쓰기 2) · 무시한 RunEvents=2 · 존게이지활성=False · 경고초=10s
|
||||
edge = Font 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
|
||||
|
||||
|
||||
── ④ 불필요 메뉴 숨김 · 복원 (§J ① · 원본 프리팹 diff 0)
|
||||
[PASS] 숨김 목록 11개 전부 실존(경로 해석 성공) — 못 찾음 0
|
||||
숨김: 메뉴 숨김 — 경로 11개 중 찾음 11 · 실제로 끈 것 11 · 못 찾음 0
|
||||
[PASS] 원본이 다시 켠 3건을 재보장으로 도로 끔 — 실측 3건 · 남은 on 0 (주기 0.5s)
|
||||
[PASS] 이미 꺼져 있으면 쓰기 0회(GC 0)
|
||||
복원: 메뉴 복원 11건
|
||||
| # | 경로(NewGameUI 루트 기준) | 실제 노드 경로 | 프리팹 저장 | 스테이지 중 | 이탈 뒤 |
|
||||
|---|---|---|---|---|---|
|
||||
| 0 | `Common/GuideQuest` | `WL_SafeArea_Back/Common/GuideQuest` | off | **off** | **on** |
|
||||
| 1 | `Common/TopRightBtns` | `WL_SafeArea_Back/Common/TopRightBtns` | off | **off** | **on** |
|
||||
| 2 | `Common/TopRightMoney` | `WL_SafeArea_Back/Common/TopRightMoney` | off | **off** | **on** |
|
||||
| 3 | `Common/Lv` | `WL_SafeArea_Back/Common/Lv` | off | **off** | **on** |
|
||||
| 4 | `IngameUIs/WL_HUD/HUD_Currency` | `IngameUIs/WL_HUD/HUD_Currency` | off | **off** | **on** |
|
||||
| 5 | `IngameUIs/WL_HUD/HUD_Level` | `IngameUIs/WL_HUD/HUD_Level` | off | **off** | **on** |
|
||||
| 6 | `IngameUIs/WL_HUD/HUD_BottomMenu` | `IngameUIs/WL_HUD/HUD_BottomMenu` | off | **off** | **on** |
|
||||
| 7 | `IngameUIs/Stage/exit` | `IngameUIs/Stage/exit` | off | **off** | **on** |
|
||||
| 8 | `IngameUIs/Stage/btn_setting` | `IngameUIs/Stage/btn_setting` | off | **off** | **on** |
|
||||
| 9 | `IngameUIs/MonsterSimpleInfo` | `IngameUIs/MonsterSimpleInfo` | off | **off** | **on** |
|
||||
| 10 | `IngameUIs/IngamePetCard` | `IngameUIs/IngamePetCard` | off | **off** | **on** |
|
||||
[PASS] 스테이지 중 전부 SetActive(false) — 11/11
|
||||
[PASS] 이탈 시 전부 켜진 값으로 복원 — 11/11
|
||||
(유지) IngameUIs/BattleUI activeSelf=True
|
||||
(유지) Common/MyInfoUI activeSelf=True
|
||||
(유지) IngameUIs/WL_HUD/WL_BossHpBar activeSelf=True
|
||||
(유지) IngameUIs/WL_HUD/WL_KillChainText activeSelf=True
|
||||
(유지) IngameUIs/WL_HUD/WL_LootToast activeSelf=True
|
||||
(유지) IngameUIs/WL_HUD/WL_HitVignette activeSelf=True
|
||||
(유지) IngameUIs/WL_HUD/WL_ReviveDialog activeSelf=True
|
||||
[PASS] 유지 목록 7개 전부 실존 — 없음 0
|
||||
유지 확인 7개 (조이스틱·스킬 패드 BattleUI · HP/MP MyInfoUI · 813 WL HUD 요소)
|
||||
[PASS] 원본 NewGameUI.prefab 파일 무변경 — 길이 5361935B(=5361935) 수정시각 17:05:53(=17:05:53)
|
||||
(참고) 미리보기 사본 dirty=True — 런타임 노드는 HideFlags.DontSave 라 저장해도 굽히지 않는다(813y 규약)
|
||||
|
||||
── ⑤ 클리어 화면 — StageCleared → 813q RunResultUI (기준서 §F-1)
|
||||
[PASS] Damaged(victim=null)는 피격으로 세지 않는다 — 실측 0
|
||||
[PASS] 피격 3회 집계 — 실측 3
|
||||
[PASS] 최대 연쇄 = 5(3 은 갱신 안 함) — 실측 5
|
||||
[PASS] StageCleared 수신 1
|
||||
[PASS] 결과 화면 표시 — 실측 open=True
|
||||
[PASS] 결과창 스테이지 모드 on
|
||||
[PASS] 제목 = "스테이지 1 클리어!" — 실측 "스테이지 1 클리어!"
|
||||
본문 =
|
||||
스테이지 1 클리어!
|
||||
클리어 시간 : 00:35
|
||||
처치 : 8 / 8
|
||||
엘리트 처치 : 0
|
||||
최대 연쇄 : 5
|
||||
피격 / 물약 : 3회 / 0개
|
||||
재도전 : 1회
|
||||
[PASS] 행 6개 — 실측 6
|
||||
[PASS] 피격 3회 → 무피격 배지 없음 — 실측 False
|
||||
배치: box size(860.00, 1080.00) 행틀=11 스테이지모드=True 버튼=[다음 스테이지](True) [로비로] 배지=False unitsPerPx=1.0000
|
||||
[PASS] 🔴 병행 RunEnded(Win) 로 결과창 중복 표시 0 — 실측 +0 · 억제 1회
|
||||
[PASS] 제목이 「런 클리어!」로 덮이지 않았다 — "스테이지 1 클리어!"
|
||||
[PASS] 피격 0회 → 무피격 배지 — 실측 True "무피격 클리어!"
|
||||
[PASS] 마지막 스테이지 제목 — 실측 "전 스테이지 클리어!"
|
||||
마지막 스테이지 배치: box size(860.00, 1080.00) 행틀=11 스테이지모드=True 버튼=[처음부터](True) [로비로] 배지=True unitsPerPx=1.0000
|
||||
본문(보스 스테이지) =
|
||||
전 스테이지 클리어!
|
||||
클리어 시간 : 00:55
|
||||
처치 : 15 / 15
|
||||
엘리트 처치 : 0
|
||||
최대 연쇄 : 0
|
||||
피격 / 물약 : 0회 / 0개
|
||||
재도전 : 2회
|
||||
무피격 클리어!
|
||||
|
||||
── ⑥ 실패 팝업 — StageFailed → 813z ReviveDialog 2버튼 (기준서 §E)
|
||||
[PASS] StageFailed 수신 1
|
||||
[PASS] 실패 팝업 표시 — 실측 open=True
|
||||
[PASS] ReviveDialog 스테이지 모드 on
|
||||
[PASS] 공용 Popup 미사용(2버튼 불가) — 실측 False
|
||||
배치: 폴백 팝업 box size(1351.11, 675.56) 버튼=2 [다시 도전] [로비로] unitsPerPx=1.7778
|
||||
[PASS] 두 번째 버튼 노출 — 실측 True
|
||||
[PASS] 버튼 = [다시 도전] [로비로] — 실측 [다시 도전] [로비로]
|
||||
[PASS] 시간 초과 문구 + 남은 적 3 — 실측 "시간 초과! / 남은 적 3마리"
|
||||
[PASS] 813z 3층 방어 유지 — SO sortingOrder 60 · 최상단 재보장 2회 · 실측 게터=-1(부모 계층 비활성 → Unity 가 overrideSorting 되돌림 · 814d §4)
|
||||
[PASS] 실패 시점에 결과창 0
|
||||
[PASS] 🔴 병행 RunEnded(Timeout) 억제 → 결과창 안 뜸 — 실측 open=False 억제 1회
|
||||
[PASS] 사망 문구 구분 — 실측 "쓰러졌습니다. / 다시 도전하시겠습니까?"
|
||||
[PASS] 사망 실패도 2버튼 모드 — 실측 True
|
||||
클릭 [다시 도전] : 다시 도전 — StageDirector.Retry() 전달
|
||||
[PASS] [다시 도전] → StageDirector.Retry() 1회 — 실측 +1
|
||||
[PASS] 누르면 팝업 닫힘 — 실측 open=False
|
||||
클릭 [로비로] : 로비로 — 전달
|
||||
[PASS] [로비로] → StageDirector.ToLobby() 1회 — 실측 +1
|
||||
[ReviveDialog] deaths=0 shows=3 confirms=1 대기중=False 공용팝업사용=False 폴백표시=False open=False 813i(ReviveRequested)=False 브리지=False
|
||||
본문="쓰러졌습니다. / 다시 도전하시겠습니까?"
|
||||
[815d] 스테이지모드=False 버튼=[부활] (두번째 없음) · 핸들러 primary=True secondary=True · 클릭 다시도전=1 로비=1 · 표시 3회
|
||||
공용 Popup.Ins=없음(에디트 모드/씬 미로드)
|
||||
최상단 = sortingOrder -1(SO 60) · 맨뒤형제=True · 재보장=6회 · 탈취감지=0 · 승격=False · 공용팝업닫음=0 · raycaster=True · blocksRaycasts=False
|
||||
워치독 = 0.0s · 무시disable=True · 최상단ON=True 주기=0.50s · DeathFlow 발동=0회 대기=0.00s
|
||||
폴백 box size(1351.11, 675.56) pos(0.00, 0.00) ok="부활" okName=WL_btn_ok
|
||||
설정 enabled=True 공용팝업=True delay=2s key=0 비용="무료"
|
||||
|
||||
|
||||
── ⑦ 클리어 화면 버튼 → StageDirector.Next() / ToLobby()
|
||||
클릭 [다음 스테이지] : 다음 스테이지 — StageDirector 호출
|
||||
[PASS] [다음 스테이지] → StageDirector.Next() 1회 — 실측 +1
|
||||
[PASS] 누르면 결과창 닫힘
|
||||
클릭 [로비로] : 로비로 — StageDirector 호출
|
||||
[PASS] [로비로] → StageDirector.ToLobby() 1회 — 실측 +1
|
||||
로비 동작: 로비로 — ReturnToLobby (toLobbyCallsReturnToLobby=1)
|
||||
클릭 [처음부터] : 다음 스테이지 — StageDirector 호출
|
||||
[PASS] [처음부터] → StageDirector.Enter(0) 1회 — 실측 +1
|
||||
[PASS] 이탈 → RunHud 스테이지 모드 off
|
||||
[PASS] 이탈 → 결과창 스테이지 모드 off
|
||||
[PASS] 이탈 → 실패 팝업 모드 off(813z 원래 1버튼)
|
||||
[StageUiBridge] 구독=True inStage=False 스테이지=3 · 입장 3 카운트 0 시작 0 적수 3 시간 0 클리어 3 실패 0 이탈 2
|
||||
실측 — 피격 0회 · 물약 0개(시작 스냅 0 / 현재 0) · 최대 연쇄 0(진행중 0)
|
||||
메뉴 — 숨김 3회 / 복원 2회 / 재보장 0회(도로 끈 것 0건 · 주기 0.50s) · 지금 숨긴 것 0건 · 못 찾은 경로 0
|
||||
버튼 — 다시도전 0 다음 2 처음부터 0 로비 1 · 로비모드=ReturnToLobby · toLobbyCallsReturnToLobby=1
|
||||
설정 enabled=True hud=True 메뉴숨김=True 결과=True 실패팝업=True · 마지막 로그 = 이탈(probe lobby) — 로비=True · 메뉴 복원 0건
|
||||
|
||||
|
||||
── ⑧ GC — 같은 값 100틱 = 표시 쓰기 0회
|
||||
[PASS] 같은 초 100틱 → 타이머 쓰기 0회 — 실측 +0
|
||||
[PASS] 같은 수 100틱 → 남은 적 쓰기 0회 — 실측 +0
|
||||
[PASS] 같은 스테이지 → 라벨 쓰기 0회 — 실측 +0
|
||||
ΔGetTotalMemory = 0 B (에디터 TMP 문자열 포함 · 빌드에는 없는 줄)
|
||||
|
||||
── ⑨ C8 — WLStageUiSettings 를 끄면 813q/813z 가 원래대로
|
||||
[PASS] RuntimeDisabled → Enabled=false
|
||||
[PASS] 축 off → 스테이지 모드 0 — 실측 False
|
||||
[PASS] 축 off → 메뉴 숨김 0 — 실측 0
|
||||
[PASS] 축 off → 결과창 스테이지 모드 0
|
||||
[PASS] 축 off → 실패 팝업 2버튼 0(813z 원래 부활 팝업)
|
||||
[PASS] 813q WLRunUiSettings.Enabled 불변 = True
|
||||
[PASS] 813q 런 HUD 원래대로 — 실측 "04:00" "존 1"
|
||||
[PASS] 복구 → Enabled=true
|
||||
|
||||
RESULT PASS (pass=94 fail=0)
|
||||
|
|
@ -0,0 +1,714 @@
|
|||
// WL-815d 프로브 — 스테이지 UI 실측 (에디트 모드 · Play 0 · 로그인 0).
|
||||
// unity command run_script --file AgentScripts/WL815d_Probe.cs --entry WL815d_Probe.RunAll
|
||||
// unity command run_script --file AgentScripts/WL815d_Probe.cs --entry WL815d_Probe.Capture
|
||||
// 산출물: <worktree>/AgentScripts/WL815d_PROBE.txt · Screenshots_WL/WL815d/*.png
|
||||
// 🔴 NewGameUI.prefab 은 LoadPrefabContents 로 열고 **저장하지 않는다**(UnloadPrefabContents(false)).
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using TMPro;
|
||||
using WL.UI;
|
||||
using WL.Combat.Survival;
|
||||
|
||||
public static class WL815d_Probe
|
||||
{
|
||||
const string kPrefab = "Assets/Res_Addr/MainUI/NewGameUI.prefab";
|
||||
|
||||
static StringBuilder _o;
|
||||
static int _pass, _fail;
|
||||
static GameObject _root;
|
||||
static long _prefabLen;
|
||||
static DateTime _prefabTime;
|
||||
|
||||
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; } }
|
||||
|
||||
public static string RunAll()
|
||||
{
|
||||
_o = new StringBuilder();
|
||||
_pass = _fail = 0;
|
||||
_o.AppendLine("# WL815d Probe " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " (edit mode · Play 0 · 로그인 0)");
|
||||
_o.AppendLine("playMode=" + Application.isPlaying);
|
||||
|
||||
bool savedStageUiDisabled = WLStageUiSettings.RuntimeDisabled;
|
||||
try
|
||||
{
|
||||
Step0_Assets();
|
||||
Step1_BarrierHook();
|
||||
Step2_Nodes();
|
||||
Step3_HudThreeItems();
|
||||
Step4_Menus();
|
||||
Step5_Clear();
|
||||
Step6_Fail();
|
||||
Step7_Counters();
|
||||
Step8_Gc();
|
||||
Step9_C8();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_fail++;
|
||||
_o.AppendLine(" [FAIL] 예외 " + e.GetType().Name + " : " + e.Message);
|
||||
_o.AppendLine(e.StackTrace);
|
||||
}
|
||||
finally
|
||||
{
|
||||
WLStageUiSettings.RuntimeDisabled = savedStageUiDisabled;
|
||||
Cleanup();
|
||||
}
|
||||
|
||||
_o.AppendLine();
|
||||
_o.AppendLine("RESULT " + (_fail == 0 ? "PASS" : "FAIL") + " (pass=" + _pass + " fail=" + _fail + ")");
|
||||
string path = Path.Combine(Path.Combine(Root, "AgentScripts"), "WL815d_PROBE.txt");
|
||||
File.WriteAllText(path, _o.ToString(), new UTF8Encoding(false));
|
||||
return _o.ToString() + "\n(파일: " + path + ")";
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────── ⓪ 에셋 · SO 값
|
||||
static void Step0_Assets()
|
||||
{
|
||||
H("⓪ 에셋 · SO 값 (전부 C45)");
|
||||
var pf = new FileInfo(Path.Combine(Root, kPrefab.Replace('/', Path.DirectorySeparatorChar)));
|
||||
_prefabLen = pf.Length; _prefabTime = pf.LastWriteTimeUtc;
|
||||
N("원본 프리팹 기준선: " + kPrefab + " " + _prefabLen + "B · " + _prefabTime.ToString("yyyy-MM-dd HH:mm:ss") + "Z");
|
||||
WLStageUiSettings.ClearCache();
|
||||
var g = WLStageUiSettings.Instance;
|
||||
Chk(g != null, "Resources.Load(\"" + WLStageUiSettings.ResourcesPath + "\") — NEW 에셋");
|
||||
if (g == null) return;
|
||||
Chk(WLStageUiSettings.Enabled, "WLStageUiSettings.Enabled = " + WLStageUiSettings.Enabled);
|
||||
N("HUD: stageLabel=\"" + g.stageLabelFormat + "\" enemy=\"" + g.enemyLabelFormat + "\" warn=" +
|
||||
g.timerWarnSeconds + "s countdown=\"" + g.countdownText + "\" 존게이지숨김=" + g.hideZoneGaugeInStage);
|
||||
N("메뉴: hide=" + g.hideMenusInStage + " 목록 " + (g.stageHidePaths != null ? g.stageHidePaths.Length : 0) +
|
||||
"개 · 유지 표기 " + (g.stageKeepPaths != null ? g.stageKeepPaths.Length : 0) + "개 · 복원=" + g.restoreMenusOnExit +
|
||||
" · 룩토글유지=" + g.keepLookToggleButtons);
|
||||
N("결과: 제목=\"" + g.stageClearTitleFormat + "\" 행 " + (g.stageResultRows != null ? g.stageResultRows.Length : 0) +
|
||||
"개 [" + string.Join(" · ", Array.ConvertAll(g.stageResultRows, r => r.ToString())) + "] · 버튼 [" +
|
||||
g.nextStageText + "] [" + g.toLobbyText + "] · 배지=\"" + g.noHitBadgeText + "\"");
|
||||
N("실패: [" + g.failRetryText + "] [" + g.failLobbyText + "] · 사망=\"" + g.failDeathMessage.Replace("\n", " / ") +
|
||||
"\" · 시간초과=\"" + g.failTimeoutMessage.Replace("\n", " / ") + "\" · 결과창억제=" + g.suppressResultOnFail);
|
||||
N("로비: lobbyMode=" + g.lobbyMode + " titleScene=\"" + g.titleSceneName + "\"");
|
||||
Chk(Mathf.Approximately(g.timerWarnSeconds, 10f), "타이머 경고 = 마지막 10 s(PD 60 s 규칙) — 실측 " + g.timerWarnSeconds);
|
||||
|
||||
var c = WL.Combat.Stage.WLStageSettings.Instance;
|
||||
Chk(c != null, "815b WLStageSettings 존재");
|
||||
if (c != null)
|
||||
{
|
||||
Chk(c.autoEnterOnMapEnter, "F-27 autoEnterOnMapEnter = 1 (맵 진입 = 스테이지 1) — 실측 " + c.autoEnterOnMapEnter);
|
||||
Chk(!c.bindReviveOkToRetry, "bindReviveOkToRetry = 0 (815d 2버튼 팝업이 대신한다) — 실측 " + c.bindReviveOkToRetry);
|
||||
Chk(c.toLobbyCallsReturnToLobby, "toLobbyCallsReturnToLobby = 1 (로비 ⓐ안) — 실측 " + c.toLobbyCallsReturnToLobby);
|
||||
Chk(c.useBarrierHook && c.clearBarrierOnExit, "장벽 훅 사용 on/off = " + c.useBarrierHook + "/" + c.clearBarrierOnExit);
|
||||
N("기타: mirrorRunEnded=" + c.mirrorRunEnded + " mirrorRunHudEvents=" + c.mirrorRunHudEvents +
|
||||
" clearHoldScale=" + c.clearHoldScale + " wrapToFirstStage=" + c.wrapToFirstStage);
|
||||
}
|
||||
var sv = WLSurvivalSettings.Instance;
|
||||
if (sv != null) Chk(Mathf.Approximately(sv.reviveWatchdogSeconds, 0f),
|
||||
"자동 부활 0 (reviveWatchdogSeconds) — 실측 " + sv.reviveWatchdogSeconds);
|
||||
|
||||
var bs = WL.Combat.Stage.WLStageBarrierSettings.Instance;
|
||||
if (bs != null)
|
||||
{
|
||||
N("815c 장벽: style=" + bs.style + "(0 None/1 Ring/2 Props/3 Fire) enabled=" + bs.enabled +
|
||||
" showVisual=" + bs.showVisual + " blockEnabled=" + bs.blockEnabled);
|
||||
Chk((int)bs.style == 2, "F-26 style = 2(Props · 울타리) — 실측 " + (int)bs.style);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────── ① 장벽 훅 2줄
|
||||
static void Step1_BarrierHook()
|
||||
{
|
||||
H("① 장벽 훅 — StageBarrierHook.Set/Clear = StageBarrier.Set/Clear");
|
||||
N(WL.Combat.Stage.StageBarrierBinding.Bind());
|
||||
Chk(WL.Combat.Stage.StageBarrierHook.HasSet, "StageBarrierHook.Set 대입됨");
|
||||
Chk(WL.Combat.Stage.StageBarrierHook.HasClear, "StageBarrierHook.Clear 대입됨");
|
||||
|
||||
WL.Combat.Stage.StageBarrierHook.ResetDiagnostics();
|
||||
int set0 = WL.Combat.Stage.StageBarrier.SetCount, clr0 = WL.Combat.Stage.StageBarrier.ClearCount;
|
||||
var center = new Vector3(115f, 0.4f, -22f);
|
||||
WL.Combat.Stage.StageBarrierHook.RaiseSet(center, 14f);
|
||||
Chk(WL.Combat.Stage.StageBarrierHook.SetCalls == 1, "훅 Set 호출 1 — 실측 " + WL.Combat.Stage.StageBarrierHook.SetCalls);
|
||||
Chk(WL.Combat.Stage.StageBarrier.SetCount == set0 + 1,
|
||||
"815c StageBarrier.Set 실제 실행 +1 — 실측 " + (WL.Combat.Stage.StageBarrier.SetCount - set0));
|
||||
Chk(WL.Combat.Stage.StageBarrier.Active, "장벽 활성 = " + WL.Combat.Stage.StageBarrier.Active +
|
||||
" 중심 " + WL.Combat.Stage.StageBarrier.Center.ToString("F1") + " r=" + WL.Combat.Stage.StageBarrier.Radius.ToString("F1"));
|
||||
WL.Combat.Stage.StageBarrierHook.RaiseClear();
|
||||
Chk(WL.Combat.Stage.StageBarrier.ClearCount == clr0 + 1,
|
||||
"815c StageBarrier.Clear 실제 실행 +1 — 실측 " + (WL.Combat.Stage.StageBarrier.ClearCount - clr0));
|
||||
Chk(!WL.Combat.Stage.StageBarrier.Active, "장벽 해제 = " + (!WL.Combat.Stage.StageBarrier.Active));
|
||||
Chk(WL.Combat.Stage.StageBarrierHook.Faults == 0, "훅 예외 0 — 실측 " + WL.Combat.Stage.StageBarrierHook.Faults);
|
||||
|
||||
// 실입장 경로(815b StageDirector) 가 훅을 부르는가
|
||||
WL.Combat.Stage.StageBarrierHook.ResetDiagnostics();
|
||||
WL.Combat.Stage.StageDirector.ResetAll();
|
||||
bool entered = false;
|
||||
try { entered = WL.Combat.Stage.StageDirector.Enter(0, "815d probe"); }
|
||||
catch (Exception e) { N("Enter 예외(에디트 모드) " + e.GetType().Name + " : " + e.Message); }
|
||||
N("StageDirector.Enter(0) = " + entered + " · 상태 " + WL.Combat.Stage.StageDirector.State +
|
||||
" · 고정 수 " + WL.Combat.Stage.StageDirector.TotalEnemies + " · 제한 " + WL.Combat.Stage.StageDirector.TimeLimit + "s");
|
||||
Chk(WL.Combat.Stage.StageBarrierHook.SetCalls >= 1,
|
||||
"입장 시 훅 Set 호출 ≥1 — 실측 " + WL.Combat.Stage.StageBarrierHook.SetCalls +
|
||||
" 중심 " + WL.Combat.Stage.StageBarrierHook.LastCenter.ToString("F1") +
|
||||
" r=" + WL.Combat.Stage.StageBarrierHook.LastRadius.ToString("F1"));
|
||||
WL.Combat.Stage.StageDirector.ToLobby();
|
||||
Chk(WL.Combat.Stage.StageBarrierHook.ClearCalls >= 1,
|
||||
"이탈 시 훅 Clear 호출 ≥1 — 실측 " + WL.Combat.Stage.StageBarrierHook.ClearCalls);
|
||||
|
||||
// C8 ① — 훅을 떼면 no-op
|
||||
N(WL.Combat.Stage.StageBarrierBinding.Unbind());
|
||||
int s1 = WL.Combat.Stage.StageBarrier.SetCount;
|
||||
WL.Combat.Stage.StageBarrierHook.RaiseSet(center, 14f);
|
||||
Chk(WL.Combat.Stage.StageBarrier.SetCount == s1, "Unbind 뒤 815c 실행 0 = no-op(C8 ①)");
|
||||
N(WL.Combat.Stage.StageBarrierBinding.Bind());
|
||||
WL.Combat.Stage.StageBarrier.Clear();
|
||||
N(WL.Combat.Stage.StageBarrierBinding.Dump());
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────── ② 노드
|
||||
static void Step2_Nodes()
|
||||
{
|
||||
H("② 노드 — NewGameUI 프리팹(저장 0)에 HUD·결과·실패 팝업 조립");
|
||||
var root = PrefabUtility.LoadPrefabContents(kPrefab);
|
||||
_root = root;
|
||||
Chk(root != null, "LoadPrefabContents(" + kPrefab + ")");
|
||||
if (root == null) return;
|
||||
|
||||
StageUiBridge.SetUiRootForProbe(root.transform);
|
||||
RunHud.Subscribe(true);
|
||||
RunResultUI.Subscribe(true);
|
||||
StageUiBridge.Subscribe(true);
|
||||
N("Bind HUD : " + RunHud.Bind(root.transform));
|
||||
N("Bind 결과 : " + RunResultUI.Bind(root.transform));
|
||||
Chk(RunHud.Bound && RunResultUI.Bound, "813q 노드 조립(WL_RunHud · WL_RunResult · HideFlags.DontSave)");
|
||||
Chk(StageUiBridge.Subscribed, "StageUiBridge 구독 on");
|
||||
Chk(WL.Combat.Stage.StageEvents.StageEntered.Count >= 1 && WL.Combat.Stage.StageEvents.StageCleared.Count >= 1 &&
|
||||
WL.Combat.Stage.StageEvents.StageFailed.Count >= 1 && WL.Combat.Stage.StageEvents.StageExited.Count >= 1,
|
||||
"StageEvents 구독 배선 — Entered " + WL.Combat.Stage.StageEvents.StageEntered.Count +
|
||||
" Countdown " + WL.Combat.Stage.StageEvents.CountdownTick.Count +
|
||||
" Started " + WL.Combat.Stage.StageEvents.StageStarted.Count +
|
||||
" Enemy " + WL.Combat.Stage.StageEvents.EnemyCountChanged.Count +
|
||||
" Time " + WL.Combat.Stage.StageEvents.TimeChanged.Count +
|
||||
" Cleared " + WL.Combat.Stage.StageEvents.StageCleared.Count +
|
||||
" Failed " + WL.Combat.Stage.StageEvents.StageFailed.Count +
|
||||
" Exited " + WL.Combat.Stage.StageEvents.StageExited.Count);
|
||||
|
||||
var dlg = FindRevive(root.transform);
|
||||
Chk(dlg != null, "813z ReviveDialog 컴포넌트 발견(IngameUIs/WL_HUD/WL_ReviveDialog)");
|
||||
if (dlg != null) N("ReviveDialog init: " + dlg.Initialize());
|
||||
Chk(ReviveDialog.Active != null, "ReviveDialog.Active 세팅");
|
||||
}
|
||||
|
||||
static ReviveDialog FindRevive(Transform root)
|
||||
{
|
||||
var all = root.GetComponentsInChildren<ReviveDialog>(true);
|
||||
return all != null && all.Length > 0 ? all[0] : null;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────── ③ 상단 3요소
|
||||
static void Step3_HudThreeItems()
|
||||
{
|
||||
H("③ 상단 3요소 — 스테이지 N · 남은 시간 mm:ss · 남은 적 M/T (+ 카운트다운)");
|
||||
RunHud.ResetState();
|
||||
StageUiBridge.ResetState();
|
||||
|
||||
WLStageUiProbeHooks.RaiseEntered(0, 1, 8, 60f, new Vector3(115f, 0.4f, -22f), 14f, 1.5f, 0);
|
||||
Chk(StageUiBridge.EnteredSeen == 1, "StageEntered 수신 1");
|
||||
Chk(RunHud.StageMode, "RunHud 스테이지 모드 on");
|
||||
Chk(RunHud.PhaseText == "스테이지 1", "① 단계 = \"스테이지 1\" — 실측 \"" + RunHud.PhaseText + "\"");
|
||||
Chk(RunHud.TimerText == "01:00", "② 타이머 = 01:00(제한 60 s) — 실측 \"" + RunHud.TimerText + "\"");
|
||||
Chk(RunHud.EnemyText == "남은 적 8 / 8", "③ 남은 적 = \"남은 적 8 / 8\" — 실측 \"" + RunHud.EnemyText + "\"");
|
||||
Chk(!RunHud.ZoneGaugeVisible, "존 게이지 접힘(기준서 §F-2) — 실측 활성=" + RunHud.ZoneGaugeVisible);
|
||||
|
||||
WLStageUiProbeHooks.RaiseCountdown(0, 1.5f, 1.5f);
|
||||
Chk(RunHud.NoticeVisible && RunHud.NoticeText == "준비",
|
||||
"카운트다운 = \"준비\" 표시 — 실측 보임=" + RunHud.NoticeVisible + " \"" + RunHud.NoticeText + "\"");
|
||||
|
||||
WLStageUiProbeHooks.RaiseStarted(0, 1, 8, 60f, 0);
|
||||
Chk(!RunHud.NoticeVisible, "전투 시작 = 카운트다운 줄 접힘 — 실측 " + RunHud.NoticeVisible);
|
||||
|
||||
WLStageUiProbeHooks.RaiseEnemyCount(0, 5, 8);
|
||||
Chk(RunHud.EnemyText == "남은 적 5 / 8", "처치 3 → \"남은 적 5 / 8\" — 실측 \"" + RunHud.EnemyText + "\"");
|
||||
WLStageUiProbeHooks.RaiseTime(0, 42f, 60f);
|
||||
Chk(RunHud.TimerText == "00:42" && !RunHud.TimerWarn, "42 s → 00:42 · 평상색 — 실측 \"" + RunHud.TimerText +
|
||||
"\" warn=" + RunHud.TimerWarn);
|
||||
|
||||
WLStageUiProbeHooks.RaiseTime(0, 9.4f, 60f);
|
||||
Chk(RunHud.TimerText == "00:10" && RunHud.TimerWarn,
|
||||
"마지막 10 s → 붉은색(경고) — 실측 \"" + RunHud.TimerText + "\" warn=" + RunHud.TimerWarn +
|
||||
" color=#" + ColorUtility.ToHtmlStringRGB(RunHud.TimerColor));
|
||||
WLStageUiProbeHooks.RaiseEnemyCount(0, 2, 8);
|
||||
Chk(RunHud.EnemyText == "남은 적 2 / 8", "남은 적 2 → 강조색 — 실측 \"" + RunHud.EnemyText + "\"");
|
||||
|
||||
// 815b 병행 발행(RunStarted/RunTick)이 스테이지 표시를 덮지 않는가
|
||||
int ignored0 = RunHud.StageEventsIgnored;
|
||||
WLRunUiProbeHooks.RaiseRunStarted(1, 60f, 1, "stage mirror");
|
||||
WLRunUiProbeHooks.RaiseRunTick(18f, 42f, 0, 6, 8, 0, 1, WL.Combat.Run.RunPhase.Zones, 1);
|
||||
Chk(RunHud.StageEventsIgnored - ignored0 == 2, "병행 RunEvents 2건 무시 — 실측 " + (RunHud.StageEventsIgnored - ignored0));
|
||||
Chk(RunHud.PhaseText == "스테이지 1" && RunHud.EnemyText == "남은 적 2 / 8" && RunHud.TimerText == "00:10",
|
||||
"🔴 병행 발행이 상단 3요소를 덮지 않는다 — \"" + RunHud.PhaseText + "\" \"" + RunHud.TimerText + "\" \"" + RunHud.EnemyText + "\"");
|
||||
N("HUD 배치: " + RunHud.ApplyLayout());
|
||||
N(RunHud.Dump());
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────── ④ 메뉴 숨김 / 복원
|
||||
static void Step4_Menus()
|
||||
{
|
||||
H("④ 불필요 메뉴 숨김 · 복원 (§J ① · 원본 프리팹 diff 0)");
|
||||
var g = WLStageUiSettings.Instance;
|
||||
if (_root == null || g == null) { Chk(false, "프리팹 루트 없음"); return; }
|
||||
|
||||
var paths = g.stageHidePaths;
|
||||
var found = new Transform[paths.Length];
|
||||
var saved = new bool[paths.Length];
|
||||
int missing = 0;
|
||||
for (int i = 0; i < paths.Length; i++)
|
||||
{
|
||||
found[i] = WLVignetteUtil.FindUiPath(_root.transform, paths[i]);
|
||||
if (found[i] == null) { missing++; continue; }
|
||||
saved[i] = found[i].gameObject.activeSelf; // 프리팹 저장 상태
|
||||
}
|
||||
Chk(missing == 0, "숨김 목록 " + paths.Length + "개 전부 실존(경로 해석 성공) — 못 찾음 " + missing);
|
||||
|
||||
// 🔴 프리팹 **저장 상태**로는 인게임 화면을 재현할 수 없다(IngameUIs 는 저장 시 꺼져 있다).
|
||||
// 실기에서 켜져 있는 상태를 만든 뒤 숨김 → 복원 왕복을 재는 것이 유일한 실측이다.
|
||||
for (int i = 0; i < paths.Length; i++)
|
||||
if (found[i] != null && !found[i].gameObject.activeSelf) found[i].gameObject.SetActive(true);
|
||||
|
||||
N("숨김: " + StageUiBridge.ApplyMenuHide(true));
|
||||
var mid = new bool[paths.Length];
|
||||
for (int i = 0; i < paths.Length; i++) mid[i] = found[i] != null && found[i].gameObject.activeSelf;
|
||||
|
||||
// 🔴 재보장 — 원본 `NewGameUI.SetUI_byGameMode` 가 스테이지 중에 같은 노드를 다시 켜는 경우
|
||||
int reOn = 0;
|
||||
for (int i = 0; i < paths.Length && reOn < 3; i++)
|
||||
if (found[i] != null) { found[i].gameObject.SetActive(true); reOn++; }
|
||||
int hit = StageUiBridge.ReassertHide();
|
||||
int stillOn = 0;
|
||||
for (int i = 0; i < paths.Length; i++) if (found[i] != null && found[i].gameObject.activeSelf) stillOn++;
|
||||
Chk(hit == reOn && stillOn == 0, "원본이 다시 켠 " + reOn + "건을 재보장으로 도로 끔 — 실측 " + hit +
|
||||
"건 · 남은 on " + stillOn + " (주기 " + g.menuReassertSeconds + "s)");
|
||||
Chk(StageUiBridge.ReassertHide() == 0, "이미 꺼져 있으면 쓰기 0회(GC 0)");
|
||||
|
||||
N("복원: " + StageUiBridge.ApplyMenuHide(false));
|
||||
int restored = 0, hidOk = 0;
|
||||
_o.AppendLine(" | # | 경로(NewGameUI 루트 기준) | 실제 노드 경로 | 프리팹 저장 | 스테이지 중 | 이탈 뒤 |");
|
||||
_o.AppendLine(" |---|---|---|---|---|---|");
|
||||
for (int i = 0; i < paths.Length; i++)
|
||||
{
|
||||
bool after = found[i] != null && found[i].gameObject.activeSelf;
|
||||
if (found[i] != null)
|
||||
{
|
||||
if (!mid[i]) hidOk++;
|
||||
if (after) restored++;
|
||||
}
|
||||
_o.AppendLine(" | " + i + " | `" + paths[i] + "` | " + (found[i] != null ? "`" + FullPath(found[i], _root.transform) + "`" : "**없음**") +
|
||||
" | " + (found[i] != null ? (saved[i] ? "on" : "off") : "-") +
|
||||
" | " + (found[i] != null ? (mid[i] ? "on" : "**off**") : "-") +
|
||||
" | " + (found[i] != null ? (after ? "**on**" : "off") : "-") + " |");
|
||||
}
|
||||
Chk(hidOk == paths.Length - missing, "스테이지 중 전부 SetActive(false) — " + hidOk + "/" + (paths.Length - missing));
|
||||
Chk(restored == paths.Length - missing, "이탈 시 전부 켜진 값으로 복원 — " + restored + "/" + (paths.Length - missing));
|
||||
// 프리팹 저장 상태로 되돌린다(미리보기 사본이지만 뒤 단계에 영향 0)
|
||||
for (int i = 0; i < paths.Length; i++)
|
||||
if (found[i] != null && found[i].gameObject.activeSelf != saved[i]) found[i].gameObject.SetActive(saved[i]);
|
||||
|
||||
// 유지 대상은 건드리지 않았는가
|
||||
int keptOk = 0, keptMissing = 0;
|
||||
for (int i = 0; i < g.stageKeepPaths.Length; i++)
|
||||
{
|
||||
var t = WLVignetteUtil.FindUiPath(_root.transform, g.stageKeepPaths[i]);
|
||||
if (t == null) { keptMissing++; N(" (유지) " + g.stageKeepPaths[i] + " : 없음"); continue; }
|
||||
keptOk++;
|
||||
N(" (유지) " + g.stageKeepPaths[i] + " activeSelf=" + t.gameObject.activeSelf);
|
||||
}
|
||||
Chk(keptMissing == 0, "유지 목록 " + g.stageKeepPaths.Length + "개 전부 실존 — 없음 " + keptMissing);
|
||||
N("유지 확인 " + keptOk + "개 (조이스틱·스킬 패드 BattleUI · HP/MP MyInfoUI · 813 WL HUD 요소)");
|
||||
|
||||
// 🔴 원본 프리팹 **파일**이 바뀌지 않았는가(LoadPrefabContents 는 메모리 사본이라 dirty 플래그는 뜬다 —
|
||||
// 실제 판정은 디스크 파일의 길이·수정시각이다. 저장은 하지 않는다 = UnloadPrefabContents).
|
||||
var fi = new FileInfo(Path.Combine(Root, kPrefab.Replace('/', Path.DirectorySeparatorChar)));
|
||||
Chk(fi.Length == _prefabLen && fi.LastWriteTimeUtc == _prefabTime,
|
||||
"원본 NewGameUI.prefab 파일 무변경 — 길이 " + fi.Length + "B(=" + _prefabLen + ") 수정시각 " +
|
||||
fi.LastWriteTimeUtc.ToString("HH:mm:ss") + "(=" + _prefabTime.ToString("HH:mm:ss") + ")");
|
||||
N("(참고) 미리보기 사본 dirty=" + EditorUtility.IsDirty(_root) +
|
||||
" — 런타임 노드는 HideFlags.DontSave 라 저장해도 굽히지 않는다(813y 규약)");
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────── ⑤ 클리어 화면
|
||||
static void Step5_Clear()
|
||||
{
|
||||
H("⑤ 클리어 화면 — StageCleared → 813q RunResultUI (기준서 §F-1)");
|
||||
var g = WLStageUiSettings.Instance;
|
||||
RunResultUI.ResetState();
|
||||
StageUiBridge.ResetState();
|
||||
StageUiBridge.ProbeSetInStage(true);
|
||||
|
||||
// 피격 3회 · 연쇄 5 를 스테이지 구간 동안 실측
|
||||
WLStageUiProbeHooks.RaiseDamaged(null); // victim 이 PC 가 아니면 세지 않는다
|
||||
Chk(StageUiBridge.Hits == 0, "Damaged(victim=null)는 피격으로 세지 않는다 — 실측 " + StageUiBridge.Hits);
|
||||
StageUiBridge.ProbeNoteHit(); StageUiBridge.ProbeNoteHit(); StageUiBridge.ProbeNoteHit();
|
||||
WLStageUiProbeHooks.RaiseChainEnded(5, 2, 1.2f);
|
||||
WLStageUiProbeHooks.RaiseChainEnded(3, 1, 0.8f);
|
||||
Chk(StageUiBridge.Hits == 3, "피격 3회 집계 — 실측 " + StageUiBridge.Hits);
|
||||
Chk(StageUiBridge.MaxChain == 5, "최대 연쇄 = 5(3 은 갱신 안 함) — 실측 " + StageUiBridge.MaxChain);
|
||||
|
||||
WLStageUiProbeHooks.RaiseCleared(0, 1, 34.2f, 60f, 8, 8, 1, false, false);
|
||||
Chk(StageUiBridge.ClearedSeen == 1, "StageCleared 수신 1");
|
||||
Chk(RunResultUI.IsOpen, "결과 화면 표시 — 실측 open=" + RunResultUI.IsOpen);
|
||||
Chk(RunResultUI.StageMode, "결과창 스테이지 모드 on");
|
||||
Chk(RunResultUI.TitleText == "스테이지 1 클리어!", "제목 = \"스테이지 1 클리어!\" — 실측 \"" + RunResultUI.TitleText + "\"");
|
||||
N("본문 =\n" + Indent(RunResultUI.BodyText));
|
||||
Chk(RunResultUI.VisibleRows == g.stageResultRows.Length,
|
||||
"행 " + g.stageResultRows.Length + "개 — 실측 " + RunResultUI.VisibleRows);
|
||||
Chk(!RunResultUI.BadgeVisible, "피격 3회 → 무피격 배지 없음 — 실측 " + RunResultUI.BadgeVisible);
|
||||
N("배치: " + RunResultUI.ApplyLayout());
|
||||
|
||||
// 병행 RunEnded(Win) 이 결과창을 두 번 띄우지 않는가
|
||||
int shows0 = RunResultUI.ShownCount;
|
||||
var mirror = new WL.Combat.Run.RunResult { outcome = WL.Combat.Run.RunOutcome.Win, runIndex = 1, totalSec = 34.2f, kills = 8 };
|
||||
WLRunUiProbeHooks.RaiseRunEnded(in mirror);
|
||||
Chk(RunResultUI.ShownCount == shows0, "🔴 병행 RunEnded(Win) 로 결과창 중복 표시 0 — 실측 +" +
|
||||
(RunResultUI.ShownCount - shows0) + " · 억제 " + RunResultUI.SuppressedRunEndedCount + "회");
|
||||
Chk(RunResultUI.TitleText == "스테이지 1 클리어!", "제목이 「런 클리어!」로 덮이지 않았다 — \"" + RunResultUI.TitleText + "\"");
|
||||
|
||||
// 무피격 클리어 → 배지
|
||||
StageUiBridge.ResetState();
|
||||
StageUiBridge.ProbeSetInStage(true);
|
||||
WLStageUiProbeHooks.RaiseCleared(1, 2, 21.5f, 60f, 10, 10, 0, false, false);
|
||||
Chk(RunResultUI.BadgeVisible && RunResultUI.BadgeText == "무피격 클리어!",
|
||||
"피격 0회 → 무피격 배지 — 실측 " + RunResultUI.BadgeVisible + " \"" + RunResultUI.BadgeText + "\"");
|
||||
|
||||
// 마지막 스테이지 → [처음부터]
|
||||
WLStageUiProbeHooks.RaiseCleared(9, 10, 55.0f, 60f, 15, 15, 2, true, true);
|
||||
Chk(RunResultUI.TitleText == "전 스테이지 클리어!", "마지막 스테이지 제목 — 실측 \"" + RunResultUI.TitleText + "\"");
|
||||
N("마지막 스테이지 배치: " + RunResultUI.ApplyLayout());
|
||||
N("본문(보스 스테이지) =\n" + Indent(RunResultUI.BodyText));
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────── ⑥ 실패 팝업
|
||||
static void Step6_Fail()
|
||||
{
|
||||
H("⑥ 실패 팝업 — StageFailed → 813z ReviveDialog 2버튼 (기준서 §E)");
|
||||
var dlg = ReviveDialog.Active;
|
||||
if (dlg == null) { Chk(false, "ReviveDialog.Active 없음"); return; }
|
||||
RunResultUI.ResetState();
|
||||
StageUiBridge.ResetState();
|
||||
StageUiBridge.ProbeSetInStage(true);
|
||||
|
||||
WLStageUiProbeHooks.RaiseFailed(0, 1, WL.Combat.Stage.StageFailReason.TimeLimit, 3, 8, 60f, 0);
|
||||
Chk(StageUiBridge.FailedSeen == 1, "StageFailed 수신 1");
|
||||
Chk(dlg.IsOpen, "실패 팝업 표시 — 실측 open=" + dlg.IsOpen);
|
||||
Chk(ReviveDialog.StageMode, "ReviveDialog 스테이지 모드 on");
|
||||
Chk(!dlg.UsedCommonPopup, "공용 Popup 미사용(2버튼 불가) — 실측 " + dlg.UsedCommonPopup);
|
||||
N("배치: " + dlg.ApplyLayout());
|
||||
Chk(dlg.SecondaryVisible, "두 번째 버튼 노출 — 실측 " + dlg.SecondaryVisible);
|
||||
Chk(dlg.PrimaryButtonText == "다시 도전" && dlg.SecondaryButtonText == "로비로",
|
||||
"버튼 = [다시 도전] [로비로] — 실측 [" + dlg.PrimaryButtonText + "] [" + dlg.SecondaryButtonText + "]");
|
||||
Chk(dlg.LastMessage.Contains("시간 초과") && dlg.LastMessage.Contains("3"),
|
||||
"시간 초과 문구 + 남은 적 3 — 실측 \"" + dlg.LastMessage.Replace("\n", " / ") + "\"");
|
||||
// 🔴 813z 3층 방어 — 전용 Canvas 는 붙었지만 **부모 계층이 비활성인 동안 Unity 가 overrideSorting 을 되돌린다**
|
||||
// (814d §4 실측). 그래서 에디트 모드의 SortingOrder 게터는 −1 이다. SO 값과 재보장 횟수로 확인한다.
|
||||
var sv2 = WLSurvivalSettings.Instance;
|
||||
Chk(sv2 != null && sv2.reviveSortingOrder == 60 && dlg.TopmostCount >= 1,
|
||||
"813z 3층 방어 유지 — SO sortingOrder " + (sv2 != null ? sv2.reviveSortingOrder : -1) +
|
||||
" · 최상단 재보장 " + dlg.TopmostCount + "회 · 실측 게터=" + dlg.SortingOrder +
|
||||
"(부모 계층 비활성 → Unity 가 overrideSorting 되돌림 · 814d §4)");
|
||||
Chk(RunResultUI.SuppressedRunEndedCount == 0 && !RunResultUI.IsOpen, "실패 시점에 결과창 0");
|
||||
|
||||
// 병행 RunEnded(Timeout) 억제
|
||||
var mirror = new WL.Combat.Run.RunResult { outcome = WL.Combat.Run.RunOutcome.Timeout, runIndex = 1, totalSec = 60f, kills = 5 };
|
||||
WLRunUiProbeHooks.RaiseRunEnded(in mirror);
|
||||
Chk(!RunResultUI.IsOpen && RunResultUI.SuppressedRunEndedCount == 1,
|
||||
"🔴 병행 RunEnded(Timeout) 억제 → 결과창 안 뜸 — 실측 open=" + RunResultUI.IsOpen +
|
||||
" 억제 " + RunResultUI.SuppressedRunEndedCount + "회");
|
||||
|
||||
// 버튼 → StageDirector
|
||||
// 🔴 순서 주의: 실기 순서는 「입장 → 실패 → 클릭」이다. 프로브도 같은 순서로 태운다
|
||||
// (입장(StageEntered)이 실패 팝업 모드를 지우기 때문 — 이것이 정상 동작이다).
|
||||
WL.Combat.Stage.StageDirector.ResetAll();
|
||||
try { WL.Combat.Stage.StageDirector.Enter(0, "815d fail probe"); } catch (Exception e) { N("Enter 예외 " + e.GetType().Name); }
|
||||
StageUiBridge.ProbeSetInStage(true);
|
||||
|
||||
// 사망 문구
|
||||
WLStageUiProbeHooks.RaiseFailed(0, 1, WL.Combat.Stage.StageFailReason.PlayerDied, 4, 8, 22f, 1);
|
||||
Chk(dlg.LastMessage.Contains("쓰러졌"), "사망 문구 구분 — 실측 \"" + dlg.LastMessage.Replace("\n", " / ") + "\"");
|
||||
Chk(ReviveDialog.StageMode, "사망 실패도 2버튼 모드 — 실측 " + ReviveDialog.StageMode);
|
||||
int retry0 = WL.Combat.Stage.StageDirector.RetryCount;
|
||||
N("클릭 [다시 도전] : " + dlg.ProbeClickPrimary());
|
||||
Chk(WL.Combat.Stage.StageDirector.RetryCount == retry0 + 1,
|
||||
"[다시 도전] → StageDirector.Retry() 1회 — 실측 +" + (WL.Combat.Stage.StageDirector.RetryCount - retry0));
|
||||
Chk(!dlg.IsOpen, "누르면 팝업 닫힘 — 실측 open=" + dlg.IsOpen);
|
||||
|
||||
WLStageUiProbeHooks.RaiseFailed(0, 1, WL.Combat.Stage.StageFailReason.PlayerDied, 4, 8, 22f, 2);
|
||||
int lobby0 = WL.Combat.Stage.StageDirector.LobbyCount;
|
||||
N("클릭 [로비로] : " + dlg.ProbeClickSecondary());
|
||||
Chk(WL.Combat.Stage.StageDirector.LobbyCount == lobby0 + 1,
|
||||
"[로비로] → StageDirector.ToLobby() 1회 — 실측 +" + (WL.Combat.Stage.StageDirector.LobbyCount - lobby0));
|
||||
N(dlg.Dump());
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────── ⑦ 결과창 버튼
|
||||
static void Step7_Counters()
|
||||
{
|
||||
H("⑦ 클리어 화면 버튼 → StageDirector.Next() / ToLobby()");
|
||||
StageUiBridge.ResetState();
|
||||
StageUiBridge.ProbeSetInStage(true);
|
||||
WL.Combat.Stage.StageDirector.ResetAll();
|
||||
try { WL.Combat.Stage.StageDirector.Enter(0, "815d btn probe"); } catch (Exception e) { N("Enter 예외 " + e.GetType().Name); }
|
||||
|
||||
WLStageUiProbeHooks.RaiseCleared(0, 1, 30f, 60f, 8, 8, 0, false, false);
|
||||
int next0 = WL.Combat.Stage.StageDirector.NextCount;
|
||||
N("클릭 [다음 스테이지] : " + RunResultUI.OnNextRun());
|
||||
Chk(WL.Combat.Stage.StageDirector.NextCount == next0 + 1,
|
||||
"[다음 스테이지] → StageDirector.Next() 1회 — 실측 +" + (WL.Combat.Stage.StageDirector.NextCount - next0));
|
||||
Chk(!RunResultUI.IsOpen, "누르면 결과창 닫힘");
|
||||
|
||||
WLStageUiProbeHooks.RaiseCleared(0, 1, 30f, 60f, 8, 8, 0, false, false);
|
||||
int lobby0 = WL.Combat.Stage.StageDirector.LobbyCount;
|
||||
N("클릭 [로비로] : " + RunResultUI.OnClose());
|
||||
Chk(WL.Combat.Stage.StageDirector.LobbyCount == lobby0 + 1,
|
||||
"[로비로] → StageDirector.ToLobby() 1회 — 실측 +" + (WL.Combat.Stage.StageDirector.LobbyCount - lobby0));
|
||||
N("로비 동작: " + StageUiBridge.LastLog);
|
||||
|
||||
// 마지막 스테이지 → [처음부터] = Enter(0)
|
||||
WLStageUiProbeHooks.RaiseCleared(9, 10, 50f, 60f, 15, 15, 0, true, true);
|
||||
int enter0 = WL.Combat.Stage.StageDirector.EnterCount;
|
||||
N("클릭 [처음부터] : " + RunResultUI.OnNextRun());
|
||||
Chk(WL.Combat.Stage.StageDirector.EnterCount == enter0 + 1,
|
||||
"[처음부터] → StageDirector.Enter(0) 1회 — 실측 +" + (WL.Combat.Stage.StageDirector.EnterCount - enter0));
|
||||
|
||||
// 이탈 → 스테이지 모드 해제 + 메뉴 복원
|
||||
WLStageUiProbeHooks.RaiseExited(0, true, "probe lobby");
|
||||
Chk(!RunHud.StageMode, "이탈 → RunHud 스테이지 모드 off");
|
||||
Chk(!RunResultUI.StageMode, "이탈 → 결과창 스테이지 모드 off");
|
||||
Chk(!ReviveDialog.StageMode, "이탈 → 실패 팝업 모드 off(813z 원래 1버튼)");
|
||||
N(StageUiBridge.Dump());
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────── ⑧ GC
|
||||
static void Step8_Gc()
|
||||
{
|
||||
H("⑧ GC — 같은 값 100틱 = 표시 쓰기 0회");
|
||||
StageUiBridge.ResetState();
|
||||
WLStageUiProbeHooks.RaiseEntered(0, 3, 12, 60f, Vector3.zero, 14f, 1.5f, 0);
|
||||
WLStageUiProbeHooks.RaiseStarted(0, 3, 12, 60f, 0);
|
||||
WLStageUiProbeHooks.RaiseTime(0, 30f, 60f);
|
||||
WLStageUiProbeHooks.RaiseEnemyCount(0, 7, 12);
|
||||
int t0 = RunHud.TimerWrites, e0 = RunHud.EnemyWrites, p0 = RunHud.StageLabelWrites;
|
||||
long m0 = GC.GetTotalMemory(false);
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
WLStageUiProbeHooks.RaiseTime(0, 30f, 60f);
|
||||
WLStageUiProbeHooks.RaiseEnemyCount(0, 7, 12);
|
||||
}
|
||||
long m1 = GC.GetTotalMemory(false);
|
||||
Chk(RunHud.TimerWrites == t0, "같은 초 100틱 → 타이머 쓰기 0회 — 실측 +" + (RunHud.TimerWrites - t0));
|
||||
Chk(RunHud.EnemyWrites == e0, "같은 수 100틱 → 남은 적 쓰기 0회 — 실측 +" + (RunHud.EnemyWrites - e0));
|
||||
Chk(RunHud.StageLabelWrites == p0, "같은 스테이지 → 라벨 쓰기 0회 — 실측 +" + (RunHud.StageLabelWrites - p0));
|
||||
N("ΔGetTotalMemory = " + (m1 - m0) + " B (에디터 TMP 문자열 포함 · 빌드에는 없는 줄)");
|
||||
WLStageUiProbeHooks.RaiseExited(0, false, "gc probe end");
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────── ⑨ C8
|
||||
static void Step9_C8()
|
||||
{
|
||||
H("⑨ C8 — WLStageUiSettings 를 끄면 813q/813z 가 원래대로");
|
||||
StageUiBridge.ResetState();
|
||||
RunHud.ResetState();
|
||||
RunResultUI.ResetState();
|
||||
WLStageUiSettings.RuntimeDisabled = true;
|
||||
Chk(!WLStageUiSettings.Enabled, "RuntimeDisabled → Enabled=false");
|
||||
|
||||
WLStageUiProbeHooks.RaiseEntered(0, 4, 15, 60f, Vector3.zero, 14f, 1.5f, 0);
|
||||
Chk(!RunHud.StageMode, "축 off → 스테이지 모드 0 — 실측 " + RunHud.StageMode);
|
||||
Chk(StageUiBridge.HiddenCount == 0, "축 off → 메뉴 숨김 0 — 실측 " + StageUiBridge.HiddenCount);
|
||||
|
||||
WLStageUiProbeHooks.RaiseCleared(0, 4, 12f, 60f, 15, 15, 0, false, false);
|
||||
Chk(!RunResultUI.StageMode, "축 off → 결과창 스테이지 모드 0");
|
||||
WLStageUiProbeHooks.RaiseFailed(0, 4, WL.Combat.Stage.StageFailReason.TimeLimit, 5, 15, 60f, 0);
|
||||
Chk(!ReviveDialog.StageMode, "축 off → 실패 팝업 2버튼 0(813z 원래 부활 팝업)");
|
||||
|
||||
// 813q 런 축은 그대로 돈다
|
||||
Chk(WLRunUiSettings.Enabled, "813q WLRunUiSettings.Enabled 불변 = " + WLRunUiSettings.Enabled);
|
||||
WLRunUiProbeHooks.RaiseRunStarted(2, 240f, 4, "c8 probe");
|
||||
Chk(RunHud.TimerText == "04:00" && RunHud.PhaseText == "존 1",
|
||||
"813q 런 HUD 원래대로 — 실측 \"" + RunHud.TimerText + "\" \"" + RunHud.PhaseText + "\"");
|
||||
|
||||
WLStageUiSettings.RuntimeDisabled = false;
|
||||
Chk(WLStageUiSettings.Enabled, "복구 → Enabled=true");
|
||||
}
|
||||
|
||||
/// <summary>root 기준 전체 경로(실제로 어느 노드를 잡았는지 실측 표기).</summary>
|
||||
static string FullPath(Transform t, Transform root)
|
||||
{
|
||||
var sb = new StringBuilder(t.name);
|
||||
var p = t.parent;
|
||||
while (p != null && p != root) { sb.Insert(0, p.name + "/"); p = p.parent; }
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
static string Indent(string s)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s)) return " (없음)";
|
||||
return " " + s.TrimEnd('\n').Replace("\n", "\n ");
|
||||
}
|
||||
|
||||
static void Cleanup()
|
||||
{
|
||||
try
|
||||
{
|
||||
StageUiBridge.Teardown();
|
||||
RunHud.Teardown();
|
||||
RunResultUI.Teardown();
|
||||
WL.Combat.Stage.StageDirector.ResetAll();
|
||||
WL.Combat.Stage.StageBarrier.Clear();
|
||||
}
|
||||
catch (Exception e) { _o.AppendLine(" (정리 예외 " + e.GetType().Name + ")"); }
|
||||
if (_root != null) { PrefabUtility.UnloadPrefabContents(_root); _root = null; }
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════
|
||||
// 캡처 3장 — 1080×1920 목업(실 노드 · 실 값 · RenderTexture 렌더)
|
||||
// ═════════════════════════════════════════════════════════════════════
|
||||
public static string Capture()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
string dir = Path.Combine(Path.Combine(Root, "Screenshots_WL"), "WL815d");
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
GameObject rig = null, prefabRoot = null;
|
||||
try
|
||||
{
|
||||
prefabRoot = PrefabUtility.LoadPrefabContents(kPrefab);
|
||||
var revive = FindRevive(prefabRoot.transform);
|
||||
|
||||
rig = new GameObject("WL815d_CaptureRig");
|
||||
rig.hideFlags = HideFlags.HideAndDontSave;
|
||||
var camGo = new GameObject("Cam");
|
||||
camGo.transform.SetParent(rig.transform, false);
|
||||
var cam = camGo.AddComponent<Camera>();
|
||||
cam.clearFlags = CameraClearFlags.SolidColor;
|
||||
cam.backgroundColor = new Color(0.09f, 0.10f, 0.12f, 1f);
|
||||
cam.orthographic = true;
|
||||
cam.orthographicSize = 960f;
|
||||
cam.nearClipPlane = 0.1f; cam.farClipPlane = 5000f;
|
||||
camGo.transform.position = new Vector3(0f, 0f, -1000f);
|
||||
|
||||
var canvasGo = new GameObject("Canvas", typeof(RectTransform));
|
||||
canvasGo.transform.SetParent(rig.transform, false);
|
||||
canvasGo.layer = 5;
|
||||
var canvas = canvasGo.AddComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceCamera;
|
||||
canvas.worldCamera = cam;
|
||||
canvas.planeDistance = 500f;
|
||||
var scaler = canvasGo.AddComponent<UnityEngine.UI.CanvasScaler>();
|
||||
scaler.uiScaleMode = UnityEngine.UI.CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1080f, 1920f);
|
||||
scaler.matchWidthOrHeight = 0.5f;
|
||||
canvasGo.AddComponent<UnityEngine.UI.GraphicRaycaster>();
|
||||
var crt = (RectTransform)canvasGo.transform;
|
||||
crt.sizeDelta = new Vector2(1080f, 1920f);
|
||||
|
||||
// 813q/813z 가 찾는 부모 경로(IngameUIs/WL_HUD)를 목업 캔버스 안에 그대로 만든다
|
||||
var ingame = NewRect(crt, "IngameUIs");
|
||||
var wlHud = NewRect(ingame, "WL_HUD");
|
||||
|
||||
// 폰트는 원본 UI 에서 빌린다(한글 글리프)
|
||||
var font = RunHud.BorrowFont(prefabRoot.transform);
|
||||
RunHud.SetFont(font); RunResultUI.SetFont(font);
|
||||
|
||||
StageUiBridge.SetUiRootForProbe(prefabRoot.transform);
|
||||
RunHud.Subscribe(true); RunResultUI.Subscribe(true); StageUiBridge.Subscribe(true);
|
||||
sb.AppendLine("HUD : " + RunHud.Bind(crt));
|
||||
sb.AppendLine("결과 : " + RunResultUI.Bind(crt));
|
||||
|
||||
// 실패 팝업은 목업 캔버스 안에 새 인스턴스로 만든다(원본 프리팹 무수정)
|
||||
var dlgGo = new GameObject("WL_ReviveDialog", typeof(RectTransform));
|
||||
dlgGo.layer = 5;
|
||||
dlgGo.transform.SetParent(wlHud, false);
|
||||
revive = dlgGo.AddComponent<ReviveDialog>();
|
||||
revive.SetFont(font);
|
||||
sb.AppendLine("팝업 : " + revive.Initialize());
|
||||
|
||||
var rt = new RenderTexture(1080, 1920, 24, RenderTextureFormat.ARGB32);
|
||||
cam.targetTexture = rt;
|
||||
|
||||
// ① HUD (스테이지 3요소)
|
||||
WLStageUiProbeHooks.RaiseEntered(0, 3, 12, 60f, new Vector3(147f, 2.6f, 30f), 14f, 1.5f, 0);
|
||||
WLStageUiProbeHooks.RaiseStarted(0, 3, 12, 60f, 0);
|
||||
WLStageUiProbeHooks.RaiseEnemyCount(0, 5, 12);
|
||||
WLStageUiProbeHooks.RaiseTime(0, 8f, 60f);
|
||||
RunHud.ApplyLayout();
|
||||
RunResultUI.Hide();
|
||||
if (revive != null) revive.HideNow();
|
||||
sb.AppendLine("① HUD \"" + RunHud.PhaseText + "\" \"" + RunHud.TimerText + "\" \"" + RunHud.EnemyText + "\"");
|
||||
sb.AppendLine(Shot(cam, rt, Path.Combine(dir, "01_hud_stage.png")));
|
||||
|
||||
// ② 클리어 화면
|
||||
StageUiBridge.ProbeSetInStage(true);
|
||||
StageUiBridge.ProbeNoteHit();
|
||||
WLStageUiProbeHooks.RaiseChainEnded(6, 2, 1.4f);
|
||||
WLStageUiProbeHooks.RaiseCleared(0, 3, 34.2f, 60f, 12, 12, 1, false, false);
|
||||
RunResultUI.ApplyLayout();
|
||||
sb.AppendLine("② 클리어 \"" + RunResultUI.TitleText + "\" 행=" + RunResultUI.VisibleRows +
|
||||
" 배지=" + RunResultUI.BadgeVisible);
|
||||
sb.AppendLine(Shot(cam, rt, Path.Combine(dir, "02_clear.png")));
|
||||
|
||||
// ③ 실패 팝업
|
||||
RunResultUI.Hide();
|
||||
StageUiBridge.ProbeSetInStage(true);
|
||||
WLStageUiProbeHooks.RaiseFailed(0, 3, WL.Combat.Stage.StageFailReason.TimeLimit, 4, 12, 60f, 0);
|
||||
if (revive != null) revive.ApplyLayout();
|
||||
sb.AppendLine("③ 실패 \"" + (revive != null ? revive.LastMessage.Replace("\n", " / ") : "-") + "\" 버튼=[" +
|
||||
(revive != null ? revive.PrimaryButtonText : "") + "] [" +
|
||||
(revive != null ? revive.SecondaryButtonText : "") + "]");
|
||||
sb.AppendLine(Shot(cam, rt, Path.Combine(dir, "03_fail.png")));
|
||||
|
||||
cam.targetTexture = null;
|
||||
UnityEngine.Object.DestroyImmediate(rt);
|
||||
}
|
||||
catch (Exception e) { sb.AppendLine("예외 " + e.GetType().Name + " : " + e.Message + "\n" + e.StackTrace); }
|
||||
finally
|
||||
{
|
||||
try { StageUiBridge.Teardown(); RunHud.Teardown(); RunResultUI.Teardown(); } catch { }
|
||||
if (rig != null) UnityEngine.Object.DestroyImmediate(rig);
|
||||
if (prefabRoot != null) PrefabUtility.UnloadPrefabContents(prefabRoot);
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
static RectTransform NewRect(RectTransform parent, string name)
|
||||
{
|
||||
var go = new GameObject(name, typeof(RectTransform));
|
||||
go.layer = 5;
|
||||
var rt = (RectTransform)go.transform;
|
||||
rt.SetParent(parent, false);
|
||||
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one;
|
||||
rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero;
|
||||
rt.localScale = Vector3.one;
|
||||
return rt;
|
||||
}
|
||||
|
||||
static string Shot(Camera cam, RenderTexture rt, string path)
|
||||
{
|
||||
Canvas.ForceUpdateCanvases();
|
||||
cam.Render();
|
||||
var prev = RenderTexture.active;
|
||||
RenderTexture.active = rt;
|
||||
var tex = new Texture2D(rt.width, rt.height, TextureFormat.RGBA32, false);
|
||||
tex.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0);
|
||||
tex.Apply();
|
||||
RenderTexture.active = prev;
|
||||
var png = tex.EncodeToPNG();
|
||||
UnityEngine.Object.DestroyImmediate(tex);
|
||||
File.WriteAllBytes(path, png);
|
||||
return " → " + path + " (" + png.Length + " B · " + rt.width + "×" + rt.height + ")";
|
||||
}
|
||||
}
|
||||
|
|
@ -346,6 +346,10 @@ namespace WL.UI
|
|||
/// <summary>지금 화면의 알림 줄(카운트다운 · 실측 덤프용).</summary>
|
||||
public static string NoticeText { get { return s_notice != null ? s_notice.text : ""; } }
|
||||
public static bool NoticeVisible { get { return s_notice != null && s_notice.gameObject.activeSelf; } }
|
||||
/// <summary>존 게이지가 지금 화면에 있는가(스테이지 모드에서는 접힌다 · 실측 덤프용).</summary>
|
||||
public static bool ZoneGaugeVisible { get { return s_zoneRoot != null && s_zoneRoot.gameObject.activeSelf; } }
|
||||
/// <summary>남은 적 줄이 지금 화면에 있는가.</summary>
|
||||
public static bool EnemyVisible { get { return s_enemy != null && s_enemy.gameObject.activeSelf; } }
|
||||
|
||||
/// <summary>스테이지 모드 on/off. off 로 되돌리면 813q 런 HUD 표시가 그대로 살아난다.</summary>
|
||||
public static string SetStageMode(bool on)
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ namespace WL.UI
|
|||
// ── 진단(프로브가 읽는다 · 실측만) ────────────────────────────────────
|
||||
public static int EnteredSeen, CountdownSeen, StartedSeen, EnemySeen, TimeSeen, ClearedSeen, FailedSeen, ExitedSeen;
|
||||
public static int MenuHideCalls, MenuRestoreCalls, MenuMissing;
|
||||
public static int MenuReassertCalls, MenuReassertHits; // 원본이 다시 켠 것을 도로 끈 횟수
|
||||
public static int RetryCalls, NextCalls, LobbyCalls, RestartCalls;
|
||||
public static string LastLog = "";
|
||||
public static string LastMissingPath = "";
|
||||
|
|
@ -274,15 +275,40 @@ namespace WL.UI
|
|||
if (e.kills > s_maxChain) s_maxChain = e.kills;
|
||||
}
|
||||
|
||||
/// <summary>매 프레임 — 진행 중인 연쇄도 최대치에 반영한다(클리어 순간 끊기지 않은 연쇄).</summary>
|
||||
private static float s_nextReassert;
|
||||
|
||||
/// <summary>매 프레임 — 진행 중인 연쇄를 최대치에 반영하고, 숨김을 주기적으로 다시 건다.</summary>
|
||||
public static string Tick()
|
||||
{
|
||||
if (!s_inStage) return "스테이지 밖 — 무동작";
|
||||
int cur = KillChain.ChainKills;
|
||||
if (cur > s_maxChain) s_maxChain = cur;
|
||||
|
||||
// 🔴 원본 `NewGameUI.SetUI_byGameMode` 가 모드 전환 때 같은 노드를 다시 켠다 → 주기적 재보장
|
||||
// (813z 최상단 재보장과 같은 방식 · 값이 이미 맞으면 아무 것도 쓰지 않는다 = GC 0)
|
||||
var g = St;
|
||||
if (g == null || !g.hideMenusInStage || g.menuReassertSeconds <= 0f) return "tick ok";
|
||||
float now = Time.unscaledTime;
|
||||
if (now < s_nextReassert) return "tick ok";
|
||||
s_nextReassert = now + g.menuReassertSeconds;
|
||||
ReassertHide();
|
||||
return "tick ok";
|
||||
}
|
||||
|
||||
/// <summary>숨긴 목록 중 다시 켜진 것을 도로 끈다(문자열 0 · 할당 0). 켠 횟수를 돌려준다.</summary>
|
||||
public static int ReassertHide()
|
||||
{
|
||||
int n = 0;
|
||||
for (int i = 0; i < s_hiddenCount; i++)
|
||||
{
|
||||
var t = s_hidden[i];
|
||||
if (t == null) continue;
|
||||
if (t.gameObject.activeSelf) { t.gameObject.SetActive(false); n++; MenuReassertHits++; }
|
||||
}
|
||||
if (n > 0) MenuReassertCalls++;
|
||||
return n;
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════
|
||||
// 버튼 동작 (815b 공개 API 만 부른다)
|
||||
// ═════════════════════════════════════════════════════════════════════
|
||||
|
|
@ -406,6 +432,15 @@ namespace WL.UI
|
|||
return s_uiRoot;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 프로브 전용 — 피격 1건을 그대로 센다. 🔴 실제 경로(`CombatEvents.Damaged` 의 `victim == MyValue.MyPC`)는
|
||||
/// 에디트 모드에 PC 가 없어 태울 수 없다(813d 미확인 ②와 같은 한계). 필터가 **거르는 쪽**은 실측 가능하다.
|
||||
/// </summary>
|
||||
public static string ProbeNoteHit() { s_hits++; return "피격 " + s_hits + "회"; }
|
||||
|
||||
/// <summary>프로브 전용 — 스테이지 안 상태를 만든다(이벤트 없이).</summary>
|
||||
public static string ProbeSetInStage(bool on) { s_inStage = on; return "inStage=" + s_inStage; }
|
||||
|
||||
/// <summary>프로브·에디트 모드용 — 루트를 직접 지정한다.</summary>
|
||||
public static string SetUiRootForProbe(Transform root)
|
||||
{
|
||||
|
|
@ -456,6 +491,7 @@ namespace WL.UI
|
|||
{
|
||||
EnteredSeen = CountdownSeen = StartedSeen = EnemySeen = TimeSeen = ClearedSeen = FailedSeen = ExitedSeen = 0;
|
||||
MenuHideCalls = MenuRestoreCalls = MenuMissing = 0;
|
||||
MenuReassertCalls = MenuReassertHits = 0; s_nextReassert = 0f;
|
||||
RetryCalls = NextCalls = LobbyCalls = RestartCalls = 0;
|
||||
s_hits = s_maxChain = 0; s_potionsAtStart = PotionUse.UsedCount;
|
||||
LastLog = ""; LastMissingPath = "";
|
||||
|
|
@ -485,7 +521,9 @@ namespace WL.UI
|
|||
sb.AppendLine(" 실측 — 피격 " + s_hits + "회 · 물약 " + PotionsUsed + "개(시작 스냅 " + s_potionsAtStart +
|
||||
" / 현재 " + PotionUse.UsedCount + ") · 최대 연쇄 " + s_maxChain +
|
||||
"(진행중 " + KillChain.ChainKills + ")");
|
||||
sb.AppendLine(" 메뉴 — 숨김 " + MenuHideCalls + "회 / 복원 " + MenuRestoreCalls + "회 · 지금 숨긴 것 " +
|
||||
sb.AppendLine(" 메뉴 — 숨김 " + MenuHideCalls + "회 / 복원 " + MenuRestoreCalls + "회 / 재보장 " +
|
||||
MenuReassertCalls + "회(도로 끈 것 " + MenuReassertHits + "건 · 주기 " +
|
||||
(g != null ? g.menuReassertSeconds.ToString("F2") : "-") + "s) · 지금 숨긴 것 " +
|
||||
s_hiddenCount + "건 · 못 찾은 경로 " + MenuMissing +
|
||||
(string.IsNullOrEmpty(LastMissingPath) ? "" : "(\"" + LastMissingPath + "\")"));
|
||||
for (int i = 0; i < s_hiddenCount; i++)
|
||||
|
|
@ -507,6 +545,105 @@ namespace WL.UI
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 🔴 **검증 전용** — 에디트 모드 프로브가 815b `StageEvents` 의 **실제 Dispatch 경로**로
|
||||
/// 가짜 이벤트를 흘려 넣는 진입점(813q `WLRunUiProbeHooks` 선례).
|
||||
/// `CombatEventList<T>.Dispatch` 는 `internal` 이라 Assets 안(같은 Assembly-CSharp)에서만 부를 수 있다.
|
||||
/// 전부 **void** 다 — 반환 문자열을 만들면 GC 측정이 오염된다. 게임 코드는 한 번도 부르지 않는다.
|
||||
/// </summary>
|
||||
public static class WLStageUiProbeHooks
|
||||
{
|
||||
public static void RaiseEntered(int index, int stageId, int total, float timeLimit, Vector3 anchor,
|
||||
float radius, float countdown, int retries)
|
||||
{
|
||||
var e = new WL.Combat.Stage.StageEnteredEvent
|
||||
{
|
||||
index = index, stageId = stageId, def = null, totalEnemies = total, timeLimit = timeLimit,
|
||||
anchor = anchor, barrierRadius = radius, countdownSeconds = countdown, retries = retries,
|
||||
time = Time.unscaledTime, frame = Time.frameCount,
|
||||
};
|
||||
WL.Combat.Stage.StageEvents.StageEntered.Dispatch(in e);
|
||||
}
|
||||
|
||||
public static void RaiseCountdown(int index, float remaining, float total)
|
||||
{
|
||||
var e = new WL.Combat.Stage.StageCountdownEvent
|
||||
{ index = index, remaining = remaining, total = total, time = Time.unscaledTime, frame = Time.frameCount };
|
||||
WL.Combat.Stage.StageEvents.CountdownTick.Dispatch(in e);
|
||||
}
|
||||
|
||||
public static void RaiseStarted(int index, int stageId, int total, float timeLimit, int retries)
|
||||
{
|
||||
var e = new WL.Combat.Stage.StageStartedEvent
|
||||
{
|
||||
index = index, stageId = stageId, totalEnemies = total, timeLimit = timeLimit, retries = retries,
|
||||
time = Time.unscaledTime, frame = Time.frameCount,
|
||||
};
|
||||
WL.Combat.Stage.StageEvents.StageStarted.Dispatch(in e);
|
||||
}
|
||||
|
||||
public static void RaiseEnemyCount(int index, int remaining, int total)
|
||||
{
|
||||
var e = new WL.Combat.Stage.StageEnemyCountEvent
|
||||
{ index = index, remaining = remaining, total = total, time = Time.unscaledTime, frame = Time.frameCount };
|
||||
WL.Combat.Stage.StageEvents.EnemyCountChanged.Dispatch(in e);
|
||||
}
|
||||
|
||||
public static void RaiseTime(int index, float remaining, float limit)
|
||||
{
|
||||
var e = new WL.Combat.Stage.StageTimeEvent
|
||||
{ index = index, remaining = remaining, limit = limit, time = Time.unscaledTime, frame = Time.frameCount };
|
||||
WL.Combat.Stage.StageEvents.TimeChanged.Dispatch(in e);
|
||||
}
|
||||
|
||||
public static void RaiseCleared(int index, int stageId, float elapsed, float limit, int killed, int total,
|
||||
int retries, bool isLast, bool isBoss)
|
||||
{
|
||||
var e = new WL.Combat.Stage.StageClearResult
|
||||
{
|
||||
index = index, stageId = stageId, win = true, failReason = WL.Combat.Stage.StageFailReason.None,
|
||||
elapsedSec = elapsed, timeLimit = limit, killed = killed, total = total, remaining = total - killed,
|
||||
retries = retries, isLastStage = isLast, isBoss = isBoss,
|
||||
time = Time.unscaledTime, frame = Time.frameCount,
|
||||
};
|
||||
WL.Combat.Stage.StageEvents.StageCleared.Dispatch(in e);
|
||||
}
|
||||
|
||||
public static void RaiseFailed(int index, int stageId, WL.Combat.Stage.StageFailReason reason,
|
||||
int remaining, int total, float elapsed, int retries)
|
||||
{
|
||||
var e = new WL.Combat.Stage.StageFailedEvent
|
||||
{
|
||||
index = index, stageId = stageId, reason = reason, remaining = remaining, total = total,
|
||||
elapsedSec = elapsed, retries = retries, time = Time.unscaledTime, frame = Time.frameCount,
|
||||
};
|
||||
WL.Combat.Stage.StageEvents.StageFailed.Dispatch(in e);
|
||||
}
|
||||
|
||||
public static void RaiseExited(int index, bool toLobby, string reason)
|
||||
{
|
||||
var e = new WL.Combat.Stage.StageExitedEvent
|
||||
{ index = index, toLobby = toLobby, reason = reason, time = Time.unscaledTime, frame = Time.frameCount };
|
||||
WL.Combat.Stage.StageEvents.StageExited.Dispatch(in e);
|
||||
}
|
||||
|
||||
/// <summary>피격 집계 검증용(같은 Dispatch 경로 · victim = 실제 PC 가 있어야 세어진다).</summary>
|
||||
public static void RaiseDamaged(Actor victim)
|
||||
{
|
||||
var e = new DamagedEvent { victim = victim, attacker = null, damage = 1.0, invincible = false,
|
||||
time = Time.unscaledTime, frame = Time.frameCount };
|
||||
CombatEvents.Damaged.Dispatch(in e);
|
||||
}
|
||||
|
||||
/// <summary>최대 연쇄 검증용(811e KillChain 의 실제 Dispatch 경로).</summary>
|
||||
public static void RaiseChainEnded(int kills, int bestTier, float duration)
|
||||
{
|
||||
var e = new KillChainEndedEvent { kills = kills, bestTier = bestTier, duration = duration,
|
||||
time = Time.unscaledTime, frame = Time.frameCount };
|
||||
KillChain.Ended.Dispatch(in e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>스테이지 UI 이음매의 시간 축(숨김 GameObject 1개 · 씬·프리팹에 굽지 않는다).</summary>
|
||||
internal sealed class StageUiRunner : MonoBehaviour
|
||||
{
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ MonoBehaviour:
|
|||
- IngameUIs/WL_HUD/WL_ReviveDialog
|
||||
keepLookToggleButtons: 1
|
||||
restoreMenusOnExit: 1
|
||||
menuReassertSeconds: 0.5
|
||||
stageResultEnabled: 1
|
||||
stageClearTitleFormat: "\uC2A4\uD14C\uC774\uC9C0 {0} \uD074\uB9AC\uC5B4!"
|
||||
stageAllClearTitleFormat: "\uC804 \uC2A4\uD14C\uC774\uC9C0 \uD074\uB9AC\uC5B4!"
|
||||
|
|
|
|||
|
|
@ -205,6 +205,11 @@ namespace WL.UI
|
|||
[Tooltip("스테이지를 나갈 때 숨긴 노드를 원래 activeSelf 로 되돌린다(끄면 숨긴 채로 둔다 — 진단 전용).")]
|
||||
public bool restoreMenusOnExit = true;
|
||||
|
||||
[Tooltip("🔴 스테이지 중 이 주기(초 · unscaled)마다 숨김을 다시 건다. 원본 `NewGameUI.SetUI_byGameMode` 가 " +
|
||||
"모드 전환 때 같은 노드를 다시 켜기 때문(813z 최상단 재보장과 같은 방식 · 값이 이미 맞으면 아무 것도 쓰지 않는다 = GC 0). " +
|
||||
"0 이면 재보장 없음(입장 1회만).")]
|
||||
public float menuReassertSeconds = 0.5f;
|
||||
|
||||
// ── 클리어 화면 (기준서 §F-1) ────────────────────────────────────────
|
||||
[Header("클리어 화면 (기준서 §F-1 · 813q RunResultUI 재사용)")]
|
||||
[Tooltip("끄면 스테이지 클리어에 813q 결과창이 **런 모드 그대로** 뜬다(제목 「런 클리어!」 · [다음 런]).")]
|
||||
|
|
|
|||
Loading…
Reference in New Issue