From d8174b77a5158d00c8b9e8e262158e850fe245e8 Mon Sep 17 00:00:00 2001 From: swrring Date: Fri, 11 Sep 2026 01:36:55 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[WL-815d]=20=EC=A4=91=EA=B0=84:=20=EC=9E=A5?= =?UTF-8?q?=EB=B2=BD=20=ED=9B=85=202=EC=A4=84=20=C2=B7=20HUD=20=EC=8A=A4?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EC=A7=80=20=EB=AA=A8=EB=93=9C(3=EC=9A=94?= =?UTF-8?q?=EC=86=8C)=20=C2=B7=20=ED=81=B4=EB=A6=AC=EC=96=B4/=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=20=ED=99=94=EB=A9=B4=202=EB=B2=84=ED=8A=BC=20=C2=B7?= =?UTF-8?q?=20=EB=A9=94=EB=89=B4=20=EC=88=A8=EA=B9=80=20SO=20=C2=B7=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=20=EC=9E=85=EC=9E=A5=20(#815)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- .../Resources/WL/WLStageSettings.asset | 6 +- .../Resources/WL/WLStageBarrierSettings.asset | 2 +- Assets/WL/Combat/Stage/StageBarrierBinding.cs | 76 +++ .../Combat/Stage/StageBarrierBinding.cs.meta | 2 + Assets/WL/UI/Scripts/ReviveDialog.cs | 161 +++++- Assets/WL/UI/Scripts/RunHud.cs | 200 ++++++- Assets/WL/UI/Scripts/RunResultUI.cs | 307 +++++++++- Assets/WL/UI/Scripts/StageUiBridge.cs | 540 ++++++++++++++++++ Assets/WL/UI/Scripts/StageUiBridge.cs.meta | 2 + .../Resources/WL/WLStageUiSettings.asset | 98 ++++ .../Resources/WL/WLStageUiSettings.asset.meta | 8 + Assets/WL/UI/Settings/WLStageUiSettings.cs | 332 +++++++++++ .../WL/UI/Settings/WLStageUiSettings.cs.meta | 2 + 13 files changed, 1710 insertions(+), 26 deletions(-) create mode 100644 Assets/WL/Combat/Stage/StageBarrierBinding.cs create mode 100644 Assets/WL/Combat/Stage/StageBarrierBinding.cs.meta create mode 100644 Assets/WL/UI/Scripts/StageUiBridge.cs create mode 100644 Assets/WL/UI/Scripts/StageUiBridge.cs.meta create mode 100644 Assets/WL/UI/Settings/Resources/WL/WLStageUiSettings.asset create mode 100644 Assets/WL/UI/Settings/Resources/WL/WLStageUiSettings.asset.meta create mode 100644 Assets/WL/UI/Settings/WLStageUiSettings.cs create mode 100644 Assets/WL/UI/Settings/WLStageUiSettings.cs.meta diff --git a/Assets/WL/Combat/Settings/Resources/WL/WLStageSettings.asset b/Assets/WL/Combat/Settings/Resources/WL/WLStageSettings.asset index 924902897..f4a6f7362 100644 --- a/Assets/WL/Combat/Settings/Resources/WL/WLStageSettings.asset +++ b/Assets/WL/Combat/Settings/Resources/WL/WLStageSettings.asset @@ -17,7 +17,7 @@ MonoBehaviour: pollSeconds: 0.1 tickIntervalSeconds: 1 pauseTimerWhileWorldHeld: 1 - autoEnterOnMapEnter: 0 + autoEnterOnMapEnter: 1 warpPlayerToAnchor: 1 useBarrierHook: 1 clearBarrierOnExit: 1 @@ -32,7 +32,7 @@ MonoBehaviour: retryRecallsMonsters: 1 retryResetsPotions: 1 retryHealsPlayer: 1 - bindReviveOkToRetry: 1 + bindReviveOkToRetry: 0 clearHoldScale: 0 mirrorRunEnded: 1 mirrorRunHudEvents: 1 @@ -41,4 +41,4 @@ MonoBehaviour: suppressRunDirector: 1 disableRunGuide: 1 resetRunDirectorOnEnter: 1 - toLobbyCallsReturnToLobby: 0 + toLobbyCallsReturnToLobby: 1 diff --git a/Assets/WL/Combat/Stage/Barrier/Resources/WL/WLStageBarrierSettings.asset b/Assets/WL/Combat/Stage/Barrier/Resources/WL/WLStageBarrierSettings.asset index 0a7ec290a..1c2efafe3 100644 --- a/Assets/WL/Combat/Stage/Barrier/Resources/WL/WLStageBarrierSettings.asset +++ b/Assets/WL/Combat/Stage/Barrier/Resources/WL/WLStageBarrierSettings.asset @@ -37,7 +37,7 @@ MonoBehaviour: carveHeight: 3 carveOverlap: 0.15 carveStaticOnly: 1 - style: 1 + style: 2 showVisual: 1 appearSeconds: 0.6 fadeSeconds: 0.6 diff --git a/Assets/WL/Combat/Stage/StageBarrierBinding.cs b/Assets/WL/Combat/Stage/StageBarrierBinding.cs new file mode 100644 index 000000000..e19426d7b --- /dev/null +++ b/Assets/WL/Combat/Stage/StageBarrierBinding.cs @@ -0,0 +1,76 @@ +// ───────────────────────────────────────────────────────────────────────────── +// StageBarrierBinding.cs — 815b 장벽 훅 ↔ 815c 장벽 구현을 잇는 **2줄** +// +// PD 지시 #815 · 발주서 WL-815d §1-1 (2026-09-11) +// 근거 = 815b 보고 §3(계약 API 표 · `StageBarrierHook.Set/Clear`) · §6(「Lead 연결 2줄」) +// 815c 보고 §1(「`StageBarrierHook` 은 정의하지 않았다 — Lead 가 2줄만 부르면 된다」) +// +// ■ 이 파일이 하는 일은 딱 이것뿐이다 +// StageBarrierHook.Set = StageBarrier.Set; // (Vector3 center, float radius) +// StageBarrierHook.Clear = StageBarrier.Clear; // () +// 시그니처가 완전히 같아서 람다·어댑터가 필요 없다(815b §6 실측). +// 둘 다 같은 네임스페이스 `WL.Combat.Stage` 라 using 도 필요 없다 +// (`StageBarrierHook` = `Stage/StageContracts.cs` · `StageBarrier` = `Stage/Barrier/StageBarrier.cs`). +// +// ■ 왜 별도 파일인가 +// 815b `StageDirector` / `StageDirectorRunner` 와 815c `Stage/Barrier/**` 를 **한 줄도 건드리지 않고** +// 이음매만 새 파일로 둔다(발주서 §2 ⓑ). 이 파일을 지우면 장벽은 no-op 로 돌아간다 = 롤백 1파일. +// +// ■ C8 롤백 3단 +// ① 이 파일 삭제 또는 `StageBarrierBinding.Unbind()` → 훅 null = no-op(815b 계약) +// ② `WLStageSettings.useBarrierHook = 0` → StageDirector 가 훅을 부르지 않는다 +// ③ `WLStageBarrierSettings.enabled = 0` → 815c 가 스스로 무동작(오브젝트 0) +// +// 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다(ErrorLogHookManager 가 플레이어 팝업을 띄운다). +// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것. +// ───────────────────────────────────────────────────────────────────────────── + +using UnityEngine; + +namespace WL.Combat.Stage +{ + /// 815b 장벽 훅에 815c 구현을 대입한다(정적 · 오브젝트 0 · 값 0). + public static class StageBarrierBinding + { + /// 지금 훅이 815c 구현을 가리키고 있는가(프로브가 읽는다). + public static bool Bound { get; private set; } + + /// 대입 횟수(진단). + public static int BindCount, UnbindCount; + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + private static void Boot() { Bind(); } + + /// 훅 2줄. 몇 번 불러도 결과가 같다(멱등). + public static string Bind() + { + StageBarrierHook.Set = StageBarrier.Set; + StageBarrierHook.Clear = StageBarrier.Clear; + Bound = true; + BindCount++; + return "장벽 훅 연결 — Set/Clear = WL.Combat.Stage.StageBarrier (bind " + BindCount + "회)"; + } + + /// 훅을 떼어 낸다(C8 ① · 프로브 A/B). 뗀 뒤에는 815b 가 값만 기록하고 아무 일도 하지 않는다. + public static string Unbind() + { + StageBarrierHook.Set = null; + StageBarrierHook.Clear = null; + Bound = false; + UnbindCount++; + return "장벽 훅 해제 — no-op 복귀(unbind " + UnbindCount + "회)"; + } + + /// 연결 상태 덤프(실측용). + public static string Dump() + { + return "[StageBarrierBinding] bound=" + Bound + + " hasSet=" + StageBarrierHook.HasSet + " hasClear=" + StageBarrierHook.HasClear + + " · 훅 호출 set=" + StageBarrierHook.SetCalls + " clear=" + StageBarrierHook.ClearCalls + + " faults=" + StageBarrierHook.Faults + + " · 마지막 " + StageBarrierHook.LastCenter.ToString("F1") + " r=" + StageBarrierHook.LastRadius.ToString("F1") + + " · 815c 실행 set=" + StageBarrier.SetCount + " clear=" + StageBarrier.ClearCount + + " active=" + StageBarrier.Active + " featureEnabled=" + StageBarrier.FeatureEnabled; + } + } +} diff --git a/Assets/WL/Combat/Stage/StageBarrierBinding.cs.meta b/Assets/WL/Combat/Stage/StageBarrierBinding.cs.meta new file mode 100644 index 000000000..43b03be5b --- /dev/null +++ b/Assets/WL/Combat/Stage/StageBarrierBinding.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8d815d01a1b2c3d4e5f60708090a0c03 diff --git a/Assets/WL/UI/Scripts/ReviveDialog.cs b/Assets/WL/UI/Scripts/ReviveDialog.cs index 04874f93e..9c836bdcf 100644 --- a/Assets/WL/UI/Scripts/ReviveDialog.cs +++ b/Assets/WL/UI/Scripts/ReviveDialog.cs @@ -53,6 +53,20 @@ namespace WL.UI /// 팝업이 뜰 때 불린다(진단·튜토리얼용 · 선택). public static System.Action ReviveShown; + // ── WL-815d 스테이지 실패 팝업 (기준서 §E) ─────────────────────────── + // 스테이지 중 실패(사망 · 60 s 초과)는 「부활」 1버튼이 아니라 **「다시 도전 / 로비로」 2버튼**이다. + // · 815b `WLStageSettings.bindReviveOkToRetry` 는 **0** 으로 내려간다(임시 배선 제거). + // · 자동 부활 0(`WLSurvivalSettings.reviveWatchdogSeconds = 0`) 은 815b 가 이미 내려 두었다. + // · 813z 3층 방어(전용 Canvas sortingOrder 60 · 재보장 · 탈취 승격)는 **그대로 살아 있다**. + // · 공용 Popup 경로는 쓰지 않는다 — 버튼 문구 2개를 프리팹 수정 없이 넣을 수 없기 때문(813q 실측과 같은 이유). + /// [다시 도전] — 대입되어 있으면 [확인] 대신 이것이 불린다(815d `StageUiBridge`). + public static System.Action PrimaryOverride; + /// [로비로] — 대입되어 있으면 두 번째 버튼이 생긴다. + public static System.Action SecondaryAction; + /// 지금 스테이지 실패 팝업 모드인가(문구·버튼 2개). + public static bool StageMode { get; private set; } + public static int StagePrimaryCount, StageSecondaryCount, StageShownCount; + /// 현재 씬에 살아 있는 인스턴스(정적 진입점이 찾는다). public static ReviveDialog Active { get; private set; } @@ -96,6 +110,9 @@ namespace WL.UI [SerializeField] private TextMeshProUGUI messageLabel; [SerializeField] private Button okButton; [SerializeField] private TextMeshProUGUI okLabel; + // WL-815d — 스테이지 실패 팝업의 두 번째 버튼(기준서 §E 「다시 도전 / 로비로」) + [SerializeField] private Button secondaryButton; + [SerializeField] private TextMeshProUGUI secondaryLabel; [Header("폰트 (에디터가 주입)")] [SerializeField] private TMP_FontAsset font; @@ -108,6 +125,8 @@ namespace WL.UI // ── WL-813z(D-3) 최상단 보장 ────────────────────────────────────────── /// 런타임 생성 확인 버튼 이름 — 원본 `NightmareResult/btn_ok` 와 겹치지 않게 접두어를 붙인다(D-9). public const string OkButtonName = "WL_btn_ok"; + /// WL-815d — 두 번째 버튼 이름(같은 D-9 규약으로 `WL_` 접두). + public const string SecondaryButtonName = "WL_btn_second"; private Canvas _ownCanvas; private GraphicRaycaster _ownRaycaster; private float _nextTopmostAt = -1f; @@ -263,7 +282,8 @@ namespace WL.UI EnsureTopmost(g); _nextTopmostAt = Time.unscaledTime + (g != null ? Mathf.Max(0f, g.reviveTopmostIntervalSeconds) : 0f); - if (s.reviveUseCommonPopup) + // 🔴 WL-815d — 스테이지 실패는 버튼이 2개라 공용 Popup(문구 1개 · 프리팹 고정 버튼)으로 못 만든다. + if (s.reviveUseCommonPopup && !StageMode) { string r = TryCommonPopup(s, msg); if (UsedCommonPopup) @@ -281,6 +301,8 @@ namespace WL.UI /// 본문 = 설정 문자열(로컬라이즈되면 reviveMessageKey 우선) + 비용 줄. public string BuildMessage(WLSurvivalUiSettings s) { + // WL-815d — 스테이지 실패 문구(사망 / 시간 초과)가 있으면 그것이 이긴다(비용 줄 없음). + if (StageMode && !string.IsNullOrEmpty(_msgOverride)) return _msgOverride; string msg = s.reviveMessage; if (s.reviveMessageKey > 0) { @@ -319,16 +341,74 @@ namespace WL.UI EnsureTopmost(WLSurvivalSettings.Instance); // WL-813z(D-3) — 승격 직후에도 최상단·레이캐스트 보장 } - /// 확인 = 부활 요청. 813i 가 ReviveRequested 를 채우면 실제 부활이 일어난다. + /// 확인 = 부활 요청. 813i 가 ReviveRequested 를 채우면 실제 부활이 일어난다. + /// 🔴 WL-815d: 스테이지 실패 팝업이면 [다시 도전](`StageDirector.Retry()`)이 대신 불린다. public string OnConfirm() { ConfirmCount++; + bool stage = StageMode && PrimaryOverride != null; HideNow(); + if (stage) + { + StagePrimaryCount++; + try { PrimaryOverride(); } catch { } + return "다시 도전 — StageDirector.Retry() 전달"; + } if (ReviveRequested == null) return "확인 — 813i(ReviveRequested) 미연결이라 부활 동작 없음(후속)"; try { ReviveRequested(); } catch { } return "확인 — 부활 요청 전달"; } + /// WL-815d — 두 번째 버튼([로비로]). + public string OnSecondary() + { + HideNow(); + if (SecondaryAction == null) return "두 번째 버튼 — 핸들러 미연결(무동작)"; + StageSecondaryCount++; + try { SecondaryAction(); } catch { } + return "로비로 — 전달"; + } + + // ── WL-815d 스테이지 실패 팝업 진입점 ──────────────────────────────── + private string _msgOverride, _okOverride, _secondOverride; + + /// + /// 스테이지 실패 팝업을 **지금** 연다(2버튼 · 전용 폴백 패널 고정 · 공용 Popup 미사용). + /// 사망 경로(DeathFlow → NotifyReviveRequested)로 이미 떠 있으면 문구·버튼만 갈아 끼운다. + /// + public string ShowStageFail(string message, string okText, string secondaryText) + { + var s = WLSurvivalUiSettings.Instance; + if (s == null) return "WLSurvivalUiSettings 에셋 없음"; + StageMode = true; + _msgOverride = message; _okOverride = okText; _secondOverride = secondaryText; + _showAt = -1f; + _open = true; + StageShownCount++; + ShownCount++; + LastMessage = message ?? ""; + UsedCommonPopup = false; // 🔴 2버튼은 공용 Popup 으로 못 만든다 → 전용 패널 + _escalated = false; + + var g = WLSurvivalSettings.Instance; + EnsureTopmost(g); // 813z 3층 방어 그대로 + _nextTopmostAt = Time.unscaledTime + (g != null ? Mathf.Max(0f, g.reviveTopmostIntervalSeconds) : 0f); + + ShowFallback(s, LastMessage); + if (ReviveShown != null) { try { ReviveShown(); } catch { } } + return "스테이지 실패 팝업 2버튼 표시 · msg=\"" + LastMessage.Replace("\n", " / ") + + "\" [" + okText + "] [" + secondaryText + "] · sortingOrder=" + SortingOrder; + } + + /// 스테이지 실패 팝업 모드를 끈다(813z 원래 1버튼 부활 팝업으로 복귀). + public string ClearStageMode() + { + StageMode = false; + _msgOverride = _okOverride = _secondOverride = null; + ApplyLayout(); + return "스테이지 실패 팝업 모드 해제 — 813z 원래대로(버튼 1개)"; + } + public void HideNow() { _showAt = -1f; @@ -417,12 +497,40 @@ namespace WL.UI if (font != null) okLabel.font = font; made = true; } + // WL-815d — 두 번째 버튼(스테이지 실패 「로비로」). 평소에는 SetActive(false) 라 813z 화면은 그대로다. + if (secondaryButton == null) + { + var t = WLVignetteUtil.NewChild(box, SecondaryButtonName); + var img = t.GetComponent(); + if (img == null) img = t.gameObject.AddComponent(); + img.raycastTarget = true; + secondaryButton = t.GetComponent