// ───────────────────────────────────────────────────────────────────────────── // WLRunSettings.cs — 3분 런 구조 값의 단일 출처(SOT · C45) // // PD 지시 #813 · 발주서 WL-813p §1 (2026-09-09) // // 에셋 경로(고정): Assets/WL/Combat/Settings/Resources/WL/WLRunSettings.asset // → Resources.Load("WL/WLRunSettings") 로 1회 로드 후 캐시 // (WLBossArenaSettings·WLLootSettings 등 기존 설정 SO 관례 그대로). // // ■ C8 롤백 (하나라도 false 면 RunDirector 는 아무 일도 하지 않는다 = 런 구조 이전 100%) // ① 에셋이 없다 → Enabled false // ② enabled = false → Enabled false // ③ RuntimeDisabled = true → Enabled false (진단·A/B 전용 · 도메인 리로드로 자동 해제) // ④ WLCombatCoreSettings.Enabled = false → Enabled false // 🔴 ④ 가 필요한 이유: 런 집계·승리 판정은 전부 CombatEvents(Killed/Spawned/SkillCast) 구독이다. // 코어가 꺼진 채 런을 걸면 처치가 0 으로 보여 「무조건 시간 종료」가 된다. // 그래서 "코어가 꺼지면 런 구조도 안 건다"(= 결과 화면도 안 뜬다)로 떨어뜨린다. // // ■ 아래 필드 기본값은 "SOT 부재 시의 실패 처리"가 아니라 에셋 생성 시 초기값이다. // 실제로 읽히는 값은 항상 .asset 이며, 코드에는 게임 수치 상수를 두지 않는다(C45). // // 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것(Actor/PCActor 가 Assembly-CSharp 에 있다). // ───────────────────────────────────────────────────────────────────────────── using UnityEngine; using WL.Combat.Core; namespace WL.Combat.Run { [CreateAssetMenu(fileName = "WLRunSettings", menuName = "WL/Run Settings", order = 327)] public sealed class WLRunSettings : ScriptableObject { public const string ResourcesPath = "WL/WLRunSettings"; static WLRunSettings s_cached; static bool s_lookupDone; /// 설정 에셋(1회 로드 후 캐시). 없으면 null. public static WLRunSettings Instance { get { if (!s_lookupDone) { s_cached = Resources.Load(ResourcesPath); s_lookupDone = true; } return s_cached; } } /// 진단·A/B 전용 런타임 스위치. 에셋을 건드리지 않고 런 구조만 끈다(도메인 리로드로 자동 false). public static bool RuntimeDisabled; /// 런 구조가 살아 있는가 = 에셋 존재 · enabled · 런타임 스위치 off · 코어 on. public static bool Enabled { get { var i = Instance; return i != null && i.enabled && !RuntimeDisabled && WLCombatCoreSettings.Enabled; } } /// 테스트용: 캐시를 비워 다음 접근 때 다시 Resources.Load 한다(에셋 제거/복구 실험). public static void ClearCache() { s_cached = null; s_lookupDone = false; } // ─────────────────────────────────────────── 전체 스위치 [Header("전체 스위치")] [Tooltip("끄면 런 상태 머신·집계·RunEvents 가 전부 비활성 — 맵은 기존 필드 맵 그대로다. 롤백 1순위.")] public bool enabled = true; [Tooltip("런 시작·존 클리어·게이트·종료를 Console 에 남긴다(검증용 · 기본 off).")] public bool verboseLog = false; // ─────────────────────────────────────────── 런 길이 [Header("런 길이 (기준서 §D-2 3분 런)")] [Tooltip("런 제한 시간(초). 이 시간을 넘기면 보스를 못 잡았어도 Timeout 으로 끝난다.")] public float runSeconds = 180f; [Tooltip("RunTick 발행 주기(초 · unscaled). 1 = 1 Hz(UI 타이머). 0 이하면 매 프레임(권장하지 않는다).")] public float tickIntervalSeconds = 1f; [Tooltip("진입 경계·타이머 폴링 주기(초 · unscaled). AutoCombatOnEnter 와 같은 폴링 방식이다.")] public float pollSeconds = 0.1f; [Tooltip("813i DeathFlow 가 세계를 멈춘 동안(부활 팝업)은 런 시계를 세운다. 끄면 실시간으로 계속 흐른다.")] public bool pauseTimerWhileWorldHeld = true; // ─────────────────────────────────────────── 존 진행 [Header("존 진행 (813e 존 스포너 · MonsterAppear.json 실측 ID)")] [Tooltip("런이 진행 순서로 보는 존 SpawnerID. zoneClearKills 와 길이가 같아야 한다(짧은 쪽 기준으로 잘린다).")] public int[] zoneSpawnerIds = new int[] { 813001, 813002, 813003, 813004 }; [Tooltip("존 클리어 임계 — 런 시작 시점 대비 그 스포너의 누적 처치 수. 기본 = MonsterAppear 의 n_MaxMonsterCount × 2.")] public int[] zoneClearKills = new int[] { 24, 30, 28, 16 }; // ─────────────────────────────────────────── 보스 게이트 [Header("보스 게이트 (813d BossArena)")] [Tooltip("보스 FieldBossData 의 n_SpawnerId(MonsterAppear 실측 813010). 0 이면 모든 보스 스폰을 이 런의 보스로 본다.")] public int bossSpawnerId = 813010; [Tooltip("존을 전부 클리어하면 BossArena.ForceOpenGate() 로 게이트를 연다.")] public bool openGateOnZonesCleared = true; [Tooltip("경과 시간이 이 값(초)을 넘으면 존이 남았어도 게이트를 연다(3분 안에 보스를 만나게 하는 안전판). 0 이하면 시간 개방 없음.")] public float openGateAtSec = 120f; // ─────────────────────────────────────────── 런 시작 · 종료 [Header("런 시작 · 종료 경계")] [Tooltip("맵(런) 진입 경계에서 런을 자동 시작한다. 끄면 RunDirector.StartRun() 을 직접 불러야 한다(813q 버튼 등).")] public bool autoStartOnMapEnter = true; [Tooltip("로비에서는 런을 시작하지 않는다(AutoCombatOnEnter 와 같은 판정).")] public bool skipLobby = true; [Tooltip("런 도중 맵을 뜨거나 로비로 돌아가면 RunEnded(Abandon) 1회 후 정지한다.")] public bool endOnMapLeave = true; // ─────────────────────────────────────────── 다음 런(재시작) [Header("다음 런 (RunDirector.Restart)")] [Tooltip("재시작 때 PC 를 존 시작점으로 되돌린다 — MyActor.On_Regen(false, rate) = 전회복 + Set_Warp(Get_ReincanationPos) 경로(813i DeathFlow 와 같다).")] public bool restartWarpsPlayer = true; [Tooltip("재시작 회복 비율(1 = 풀피). restartWarpsPlayer 가 꺼져 있으면 쓰이지 않는다.")] public float restartHealRate = 1f; [Tooltip("재시작 때 BossArena 게이트를 다시 잠근다(BossArena.ResetGate · 존 누적 카운터는 건드리지 않는다).")] public bool restartRearmsBossGate = true; [Tooltip("재시작 때 813i 물약 잔량을 런 기준으로 되돌린다(PotionUse.ResetRun).")] public bool restartResetsPotions = true; } }