[WL-813z] 핫픽스: 부활 워치독 · 팝업 최상단 · WL_btn_ok · 토스트 클램프 (#813)
This commit is contained in:
parent
315172ecb6
commit
a2c5a63bab
|
|
@ -0,0 +1,554 @@
|
||||||
|
// WL-813z 프로브 — Q4-a 핫픽스 실측(에디트 모드 · Play 0 · #813).
|
||||||
|
// run_script --file AgentScripts/WL813z_Probe.cs --entry WL813z_Probe.<Entry>
|
||||||
|
// Watchdog · Topmost · Toast · All
|
||||||
|
// 로그는 <worktree>/Logs/WL813z_*.txt (Assets 밖 · 미추적).
|
||||||
|
//
|
||||||
|
// 🔴 에디트 모드에서 `Time.timeScale` 에 **쓰지 않는다** — 쓰면 ProjectSettings/TimeManager.asset 이 더러워진다
|
||||||
|
// (811b-fix 실측 · DeathFlow.HoldWorld 도 같은 이유로 `Application.isPlaying` 가드를 둔다).
|
||||||
|
// 세계 정지 복원은 ① `DeathFlow.WorldHeld` 전이 ② `MyValue.Get_GameSpeed()` 실측값 ③ `Time.timeScale` 읽기로 증명한다.
|
||||||
|
// 🔴 씬·프리팹을 저장하지 않는다. 만든 노드는 finally 에서 전부 DestroyImmediate.
|
||||||
|
// 🔴 SO 값은 메모리에서만 바꾸고 finally 에서 원복한다(AssetDatabase.SaveAssets 호출 0).
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using TMPro;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.UI;
|
||||||
|
using UnityEditor;
|
||||||
|
using WL.Combat.Survival;
|
||||||
|
using WL.UI;
|
||||||
|
|
||||||
|
public static class WL813z_Probe
|
||||||
|
{
|
||||||
|
const BindingFlags NP = BindingFlags.Instance | BindingFlags.NonPublic;
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────── 공통
|
||||||
|
|
||||||
|
static string LogDir
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
string d = Path.Combine(Directory.GetParent(Application.dataPath).FullName, "Logs");
|
||||||
|
if (!Directory.Exists(d)) Directory.CreateDirectory(d);
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static string Write(string name, string body)
|
||||||
|
{
|
||||||
|
string p = Path.Combine(LogDir, name);
|
||||||
|
File.WriteAllText(p, body, new UTF8Encoding(false));
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
static string Ok(bool b) { return b ? "PASS" : "**FAIL**"; }
|
||||||
|
|
||||||
|
/// <summary>세로 1080×1920 기준(참조 1920×1080 Expand)의 합성 캔버스 + Safe Area 부모.</summary>
|
||||||
|
static RectTransform BuildCanvas(out GameObject root, out Canvas canvas, out float unitsPerPx)
|
||||||
|
{
|
||||||
|
root = new GameObject("__WL813z_Canvas", typeof(RectTransform));
|
||||||
|
root.hideFlags = HideFlags.DontSave;
|
||||||
|
canvas = root.AddComponent<Canvas>();
|
||||||
|
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||||
|
var scaler = root.AddComponent<CanvasScaler>();
|
||||||
|
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||||
|
scaler.referenceResolution = new Vector2(1920f, 1080f); // 원본 NewGameUI 와 같은 설정(802b 실측 1.7778)
|
||||||
|
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.Expand;
|
||||||
|
root.AddComponent<GraphicRaycaster>();
|
||||||
|
|
||||||
|
var s = WLCombatTextSettings.Instance;
|
||||||
|
unitsPerPx = s != null ? s.UnitsPerPx(canvas) : 1f;
|
||||||
|
|
||||||
|
// WL_HUD 대역(SafeAreaFitter 아래 = Safe Area) — 배치가 화면 크기에 흔들리지 않게 크기를 못박는다.
|
||||||
|
var hud = new GameObject("WL_HUD", typeof(RectTransform)).GetComponent<RectTransform>();
|
||||||
|
hud.gameObject.hideFlags = HideFlags.DontSave;
|
||||||
|
hud.SetParent(root.transform, false);
|
||||||
|
hud.anchorMin = hud.anchorMax = new Vector2(0.5f, 0.5f);
|
||||||
|
hud.pivot = new Vector2(0.5f, 0.5f);
|
||||||
|
hud.anchoredPosition = Vector2.zero;
|
||||||
|
hud.sizeDelta = new Vector2(1080f * unitsPerPx, 1920f * unitsPerPx);
|
||||||
|
return hud;
|
||||||
|
}
|
||||||
|
|
||||||
|
static RectTransform Decoy(RectTransform parent, string n)
|
||||||
|
{
|
||||||
|
var rt = new GameObject(n, typeof(RectTransform)).GetComponent<RectTransform>();
|
||||||
|
rt.gameObject.hideFlags = HideFlags.DontSave;
|
||||||
|
rt.SetParent(parent, false);
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>813y 프로브와 같은 가짜 메인 PC(사망 상태). 원본 회복 경로는 ReviveOverride 로 막는다.</summary>
|
||||||
|
static PCActor MakePC()
|
||||||
|
{
|
||||||
|
var go = new GameObject("__WL813z_PC");
|
||||||
|
go.hideFlags = HideFlags.DontSave;
|
||||||
|
var pc = go.AddComponent<PCActor>();
|
||||||
|
pc.m_Role = eRole.PC; pc.m_SubRole = eSubRol.None;
|
||||||
|
var stat = new ActorStatInfo(eRole.PC);
|
||||||
|
stat.Set_Stat(eStat.MaxHP, 1000d); stat.Set_Stat(eStat.HP, 0d);
|
||||||
|
typeof(Actor).GetField("m_Stat", NP).SetValue(pc, stat);
|
||||||
|
typeof(Actor).GetField("m_Enemy", NP).SetValue(pc, false);
|
||||||
|
typeof(Actor).GetField("DeadStatus", NP).SetValue(pc, true);
|
||||||
|
return pc;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void OnReq(in ReviveRequestEvent e) { s_reqSeen++; }
|
||||||
|
static int s_reqSeen;
|
||||||
|
|
||||||
|
static void Kill(GameObject go) { if (go != null) UnityEngine.Object.DestroyImmediate(go); }
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────── ① D-3 부활 워치독
|
||||||
|
|
||||||
|
|
||||||
|
public static string Watchdog()
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("=== WL-813z ① D-3 부활 워치독 (에디트 모드 · Play 0) ===");
|
||||||
|
var st = WLSurvivalSettings.Instance;
|
||||||
|
if (st == null) { var b0 = sb.AppendLine("**FAIL** WLSurvivalSettings.asset 없음").ToString(); return b0 + "\n→ " + Write("WL813z_watchdog.txt", b0); }
|
||||||
|
|
||||||
|
float ts0 = Time.timeScale;
|
||||||
|
int ov0 = WLSurvivalSettings.RuntimeOverride;
|
||||||
|
float wd0 = st.reviveWatchdogSeconds; bool ig0 = st.reviveWatchdogIgnoresDisable; bool lg0 = st.reviveWatchdogLog;
|
||||||
|
float to0 = st.autoReviveTimeoutSeconds; bool an0 = st.autoReviveWhenNoListener; bool hw0 = st.holdWorldWhileReviving;
|
||||||
|
var ovr0 = DeathFlow.ReviveOverride;
|
||||||
|
int overrideCalls = 0;
|
||||||
|
PCActor pc = null;
|
||||||
|
|
||||||
|
sb.AppendLine("에셋 값 실측: reviveWatchdogSeconds=" + wd0.ToString("F1") + "s · 무시disable=" + ig0 +
|
||||||
|
" · log=" + lg0 + " · autoReviveTimeoutSeconds=" + to0.ToString("F1") + "s · holdWorld=" + hw0);
|
||||||
|
sb.AppendLine("세계 복원값 실측: MyValue.Get_GameSpeed() = " + MyValue.Get_GameSpeed().ToString("F2") +
|
||||||
|
" · 시작 Time.timeScale = " + ts0.ToString("F2") +
|
||||||
|
" (에디트 모드에선 DeathFlow.HoldWorld 가 isPlaying 가드로 0 을 쓰지 않는다 = TimeManager.asset 무변경)");
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
WLSurvivalSettings.RuntimeOverride = 1;
|
||||||
|
DeathFlow.ReviveOverride = (p, r) => { overrideCalls++; };
|
||||||
|
s_reqSeen = 0;
|
||||||
|
DeathFlow.ReviveRequested.Add(OnReq); // 구독자 1 = autoReviveWhenNoListener 무력화(813y 와 같은 조건)
|
||||||
|
pc = MakePC();
|
||||||
|
|
||||||
|
float disp = st.deathDisplaySeconds;
|
||||||
|
|
||||||
|
// ── A. 기본 20 s 워치독
|
||||||
|
st.reviveWatchdogSeconds = 20f; st.reviveWatchdogIgnoresDisable = true; st.reviveWatchdogLog = true;
|
||||||
|
DeathFlow.ResetDiagnostics();
|
||||||
|
float t = 1000f;
|
||||||
|
DeathFlow.Tick(t);
|
||||||
|
DeathFlow.OnPCDied(pc);
|
||||||
|
sb.AppendLine("[A] 20 s 워치독 (정상 · 스위치 ON)");
|
||||||
|
sb.AppendLine(" 사망 → state=" + DeathFlow.State + " deathCount=" + DeathFlow.DeathCount);
|
||||||
|
float req = t + disp + 0.01f;
|
||||||
|
DeathFlow.Tick(req);
|
||||||
|
sb.AppendLine(" +" + disp.ToString("F2") + "s → state=" + DeathFlow.State + " · ReviveRequested 수신=" + s_reqSeen +
|
||||||
|
" · 구독자=" + DeathFlow.ReviveRequested.Count + " · WorldHeld=" + DeathFlow.WorldHeld);
|
||||||
|
DeathFlow.Tick(req + 19.9f);
|
||||||
|
sb.AppendLine(" +19.9s(팝업이 에러 모달에 가려 확인 못 누른 상태) → state=" + DeathFlow.State +
|
||||||
|
" 대기=" + DeathFlow.AwaitingSeconds.ToString("F2") + "s · 워치독=" + DeathFlow.WatchdogCount + "회 · " +
|
||||||
|
Ok(DeathFlow.State == DeathState.AwaitingRevive && DeathFlow.WatchdogCount == 0));
|
||||||
|
DeathFlow.Tick(req + 20.02f);
|
||||||
|
bool a = DeathFlow.State == DeathState.None && DeathFlow.WatchdogCount == 1 && DeathFlow.AutoReviveCount == 1
|
||||||
|
&& DeathFlow.ReviveCount == 1 && !DeathFlow.WorldHeld && overrideCalls == 1;
|
||||||
|
sb.AppendLine(" +20.02s → state=" + DeathFlow.State + " · 워치독=" + DeathFlow.WatchdogCount + "회(" +
|
||||||
|
DeathFlow.LastWatchdogSeconds.ToString("F2") + "s) · auto=" + DeathFlow.AutoReviveCount +
|
||||||
|
" · reviveCount=" + DeathFlow.ReviveCount + " · 부활실행=" + overrideCalls +
|
||||||
|
" · WorldHeld=" + DeathFlow.WorldHeld + " · timeScale=" + Time.timeScale.ToString("F2") + " · " + Ok(a));
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
// ── B. RuntimeDisabled 무관 (도중에 스위치가 꺼져도 워치독이 돈다)
|
||||||
|
DeathFlow.ResetDiagnostics(); overrideCalls = 0; s_reqSeen = 0;
|
||||||
|
t = 2000f; DeathFlow.Tick(t); DeathFlow.OnPCDied(pc);
|
||||||
|
req = t + disp + 0.01f; DeathFlow.Tick(req);
|
||||||
|
WLSurvivalSettings.RuntimeOverride = -1; // = QA 의 RuntimeDisabled
|
||||||
|
sb.AppendLine("[B] 대기 중 RuntimeDisabled(-1) · reviveWatchdogIgnoresDisable=true");
|
||||||
|
sb.AppendLine(" Enabled=" + WLSurvivalSettings.Enabled + " state=" + DeathFlow.State);
|
||||||
|
DeathFlow.Tick(req + 20.02f);
|
||||||
|
bool bb = DeathFlow.State == DeathState.None && DeathFlow.WatchdogCount == 1 && DeathFlow.AbortCount == 0;
|
||||||
|
sb.AppendLine(" +20.02s → state=" + DeathFlow.State + " · 워치독=" + DeathFlow.WatchdogCount + "회 · abort=" +
|
||||||
|
DeathFlow.AbortCount + " · 부활실행=" + overrideCalls + " · WorldHeld=" + DeathFlow.WorldHeld + " · " + Ok(bb));
|
||||||
|
|
||||||
|
// ── B'. 무시disable=false 면 워치독 대신 Abort — 이때도 세계 정지는 풀린다(잠금 0)
|
||||||
|
st.reviveWatchdogIgnoresDisable = false;
|
||||||
|
DeathFlow.ResetDiagnostics(); overrideCalls = 0;
|
||||||
|
WLSurvivalSettings.RuntimeOverride = 1;
|
||||||
|
t = 3000f; DeathFlow.Tick(t); DeathFlow.OnPCDied(pc);
|
||||||
|
req = t + disp + 0.01f; DeathFlow.Tick(req);
|
||||||
|
WLSurvivalSettings.RuntimeOverride = -1;
|
||||||
|
DeathFlow.Tick(req + 0.02f);
|
||||||
|
bool bp = DeathFlow.State == DeathState.None && DeathFlow.AbortCount == 1 && !DeathFlow.WorldHeld;
|
||||||
|
sb.AppendLine(" 무시disable=false → 대신 Abort(\"" + DeathFlow.LastAbortReason + "\") · state=" + DeathFlow.State +
|
||||||
|
" · WorldHeld=" + DeathFlow.WorldHeld + " (ReleaseWorld 통과 = 잠금 0) · " + Ok(bp));
|
||||||
|
st.reviveWatchdogIgnoresDisable = true;
|
||||||
|
WLSurvivalSettings.RuntimeOverride = 1;
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
// ── C. C8 롤백 — reviveWatchdogSeconds = 0 이면 워치독이 없다(813i 원래 동작 100%)
|
||||||
|
st.reviveWatchdogSeconds = 0f;
|
||||||
|
DeathFlow.ResetDiagnostics(); overrideCalls = 0;
|
||||||
|
t = 4000f; DeathFlow.Tick(t); DeathFlow.OnPCDied(pc);
|
||||||
|
req = t + disp + 0.01f; DeathFlow.Tick(req);
|
||||||
|
DeathFlow.Tick(req + 999f);
|
||||||
|
bool c = DeathFlow.State == DeathState.AwaitingRevive && DeathFlow.WatchdogCount == 0;
|
||||||
|
sb.AppendLine("[C] C8 롤백 reviveWatchdogSeconds=0 → +999s 뒤에도 state=" + DeathFlow.State +
|
||||||
|
" · 워치독=" + DeathFlow.WatchdogCount + "회 · " + Ok(c) + " (= 813i 원래 동작)");
|
||||||
|
DeathFlow.Abort("probe cleanup");
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
// ── D. 정상 확인 버튼 경로가 워치독보다 먼저면 워치독은 안 돈다
|
||||||
|
st.reviveWatchdogSeconds = 20f;
|
||||||
|
DeathFlow.ResetDiagnostics(); overrideCalls = 0;
|
||||||
|
t = 5000f; DeathFlow.Tick(t); DeathFlow.OnPCDied(pc);
|
||||||
|
req = t + disp + 0.01f; DeathFlow.Tick(req);
|
||||||
|
DeathFlow.Tick(req + 3f);
|
||||||
|
bool revived = DeathFlow.Revive();
|
||||||
|
bool d = revived && DeathFlow.State == DeathState.None && DeathFlow.WatchdogCount == 0 && DeathFlow.AutoReviveCount == 0;
|
||||||
|
sb.AppendLine("[D] 3 s 뒤 확인 버튼(DeathFlow.Revive()) → " + revived + " · state=" + DeathFlow.State +
|
||||||
|
" · 워치독=" + DeathFlow.WatchdogCount + "회 · auto=" + DeathFlow.AutoReviveCount + " · " + Ok(d));
|
||||||
|
}
|
||||||
|
catch (Exception ex) { sb.AppendLine("**예외** " + ex); }
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
DeathFlow.ReviveRequested.Remove(OnReq);
|
||||||
|
DeathFlow.Abort("probe finally");
|
||||||
|
DeathFlow.ReviveOverride = ovr0;
|
||||||
|
DeathFlow.ResetDiagnostics();
|
||||||
|
st.reviveWatchdogSeconds = wd0; st.reviveWatchdogIgnoresDisable = ig0; st.reviveWatchdogLog = lg0;
|
||||||
|
st.autoReviveTimeoutSeconds = to0; st.autoReviveWhenNoListener = an0; st.holdWorldWhileReviving = hw0;
|
||||||
|
WLSurvivalSettings.RuntimeOverride = ov0;
|
||||||
|
if (pc != null) Kill(pc.gameObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("정리: state=" + DeathFlow.State + " · WorldHeld=" + DeathFlow.WorldHeld +
|
||||||
|
" · 구독 Req=" + DeathFlow.ReviveRequested.Count +
|
||||||
|
" · timeScale " + ts0.ToString("F2") + " → " + Time.timeScale.ToString("F2") +
|
||||||
|
" (에디트 모드 쓰기 0) · SO 원복 완료");
|
||||||
|
var body = sb.ToString();
|
||||||
|
return body + "\n→ " + Write("WL813z_watchdog.txt", body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────── ② 부활 팝업 최상단 보장 + WL_btn_ok
|
||||||
|
|
||||||
|
|
||||||
|
public static string Topmost()
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("=== WL-813z ② 부활 팝업 최상단 보장(D-3) + WL_btn_ok(D-9) ===");
|
||||||
|
var st = WLSurvivalSettings.Instance;
|
||||||
|
var us = WLSurvivalUiSettings.Instance;
|
||||||
|
if (st == null || us == null)
|
||||||
|
{
|
||||||
|
var b0 = sb.AppendLine("**FAIL** WLSurvivalSettings/WLSurvivalUiSettings 에셋 없음").ToString();
|
||||||
|
return b0 + "\n→ " + Write("WL813z_topmost.txt", b0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool tm0 = st.reviveTopmostEnabled; int so0 = st.reviveSortingOrder;
|
||||||
|
float iv0 = st.reviveTopmostIntervalSeconds; bool es0 = st.reviveEscalateOnHijack;
|
||||||
|
GameObject root = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Canvas canvas; float u;
|
||||||
|
var hud = BuildCanvas(out root, out canvas, out u);
|
||||||
|
sb.AppendLine("합성 캔버스: unitsPerPx=" + u.ToString("F4") + " · WL_HUD size=" + hud.sizeDelta +
|
||||||
|
" · 공용 팝업 프리팹 SortOrder_5 의 Canvas.m_SortingOrder = 5 (실측 · Assets/ResWork/UIPrefabs/Title/SortOrder_5.prefab:9159)");
|
||||||
|
sb.AppendLine("에셋 값: reviveTopmostEnabled=" + tm0 + " · reviveSortingOrder=" + so0 +
|
||||||
|
" · 재보장 주기=" + iv0.ToString("F2") + "s · 승격=" + es0);
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
Decoy(hud, "기존팝업_A");
|
||||||
|
var dlgRt = Decoy(hud, "WL_ReviveDialog");
|
||||||
|
Decoy(hud, "기존팝업_B");
|
||||||
|
var dlg = dlgRt.gameObject.AddComponent<ReviveDialog>();
|
||||||
|
|
||||||
|
sb.AppendLine("[A] 표시 순간 최상단 · 레이캐스트");
|
||||||
|
sb.AppendLine(" init → " + dlg.Initialize().Split('·')[0].Trim());
|
||||||
|
sb.AppendLine(" 표시 전: 맨뒤형제=" + dlg.IsLastSibling + " sortingOrder=" + dlg.SortingOrder);
|
||||||
|
string shown = dlg.ShowNow();
|
||||||
|
var cv = dlgRt.GetComponent<Canvas>();
|
||||||
|
var gr = dlgRt.GetComponent<GraphicRaycaster>();
|
||||||
|
var cg = dlgRt.GetComponent<CanvasGroup>();
|
||||||
|
bool a = dlg.IsLastSibling && cv != null && cv.overrideSorting && cv.sortingOrder == st.reviveSortingOrder
|
||||||
|
&& cv.sortingOrder > 5 && gr != null && gr.enabled && cg != null && cg.blocksRaycasts;
|
||||||
|
sb.AppendLine(" ShowNow → " + shown);
|
||||||
|
sb.AppendLine(" 맨뒤형제=" + dlg.IsLastSibling + " · overrideSorting=" + (cv != null && cv.overrideSorting) +
|
||||||
|
" · sortingOrder=" + (cv != null ? cv.sortingOrder : -1) + "(> 공용 5) · GraphicRaycaster=" +
|
||||||
|
(gr != null && gr.enabled) + " · CanvasGroup.blocksRaycasts=" + (cg != null && cg.blocksRaycasts) +
|
||||||
|
" · " + Ok(a));
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
sb.AppendLine("[B] 표시 중 " + iv0.ToString("F2") + "s 마다 최상단 재보장");
|
||||||
|
float now = Time.unscaledTime;
|
||||||
|
Decoy(hud, "나중에_뜬_에러팝업"); // 부활 팝업 위로 올라온다
|
||||||
|
bool before = dlg.IsLastSibling;
|
||||||
|
int tc0 = dlg.TopmostCount;
|
||||||
|
dlg.Tick(now + iv0 + 0.01f);
|
||||||
|
bool b = !before && dlg.IsLastSibling && dlg.TopmostCount > tc0;
|
||||||
|
sb.AppendLine(" 에러 팝업이 뒤에 붙음 → 맨뒤형제=" + before + " → Tick(+" + (iv0 + 0.01f).ToString("F2") +
|
||||||
|
"s) → 맨뒤형제=" + dlg.IsLastSibling + " · 재보장 " + tc0 + "→" + dlg.TopmostCount + "회 · " + Ok(b));
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
sb.AppendLine("[C] 재보장 루프 GC 0");
|
||||||
|
for (int i = 0; i < 20; i++) dlg.Tick(now + 100f + i * (iv0 + 0.01f)); // 워밍업(첫 AddComponent 배제)
|
||||||
|
long m0 = GC.GetTotalMemory(true);
|
||||||
|
int n = 600;
|
||||||
|
for (int i = 0; i < n; i++) dlg.Tick(now + 200f + i * (iv0 + 0.01f));
|
||||||
|
long m1 = GC.GetTotalMemory(false);
|
||||||
|
long d = m1 - m0;
|
||||||
|
sb.AppendLine(" Tick ×" + n + "(전부 재보장 실행) → GC.GetTotalMemory 델타 = " + d + " B · 재보장 누계 " +
|
||||||
|
dlg.TopmostCount + "회 · " + Ok(d <= 0));
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
sb.AppendLine("[D] D-9 런타임 버튼 이름");
|
||||||
|
var okName = ReviveDialog.OkButtonName;
|
||||||
|
Transform found = null, old = null;
|
||||||
|
foreach (var t in dlgRt.GetComponentsInChildren<Transform>(true))
|
||||||
|
{
|
||||||
|
if (t.name == okName) found = t;
|
||||||
|
if (t.name == "btn_ok") old = t;
|
||||||
|
}
|
||||||
|
bool dd = found != null && found.GetComponent<Button>() != null && old == null && okName == "WL_btn_ok";
|
||||||
|
sb.AppendLine(" ReviveDialog.OkButtonName=\"" + okName + "\" · 노드 존재=" + (found != null) +
|
||||||
|
" · Button=" + (found != null && found.GetComponent<Button>() != null) +
|
||||||
|
" · 옛 \"btn_ok\" 노드=" + (old != null ? old.name : "없음") + " · " + Ok(dd));
|
||||||
|
sb.AppendLine(" → QA 절차: `LightProbe.ClickButton [\"WL_btn_ok\"]` (원본 `NightmareResult/btn_ok` 와 이름 충돌 0)");
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
sb.AppendLine("[E] 공용 Popup 탈취 → 전용 팝업 승격");
|
||||||
|
dlg.HideNow();
|
||||||
|
sb.AppendLine(" " + dlg.ProbeSimulateCommonPopup("존 시작점에서 부활합니다.\n무료"));
|
||||||
|
sb.AppendLine(" (에디트 모드는 Popup.Ins=" + (Popup.Ins != null ? "있음" : "없음") + " → '공용 팝업이 사라졌다/덮였다'와 같은 조건)");
|
||||||
|
string r = dlg.ProbeCheckHijack();
|
||||||
|
bool e = dlg.HijackCount == 1 && dlg.Escalated && dlg.FallbackVisible && dlg.IsLastSibling && !dlg.UsedCommonPopup;
|
||||||
|
sb.AppendLine(" " + r + " · " + Ok(e));
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
sb.AppendLine("[F] C8 롤백");
|
||||||
|
st.reviveTopmostEnabled = false;
|
||||||
|
var dlg2Rt = Decoy(hud, "WL_ReviveDialog_C8");
|
||||||
|
var dlg2 = dlg2Rt.gameObject.AddComponent<ReviveDialog>();
|
||||||
|
dlg2.Initialize();
|
||||||
|
Decoy(hud, "C8_뒤형제");
|
||||||
|
dlg2.ShowNow();
|
||||||
|
bool f1 = dlg2Rt.GetComponent<Canvas>() == null && !dlg2.IsLastSibling;
|
||||||
|
sb.AppendLine(" reviveTopmostEnabled=0 → Canvas 부착=" + (dlg2Rt.GetComponent<Canvas>() != null) +
|
||||||
|
" · 맨뒤형제=" + dlg2.IsLastSibling + " · " + Ok(f1) + " (813y 동작 100%)");
|
||||||
|
st.reviveTopmostEnabled = tm0;
|
||||||
|
|
||||||
|
st.reviveEscalateOnHijack = false;
|
||||||
|
dlg2.HideNow();
|
||||||
|
dlg2.ProbeSimulateCommonPopup("존 시작점에서 부활합니다.");
|
||||||
|
int hj0 = dlg2.HijackCount;
|
||||||
|
dlg2.ProbeCheckHijack();
|
||||||
|
bool f2 = dlg2.HijackCount == hj0 && !dlg2.Escalated;
|
||||||
|
sb.AppendLine(" reviveEscalateOnHijack=0 → 탈취감지 " + hj0 + "→" + dlg2.HijackCount + " · 승격=" + dlg2.Escalated + " · " + Ok(f2));
|
||||||
|
st.reviveEscalateOnHijack = es0;
|
||||||
|
|
||||||
|
dlg2.HideNow();
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
// ── G. 공용 Popup 싱글턴을 가짜로 세워 D-3 사슬 전체를 재현한다.
|
||||||
|
// (에디트 모드엔 Popup·BackKeyMgr 인스턴스가 없다 — 둘 다 public static 필드라 직접 세운다.)
|
||||||
|
sb.AppendLine("[G] 공용 Popup 실물 재현 — 탈취 감지 / 남의 팝업 불간섭 / QA-4 닫힘");
|
||||||
|
var popup0 = Popup.Ins; var bk0 = BackKeyMgr.Ins;
|
||||||
|
GameObject fakeBk = null, fakePopupGo = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fakeBk = new GameObject("__WL813z_BackKeyMgr"); fakeBk.hideFlags = HideFlags.DontSave;
|
||||||
|
BackKeyMgr.Ins = fakeBk.AddComponent<BackKeyMgr>();
|
||||||
|
|
||||||
|
fakePopupGo = new GameObject("__WL813z_Popup"); fakePopupGo.hideFlags = HideFlags.DontSave;
|
||||||
|
var fp = fakePopupGo.AddComponent<Popup>();
|
||||||
|
var popBody = new GameObject("m_popup"); popBody.hideFlags = HideFlags.DontSave; popBody.transform.SetParent(fakePopupGo.transform, false);
|
||||||
|
var msgGo = new GameObject("label_msg", typeof(RectTransform)); msgGo.hideFlags = HideFlags.DontSave;
|
||||||
|
msgGo.transform.SetParent(popBody.transform, false);
|
||||||
|
fp.m_popup = popBody;
|
||||||
|
fp.label_msg = msgGo.AddComponent<TextMeshProUGUI>();
|
||||||
|
Popup.Ins = fp;
|
||||||
|
|
||||||
|
const string mine = "존 시작점에서 부활합니다.\n무료";
|
||||||
|
|
||||||
|
// G-1. 우리 본문이 그대로면 탈취 아님(오탐 0)
|
||||||
|
dlg.HideNow();
|
||||||
|
dlg.ProbeSimulateCommonPopup(mine);
|
||||||
|
popBody.SetActive(true); fp.label_msg.text = mine;
|
||||||
|
int hj = dlg.HijackCount;
|
||||||
|
dlg.ProbeCheckHijack();
|
||||||
|
bool g1 = dlg.HijackCount == hj && !dlg.Escalated && dlg.UsedCommonPopup;
|
||||||
|
sb.AppendLine(" G-1 본문 그대로 → 탈취감지 " + hj + "→" + dlg.HijackCount + " · 승격=" + dlg.Escalated + " · " + Ok(g1) + " (오탐 0)");
|
||||||
|
|
||||||
|
// G-2. QA-4 — 부활이 끝나면 공용 Popup 도 닫는다(우리 것일 때만)
|
||||||
|
int closed0 = dlg.CommonPopupClosed;
|
||||||
|
dlg.HideNow();
|
||||||
|
bool g2 = !popBody.activeSelf && dlg.CommonPopupClosed == closed0 + 1;
|
||||||
|
sb.AppendLine(" G-2 HideNow(부활 완료) → 공용 Popup active=" + popBody.activeSelf + " · 우리가 닫음 " +
|
||||||
|
closed0 + "→" + dlg.CommonPopupClosed + " · " + Ok(g2) + " (Q4-a QA-4 해소)");
|
||||||
|
|
||||||
|
// G-3. ErrorLogHookManager 가 같은 싱글턴을 덮어쓴 상태 = D-3 그 자체
|
||||||
|
dlg.ProbeSimulateCommonPopup(mine);
|
||||||
|
popBody.SetActive(true); fp.label_msg.text = mine;
|
||||||
|
fp.label_msg.text = "NullReferenceException: Object reference not set …"; // = ErrorLogHookManager.cs:33 이 덮어쓴 뒤
|
||||||
|
int hj2 = dlg.HijackCount;
|
||||||
|
dlg.ProbeCheckHijack();
|
||||||
|
bool g3 = dlg.HijackCount == hj2 + 1 && dlg.Escalated && dlg.FallbackVisible && dlg.IsLastSibling && !dlg.UsedCommonPopup;
|
||||||
|
sb.AppendLine(" G-3 에러 모달이 덮음(본문 교체 = m_ok 도 null 로 지워진 상태) → 탈취감지 " + hj2 + "→" + dlg.HijackCount +
|
||||||
|
" · 승격=" + dlg.Escalated + " · 전용 팝업 표시=" + dlg.FallbackVisible +
|
||||||
|
" · 맨뒤형제=" + dlg.IsLastSibling + " · sortingOrder=" + dlg.SortingOrder + " · " + Ok(g3));
|
||||||
|
|
||||||
|
// G-4. 남의 팝업은 닫지 않는다
|
||||||
|
int closed1 = dlg.CommonPopupClosed;
|
||||||
|
dlg.HideNow();
|
||||||
|
bool g4 = popBody.activeSelf && dlg.CommonPopupClosed == closed1;
|
||||||
|
sb.AppendLine(" G-4 남의(에러) 팝업 → HideNow 후에도 active=" + popBody.activeSelf + " · 닫은 수 " +
|
||||||
|
closed1 + "→" + dlg.CommonPopupClosed + " · " + Ok(g4) + " (불간섭)");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Popup.Ins = popup0; BackKeyMgr.Ins = bk0;
|
||||||
|
Kill(fakePopupGo); Kill(fakeBk);
|
||||||
|
}
|
||||||
|
|
||||||
|
dlg.HideNow();
|
||||||
|
}
|
||||||
|
catch (Exception ex) { sb.AppendLine("**예외** " + ex); }
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
st.reviveTopmostEnabled = tm0; st.reviveSortingOrder = so0;
|
||||||
|
st.reviveTopmostIntervalSeconds = iv0; st.reviveEscalateOnHijack = es0;
|
||||||
|
Kill(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.AppendLine("정리: 합성 노드 파기 · SO 원복 · 씬/프리팹 저장 0");
|
||||||
|
var body = sb.ToString();
|
||||||
|
return body + "\n→ " + Write("WL813z_topmost.txt", body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────── ③ D-7 전리품 토스트 Safe Area 클램프
|
||||||
|
|
||||||
|
|
||||||
|
public static string Toast()
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("=== WL-813z ③ D-7 전리품 토스트 Safe Area 클램프 ===");
|
||||||
|
var s = WLCombatTextSettings.Instance;
|
||||||
|
if (s == null) { var b0 = sb.AppendLine("**FAIL** WLCombatTextSettings.asset 없음").ToString(); return b0 + "\n→ " + Write("WL813z_toast.txt", b0); }
|
||||||
|
|
||||||
|
bool cl0 = s.lootToastClampToSafeArea; float mw0 = s.lootToastMaxWidthPx;
|
||||||
|
float sp0 = s.lootToastSafePadPx; bool el0 = s.lootToastEllipsis;
|
||||||
|
GameObject root = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Canvas canvas; float u;
|
||||||
|
var hud = BuildCanvas(out root, out canvas, out u);
|
||||||
|
var toastRt = Decoy(hud, "WL_LootToast");
|
||||||
|
var toast = toastRt.gameObject.AddComponent<LootToast>();
|
||||||
|
|
||||||
|
sb.AppendLine("합성 Safe Area: 폭 1080 px(=" + (1080f * u).ToString("F1") + " 유닛) · unitsPerPx=" + u.ToString("F4"));
|
||||||
|
sb.AppendLine("에셋 값: widthPx=" + s.lootToastWidthPx.ToString("F0") + " · rightMarginPx=" + s.lootToastRightMarginPx.ToString("F0") +
|
||||||
|
" · slideInPx=" + s.lootToastSlideInPx.ToString("F0") + " · panelPadXPx=" + s.lootToastPanelPadXPx.ToString("F0") +
|
||||||
|
" · centerOffsetPx=" + s.lootToastCenterOffsetPx.ToString("F0") + "(유지)");
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
// ── A. 클램프 OFF = Q4-a 가 본 상태를 그대로 재현
|
||||||
|
s.lootToastClampToSafeArea = false;
|
||||||
|
toast.Initialize();
|
||||||
|
sb.AppendLine("[A] 클램프 OFF(= 813y 현재 · D-7 재현)");
|
||||||
|
sb.AppendLine(" " + toast.LastClamp);
|
||||||
|
float overOff = s.lootToastSlideInPx + s.lootToastPanelPadXPx - s.lootToastRightMarginPx;
|
||||||
|
sb.AppendLine(" 등장 프레임 우측 침범 = slideIn " + s.lootToastSlideInPx.ToString("F0") + " + 패널여백 " +
|
||||||
|
s.lootToastPanelPadXPx.ToString("F0") + " − 우여백 " + s.lootToastRightMarginPx.ToString("F0") +
|
||||||
|
" = **+" + overOff.ToString("F0") + " px**(Safe Area 밖 · " + s.lootToastSlideSeconds.ToString("F2") +
|
||||||
|
"s 동안) → Q4-a 캡처 23_arena_12 / 29_levelup_01 의 잘린 `[-]` 와 일치 · " + Ok(overOff > 0f));
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
// ── B. 클램프 ON
|
||||||
|
s.lootToastClampToSafeArea = true;
|
||||||
|
toast.Initialize();
|
||||||
|
sb.AppendLine("[B] 클램프 ON");
|
||||||
|
sb.AppendLine(" " + toast.LastClamp);
|
||||||
|
var lineRoot = toastRt.Find("Lines") as RectTransform;
|
||||||
|
var line0 = lineRoot != null ? lineRoot.Find("Line0") as RectTransform : null;
|
||||||
|
var label0 = line0 != null ? line0.Find("Label") as RectTransform : null;
|
||||||
|
float rightEdgeUnits = lineRoot != null ? lineRoot.anchoredPosition.x : 0f; // 부모 우측 기준(0 = Safe Area 우변)
|
||||||
|
float padUnits = (s.lootToastPanelEnabled ? s.lootToastPanelPadXPx : 0f) * u;
|
||||||
|
float slideMax = (float)typeof(LootToast).GetField("_slideMaxPx", NP).GetValue(toast);
|
||||||
|
float widthPx = (float)typeof(LootToast).GetField("_widthPx", NP).GetValue(toast);
|
||||||
|
float overOn = (rightEdgeUnits + slideMax * u + padUnits) / u; // > 0 이면 Safe Area 밖
|
||||||
|
bool b = overOn <= 0.001f && widthPx <= s.lootToastMaxWidthPx + 0.001f;
|
||||||
|
sb.AppendLine(" 줄 뿌리 우측 = " + (rightEdgeUnits / u).ToString("F1") + " px(0 = Safe Area 우변) · 슬라이드 상한 = " +
|
||||||
|
slideMax.ToString("F1") + " px · 패널 여백 " + s.lootToastPanelPadXPx.ToString("F0") + " px");
|
||||||
|
sb.AppendLine(" 등장 프레임 우측 침범 = **" + overOn.ToString("F1") + " px** (≤ 0 = Safe Area 안) · 실폭 " +
|
||||||
|
widthPx.ToString("F1") + " px ≤ 상한 " + s.lootToastMaxWidthPx.ToString("F0") + " px · " + Ok(b));
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
// ── C. 긴 이름 말줄임
|
||||||
|
sb.AppendLine("[C] 긴 아이템 이름 말줄임(…)");
|
||||||
|
var tmp = label0 != null ? label0.GetComponent<TextMeshProUGUI>() : null;
|
||||||
|
if (tmp == null) sb.AppendLine(" **FAIL** Line0/Label 없음");
|
||||||
|
else
|
||||||
|
{
|
||||||
|
string longName = "아주아주긴전설급아이템이름테스트문자열0123456789";
|
||||||
|
tmp.text = longName;
|
||||||
|
tmp.ForceMeshUpdate();
|
||||||
|
int shownChars = tmp.textInfo.characterCount;
|
||||||
|
// TMP 는 말줄임을 넣은 뒤 다음 칸에 0x03(End Of Text)을 넣는다(TMP_Text.cs:4067·4077 실측) —
|
||||||
|
// 그래서 마지막 칸이 아니라 **가시 구간 전체**에서 '…' 를 찾는다.
|
||||||
|
bool hasEllipsis = false; int ellipsisAt = -1;
|
||||||
|
for (int ci = 0; ci < shownChars; ci++)
|
||||||
|
if (tmp.textInfo.characterInfo[ci].character == '…') { hasEllipsis = true; ellipsisAt = ci; break; }
|
||||||
|
float lineW = tmp.textInfo.lineCount > 0 ? tmp.textInfo.lineInfo[0].width / u : 0f;
|
||||||
|
float rectW = tmp.rectTransform.rect.width / u;
|
||||||
|
bool c = tmp.textWrappingMode == TextWrappingModes.NoWrap && tmp.overflowMode == TextOverflowModes.Ellipsis
|
||||||
|
&& tmp.isTextTruncated && hasEllipsis && lineW <= rectW + 0.5f;
|
||||||
|
sb.AppendLine(" wrapping=" + tmp.textWrappingMode + " · overflow=" + tmp.overflowMode +
|
||||||
|
" · 원문 " + longName.Length + "자 → 표시 " + shownChars + "자 · isTextTruncated=" + tmp.isTextTruncated +
|
||||||
|
" · '…' 위치 " + ellipsisAt + " · 줄 폭 " + lineW.ToString("F1") + " px ≤ rect " +
|
||||||
|
rectW.ToString("F1") + " px · " + Ok(c));
|
||||||
|
tmp.text = "";
|
||||||
|
}
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
// ── D. C8 롤백
|
||||||
|
s.lootToastClampToSafeArea = false;
|
||||||
|
toast.Initialize();
|
||||||
|
float w2 = (float)typeof(LootToast).GetField("_widthPx", NP).GetValue(toast);
|
||||||
|
float sm2 = (float)typeof(LootToast).GetField("_slideMaxPx", NP).GetValue(toast);
|
||||||
|
bool d = Mathf.Approximately(w2, s.lootToastWidthPx) && Mathf.Approximately(sm2, s.lootToastSlideInPx);
|
||||||
|
sb.AppendLine("[D] C8 롤백 lootToastClampToSafeArea=0 → 폭 " + w2.ToString("F1") + " px(에셋 " +
|
||||||
|
s.lootToastWidthPx.ToString("F0") + ") · 슬라이드 " + sm2.ToString("F1") + " px(에셋 " +
|
||||||
|
s.lootToastSlideInPx.ToString("F0") + ") · " + Ok(d) + " (813y 배치 100%)");
|
||||||
|
toast.Subscribe(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex) { sb.AppendLine("**예외** " + ex); }
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
s.lootToastClampToSafeArea = cl0; s.lootToastMaxWidthPx = mw0;
|
||||||
|
s.lootToastSafePadPx = sp0; s.lootToastEllipsis = el0;
|
||||||
|
Kill(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.AppendLine("정리: 합성 노드 파기 · SO 원복 · Killed 구독 해제");
|
||||||
|
var body = sb.ToString();
|
||||||
|
return body + "\n→ " + Write("WL813z_toast.txt", body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────── 전부
|
||||||
|
|
||||||
|
|
||||||
|
public static string All()
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("################ WL-813z 프로브 전체 ################");
|
||||||
|
sb.AppendLine(Watchdog()); sb.AppendLine();
|
||||||
|
sb.AppendLine(Topmost()); sb.AppendLine();
|
||||||
|
sb.AppendLine(Toast());
|
||||||
|
var body = sb.ToString();
|
||||||
|
return body + "\n→ " + Write("WL813z_all.txt", body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -32,3 +32,10 @@ MonoBehaviour:
|
||||||
reviveInvincibleSeconds: 1.5
|
reviveInvincibleSeconds: 1.5
|
||||||
reviveHealRate: 1
|
reviveHealRate: 1
|
||||||
reviveCompanions: 1
|
reviveCompanions: 1
|
||||||
|
reviveWatchdogSeconds: 20
|
||||||
|
reviveWatchdogIgnoresDisable: 1
|
||||||
|
reviveWatchdogLog: 1
|
||||||
|
reviveTopmostEnabled: 1
|
||||||
|
reviveSortingOrder: 60
|
||||||
|
reviveTopmostIntervalSeconds: 0.5
|
||||||
|
reviveEscalateOnHijack: 1
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,9 @@ namespace WL.Combat.Survival
|
||||||
public static int DeathCount, ReviveCount, ExternalReviveCount, AutoReviveCount, AbortCount;
|
public static int DeathCount, ReviveCount, ExternalReviveCount, AutoReviveCount, AbortCount;
|
||||||
public static float LastDeathToRequest, LastRequestToRevive;
|
public static float LastDeathToRequest, LastRequestToRevive;
|
||||||
public static string LastAbortReason = "";
|
public static string LastAbortReason = "";
|
||||||
|
/// <summary>WL-813z(D-3) — 부활 워치독이 발동한 횟수 · 마지막 발동까지 걸린 대기 시간(초 · unscaled).</summary>
|
||||||
|
public static int WatchdogCount;
|
||||||
|
public static float LastWatchdogSeconds;
|
||||||
|
|
||||||
/// <summary>프로브 전용 — 부활 실행을 바꿔치기한다(에디트 모드에는 LoadMapMgr·ActorInfo 가 없다). 런타임에서는 null 로 둘 것.</summary>
|
/// <summary>프로브 전용 — 부활 실행을 바꿔치기한다(에디트 모드에는 LoadMapMgr·ActorInfo 가 없다). 런타임에서는 null 로 둘 것.</summary>
|
||||||
public static Action<PCActor, float> ReviveOverride;
|
public static Action<PCActor, float> ReviveOverride;
|
||||||
|
|
@ -60,6 +63,8 @@ namespace WL.Combat.Survival
|
||||||
public static bool WorldHeld { get { return s_worldHeld; } }
|
public static bool WorldHeld { get { return s_worldHeld; } }
|
||||||
/// <summary>사망 연출 남은 시간(초 · unscaled · 813j 가 카운트다운에 써도 된다).</summary>
|
/// <summary>사망 연출 남은 시간(초 · unscaled · 813j 가 카운트다운에 써도 된다).</summary>
|
||||||
public static float DisplayRemaining { get { var st = St; return s_state == DeathState.Dying && st != null ? Mathf.Max(0f, st.deathDisplaySeconds - (s_now - s_diedAt)) : 0f; } }
|
public static float DisplayRemaining { get { var st = St; return s_state == DeathState.Dying && st != null ? Mathf.Max(0f, st.deathDisplaySeconds - (s_now - s_diedAt)) : 0f; } }
|
||||||
|
/// <summary>부활 팝업이 응답을 기다린 시간(초 · unscaled · WL-813z 워치독 진단).</summary>
|
||||||
|
public static float AwaitingSeconds { get { return s_state == DeathState.AwaitingRevive ? Mathf.Max(0f, s_now - s_requestedAt) : 0f; } }
|
||||||
|
|
||||||
static WLSurvivalSettings St { get { return WLSurvivalSettings.Instance; } }
|
static WLSurvivalSettings St { get { return WLSurvivalSettings.Instance; } }
|
||||||
static bool Verbose { get { var st = St; return st != null && st.verboseLog; } }
|
static bool Verbose { get { var st = St; return st != null && st.verboseLog; } }
|
||||||
|
|
@ -83,8 +88,24 @@ namespace WL.Combat.Survival
|
||||||
public static void Tick(float unscaledNow)
|
public static void Tick(float unscaledNow)
|
||||||
{
|
{
|
||||||
s_now = unscaledNow;
|
s_now = unscaledNow;
|
||||||
if (s_state == DeathState.None) return;
|
if (s_state == DeathState.None) { if (s_worldHeld) ReleaseWorld(); return; } // 상태는 끝났는데 정지만 남은 경우(안전망)
|
||||||
var st = St;
|
var st = St;
|
||||||
|
|
||||||
|
// ── WL-813z(D-3) 부활 워치독 = 마지막 방어선. Enabled 판정보다 **먼저** 본다.
|
||||||
|
// 공용 Popup 이 에러 모달에 덮여 확인 콜백(m_ok)이 지워져도, SO 스위치가 도중에 꺼져도,
|
||||||
|
// 이 시간이 지나면 반드시 자동 부활하고 ReleaseWorld() 가 timeScale 을 되돌린다.
|
||||||
|
if (s_state == DeathState.AwaitingRevive && st != null && st.reviveWatchdogSeconds > 0f
|
||||||
|
&& (st.reviveWatchdogIgnoresDisable || WLSurvivalSettings.Enabled)
|
||||||
|
&& s_now - s_requestedAt >= st.reviveWatchdogSeconds)
|
||||||
|
{
|
||||||
|
WatchdogCount++;
|
||||||
|
LastWatchdogSeconds = s_now - s_requestedAt;
|
||||||
|
if (st.reviveWatchdogLog) // Log(=Error 아님) — ErrorLogHookManager 모달을 부르지 않는다
|
||||||
|
Debug.Log("[DeathFlow] revive watchdog " + LastWatchdogSeconds.ToString("F1") + "s 초과 — 자동 부활 · 세계 정지 해제(D-3)");
|
||||||
|
Finish(false, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (st == null || !WLSurvivalSettings.Enabled) { Abort("disabled"); return; }
|
if (st == null || !WLSurvivalSettings.Enabled) { Abort("disabled"); return; }
|
||||||
if (s_pc == null) { Abort("pc destroyed"); return; }
|
if (s_pc == null) { Abort("pc destroyed"); return; }
|
||||||
if (!s_pc.IsOnlyDead()) { Finish(true, false); return; } // 바깥(원본 After_Die · 리스폰)이 먼저 살렸다
|
if (!s_pc.IsOnlyDead()) { Finish(true, false); return; } // 바깥(원본 After_Die · 리스폰)이 먼저 살렸다
|
||||||
|
|
@ -182,6 +203,7 @@ namespace WL.Combat.Survival
|
||||||
if (!s_worldHeld) return;
|
if (!s_worldHeld) return;
|
||||||
s_worldHeld = false;
|
s_worldHeld = false;
|
||||||
if (Time.timeScale == 0f) Time.timeScale = MyValue.Get_GameSpeed();
|
if (Time.timeScale == 0f) Time.timeScale = MyValue.Get_GameSpeed();
|
||||||
|
if (Time.timeScale <= 0f) Time.timeScale = 1f; // WL-813z(D-3) 안전망 — Get_GameSpeed 가 0 을 주면 그대로 잠긴다(현재 구현은 항상 1f · 실측)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>프로브용: 상태·카운터 초기화(구독은 유지 · 세계 정지 해제).</summary>
|
/// <summary>프로브용: 상태·카운터 초기화(구독은 유지 · 세계 정지 해제).</summary>
|
||||||
|
|
@ -191,6 +213,7 @@ namespace WL.Combat.Survival
|
||||||
s_state = DeathState.None; s_pc = null; s_acceptEarly = false; s_diedAt = s_requestedAt = 0f;
|
s_state = DeathState.None; s_pc = null; s_acceptEarly = false; s_diedAt = s_requestedAt = 0f;
|
||||||
DeathCount = ReviveCount = ExternalReviveCount = AutoReviveCount = AbortCount = 0;
|
DeathCount = ReviveCount = ExternalReviveCount = AutoReviveCount = AbortCount = 0;
|
||||||
LastDeathToRequest = LastRequestToRevive = 0f; LastAbortReason = "";
|
LastDeathToRequest = LastRequestToRevive = 0f; LastAbortReason = "";
|
||||||
|
WatchdogCount = 0; LastWatchdogSeconds = 0f;
|
||||||
Died.Raised = Died.Dispatched = 0; ReviveRequested.Raised = ReviveRequested.Dispatched = 0; Revived.Raised = Revived.Dispatched = 0;
|
Died.Raised = Died.Dispatched = 0; ReviveRequested.Raised = ReviveRequested.Dispatched = 0; Revived.Raised = Revived.Dispatched = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -128,5 +128,39 @@ namespace WL.Combat.Survival
|
||||||
|
|
||||||
[Tooltip("부활 시 소환수·펫·일꾼도 On_Regen 한다(원본 After_Die 의 PC 분기와 동일).")]
|
[Tooltip("부활 시 소환수·펫·일꾼도 On_Regen 한다(원본 After_Die 의 PC 분기와 동일).")]
|
||||||
public bool reviveCompanions = true;
|
public bool reviveCompanions = true;
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────── 사망 중 영구 정지 방지 (Q4-a D-3 · WL-813z)
|
||||||
|
// 실측 근거(2026-09-09 · Popup.cs 전문 54줄 · ErrorLogHookManager.cs:33)
|
||||||
|
// 공용 Popup 은 노드 1개짜리 싱글턴이고 Set() 이 m_ok 를 **덮어쓴다**.
|
||||||
|
// ErrorLogHookManager 가 Error/Exception 마다 Popup.Ins.Set(One, 174, null, null, log) 을 부르면
|
||||||
|
// 부활 팝업이 넘긴 확인 콜백(m_ok)이 null 로 지워진다 → 확인을 눌러도 부활이 일어나지 않고
|
||||||
|
// holdWorldWhileReviving 이 세운 timeScale=0 이 영영 안 풀린다(Q4-a 실측 2분 30초).
|
||||||
|
// → 아래 워치독이 **마지막 방어선**이다. autoReviveTimeoutSeconds 는 정상 흐름의 응답 상한이고,
|
||||||
|
// 이 값은 "무슨 일이 있어도 세계는 다시 흐른다"를 보장하는 안전장치라 별도 필드로 둔다.
|
||||||
|
[Header("사망 중 영구 정지 방지 (Q4-a D-3 · WL-813z)")]
|
||||||
|
[Tooltip("AwaitingRevive(부활 팝업 대기)가 이 시간(초 · unscaled)을 넘기면 자동 부활하고 세계 정지를 푼다. 0 = 워치독 없음(C8 롤백 = 0).")]
|
||||||
|
public float reviveWatchdogSeconds = 20f;
|
||||||
|
|
||||||
|
[Tooltip("켜면 이 SO 의 enabled·RuntimeOverride 가 꺼져도 워치독만은 계속 돈다(이미 세계를 세워 둔 상태에서 스위치가 꺼져 잠기는 것을 막는다).")]
|
||||||
|
public bool reviveWatchdogIgnoresDisable = true;
|
||||||
|
|
||||||
|
[Tooltip("워치독이 발동하면 Console 에 Log 1줄을 남긴다(Warning/Error 가 아니라 ErrorLogHookManager 모달을 부르지 않는다).")]
|
||||||
|
public bool reviveWatchdogLog = true;
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────── 부활 팝업 최상단 보장 (UI · ReviveDialog 가 읽는다)
|
||||||
|
// 🔴 UI 값이지만 이번 발주(WL-813z §2)가 허용한 UI 파일 목록에 WLSurvivalUiSettings.cs 가 없어
|
||||||
|
// 813q 와의 충돌 0 을 지키려고 사망/부활 소유 SO 인 여기에 둔다(Lead 판단으로 이관 가능).
|
||||||
|
[Header("부활 팝업 최상단 보장 (D-3 · ReviveDialog 가 읽는다 · WL-813z)")]
|
||||||
|
[Tooltip("끄면 최상단 보장·재보장·승격을 전부 하지 않는다(C8 롤백 = 0 · 813y 동작 100%).")]
|
||||||
|
public bool reviveTopmostEnabled = true;
|
||||||
|
|
||||||
|
[Tooltip("부활 팝업 전용 캔버스의 sortingOrder. 공용 팝업 프리팹 SortOrder_5 의 Canvas.m_SortingOrder = 5 (실측) 보다 커야 한다.")]
|
||||||
|
public int reviveSortingOrder = 60;
|
||||||
|
|
||||||
|
[Tooltip("표시 중 최상단·레이캐스트를 다시 보장하는 주기(초 · unscaled · 0 이면 표시 순간 1회만). GC 0 경로.")]
|
||||||
|
public float reviveTopmostIntervalSeconds = 0.5f;
|
||||||
|
|
||||||
|
[Tooltip("공용 Popup 이 다른 팝업(에러 모달 등)에 덮인 것을 감지하면 전용 폴백 팝업으로 승격해 확인 버튼을 되살린다.")]
|
||||||
|
public bool reviveEscalateOnHijack = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,11 @@ namespace WL.UI
|
||||||
private float _nextPoll;
|
private float _nextPoll;
|
||||||
private bool _armed; // Killed 를 한 번이라도 본 뒤부터 폴링한다(로비에서 헛돌지 않게)
|
private bool _armed; // Killed 를 한 번이라도 본 뒤부터 폴링한다(로비에서 헛돌지 않게)
|
||||||
|
|
||||||
|
// ── WL-813z(D-7) Safe Area 클램프 실값(ApplyLayout 이 계산 · Update 슬라이드가 읽는다)
|
||||||
|
private float _widthPx, _rightPx, _padPx, _slideMaxPx;
|
||||||
|
/// <summary>WL-813z — 마지막 클램프 실측(프로브·Dump 용).</summary>
|
||||||
|
public string LastClamp { get; private set; } = "";
|
||||||
|
|
||||||
// ── 진단(프로브가 읽는다 · 실측만)
|
// ── 진단(프로브가 읽는다 · 실측만)
|
||||||
public int KilledSeen { get; private set; }
|
public int KilledSeen { get; private set; }
|
||||||
public int PushCount { get; private set; }
|
public int PushCount { get; private set; }
|
||||||
|
|
@ -132,25 +137,55 @@ namespace WL.UI
|
||||||
float u = WLTextFxUtil.UnitsPerPx(this, s);
|
float u = WLTextFxUtil.UnitsPerPx(this, s);
|
||||||
WLTextFxUtil.Stretch(_rt);
|
WLTextFxUtil.Stretch(_rt);
|
||||||
|
|
||||||
|
// ── WL-813z(D-7) — 폭·오른쪽 여백·슬라이드 시작 x 를 Safe Area 안으로 클램프.
|
||||||
|
// 부모(IngameUIs/WL_HUD)는 SafeAreaFitter 아래이므로 이 RectTransform 의 rect 가 곧 Safe Area 다.
|
||||||
|
// 중심 오프셋(lootToastCenterOffsetPx)은 발주서 지시대로 **그대로 둔다**.
|
||||||
|
_widthPx = s.lootToastWidthPx;
|
||||||
|
_rightPx = s.lootToastRightMarginPx;
|
||||||
|
_padPx = 0f;
|
||||||
|
_slideMaxPx = s.lootToastSlideInPx;
|
||||||
|
float parentPx = u > 0.0001f ? _rt.rect.width / u : 0f;
|
||||||
|
if (s.lootToastClampToSafeArea)
|
||||||
|
{
|
||||||
|
_padPx = Mathf.Max(0f, s.lootToastSafePadPx) + (s.lootToastPanelEnabled ? Mathf.Max(0f, s.lootToastPanelPadXPx) : 0f);
|
||||||
|
_rightPx = Mathf.Max(_padPx, _rightPx);
|
||||||
|
if (s.lootToastMaxWidthPx > 0f) _widthPx = Mathf.Min(_widthPx, s.lootToastMaxWidthPx);
|
||||||
|
if (parentPx > 1f) _widthPx = Mathf.Min(_widthPx, parentPx - _rightPx - _padPx);
|
||||||
|
_widthPx = Mathf.Max(1f, _widthPx);
|
||||||
|
_slideMaxPx = Mathf.Clamp(_rightPx - _padPx, 0f, s.lootToastSlideInPx); // 등장 중에도 오른쪽 밖으로 못 나간다
|
||||||
|
}
|
||||||
|
LastClamp = "부모=" + parentPx.ToString("F1") + "px · 폭 " + s.lootToastWidthPx.ToString("F0") + "→" + _widthPx.ToString("F1") +
|
||||||
|
"px(상한 " + s.lootToastMaxWidthPx.ToString("F0") + ") · 우여백 " + s.lootToastRightMarginPx.ToString("F0") + "→" + _rightPx.ToString("F1") +
|
||||||
|
"px · pad=" + _padPx.ToString("F1") + "px · 슬라이드 " + s.lootToastSlideInPx.ToString("F0") + "→" + _slideMaxPx.ToString("F1") +
|
||||||
|
"px · 우측 최대 침범 " + Mathf.Max(0f, _slideMaxPx + _padPx - _rightPx).ToString("F1") + "px · clamp=" + s.lootToastClampToSafeArea;
|
||||||
|
|
||||||
lineRoot.anchorMin = lineRoot.anchorMax = new Vector2(1f, 0.5f); // 우측 · 세로 중앙 기준
|
lineRoot.anchorMin = lineRoot.anchorMax = new Vector2(1f, 0.5f); // 우측 · 세로 중앙 기준
|
||||||
lineRoot.pivot = new Vector2(1f, 0.5f);
|
lineRoot.pivot = new Vector2(1f, 0.5f);
|
||||||
lineRoot.sizeDelta = new Vector2(s.lootToastWidthPx * u, s.lootToastLineHeightPx * s.lootToastLines * u);
|
lineRoot.sizeDelta = new Vector2(_widthPx * u, s.lootToastLineHeightPx * s.lootToastLines * u);
|
||||||
lineRoot.anchoredPosition = new Vector2(-s.lootToastRightMarginPx * u, s.lootToastCenterOffsetPx * u);
|
lineRoot.anchoredPosition = new Vector2(-_rightPx * u, s.lootToastCenterOffsetPx * u);
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.Append("lines=").Append(_lines.Count).Append(" root pos=").Append(lineRoot.anchoredPosition)
|
sb.Append("lines=").Append(_lines.Count).Append(" root pos=").Append(lineRoot.anchoredPosition)
|
||||||
.Append(" size=").Append(lineRoot.sizeDelta).Append(" unitsPerPx=").Append(u.ToString("F4"));
|
.Append(" size=").Append(lineRoot.sizeDelta).Append(" unitsPerPx=").Append(u.ToString("F4"))
|
||||||
|
.Append(" · ").Append(LastClamp);
|
||||||
for (int i = 0; i < _lines.Count; i++)
|
for (int i = 0; i < _lines.Count; i++)
|
||||||
{
|
{
|
||||||
var l = _lines[i];
|
var l = _lines[i];
|
||||||
l.rt.anchorMin = l.rt.anchorMax = new Vector2(1f, 1f);
|
l.rt.anchorMin = l.rt.anchorMax = new Vector2(1f, 1f);
|
||||||
l.rt.pivot = new Vector2(1f, 1f);
|
l.rt.pivot = new Vector2(1f, 1f);
|
||||||
l.rt.sizeDelta = new Vector2(s.lootToastWidthPx * u, s.lootToastLineHeightPx * u);
|
l.rt.sizeDelta = new Vector2(_widthPx * u, s.lootToastLineHeightPx * u);
|
||||||
l.rt.anchoredPosition = new Vector2(0f, -s.lootToastLineHeightPx * i * u);
|
l.rt.anchoredPosition = new Vector2(0f, -s.lootToastLineHeightPx * i * u);
|
||||||
WLTextFxUtil.Stretch(l.label.rectTransform);
|
WLTextFxUtil.Stretch(l.label.rectTransform);
|
||||||
l.label.fontSize = s.lootToastFontPx * u;
|
l.label.fontSize = s.lootToastFontPx * u;
|
||||||
l.label.alignment = TextAlignmentOptions.MidlineRight;
|
l.label.alignment = TextAlignmentOptions.MidlineRight;
|
||||||
|
|
||||||
|
// WL-813z(D-7) — 긴 아이템 이름은 줄바꿈 대신 말줄임(…)으로 자른다(폭 밖으로 밀려나지 않게).
|
||||||
|
if (s.lootToastEllipsis)
|
||||||
|
{
|
||||||
|
if (l.label.textWrappingMode != TextWrappingModes.NoWrap) l.label.textWrappingMode = TextWrappingModes.NoWrap;
|
||||||
|
if (l.label.overflowMode != TextOverflowModes.Ellipsis) l.label.overflowMode = TextOverflowModes.Ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
// WL-813y — 줄 뒤 패널(대비) + 글자 아웃라인. 값은 전부 SO.
|
// WL-813y — 줄 뒤 패널(대비) + 글자 아웃라인. 값은 전부 SO.
|
||||||
if (l.panel != null)
|
if (l.panel != null)
|
||||||
{
|
{
|
||||||
|
|
@ -293,7 +328,9 @@ namespace WL.UI
|
||||||
if (s.lootToastSlideInPx > 0f && s.lootToastSlideSeconds > 0f)
|
if (s.lootToastSlideInPx > 0f && s.lootToastSlideSeconds > 0f)
|
||||||
{
|
{
|
||||||
float k = Mathf.Clamp01(l.elapsed / s.lootToastSlideSeconds);
|
float k = Mathf.Clamp01(l.elapsed / s.lootToastSlideSeconds);
|
||||||
float x = Mathf.Lerp(s.lootToastSlideInPx, 0f, k) * u;
|
// WL-813z(D-7) — 등장 x 상한 = Safe Area 를 넘지 않는 값(ApplyLayout 계산 · 클램프 off 면 원래 값).
|
||||||
|
float startPx = s.lootToastClampToSafeArea ? _slideMaxPx : s.lootToastSlideInPx;
|
||||||
|
float x = Mathf.Lerp(startPx, 0f, k) * u;
|
||||||
l.rt.anchoredPosition = new Vector2(x, -s.lootToastLineHeightPx * i * u);
|
l.rt.anchoredPosition = new Vector2(x, -s.lootToastLineHeightPx * i * u);
|
||||||
}
|
}
|
||||||
if (l.life <= 0f) { l.used = false; l.group.alpha = 0f; l.label.text = ""; }
|
if (l.life <= 0f) { l.used = false; l.group.alpha = 0f; l.label.text = ""; }
|
||||||
|
|
@ -369,6 +406,7 @@ namespace WL.UI
|
||||||
" rightMarginPx=" + st.lootToastRightMarginPx + " centerOffsetPx=" + st.lootToastCenterOffsetPx +
|
" rightMarginPx=" + st.lootToastRightMarginPx + " centerOffsetPx=" + st.lootToastCenterOffsetPx +
|
||||||
" panel=" + st.lootToastPanelEnabled + " 밝기하한=" + st.lootToastMinBrightness +
|
" panel=" + st.lootToastPanelEnabled + " 밝기하한=" + st.lootToastMinBrightness +
|
||||||
" · 등급1 태그 " + st.GradeColorTag(1) + " → " + st.GradeColorTagBright(1));
|
" · 등급1 태그 " + st.GradeColorTag(1) + " → " + st.GradeColorTagBright(1));
|
||||||
|
sb.AppendLine(" clamp = " + LastClamp);
|
||||||
sb.AppendLine(" edge = " + LastEdge);
|
sb.AppendLine(" edge = " + LastEdge);
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,22 @@
|
||||||
// 🔴 어셈블리 주의: Assets/WL/UI/ 에 .asmdef 를 만들지 말 것(Assembly-CSharp 단일).
|
// 🔴 어셈블리 주의: 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 System.Text;
|
||||||
using TMPro;
|
using TMPro;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.UI;
|
using UnityEngine.UI;
|
||||||
|
using WL.Combat.Survival;
|
||||||
|
|
||||||
namespace WL.UI
|
namespace WL.UI
|
||||||
{
|
{
|
||||||
|
|
@ -87,6 +99,14 @@ namespace WL.UI
|
||||||
private float _showAt = -1f;
|
private float _showAt = -1f;
|
||||||
private bool _open; // 팝업이 열려 있는가(공용 Popup 경로 포함 · WL-813y)
|
private bool _open; // 팝업이 열려 있는가(공용 Popup 경로 포함 · WL-813y)
|
||||||
|
|
||||||
|
// ── WL-813z(D-3) 최상단 보장 ──────────────────────────────────────────
|
||||||
|
/// <summary>런타임 생성 확인 버튼 이름 — 원본 `NightmareResult/btn_ok` 와 겹치지 않게 접두어를 붙인다(D-9).</summary>
|
||||||
|
public const string OkButtonName = "WL_btn_ok";
|
||||||
|
private Canvas _ownCanvas;
|
||||||
|
private GraphicRaycaster _ownRaycaster;
|
||||||
|
private float _nextTopmostAt = -1f;
|
||||||
|
private bool _escalated; // 공용 Popup 이 덮여 전용 팝업으로 승격했다
|
||||||
|
|
||||||
// ── 진단 ──────────────────────────────────────────────────────────────
|
// ── 진단 ──────────────────────────────────────────────────────────────
|
||||||
public int DeathSeen { get; private set; }
|
public int DeathSeen { get; private set; }
|
||||||
public int ShownCount { get; private set; }
|
public int ShownCount { get; private set; }
|
||||||
|
|
@ -95,6 +115,14 @@ namespace WL.UI
|
||||||
public string LastMessage { get; private set; } = "";
|
public string LastMessage { get; private set; } = "";
|
||||||
public bool FallbackVisible { get { return group != null && group.alpha > 0.001f; } }
|
public bool FallbackVisible { get { return group != null && group.alpha > 0.001f; } }
|
||||||
public bool Pending { get { return _showAt >= 0f; } }
|
public bool Pending { get { return _showAt >= 0f; } }
|
||||||
|
/// <summary>WL-813z — 최상단 재보장 횟수 · 공용 Popup 탈취 감지 횟수 · 우리가 닫은 공용 Popup 수.</summary>
|
||||||
|
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<RectTransform>(); Active = this; }
|
private void Awake() { _rt = GetComponent<RectTransform>(); Active = this; }
|
||||||
private void OnEnable() { Active = this; Initialize(); }
|
private void OnEnable() { Active = this; Initialize(); }
|
||||||
|
|
@ -124,14 +152,85 @@ namespace WL.UI
|
||||||
|
|
||||||
private void Update() { Tick(Time.unscaledTime); }
|
private void Update() { Tick(Time.unscaledTime); }
|
||||||
|
|
||||||
/// <summary>지연 만료 검사. 프로브가 시간을 넣어 같은 경로로 검증한다.</summary>
|
/// <summary>지연 만료 검사 + 표시 중 최상단 재보장. 프로브가 시간을 넣어 같은 경로로 검증한다.</summary>
|
||||||
public void Tick(float now)
|
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;
|
if (_showAt < 0f || now < _showAt) return;
|
||||||
_showAt = -1f;
|
_showAt = -1f;
|
||||||
ShowNow();
|
ShowNow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// WL-813z(D-3) — 전용 노드를 형제 맨 뒤 + 전용 캔버스(sortingOrder = SO)로 올리고 레이캐스트를 살린다.
|
||||||
|
/// 공용 팝업 프리팹 `SortOrder_5` 의 Canvas.m_SortingOrder = 5(실측)보다 커야 확인 버튼이 안 가린다.
|
||||||
|
/// 값이 이미 맞으면 아무 것도 쓰지 않는다(GC 0 · SetDirty 0).
|
||||||
|
/// </summary>
|
||||||
|
public void EnsureTopmost(WLSurvivalSettings g)
|
||||||
|
{
|
||||||
|
if (g == null || !g.reviveTopmostEnabled) return;
|
||||||
|
if (_rt == null) _rt = GetComponent<RectTransform>();
|
||||||
|
if (_rt == null) return;
|
||||||
|
|
||||||
|
var parent = _rt.parent;
|
||||||
|
if (parent != null && _rt.GetSiblingIndex() != parent.childCount - 1) _rt.SetAsLastSibling();
|
||||||
|
|
||||||
|
if (_ownCanvas == null)
|
||||||
|
{
|
||||||
|
_ownCanvas = GetComponent<Canvas>();
|
||||||
|
if (_ownCanvas == null) _ownCanvas = gameObject.AddComponent<Canvas>();
|
||||||
|
}
|
||||||
|
if (!_ownCanvas.overrideSorting) _ownCanvas.overrideSorting = true;
|
||||||
|
if (_ownCanvas.sortingOrder != g.reviveSortingOrder) _ownCanvas.sortingOrder = g.reviveSortingOrder;
|
||||||
|
|
||||||
|
// 전용 캔버스를 얹으면 부모 GraphicRaycaster 의 레이캐스트가 여기서 끊긴다 — 자기 것을 붙인다.
|
||||||
|
if (_ownRaycaster == null)
|
||||||
|
{
|
||||||
|
_ownRaycaster = GetComponent<GraphicRaycaster>();
|
||||||
|
if (_ownRaycaster == null) _ownRaycaster = gameObject.AddComponent<GraphicRaycaster>();
|
||||||
|
}
|
||||||
|
if (!_ownRaycaster.enabled) _ownRaycaster.enabled = true;
|
||||||
|
|
||||||
|
TopmostCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// WL-813z(D-3) — 공용 Popup 이 남의 내용으로 바뀌었는지(= 우리 확인 콜백 m_ok 가 지워졌는지) 본다.
|
||||||
|
/// m_ok 는 private 이라 직접 못 읽으므로 **본문 텍스트와 활성 상태**로 판정한다(문자열 비교 = 할당 0).
|
||||||
|
/// 탈취를 확인하면 전용 폴백 팝업으로 승격한다 — 그쪽은 최상단 캔버스라 확인 버튼에 반드시 닿는다.
|
||||||
|
/// </summary>
|
||||||
|
private void CheckCommonPopupHijack(WLSurvivalSettings g)
|
||||||
|
{
|
||||||
|
if (!g.reviveEscalateOnHijack || !UsedCommonPopup || _escalated) return;
|
||||||
|
bool lost;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var p = Popup.Ins;
|
||||||
|
if (p == null || p.m_popup == null || p.label_msg == null) lost = true;
|
||||||
|
else lost = !p.m_popup.activeSelf || !string.Equals(p.label_msg.text, LastMessage);
|
||||||
|
}
|
||||||
|
catch { lost = true; }
|
||||||
|
if (!lost) return;
|
||||||
|
|
||||||
|
HijackCount++;
|
||||||
|
_escalated = true;
|
||||||
|
UsedCommonPopup = false;
|
||||||
|
var s = WLSurvivalUiSettings.Instance;
|
||||||
|
if (s != null) ShowFallback(s, LastMessage);
|
||||||
|
Debug.Log("[ReviveDialog] 공용 Popup 이 다른 팝업에 덮였다 — 전용 부활 팝업으로 승격(D-3)");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>예약을 지우고, 아직 안 떠 있을 때만 연다(WL-813y · 813i 이중 통지 방지).</summary>
|
/// <summary>예약을 지우고, 아직 안 떠 있을 때만 연다(WL-813y · 813i 이중 통지 방지).</summary>
|
||||||
public string ShowNowOnce()
|
public string ShowNowOnce()
|
||||||
{
|
{
|
||||||
|
|
@ -151,6 +250,12 @@ namespace WL.UI
|
||||||
LastMessage = msg;
|
LastMessage = msg;
|
||||||
ShownCount++;
|
ShownCount++;
|
||||||
UsedCommonPopup = false;
|
UsedCommonPopup = false;
|
||||||
|
_escalated = false;
|
||||||
|
|
||||||
|
// WL-813z(D-3) — 표시 순간 최상단 1회 + 재보장 시각 예약(공용 팝업 경로에서도 승격 대비로 올려 둔다)
|
||||||
|
var g = WLSurvivalSettings.Instance;
|
||||||
|
EnsureTopmost(g);
|
||||||
|
_nextTopmostAt = Time.unscaledTime + (g != null ? Mathf.Max(0f, g.reviveTopmostIntervalSeconds) : 0f);
|
||||||
|
|
||||||
if (s.reviveUseCommonPopup)
|
if (s.reviveUseCommonPopup)
|
||||||
{
|
{
|
||||||
|
|
@ -205,6 +310,7 @@ namespace WL.UI
|
||||||
ApplyLayout();
|
ApplyLayout();
|
||||||
if (messageLabel != null) messageLabel.text = msg;
|
if (messageLabel != null) messageLabel.text = msg;
|
||||||
if (group != null) { group.alpha = 1f; group.blocksRaycasts = true; group.interactable = true; }
|
if (group != null) { group.alpha = 1f; group.blocksRaycasts = true; group.interactable = true; }
|
||||||
|
EnsureTopmost(WLSurvivalSettings.Instance); // WL-813z(D-3) — 승격 직후에도 최상단·레이캐스트 보장
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>확인 = 부활 요청. 813i 가 ReviveRequested 를 채우면 실제 부활이 일어난다.</summary>
|
/// <summary>확인 = 부활 요청. 813i 가 ReviveRequested 를 채우면 실제 부활이 일어난다.</summary>
|
||||||
|
|
@ -220,11 +326,33 @@ namespace WL.UI
|
||||||
public void HideNow()
|
public void HideNow()
|
||||||
{
|
{
|
||||||
_showAt = -1f;
|
_showAt = -1f;
|
||||||
|
bool wasOpen = _open;
|
||||||
_open = false;
|
_open = false;
|
||||||
|
_nextTopmostAt = -1f;
|
||||||
|
_escalated = false;
|
||||||
|
if (wasOpen) CloseCommonPopupIfOurs(); // WL-813z — QA-4 「부활했는데 공용 팝업이 남는다」
|
||||||
if (group == null) return;
|
if (group == null) return;
|
||||||
group.alpha = 0f; group.blocksRaycasts = false; group.interactable = false;
|
group.alpha = 0f; group.blocksRaycasts = false; group.interactable = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// WL-813z — 공용 Popup 이 **아직 우리 본문을 들고 있을 때만** 닫는다(남의 팝업은 건드리지 않는다).
|
||||||
|
/// `OnClick_Back()` = 원본 `ClosePopup()`(SetActive(false) + BackKeyMgr.Del_Back) + `m_no?.Invoke()`.
|
||||||
|
/// 우리는 `_no` 를 넘긴 적이 없고 본문이 우리 것이면 `Set()` 이 다시 안 불린 것이라 `m_no` 는 null 이다(Popup.cs:14 실측).
|
||||||
|
/// </summary>
|
||||||
|
private void CloseCommonPopupIfOurs()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var p = Popup.Ins;
|
||||||
|
if (p == null || p.m_popup == null || !p.m_popup.activeSelf) return;
|
||||||
|
if (p.label_msg == null || !string.Equals(p.label_msg.text, LastMessage)) return;
|
||||||
|
p.OnClick_Back();
|
||||||
|
CommonPopupClosed++;
|
||||||
|
}
|
||||||
|
catch { /* 씬 미로드·에디트 모드 — 무동작 */ }
|
||||||
|
}
|
||||||
|
|
||||||
// ── 구성 · 배치 (폴백 팝업) ──────────────────────────────────────────
|
// ── 구성 · 배치 (폴백 팝업) ──────────────────────────────────────────
|
||||||
public bool BuildIfNeeded()
|
public bool BuildIfNeeded()
|
||||||
{
|
{
|
||||||
|
|
@ -260,7 +388,9 @@ namespace WL.UI
|
||||||
}
|
}
|
||||||
if (okButton == null)
|
if (okButton == null)
|
||||||
{
|
{
|
||||||
var t = WLVignetteUtil.NewChild(box, "btn_ok");
|
// WL-813z(D-9): 이름이 원본 `NewGameUI(Clone)/…/NightmareResult/btn_ok` 와 겹쳐
|
||||||
|
// `LightProbe.ClickButton ["btn_ok"]` 가 비활성인 원본 쪽을 잡았다 → `WL_` 접두어로 분리.
|
||||||
|
var t = WLVignetteUtil.NewChild(box, OkButtonName);
|
||||||
var img = t.GetComponent<Image>();
|
var img = t.GetComponent<Image>();
|
||||||
if (img == null) img = t.gameObject.AddComponent<Image>();
|
if (img == null) img = t.gameObject.AddComponent<Image>();
|
||||||
img.raycastTarget = true;
|
img.raycastTarget = true;
|
||||||
|
|
@ -281,6 +411,8 @@ namespace WL.UI
|
||||||
if (font != null) okLabel.font = font;
|
if (font != null) okLabel.font = font;
|
||||||
made = true;
|
made = true;
|
||||||
}
|
}
|
||||||
|
// WL-813z(D-9) — 이전 세션이 만든(또는 직렬화된) `btn_ok` 도 이름을 통일한다.
|
||||||
|
if (okButton != null && okButton.gameObject.name != OkButtonName) okButton.gameObject.name = OkButtonName;
|
||||||
return made;
|
return made;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -351,6 +483,30 @@ namespace WL.UI
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 검증 전용 ────────────────────────────────────────────────────────
|
// ── 검증 전용 ────────────────────────────────────────────────────────
|
||||||
|
/// <summary>
|
||||||
|
/// 프로브 전용(WL-813z) — 에디트 모드에는 공용 `Popup` 싱글턴이 없어 그 경로를 탈 수 없다.
|
||||||
|
/// "공용 Popup 으로 열려 있다" 상태만 만들어 탈취 감지·승격 경로를 같은 코드로 검증한다. 런타임 호출 0.
|
||||||
|
/// </summary>
|
||||||
|
public string ProbeSimulateCommonPopup(string msg)
|
||||||
|
{
|
||||||
|
LastMessage = msg ?? "";
|
||||||
|
UsedCommonPopup = true;
|
||||||
|
_escalated = false;
|
||||||
|
_open = true;
|
||||||
|
ShownCount++;
|
||||||
|
return "공용 Popup 사용 표시 · open=" + _open + " 본문=\"" + LastMessage.Replace("\n", " / ") + "\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>프로브 전용(WL-813z) — 탈취 감지 1회를 즉시 돌린다.</summary>
|
||||||
|
public string ProbeCheckHijack()
|
||||||
|
{
|
||||||
|
var g = WLSurvivalSettings.Instance;
|
||||||
|
if (g == null) return "WLSurvivalSettings 에셋 없음 — 판정 불가";
|
||||||
|
CheckCommonPopupHijack(g);
|
||||||
|
return "탈취감지=" + HijackCount + "회 · 승격=" + _escalated + " · 공용팝업사용=" + UsedCommonPopup +
|
||||||
|
" · 폴백표시=" + FallbackVisible + " · 맨뒤형제=" + IsLastSibling + " · sortingOrder=" + SortingOrder;
|
||||||
|
}
|
||||||
|
|
||||||
public string Dump()
|
public string Dump()
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
|
|
@ -362,9 +518,21 @@ namespace WL.UI
|
||||||
" 브리지=" + WLSurvivalUiBridge.Installed);
|
" 브리지=" + WLSurvivalUiBridge.Installed);
|
||||||
sb.AppendLine(" 본문=\"" + LastMessage.Replace("\n", " / ") + "\"");
|
sb.AppendLine(" 본문=\"" + LastMessage.Replace("\n", " / ") + "\"");
|
||||||
sb.AppendLine(" 공용 Popup.Ins=" + (Popup.Ins != null ? "있음" : "없음(에디트 모드/씬 미로드)"));
|
sb.AppendLine(" 공용 Popup.Ins=" + (Popup.Ins != null ? "있음" : "없음(에디트 모드/씬 미로드)"));
|
||||||
|
// WL-813z(D-3/D-9)
|
||||||
|
var g = WLSurvivalSettings.Instance;
|
||||||
|
sb.AppendLine(" 최상단 = sortingOrder " + SortingOrder + "(SO " + (g != null ? g.reviveSortingOrder.ToString() : "-") +
|
||||||
|
") · 맨뒤형제=" + IsLastSibling + " · 재보장=" + TopmostCount + "회 · 탈취감지=" + HijackCount +
|
||||||
|
" · 승격=" + _escalated + " · 공용팝업닫음=" + CommonPopupClosed +
|
||||||
|
" · raycaster=" + (_ownRaycaster != null && _ownRaycaster.enabled) +
|
||||||
|
" · blocksRaycasts=" + (group != null && group.blocksRaycasts));
|
||||||
|
sb.AppendLine(" 워치독 = " + (g == null ? "(WLSurvivalSettings 없음)" :
|
||||||
|
g.reviveWatchdogSeconds.ToString("F1") + "s · 무시disable=" + g.reviveWatchdogIgnoresDisable +
|
||||||
|
" · 최상단ON=" + g.reviveTopmostEnabled + " 주기=" + g.reviveTopmostIntervalSeconds.ToString("F2") + "s") +
|
||||||
|
" · DeathFlow 발동=" + DeathFlow.WatchdogCount + "회 대기=" + DeathFlow.AwaitingSeconds.ToString("F2") + "s");
|
||||||
if (box != null)
|
if (box != null)
|
||||||
sb.AppendLine(" 폴백 box size" + box.sizeDelta + " pos" + box.anchoredPosition +
|
sb.AppendLine(" 폴백 box size" + box.sizeDelta + " pos" + box.anchoredPosition +
|
||||||
" ok=\"" + (okLabel != null ? okLabel.text : "-") + "\"");
|
" ok=\"" + (okLabel != null ? okLabel.text : "-") + "\" okName=" +
|
||||||
|
(okButton != null ? okButton.gameObject.name : "-"));
|
||||||
sb.AppendLine(" 설정 " + (s == null ? "(에셋 없음)" :
|
sb.AppendLine(" 설정 " + (s == null ? "(에셋 없음)" :
|
||||||
"enabled=" + s.reviveEnabled + " 공용팝업=" + s.reviveUseCommonPopup +
|
"enabled=" + s.reviveEnabled + " 공용팝업=" + s.reviveUseCommonPopup +
|
||||||
" delay=" + s.reviveDelaySeconds + "s key=" + s.reviveMessageKey +
|
" delay=" + s.reviveDelaySeconds + "s key=" + s.reviveMessageKey +
|
||||||
|
|
|
||||||
|
|
@ -127,3 +127,7 @@ MonoBehaviour:
|
||||||
lootToastPanelPadYPx: 6
|
lootToastPanelPadYPx: 6
|
||||||
lootToastMinBrightColor: {r: 0.95, g: 0.96, b: 0.98, a: 1}
|
lootToastMinBrightColor: {r: 0.95, g: 0.96, b: 0.98, a: 1}
|
||||||
lootToastMinBrightness: 0.72
|
lootToastMinBrightness: 0.72
|
||||||
|
lootToastClampToSafeArea: 1
|
||||||
|
lootToastMaxWidthPx: 560
|
||||||
|
lootToastSafePadPx: 8
|
||||||
|
lootToastEllipsis: 1
|
||||||
|
|
|
||||||
|
|
@ -219,6 +219,24 @@ namespace WL.UI
|
||||||
[Tooltip("등급 색 팔레트(index 0 = 등급 1). 비우면 MyValue.Get_GradeColor(코드 팔레트)를 그대로 쓴다.")]
|
[Tooltip("등급 색 팔레트(index 0 = 등급 1). 비우면 MyValue.Get_GradeColor(코드 팔레트)를 그대로 쓴다.")]
|
||||||
public Color[] lootToastGradeColors = new Color[0];
|
public Color[] lootToastGradeColors = new Color[0];
|
||||||
|
|
||||||
|
// ─────────────────────────────────────── 전리품 토스트 Safe Area 클램프 (Q4-a D-7 · WL-813z)
|
||||||
|
// 실측 원인: 줄이 등장할 때 x 가 +lootToastSlideInPx(60) 에서 0 으로 온다. 줄 뿌리는 오른쪽 가장자리에서
|
||||||
|
// lootToastRightMarginPx(40) 안쪽에 있으므로 등장 프레임의 오른쪽 끝은 −40 + 60 = **+20 px**,
|
||||||
|
// 뒤 패널(lootToastPanelPadXPx = 18)까지 더하면 **+38 px** 가 Safe Area 밖으로 나간다
|
||||||
|
// (0.14 s 동안 · Q4-a 캡처 `23_arena_12.png`·`29_levelup_01.png` 의 잘린 `[-]` 조각과 일치).
|
||||||
|
[Header("전리품 토스트 Safe Area 클램프 (Q4-a D-7 · WL-813z)")]
|
||||||
|
[Tooltip("끄면 813y 배치 100%(C8 롤백 = 0). 켜면 폭·오른쪽 여백·슬라이드 시작 x 를 Safe Area 안으로 자른다.")]
|
||||||
|
public bool lootToastClampToSafeArea = true;
|
||||||
|
|
||||||
|
[Tooltip("줄 폭 상한(px · 0 이면 상한 없음). 실제 폭 = min(lootToastWidthPx, 이 값, 부모 폭 − 좌우 여백).")]
|
||||||
|
public float lootToastMaxWidthPx = 560f;
|
||||||
|
|
||||||
|
[Tooltip("Safe Area 안쪽으로 더 확보할 여백(px). 뒤 패널 여백(lootToastPanelPadXPx)은 여기에 자동으로 더해진다.")]
|
||||||
|
public float lootToastSafePadPx = 8f;
|
||||||
|
|
||||||
|
[Tooltip("이름이 폭을 넘으면 줄바꿈 대신 말줄임(…)으로 자른다(TMP textWrappingMode=NoWrap · overflowMode=Ellipsis).")]
|
||||||
|
public bool lootToastEllipsis = true;
|
||||||
|
|
||||||
// ── ④ 레벨업 스탯 팝업 (기준서 §B 요소 5) ─────────────────────────────
|
// ── ④ 레벨업 스탯 팝업 (기준서 §B 요소 5) ─────────────────────────────
|
||||||
[Header("④ 레벨업 스탯 팝업 (CombatEvents.LevelUp 구독 · 스탯 3줄)")]
|
[Header("④ 레벨업 스탯 팝업 (CombatEvents.LevelUp 구독 · 스탯 3줄)")]
|
||||||
[Tooltip("스탯 팝업 표시 여부.")]
|
[Tooltip("스탯 팝업 표시 여부.")]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue