158 lines
11 KiB
C#
158 lines
11 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;
|
|
|
|
// ── #813t3 : 시야(탐색) · 추적(이동) 반경 ────────────────────────────────
|
|
[Header("■ #813t3 — 시야·추적 반경 (PD 2026-09-11 「반경 3 m 로 제한」)")]
|
|
[Tooltip("추적(이동) 반경(m). 이미 잡은 타깃이 이 거리를 넘어가면 놓는다 = 「3 m 밖 적은 무시」. " +
|
|
"위 retargetSearchRange(탐색)는 '새로 고르는' 거리이고 이 값은 '계속 쫓는' 거리다 — " +
|
|
"탐색만 줄이면 한 번 잡은 적은 죽을 때까지 따라간다(PD 관측 「한참 동안 적을 추적」). " +
|
|
"0 이면 놓지 않는다(813t3 이전 동작). 보통 retargetSearchRange 와 같은 값으로 둔다.")]
|
|
[Min(0f)] public float chaseRange = 0f;
|
|
|
|
[Tooltip("타깃을 놓을 때 원본 「락 타깃」(Actor.m_IsLockTarget)까지 무시할지. " +
|
|
"꺼 두면 813w 가이드가 보스에 건 락은 유지된다(= 락이 걸린 보스는 반경 밖이어도 계속 쫓는다).")]
|
|
public bool chaseDropIgnoresLock = false;
|
|
|
|
[Tooltip("타깃을 놓는 순간 달려가던 NavMesh 경로도 끊고 Idle 로 세울지. " +
|
|
"🔴 조이스틱 조작 중이거나 813w 가이드가 이동을 쥐고 있을 때는 건드리지 않는다(둘이 싸우지 않게).")]
|
|
public bool stopChaseMoveOnDrop = true;
|
|
|
|
[Tooltip("시야 가드 폴링 간격(초).")]
|
|
[Min(0.02f)] public float chaseCheckSeconds = 0.1f;
|
|
|
|
[Tooltip("시야 가드가 타깃을 놓을 때 로그 1줄.")]
|
|
public bool chaseVerboseLog = false;
|
|
|
|
// ── 정적 접근자 (에셋 부재 시 = 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; } }
|
|
|
|
// #813t3 — 0 = 기존 동작(놓지 않음). 값은 에셋(SO)이 갖는다.
|
|
public static float ChaseRange { get { var i = Instance; return i != null ? i.chaseRange : 0f; } }
|
|
public static bool ChaseDropIgnoresLock { get { var i = Instance; return i != null && i.chaseDropIgnoresLock; } }
|
|
public static bool StopChaseMoveOnDrop { get { var i = Instance; return i == null || i.stopChaseMoveOnDrop; } }
|
|
public static float ChaseCheckSeconds { get { var i = Instance; return i != null ? i.chaseCheckSeconds : 0.1f; } }
|
|
public static bool ChaseVerboseLog { get { var i = Instance; return i != null && i.chaseVerboseLog; } }
|
|
|
|
/// <summary>
|
|
/// 프로브 전용(813w5 `WLBossArenaSettings.SetInstanceForProbe` 선례) — 임시 SO 를 잠시 Instance 자리에 끼운다.
|
|
/// C8 진리표를 실 에셋을 건드리지 않고 검증하기 위한 것. null 을 주면 캐시를 비우고 실 에셋으로 돌아간다.
|
|
/// </summary>
|
|
public static void SetInstanceForProbe(WLTargetingSettings so)
|
|
{
|
|
if (so == null) { ClearCache(); return; }
|
|
s_cached = so;
|
|
s_lookupDone = true;
|
|
}
|
|
}
|
|
}
|