// ───────────────────────────────────────────────────────────────────────────── // WLRunUiSettings.cs — 런 HUD · 런 결과 화면 값의 단일 출처 (WL-813q · #813) // // 기준서 §D-2 813q 행 + 발주서 WL-813q §1 (Lead 설계 결정). // 구독 계약 = 813p 완료보고 §4 `RunEvents` 표(`Assets/WL/Combat/Run/RunEvents.cs`). // // ■ 왜 이 에셋인가 (C45) // 위치·글자 크기·색·행 순서·문구를 코드 상수로 두지 않는다. 이 파일에는 "필드와 기본값"만 있고, // 게임이 읽는 값은 `Resources/WL/WLRunUiSettings.asset` 이다(PD·기획이 인스펙터에서 바꾼다). // 🔴 런 길이(`runSeconds`)·존 수 같은 **게임 값은 여기 없다** — 그건 813p `WLRunSettings` 소유이고 // UI 는 `RunStarted/RunTick` 페이로드로 받는다(수치 중복 0). // // ■ 좌표계 (px) // 813c 와 같은 **세로 1080×1920 기준 화면 픽셀**. px → 캔버스 유닛 환산은 // `WLHudLayoutSettings.UnitsPerPx(canvas)` **하나만** 쓴다(산식 중복 0 · 813tj 선례). // // ■ C8 롤백 // `enabled = 0` → HUD·결과 화면 둘 다 구독 0 · 노드 0 · 표시 0. // 813p `RunDirector` 는 그대로 돈다(이벤트만 아무도 안 듣는다). // // 🔴 어셈블리 주의: Assets/WL/UI/ 에 .asmdef 를 만들지 말 것(Assembly-CSharp 단일). // ───────────────────────────────────────────────────────────────────────────── using UnityEngine; namespace WL.UI { /// 결과 화면 본문 행 종류 — 배열 순서 = 표시 순서 · 배열에 없으면 표시 안 함. public enum WLRunResultRow { /// 총 시간(mm:ss). TotalTime = 0, /// 보스 처치 시간(미발생 −1 → 대시 문구). BossClear = 1, /// 처치 수(보스 n). Kills = 2, /// 최대 연쇄 처치. MaxChain = 3, /// 최대 피해(합산 또는 단일 — resultMaxDamageUseBurst). MaxDamage = 4, /// 레벨 from → to. Level = 5, /// 등급별 획득(0 인 등급 생략 · 등급 색). LootByGrade = 6, /// 획득 골드. Gold = 7, /// 사망 / 물약. DeathsPotions = 8, /// 엘리트 처치 수(WL-813q2 · 813p RunResult 에 없어 UI 가 직접 센다). EliteKills = 9, /// 카드 선택 수(WL-813q3 · 813o `ZonePickup.PicksThisRun` · 런 종료 순간 스냅샷). CardPicks = 10, } [CreateAssetMenu(fileName = "WLRunUiSettings", menuName = "WL/Run UI Settings", order = 33)] public sealed class WLRunUiSettings : ScriptableObject { /// Resources 경로 — 런타임 로드 키. public const string ResourcesPath = "WL/WLRunUiSettings"; private static WLRunUiSettings s_instance; private static bool s_tried; /// 에셋이 없으면 null(호출부는 null 이면 전부 건너뛴다 = C8 롤백). public static WLRunUiSettings Instance { get { if (s_instance == null && !s_tried) { s_tried = true; s_instance = Resources.Load(ResourcesPath); } return s_instance; } } /// 에디터에서 에셋을 새로 만들거나 값을 바꾼 뒤 캐시를 버린다. public static void ClearCache() { s_instance = null; s_tried = false; } /// 프로브·QA 용 런타임 스위치(에셋을 건드리지 않고 끈다). public static bool RuntimeDisabled; /// 런 UI 축이 살아 있는가 = 에셋 존재 · enabled · 런타임 스위치 off. public static bool Enabled { get { if (RuntimeDisabled) return false; var s = Instance; return s != null && s.enabled_; } } // ── 공통 ────────────────────────────────────────────────────────────── [Header("공통 (C8 롤백)")] [Tooltip("끄면 런 HUD·결과 화면 둘 다 구독 0 · 노드 0 · 표시 0. 813p RunDirector 는 그대로 돈다.")] // ScriptableObject 에 이미 `enabled` 라는 이름이 없지만(그건 MonoBehaviour), 혼동을 피해 뒤에 _ 를 붙인다. // 인스펙터·에셋 YAML 에는 `enabled_` 로 나온다. public bool enabled_ = true; [Tooltip("켜면 런 UI 가 상태 전이마다 Console 에 1줄 남긴다(문자열을 만들기 전에 이 값으로 막는다 · GC 주의).")] public bool verboseLog = false; // ── 런 HUD ──────────────────────────────────────────────────────────── [Header("런 HUD — 배치 (Safe Area 안 · WL_HUD 자식)")] [Tooltip("끄면 HUD 노드를 만들지도 갱신하지도 않는다(결과 화면은 그대로).")] public bool runHudEnabled = true; [Tooltip("HUD 를 붙일 부모 노드의 UI 루트 기준 경로(813y 실측 = IngameUIs/WL_HUD · SafeAreaFitter 아래).")] public string runHudParentPath = "IngameUIs/WL_HUD"; [Tooltip("Safe Area 상단에서 HUD 위쪽까지 여백(px). 813c 보스 HP 바(top 190 + 높이 66 = 256)와 겹치지 않게 그 아래.")] public float runHudTopMarginPx = 272f; [Tooltip("가로 중앙 기준 오프셋(px · 양수 = 오른쪽). 0 = 상단 중앙.")] public float runHudCenterOffsetXPx = 0f; [Tooltip("HUD 폭(px) — 존 게이지가 이 폭을 칸 수로 나눠 쓴다.")] public float runHudWidthPx = 560f; [Tooltip("런이 끝나면(승리·시간 종료·이탈 모두) HUD 를 숨긴다.")] public bool runHudHideOnEnd = true; [Header("런 HUD — 남은 시간")] [Tooltip("남은 시간 mm:ss 표시.")] public bool timerEnabled = true; [Tooltip("타이머 글자 크기(px).")] public float timerFontPx = 56f; [Tooltip("타이머 평상시 색.")] public Color timerColor = new Color(0.949f, 0.961f, 0.980f, 1f); [Tooltip("남은 시간이 이 값 이하면 경고 색으로 바뀐다(초). 0 이면 경고 없음.")] public float timerWarnSeconds = 30f; [Tooltip("경고 색(마지막 구간).")] public Color timerWarnColor = new Color(1f, 0.290f, 0.278f, 1f); [Tooltip("타이머 아웃라인·그림자(813y WLTextFxUtil.ApplyEdge).")] public WLTextEdge timerEdge = new WLTextEdge { enabled = true, outlineWidth = 0.28f, outlineColor = new Color(0.05f, 0.03f, 0.02f, 1f), shadowOffset = new Vector2(0.5f, -0.5f), shadowColor = new Color(0f, 0f, 0f, 0.8f), shadowSoftness = 0.22f, faceDilate = 0.08f, }; [Header("런 HUD — 존 게이지")] [Tooltip("존 게이지 칸 표시(칸 수 = RunStarted.zoneCount).")] public bool zoneGaugeEnabled = true; [Tooltip("타이머 아래 간격(px).")] public float zoneGaugeTopGapPx = 10f; [Tooltip("칸 높이(px).")] public float zoneCellHeightPx = 18f; [Tooltip("칸 사이 간격(px).")] public float zoneCellGapPx = 8f; [Tooltip("빈 칸(배경) 색.")] public Color zoneCellBgColor = new Color(0.05f, 0.05f, 0.06f, 0.78f); [Tooltip("진행 중인 칸의 채움 색(zoneKills / zoneRequired).")] public Color zoneCellFillColor = new Color(0.99f, 0.78f, 0.30f, 1f); [Tooltip("클리어한 칸의 채움 색.")] public Color zoneCellClearColor = new Color(0.137f, 0.906f, 0.529f, 1f); [Tooltip("존 클리어 순간 칸이 커졌다 돌아오는 배율(1 이면 펄스 없음).")] public float zonePulseScale = 1.35f; [Tooltip("펄스 시간(초 · unscaled).")] public float zonePulseSeconds = 0.3f; [Header("런 HUD — 단계 라벨")] [Tooltip("단계 라벨(\"존 N\" → 게이트 뒤 \"BOSS\") 표시.")] public bool phaseLabelEnabled = true; [Tooltip("존 게이지 아래 간격(px).")] public float phaseTopGapPx = 8f; [Tooltip("단계 라벨 글자 크기(px).")] public float phaseFontPx = 30f; [Tooltip("존 단계 문구 — {0} = 존 번호(1부터).")] public string phaseZoneFormat = "존 {0}"; [Tooltip("보스 게이트 개방 뒤 문구.")] public string phaseBossText = "BOSS"; [Tooltip("존 단계 라벨 색.")] public Color phaseColor = new Color(0.90f, 0.91f, 0.94f, 1f); [Tooltip("보스 단계 라벨 색.")] public Color phaseBossColor = new Color(1f, 0.62f, 0.24f, 1f); [Tooltip("단계 라벨 아웃라인·그림자.")] public WLTextEdge phaseEdge = new WLTextEdge { enabled = true, outlineWidth = 0.24f, outlineColor = new Color(0.05f, 0.03f, 0.02f, 1f), shadowOffset = new Vector2(0.4f, -0.4f), shadowColor = new Color(0f, 0f, 0f, 0.75f), shadowSoftness = 0.18f, faceDilate = 0.06f, }; // ── 결과 화면 ───────────────────────────────────────────────────────── [Header("런 결과 화면 (RunEnded)")] [Tooltip("끄면 결과 화면을 만들지도 띄우지도 않는다(HUD 는 그대로).")] public bool resultEnabled = true; [Tooltip("결과 화면을 붙일 부모 노드 경로(813tj ReviveDialog 와 같은 대역).")] public string resultParentPath = "IngameUIs/WL_HUD"; [Tooltip("공용 팝업(Popup.Ins · SortOrder_5)에 본문을 밀어 넣는다. 실측상 공용 팝업은 본문 라벨 1개 + " + "프리팹 고정 버튼 문구라 \"다음 런\" 문구를 프리팹 수정 없이 넣을 수 없다 → 기본 off(런타임 패널).")] public bool resultUseCommonPopup = false; [Tooltip("이탈(Abandon)에도 결과 화면을 띄운다. 발주서 §1-2 = 띄우지 않는다(HUD 리셋만).")] public bool resultShowOnAbandon = false; [Tooltip("부활 팝업(813i/813tj)이 떠 있거나 예약 중이면 결과 화면을 미뤘다가 닫힌 뒤 띄운다.")] public bool resultDeferWhileRevive = true; // ── D-18 (WL-813q2) ────────────────────────────────────────────────── // Q5 캡처 `PD2_07` 실측: 결과 화면("시간 종료")이 **먼저** 뜬 뒤 사망 → 부활 팝업이 그 위에 겹쳐 // 9행 중 4행을 가린다. 813q 는 `ReviveDialog.Busy` 만 봤는데 RunEnded 시점에는 아직 아무도 // 죽지 않아 Busy=false 였다 → 미룸이 걸리지 않는다. 아래 3개가 그 구멍을 막는다. [Header("결과 화면 — 부활 겹침 (WL-813q2 · D-18)")] [Tooltip("813i DeathFlow 상태(Dying/AwaitingRevive)를 **주 판정**으로 쓴다. " + "ReviveDialog.Busy(공용 팝업 경로)는 보조로 남는다. 끄면 813q 동작(Busy 만).")] public bool resultDeferWhileDying = true; [Tooltip("런이 끝난 뒤(RunDirector.Phase == Ended · EndRun ~ 다음 StartRun 사이) 죽으면 부활 팝업을 " + "생략하고 즉시 부활한다(813i DeathFlow.Revive() 공개 API). Idle(로비)은 건드리지 않는다.")] public bool skipRevivePopupAfterRunEnd = true; [Tooltip("결과 화면에 전용 Canvas(overrideSorting)를 붙인다. 끄면 형제 순서만으로 겹친다(813q 동작).")] public bool resultOwnCanvas = true; [Tooltip("결과 화면 평상시 sortingOrder. 813z 부활 팝업(reviveSortingOrder=60)보다 **아래**여야 한다.")] public int resultSortingOrder = 40; [Tooltip("부활 진행 중 결과 화면 sortingOrder(더 아래로 재정렬).")] public int resultSortingOrderWhileRevive = 20; [Tooltip("결과 화면이 이미 떠 있는데 사망하면 부활 뒤로 재정렬한다(sortingOrder 낮춤 + SetAsFirstSibling).")] public bool resultLowerWhileRevive = true; [Tooltip("부활이 끝나면 결과 화면을 다시 앞으로 되돌린다(sortingOrder 복귀 + SetAsLastSibling).")] public bool resultRaiseAfterRevive = true; [Tooltip("승리 제목.")] public string resultWinTitle = "런 클리어!"; [Tooltip("시간 종료 제목.")] public string resultTimeoutTitle = "시간 종료"; [Tooltip("이탈 제목(resultShowOnAbandon 을 켰을 때만 쓰인다).")] public string resultAbandonTitle = "런 중단"; [Tooltip("값이 없을 때(−1) 쓰는 문구.")] public string resultDashText = "—"; [Header("결과 화면 — 배치 · 색")] [Tooltip("팝업 상자 크기(px).")] public Vector2 resultBoxSizePx = new Vector2(860f, 1080f); [Tooltip("뒤 화면을 덮는 딤 색.")] public Color resultDimColor = new Color(0f, 0f, 0f, 0.62f); [Tooltip("팝업 상자 바탕 색.")] public Color resultBgColor = new Color(0.06f, 0.07f, 0.09f, 0.94f); [Tooltip("상자 안쪽 좌우 여백(px).")] public float resultPadXPx = 40f; [Tooltip("상자 위쪽 여백(px).")] public float resultPadTopPx = 36f; [Tooltip("제목 글자 크기(px).")] public float resultTitleFontPx = 56f; [Tooltip("승리 제목 색.")] public Color resultTitleWinColor = new Color(1f, 0.86f, 0.42f, 1f); [Tooltip("시간 종료·이탈 제목 색.")] public Color resultTitleFailColor = new Color(0.86f, 0.88f, 0.92f, 1f); [Tooltip("제목 아래 본문까지 간격(px).")] public float resultTitleGapPx = 28f; [Tooltip("본문 행 글자 크기(px).")] public float resultRowFontPx = 34f; [Tooltip("본문 행 높이(px).")] public float resultRowHeightPx = 56f; [Tooltip("행 이름(왼쪽) 색.")] public Color resultRowLabelColor = new Color(0.70f, 0.73f, 0.79f, 1f); [Tooltip("행 값(오른쪽) 색.")] public Color resultRowValueColor = new Color(0.96f, 0.97f, 0.99f, 1f); [Tooltip("본문 행 아웃라인·그림자.")] public WLTextEdge resultEdge = new WLTextEdge { enabled = true, outlineWidth = 0.22f, outlineColor = new Color(0.03f, 0.02f, 0.02f, 1f), shadowOffset = new Vector2(0.35f, -0.35f), shadowColor = new Color(0f, 0f, 0f, 0.7f), shadowSoftness = 0.15f, faceDilate = 0.05f, }; [Header("결과 화면 — 본문 행 (배열 순서 = 표시 순서 · 빼면 표시 안 함)")] [Tooltip("발주서 §1-2 기본 순서: 총 시간 · 보스 처치 · 처치 수 · 최대 연쇄 · 최대 피해 · 레벨 · 등급별 획득 · 골드 · 사망/물약")] public WLRunResultRow[] resultRows = new WLRunResultRow[] { WLRunResultRow.TotalTime, WLRunResultRow.BossClear, WLRunResultRow.Kills, WLRunResultRow.MaxChain, WLRunResultRow.MaxDamage, WLRunResultRow.Level, WLRunResultRow.LootByGrade, WLRunResultRow.Gold, WLRunResultRow.DeathsPotions, }; [Tooltip("행 이름 문구 — 위 배열의 순서와 무관하게 종류별로 붙는다.")] public string rowTotalTimeLabel = "총 시간"; public string rowBossClearLabel = "보스 처치"; public string rowKillsLabel = "처치 수"; public string rowMaxChainLabel = "최대 연쇄"; public string rowMaxDamageLabel = "최대 피해"; public string rowLevelLabel = "레벨"; public string rowLootLabel = "획득"; public string rowGoldLabel = "골드"; public string rowDeathsLabel = "사망 / 물약"; public string rowEliteKillsLabel = "엘리트 처치"; public string rowCardPicksLabel = "카드 선택"; [Tooltip("처치 수 값 문구 — {0} = 전체 처치, {1} = 그중 보스.")] public string rowKillsFormat = "{0} (보스 {1})"; [Tooltip("레벨 값 문구 — {0} = 시작 레벨, {1} = 종료 레벨.")] public string rowLevelFormat = "{0} → {1}"; [Tooltip("사망/물약 값 문구 — {0} = 사망, {1} = 물약 사용.")] public string rowDeathsFormat = "{0} / {1}"; [Tooltip("등급별 획득 한 칸 문구 — {0} = 개수. 앞에 등급 색 태그가 붙는다.")] public string rowLootGradeFormat = "{0}"; [Tooltip("등급별 획득 칸 사이 구분 문구.")] public string rowLootGradeSeparator = " "; [Tooltip("등급별 획득이 0 건일 때 값 문구.")] public string rowLootEmptyText = "0"; [Tooltip("최대 피해를 합산(DamageMergeEvent.total 최대 · maxDamageBurst)으로 본다. 끄면 단일 타격 최대(maxDamage).")] public bool resultMaxDamageUseBurst = true; [Tooltip("레벨 행은 levelFrom·levelTo 가 둘 다 0 이면(에디트 모드 등) 감춘다.")] public bool resultHideLevelWhenZero = true; // ── 행 보강 (WL-813q2 §1-3) ─────────────────────────────────────────── [Header("결과 화면 — 행 보강 (WL-813q2)")] [Tooltip("보스 미처치 행 문구(HP 를 모를 때). 비우면 resultDashText(—)를 그대로 쓴다.")] public string resultBossNotClearedText = "미처치"; [Tooltip("보스 미처치 + 남은 HP 를 아는 경우의 문구 — {0} = 남은 HP 퍼센트(정수).")] public string resultBossNotClearedHpFormat = "미처치 (HP 남음 {0} %)"; [Tooltip("🔴 813p RunResult 에는 보스 HP 필드가 없다(실측). 켜면 813c 보스 HP 바가 이번 런에 " + "실제로 물었던 마지막 비율을 쓴다(UI 자체 실측). 끄면 남은 HP 를 쓰지 않는다.\n" + "WL-813q3 부터는 resultBossHpFromPayload 가 먼저이고 이 값은 폴백이다.")] public bool resultBossHpFromUi = true; // ── 보스 HP 원천 (WL-813q3 · 813w6 D-44) ────────────────────────────── [Tooltip("🔴 WL-813q3 — 813w6 이 RunResult 에 bossHpRatio/bossHp/bossHpMax 를 실었다(실측). " + "켜면 결과창은 RunEnded 페이로드의 스냅샷(= BossArena.ParkBoss 만피 복구 **이전** 값)을 쓴다. " + "런 종료 순간에 복사해 두므로 부활 미룸(813q2 D-18)으로 늦게 그려도 값이 안 바뀐다. " + "끄면 813q2 동작(= BossHpBar.RunBossHpRatio · 늦게 그리면 만피 100 %)으로 돌아간다.")] public bool resultBossHpFromPayload = true; [Tooltip("🔴 813p RunResult 에는 엘리트 집계가 없다(실측). 켜면 UI 가 CombatEvents.Killed 의 " + "subRole==Elite 를 런 중에 직접 센다. 끄면 엘리트 행은 0 으로 나온다.")] public bool resultCountElitesInUi = true; [Tooltip("🔴 WL-813q3 — 813p RunResult 에는 카드 선택 집계도 없다(실측). 켜면 813o " + "ZonePickup.PicksThisRun(런 시작에 0) 을 런 종료 순간에 스냅샷해서 쓴다. 끄면 0.")] public bool resultCountCardPicksInUi = true; [Header("결과 화면 — 버튼")] [Tooltip("\"다음 런\" 버튼 문구 — 누르면 RunDirector.Restart() 하나만 부른다(813p 공개 API).")] public string resultNextRunText = "다음 런"; [Tooltip("\"닫기\" 버튼 문구.")] public string resultCloseText = "닫기"; [Tooltip("버튼 크기(px).")] public Vector2 resultButtonSizePx = new Vector2(340f, 104f); [Tooltip("버튼 사이 간격(px).")] public float resultButtonGapPx = 28f; [Tooltip("상자 아래에서 버튼까지 여백(px).")] public float resultButtonBottomPx = 36f; [Tooltip("버튼 글자 크기(px).")] public float resultButtonFontPx = 38f; [Tooltip("\"다음 런\" 버튼 바탕 색.")] public Color resultNextBgColor = new Color(0.137f, 0.906f, 0.529f, 1f); [Tooltip("\"다음 런\" 버튼 글자 색.")] public Color resultNextLabelColor = new Color(0.04f, 0.09f, 0.06f, 1f); [Tooltip("\"닫기\" 버튼 바탕 색.")] public Color resultCloseBgColor = new Color(0.16f, 0.18f, 0.22f, 1f); [Tooltip("\"닫기\" 버튼 글자 색.")] public Color resultCloseLabelColor = new Color(0.90f, 0.92f, 0.95f, 1f); // ── 다음 런 리셋 (WL-813q2 · D-23) ──────────────────────────────────── // Q5 `R3_30_after_next` 실측: 「다음 런」 뒤 이전 런의 보스 HP 바가 남고 타이머가 붉게 시작한다. // 타이머는 RunStarted 에서 s_shownWarn 을 false 로 되돌리지만 **색을 다시 쓰지 않아서** // (warn != s_shownWarn 이 false → 색 대입 생략) 이전 런의 경고색이 그대로 남는다. [Header("다음 런 리셋 (WL-813q2 · D-23)")] [Tooltip("RunStarted 마다 타이머 색을 기본색으로 되돌린다(경고색 잔존 버그).")] public bool timerResetColorOnRunStart = true; [Tooltip("RunStarted 마다 보스 HP 바·등장 배너 상태를 리셋한다(이전 런의 바 잔존).")] public bool bossResetOnRunStart = true; [Tooltip("리셋 뒤 살아 있는 보스가 있으면 보스 HP 바를 다시 물린다(813y ⑤ 늦은 구독 경로 재사용).")] public bool bossBarRebindOnRunStart = true; [Tooltip("리셋 뒤 살아 있는 보스가 있으면 등장 배너도 다시 띄운다(기본 끔 — 새 런 시작에 배너는 요란하다).")] public bool bossBannerRetriggerOnRunStart = false; // ── 파생 헬퍼 (코드 상수 0) ─────────────────────────────────────────── /// px → 캔버스 유닛 계수. 813c 의 산식 하나만 쓴다(중복 0). public float UnitsPerPx(Canvas canvas) { var hud = WLHudLayoutSettings.Instance; return hud != null ? hud.UnitsPerPx(canvas) : 1f; } /// 이 행을 표시하는가(배열에 있으면 표시). public bool HasRow(WLRunResultRow row) { if (resultRows == null) return false; for (int i = 0; i < resultRows.Length; i++) if (resultRows[i] == row) return true; return false; } /// 결과 제목(종료 사유별). public string TitleFor(WL.Combat.Run.RunOutcome outcome) { switch (outcome) { case WL.Combat.Run.RunOutcome.Win: return resultWinTitle; case WL.Combat.Run.RunOutcome.Timeout: return resultTimeoutTitle; default: return resultAbandonTitle; } } /// 결과 제목 색(종료 사유별). public Color TitleColorFor(WL.Combat.Run.RunOutcome outcome) { return outcome == WL.Combat.Run.RunOutcome.Win ? resultTitleWinColor : resultTitleFailColor; } } }