// ───────────────────────────────────────────────────────────────────────────── // WLZonePickupSettings.cs — 존 클리어 픽업 카드 값의 단일 출처 (WL-813o · #813) // // 기준서 §D-2 813o 행 + 기획안 F-3 + 발주서 WL-813o §1(Lead 설계 결정). // 트리거 계약 = 813p 완료보고 §4 `RunEvents.ZoneCleared`(`Assets/WL/Combat/Run/RunEvents.cs` · 읽기만). // // ■ 왜 이 에셋인가 (C45) // 타임아웃 초·카드 수·존 포함 범위·배치·색·문구를 코드 상수로 두지 않는다. 이 파일에는 // "필드와 기본값"만 있고, 게임이 읽는 값은 `Resources/WL/WLZonePickupSettings.asset` 이다. // 🔴 런 길이·존 수 같은 **게임 값은 여기 없다** — 813p `WLRunSettings` 소유이고 // 픽업은 `ZoneCleared` 페이로드와 `RunDirector.ZoneCount` 로 받는다(수치 중복 0). // // ■ 좌표계 (px) // 813c/813q 와 같은 **세로 1080×1920 기준 화면 픽셀**. px → 캔버스 유닛 환산은 // `WLHudLayoutSettings.UnitsPerPx(canvas)` **하나만** 쓴다(산식 중복 0). // // ■ C8 롤백 // `enabled_ = 0` → 구독 0 · 노드 0 · 표시 0 · 런 타이머 정지 0 = 813o 이전 동작 100%. // // 🔴 어셈블리 주의: Assets/WL/UI/ 에 .asmdef 를 만들지 말 것(Assembly-CSharp 단일). // ───────────────────────────────────────────────────────────────────────────── using UnityEngine; namespace WL.UI { [CreateAssetMenu(fileName = "WLZonePickupSettings", menuName = "WL/Zone Pickup Settings", order = 34)] public sealed class WLZonePickupSettings : ScriptableObject { /// Resources 경로 — 런타임 로드 키. public const string ResourcesPath = "WL/WLZonePickupSettings"; private static WLZonePickupSettings s_instance; private static bool s_tried; /// 에셋이 없으면 null(호출부는 null 이면 전부 건너뛴다 = C8 롤백). public static WLZonePickupSettings 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; /// 픽업 축이 살아 있는가 = 에셋 존재 · enabled_ · 런타임 스위치 off. public static bool Enabled { get { if (RuntimeDisabled) return false; var s = Instance; return s != null && s.enabled_; } } // ── 공통 ────────────────────────────────────────────────────────────── [Header("공통 (C8 롤백)")] [Tooltip("끄면 구독 0 · 노드 0 · 표시 0 · 런 타이머 정지 0 = 813o 이전 동작 100%.")] public bool enabled_ = true; [Tooltip("켜면 상태 전이마다 Console 에 1줄 남긴다(문자열을 만들기 전에 이 값으로 막는다 · GC 주의).")] public bool verboseLog = false; // ── 트리거 ──────────────────────────────────────────────────────────── [Header("트리거 — 813p RunEvents.ZoneCleared")] [Tooltip("존 4(마지막 존)에서도 카드를 띄울지. 기본 끔 = 보스 흐름 방해 금지(발주서 §1-1).")] public bool includeFinalZone = false; [Tooltip("이 인덱스(0부터) 이상인 존만 카드를 띄운다. 0 = 존1부터 전부.")] public int minZoneIndex = 0; [Tooltip("런 1회당 최대 카드 횟수. 0 = 무제한(존 수만큼).")] public int maxPicksPerRun = 0; [Tooltip("ZoneCleared 를 받고 실제로 카드를 열기까지의 지연(초 · unscaled). 존 클리어 연출과 겹치지 않게 약간 둔다.")] public float openDelaySeconds = 0.35f; [Tooltip("813w RunGuide 가 다음 존으로 이동 중(Travel)이면 도착한 뒤에 연다.")] public bool waitForGuideArrival = true; [Tooltip("가이드 이동을 기다리는 최대 시간(초 · unscaled). 넘으면 이동 중이어도 연다(발주서 §1-1 = 8s).")] public float maxGuideDelaySeconds = 8f; [Tooltip("813i/813z 부활 팝업이 떠 있거나 예약 중이면 부활을 우선하고 카드를 미룬다.")] public bool deferWhileRevive = true; [Tooltip("부활 때문에 미룰 수 있는 최대 시간(초 · unscaled). 넘으면 이번 픽업을 버린다(무한 대기 금지).")] public float maxDeferSeconds = 45f; // ── 카드 ────────────────────────────────────────────────────────────── [Header("카드 — 원본 table_selectskill / InGameInfo.Add_SelectSkill")] [Tooltip("한 번에 보여 줄 카드 수(기획안 F-3 = 3장 중 1장).")] public int cardCount = 3; [Tooltip("원본 SelectSkillUI 노드(GameUI.Ins.IngameUI.m_SelectSkillUI)가 있으면 그걸 연다. " + "없으면 같은 원본 데이터·효과 경로를 쓰는 런타임 패널로 폴백한다. " + "🔴 실측(2026-09-09): 그 노드는 어떤 씬/프리팹에도 없다 = 항상 폴백.")] public bool preferOriginalUi = true; // ── 타임아웃 ────────────────────────────────────────────────────────── [Header("타임아웃 — 자동 선택")] [Tooltip("카드가 열린 뒤 자동 선택까지의 시간(초). 발주서 §1-2 = 10s.")] public float pickTimeoutSeconds = 10f; [Tooltip("타임아웃을 unscaled 시간으로 잰다(timeScale 0 이어도 흐른다).")] public bool timeoutUnscaled = true; [Tooltip("타임아웃 시 첫 카드를 고른다(발주서 §1-2). 끄면 원본 방식대로 무작위 카드.")] public bool autoPickFirstCard = true; [Tooltip("남은 초 표시 문구. TMP 무할당 오버로드 SetText(format, float) 를 쓴다 — {0} 하나만 넣을 것.")] public string timerFormat = "{0}초 후 자동 선택"; [Tooltip("남은 초가 이 값 이하면 경고색. 0 이면 경고색 없음.")] public float timerWarnSeconds = 3f; // ── 정지 ────────────────────────────────────────────────────────────── [Header("정지 — 런 타이머 / 자동전투")] [Tooltip("카드가 열려 있는 동안 813p 런 타이머를 멈춘다(RunDirector.PauseTimer · 세계 정지 없음).")] public bool pauseRunTimer = true; [Tooltip("카드가 열려 있는 동안 timeScale 을 0 으로 세운다(813z DeathFlow.HoldWorld 방식). " + "기본 끔 = 전투는 계속(발주서 §1-3 권장). 🔴 켜면 부활 팝업과 정지 주체가 겹친다.")] public bool holdWorld = false; [Tooltip("holdWorld 가 켜졌을 때 세울 timeScale 값.")] public float holdWorldTimeScale = 0f; [Tooltip("카드가 열려 있는 동안 원본 ActorInfo.Ins.All_Stop(true) 로 액터를 멈춘다 " + "(= 원본 SelectSkillUI.Set/SkillSelect 가 하던 그 경로). 선택 즉시 All_Stop(false) 로 되돌린다.")] public bool stopActorsWhileOpen = true; // ── 배치 ────────────────────────────────────────────────────────────── [Header("배치 (세로 1080×1920 기준 px)")] [Tooltip("패널을 붙일 부모 노드의 UI 루트 기준 경로(813y/813q 실측 = IngameUIs/WL_HUD · SafeAreaFitter 아래).")] public string parentPath = "IngameUIs/WL_HUD"; [Tooltip("배경 어둡게(딤) 색.")] public Color dimColor = new Color(0f, 0f, 0f, 0.62f); [Tooltip("딤이 터치를 먹는다(카드 밖을 눌러도 아무 일도 없게).")] public bool dimBlocksTouch = true; [Tooltip("카드 묶음 상자 크기(px).")] public Vector2 boxSizePx = new Vector2(980f, 720f); [Tooltip("상자 중심의 세로 오프셋(px · 양수 = 위). 0 = 화면 중앙.")] public float boxCenterOffsetYPx = 40f; [Tooltip("상자 배경색.")] public Color boxColor = new Color(0.043f, 0.047f, 0.071f, 0.92f); [Tooltip("상자 좌우 안쪽 여백(px).")] public float padXPx = 28f; [Tooltip("상자 위쪽 안쪽 여백(px).")] public float padTopPx = 26f; [Header("제목 · 남은 초")] [Tooltip("제목 문구.")] public string titleText = "스킬 선택"; [Tooltip("제목 글자 크기(px).")] public float titleFontPx = 54f; [Tooltip("제목 색.")] public Color titleColor = new Color(0.949f, 0.961f, 0.98f, 1f); [Tooltip("제목 아래 간격(px).")] public float titleGapPx = 14f; [Tooltip("남은 초 글자 크기(px).")] public float timerFontPx = 36f; [Tooltip("남은 초 색.")] public Color timerColor = new Color(0.78f, 0.82f, 0.88f, 1f); [Tooltip("남은 초 경고색(timerWarnSeconds 이하).")] public Color timerWarnColor = new Color(1f, 0.29f, 0.28f, 1f); [Tooltip("남은 초 줄 아래 간격(px).")] public float timerGapPx = 16f; [Header("카드 모양")] [Tooltip("카드 1장 크기(px).")] public Vector2 cardSizePx = new Vector2(288f, 430f); [Tooltip("카드 사이 간격(px).")] public float cardGapPx = 22f; [Tooltip("카드 배경색.")] public Color cardBgColor = new Color(0.12f, 0.13f, 0.18f, 1f); [Tooltip("펫 스킬 카드 배경색(원본 SelectSkillCard 의 petreinforce_bg 대응).")] public Color cardPetBgColor = new Color(0.20f, 0.13f, 0.22f, 1f); [Tooltip("카드 이름 글자 크기(px).")] public float cardNameFontPx = 38f; [Tooltip("카드 이름 색.")] public Color cardNameColor = new Color(1f, 0.86f, 0.45f, 1f); [Tooltip("카드 설명 글자 크기(px).")] public float cardDescFontPx = 26f; [Tooltip("카드 설명 색.")] public Color cardDescColor = new Color(0.86f, 0.89f, 0.94f, 1f); [Tooltip("카드 안쪽 여백(px).")] public float cardPadPx = 16f; [Tooltip("카드 이름 줄 높이(px).")] public float cardNameHeightPx = 84f; // ── 텍스트 테두리 (813y WLTextFxUtil.ApplyEdge) ──────────────────────── [Header("텍스트 테두리 (813y ApplyEdge · TMP 머티리얼 아웃라인)")] [Tooltip("제목·남은 초·카드 글자에 씌울 아웃라인/그림자.")] public WLTextEdge edge = new WLTextEdge { enabled = true, outlineWidth = 0.26f, outlineColor = new Color(0.03f, 0.03f, 0.05f, 1f), shadowOffset = new Vector2(0.55f, -0.55f), shadowColor = new Color(0f, 0f, 0f, 0.72f), shadowSoftness = 0.28f, faceDilate = 0.06f, }; // ── 환산 ────────────────────────────────────────────────────────────── /// px → 캔버스 유닛. 813c `WLHudLayoutSettings` 의 산식 하나만 쓴다(중복 0). public float UnitsPerPx(Canvas canvas) { var hud = WLHudLayoutSettings.Instance; return hud != null ? hud.UnitsPerPx(canvas) : 1f; } } }