// ─────────────────────────────────────────────────────────────────────────────
// 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;
}
}
}