Project_WL/Assets/WL/Combat/Nav/WLTerrainGuardSettings.cs

143 lines
8.0 KiB
C#
Raw Normal View History

// ─────────────────────────────────────────────────────────────────────────────
// WLTerrainGuardSettings.cs — 언덕(이동 불가 지형) 가드 값의 단일 출처(SOT · C45)
//
// PD 지시 #813 (2026-09-10) · 발주서 WL-813t2 §1-3 · §1-4
// 「언덕 지형은 플레이어나 몬스터가 갈 수 없도록 · 언덕을 뚫고 가는 상황 및 카메라 연출 꼬임 ·
// 이동 불가 지역에는 몬스터가 생성되어서도 안 됨」
// 근거 = QA W2-12 D-48(PC 가 아레나 밖 고지대 y 3.66 → 5.48 로 올라가 보스를 한 대도 못 때리고 시간 종료)
//
// 에셋 경로(고정): Assets/WL/Combat/Settings/Resources/WL/WLTerrainGuardSettings.asset
// → Resources.Load<WLTerrainGuardSettings>("WL/WLTerrainGuardSettings") 로 1회 로드 후 캐시
// (WLCombatCoreSettings·WLBossArenaSettings 등 기존 설정 SO 관례 그대로)
//
// ■ C8 롤백
// ① 에셋이 없다 / ② enabled = 0 / ③ RuntimeDisabled = true → 러너 자체가 뜨지 않는다(오브젝트 0).
// 축별로도 끌 수 있다 — terrainGuardEnabled = 0(가드만 off) · spawnGuardEnabled = 0(스폰 보정만 off).
// 🔴 코어 스위치(WLCombatCoreSettings.Enabled)에 의존하지 않는다 — MobNavRecovery(813x)와 같은 이유로,
// 코어를 끈 A/B 중에도 「언덕에 올라간 채 굳는」 상태가 되살아나면 안 된다.
//
// ■ 아래 기본값은 에셋 생성 시 초기값이다. 실제로 읽히는 값은 항상 .asset 이며 코드에 수치 상수는 없다(C45).
//
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것(Actor/MobActor 가 Assembly-CSharp 에 있다).
// ─────────────────────────────────────────────────────────────────────────────
using UnityEngine;
namespace WL.Combat.Nav
{
[CreateAssetMenu(fileName = "WLTerrainGuardSettings", menuName = "WL/Terrain Guard Settings", order = 332)]
public sealed class WLTerrainGuardSettings : ScriptableObject
{
public const string ResourcesPath = "WL/WLTerrainGuardSettings";
static WLTerrainGuardSettings s_cached;
static bool s_lookupDone;
/// <summary>설정 에셋(1회 로드 후 캐시). 없으면 null = 기능 전체 off.</summary>
public static WLTerrainGuardSettings Instance
{
get
{
if (!s_lookupDone)
{
s_cached = Resources.Load<WLTerrainGuardSettings>(ResourcesPath);
s_lookupDone = true;
}
return s_cached;
}
}
/// <summary>진단·A/B 전용 런타임 스위치. 도메인 리로드로 자동 false.</summary>
public static bool RuntimeDisabled;
/// <summary>두 축(지형 가드 · 스폰 보정)이 살아 있는가.</summary>
public static bool Active
{
get { var i = Instance; return i != null && i.enabled && !RuntimeDisabled; }
}
/// <summary>테스트용: 캐시를 비워 다음 접근 때 다시 Resources.Load 한다.</summary>
public static void ClearCache() { s_cached = null; s_lookupDone = false; }
/// <summary>프로브용: 임시 SO 를 잠시 Instance 자리에 끼운다(C8 진리표 · 실에셋은 읽기만 한다).</summary>
public static void SetInstanceForProbe(WLTerrainGuardSettings so) { s_cached = so; s_lookupDone = true; }
// ─────────────────────────────────────────────────────── 공통
[Header("공통")]
[Tooltip("0 이면 러너가 뜨지 않는다 = 원본 동작 100% (C8 롤백).")]
public bool enabled = true;
[Tooltip("복구·보정 1건마다 로그 1줄.")]
public bool verboseLog;
[Tooltip("점검 주기(초). 액터 1명당 판정 1회.")]
public float checkSeconds = 0.2f;
[Tooltip("몬스터 목록 재수집 주기(초). FindObjectsByType 는 여기서만 부른다(GC).")]
public float rescanSeconds = 3f;
// ─────────────────────────────────────────────────────── 지형 가드(§1-3)
[Header("지형 가드 — 언덕 위/NavMesh 밖 복귀")]
[Tooltip("0 이면 지형 가드만 끈다.")]
public bool terrainGuardEnabled = true;
[Tooltip("이 높이(m)를 넘는 곳에 서 있으면 언덕으로 본다. 813t2 재베이크 후 WL_Nature 의 walkable 최고 y = 2.96 m → 여유 0.5 m.")]
public float maxWalkableY = 3.5f;
[Tooltip("이 반경(m) 안에 NavMesh 가 없으면 「NavMesh 밖」으로 본다. 원본 넉백·점프의 짧은 이탈을 흘려보낼 만큼은 커야 한다.")]
public float offNavMeshTolerance = 1f;
[Tooltip("이상 상태가 이 시간(초)을 넘게 지속돼야 되돌린다. 점프·넉백 중에는 원본이 의도적으로 에이전트를 끈다.")]
public float dwellSeconds = 1f;
[Tooltip("같은 액터를 다시 되돌리기까지의 최소 간격(초).")]
public float warpCooldownSeconds = 3f;
[Tooltip("되돌릴 지점을 찾는 1차 반경(m).")]
public float warpNearRadius = 4f;
[Tooltip("1차에서 못 찾으면 쓰는 2차 반경(m).")]
public float warpFarRadius = 30f;
[Tooltip("플레이어를 가드 대상에 넣는다(D-48 의 당사자).")]
public bool guardPlayer = true;
[Tooltip("일반 몬스터를 가드 대상에 넣는다.")]
public bool guardMobs = true;
[Tooltip("보스를 가드 대상에 넣는다(Anubis 스킬2 점프).")]
public bool guardBoss = true;
[Tooltip("플레이어를 되돌린 직후 카메라 이동 리드를 0 으로 스냅한다(PD 「카메라 연출 꼬임」 · RealCamera 무수정).")]
public bool warpSnapsCameraLead = true;
[Header("높이 규칙의 적용 범위")]
[Tooltip("높이 규칙(maxWalkableY)을 적용할 맵 ID. WL_Nature = -1(BattleMapConfig 실측). " +
"다른 맵은 고지대가 정상 플레이 영역일 수 있어 높이 규칙을 적용하지 않는다(「NavMesh 밖」 규칙만 남는다).")]
public int[] heightGuardMapIds = new int[] { -1 };
[Tooltip("1 이면 맵 ID 와 무관하게 높이 규칙을 적용한다(진단용).")]
public bool heightGuardAllMaps;
[Tooltip("현재 맵 ID 를 못 읽을 때 높이 규칙을 적용할지. WL_Nature 는 로비 겸용(Load_Map(-1))이라 기본 1.")]
public bool heightGuardWhenMapUnknown = true;
// ─────────────────────────────────────────────────────── 스폰 위치 검증(§1-4)
[Header("스폰 위치 검증 — 이동 불가 지역 스폰 0")]
[Tooltip("0 이면 스폰 보정만 끈다.")]
public bool spawnGuardEnabled = true;
[Tooltip("새로 나타난 몬스터를 훑는 주기(초).")]
public float spawnCheckSeconds = 0.5f;
[Tooltip("스폰 지점이 유효한지 볼 때 쓰는 NavMesh 표집 반경(m). 원본 MapData.spawnSnapRadius(WL_Nature = 2)와 같은 뜻.")]
public float spawnSampleRadius = 2f;
[Tooltip("표집 재시도 횟수. 시도마다 반경을 1배씩 넓힌다(2 → 4 → 6 m).")]
public int spawnRetries = 3;
[Tooltip("재시도가 전부 실패하면 스포너 노드 중심으로 옮긴다(발주서 §1-4).")]
public bool spawnFallbackToSpawnerNode = true;
}
}