Project_WL/Assets/WL/Settings/WLCollisionTuning.cs

165 lines
9.1 KiB
C#

// ─────────────────────────────────────────────────────────────────────────────
// WLCollisionTuning.cs — PD 지시 #768/#769 런타임 적용부
//
// PD 지시 #768 (2026-09-06): "플레이어와 몬스터의 충돌범위가 너무 넓은 것 같아.
// (좁게 만들어서 대부분 지나갈 수 있도록 해야 해. (단, 보스 몬스터는 몬스터 크기를 고려해 크게 잡아도 됨)"
// PD 지시 #769 (2026-09-06): "몬스터 투사체 크기를 지금 크기에서 70% 줄여줘 (작게)"
//
// 설계:
// - 값은 전부 WLGameplaySettings 에셋(데이터)에서 읽는다(C45). 이 파일에는 게임 수치 상수가 없다.
// - 프리팹 46(PC) + 299(몹) + 145(투사체)개를 개별 편집하지 않고 **런타임 1지점**에서 적용한다.
// → 롤백 = 에셋의 체크박스 하나(agentTuningEnabled / projectile 배율 1.0). 코드 되돌리기·재빌드 불필요(C8).
// - 오브젝트 풀 재사용 시 값이 누적되지 않도록 **항상 원본(프리팹) 값에서 다시 계산**한다.
//
// 🔴 어셈블리 주의: Assets/Script/ 에는 .asmdef 가 없다(전부 Assembly-CSharp).
// 이 폴더에도 .asmdef 를 만들지 말 것.
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
namespace WL.Settings
{
/// <summary>
/// NavMeshAgent(이동 차단 반경·회피)와 몬스터 투사체 크기의 런타임 적용부.
/// 호출 지점 = PCActor.Set / MobActor.Set(MonsterTableData…) / ProjectileBase.Set.
/// </summary>
public static class WLCollisionTuning
{
// 인스턴스별 원본 반경 캐시. 풀에서 재사용된 몹에 배율이 누적되는 것을 막는다.
static readonly Dictionary<int, float> s_originRadius = new Dictionary<int, float>(512);
// 투사체 프리팹 인스턴스별 "렌더러가 있는가"(= 눈에 보이는 투사체인가) 캐시.
static readonly Dictionary<int, bool> s_hasRenderer = new Dictionary<int, bool>(256);
/// <summary>이 에이전트의 원본(프리팹) 반경. 처음 보는 에이전트면 현재 값을 원본으로 기록한다.</summary>
public static float OriginRadius(NavMeshAgent agent)
{
if (agent == null) return 0f;
int id = agent.GetInstanceID();
float r;
if (s_originRadius.TryGetValue(id, out r)) return r;
if (s_originRadius.Count > 8192) s_originRadius.Clear(); // 씬 전환 누적 방지
r = agent.radius;
s_originRadius[id] = r;
return r;
}
/// <summary>플레이어(PC)에 적용. 펫·NPC 는 호출하지 않는다(PetActor 는 PCActor 를 상속하지 않는다).</summary>
public static void ApplyPC(NavMeshAgent agent)
{
var s = WLGameplaySettings.Instance;
if (agent == null || s == null || !s.agentTuningEnabled) return;
float origin = OriginRadius(agent);
agent.radius = s.pcAgentRadius > 0f ? s.pcAgentRadius : origin;
if (s.pcAvoidancePriority >= 0) agent.avoidancePriority = Mathf.Clamp(s.pcAvoidancePriority, 0, 99);
if ((int)s.pcAvoidance >= 0) agent.obstacleAvoidanceType = (ObstacleAvoidanceType)(int)s.pcAvoidance;
}
/// <summary>몬스터에 적용. subRole = MonsterList 테이블의 e_MonsterType(None/Elite/Boss).</summary>
public static void ApplyMob(NavMeshAgent agent, eSubRol subRole)
{
var s = WLGameplaySettings.Instance;
if (agent == null || s == null) return;
// #816x — 몸 크기(PD "기본적인 몬스터의 크기도 50% 키워줘"). 에이전트 튜닝 스위치와 독립이다.
ApplyMobBodyScale(agent.transform, subRole, s);
if (!s.agentTuningEnabled) return;
bool isBoss = subRole == eSubRol.Boss;
bool isElite = subRole == eSubRol.Elite;
float origin = OriginRadius(agent);
float scale = isBoss ? s.bossAgentRadiusScale : isElite ? s.eliteAgentRadiusScale : s.mobAgentRadiusScale;
float r = origin * (scale > 0f ? scale : 1f);
if (!isBoss && s.mobAgentRadiusMin > 0f && r < s.mobAgentRadiusMin) r = s.mobAgentRadiusMin;
agent.radius = r;
// 보스는 PD 지시대로 "크게" 유지 — 반경 배율(기본 1.0) 외에는 손대지 않는다.
if (isBoss && !s.tuneBossAgent) return;
if (s.mobAvoidancePriority >= 0) agent.avoidancePriority = Mathf.Clamp(s.mobAvoidancePriority, 0, 99);
// 회피 품질 오버라이드는 일반 몹(None)만. 정예·보스는 원본 유지(덩치가 있으면 막아야 한다).
if (!isElite && !isBoss && (int)s.mobAvoidance >= 0)
agent.obstacleAvoidanceType = (ObstacleAvoidanceType)(int)s.mobAvoidance;
}
// ── #816x 몬스터 몸 크기 ────────────────────────────────────────────────
/// <summary>몸 크기를 적용한 누적 횟수 · 보스 경계에서 잘린 횟수 · 마지막 값(검증용 읽기 전용).</summary>
public static int BodyScaleApplied, BodyScaleClamped;
public static float LastBodyScaleBase, LastBodyScaleApplied;
/// <summary>
/// 몬스터 루트 스케일에 타입별 배율을 곱한다(#816x · PD "기본적인 몬스터의 크기도 50% 키워줘").
///
/// 호출 시점 = MobActor.Set 안, `transform.localScale = f_DefaultScale * Vector3.one`(:87) **직후**(:118).
/// 따라서 base 는 언제나 테이블 기본값이고, 풀에서 재사용돼도 다시 기본값에서 곱한다 = **누적 없음**.
/// 루트 스케일이므로 자식(콜라이더·tf_Top·HP 바)은 함께 커지고,
/// NavMeshAgent.radius(월드 단위 · 바로 아래에서 별도 계산) 와 데이터 값(이동 속도 · f_BaseATKRange)은 **그대로다**.
/// </summary>
public static void ApplyMobBodyScale(Transform root, eSubRol subRole, WLGameplaySettings s)
{
if (root == null || s == null || !s.mobBodyScaleEnabled) return;
float m = subRole == eSubRol.Boss ? s.bossBodyScale
: subRole == eSubRol.Elite ? s.eliteBodyScale
: s.mobBodyScale;
if (m <= 0f || Mathf.Abs(m - 1f) < 0.0001f) return;
float bas = root.localScale.x;
if (bas <= 0.0001f) return;
float want = bas * m;
// 원본이 localScale.x > 2 를 '보스급' 판정에 쓴다(ProjectileBase:305/308 · Actor:950).
// 스위치가 켜져 있을 때만 그 경계를 넘지 않게 자른다(기본은 꺼짐 = PD 지시 그대로 ×배율).
float th = s.bodyScaleBossThreshold;
if (s.mobBodyScaleKeepUnderBossThreshold && th > 0f && bas <= th && want > th)
{
want = th - Mathf.Max(s.bodyScaleThresholdEpsilon, 0f);
BodyScaleClamped++;
}
root.localScale = Vector3.one * want;
BodyScaleApplied++;
LastBodyScaleBase = bas;
LastBodyScaleApplied = want;
}
/// <summary>
/// 이 투사체에 곱할 크기 배율. 1 이면 변경하지 않는다.
/// 플레이어·펫 투사체는 항상 1(무변경) — 몬스터(eRole.Mob)가 쏜 것만 대상.
/// </summary>
public static float ProjectileScale(Actor shooter, GameObject projectile)
{
var s = WLGameplaySettings.Instance;
if (s == null || DSUtil.CheckNull(shooter)) return 1f;
if (!shooter.IsRole(eRole.Mob)) return 1f;
float mult = shooter.IsSubRole(eSubRol.Boss) ? s.bossProjectileScale
: shooter.IsSubRole(eSubRol.Elite) ? s.eliteProjectileScale
: s.mobProjectileScale;
if (mult <= 0f || mult == 1f) return 1f;
// 렌더러가 없는 프리팹 = 눈에 보이지 않는 순수 타격 판정 박스(근접 공격 등).
// 이걸 줄이면 "투사체가 작아지는" 효과는 없고 몹의 공격 사거리만 줄어든다 → 기본은 제외.
if (s.projectileScaleVisibleOnly && !HasRenderer(projectile)) return 1f;
return mult;
}
static bool HasRenderer(GameObject go)
{
if (go == null) return true;
int id = go.GetInstanceID();
bool v;
if (s_hasRenderer.TryGetValue(id, out v)) return v;
if (s_hasRenderer.Count > 4096) s_hasRenderer.Clear();
v = go.GetComponentInChildren<Renderer>(true) != null;
s_hasRenderer[id] = v;
return v;
}
}
}