// ───────────────────────────────────────────────────────────────────────────── // 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 { /// /// WL-813o2 — 효과 클래스가 **코드 문자열로** 추가 요구하는 Addressables 키 1건. /// 예: `Select_Star.cs:27 Shoot_Projectile("ProjectileStar", …)` → `Assets/Res_Addr/Projectile/ProjectileStar.prefab`. /// 값(키 문자열)을 코드에 두지 않기 위해 SO 배열로 뺀다(C45). /// [System.Serializable] public sealed class WLPickupExtraKey { [Tooltip("eEffect 이름(table_selectskill 의 SkillEffect 열과 같은 문자열).")] public string skillEffect = ""; [Tooltip("그 효과가 추가로 요구하는 Addressables 키(에셋 경로 그대로).")] public string assetKey = ""; [Tooltip("메모 — 어느 파일 몇 줄에서 나온 키인지(실측 근거).")] public string source = ""; } [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; // ── WL-813c2 (Q6 D-29) — 카드가 **먼저 열린 뒤** 죽는 경우 ───────────── // `deferWhileRevive` 는 열기 전(Pending)만 본다. 카드가 열린 뒤 사망하면 부활 팝업이 카드를 덮고, // 그 밑에서 타이머가 계속 흘러 플레이어가 보지도 못한 채 자동 선택된다(`R3_15` · 10초 → 2초 → 자동). [Tooltip("카드가 열려 있는 동안 부활 팝업이 뜨면 자동 선택 타임아웃을 **멈춘다**(부활 뒤 남은 시간부터 재개). " + "끄면 813o 동작 그대로 = C8 롤백.")] public bool pauseTimeoutWhileRevive = true; [Tooltip("카드 루트에 전용 Canvas(overrideSorting)를 붙여 그리는 순서를 값으로 고정한다. 끄면 형제 순서만 쓴다(813o 동작).")] public bool ownCanvasEnabled = true; [Tooltip("카드 전용 Canvas 의 sortingOrder. 🔴 813z 부활 팝업(`WLSurvivalSettings.reviveSortingOrder` = 60 실측)보다 " + "**작아야** 부활이 카드 위로 온다(발주서 §1-3). 공용 팝업 프리팹 SortOrder_5 = 5.")] public int cardSortingOrder = 40; // ── 카드 ────────────────────────────────────────────────────────────── [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; // ── WL-813o2 (Q9 D-41) — 참조 프리팹이 없는 카드 제외 ───────────────── // 「별」(ID 14 · Select_Star)은 `Assets/Res_Addr/Projectile/ProjectileStar.prefab` 을 // 초당 20발 요구하는데 그 파일이 없다 → InvalidKeyException 845건/런 + 결과창 모달(D-42). // 원본 `table_selectskill.Get_DataList(n)` 은 그대로 쓰고 **결과만 후필터**한다(원본 수정 0). [Header("풀 검증 — 부재 프리팹 카드 제외 (WL-813o2 · Q9 D-41)")] [Tooltip("끄면 813o 동작 그대로(제외 0 · 재뽑기 0 · 키 검사 0) = C8 롤백.")] public bool validatePool = true; [Tooltip("뽑기에서 뺄 table_selectskill 행 ID. 에디트 모드 풀 검증(AgentScripts/WL813o2_Probe)이 실측한 목록. " + "비우면 정적 제외 0(런타임 키 검사만 남는다).")] public int[] excludeSkillIds = new int[] { 3, 11, 14 }; [Tooltip("런타임에도 카드마다 참조 키가 Addressables 에 있는지 검사해 없으면 뺀다(ID 당 1회 · 결과 캐시 · GC 0). " + "카탈로그가 아직 없으면 검사를 건너뛴다(모르면 빼지 않는다).")] public bool runtimeKeyCheck = true; [Tooltip("제외 때문에 카드가 모자라면 원본 뽑기를 다시 부르는 최대 횟수. 0 = 재뽑기 없음.")] public int maxRedraws = 6; [Tooltip("제외·부재 키를 Console 에 1줄 남긴다(같은 ID 는 1회만 · Error 아님 = 모달 유발 0).")] public bool logExclusion = true; [Tooltip("원본 InGameInfo.Get_SkillPrefabPath 와 같은 규칙 — eEffect 이름에 'Select' 가 있으면 이 형식.")] public string selectSkillPathFormat = "Assets/Res_Addr/SelectSkill/{0}.prefab"; [Tooltip("원본 규칙 — eEffect 이름에 'Skill' 이 있으면 이 형식.")] public string skillPathFormat = "Assets/Res_Addr/Skill/{0}.prefab"; [Tooltip("원본 규칙 — 그 외에는 이 형식.")] public string effectPathFormat = "Assets/Res_Addr/Effect/{0}.prefab"; [Tooltip("원본 InGameInfo.Add_SelectSkill 스위치가 **프리팹을 로드하지 않는** eEffect 이름들(실측). " + "이 목록의 행은 1차 키를 만들지 않는다(없는 프리팹으로 오판하지 않기 위해).")] public string[] noPrefabEffects = new string[0]; [Tooltip("원본 효과 클래스(Select_*.cs)가 코드 문자열로 추가 요구하는 키(실측 grep). " + "1차 키와 별개로 이것들도 존재 검사한다.")] public WLPickupExtraKey[] extraKeys = new WLPickupExtraKey[0]; // ── 타임아웃 ────────────────────────────────────────────────────────── [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; } } }