Project_WL/Assets/WL/Settings/WLTargetingSettings.cs

118 lines
8.6 KiB
C#

// ─────────────────────────────────────────────────────────────────────────────
// WLTargetingSettings.cs — 플레이어 타겟팅 규칙 단일 출처(SOT)
//
// PD 지시 #770 (2026-09-06):
// 1. 공격 시작 시점에 가장 가까운 대상을 타겟팅하되, 캐릭터가 바라보는 방향을 우선한다.
// 2. 콤보 중 적이 사망하면 그 시점에 가장 가까운 적으로 다시 타겟팅해 공격한다.
// 3. 타겟을 바꿀 때 이동부터 하지 말고, 공격 사거리 안이면 즉시 공격한다.
// 4. 플레이어 자동 공격 AI 에 "자신을 때린 대상 우선"(어그로) 개념이 있어서는 안 된다.
//
// 목적:
// 타겟팅 규칙의 수치·스위치를 코드 상수로 두지 않는다(C45). 값 조정·롤백은 이 에셋의
// 필드 하나만 고치면 되고, 코드 되돌리기·재빌드가 필요 없다(C8 롤백 경로).
//
// 로드 관례:
// 같은 폴더의 WLGameplaySettings 와 동일하게 Resources 로 1회 로드 후 캐시한다.
// 에셋 경로 = Assets/WL/Settings/Resources/WL/WLTargetingSettings.asset
//
// 🔴 에셋이 없을 때의 기본값 = "PD 지시대로 동작하는 값".
// (WLGameplaySettings #763 과 같은 원칙 — SOT 누락으로 PD 지시가 무력화되면 안 된다.
// 게임 데이터 값의 코드 하드코딩이 아니라 SOT 부재 시의 실패 처리다.)
//
// 🔴 어셈블리 주의: 힘민지 Assets/Script/ 에는 .asmdef 가 없다(전부 Assembly-CSharp).
// Assets/WL/Settings/ 에도 .asmdef 를 만들지 말 것.
// ─────────────────────────────────────────────────────────────────────────────
using UnityEngine;
namespace WL.Settings
{
/// <summary>
/// 플레이어(PC) 타겟팅 규칙 설정. 코드 상수 금지(C45) — 모든 값을 이 에셋에서 읽는다.
/// 펫(PetActor)·소환수·몬스터의 타겟팅에는 관여하지 않는다.
/// </summary>
[CreateAssetMenu(fileName = "WLTargetingSettings", menuName = "WL/Targeting Settings")]
public sealed class WLTargetingSettings : ScriptableObject
{
// ── 로드 경로 (게임 데이터 값이 아니라 에셋 주소다 · WLGameplaySettings 와 동일 규약) ──
public const string ResourcesPath = "WL/WLTargetingSettings";
private static WLTargetingSettings s_cached;
private static bool s_lookupDone;
/// <summary>설정 에셋(1회 로드 후 캐시). 에셋이 없으면 null.</summary>
public static WLTargetingSettings Instance
{
get
{
if (s_lookupDone) return s_cached;
s_lookupDone = true;
s_cached = Resources.Load<WLTargetingSettings>(ResourcesPath);
return s_cached;
}
}
/// <summary>캐시를 비운다(에디터에서 에셋을 새로 만들었을 때 · 테스트용).</summary>
public static void ClearCache()
{
s_cached = null;
s_lookupDone = false;
}
// ── 규칙 1 : 정면 우선 타겟팅 ────────────────────────────────────────────
[Header("■ 규칙 1 — 정면 우선 타겟팅 (PD #770-1)")]
[Tooltip("정면 원뿔의 전체 각도(도). 이 원뿔 안에 후보가 하나라도 있으면 그 안에서만 타겟을 고른다. " +
"0 이면 원뿔 제한 없이 아래 가중치 점수만 쓴다. 360 이면 사실상 전방위.")]
[Range(0f, 360f)] public float frontConeDeg = 90f;
[Tooltip("점수 = 거리 x (1 + 가중치 x (1 - cosθ)). θ = 캐릭터 정면과 적 방향의 수평 각. " +
"0 이면 거리만 본다(기존 동작). 값이 클수록 등 뒤 적을 더 강하게 배제한다. " +
"1.0 이면 바로 등 뒤(180도) 적의 점수가 거리의 3배가 된다.")]
[Min(0f)] public float facingWeight = 1f;
[Tooltip("공격 콤보를 시작하는 순간에 타겟을 다시 고를지(PD #770-1 '공격 시작 시점'). " +
"후보는 지금 바로 때릴 수 있는 거리 안으로 제한하며, 그 안에 아무도 없으면 기존 타겟을 유지한다.")]
public bool attackStartRetarget = true;
// ── 규칙 2 : 콤보 중 적 사망 시 재타겟 ───────────────────────────────────
[Header("■ 규칙 2 — 처치 시 즉시 재타겟 (PD #770-2)")]
[Tooltip("타겟이 죽는 즉시(공격/스킬 애니메이션 재생 중이어도) 가장 가까운 적으로 다시 타겟팅한다. " +
"새 타겟이 사거리 안이면 콤보를 끊지 않고 몸만 돌려 계속 때린다.")]
public bool retargetOnKill = true;
// ── 규칙 3 : 사거리 안이면 즉시 공격 ─────────────────────────────────────
[Header("■ 규칙 3 — 사거리 안이면 이동 없이 즉시 공격 (PD #770-3)")]
[Tooltip("공격 판정 여유(m). 정지 거리(stoppingDistance)에 이 값을 더한 범위 안이면 접근하지 않고 바로 때린다. " +
"실제 타격 판정 박스가 정지 거리보다 약간 길어서 생기는 '한 걸음 다가갔다 때리는' 동작을 없앤다. " +
"너무 키우면 닿지 않는 거리에서 헛스윙하므로 0.5 m 이상은 권하지 않는다.")]
[Min(0f)] public float immediateAttackMargin = 0.2f;
[Tooltip("재타겟 탐색 거리(m). 0 이면 기존 탐색 거리(table_GlobalValue f_PC_FindEnemyRange + AttackRange_Up)를 그대로 쓴다.")]
[Min(0f)] public float retargetSearchRange = 0f;
// ── 규칙 4 : 어그로 금지 ─────────────────────────────────────────────────
[Header("■ 규칙 4 — 피격 어그로 금지 (PD #770-4)")]
[Tooltip("플레이어가 맞았을 때 때린 상대를 타겟으로 잡을지. PD 지시로 꺼 둔 상태다(= 어그로 없음). " +
"🔴 끄면 자동 전투(Auto)가 꺼진 계정은 맞아도 스스로 공격을 시작하지 않는다 — " +
"공격 버튼을 누르거나 Auto 를 켜야 타겟을 잡는다. 펫·소환수·몬스터에는 영향이 없다.")]
public bool pcAggroOnHit = false;
// ── #773 : 자동 교전 (계정 Auto 설정과 무관하게 탐색 거리 안의 적을 자동 타겟) ──
[Header("■ #773 — 자동 교전 (PD 2026-09-06 '플레이어가 공격을 못함' 회귀 수정)")]
[Tooltip("켜면 계정의 자동 전투(Auto) 설정과 무관하게 탐색 거리 안의 적을 규칙 1(정면 우선 최근접)대로 자동 타겟·공격한다. " +
"규칙 4(피격 어그로 금지) 때문에 Auto OFF 계정이 교전을 시작하지 못하던 문제의 영구 수정. " +
"끄면 #770 검증 시점 동작(Auto OFF 계정은 공격 버튼/Auto ON 이 시작점)으로 돌아간다. 펫·소환수·몬스터 무관.")]
public bool autoEngage = true;
// ── 정적 접근자 (에셋 부재 시 = PD 지시대로 동작하는 값) ─────────────────
public static bool AutoEngage { get { var i = Instance; return i == null || i.autoEngage; } }
public static float FrontConeDeg { get { var i = Instance; return i != null ? i.frontConeDeg : 90f; } }
public static float FacingWeight { get { var i = Instance; return i != null ? i.facingWeight : 1f; } }
public static bool AttackStartRetarget { get { var i = Instance; return i == null || i.attackStartRetarget; } }
public static bool RetargetOnKill { get { var i = Instance; return i == null || i.retargetOnKill; } }
public static float ImmediateAttackMargin { get { var i = Instance; return i != null ? i.immediateAttackMargin : 0.2f; } }
public static float RetargetSearchRange { get { var i = Instance; return i != null ? i.retargetSearchRange : 0f; } }
public static bool PcAggroOnHit { get { var i = Instance; return i != null && i.pcAggroOnHit; } }
}
}