Project_WL/Assets/WL/Settings/WLGameplaySettings.cs

249 lines
17 KiB
C#
Raw 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.

// ─────────────────────────────────────────────────────────────────────────────
// 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
{
/// <summary>
/// WL 게임플레이 전역 스위치. 코드 상수 금지(C45) — 모든 값을 이 에셋에서 읽는다.
/// </summary>
[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;
/// <summary>설정 에셋(1회 로드 후 캐시). 에셋이 없으면 null.</summary>
public static WLGameplaySettings Instance
{
get
{
if (s_lookupDone) return s_cached;
s_lookupDone = true;
s_cached = Resources.Load<WLGameplaySettings>(ResourcesPath);
return s_cached;
}
}
/// <summary>캐시를 비운다(에디터에서 에셋을 새로 만들었을 때 · 테스트용).</summary>
public static void ClearCache()
{
s_cached = null;
s_lookupDone = false;
}
// ── 펫 (#763) ────────────────────────────────────────────────────────────
[Header("■ 펫 (PD 지시 #763)")]
[Tooltip("플레이어 펫을 게임에 등장시킬지. PD 지시로 꺼 둔 상태다 — 다시 등장시키라는 지시가 오면 여기를 켠다. " +
"끄면 장비창에 펫이 장착되어 있어도 프리팹을 로드하지 않고 펫 카드를 Empty 로 둔다. " +
"적 펫(PvP·투견장 상대)에는 영향이 없다.")]
public bool spawnPlayerPet = false;
/// <summary>
/// 플레이어 펫을 등장시킬지. **에셋이 없으면 false**(= 등장 안 함)로 본다 —
/// PD 지시가 에셋 누락으로 무력화되지 않게 하기 위한 안전 기본값이며,
/// 게임 데이터 값의 코드 하드코딩이 아니라 SOT 부재 시의 실패 처리다.
/// </summary>
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;
/// <summary>
/// 플레이어가 무적인지. **에셋이 없으면 false**(= 무적 아님) — SOT 부재 시
/// 게임을 원래 규칙대로 두는 안전 처리이며, 게임 데이터 값의 하드코딩이 아니다.
/// </summary>
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;
/// <summary>NavMeshAgent.obstacleAvoidanceType 오버라이드. Keep(-1) = 프리팹 원본 유지.</summary>
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" };
/// <summary>우측 하단 전투 패드를 숨길지. 에셋이 없으면 false(= 원래대로 보임).</summary>
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" };
/// <summary>인게임에서 채팅창을 가릴지. 에셋이 없으면 false(= 원래대로 보임).</summary>
public static bool HideIngameChat
{
get { var i = Instance; return i != null && i.hideIngameChat; }
}
}
}