1345 lines
76 KiB
C#
1345 lines
76 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 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).
|
||
//
|
||
// ■ WL-813q2(D-18) — 위 순서에 구멍이 있었다.
|
||
// Q5 캡처 `PD2_07_result_revive_overlap.png` 실측: 제목이 **"시간 종료"** = 결과 화면이 **먼저** 떴고,
|
||
// 그 뒤에 죽어서 부활 팝업(813z 전용 Canvas `sortingOrder=60`)이 **위에** 겹쳤다 → 9행 중 4행이 가려진다.
|
||
// `RunEnded` 시점에는 아직 아무도 죽지 않았으니 `ReviveDialog.Busy` 가 false 라 미룸이 걸릴 수 없다.
|
||
// 3층으로 막는다(전부 SO · 각각 끄면 813q 동작):
|
||
// ① `resultDeferWhileDying` — 미룸 판정을 813i `DeathFlow.State`(Dying/AwaitingRevive)로 바꾼다
|
||
// (`ReviveDialog.Busy` 는 보조 · 813z 워치독 부활 경로처럼 팝업이 안 잡히는 경우까지 덮는다).
|
||
// ② `skipRevivePopupAfterRunEnd` — 런이 끝난 뒤(`RunDirector.IsRunning=false`) 사망은 런 밖이므로
|
||
// `DeathFlow.Revive()`(813i 공개 API)를 바로 불러 **팝업 자체를 생략**한다. = 겹칠 일이 없다.
|
||
// ③ `resultLowerWhileRevive` — 그래도 겹치면 결과 화면을 **부활 뒤로 재정렬**
|
||
// (전용 Canvas `sortingOrder` 40 → 20 · `SetAsFirstSibling`), 부활이 끝나면 되돌린다.
|
||
// 🔴 Gameplay 는 **읽기·구독·공개 API 호출만** 한다(파일 수정 0).
|
||
//
|
||
// ■ WL-813q3(D-44) — 「보스 처치 : 미처치 (HP 남음 N %)」 의 원천을 바꿨다.
|
||
// 813w6 이 `RunEnded` 페이로드에 **`bossHpRatio`/`bossHp`/`bossHpMax`** 를 실었다(`RunEvents.cs` 실측 ·
|
||
// 순서 = HP 스냅샷 → `RunEnded` 발행 → `BossArena.ParkBoss`(만피 복구)). 813q2 는 이 필드가 없어
|
||
// **표시 시점**에 `BossHpBar.RunBossHpRatio` 를 읽었고, 부활 미룸(D-18)으로 결과가 늦게 그려지면
|
||
// 그 사이 Park 이 돌아 **만피 100 %** 가 찍혔다(813w6 보고 §6-1 「잔여」).
|
||
// 이제 `CopyResult` 가 런 종료 순간에 구조체째 복사한 **`s_result.bossHpRatio`** 를 쓴다
|
||
// → 늦게 그려도 스냅샷 값 그대로. 페이로드가 −1(보스 미등장·못 읽음)이면 813q2 폴백으로 간다.
|
||
// C8 = `resultBossHpFromPayload = 0` → 813q2 동작(만피 재현). 원천은 `LastBossHpSource` 로 읽는다.
|
||
// 같은 발주로 「카드 선택」 행(813o `ZonePickup.PicksThisRun` · 읽기만)을 추가했다.
|
||
//
|
||
// ■ 프리팹 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.Core;
|
||
using WL.Combat.Run;
|
||
using WL.Combat.Survival;
|
||
|
||
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;
|
||
private static Canvas s_canvas; // WL-813q2 — 전용 Canvas(부활 팝업 60 아래)
|
||
private static GraphicRaycaster s_raycaster;
|
||
|
||
// ── 복사 버퍼 (🔴 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 bool s_lowered; // WL-813q2 — 부활 뒤로 재정렬해 둔 상태인가
|
||
private static int s_eliteKills; // WL-813q2 — 런 중 엘리트 처치(UI 자체 집계)
|
||
private static int s_eliteKillsAtEnd; // RunEnded 순간의 스냅샷(결과 화면이 쓰는 값)
|
||
private static int s_cardPicksAtEnd; // WL-813q3 — RunEnded 순간의 813o 카드 선택 수 스냅샷
|
||
private static readonly StringBuilder s_sb = new StringBuilder(128);
|
||
private static readonly char[] s_timeBuf = new char[8];
|
||
|
||
// ── WL-815d 스테이지 모드 ─────────────────────────────────────────────
|
||
// 기준서 §F-1 — 같은 패널을 스테이지 클리어 화면으로 쓴다.
|
||
// · 제목 = "스테이지 N 클리어!" · 행 = `WLStageUiSettings.stageResultRows`
|
||
// · 버튼 = [다음 스테이지] → StageDirector.Next() / [로비로] → 「로비로」(StageUiBridge 가 대입)
|
||
// 🔴 값의 출처는 **`StageClearResult` 페이로드 + UI 자체 집계**다. 815b `mirrorRunEnded` 가
|
||
// 내보내는 RunResult 에는 레벨·골드·등급별 획득이 없다(실측) → 그 행은 기본 배열에서 뺐다.
|
||
private static bool s_stageMode;
|
||
private static WL.Combat.Stage.StageClearResult s_stage;
|
||
private static bool s_hasStage;
|
||
private static int s_stageHits, s_stagePotions, s_stageMaxChain;
|
||
private static TextMeshProUGUI s_badge; // 무피격 클리어 배지
|
||
private static bool s_suppressNextRunEnded; // 실패 경로 — 병행 RunEnded 로 결과창이 겹쳐 뜨는 것을 막는다
|
||
|
||
// ── WL-816m 던전 모드 (#816) ─────────────────────────────────────────
|
||
// 🔴 이 파일은 던전인지 **스스로 판정하지 않는다** — 유일한 이음매 `StageUiBridge` 가
|
||
// `WLDungeonSceneSetup.Current` 를 보고 이름·골드를 넘겨준다(UI ↔ 게임 로직 분리).
|
||
// s_dungeonName == null 이면 #815 스테이지 모드 그대로다(한 글자도 안 바뀐다).
|
||
private static string s_dungeonName;
|
||
private static int s_dungeonGold = -1; // -1 = 모름(보상 축이 꺼져 있거나 던전이 아님)
|
||
|
||
/// <summary>815d 가 대입한다 — [다음 스테이지] / [처음부터].</summary>
|
||
public static System.Action StagePrimary;
|
||
/// <summary>815d 가 대입한다 — [로비로].</summary>
|
||
public static System.Action StageSecondary;
|
||
|
||
public static int StagePrimaryCount, StageSecondaryCount, StageShownCount, SuppressedRunEndedCount;
|
||
|
||
// ── 진단(프로브가 읽는다 · 실측만) ────────────────────────────────────
|
||
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 = "";
|
||
// ── WL-813q2(D-18) 진단 ──────────────────────────────────────────────
|
||
public static int DiedSeen, ReviveRequestSeen, RevivedSeen;
|
||
public static int SkipPopupReviveCount; // 런 밖 사망 → 팝업 생략 즉시 부활을 부른 횟수
|
||
public static int LowerCount, RaiseCount; // 결과 화면을 부활 뒤로 내린/되돌린 횟수
|
||
public static int EliteKilledSeen; // 런 중 본 엘리트 처치(UI 자체 집계)
|
||
public static string LastDeathLog = "";
|
||
// ── WL-813q3(D-44) 진단 ──────────────────────────────────────────────
|
||
/// <summary>마지막 「보스 처치」 행이 남은 HP 를 어디서 읽었나 — "페이로드"(813w6) / "UI바"(813q2) / "없음".</summary>
|
||
public static string LastBossHpSource = "없음";
|
||
/// <summary>런 종료 순간의 보스 HP 스냅샷(813w6 페이로드 · −1 = 미발생) — 늦게 그려도 안 바뀐다.</summary>
|
||
public static float BossHpRatioAtEnd { get { return s_result.bossHpRatio; } }
|
||
public static double BossHpAtEnd { get { return s_result.bossHp; } }
|
||
public static double BossHpMaxAtEnd { get { return s_result.bossHpMax; } }
|
||
/// <summary>런 종료 순간의 813o 카드 선택 수 스냅샷.</summary>
|
||
public static int CardPicksAtEnd { get { return s_cardPicksAtEnd; } }
|
||
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 bool Lowered { get { return s_lowered; } }
|
||
public static int SortingOrder { get { return s_canvas != null && s_canvas.overrideSorting ? s_canvas.sortingOrder : -1; } }
|
||
public static int SiblingIndex { get { return s_root != null ? s_root.GetSiblingIndex() : -1; } }
|
||
public static int EliteKills { get { return s_eliteKills; } }
|
||
public static int EliteKillsAtEnd { get { return s_eliteKillsAtEnd; } }
|
||
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);
|
||
// WL-813q2 — 813i DeathFlow 는 **읽기·구독만**(Gameplay 파일 수정 0)
|
||
DeathFlow.Died.Add(OnDied);
|
||
DeathFlow.ReviveRequested.Add(OnReviveRequested);
|
||
DeathFlow.Revived.Add(OnRevived);
|
||
CombatEvents.Killed.Add(OnKilled); // 엘리트 집계(RunResult 에 없다)
|
||
}
|
||
else
|
||
{
|
||
RunEvents.RunEnded.Remove(OnRunEnded); RunEvents.RunStarted.Remove(OnRunStarted);
|
||
DeathFlow.Died.Remove(OnDied);
|
||
DeathFlow.ReviveRequested.Remove(OnReviveRequested);
|
||
DeathFlow.Revived.Remove(OnRevived);
|
||
CombatEvents.Killed.Remove(OnKilled);
|
||
}
|
||
s_subscribed = on;
|
||
return "구독=" + on + " (RunEnded/RunStarted + DeathFlow 3종 + Killed)";
|
||
}
|
||
|
||
// ── 이벤트 ────────────────────────────────────────────────────────────
|
||
private static void OnRunStarted(in RunStartedEvent e)
|
||
{
|
||
s_pending = false;
|
||
s_eliteKills = 0; // WL-813q2 — 런마다 엘리트 집계 리셋
|
||
if (s_open) Hide(); // 다음 런이 시작되면 결과 화면은 닫힌다
|
||
RaiseToFront(); // 다음 런에서 재정렬이 남지 않게 되돌린다
|
||
}
|
||
|
||
/// <summary>엘리트 처치 집계(🔴 813p RunResult 에 엘리트 필드가 없다 · 문자열 0 = GC 0).</summary>
|
||
private static void OnKilled(in KilledEvent e)
|
||
{
|
||
if (e.subRole != eSubRol.Elite) return;
|
||
EliteKilledSeen++;
|
||
if (!RunDirector.IsRunning) return; // 런 밖 처치는 이번 런 값이 아니다
|
||
s_eliteKills++;
|
||
}
|
||
|
||
// ── WL-813q2(D-18) 사망/부활 ──────────────────────────────────────────
|
||
/// <summary>사망 시작(813i `DeathFlow.Died`) — 런 밖이면 팝업을 생략하고 즉시 부활시킨다.</summary>
|
||
private static void OnDied(in DeathEvent e)
|
||
{
|
||
DiedSeen++;
|
||
HandleDeathStart("Died");
|
||
}
|
||
|
||
/// <summary>부활 팝업 요청(813i `DeathFlow.ReviveRequested`) — Died 를 놓친 경우의 2차 방어.</summary>
|
||
private static void OnReviveRequested(in ReviveRequestEvent e)
|
||
{
|
||
ReviveRequestSeen++;
|
||
HandleDeathStart("ReviveRequested");
|
||
}
|
||
|
||
/// <summary>런 밖 사망이면 즉시 부활 · 아니면 이미 떠 있는 결과 화면을 부활 뒤로 재정렬.</summary>
|
||
private static string HandleDeathStart(string from)
|
||
{
|
||
var s = St;
|
||
if (s == null || !WLRunUiSettings.Enabled || !s.resultEnabled) return "off";
|
||
|
||
// ② 런이 **끝난 뒤**(RunPhase.Ended = EndRun ~ 다음 StartRun 사이)의 사망은 런 밖이다 —
|
||
// 부활을 물을 이유가 없으니 팝업을 생략하고 바로 살린다.
|
||
// 🔴 Idle(런을 아직 시작도 안 한 로비 등)은 건드리지 않는다 = 813i 원래 흐름 그대로.
|
||
if (s.skipRevivePopupAfterRunEnd && RunDirector.Phase == RunPhase.Ended)
|
||
{
|
||
bool ok = DeathFlow.Revive(); // 813i 공개 API(Dying = 연출 끝에 · AwaitingRevive = 즉시)
|
||
if (ok) SkipPopupReviveCount++;
|
||
LastDeathLog = from + " — 런 밖 사망 → 팝업 생략 즉시 부활(" + ok + ")";
|
||
if (s.verboseLog) Debug.Log("[RunResultUI] " + LastDeathLog);
|
||
return LastDeathLog;
|
||
}
|
||
|
||
// ③ 그래도 겹치면(런 중 사망 + 결과 화면이 이미 떠 있음) 결과를 부활 뒤로 내린다.
|
||
if (s_open && s.resultLowerWhileRevive) LowerBehindRevive();
|
||
LastDeathLog = from + " — 런 중 사망(재정렬=" + s_lowered + ")";
|
||
return LastDeathLog;
|
||
}
|
||
|
||
/// <summary>부활 완료(813i `DeathFlow.Revived` · 워치독 자동 부활 포함) — 결과를 다시 앞으로.</summary>
|
||
private static void OnRevived(in RevivedEvent e)
|
||
{
|
||
RevivedSeen++;
|
||
RaiseToFront(); // 미뤄 둔 결과는 러너 Tick 이 곧 띄운다
|
||
}
|
||
|
||
private static void OnRunEnded(in RunResult e)
|
||
{
|
||
EndedSeen++;
|
||
s_eliteKillsAtEnd = s_eliteKills; // WL-813q2 — 결과 화면이 쓰는 값은 이 순간의 스냅샷
|
||
s_cardPicksAtEnd = ZonePickup.PicksThisRun; // WL-813q3 — 813o 카운터도 이 순간에 뜬다(읽기만)
|
||
// 🔴 WL-813q3 — 배열 3종 + **보스 HP 스냅샷(813w6 `e.bossHpRatio`)** 을 즉시 자기 버퍼로 복사.
|
||
// 이 뒤에 BossArena 가 미뤄 둔 Park(만피 복구)을 돌려도 결과창 값은 안 바뀐다.
|
||
CopyResult(in e);
|
||
var s = St;
|
||
if (s == null || !WLRunUiSettings.Enabled || !s.resultEnabled) return;
|
||
// WL-815d — 스테이지 실패(사망·시간 초과)는 813z 2버튼 팝업이 맡는다. 815b `mirrorRunEnded` 가
|
||
// 같은 프레임에 내보내는 RunEnded(Timeout/Abandon)로 결과창이 **겹쳐 뜨는 것**을 여기서 막는다.
|
||
if (s_suppressNextRunEnded)
|
||
{
|
||
s_suppressNextRunEnded = false;
|
||
SuppressedRunEndedCount++;
|
||
s_pending = false;
|
||
if (s.verboseLog) Debug.Log("[RunResultUI] 815d — 스테이지 실패 경로: 병행 RunEnded 억제");
|
||
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>
|
||
/// WL-813q2(D-18) — 「부활 우선」 판정. **주 판정 = 813i `DeathFlow.State`**(Dying/AwaitingRevive),
|
||
/// `ReviveDialog.Busy`(공용 팝업 경로 · 813q)는 보조. 둘 다 SO 로 끌 수 있다.
|
||
/// </summary>
|
||
public static bool ReviveBusy
|
||
{
|
||
get
|
||
{
|
||
var s = St;
|
||
if (s == null) return false;
|
||
if (s.resultDeferWhileDying && DeathFlow.State != DeathState.None) return true;
|
||
if (s.resultDeferWhileRevive && ReviveDialog.Busy) return true;
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>「부활 우선」이 걸린 이유(실측 덤프용 · 표시 경로가 아니다).</summary>
|
||
public static string ReviveBusyReason
|
||
{
|
||
get
|
||
{
|
||
var s = St;
|
||
if (s == null) return "설정 없음";
|
||
if (s.resultDeferWhileDying && DeathFlow.State != DeathState.None) return "DeathFlow." + DeathFlow.State;
|
||
if (s.resultDeferWhileRevive && ReviveDialog.Busy) return "ReviveDialog.Busy";
|
||
return "없음";
|
||
}
|
||
}
|
||
|
||
/// <summary>부활 팝업이 비면 띄운다. 아직 떠 있으면 미뤄 둔다(러너가 다시 부른다).</summary>
|
||
public static string TryShow(float now)
|
||
{
|
||
var s = St;
|
||
if (s == null) return "WLRunUiSettings 에셋 없음";
|
||
if (!s_pending) return "대기 중인 결과 없음";
|
||
if (ReviveBusy)
|
||
{
|
||
DeferredCount++;
|
||
LastShowLog = "부활 우선 — 결과 화면 대기(" + ReviveBusyReason + ")";
|
||
return LastShowLog;
|
||
}
|
||
s_pending = false;
|
||
return Show();
|
||
}
|
||
|
||
// ── WL-813q2(D-18) 재정렬 — 813z 부활 팝업(sortingOrder 60)보다 아래로 ───
|
||
/// <summary>결과 화면을 부활 팝업 뒤로 내린다(sortingOrder 낮춤 + 형제 맨 앞).</summary>
|
||
public static string LowerBehindRevive()
|
||
{
|
||
var s = St;
|
||
if (s == null || s_root == null) return "노드 없음";
|
||
if (s_lowered) return "이미 재정렬됨";
|
||
s_lowered = true;
|
||
LowerCount++;
|
||
EnsureCanvas(s);
|
||
if (s_canvas != null) s_canvas.sortingOrder = s.resultSortingOrderWhileRevive;
|
||
s_root.SetAsFirstSibling();
|
||
return "부활 뒤로 재정렬 sortingOrder=" + SortingOrder + " sibling=" + SiblingIndex;
|
||
}
|
||
|
||
/// <summary>재정렬을 되돌린다(부활 완료·다음 런).</summary>
|
||
public static string RaiseToFront()
|
||
{
|
||
var s = St;
|
||
if (s == null || s_root == null) { s_lowered = false; return "노드 없음"; }
|
||
if (!s_lowered) return "재정렬 없음";
|
||
s_lowered = false;
|
||
if (!s.resultRaiseAfterRevive) return "resultRaiseAfterRevive=0 — 되돌리지 않음";
|
||
RaiseCount++;
|
||
EnsureCanvas(s);
|
||
if (s_canvas != null) s_canvas.sortingOrder = s.resultSortingOrder;
|
||
s_root.SetAsLastSibling();
|
||
return "앞으로 복귀 sortingOrder=" + SortingOrder + " sibling=" + SiblingIndex;
|
||
}
|
||
|
||
/// <summary>전용 Canvas(overrideSorting) 보장 — 813z 부활 팝업과 같은 방식.</summary>
|
||
private static void EnsureCanvas(WLRunUiSettings s)
|
||
{
|
||
if (s == null || s_root == null || !s.resultOwnCanvas) return;
|
||
if (s_canvas == null)
|
||
{
|
||
s_canvas = s_root.GetComponent<Canvas>();
|
||
if (s_canvas == null) s_canvas = s_root.gameObject.AddComponent<Canvas>();
|
||
}
|
||
if (!s_canvas.overrideSorting) s_canvas.overrideSorting = true;
|
||
if (s_raycaster == null)
|
||
{
|
||
s_raycaster = s_root.GetComponent<GraphicRaycaster>();
|
||
if (s_raycaster == null) s_raycaster = s_root.gameObject.AddComponent<GraphicRaycaster>();
|
||
}
|
||
}
|
||
|
||
/// <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 위로
|
||
s_lowered = false;
|
||
EnsureCanvas(s); // WL-813q2 — 부활 팝업(60)보다 아래 고정
|
||
if (s_canvas != null) s_canvas.sortingOrder = s.resultSortingOrder;
|
||
LastShowLog = "런타임 패널 표시 outcome=" + s_result.outcome + " 행=" + VisibleRows +
|
||
" sortingOrder=" + SortingOrder;
|
||
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 하나만 부른다. 🔴 WL-815d: 스테이지 모드면 [다음 스테이지] 로 동작한다.</summary>
|
||
public static string OnNextRun()
|
||
{
|
||
NextRunCount++;
|
||
Hide();
|
||
if (s_stageMode)
|
||
{
|
||
StagePrimaryCount++;
|
||
if (StagePrimary == null) return "다음 스테이지 — 815d 핸들러 미연결(무동작)";
|
||
try { StagePrimary(); }
|
||
catch (System.Exception e) { return "다음 스테이지 — 예외(" + e.GetType().Name + ")"; }
|
||
return "다음 스테이지 — StageDirector 호출";
|
||
}
|
||
try { RunDirector.Restart(); }
|
||
catch (System.Exception e) { return "다음 런 — RunDirector.Restart() 예외(" + e.GetType().Name + ")"; }
|
||
return "다음 런 — RunDirector.Restart() 호출";
|
||
}
|
||
|
||
/// <summary>[닫기] — 🔴 WL-815d: 스테이지 모드면 [로비로] 로 동작한다.</summary>
|
||
public static string OnClose()
|
||
{
|
||
CloseCount++;
|
||
Hide();
|
||
if (s_stageMode)
|
||
{
|
||
StageSecondaryCount++;
|
||
if (StageSecondary == null) return "로비로 — 815d 핸들러 미연결(무동작)";
|
||
try { StageSecondary(); }
|
||
catch (System.Exception e) { return "로비로 — 예외(" + e.GetType().Name + ")"; }
|
||
return "로비로 — StageDirector 호출";
|
||
}
|
||
return "닫기";
|
||
}
|
||
|
||
// ═════════════════════════════════════════════════════════════════════
|
||
// WL-815d — 스테이지 클리어 화면 (기준서 §F-1)
|
||
// ═════════════════════════════════════════════════════════════════════
|
||
|
||
/// <summary>스테이지 모드인가(실측 덤프용).</summary>
|
||
public static bool StageMode { get { return s_stageMode; } }
|
||
/// <summary>마지막으로 받은 스테이지 결과(실측 덤프용).</summary>
|
||
public static WL.Combat.Stage.StageClearResult StageResultCopy { get { return s_stage; } }
|
||
public static bool BadgeVisible { get { return s_badge != null && s_badge.gameObject.activeSelf; } }
|
||
public static string BadgeText { get { return s_badge != null ? s_badge.text : ""; } }
|
||
|
||
/// <summary>스테이지 모드 on/off(off 로 되돌리면 813q 런 결과 화면 그대로).</summary>
|
||
public static string SetStageMode(bool on)
|
||
{
|
||
s_stageMode = on;
|
||
if (!on)
|
||
{
|
||
s_hasStage = false;
|
||
s_dungeonName = null; s_dungeonGold = -1; // WL-816m — 던전 문맥도 함께 비운다
|
||
if (s_badge != null && s_badge.gameObject.activeSelf) s_badge.gameObject.SetActive(false);
|
||
}
|
||
return "결과창 스테이지 모드=" + on;
|
||
}
|
||
|
||
// ── WL-816m 던전 문맥 (실측 덤프용) ──────────────────────────────────
|
||
/// <summary>이번 결과가 던전인가(= StageUiBridge 가 이름을 넘겼는가).</summary>
|
||
public static bool IsDungeonResult { get { return !string.IsNullOrEmpty(s_dungeonName); } }
|
||
/// <summary>이번 결과의 던전 이름(던전이 아니면 빈 문자열).</summary>
|
||
public static string DungeonName { get { return s_dungeonName ?? ""; } }
|
||
/// <summary>이번 결과의 획득 골드(모르면 -1).</summary>
|
||
public static int DungeonGold { get { return s_dungeonGold; } }
|
||
|
||
/// <summary>다음 `RunEnded` 1건을 무시한다(스테이지 실패 경로 · 결과창 중복 표시 0).</summary>
|
||
public static string SuppressNextRunEnded()
|
||
{
|
||
s_suppressNextRunEnded = true;
|
||
s_pending = false;
|
||
return "다음 RunEnded 1건 억제 예약";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 스테이지 클리어 결과를 받아 화면을 띄운다(부활 팝업이 떠 있으면 813q2 규칙대로 미룬다).
|
||
/// hits/potions/maxChain 은 <see cref="StageUiBridge"/> 가 스테이지 구간 동안 실측한 값이다.
|
||
/// </summary>
|
||
public static string ShowStage(in WL.Combat.Stage.StageClearResult r, int hits, int potions, int maxChain)
|
||
{
|
||
return ShowStage(in r, hits, potions, maxChain, null, -1);
|
||
}
|
||
|
||
/// <summary>
|
||
/// WL-816m — 던전 문맥을 함께 받는 경로. <paramref name="dungeonName"/> 이 비어 있으면
|
||
/// 위 4인자 경로와 **완전히 같다**(#815 스테이지 모드 무변경).
|
||
/// </summary>
|
||
public static string ShowStage(in WL.Combat.Stage.StageClearResult r, int hits, int potions, int maxChain,
|
||
string dungeonName, int dungeonGold)
|
||
{
|
||
var s = St;
|
||
var g = WLStageUiSettings.Instance;
|
||
if (s == null) return "WLRunUiSettings 에셋 없음";
|
||
if (g == null || !WLStageUiSettings.Enabled || !g.stageResultEnabled) return "815d off — 스테이지 결과 생략";
|
||
if (!WLRunUiSettings.Enabled || !s.resultEnabled) return "813q off — 결과 화면 생략";
|
||
|
||
s_dungeonName = g.dungeonModeEnabled && !string.IsNullOrEmpty(dungeonName) ? dungeonName : null;
|
||
s_dungeonGold = s_dungeonName != null ? dungeonGold : -1;
|
||
|
||
s_stage = r; s_hasStage = true;
|
||
s_stageHits = hits; s_stagePotions = potions; s_stageMaxChain = maxChain;
|
||
s_stageMode = true;
|
||
// 🔴 병행 RunEnded 는 StageCleared **뒤에** 온다(815b `Clear()` 실측) → 엘리트 스냅샷을 여기서 뜬다.
|
||
s_eliteKillsAtEnd = s_eliteKills;
|
||
s_cardPicksAtEnd = ZonePickup.PicksThisRun;
|
||
StageShownCount++;
|
||
s_suppressNextRunEnded = true; // 뒤따라오는 병행 RunEnded(Win) 는 무시(중복 표시 0)
|
||
s_pending = true;
|
||
return TryShow(Time.unscaledTime);
|
||
}
|
||
|
||
/// <summary>스테이지 결과 제목(마지막 스테이지면 다른 문구). WL-816m — 던전이면 「던전 N 클리어!」.</summary>
|
||
private static string StageTitle(WLStageUiSettings g)
|
||
{
|
||
// 🔴 WL-816m — 던전은 스테이지 표 id(던전 1 = 11)가 아니라 던전 이름을 쓴다.
|
||
// isLastStage(전 스테이지 클리어) 문구도 던전에는 해당하지 않는다(던전은 표의 한 줄이다).
|
||
if (s_dungeonName != null && g.dungeonModeEnabled)
|
||
return FormatName(g.dungeonClearTitleFormat, s_dungeonName);
|
||
|
||
string f = s_stage.isLastStage ? g.stageAllClearTitleFormat : g.stageClearTitleFormat;
|
||
return FormatOne(f, s_stage.stageId > 0 ? s_stage.stageId : s_stage.index + 1);
|
||
}
|
||
|
||
/// <summary>"{0}" 한 자리를 **문자열**로 치환(string.Format 없이 · 클리어 1회 경로라 틱 부담 0).</summary>
|
||
private static string FormatName(string format, string a0)
|
||
{
|
||
if (string.IsNullOrEmpty(format)) return a0 ?? "";
|
||
int i = format.IndexOf("{0}", System.StringComparison.Ordinal);
|
||
if (i < 0) return format;
|
||
s_sb.Length = 0;
|
||
s_sb.Append(format, 0, i).Append(a0).Append(format, i + 3, format.Length - i - 3);
|
||
return s_sb.ToString();
|
||
}
|
||
|
||
/// <summary>"{0}" 한 자리 치환(정수 전용 · string.Format 없이 · GC 최소).</summary>
|
||
private static string FormatOne(string format, long a0)
|
||
{
|
||
if (string.IsNullOrEmpty(format)) return a0.ToString();
|
||
s_sb.Length = 0;
|
||
AppendFormat2(s_sb, format, a0, 0);
|
||
return s_sb.ToString();
|
||
}
|
||
|
||
/// <summary>스테이지 본문(기준서 §F-1). 813q 의 행 노드를 그대로 재사용한다.</summary>
|
||
private static string BuildStageBody(WLRunUiSettings s, WLStageUiSettings g)
|
||
{
|
||
// 🔴 WL-816m — 던전이면 「획득 골드」 행이 한 줄 늘어난다.
|
||
// stageResultRows 에 Gold 를 이미 넣어 뒀다면 그 자리에 쓰고 줄을 더하지 않는다(중복 0).
|
||
bool goldRow = s_dungeonName != null && g.dungeonModeEnabled && g.dungeonGoldRowEnabled &&
|
||
!g.HasRow(WLStageResultRow.Gold);
|
||
EnsureStageRows(s, g, goldRow ? 1 : 0);
|
||
|
||
var body = new StringBuilder(256);
|
||
string title = StageTitle(g);
|
||
body.Append(title).Append('\n');
|
||
|
||
int slot = 0;
|
||
var rows = g.stageResultRows != null ? g.stageResultRows : new WLStageResultRow[0];
|
||
for (int i = 0; i < rows.Length; i++)
|
||
{
|
||
var kind = rows[i];
|
||
if (kind == WLStageResultRow.BossClear && !s_stage.isBoss) continue; // 보스 스테이지에서만
|
||
|
||
string label = StageLabelOf(g, kind);
|
||
s_sb.Length = 0;
|
||
StageValueOf(g, kind, s_sb);
|
||
WriteStageRow(slot, label, s_sb.ToString(), body);
|
||
slot++;
|
||
}
|
||
if (goldRow)
|
||
{
|
||
s_sb.Length = 0;
|
||
StageValueOf(g, WLStageResultRow.Gold, s_sb);
|
||
WriteStageRow(slot, StageLabelOf(g, WLStageResultRow.Gold), s_sb.ToString(), body);
|
||
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(title);
|
||
s_title.color = s.resultTitleWinColor;
|
||
}
|
||
|
||
// 무피격 배지(기준서 §F-1 「제안」) — 피격 0회 클리어일 때만
|
||
bool badge = g.noHitBadgeEnabled && s_stage.win && s_stageHits <= 0;
|
||
if (s_badge != null)
|
||
{
|
||
if (s_badge.gameObject.activeSelf != badge) s_badge.gameObject.SetActive(badge);
|
||
if (badge) { s_badge.SetText(g.noHitBadgeText); s_badge.color = g.noHitBadgeColor; body.Append(g.noHitBadgeText).Append('\n'); }
|
||
}
|
||
|
||
BodyText = body.ToString();
|
||
return "스테이지 행 " + slot + "개 · 배지=" + badge;
|
||
}
|
||
|
||
/// <summary>행 하나를 노드에 쓰고 본문 문자열에도 남긴다(노드가 없어도 본문은 끝까지 만든다).</summary>
|
||
private static void WriteStageRow(int slot, string label, string value, StringBuilder body)
|
||
{
|
||
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');
|
||
}
|
||
|
||
private static void EnsureStageRows(WLRunUiSettings s, WLStageUiSettings g, int extra)
|
||
{
|
||
int want = (g.stageResultRows != null ? g.stageResultRows.Length : 0) + extra;
|
||
if (s_rowRoot == null || s_row.Length >= want) return;
|
||
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);
|
||
}
|
||
}
|
||
|
||
private static string StageLabelOf(WLStageUiSettings g, WLStageResultRow row)
|
||
{
|
||
switch (row)
|
||
{
|
||
case WLStageResultRow.ClearTime: return g.rowClearTimeLabel;
|
||
case WLStageResultRow.Kills: return g.rowKillsLabel;
|
||
case WLStageResultRow.EliteKills: return g.rowEliteLabel;
|
||
case WLStageResultRow.MaxChain: return g.rowMaxChainLabel;
|
||
case WLStageResultRow.HitsPotions: return g.rowHitsPotionsLabel;
|
||
case WLStageResultRow.Retries: return g.rowRetriesLabel;
|
||
case WLStageResultRow.BossClear: return g.rowBossLabel;
|
||
case WLStageResultRow.Level: return g.rowLevelLabel;
|
||
// 🔴 WL-816m — 던전이면 「획득 골드」, 스테이지 모드면 지금까지와 같은 「골드」.
|
||
case WLStageResultRow.Gold: return s_dungeonName != null && g.dungeonModeEnabled ? g.rowDungeonGoldLabel : g.rowGoldLabel;
|
||
default: return g.rowLootLabel;
|
||
}
|
||
}
|
||
|
||
private static void StageValueOf(WLStageUiSettings g, WLStageResultRow row, StringBuilder sb)
|
||
{
|
||
var s = St;
|
||
switch (row)
|
||
{
|
||
case WLStageResultRow.ClearTime:
|
||
AppendTime(sb, s_stage.elapsedSec, s);
|
||
break;
|
||
case WLStageResultRow.Kills:
|
||
AppendFormat2(sb, g.rowKillsFormat, s_stage.killed, s_stage.total);
|
||
break;
|
||
case WLStageResultRow.EliteKills:
|
||
sb.Append(s != null && s.resultCountElitesInUi ? s_eliteKillsAtEnd : 0);
|
||
break;
|
||
case WLStageResultRow.MaxChain:
|
||
sb.Append(s_stageMaxChain);
|
||
break;
|
||
case WLStageResultRow.HitsPotions:
|
||
AppendFormat2(sb, g.rowHitsPotionsFormat, s_stageHits, s_stagePotions);
|
||
break;
|
||
case WLStageResultRow.Retries:
|
||
AppendFormat2(sb, g.rowRetriesFormat, s_stage.retries, 0);
|
||
break;
|
||
case WLStageResultRow.BossClear:
|
||
sb.Append(s_stage.win ? g.rowBossClearedText : g.rowUnknownText);
|
||
break;
|
||
case WLStageResultRow.Gold:
|
||
// 🔴 WL-816m — 던전이면 섬 축이 실제로 지급할 값(StageUiBridge 가 WLIslandBridge.ComputeGold 로 받아 넘긴다).
|
||
// 던전이 아니거나 보상 축이 꺼져 있으면 지금까지처럼 「미확인」.
|
||
if (s_dungeonName != null && g.dungeonModeEnabled && s_dungeonGold >= 0)
|
||
AppendFormat2(sb, g.rowDungeonGoldFormat, s_dungeonGold, 0);
|
||
else sb.Append(g.rowUnknownText);
|
||
break;
|
||
default:
|
||
// 🔴 레벨 · 등급별 획득 = 815b 페이로드에 없다(실측) → 「미확인」 표기
|
||
sb.Append(g.rowUnknownText);
|
||
break;
|
||
}
|
||
}
|
||
|
||
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 "설정 없음";
|
||
// WL-815d — 스테이지 클리어면 §F-1 행으로 그린다(813q 런 행 구성은 그대로 둔다).
|
||
if (s_stageMode && s_hasStage)
|
||
{
|
||
var g = WLStageUiSettings.Instance;
|
||
if (g != null && WLStageUiSettings.Enabled && g.stageResultEnabled) return BuildStageBody(s, g);
|
||
}
|
||
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;
|
||
case WLRunResultRow.EliteKills: return s.rowEliteKillsLabel;
|
||
case WLRunResultRow.CardPicks: return s.rowCardPicksLabel;
|
||
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:
|
||
AppendBossClear(sb, s); // WL-813q2 — 미처치면 "미처치 (HP 남음 N %)"
|
||
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;
|
||
case WLRunResultRow.EliteKills:
|
||
sb.Append(s.resultCountElitesInUi ? s_eliteKillsAtEnd : 0);
|
||
break;
|
||
case WLRunResultRow.CardPicks:
|
||
sb.Append(s.resultCountCardPicksInUi ? s_cardPicksAtEnd : 0);
|
||
break;
|
||
default:
|
||
AppendFormat2(sb, s.rowDeathsFormat, s_result.deaths, s_result.potionsUsed);
|
||
break;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// WL-813q3 §1-1 — 보스 처치 행. 처치했으면 mm:ss, 아니면 "미처치 (HP 남음 N %)".
|
||
/// 🔴 남은 HP 의 원천이 바뀌었다: **`RunEnded` 페이로드 `bossHpRatio`**(813w6 D-44 스냅샷 · 0~1 · −1 = 미발생).
|
||
/// `CopyResult` 가 런 종료 순간에 구조체째 복사해 두므로 **부활 미룸(813q2 D-18)으로 결과가 늦게 그려져도**
|
||
/// 그 사이 `BossArena.ParkBoss` 가 보스를 만피로 되돌린 것과 무관하게 스냅샷 값 그대로다.
|
||
/// 페이로드가 −1(보스 미등장 · 못 읽음)이면 813q2 폴백(`BossHpBar.RunBossHpRatio` · UI 자체 실측)으로 간다.
|
||
/// 둘 다 없으면 퍼센트 없이 "미처치"만 쓴다. 원천은 `LastBossHpSource` 로 실측한다.
|
||
/// </summary>
|
||
private static void AppendBossClear(StringBuilder sb, WLRunUiSettings s)
|
||
{
|
||
if (s_result.bossClearSec >= 0f) { AppendTime(sb, s_result.bossClearSec, s); return; }
|
||
|
||
// ① 813w6 페이로드 스냅샷(Park 이전 값) → ② 813q2 UI 실측 폴백 → ③ 없음
|
||
float ratio = -1f;
|
||
LastBossHpSource = "없음";
|
||
if (s.resultBossHpFromPayload && s_result.bossHpRatio >= 0f)
|
||
{
|
||
ratio = s_result.bossHpRatio;
|
||
LastBossHpSource = "페이로드"; // RunEnded.bossHpRatio (813w6)
|
||
}
|
||
else if (s.resultBossHpFromUi)
|
||
{
|
||
ratio = BossHpBar.RunBossHpRatio;
|
||
if (ratio >= 0f) LastBossHpSource = "UI바"; // BossHpBar.RunBossHpRatio (813q2)
|
||
}
|
||
|
||
if (ratio >= 0f && !string.IsNullOrEmpty(s.resultBossNotClearedHpFormat))
|
||
{
|
||
// 0 % 는 "죽었는데 처치 기록이 없는" 경우라 올림하지 않는다(살아 있으면 최소 1 %).
|
||
int pct = (int)System.Math.Round(Mathf.Clamp01(ratio) * 100f, System.MidpointRounding.AwayFromZero);
|
||
if (pct <= 0 && ratio > 0f) pct = 1;
|
||
AppendFormat2(sb, s.resultBossNotClearedHpFormat, pct, 0);
|
||
return;
|
||
}
|
||
sb.Append(string.IsNullOrEmpty(s.resultBossNotClearedText) ? s.resultDashText : s.resultBossNotClearedText);
|
||
}
|
||
|
||
/// <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_badge = WLTextFxUtil.NewText(s_box, "Badge", s_font, TextAlignmentOptions.Center); // WL-815d 무피격 배지
|
||
s_badge.gameObject.SetActive(false);
|
||
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);
|
||
EnsureCanvas(s); // WL-813q2 — 전용 Canvas(overrideSorting)
|
||
if (s_canvas != null) s_canvas.sortingOrder = s.resultSortingOrder;
|
||
s_lowered = false;
|
||
string layout = ApplyLayout();
|
||
Hide();
|
||
return "연결 " + s.resultParentPath + " · " + layout + " · sortingOrder=" + SortingOrder;
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
// WL-815d — 무피격 배지(제목 바로 아래 · 켜져 있을 때만 본문을 밀어 내린다)
|
||
var g815 = WLStageUiSettings.Instance;
|
||
float badgeH = 0f;
|
||
if (s_badge != null && g815 != null)
|
||
{
|
||
badgeH = s_badge.gameObject.activeSelf ? g815.noHitBadgeFontPx : 0f;
|
||
var rt = s_badge.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 + s.resultTitleFontPx + 8f) * u);
|
||
rt.sizeDelta = new Vector2(0f, g815.noHitBadgeFontPx * u);
|
||
s_badge.fontSize = g815.noHitBadgeFontPx * u;
|
||
s_badge.alignment = TextAlignmentOptions.Center;
|
||
s_badge.color = g815.noHitBadgeColor;
|
||
WLTextFxUtil.ApplyEdge(s_badge, s.resultEdge);
|
||
}
|
||
|
||
float rowTop = (s.resultPadTopPx + s.resultTitleFontPx + s.resultTitleGapPx) * u + badgeH * 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);
|
||
}
|
||
}
|
||
|
||
// WL-815d — 버튼 문구: 스테이지 모드면 [다음 스테이지](마지막이면 [처음부터]) / [로비로]
|
||
string leftText = s.resultNextRunText, rightText = s.resultCloseText;
|
||
bool leftOn = true;
|
||
if (s_stageMode && g815 != null)
|
||
{
|
||
bool last = s_hasStage && s_stage.isLastStage;
|
||
leftText = last ? g815.restartFromFirstText : g815.nextStageText;
|
||
rightText = g815.toLobbyText;
|
||
leftOn = !last || g815.showRestartOnLastStage;
|
||
}
|
||
LayoutButton(s_nextButton, s_nextBg, s_nextLabel, s, u, leftOn ? -1 : 0, leftText, s.resultNextBgColor, s.resultNextLabelColor);
|
||
LayoutButton(s_closeButton, s_closeBg, s_closeLabel, s, u, leftOn ? 1 : 0, rightText, s.resultCloseBgColor, s.resultCloseLabelColor);
|
||
if (s_nextButton != null && s_nextButton.gameObject.activeSelf != leftOn) s_nextButton.gameObject.SetActive(leftOn);
|
||
|
||
return "box size" + s_box.sizeDelta + " 행틀=" + s_row.Length +
|
||
" 스테이지모드=" + s_stageMode + " 버튼=[" + leftText + "](" + leftOn + ") [" + rightText + "]" +
|
||
" 배지=" + (s_badge != null && s_badge.gameObject.activeSelf) +
|
||
" 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_badge != null) s_badge.font = f; // WL-815d
|
||
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;
|
||
DiedSeen = ReviveRequestSeen = RevivedSeen = SkipPopupReviveCount = 0;
|
||
LowerCount = RaiseCount = EliteKilledSeen = 0;
|
||
s_eliteKills = s_eliteKillsAtEnd = 0;
|
||
s_cardPicksAtEnd = 0; LastBossHpSource = "없음"; // WL-813q3
|
||
s_lowered = false; LastDeathLog = "";
|
||
s_pending = false; UsedCommonPopup = false; LastShowLog = "";
|
||
// WL-815d
|
||
StagePrimaryCount = StageSecondaryCount = StageShownCount = SuppressedRunEndedCount = 0;
|
||
s_hasStage = false; s_suppressNextRunEnded = false;
|
||
s_stageHits = s_stagePotions = s_stageMaxChain = 0;
|
||
if (s_badge != null && s_badge.gameObject.activeSelf) s_badge.gameObject.SetActive(false);
|
||
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_canvas = null; s_raycaster = 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_badge = null; s_stageMode = false; // WL-815d
|
||
s_dungeonName = null; s_dungeonGold = -1; // WL-816m
|
||
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);
|
||
sb.AppendLine(" [815d] 스테이지모드=" + s_stageMode + " 결과있음=" + s_hasStage +
|
||
" 표시 " + StageShownCount + "회 · 버튼 다음=" + StagePrimaryCount + " 로비=" + StageSecondaryCount +
|
||
"(핸들러 " + (StagePrimary != null) + "/" + (StageSecondary != null) + ")" +
|
||
" · RunEnded 억제 " + SuppressedRunEndedCount + "회(예약=" + s_suppressNextRunEnded + ")" +
|
||
" · 배지=" + BadgeVisible + "\"" + BadgeText + "\"" +
|
||
" · 스테이지 idx=" + s_stage.index + " id=" + s_stage.stageId + " win=" + s_stage.win +
|
||
" " + s_stage.elapsedSec.ToString("F1") + "s " + s_stage.killed + "/" + s_stage.total +
|
||
" 재도전=" + s_stage.retries + " 마지막=" + s_stage.isLastStage + " 보스=" + s_stage.isBoss +
|
||
" · 피격=" + s_stageHits + " 물약=" + s_stagePotions + " 최대연쇄=" + s_stageMaxChain);
|
||
sb.AppendLine(" [816m] 던전=" + IsDungeonResult + " \"" + DungeonName + "\" 골드=" + s_dungeonGold);
|
||
sb.AppendLine(" [813q2] 부활우선=" + ReviveBusy + "(" + ReviveBusyReason + ") · DeathFlow=" + DeathFlow.State +
|
||
" 런=" + RunDirector.Phase + "(running=" + RunDirector.IsRunning + ") · died=" + DiedSeen + " request=" + ReviveRequestSeen +
|
||
" revived=" + RevivedSeen + " 팝업생략부활=" + SkipPopupReviveCount +
|
||
" · 재정렬=" + s_lowered + "(내림 " + LowerCount + "/복귀 " + RaiseCount + ")" +
|
||
" sortingOrder=" + SortingOrder + " sibling=" + SiblingIndex);
|
||
sb.AppendLine(" [813q2] 엘리트 런중=" + s_eliteKills + " 종료스냅=" + s_eliteKillsAtEnd +
|
||
" 전체본것=" + EliteKilledSeen + " · 보스HP(UI 실측)=" +
|
||
(BossHpBar.RunBossHpRatio >= 0f ? (BossHpBar.RunBossHpRatio * 100f).ToString("F1") + "%" : "없음") +
|
||
" · 마지막 사망 로그 = " + LastDeathLog);
|
||
sb.AppendLine(" [813q3] 보스HP 원천=" + LastBossHpSource + " · 페이로드 스냅샷 ratio=" +
|
||
(BossHpRatioAtEnd >= 0f ? (BossHpRatioAtEnd * 100f).ToString("F1") + "%" : "없음(−1)") +
|
||
" hp=" + BossHpAtEnd.ToString("F0") + "/" + BossHpMaxAtEnd.ToString("F0") +
|
||
" · 카드 선택 종료스냅=" + s_cardPicksAtEnd + "(현재 " + ZonePickup.PicksThisRun + ")");
|
||
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 +
|
||
" · [813q2] 사망중미룸=" + s.resultDeferWhileDying + " 런밖즉시부활=" + s.skipRevivePopupAfterRunEnd +
|
||
" 전용캔버스=" + s.resultOwnCanvas + "(" + s.resultSortingOrder + "→" + s.resultSortingOrderWhileRevive + ")" +
|
||
" 재정렬=" + s.resultLowerWhileRevive + "/" + s.resultRaiseAfterRevive +
|
||
" 엘리트UI집계=" + s.resultCountElitesInUi + " 보스HP UI=" + s.resultBossHpFromUi +
|
||
" · [813q3] 보스HP 페이로드=" + s.resultBossHpFromPayload +
|
||
" 카드선택UI집계=" + s.resultCountCardPicksInUi);
|
||
return sb.ToString();
|
||
}
|
||
}
|
||
}
|