// ───────────────────────────────────────────────────────────────────────────── // WLGameplaySettings.cs — WL 게임플레이 전역 스위치 단일 출처(SOT) // // PD 지시 #763 (2026-09-06): // "내가 지시할때까지 플레이어의 펫은 등장시키지 마." // // 목적: // "펫을 등장시킬지"는 코드가 아니라 **데이터**로 둔다(C45). 코드에는 상수 값을 넣지 않고 // 이 ScriptableObject 를 읽는다. PD 가 다시 지시하면 에셋의 체크박스 하나만 켜면 된다 // — 코드 되돌리기·재빌드 없이 원복되는 롤백 경로(C8). // // 로드 관례: // 같은 폴더의 SlashTrailSettings 와 동일하게 Resources 로 1회 로드 후 캐시한다. // 에셋 경로 = Assets/WL/Settings/Resources/WL/WLGameplaySettings.asset // // 🔴 어셈블리 주의: 힘민지 Assets/Script/ 에는 .asmdef 가 없다(전부 Assembly-CSharp). // Assets/WL/Settings/ 에도 .asmdef 를 만들지 말 것. // ───────────────────────────────────────────────────────────────────────────── using UnityEngine; namespace WL.Settings { /// /// WL 게임플레이 전역 스위치. 코드 상수 금지(C45) — 모든 값을 이 에셋에서 읽는다. /// [CreateAssetMenu(fileName = "WLGameplaySettings", menuName = "WL/Gameplay Settings")] public sealed class WLGameplaySettings : ScriptableObject { // ── 로드 경로 (게임 데이터 값이 아니라 에셋 주소다 · SlashTrailSettings 와 동일 규약) ── public const string ResourcesPath = "WL/WLGameplaySettings"; private static WLGameplaySettings s_cached; private static bool s_lookupDone; /// 설정 에셋(1회 로드 후 캐시). 에셋이 없으면 null. public static WLGameplaySettings Instance { get { if (s_lookupDone) return s_cached; s_lookupDone = true; s_cached = Resources.Load(ResourcesPath); return s_cached; } } /// 캐시를 비운다(에디터에서 에셋을 새로 만들었을 때 · 테스트용). public static void ClearCache() { s_cached = null; s_lookupDone = false; } // ── 펫 (#763) ──────────────────────────────────────────────────────────── [Header("■ 펫 (PD 지시 #763)")] [Tooltip("플레이어 펫을 게임에 등장시킬지. PD 지시로 꺼 둔 상태다 — 다시 등장시키라는 지시가 오면 여기를 켠다. " + "끄면 장비창에 펫이 장착되어 있어도 프리팹을 로드하지 않고 펫 카드를 Empty 로 둔다. " + "적 펫(PvP·투견장 상대)에는 영향이 없다.")] public bool spawnPlayerPet = false; /// /// 플레이어 펫을 등장시킬지. **에셋이 없으면 false**(= 등장 안 함)로 본다 — /// PD 지시가 에셋 누락으로 무력화되지 않게 하기 위한 안전 기본값이며, /// 게임 데이터 값의 코드 하드코딩이 아니라 SOT 부재 시의 실패 처리다. /// public static bool SpawnPlayerPet { get { var i = Instance; return i != null && i.spawnPlayerPet; } } // ── 무적 (#765 · 임시) ─────────────────────────────────────────────────── [Header("■ 플레이어 무적 (PD 지시 #765 · 임시)")] [Tooltip("임시 · PD 지시 #765 · 끄면 원상. 테스트 편의를 위해 플레이어(PC)가 피해를 받지 않는다. " + "MyActor.Get_Damage 진입에서 곧바로 반환하므로 HP 는 줄지 않는다. " + "넉백 등 상태이상까지 막지는 않는다. 펫·적에게는 영향이 없다.")] public bool playerInvincible = true; /// /// 플레이어가 무적인지. **에셋이 없으면 false**(= 무적 아님) — SOT 부재 시 /// 게임을 원래 규칙대로 두는 안전 처리이며, 게임 데이터 값의 하드코딩이 아니다. /// public static bool PlayerInvincible { get { var i = Instance; return i != null && i.playerInvincible; } } // ── 충돌(이동 차단) 범위 · NavMeshAgent (#768) ─────────────────────────── // PD 지시 #768 (2026-09-06): "플레이어와 몬스터의 충돌범위가 너무 넓은 것 같아. // (좁게 만들어서 대부분 지나갈 수 있도록 해야 해. (단, 보스 몬스터는 몬스터 크기를 고려해 크게 잡아도 됨)" // 실측(2026-09-06): 플레이어·몬스터가 서로를 밀어내는 것은 물리 콜라이더가 아니라 NavMeshAgent 다. // - PC 프리팹 Ai01: CapsuleCollider 는 IsTrigger, Rigidbody 는 Constraints=126(위치·회전 전부 고정) // - MobActor.Start 가 몹 Rigidbody 를 isKinematic 으로 강제 → 물리로는 서로를 밀 수 없다 // - 남는 것은 NavMeshAgent 의 radius + 상호 회피(RVO). 몹 프리팹의 avoidancePriority=10 이 PC(50)보다 // 우선순위가 높아, 실제로는 플레이어가 몹을 비켜 가도록 밀려난다. // 적용 지점 = PCActor.Set / MobActor.Set(MonsterTableData…) → WLCollisionTuning (프리팹 개별 편집 없음). [Header("■ 충돌 범위 · NavMeshAgent (PD 지시 #768)")] [Tooltip("이 섹션 전체의 마스터 스위치. 끄면 프리팹 원본 값 그대로 동작한다(= 지시 이전 상태로 롤백).")] public bool agentTuningEnabled = true; [Tooltip("플레이어(PC)의 NavMeshAgent 반경(m). 0 이하면 프리팹 원본 유지. 프리팹 원본 = 0.4")] public float pcAgentRadius = 0.15f; [Tooltip("일반 몹(MonsterList e_MonsterType = None · 251종)의 원본 반경에 곱할 배율. 1 이면 원본 유지.")] public float mobAgentRadiusScale = 0.5f; [Tooltip("일반·정예 몹 반경의 하한(m). 배율을 곱한 결과가 이 값보다 작아지지 않는다. 0 이면 하한 없음.")] public float mobAgentRadiusMin = 0.08f; [Tooltip("정예 몹(Elite · 36종)의 반경 배율.")] public float eliteAgentRadiusScale = 0.75f; [Tooltip("보스(Boss · 12종)의 반경 배율. PD 지시대로 보스는 크게 유지 = 1.0(원본).")] public float bossAgentRadiusScale = 1f; [Tooltip("플레이어의 회피 우선순위(0 = 최우선 · 낮을수록 남이 비켜 준다 · Unity 기본 50). 음수면 원본 유지.")] public int pcAvoidancePriority = 0; [Tooltip("일반·정예 몹의 회피 우선순위(프리팹 원본 = 10 · Unity 기본 50). 음수면 원본 유지. " + "보스는 tuneBossAgent 가 꺼져 있으면 손대지 않는다.")] public int mobAvoidancePriority = 50; [Tooltip("일반 몹(None)의 상호 회피 품질. Keep = 프리팹 원본(LowQuality). None 이면 서로 겹쳐 지나갈 수 있다.")] public AvoidanceOverride mobAvoidance = AvoidanceOverride.Keep; [Tooltip("플레이어의 상호 회피 품질. Keep = 프리팹 원본(LowQuality). None 이면 플레이어가 몹을 피해 밀려나지 않는다.")] public AvoidanceOverride pcAvoidance = AvoidanceOverride.Keep; [Tooltip("켜면 보스에게도 회피 우선순위·회피 품질 오버라이드를 적용한다. 기본은 꺼짐(보스는 원본 유지).")] public bool tuneBossAgent = false; /// NavMeshAgent.obstacleAvoidanceType 오버라이드. Keep(-1) = 프리팹 원본 유지. public enum AvoidanceOverride { Keep = -1, None = 0, LowQuality = 1, MedQuality = 2, GoodQuality = 3, HighQuality = 4, } // ── 몬스터 투사체 크기 (#769) ──────────────────────────────────────────── // PD 지시 #769 (2026-09-06): "몬스터 투사체 크기를 지금 크기에서 70% 줄여줘 (작게)" → 배율 0.3 // 적용 지점 = ProjectileBase.Set → WLCollisionTuning.ProjectileScale. 플레이어·펫 투사체는 무변경. [Header("■ 몬스터 투사체 크기 (PD 지시 #769)")] [Tooltip("일반 몹이 쏜 투사체의 크기 배율(원본 대비). 0.3 = 70% 축소. 1 이면 원본.")] public float mobProjectileScale = 0.3f; [Tooltip("정예 몹(Elite)이 쏜 투사체의 크기 배율.")] public float eliteProjectileScale = 0.3f; [Tooltip("보스(Boss)가 쏜 투사체의 크기 배율. PD 가 따로 조절할 수 있게 분리해 두었다.")] public float bossProjectileScale = 0.3f; [Tooltip("켜면 '렌더러가 있는(눈에 보이는)' 투사체만 축소한다. 렌더러가 없는 프리팹은 근접 공격 등의 " + "보이지 않는 타격 판정 박스(예: P_*_Meele = 3x3x3 트리거 박스 · 수명 0.1초)라, 줄이면 화면상 변화 없이 " + "몹의 공격 사거리만 줄어 빗나간다. 끄면 판정 박스까지 전부 축소한다(= PD 지시 문자 그대로).")] public bool projectileScaleVisibleOnly = true; // ── 몬스터 몸 크기 (#816x) ─────────────────────────────────────────────── // PD 지시 #816 (2026-09-15): "기본적인 몬스터의 크기도 50% 키워줘" → 일반 몹 배율 1.5 // 적용 지점 = MobActor.Set 이 이미 부르는 WLCollisionTuning.ApplyMob(원본 훅 재사용 · 새 훅 0줄). // MobActor.cs:87 이 transform.localScale = f_DefaultScale 을 대입한 **직후**(:118)라 // 매 스폰·풀 재사용마다 테이블 기본값에서 다시 곱한다 = 누적 없음. // 데이터 표(MonsterList.json f_DefaultScale)는 손대지 않는다 — 런타임 루트 스케일만. [Header("■ 몬스터 몸 크기 (PD 지시 #816x)")] [Tooltip("켜면 몬스터 루트 스케일에 아래 배율을 곱한다. 끄면 테이블 f_DefaultScale 그대로(C8 롤백).")] public bool mobBodyScaleEnabled = true; [Tooltip("일반 몹(None)의 몸 크기 배율. 1.5 = PD 지시 '50% 크게'. 1 이면 무변경.")] public float mobBodyScale = 1.5f; [Tooltip("정예 몹(Elite)의 몸 크기 배율. PD 지시는 '기본적인 몬스터'라 기본 1(무변경).")] public float eliteBodyScale = 1f; [Tooltip("보스(Boss)의 몸 크기 배율. PD 지시 범위 밖이라 기본 1(무변경).")] public float bossBodyScale = 1f; [Tooltip("켜면 배율 결과가 보스 판정 경계(bodyScaleBossThreshold)를 넘지 않도록 자른다.\n" + "원본 ProjectileBase.Check_TargetBoss(:305/308) 와 Actor.Get_ScaleSizeDist(:950) 가 " + "localScale.x > 2 를 '보스급' 판정에 쓰기 때문이다. 끄면 PD 지시대로 곧이곧대로 ×배율(기본 = 끔).")] public bool mobBodyScaleKeepUnderBossThreshold = false; [Tooltip("위 자르기의 경계값. 원본 코드가 쓰는 2 와 같아야 한다.")] public float bodyScaleBossThreshold = 2f; [Tooltip("경계에서 얼마나 아래로 자를지(여유). 0.02 = 1.98 로 자른다.")] public float bodyScaleThresholdEpsilon = 0.02f; // ── 인게임 HUD 오버라이드 (#775) ───────────────────────────────────────── // PD 지시 #775 (2026-09-06): "우측 하단 공격 버튼 및 스킬 아이콘 버튼은 추후 변경해서 사용할 // 예정이므로 지금은 보이지 않게 숨기고, 포트레이트 뷰에 맞게 WL 프로젝트 UI를 배치해줘." // 적용 지점 = NewGameUI.SetUI_byGameMode 끝 → WL.UI.WLIngameUiOverride.Apply (런타임 전용). // 프리팹을 편집하는 것이 아니라 런타임에 SetActive 만 바꾼다 → 스위치를 끄면 즉시 원복(C8). [Header("■ 인게임 HUD 오버라이드 (PD 지시 #775)")] [Tooltip("PD #775 임시 · 추후 교체. 우측 하단 전투 패드(공격·회피·스킬 슬롯·자동 타겟 토글)를 인게임에서 숨긴다. " + "끄면 다음 인게임 진입(또는 WLIngameUiOverride.ReapplyAll 호출) 시 즉시 원복된다. " + "숨겨도 전투는 성립한다 — 플레이어 공격은 WLTargetingSettings.autoEngage(자동 교전)가 담당한다.")] public bool hideBattlePad = true; [Tooltip("숨길 전투 패드 루트의 경로 목록. NewGameUI 프리팹 루트 기준 상대 경로다(대소문자 구분). " + "실측(2026-09-06): 인게임 전투 패드 = NewGameUI/IngameUIs/BattleUI (Attack·Avoid·Equip·btn_auto·SkillCard×6). " + "같은 이름의 BattleUI 가 SkillEquipUI·SkillLayout 아래에도 있어 이름만으로 찾으면 스킬 화면까지 " + "꺼지므로 반드시 경로로 지정한다.")] public string[] battlePadPaths = new[] { "IngameUIs/BattleUI" }; [Tooltip("인게임(Stage·Dungeon)에서만 끌 힘민지 공용 UI(NewGameUI/Common 의 자식) 이름 목록. " + "로비로 돌아오면 자동으로 다시 켠다 — 로비 화면은 손대지 않는다. " + "기준 = WL HUD 와 '같은 기능'이라 중복되는 것만. " + "TopRightMoney = WL HUD_Currency 와 같은 재화 표시 · Lv = WL HUD_Level 과 같은 레벨/경험치 표시(화면 하단 EXP 바). " + "되돌리려면 해당 이름을 목록에서 지우면 된다.")] public string[] ingameHideCommonObjectNames = new[] { "TopRightMoney", "Lv" }; /// 우측 하단 전투 패드를 숨길지. 에셋이 없으면 false(= 원래대로 보임). public static bool HideBattlePad { get { var i = Instance; return i != null && i.hideBattlePad; } } // ── 인게임 채팅창 숨김 (#796) ──────────────────────────────────────────── // PD 지시 #796 (2026-09-07): "하단 채팅창을 일단 숨기고 메뉴 버튼을 화면 하단에 가득 채워질 수 있도록 배치" // 적용 지점 = NewGameUI.SetUI_byGameMode 끝 → WL.UI.WLIngameUiOverride.Apply (런타임 전용). // // 🔴 SetActive(false) 를 쓰지 않는 이유(실측 2026-09-07): // ChatUI.Awake 가 밴 목록(m_ChatBan)을 로드하고 Start 가 m_BanListUI.Check() 를 부른다. // Awake 전에 꺼 버리면 m_ChatBan 이 null 인 채로 남고, 채팅 서버 메시지가 오는 순간 // MessageInfo.Update → ChatUI.Add_Chat 의 m_ChatBan.m_List 에서 NullReferenceException 이 난다. // 그래서 오브젝트는 켠 채로 두고 CanvasGroup 으로만 가린다 → 채팅 시스템 코드 경로는 그대로 산다. [Header("■ 인게임 채팅창 숨김 (PD 지시 #796)")] [Tooltip("PD #796 임시 · 인게임(Stage·Dungeon)에서 화면 최하단 채팅창(입력창·스크롤뷰·순위 표시)을 가린다. " + "끄면 다음 인게임 진입(또는 WLIngameUiOverride.ReapplyAll 호출) 시 즉시 원복된다. " + "오브젝트를 끄는 것이 아니라 CanvasGroup alpha 0 + blocksRaycasts false 로 가리므로 " + "채팅 서버 수신·밴 목록 등 채팅 시스템 동작은 그대로 유지된다.")] public bool hideIngameChat = true; [Tooltip("가릴 대상의 경로 목록. NewGameUI 프리팹 루트 기준 상대 경로다(대소문자 구분). " + "실측(2026-09-07): 채팅창 = NewGameUI/MessageInfo/Chat (ChatUI · bg · scrollview_chat · rank · " + "inputfield_chat · btn_send · BanList). 되돌리려면 이 목록을 비우면 된다.")] public string[] ingameHideCanvasGroupPaths = new[] { "MessageInfo/Chat" }; /// 인게임에서 채팅창을 가릴지. 에셋이 없으면 false(= 원래대로 보임). public static bool HideIngameChat { get { var i = Instance; return i != null && i.hideIngameChat; } } } }