// ─────────────────────────────────────────────────────────────────────────────
// 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 단일).
// ─────────────────────────────────────────────────────────────────────────────
// ■ WL-813z (Q4-a D-3 · D-9) — 2026-09-09
// D-3 실측: 공용 `Popup` 은 **노드 1개짜리 싱글턴**(`Assets/Script/Info/Popup.cs` 전문 54줄)이고
// `Set()` 이 `m_ok` 를 매번 덮어쓴다. `Util/ErrorLogHookManager.cs:33` 이 Error/Exception 마다
// `Popup.Ins.Set(One, 174, null, null, log)` 을 부르므로 **부활 팝업이 넘긴 확인 콜백이 null 로 지워진다**
// → 확인을 눌러도 부활이 안 되고 `timeScale=0` 이 영영 안 풀린다(Q4-a 슬롯① 2분 30초).
// 대응 3층: ① 813i DeathFlow.reviveWatchdogSeconds(자동 부활 · 마지막 방어선)
// ② 전용 노드를 최상단 캔버스(sortingOrder > SortOrder_5 의 5)로 올려 확인 버튼을 항상 노출
// ③ 공용 Popup 이 남의 내용으로 바뀌면 감지해 전용 팝업으로 **승격**
// 부수 수정: `HideNow()` 가 **우리 것일 때만** 공용 Popup 도 닫는다(Q4-a QA-4 「팝업이 안 닫힌다」).
// D-9: 런타임 생성 확인 버튼 이름 `btn_ok` → `WL_btn_ok`(원본 `NightmareResult/btn_ok` 와 이름 충돌 제거).
using System.Text;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using WL.Combat.Survival;
namespace WL.UI
{
[DisallowMultipleComponent]
[RequireComponent(typeof(RectTransform))]
public class ReviveDialog : MonoBehaviour
{
// ── 813i 연결점 (「후속」) ────────────────────────────────────────────
/// 확인을 누르면 불린다. 813i 가 존 시작점 부활을 붙인다.
public static System.Action ReviveRequested;
/// 팝업이 뜰 때 불린다(진단·튜토리얼용 · 선택).
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; }
///
/// 부활 팝업이 지금 떠 있거나 표시 예약 중인가(공용 Popup 경로 포함).
/// WL-813q 결과 화면이 「부활 팝업 우선」 순서를 지킬 때 본다(읽기 전용 · 동작 변경 0).
///
public static bool Busy { get { return Active != null && (Active._open || Active._showAt >= 0f); } }
/// 813i DeathFlow 가 사망 연출 뒤 부르는 진입점. 인스턴스가 없으면 무동작.
public static string NotifyDeath()
{
if (Active == null) return "ReviveDialog 인스턴스 없음 — 무동작";
return Active.QueueShow(Time.unscaledTime);
}
// ── WL-813y — 813i DeathFlow 와 이어 붙인 진입점 2개 ──────────────────
///
/// `DeathFlow.ReviveRequested` 진입점 — 813i 가 **사망 연출을 이미 기다렸으므로** 지연 없이 연다.
/// (`NotifyDeath()` 의 UI 지연과 겹쳐 두 번 뜨지 않도록 예약을 지우고 1회만 연다.)
///
public static string NotifyReviveRequested()
{
if (Active == null) return "ReviveDialog 인스턴스 없음 — 무동작";
return Active.ShowNowOnce();
}
/// `DeathFlow.Revived` 진입점 — 부활이 끝났으니 팝업을 닫는다(자동/외부 부활 포함).
public static string NotifyRevived()
{
if (Active == null) return "ReviveDialog 인스턴스 없음 — 무동작";
Active.HideNow();
return "부활 완료 — 팝업 닫음";
}
[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;
// WL-815d — 스테이지 실패 팝업의 두 번째 버튼(기준서 §E 「다시 도전 / 로비로」)
[SerializeField] private Button secondaryButton;
[SerializeField] private TextMeshProUGUI secondaryLabel;
[Header("폰트 (에디터가 주입)")]
[SerializeField] private TMP_FontAsset font;
private RectTransform _rt;
private Canvas _canvas;
private float _showAt = -1f;
private bool _open; // 팝업이 열려 있는가(공용 Popup 경로 포함 · WL-813y)
// ── 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;
private bool _escalated; // 공용 Popup 이 덮여 전용 팝업으로 승격했다
// ── 진단 ──────────────────────────────────────────────────────────────
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; } }
/// WL-813z — 최상단 재보장 횟수 · 공용 Popup 탈취 감지 횟수 · 우리가 닫은 공용 Popup 수.
public int TopmostCount { get; private set; }
public int HijackCount { get; private set; }
public int CommonPopupClosed { get; private set; }
public bool Escalated { get { return _escalated; } }
public bool IsOpen { get { return _open; } }
public int SortingOrder { get { return _ownCanvas != null && _ownCanvas.overrideSorting ? _ownCanvas.sortingOrder : -1; } }
public bool IsLastSibling { get { return _rt != null && _rt.parent != null && _rt.GetSiblingIndex() == _rt.parent.childCount - 1; } }
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)
{
// ── WL-813z(D-3) 표시 중 재보장(GC 0 — 문자열·할당 없음 · 바뀐 값만 쓴다)
if (_open)
{
var g = WLSurvivalSettings.Instance;
if (g != null && g.reviveTopmostEnabled && g.reviveTopmostIntervalSeconds > 0f && now >= _nextTopmostAt)
{
_nextTopmostAt = now + g.reviveTopmostIntervalSeconds;
EnsureTopmost(g);
CheckCommonPopupHijack(g);
}
}
if (_showAt < 0f || now < _showAt) return;
_showAt = -1f;
ShowNow();
}
///
/// WL-813z(D-3) — 전용 노드를 형제 맨 뒤 + 전용 캔버스(sortingOrder = SO)로 올리고 레이캐스트를 살린다.
/// 공용 팝업 프리팹 `SortOrder_5` 의 Canvas.m_SortingOrder = 5(실측)보다 커야 확인 버튼이 안 가린다.
/// 값이 이미 맞으면 아무 것도 쓰지 않는다(GC 0 · SetDirty 0).
///
public void EnsureTopmost(WLSurvivalSettings g)
{
if (g == null || !g.reviveTopmostEnabled) return;
if (_rt == null) _rt = GetComponent();
if (_rt == null) return;
var parent = _rt.parent;
if (parent != null && _rt.GetSiblingIndex() != parent.childCount - 1) _rt.SetAsLastSibling();
if (_ownCanvas == null)
{
_ownCanvas = GetComponent