// ───────────────────────────────────────────────────────────────────────────── // ReviveDialog.cs — 사망 시 부활 팝업 (WL-813tj · 813j · #813) // // 기준서 v1 §D-1 813j 행: "사망 시 부활 팝업(공용 팝업 SortOrder_5 재사용 · // '존 시작점에서 부활' · 비용 항목은 PD BM 전까지 무료)" // §B 요소 8: 사망 연출 2 s → 부활(존 시작점 · 비용 = PD BM). // // ■ 🔴 사망 통지는 813i(Gameplay) 가 준다 — 실측 근거 // `CombatEvents.RaiseKilled` 의 호출처는 `Assets/Script/Character/Mob/MobActor.cs:435` **하나뿐**이라 // PC 사망은 어떤 811 이벤트로도 오지 않는다(2026-09-09 실측 · grep 전수). // `wl/gameplay/WL-813i-survival` 브랜치는 main 대비 커밋 0 = `WL.Combat.Survival.DeathFlow` 도 없다. // 그래서 존재하지 않는 타입을 참조하지 않고 **정적 진입점**만 열어 둔다(「후속」): // WL.UI.ReviveDialog.NotifyDeath(); // 813i DeathFlow 가 사망 연출 뒤 1줄 // WL.UI.ReviveDialog.ReviveRequested = () => ...; // 813i 가 존 시작점 On_Regen 을 붙인다 // 813i 없이도 팝업은 "표시 + 확인" 까지 완전히 동작한다(부활 동작만 비어 있다). // // ■ 공용 팝업 재사용 // `Assets/Script/Info/Popup.cs`(싱글턴 `Popup.Ins` · Addressables 프리팹 `SortOrder_5` 안 · 802b 가 // 같은 프리팹에 SafeAreaFitter 를 붙였다)를 그대로 쓴다. 문구는 localtext 키가 없으므로 // `Set()` 직후 `label_msg.text` 를 설정 문자열로 덮는다(원본 Popup.cs **무수정**). // 공용 팝업이 없거나(씬 미로드) 표가 없으면 자체 폴백 노드로 떨어진다. // // ■ 값 = WLSurvivalUiSettings.asset (C45) · 시간 = Time.unscaledTime // 🔴 어셈블리 주의: Assets/WL/UI/ 에 .asmdef 를 만들지 말 것(Assembly-CSharp 단일). // ───────────────────────────────────────────────────────────────────────────── using System.Text; using TMPro; using UnityEngine; using UnityEngine.UI; namespace WL.UI { [DisallowMultipleComponent] [RequireComponent(typeof(RectTransform))] public class ReviveDialog : MonoBehaviour { // ── 813i 연결점 (「후속」) ──────────────────────────────────────────── /// 확인을 누르면 불린다. 813i 가 존 시작점 부활을 붙인다. public static System.Action ReviveRequested; /// 팝업이 뜰 때 불린다(진단·튜토리얼용 · 선택). public static System.Action ReviveShown; /// 현재 씬에 살아 있는 인스턴스(정적 진입점이 찾는다). public static ReviveDialog Active { get; private set; } /// 813i DeathFlow 가 사망 연출 뒤 부르는 진입점. 인스턴스가 없으면 무동작. public static string NotifyDeath() { if (Active == null) return "ReviveDialog 인스턴스 없음 — 무동작"; return Active.QueueShow(Time.unscaledTime); } [Header("폴백 팝업 구성 요소 (공용 Popup 이 없을 때만 쓴다)")] [SerializeField] private CanvasGroup group; [SerializeField] private RectTransform box; [SerializeField] private Image dim; [SerializeField] private Image boxBg; [SerializeField] private TextMeshProUGUI messageLabel; [SerializeField] private Button okButton; [SerializeField] private TextMeshProUGUI okLabel; [Header("폰트 (에디터가 주입)")] [SerializeField] private TMP_FontAsset font; private RectTransform _rt; private Canvas _canvas; private float _showAt = -1f; // ── 진단 ────────────────────────────────────────────────────────────── public int DeathSeen { get; private set; } public int ShownCount { get; private set; } public int ConfirmCount { get; private set; } public bool UsedCommonPopup { get; private set; } public string LastMessage { get; private set; } = ""; public bool FallbackVisible { get { return group != null && group.alpha > 0.001f; } } public bool Pending { get { return _showAt >= 0f; } } private void Awake() { _rt = GetComponent(); Active = this; } private void OnEnable() { Active = this; Initialize(); } private void OnDisable() { if (Active == this) Active = null; } public string Initialize() { if (_rt == null) _rt = GetComponent(); Active = this; BuildIfNeeded(); string layout = ApplyLayout(); HideNow(); return "initialized · " + layout; } /// 사망 통지 → 설정된 지연 뒤에 팝업을 연다(사망 연출 시간은 813i 소유). public string QueueShow(float now) { DeathSeen++; var s = WLSurvivalUiSettings.Instance; if (s == null) return "WLSurvivalUiSettings 에셋 없음 — 무동작"; if (!s.reviveEnabled) return "reviveEnabled=false — 무동작"; _showAt = now + Mathf.Max(0f, s.reviveDelaySeconds); if (s.verboseLog) Debug.Log("[ReviveDialog] 사망 통지 — " + s.reviveDelaySeconds + "s 뒤 팝업"); return "예약 delay=" + s.reviveDelaySeconds.ToString("F2") + "s"; } private void Update() { Tick(Time.unscaledTime); } /// 지연 만료 검사. 프로브가 시간을 넣어 같은 경로로 검증한다. public void Tick(float now) { if (_showAt < 0f || now < _showAt) return; _showAt = -1f; ShowNow(); } /// 지금 바로 팝업을 연다(공용 Popup 우선 · 실패하면 폴백 노드). public string ShowNow() { var s = WLSurvivalUiSettings.Instance; if (s == null) return "WLSurvivalUiSettings 에셋 없음"; string msg = BuildMessage(s); LastMessage = msg; ShownCount++; UsedCommonPopup = false; if (s.reviveUseCommonPopup) { string r = TryCommonPopup(s, msg); if (UsedCommonPopup) { if (ReviveShown != null) { try { ReviveShown(); } catch { } } return r; } } ShowFallback(s, msg); if (ReviveShown != null) { try { ReviveShown(); } catch { } } return "폴백 팝업 표시 msg=\"" + msg + "\""; } /// 본문 = 설정 문자열(로컬라이즈되면 reviveMessageKey 우선) + 비용 줄. public string BuildMessage(WLSurvivalUiSettings s) { string msg = s.reviveMessage; if (s.reviveMessageKey > 0) { try { if (table_localtext.Ins != null) msg = table_localtext.Ins.Get_Text(s.reviveMessageKey); } catch { } } if (s.reviveShowCostLine && !string.IsNullOrEmpty(s.reviveCostText)) msg += "\n" + s.reviveCostText; return msg; } /// 공용 Popup 싱글턴 재사용. 실패(미로드·표 없음)하면 UsedCommonPopup=false 로 남긴다. private string TryCommonPopup(WLSurvivalUiSettings s, string msg) { try { if (Popup.Ins == null) return "공용 Popup 없음 — 폴백"; Popup.Ins.Set(ePopupType.One, s.reviveMessageKey, OnOkListener); // Action = void 시그니처 if (Popup.Ins.label_msg != null) Popup.Ins.label_msg.text = msg; // 문구는 SO 가 주인 UsedCommonPopup = true; return "공용 Popup(SortOrder_5) 표시 msg=\"" + msg + "\""; } catch (System.Exception e) { UsedCommonPopup = false; return "공용 Popup 실패(" + e.GetType().Name + ") — 폴백"; } } private void ShowFallback(WLSurvivalUiSettings s, string msg) { BuildIfNeeded(); ApplyLayout(); if (messageLabel != null) messageLabel.text = msg; if (group != null) { group.alpha = 1f; group.blocksRaycasts = true; group.interactable = true; } } /// 확인 = 부활 요청. 813i 가 ReviveRequested 를 채우면 실제 부활이 일어난다. public string OnConfirm() { ConfirmCount++; HideNow(); if (ReviveRequested == null) return "확인 — 813i(ReviveRequested) 미연결이라 부활 동작 없음(후속)"; try { ReviveRequested(); } catch { } return "확인 — 부활 요청 전달"; } public void HideNow() { _showAt = -1f; if (group == null) return; group.alpha = 0f; group.blocksRaycasts = false; group.interactable = false; } // ── 구성 · 배치 (폴백 팝업) ────────────────────────────────────────── public bool BuildIfNeeded() { if (_rt == null) _rt = GetComponent(); bool made = false; if (group == null) { group = GetComponent(); if (group == null) { group = gameObject.AddComponent(); made = true; } } if (dim == null) { var d = WLVignetteUtil.NewChild(_rt, "Dim"); WLVignetteUtil.Stretch(d); dim = d.GetComponent(); if (dim == null) dim = d.gameObject.AddComponent(); dim.raycastTarget = true; made = true; } if (box == null) { box = WLVignetteUtil.NewChild(_rt, "Box"); made = true; } if (boxBg == null) { boxBg = box.GetComponent(); if (boxBg == null) { boxBg = box.gameObject.AddComponent(); made = true; } boxBg.raycastTarget = true; } if (messageLabel == null) { var t = WLVignetteUtil.NewChild(box, "Msg"); messageLabel = t.GetComponent(); if (messageLabel == null) messageLabel = t.gameObject.AddComponent(); messageLabel.raycastTarget = false; messageLabel.alignment = TextAlignmentOptions.Center; if (font != null) messageLabel.font = font; made = true; } if (okButton == null) { var t = WLVignetteUtil.NewChild(box, "btn_ok"); var img = t.GetComponent(); if (img == null) img = t.gameObject.AddComponent(); img.raycastTarget = true; okButton = t.GetComponent