Project_WL/Assets/WL/UI/Scripts/ReviveDialog.cs

382 lines
19 KiB
C#

// ─────────────────────────────────────────────────────────────────────────────
// 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 연결점 (「후속」) ────────────────────────────────────────────
/// <summary>확인을 누르면 불린다. 813i 가 존 시작점 부활을 붙인다.</summary>
public static System.Action ReviveRequested;
/// <summary>팝업이 뜰 때 불린다(진단·튜토리얼용 · 선택).</summary>
public static System.Action ReviveShown;
/// <summary>현재 씬에 살아 있는 인스턴스(정적 진입점이 찾는다).</summary>
public static ReviveDialog Active { get; private set; }
/// <summary>
/// 부활 팝업이 지금 떠 있거나 표시 예약 중인가(공용 Popup 경로 포함).
/// WL-813q 결과 화면이 「부활 팝업 우선」 순서를 지킬 때 본다(읽기 전용 · 동작 변경 0).
/// </summary>
public static bool Busy { get { return Active != null && (Active._open || Active._showAt >= 0f); } }
/// <summary>813i DeathFlow 가 사망 연출 뒤 부르는 진입점. 인스턴스가 없으면 무동작.</summary>
public static string NotifyDeath()
{
if (Active == null) return "ReviveDialog 인스턴스 없음 — 무동작";
return Active.QueueShow(Time.unscaledTime);
}
// ── WL-813y — 813i DeathFlow 와 이어 붙인 진입점 2개 ──────────────────
/// <summary>
/// `DeathFlow.ReviveRequested` 진입점 — 813i 가 **사망 연출을 이미 기다렸으므로** 지연 없이 연다.
/// (`NotifyDeath()` 의 UI 지연과 겹쳐 두 번 뜨지 않도록 예약을 지우고 1회만 연다.)
/// </summary>
public static string NotifyReviveRequested()
{
if (Active == null) return "ReviveDialog 인스턴스 없음 — 무동작";
return Active.ShowNowOnce();
}
/// <summary>`DeathFlow.Revived` 진입점 — 부활이 끝났으니 팝업을 닫는다(자동/외부 부활 포함).</summary>
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;
[Header("폰트 (에디터가 주입)")]
[SerializeField] private TMP_FontAsset font;
private RectTransform _rt;
private Canvas _canvas;
private float _showAt = -1f;
private bool _open; // 팝업이 열려 있는가(공용 Popup 경로 포함 · WL-813y)
// ── 진단 ──────────────────────────────────────────────────────────────
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<RectTransform>(); Active = this; }
private void OnEnable() { Active = this; Initialize(); }
private void OnDisable() { if (Active == this) Active = null; }
public string Initialize()
{
if (_rt == null) _rt = GetComponent<RectTransform>();
Active = this;
BuildIfNeeded();
string layout = ApplyLayout();
HideNow();
return "initialized · " + layout;
}
/// <summary>사망 통지 → 설정된 지연 뒤에 팝업을 연다(사망 연출 시간은 813i 소유).</summary>
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); }
/// <summary>지연 만료 검사. 프로브가 시간을 넣어 같은 경로로 검증한다.</summary>
public void Tick(float now)
{
if (_showAt < 0f || now < _showAt) return;
_showAt = -1f;
ShowNow();
}
/// <summary>예약을 지우고, 아직 안 떠 있을 때만 연다(WL-813y · 813i 이중 통지 방지).</summary>
public string ShowNowOnce()
{
_showAt = -1f;
if (_open) return "이미 표시 중 — 중복 무시(shows=" + ShownCount + ")";
return ShowNow();
}
/// <summary>지금 바로 팝업을 연다(공용 Popup 우선 · 실패하면 폴백 노드).</summary>
public string ShowNow()
{
var s = WLSurvivalUiSettings.Instance;
if (s == null) return "WLSurvivalUiSettings 에셋 없음";
_open = true;
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 + "\"";
}
/// <summary>본문 = 설정 문자열(로컬라이즈되면 reviveMessageKey 우선) + 비용 줄.</summary>
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;
}
/// <summary>공용 Popup 싱글턴 재사용. 실패(미로드·표 없음)하면 UsedCommonPopup=false 로 남긴다.</summary>
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; }
}
/// <summary>확인 = 부활 요청. 813i 가 ReviveRequested 를 채우면 실제 부활이 일어난다.</summary>
public string OnConfirm()
{
ConfirmCount++;
HideNow();
if (ReviveRequested == null) return "확인 — 813i(ReviveRequested) 미연결이라 부활 동작 없음(후속)";
try { ReviveRequested(); } catch { }
return "확인 — 부활 요청 전달";
}
public void HideNow()
{
_showAt = -1f;
_open = false;
if (group == null) return;
group.alpha = 0f; group.blocksRaycasts = false; group.interactable = false;
}
// ── 구성 · 배치 (폴백 팝업) ──────────────────────────────────────────
public bool BuildIfNeeded()
{
if (_rt == null) _rt = GetComponent<RectTransform>();
bool made = false;
if (group == null) { group = GetComponent<CanvasGroup>(); if (group == null) { group = gameObject.AddComponent<CanvasGroup>(); made = true; } }
if (dim == null)
{
var d = WLVignetteUtil.NewChild(_rt, "Dim");
WLVignetteUtil.Stretch(d);
dim = d.GetComponent<Image>();
if (dim == null) dim = d.gameObject.AddComponent<Image>();
dim.raycastTarget = true;
made = true;
}
if (box == null) { box = WLVignetteUtil.NewChild(_rt, "Box"); made = true; }
if (boxBg == null)
{
boxBg = box.GetComponent<Image>();
if (boxBg == null) { boxBg = box.gameObject.AddComponent<Image>(); made = true; }
boxBg.raycastTarget = true;
}
if (messageLabel == null)
{
var t = WLVignetteUtil.NewChild(box, "Msg");
messageLabel = t.GetComponent<TextMeshProUGUI>();
if (messageLabel == null) messageLabel = t.gameObject.AddComponent<TextMeshProUGUI>();
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<Image>();
if (img == null) img = t.gameObject.AddComponent<Image>();
img.raycastTarget = true;
okButton = t.GetComponent<Button>();
if (okButton == null) okButton = t.gameObject.AddComponent<Button>();
okButton.targetGraphic = img;
okButton.onClick.RemoveListener(OnOkListener);
okButton.onClick.AddListener(OnOkListener);
made = true;
}
if (okLabel == null)
{
var t = WLVignetteUtil.NewChild(okButton.GetComponent<RectTransform>(), "Label");
okLabel = t.GetComponent<TextMeshProUGUI>();
if (okLabel == null) okLabel = t.gameObject.AddComponent<TextMeshProUGUI>();
okLabel.raycastTarget = false;
okLabel.alignment = TextAlignmentOptions.Center;
if (font != null) okLabel.font = font;
made = true;
}
return made;
}
private void OnOkListener() { OnConfirm(); }
public string ApplyLayout()
{
if (_rt == null) _rt = GetComponent<RectTransform>();
var s = WLSurvivalUiSettings.Instance;
if (s == null) return "WLSurvivalUiSettings 에셋 없음 — 배치 건너뜀";
if (box == null) return "구성 요소 없음 — BuildIfNeeded 먼저";
float u = UnitsPerPx();
WLVignetteUtil.Stretch(_rt);
_rt.localScale = Vector3.one;
if (dim != null) dim.color = new Color(0f, 0f, 0f, 0.6f);
box.anchorMin = new Vector2(0.5f, 0.5f);
box.anchorMax = new Vector2(0.5f, 0.5f);
box.pivot = new Vector2(0.5f, 0.5f);
box.anchoredPosition = Vector2.zero;
box.sizeDelta = new Vector2(s.reviveFallbackSizePx.x * u, s.reviveFallbackSizePx.y * u);
if (boxBg != null) boxBg.color = s.reviveFallbackBgColor;
if (messageLabel != null)
{
var t = messageLabel.rectTransform;
t.anchorMin = new Vector2(0f, 0.35f); t.anchorMax = new Vector2(1f, 1f);
t.offsetMin = new Vector2(24f * u, 0f); t.offsetMax = new Vector2(-24f * u, -24f * u);
messageLabel.fontSize = s.reviveFallbackFontPx * u;
messageLabel.color = Color.white;
if (font != null && messageLabel.font != font) messageLabel.font = font;
}
if (okButton != null)
{
var t = okButton.GetComponent<RectTransform>();
t.anchorMin = new Vector2(0.5f, 0f); t.anchorMax = new Vector2(0.5f, 0f);
t.pivot = new Vector2(0.5f, 0f);
t.anchoredPosition = new Vector2(0f, 28f * u);
t.sizeDelta = new Vector2(320f * u, 96f * u);
var img = okButton.targetGraphic as Image;
if (img != null) img.color = new Color(0.137f, 0.906f, 0.529f, 1f);
}
if (okLabel != null)
{
WLVignetteUtil.Stretch(okLabel.rectTransform);
okLabel.text = s.reviveOkText;
okLabel.fontSize = s.reviveFallbackFontPx * u * 0.8f;
okLabel.color = new Color(0.04f, 0.09f, 0.06f, 1f);
if (font != null && okLabel.font != font) okLabel.font = font;
}
return "폴백 팝업 box size" + box.sizeDelta + " unitsPerPx=" + u.ToString("F4");
}
public float UnitsPerPx()
{
if (_canvas == null) _canvas = GetComponentInParent<Canvas>(true);
return WLVignetteUtil.UnitsPerPx(_canvas);
}
public void SetFont(TMP_FontAsset f)
{
font = f;
if (f == null) return;
if (messageLabel != null) messageLabel.font = f;
if (okLabel != null) okLabel.font = f;
}
// ── 검증 전용 ────────────────────────────────────────────────────────
public string Dump()
{
var sb = new StringBuilder();
var s = WLSurvivalUiSettings.Instance;
sb.AppendLine("[ReviveDialog] deaths=" + DeathSeen + " shows=" + ShownCount + " confirms=" + ConfirmCount +
" 대기중=" + Pending + " 공용팝업사용=" + UsedCommonPopup +
" 폴백표시=" + FallbackVisible + " open=" + _open +
" 813i(ReviveRequested)=" + (ReviveRequested != null) +
" 브리지=" + WLSurvivalUiBridge.Installed);
sb.AppendLine(" 본문=\"" + LastMessage.Replace("\n", " / ") + "\"");
sb.AppendLine(" 공용 Popup.Ins=" + (Popup.Ins != null ? "있음" : "없음(에디트 모드/씬 미로드)"));
if (box != null)
sb.AppendLine(" 폴백 box size" + box.sizeDelta + " pos" + box.anchoredPosition +
" ok=\"" + (okLabel != null ? okLabel.text : "-") + "\"");
sb.AppendLine(" 설정 " + (s == null ? "(에셋 없음)" :
"enabled=" + s.reviveEnabled + " 공용팝업=" + s.reviveUseCommonPopup +
" delay=" + s.reviveDelaySeconds + "s key=" + s.reviveMessageKey +
" 비용=\"" + s.reviveCostText + "\""));
return sb.ToString();
}
}
}