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

699 lines
36 KiB
C#
Raw Normal View History

// ─────────────────────────────────────────────────────────────────────────────
// RunResultUI.cs — 런 결과 화면(승리 / 시간 종료 · 집계 · 등급별 획득 · "다음 런")
// (WL-813q · #813)
//
// 기준서 §D-2 813q 행 · 발주서 WL-813q §1-2.
// 구독 계약 = 813p 완료보고 §4 표 = `Assets/WL/Combat/Run/RunEvents.cs`(**읽기만** · 수정 0).
//
// 🔴 **배열 3종은 즉시 복사한다** — 813p 계약 §4 마지막 줄:
// `RunResult.zoneClearSec` · `zoneKills` · `lootByGrade` 는 `RunDirector` 가 런 사이에 **재사용**한다.
// 참조를 보관하면 다음 런에서 덮어써지므로 `RunEnded` 를 받는 즉시 자기 버퍼로 복사하고
// 구조체의 배열 필드를 그 버퍼로 갈아 끼운다(프로브가 「원본을 덮어써도 화면 값 불변」으로 증명).
// 버퍼는 길이가 모자랄 때만 새로 잡는다 → 두 번째 런부터 할당 0.
//
// ■ 팝업 방식 = 813tj `ReviveDialog` 와 같은 구조(공용 팝업 시도 → 런타임 패널 폴백)
// 실측: 공용 `Popup`(`Assets/Script/Info/Popup.cs` · `SortOrder_5`)은 본문 라벨(`label_msg`) **1개**와
// 프리팹에 고정된 버튼 문구만 준다 → "다음 런"·"닫기" 문구를 **프리팹 수정 없이** 넣을 수 없고
// 9행 + 등급 색 줄이 작은 메시지 상자에서 잘린다. 그래서 기본 경로는 **런타임 패널**
// (= ReviveDialog 가 에디트 모드에서 실제로 타는 그 폴백 경로와 같은 조립 방식)이고,
// 공용 팝업 경로는 SO 스위치 `resultUseCommonPopup` 로 남겨 둔다(기본 off).
//
// ■ 순서 — 813i/813tj 부활 팝업이 떠 있거나 예약 중이면 **부활 팝업이 우선**이다.
// 결과 화면은 미뤘다가(`resultDeferWhileRevive`) 부활 팝업이 닫힌 뒤에 뜬다.
// `TimeScaleArbiter` 는 **건드리지 않는다**(발주서 §1-2).
//
// ■ 프리팹 diff 0 — 노드는 전부 런타임 생성(`HideFlags.DontSave`) · 1회 만들고 재사용.
// ■ C8 롤백 — `WLRunUiSettings.enabled_ = 0`(또는 `resultEnabled = 0`) → 구독 0 · 노드 0 · 표시 0.
// 🔴 어셈블리 주의: Assets/WL/UI/ 에 .asmdef 를 만들지 말 것(Assembly-CSharp 단일).
// ─────────────────────────────────────────────────────────────────────────────
using System.Text;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using WL.Combat.Run;
namespace WL.UI
{
/// <summary>런 결과 화면(정적 · 노드는 런타임 생성 · 1회 만들고 재사용).</summary>
public static class RunResultUI
{
// ── 노드 ──────────────────────────────────────────────────────────────
private static Transform s_uiRoot;
private static RectTransform s_root;
private static CanvasGroup s_group;
private static Image s_dim;
private static RectTransform s_box;
private static Image s_boxBg;
private static TextMeshProUGUI s_title;
private static RectTransform s_rowRoot;
private static TextMeshProUGUI[] s_rowLabel = new TextMeshProUGUI[0];
private static TextMeshProUGUI[] s_rowValue = new TextMeshProUGUI[0];
private static RectTransform[] s_row = new RectTransform[0];
private static Button s_nextButton, s_closeButton;
private static TextMeshProUGUI s_nextLabel, s_closeLabel;
private static Image s_nextBg, s_closeBg;
private static TMP_FontAsset s_font;
// ── 복사 버퍼 (🔴 813p 배열 재사용 계약) ──────────────────────────────
private static RunResult s_result;
private static float[] s_zoneClearSec = new float[0];
private static int[] s_zoneKills = new int[0];
private static int[] s_lootByGrade = new int[0];
private static int s_zoneLen, s_lootLen;
// ── 상태 ──────────────────────────────────────────────────────────────
private static bool s_subscribed;
private static bool s_open;
private static bool s_pending; // 부활 팝업 때문에 미뤄 둔 결과가 있는가
private static readonly StringBuilder s_sb = new StringBuilder(128);
private static readonly char[] s_timeBuf = new char[8];
// ── 진단(프로브가 읽는다 · 실측만) ────────────────────────────────────
public static int EndedSeen, ShownCount, DeferredCount, NextRunCount, CloseCount, SkipAbandonCount;
public static int BufferGrowCount; // 복사 버퍼를 새로 잡은 횟수(2번째 런부터 0 = GC 0)
public static bool UsedCommonPopup;
public static string LastShowLog = "";
public static bool Subscribed { get { return s_subscribed; } }
public static bool Bound { get { return s_root != null; } }
public static bool IsOpen { get { return s_open; } }
public static bool Pending { get { return s_pending; } }
public static int VisibleRows { get; private set; }
/// <summary>복사해 둔 결과(배열 3종은 **자기 버퍼**를 가리킨다).</summary>
public static RunResult Copied { get { return s_result; } }
public static float CopiedZoneClearSec(int i) { return i >= 0 && i < s_zoneLen ? s_zoneClearSec[i] : -1f; }
public static int CopiedZoneKills(int i) { return i >= 0 && i < s_zoneLen ? s_zoneKills[i] : -1; }
public static int CopiedLootByGrade(int g) { return g >= 0 && g < s_lootLen ? s_lootByGrade[g] : -1; }
public static string TitleText { get { return s_title != null ? s_title.text : ""; } }
private static WLRunUiSettings St { get { return WLRunUiSettings.Instance; } }
// ── 부팅 ──────────────────────────────────────────────────────────────
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
private static void Boot()
{
var s = St;
if (!WLRunUiSettings.Enabled || s == null || !s.resultEnabled) return;
Subscribe(true);
WLRunUiRunner.Ensure();
}
public static string Subscribe(bool on)
{
if (on == s_subscribed) return "구독 변화 없음(" + s_subscribed + ")";
if (on) { RunEvents.RunEnded.Add(OnRunEnded); RunEvents.RunStarted.Add(OnRunStarted); }
else { RunEvents.RunEnded.Remove(OnRunEnded); RunEvents.RunStarted.Remove(OnRunStarted); }
s_subscribed = on;
return "구독=" + on + " (RunEnded/RunStarted)";
}
// ── 이벤트 ────────────────────────────────────────────────────────────
private static void OnRunStarted(in RunStartedEvent e)
{
s_pending = false;
if (s_open) Hide(); // 다음 런이 시작되면 결과 화면은 닫힌다
}
private static void OnRunEnded(in RunResult e)
{
EndedSeen++;
CopyResult(in e); // 🔴 배열 3종을 즉시 자기 버퍼로 복사
var s = St;
if (s == null || !WLRunUiSettings.Enabled || !s.resultEnabled) return;
if (e.outcome == RunOutcome.Abandon && !s.resultShowOnAbandon)
{
SkipAbandonCount++;
s_pending = false;
if (s.verboseLog) Debug.Log("[RunResultUI] 이탈(Abandon) — 팝업 없음(HUD 리셋만)");
return; // 발주서 §1-2: 이탈은 팝업 없음
}
s_pending = true;
TryShow(Time.unscaledTime);
}
/// <summary>🔴 재사용 배열 3종을 자기 버퍼로 복사하고 구조체의 배열 필드를 갈아 끼운다.</summary>
public static void CopyResult(in RunResult r)
{
s_result = r; // 스칼라 전부(배열 필드는 아직 원본 참조)
int zn = r.zoneCount;
if (r.zoneClearSec != null && r.zoneClearSec.Length < zn) zn = r.zoneClearSec.Length;
if (r.zoneKills != null && r.zoneKills.Length < zn) zn = r.zoneKills.Length;
if (zn < 0) zn = 0;
s_zoneClearSec = EnsureFloat(s_zoneClearSec, zn);
s_zoneKills = EnsureInt(s_zoneKills, zn);
for (int i = 0; i < zn; i++)
{
s_zoneClearSec[i] = r.zoneClearSec != null ? r.zoneClearSec[i] : -1f;
s_zoneKills[i] = r.zoneKills != null ? r.zoneKills[i] : 0;
}
s_zoneLen = zn;
int ln = r.lootByGrade != null ? r.lootByGrade.Length : 0;
s_lootByGrade = EnsureInt(s_lootByGrade, ln);
for (int i = 0; i < ln; i++) s_lootByGrade[i] = r.lootByGrade[i];
s_lootLen = ln;
// 원본 참조를 버리고 자기 버퍼를 가리키게 한다(다음 런에 덮어써져도 화면 값은 그대로).
s_result.zoneClearSec = s_zoneClearSec;
s_result.zoneKills = s_zoneKills;
s_result.lootByGrade = s_lootByGrade;
}
private static float[] EnsureFloat(float[] buf, int n)
{
if (buf != null && buf.Length >= n) return buf;
BufferGrowCount++;
return new float[Mathf.Max(1, n)];
}
private static int[] EnsureInt(int[] buf, int n)
{
if (buf != null && buf.Length >= n) return buf;
BufferGrowCount++;
return new int[Mathf.Max(1, n)];
}
// ── 표시 ──────────────────────────────────────────────────────────────
/// <summary>부활 팝업이 비면 띄운다. 아직 떠 있으면 미뤄 둔다(러너가 다시 부른다).</summary>
public static string TryShow(float now)
{
var s = St;
if (s == null) return "WLRunUiSettings 에셋 없음";
if (!s_pending) return "대기 중인 결과 없음";
if (s.resultDeferWhileRevive && ReviveDialog.Busy)
{
DeferredCount++;
LastShowLog = "부활 팝업 우선 — 결과 화면 대기";
return LastShowLog;
}
s_pending = false;
return Show();
}
/// <summary>지금 바로 결과 화면을 띄운다(공용 팝업 스위치 → 런타임 패널).</summary>
public static string Show()
{
var s = St;
if (s == null) return "WLRunUiSettings 에셋 없음";
ShownCount++;
UsedCommonPopup = false;
BuildBody(s); // 본문 문자열/행을 채운다(공용 팝업도 이 결과를 쓴다)
if (s.resultUseCommonPopup)
{
string r = TryCommonPopup(s);
if (UsedCommonPopup) { s_open = true; LastShowLog = r; return r; }
}
if (s_root == null) { LastShowLog = "노드 미조립 — Bind 먼저"; return LastShowLog; }
ApplyLayout();
s_open = true;
if (s_group != null) { s_group.alpha = 1f; s_group.blocksRaycasts = true; s_group.interactable = true; }
if (s_root.gameObject.activeSelf == false) s_root.gameObject.SetActive(true);
s_root.SetAsLastSibling(); // 형제 순서 = 그리는 순서(813tj 교훈) — HUD 위로
LastShowLog = "런타임 패널 표시 outcome=" + s_result.outcome + " 행=" + VisibleRows;
if (s.verboseLog) Debug.Log("[RunResultUI] " + LastShowLog);
return LastShowLog;
}
/// <summary>공용 Popup 싱글턴(SortOrder_5) 재사용 — ReviveDialog 와 같은 시도/폴백 구조.</summary>
private static string TryCommonPopup(WLRunUiSettings s)
{
try
{
if (Popup.Ins == null) return "공용 Popup 없음 — 런타임 패널로";
Popup.Ins.Set(ePopupType.Two, 0, OnNextRunListener, OnCloseListener);
if (Popup.Ins.label_msg != null) Popup.Ins.label_msg.text = BodyText;
UsedCommonPopup = true;
return "공용 Popup(SortOrder_5) 표시 — 버튼 문구는 프리팹 소유(미확인)";
}
catch (System.Exception e)
{
UsedCommonPopup = false;
return "공용 Popup 실패(" + e.GetType().Name + ") — 런타임 패널로";
}
}
public static void Hide()
{
s_open = false;
if (s_group != null) { s_group.alpha = 0f; s_group.blocksRaycasts = false; s_group.interactable = false; }
}
/// <summary>[다음 런] — 813p 공개 API 하나만 부른다.</summary>
public static string OnNextRun()
{
NextRunCount++;
Hide();
try { RunDirector.Restart(); }
catch (System.Exception e) { return "다음 런 — RunDirector.Restart() 예외(" + e.GetType().Name + ")"; }
return "다음 런 — RunDirector.Restart() 호출";
}
/// <summary>[닫기].</summary>
public static string OnClose() { CloseCount++; Hide(); return "닫기"; }
private static void OnNextRunListener() { OnNextRun(); }
private static void OnCloseListener() { OnClose(); }
// ── 러너(매 프레임) ───────────────────────────────────────────────────
public static string Tick(float now)
{
var s = St;
if (s == null) return "WLRunUiSettings 에셋 없음";
if (!WLRunUiSettings.Enabled || !s.resultEnabled) { if (s_open) Hide(); return "off — 무동작"; }
if (s_pending) return TryShow(now);
return "tick ok";
}
// ── 본문 ──────────────────────────────────────────────────────────────
/// <summary>마지막으로 만든 본문 전체 문자열(공용 팝업 · 실측 덤프용).</summary>
public static string BodyText { get; private set; } = "";
/// <summary>행 종류별 마지막 값 문자열(실측 덤프용).</summary>
public static string RowValueText(WLRunResultRow row)
{
var s = St;
if (s == null || s.resultRows == null) return "";
int slot = 0;
for (int i = 0; i < s.resultRows.Length; i++)
{
if (!RowApplies(s, s.resultRows[i])) continue;
if (s.resultRows[i] == row) return slot < s_rowValue.Length && s_rowValue[slot] != null ? s_rowValue[slot].text : "";
slot++;
}
return "";
}
/// <summary>그 행을 이번 결과에 실제로 그리는가(레벨 0 숨김 등).</summary>
private static bool RowApplies(WLRunUiSettings s, WLRunResultRow row)
{
if (row == WLRunResultRow.Level && s.resultHideLevelWhenZero &&
s_result.levelFrom == 0 && s_result.levelTo == 0) return false;
return true;
}
/// <summary>행 라벨·값을 채운다(런 종료 1회 · 틱 경로가 아니다).</summary>
public static string BuildBody(WLRunUiSettings s)
{
if (s == null) return "설정 없음";
EnsureRows(s);
var body = new StringBuilder(256);
body.Append(s.TitleFor(s_result.outcome)).Append('\n');
int slot = 0;
var rows = s.resultRows != null ? s.resultRows : new WLRunResultRow[0];
for (int i = 0; i < rows.Length; i++)
{
var kind = rows[i];
if (!RowApplies(s, kind)) continue;
string label = LabelOf(s, kind);
s_sb.Length = 0;
ValueOf(s, kind, s_sb);
string value = s_sb.ToString();
// 노드가 아직 없어도(공용 팝업 경로 · Bind 전 프로브) 본문 문자열은 끝까지 만든다.
if (slot < s_row.Length)
{
if (s_rowLabel[slot] != null) s_rowLabel[slot].SetText(label);
if (s_rowValue[slot] != null) s_rowValue[slot].SetText(value);
if (s_row[slot] != null && !s_row[slot].gameObject.activeSelf) s_row[slot].gameObject.SetActive(true);
}
body.Append(label).Append(" : ").Append(value).Append('\n');
slot++;
}
for (int i = slot; i < s_row.Length; i++)
if (s_row[i] != null && s_row[i].gameObject.activeSelf) s_row[i].gameObject.SetActive(false);
VisibleRows = slot;
if (s_title != null)
{
s_title.SetText(s.TitleFor(s_result.outcome));
s_title.color = s.TitleColorFor(s_result.outcome);
}
BodyText = body.ToString();
return "행 " + slot + "개";
}
private static string LabelOf(WLRunUiSettings s, WLRunResultRow row)
{
switch (row)
{
case WLRunResultRow.TotalTime: return s.rowTotalTimeLabel;
case WLRunResultRow.BossClear: return s.rowBossClearLabel;
case WLRunResultRow.Kills: return s.rowKillsLabel;
case WLRunResultRow.MaxChain: return s.rowMaxChainLabel;
case WLRunResultRow.MaxDamage: return s.rowMaxDamageLabel;
case WLRunResultRow.Level: return s.rowLevelLabel;
case WLRunResultRow.LootByGrade: return s.rowLootLabel;
case WLRunResultRow.Gold: return s.rowGoldLabel;
default: return s.rowDeathsLabel;
}
}
private static void ValueOf(WLRunUiSettings s, WLRunResultRow row, StringBuilder sb)
{
switch (row)
{
case WLRunResultRow.TotalTime:
AppendTime(sb, s_result.totalSec, s);
break;
case WLRunResultRow.BossClear:
AppendTime(sb, s_result.bossClearSec, s);
break;
case WLRunResultRow.Kills:
AppendFormat2(sb, s.rowKillsFormat, s_result.kills, s_result.bossKills);
break;
case WLRunResultRow.MaxChain:
sb.Append(s_result.maxChain);
break;
case WLRunResultRow.MaxDamage:
// 🔴 Mathf.RoundToInt 는 짝수 반올림(8888.5 → 8888)이라 피해 표기에 맞지 않는다 — 0.5 는 올린다.
sb.Append((long)System.Math.Round(
s.resultMaxDamageUseBurst ? s_result.maxDamageBurst : s_result.maxDamage,
System.MidpointRounding.AwayFromZero));
break;
case WLRunResultRow.Level:
AppendFormat2(sb, s.rowLevelFormat, s_result.levelFrom, s_result.levelTo);
break;
case WLRunResultRow.LootByGrade:
AppendLootByGrade(sb, s);
break;
case WLRunResultRow.Gold:
sb.Append(s_result.gold);
break;
default:
AppendFormat2(sb, s.rowDeathsFormat, s_result.deaths, s_result.potionsUsed);
break;
}
}
/// <summary>초 → mm:ss (음수 = 미발생 → 대시 문구).</summary>
private static void AppendTime(StringBuilder sb, float sec, WLRunUiSettings s)
{
if (sec < 0f) { sb.Append(s.resultDashText); return; }
int n = RunHud.FormatMMSS(s_timeBuf, sec);
sb.Append(s_timeBuf, 0, n);
}
/// <summary>"{0} / {1}" 같은 두 자리 치환(정수 전용 · string.Format 없이).</summary>
private static void AppendFormat2(StringBuilder sb, string format, long a0, long a1)
{
if (string.IsNullOrEmpty(format)) { sb.Append(a0).Append(' ').Append(a1); return; }
for (int i = 0; i < format.Length; i++)
{
if (format[i] == '{' && i + 2 < format.Length && format[i + 2] == '}')
{
char c = format[i + 1];
if (c == '0') { sb.Append(a0); i += 2; continue; }
if (c == '1') { sb.Append(a1); i += 2; continue; }
}
sb.Append(format[i]);
}
}
/// <summary>등급별 획득 — 0 인 등급은 생략 · 색은 813f/813y 등급 팔레트(밝기 하한 적용).</summary>
private static void AppendLootByGrade(StringBuilder sb, WLRunUiSettings s)
{
var text = WLCombatTextSettings.Instance;
int shown = 0;
for (int g = 1; g < s_lootLen; g++) // 인덱스 = 등급(1~9) · [0] 미사용(813p 계약)
{
int n = s_lootByGrade[g];
if (n <= 0) continue;
if (shown > 0) sb.Append(s.rowLootGradeSeparator);
if (text != null) sb.Append(text.GradeColorTagBright(g));
AppendFormat2(sb, s.rowLootGradeFormat, n, g);
if (text != null) sb.Append("</color>");
shown++;
}
if (shown == 0) sb.Append(s.rowLootEmptyText);
}
// ── 조립 (런타임 생성 · 프리팹 diff 0 · 1회) ──────────────────────────
public static string Bind(Transform uiRoot)
{
var s = St;
if (s == null) return "WLRunUiSettings 에셋 없음";
if (!WLRunUiSettings.Enabled || !s.resultEnabled) return "off — 조립 생략";
if (uiRoot == null) return "uiRoot=null";
if (s_root != null && s_uiRoot == uiRoot) return "이미 연결됨 — " + s_root.name;
s_uiRoot = uiRoot;
var prt = WLVignetteUtil.FindUiPath(uiRoot, s.resultParentPath) as RectTransform;
if (prt == null) return "부모 없음 — 경로 \"" + s.resultParentPath + "\"";
if (s_font == null) s_font = RunHud.BorrowFont(uiRoot);
if (s_root == null || s_root.parent != prt)
{
var go = new GameObject("WL_RunResult", typeof(RectTransform));
go.layer = WLTextFxUtil.UILayer;
go.hideFlags = HideFlags.DontSave;
s_root = (RectTransform)go.transform;
s_root.SetParent(prt, false);
s_group = go.AddComponent<CanvasGroup>();
var dimRt = WLVignetteUtil.NewChild(s_root, "Dim");
WLVignetteUtil.Stretch(dimRt);
s_dim = dimRt.GetComponent<Image>();
if (s_dim == null) s_dim = dimRt.gameObject.AddComponent<Image>();
s_dim.raycastTarget = true;
s_box = WLVignetteUtil.NewChild(s_root, "Box");
s_boxBg = s_box.GetComponent<Image>();
if (s_boxBg == null) s_boxBg = s_box.gameObject.AddComponent<Image>();
s_boxBg.raycastTarget = true;
s_title = WLTextFxUtil.NewText(s_box, "Title", s_font, TextAlignmentOptions.Center);
s_rowRoot = WLTextFxUtil.NewChild(s_box, "Rows");
s_nextButton = NewButton(s_box, "btn_next", out s_nextBg, out s_nextLabel, OnNextRunListener);
s_closeButton = NewButton(s_box, "btn_close", out s_closeBg, out s_closeLabel, OnCloseListener);
}
EnsureRows(s);
string layout = ApplyLayout();
Hide();
return "연결 " + s.resultParentPath + " · " + layout;
}
private static Button NewButton(RectTransform parent, string name, out Image bg, out TextMeshProUGUI label,
UnityEngine.Events.UnityAction onClick)
{
var t = WLVignetteUtil.NewChild(parent, name);
bg = t.GetComponent<Image>();
if (bg == null) bg = t.gameObject.AddComponent<Image>();
bg.raycastTarget = true;
var b = t.GetComponent<Button>();
if (b == null) b = t.gameObject.AddComponent<Button>();
b.targetGraphic = bg;
b.onClick.RemoveListener(onClick);
b.onClick.AddListener(onClick);
label = WLTextFxUtil.NewText(t, "Label", s_font, TextAlignmentOptions.Center);
WLTextFxUtil.Stretch(label.rectTransform);
return b;
}
/// <summary>행 노드를 설정의 행 수만큼 만든다(줄이지 않는다 · 재사용).</summary>
public static bool EnsureRows(WLRunUiSettings s)
{
if (s_rowRoot == null) return false;
int want = s != null && s.resultRows != null ? s.resultRows.Length : 0;
if (s_row.Length >= want) return false;
int old = s_row.Length;
System.Array.Resize(ref s_row, want);
System.Array.Resize(ref s_rowLabel, want);
System.Array.Resize(ref s_rowValue, want);
for (int i = old; i < want; i++)
{
var rt = WLTextFxUtil.NewChild(s_rowRoot, "Row" + i);
s_row[i] = rt;
s_rowLabel[i] = WLTextFxUtil.NewText(rt, "Label", s_font, TextAlignmentOptions.MidlineLeft);
s_rowValue[i] = WLTextFxUtil.NewText(rt, "Value", s_font, TextAlignmentOptions.MidlineRight);
}
return true;
}
public static string ApplyLayout()
{
var s = St;
if (s == null) return "WLRunUiSettings 에셋 없음 — 배치 건너뜀";
if (s_root == null || s_box == null) return "노드 없음 — Bind 먼저";
var canvas = s_root.GetComponentInParent<Canvas>(true);
float u = s.UnitsPerPx(canvas != null ? canvas.rootCanvas : null);
WLVignetteUtil.Stretch(s_root);
s_root.localScale = Vector3.one;
if (s_dim != null) s_dim.color = s.resultDimColor;
s_box.anchorMin = s_box.anchorMax = new Vector2(0.5f, 0.5f);
s_box.pivot = new Vector2(0.5f, 0.5f);
s_box.anchoredPosition = Vector2.zero;
s_box.sizeDelta = new Vector2(s.resultBoxSizePx.x * u, s.resultBoxSizePx.y * u);
s_box.localScale = Vector3.one;
if (s_boxBg != null) s_boxBg.color = s.resultBgColor;
if (s_title != null)
{
var rt = s_title.rectTransform;
rt.anchorMin = new Vector2(0f, 1f); rt.anchorMax = new Vector2(1f, 1f);
rt.pivot = new Vector2(0.5f, 1f);
rt.offsetMin = new Vector2(s.resultPadXPx * u, 0f); rt.offsetMax = new Vector2(-s.resultPadXPx * u, 0f);
rt.anchoredPosition = new Vector2(0f, -s.resultPadTopPx * u);
rt.sizeDelta = new Vector2(0f, s.resultTitleFontPx * u);
s_title.fontSize = s.resultTitleFontPx * u;
s_title.alignment = TextAlignmentOptions.Center;
WLTextFxUtil.ApplyEdge(s_title, s.resultEdge);
}
float rowTop = (s.resultPadTopPx + s.resultTitleFontPx + s.resultTitleGapPx) * u;
if (s_rowRoot != null)
{
s_rowRoot.anchorMin = new Vector2(0f, 1f); s_rowRoot.anchorMax = new Vector2(1f, 1f);
s_rowRoot.pivot = new Vector2(0.5f, 1f);
s_rowRoot.offsetMin = new Vector2(s.resultPadXPx * u, 0f); s_rowRoot.offsetMax = new Vector2(-s.resultPadXPx * u, 0f);
s_rowRoot.anchoredPosition = new Vector2(0f, -rowTop);
s_rowRoot.sizeDelta = new Vector2(0f, s.resultRowHeightPx * Mathf.Max(1, s_row.Length) * u);
for (int i = 0; i < s_row.Length; i++)
{
var rt = s_row[i];
if (rt == null) continue;
rt.anchorMin = new Vector2(0f, 1f); rt.anchorMax = new Vector2(1f, 1f);
rt.pivot = new Vector2(0.5f, 1f);
rt.offsetMin = new Vector2(0f, 0f); rt.offsetMax = new Vector2(0f, 0f);
rt.anchoredPosition = new Vector2(0f, -s.resultRowHeightPx * i * u);
rt.sizeDelta = new Vector2(0f, s.resultRowHeightPx * u);
rt.localScale = Vector3.one;
ApplyRowLabel(s_rowLabel[i], s, u, true);
ApplyRowLabel(s_rowValue[i], s, u, false);
}
}
LayoutButton(s_nextButton, s_nextBg, s_nextLabel, s, u, -1, s.resultNextRunText, s.resultNextBgColor, s.resultNextLabelColor);
LayoutButton(s_closeButton, s_closeBg, s_closeLabel, s, u, 1, s.resultCloseText, s.resultCloseBgColor, s.resultCloseLabelColor);
return "box size" + s_box.sizeDelta + " 행틀=" + s_row.Length + " unitsPerPx=" + u.ToString("F4");
}
private static void ApplyRowLabel(TextMeshProUGUI t, WLRunUiSettings s, float u, bool left)
{
if (t == null) return;
var rt = t.rectTransform;
rt.anchorMin = left ? new Vector2(0f, 0f) : new Vector2(0.35f, 0f);
rt.anchorMax = left ? new Vector2(0.35f, 1f) : new Vector2(1f, 1f);
rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero;
rt.pivot = new Vector2(0.5f, 0.5f);
t.fontSize = s.resultRowFontPx * u;
t.alignment = left ? TextAlignmentOptions.MidlineLeft : TextAlignmentOptions.MidlineRight;
t.color = left ? s.resultRowLabelColor : s.resultRowValueColor;
WLTextFxUtil.ApplyEdge(t, s.resultEdge);
}
private static void LayoutButton(Button b, Image bg, TextMeshProUGUI label, WLRunUiSettings s, float u,
int side, string text, Color bgColor, Color labelColor)
{
if (b == null) return;
var rt = b.GetComponent<RectTransform>();
rt.anchorMin = rt.anchorMax = new Vector2(0.5f, 0f);
rt.pivot = new Vector2(0.5f, 0f);
float half = (s.resultButtonSizePx.x + s.resultButtonGapPx) * 0.5f;
rt.anchoredPosition = new Vector2(side * half * u, s.resultButtonBottomPx * u);
rt.sizeDelta = new Vector2(s.resultButtonSizePx.x * u, s.resultButtonSizePx.y * u);
rt.localScale = Vector3.one;
if (bg != null) bg.color = bgColor;
if (label != null)
{
label.SetText(text);
label.fontSize = s.resultButtonFontPx * u;
label.color = labelColor;
label.alignment = TextAlignmentOptions.Center;
if (s_font != null && label.font != s_font) label.font = s_font;
}
}
/// <summary>폰트를 직접 지정한다(프로브·QA).</summary>
public static void SetFont(TMP_FontAsset f)
{
s_font = f;
if (f == null) return;
if (s_title != null) s_title.font = f;
if (s_nextLabel != null) s_nextLabel.font = f;
if (s_closeLabel != null) s_closeLabel.font = f;
for (int i = 0; i < s_row.Length; i++)
{
if (s_rowLabel[i] != null) s_rowLabel[i].font = f;
if (s_rowValue[i] != null) s_rowValue[i].font = f;
}
}
// ── 프로브 · 진단 ─────────────────────────────────────────────────────
public static string ResetState()
{
EndedSeen = ShownCount = DeferredCount = NextRunCount = CloseCount = SkipAbandonCount = 0;
BufferGrowCount = 0;
s_pending = false; UsedCommonPopup = false; LastShowLog = "";
Hide();
return "상태 초기화";
}
public static string Teardown()
{
Subscribe(false);
if (s_root != null)
{
if (Application.isPlaying) Object.Destroy(s_root.gameObject);
else Object.DestroyImmediate(s_root.gameObject);
}
s_root = null; s_group = null; s_dim = null; s_box = null; s_boxBg = null;
s_title = null; s_rowRoot = null; s_nextButton = null; s_closeButton = null;
s_nextLabel = null; s_closeLabel = null; s_nextBg = null; s_closeBg = null;
s_row = new RectTransform[0]; s_rowLabel = new TextMeshProUGUI[0]; s_rowValue = new TextMeshProUGUI[0];
s_uiRoot = null;
ResetState();
return "teardown 완료";
}
public static string Dump()
{
var s = St;
var sb = new StringBuilder();
sb.AppendLine("[RunResultUI] 구독=" + s_subscribed + " bound=" + Bound + " open=" + s_open +
" pending=" + s_pending + " · ended=" + EndedSeen + " shows=" + ShownCount +
" defer=" + DeferredCount + " next=" + NextRunCount + " close=" + CloseCount +
" abandon생략=" + SkipAbandonCount + " 버퍼증설=" + BufferGrowCount +
" 공용팝업=" + UsedCommonPopup);
sb.AppendLine(" 제목 \"" + TitleText + "\" · 행 " + VisibleRows + "개 · 부활팝업Busy=" + ReviveDialog.Busy);
if (s != null && s.resultRows != null)
for (int i = 0; i < s.resultRows.Length; i++)
{
var kind = s.resultRows[i];
if (!RowApplies(s, kind)) { sb.AppendLine(" - " + kind + " : (숨김)"); continue; }
sb.AppendLine(" - " + kind + " : \"" + RowValueText(kind) + "\"");
}
sb.AppendLine(" 복사본 outcome=" + s_result.outcome + " runIndex=" + s_result.runIndex +
" total=" + s_result.totalSec.ToString("F1") + "s bossClear=" + s_result.bossClearSec.ToString("F1") +
"s kills=" + s_result.kills + "(보스 " + s_result.bossKills + ") chain=" + s_result.maxChain +
" dmg=" + s_result.maxDamage.ToString("F1") + "/" + s_result.maxDamageBurst.ToString("F1") +
" lv=" + s_result.levelFrom + "→" + s_result.levelTo + " lootTotal=" + s_result.lootTotal +
" gold=" + s_result.gold + " deaths=" + s_result.deaths + " potions=" + s_result.potionsUsed);
sb.Append(" 복사 배열 zone(" + s_zoneLen + "):");
for (int i = 0; i < s_zoneLen; i++) sb.Append(" [").Append(i).Append("]=").Append(s_zoneClearSec[i].ToString("F1")).Append('/').Append(s_zoneKills[i]);
sb.Append(" · loot(" + s_lootLen + "):");
for (int i = 0; i < s_lootLen; i++) sb.Append(' ').Append(s_lootByGrade[i]);
sb.AppendLine();
sb.AppendLine(" 본문 = " + BodyText.Replace("\n", " | "));
if (s != null)
sb.AppendLine(" 설정 enabled=" + s.enabled_ + " result=" + s.resultEnabled + " 공용팝업=" + s.resultUseCommonPopup +
" abandon표시=" + s.resultShowOnAbandon + " 부활우선=" + s.resultDeferWhileRevive +
" box=" + s.resultBoxSizePx + " 행수=" + (s.resultRows != null ? s.resultRows.Length : 0) +
" 합산피해=" + s.resultMaxDamageUseBurst);
return sb.ToString();
}
}
}