Project_WL/Assets/WL/UI/Settings/WLZonePickupSettings.cs

320 lines
18 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─────────────────────────────────────────────────────────────────────────────
// 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
{
/// <summary>
/// WL-813o2 — 효과 클래스가 **코드 문자열로** 추가 요구하는 Addressables 키 1건.
/// 예: `Select_Star.cs:27 Shoot_Projectile("ProjectileStar", …)` → `Assets/Res_Addr/Projectile/ProjectileStar.prefab`.
/// 값(키 문자열)을 코드에 두지 않기 위해 SO 배열로 뺀다(C45).
/// </summary>
[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
{
/// <summary>Resources 경로 — 런타임 로드 키.</summary>
public const string ResourcesPath = "WL/WLZonePickupSettings";
private static WLZonePickupSettings s_instance;
private static bool s_tried;
/// <summary>에셋이 없으면 null(호출부는 null 이면 전부 건너뛴다 = C8 롤백).</summary>
public static WLZonePickupSettings Instance
{
get
{
if (s_instance == null && !s_tried)
{
s_tried = true;
s_instance = Resources.Load<WLZonePickupSettings>(ResourcesPath);
}
return s_instance;
}
}
/// <summary>에디터에서 에셋을 새로 만들거나 값을 바꾼 뒤 캐시를 버린다.</summary>
public static void ClearCache() { s_instance = null; s_tried = false; }
/// <summary>프로브·QA 용 런타임 스위치(에셋을 건드리지 않고 끈다).</summary>
public static bool RuntimeDisabled;
/// <summary>픽업 축이 살아 있는가 = 에셋 존재 · enabled_ · 런타임 스위치 off.</summary>
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,
};
// ── 환산 ──────────────────────────────────────────────────────────────
/// <summary>px → 캔버스 유닛. 813c `WLHudLayoutSettings` 의 산식 하나만 쓴다(중복 0).</summary>
public float UnitsPerPx(Canvas canvas)
{
var hud = WLHudLayoutSettings.Instance;
return hud != null ? hud.UnitsPerPx(canvas) : 1f;
}
}
}