Project_WL/Assets/WL/Combat/Boss/WLBossArenaSettings.cs

280 lines
19 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.

// ─────────────────────────────────────────────────────────────────────────────
// WLBossArenaSettings.cs — 보스전 게이트 · 아레나 값의 단일 출처(SOT · C45)
//
// PD 지시 #813 · 발주서 WL-813d §1-4 (2026-09-09)
//
// 에셋 경로(고정): Assets/WL/Combat/Settings/Resources/WL/WLBossArenaSettings.asset
// → Resources.Load<WLBossArenaSettings>("WL/WLBossArenaSettings") 로 1회 로드 후 캐시
// (WLCombatCoreSettings·WLLootSettings 등 기존 설정 SO 관례 그대로).
//
// ■ C8 롤백 (하나라도 false 면 BossArena 는 아무 일도 하지 않는다 = 813e 상시 배치 100%)
// ① 에셋이 없다 → Active false
// ② enabled = false → Active false
// ③ RuntimeDisabled = true → Active false (진단·A/B 전용 · 도메인 리로드로 자동 해제)
// ④ WLCombatCoreSettings.Enabled = false → Active false
// 🔴 ④ 가 반드시 필요하다: 게이트는 CombatEvents.Killed/Spawned 구독으로만 열린다.
// 코어가 꺼진 채 게이트를 걸면 이벤트가 0 이라 보스가 영원히 안 나온다.
// 그래서 "코어가 꺼지면 게이트도 안 건다"(= 맵 로드 즉시 보스 배치)로 떨어뜨린다.
//
// ■ 아래 필드 기본값은 "SOT 부재 시의 실패 처리"가 아니라 에셋 생성 시 초기값이다.
// 실제로 읽히는 값은 항상 .asset 이며, 코드에는 게임 수치 상수를 두지 않는다(C45).
//
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것(FieldBossData/MobActor 가 Assembly-CSharp 에 있다).
// ─────────────────────────────────────────────────────────────────────────────
using UnityEngine;
using WL.Combat.Core;
namespace WL.Combat.Boss
{
/// <summary>보스 등장 게이트 판정 방식.</summary>
public enum eBossGateMode
{
/// <summary>게이트 존이 「동시에」 생존 0 이 되는 순간(전멸).</summary>
ZoneCleared = 0,
/// <summary>관측 존들의 누적 처치 수가 requiredKills 이상.</summary>
KillCount = 1,
/// <summary>둘 중 먼저 만족하는 쪽(권장 — 리젠형 존에서 전멸이 안 잡혀도 열린다).</summary>
Either = 2,
/// <summary>둘 다 만족해야 열린다.</summary>
Both = 3,
}
[CreateAssetMenu(fileName = "WLBossArenaSettings", menuName = "WL/Boss Arena Settings", order = 326)]
public sealed class WLBossArenaSettings : ScriptableObject
{
public const string ResourcesPath = "WL/WLBossArenaSettings";
static WLBossArenaSettings s_cached;
static bool s_lookupDone;
/// <summary>설정 에셋(1회 로드 후 캐시). 없으면 null.</summary>
public static WLBossArenaSettings Instance
{
get
{
if (!s_lookupDone)
{
s_cached = Resources.Load<WLBossArenaSettings>(ResourcesPath);
s_lookupDone = true;
}
return s_cached;
}
}
/// <summary>진단·A/B 전용 런타임 스위치. 에셋을 건드리지 않고 게이트만 끈다(도메인 리로드로 자동 false).</summary>
public static bool RuntimeDisabled;
/// <summary>게이트가 살아 있는가 = 에셋 존재 · enabled · 런타임 스위치 off · 코어 on. false 면 813e 상시 배치 그대로.</summary>
public static bool Active
{
get
{
var i = Instance;
return i != null && i.enabled && !RuntimeDisabled && WLCombatCoreSettings.Enabled;
}
}
/// <summary>테스트용: 캐시를 비워 다음 접근 때 다시 Resources.Load 한다(에셋 제거/복구 실험).</summary>
public static void ClearCache() { s_cached = null; s_lookupDone = false; }
/// <summary>
/// 813w5 프로브용 — 임시 SO 를 잠시 Instance 자리에 끼운다(C8 진리표 · **실에셋은 읽기만** 한다).
/// null 을 넣으면 캐시가 비워져 다음 접근에서 실에셋을 다시 로드한다(= ClearCache).
/// 🔴 런타임 게임 경로에서는 절대 부르지 않는다.
/// </summary>
public static void SetInstanceForProbe(WLBossArenaSettings so)
{
s_cached = so;
s_lookupDone = so != null;
}
// ─────────────────────────────────────────── 전체 스위치
[Header("전체 스위치")]
[Tooltip("끄면 보스 등장 게이트·아레나 경계·처치 후 정리가 전부 비활성 — 813e 의 「맵 로드 즉시 상시 배치」 100%. 롤백 1순위.")]
public bool enabled = true;
[Tooltip("게이트 판정·리셋·정리를 Console 에 남긴다(검증용 · 기본 off).")]
public bool verboseLog = false;
// ─────────────────────────────────────────── 대상 보스
[Header("대상 보스 (WL_Nature · Anubis)")]
[Tooltip("게이트를 걸 FieldBossData 의 n_SpawnerId. 다른 값의 FieldBossData 는 손대지 않는다(원본 즉시 배치). 0 이면 모든 FieldBossData 가 대상.")]
public int bossSpawnerId = 813010;
[Tooltip("보스의 MonsterList n_MonsterID. 추적 반경 데이터화 대상 행이다(0 이면 반경을 건드리지 않는다).")]
public int bossMonsterId = 10006;
// ─────────────────────────────────────────── 등장 게이트
[Header("등장 게이트 (존 전멸 → 보스 등장)")]
[Tooltip("끄면 게이트를 걸지 않는다 — 보스는 맵 로드 즉시 등장(813e 그대로). 아레나 경계·정리만 쓰고 싶을 때.")]
public bool gateEnabled = true;
[Tooltip("생존/처치를 세는 존 SpawnerID 전체. 누적 처치(KillCount) 판정의 모수다.")]
public int[] zoneSpawnerIds = new int[] { 813001, 813002, 813003, 813004, 813005 };
[Tooltip("「전멸」을 봐야 하는 존 SpawnerID. 이 존들이 동시에 생존 0 이 되면 게이트가 열린다(ZoneCleared).")]
public int[] gateZoneSpawnerIds = new int[] { 813004, 813005 };
[Tooltip("판정 방식. 813e 존은 리젠형(Random/Order)이라 전멸이 순간적이다 → Either 권장.")]
public eBossGateMode gateMode = eBossGateMode.Either;
[Tooltip("KillCount 방식의 임계 — zoneSpawnerIds 누적 처치 수. 813e 존 동시 생존 합 = 12+15+14+8+5 = 54.")]
public int requiredKills = 54;
[Tooltip("true 면 「한 번도 스폰된 적 없는 존」은 전멸로 치지 않는다(맵 로드 직후 alive 0 오판 방지).")]
public bool requireZoneSpawnedOnce = true;
[Tooltip("조건 충족 후 실제 등장까지의 지연(초 · unscaled). 0 이면 같은 프레임에 등장. 컷인·배너를 앞세우려면 여기서 벌린다.")]
public float gateOpenDelaySeconds = 0f;
// ─────────────────────────────────────────── 아레나
[Header("아레나 경계 (813e 실측 중심 (139, 0.8, 20) · 반경 20)")]
[Tooltip("끄면 추적 반경 데이터화·리쉬·리셋을 하지 않는다(게이트만 쓴다).")]
public bool arenaEnabled = true;
[Tooltip("true 면 아레나 중심 = FieldBossData 의 월드 좌표(813e 가 심은 그 자리). false 면 arenaCenter 를 쓴다.")]
public bool useFieldBossPositionAsCenter = true;
[Tooltip("useFieldBossPositionAsCenter = false 일 때의 아레나 중심(월드).")]
public Vector3 arenaCenter = new Vector3(139f, 0.8f, -20f);
[Tooltip("아레나 반경(m · XZ 평면). 813e 실측: 최근접 스포너까지 20.1 m.")]
public float arenaRadius = 20f;
[Tooltip("보스 등장 직전 MonsterList 행(bossMonsterId)의 f_BaseChaseRange 에 넣을 값. 0 이하면 테이블 값 그대로(원본 50).")]
public float bossChaseRange = 20f;
[Tooltip("보스 처치 후 정리 때 f_BaseChaseRange 를 원래 값으로 되돌린다(같은 세션에서 다른 맵이 같은 몬스터를 쓸 때 대비).")]
public bool restoreChaseRangeOnCleanup = true;
// ─────────────────────────────────────────── 리쉬 · 리셋
[Header("리쉬 · 리셋 (아레나 이탈)")]
[Tooltip("끄면 보스가 아레나 밖으로 나가도 되돌리지 않는다(테이블 반경만으로 제어).")]
public bool leashEnabled = true;
[Tooltip("경계 검사 주기(초 · unscaled). 0 이하면 매 프레임.")]
public float leashCheckIntervalSeconds = 0.25f;
[Tooltip("보스가 아레나 중심에서 (반경 + 이 값) 을 넘으면 복귀시킨다.")]
public float leashMargin = 2f;
[Tooltip("PC 가 아레나 밖으로 나가면 보스를 리셋한다(발주서 §1-2 「SO 옵션」).")]
public bool resetWhenPlayerLeavesArena = true;
[Tooltip("PC 이탈 판정 여유 — PC 가 (반경 + 이 값) 밖일 때만 이탈로 본다.")]
public float playerLeaveMargin = 4f;
[Tooltip("PC 이탈이 이 시간(초 · unscaled) 이상 이어져야 리셋한다. 경계에서 왔다 갔다 할 때 리셋이 튀는 것을 막는다.")]
public float playerLeaveGraceSeconds = 1.5f;
[Tooltip("리셋 때 보스를 회복시킨다. 끄면 워프·타깃 해제만 한다(회복 0).")]
public bool resetHealsBoss = true;
[Tooltip("만피 리셋(On_Regen) 회복 비율(1 = 풀피). leashHealPercent >= 1 일 때만 쓰인다.")]
public float resetHealRate = 1f;
// ── WL-813w §1-3 (Q4b D-11): 리쉬 1회당 만피 복구가 69회 = 보스 HP 가 절대 안 준다 → 부분 회복으로 완화
[Tooltip("리쉬 리셋 때의 회복량 = 최대 HP 대비 비율(Actor.Heal(rate) · 누적 회복). "
+ "🔴 1 이상이면 813d 원래 동작(On_Regen 만피 리셋 = C8 롤백). 0 이면 회복 없이 워프만. "
+ "🔴 813w2 §1-4: Q5 실측(런당 리셋 15~19회 · 회복 8~9회)에서 0.15 는 깎은 HP 를 되돌렸다 → 0.05.")]
public float leashHealPercent = 0.05f;
[Tooltip("부분 회복의 최소 간격(초 · unscaled). 이 안에 리쉬가 다시 걸리면 워프·타깃 해제만 하고 회복은 건너뛴다(최대 1회/10 s).")]
public float leashHealCooldownSeconds = 10f;
// ─────────────────────────────────────────── 보스 이름 (D-12)
[Header("보스 이름 (D-12 · HP 바 라벨)")]
[Tooltip("보스 HP 바 라벨을 MonsterList.n_MonsterName(테이블 이름 · 813tj 배너와 같은 경로)으로 쓴다. "
+ "끄면 원본 gameObject.name 그대로(= 「Anubis(Clone)」). "
+ "🔴 발주서 §2 가 UI 수정을 BossHpBar.cs 1파일로 묶어 UI 설정 SO 에 필드를 넣을 수 없어 여기 둔다(bossMonsterId 폴백과 같은 자리).")]
public bool bossNameFromTable = true;
// ─────────────────────────────────────────── 처치 후 정리
[Header("처치 후 정리")]
[Tooltip("보스 처치 후 FieldBossData 의 RegenTime 재등장 코루틴을 끊는다(발주서 §1-3 「리젠 없음」).")]
public bool suppressRegenAfterBossKill = true;
[Tooltip("처치 → 정리까지의 지연(초 · unscaled). 킬캠(811d)·폭발 드랍(813f)·디졸브가 끝난 뒤에 손대려고 벌린다. RegenTime(60) 보다 작아야 한다.")]
public float cleanupDelaySeconds = 3f;
[Tooltip("정리 때 아레나 존 스포너(zoneSpawnerIds)를 비활성화한다. 기본 off — 켜면 남은 잡몹이 자식째 사라진다.")]
public bool deactivateZonesAfterBossKill = false;
[Tooltip("정리 때 게이트를 다시 잠근다(존을 다시 밀면 보스 재등장). 기본 off = 1회성.")]
public bool rearmGateAfterBossKill = false;
// ─────────────────────────────────────────── 813w3 §1-3 잡몹 유입 차단
[Header("잡몹 유입 차단 (813w3 §1-3 · 보스 생존 중)")]
[Tooltip("보스가 살아 있는 동안 아레나 근처 존 스포너의 「사망 후 리젠」 예약(MobControlMgr 의 Set_Coroutine)을 "
+ "끊어 새 잡몹이 아레나로 흘러드는 것을 막는다. 이미 살아 있는 잡몹은 원본 규칙 그대로 둔다. "
+ "끄면 813d 그대로(존 스포너 무간섭).")]
public bool holdZoneSpawnNearArena = true;
[Tooltip("차단 대상 = 아레나 중심에서 (arenaRadius + 이 값) 안에 있는 존 스포너(XZ). "
+ "813e 실측 배치에서 813001(24.1 m)·813004(20.1 m)가 여기에 든다.")]
public float zoneSpawnHoldMargin = 10f;
// ─────────────────────────────────────────── 813w3 §1-4 다음 런 리셋(D-27)
[Header("다음 런 리셋 (813w3 · D-27 게이트 조기 개방 · 보스 미등장)")]
[Tooltip("🔴 ResetGate() 때 존 누적 처치(TotalZoneKills)·스폰 수를 「이 런 기준」으로 다시 스냅샷한다. "
+ "끄면 813p 그대로 = 지난 런의 누적 121 킬이 남아 두 번째 런 첫 처치에서 게이트가 즉시 열린다(D-27).")]
public bool restartRebasesGateKills = true;
[Tooltip("🔴 ResetGate() 때 보스 스폰 보류(HoldSpawn)를 다시 무장한다(released 플래그 원복). "
+ "끄면 813p 그대로 = Release() 가 보류 목록을 전부 건너뛰어 보스가 영영 안 나온다(D-27).")]
public bool restartRearmsHoldSpawn = true;
[Tooltip("ResetGate() 때 살아 있는 보스를 아레나 중심으로 워프 · HP 만피 · 타깃 해제한다(리쉬 리셋 1회). "
+ "이때 BossSpawned 를 유지해 두 번째 런의 게이트 개방에서 BossStarted 가 다시 난다(HP 바 재바인딩).")]
public bool restartResetsLivingBoss = true;
[Tooltip("죽은 보스는 게이트가 열릴 때 원본 리스폰 규칙(FieldBossData.AfterDie 의 MobActor.On_Regen())을 "
+ "그대로 한 번 태워 되살린다 — FieldBossData.Start 재진입(클론 2개)이 아니다. 끄면 되살리지 않는다.")]
public bool restartRegensDeadBoss = true;
[Tooltip("🔴 런이 진행 중이 아니면(RunEnded 뒤 · 결과창) 리쉬 판정·게이트 평가를 멈춘다. "
+ "끄면 813d 그대로 = 런 밖에서도 리쉬 리셋이 계속 쌓여 보스 HP 가 되돌아온다(D-27 실측 68 → 190). "
+ "런 축(WLRunSettings)이 꺼져 있거나 런이 한 번도 시작되지 않았으면 이 스위치와 무관하게 813d 그대로 돈다.")]
public bool suspendOutsideRun = true;
// ─────────────────────────────────────────── 813w5 §1-2 다음 런 앞판 보스 정리(D-39)
[Header("다음 런 앞판 보스 정리 (813w5 · D-39 보스가 존0 까지 추격)")]
[Tooltip("🔴 RunEnded(결과창) 순간에도 살아 있는 보스를 아레나 중심으로 되돌린다(워프·만피·타깃 해제·홀드). "
+ "끄면 813w3 그대로 = [다음 런] 1 (D-39).")]
public bool parkBossOnRunEnd = true;
[Tooltip("🔴 보스를 되돌린 뒤 이 시간(초 · unscaled) 동안 이동·타깃을 금지한다(순찰 대기 고정 + NavMesh 정지). "
+ "0 이면 홀드 없음 = 813w3 그대로. 원본 StopMove_Imm 은 protected 라 같은 본체를 공개 Get_Agent() 로 재현한다.")]
public float nextRunBossHoldSeconds = 30f;
[Tooltip("홀드 중 보스가 아레나 중심에서 이 거리(m · XZ)를 넘으면 중심으로 다시 워프한다. 0 이하면 매 틱 워프.")]
public float nextRunBossHoldRewarpMargin = 1.5f;
[Tooltip("🔴 「다음 런」 리셋에서 만피(On_Regen)보다 워프를 먼저 한다. "
+ "원본 MobActor.On_Regen 은 그 안에서 Check_Battle() → Change_MobStatus(Battle) → FindNextTarget(10000f) 로 "
+ "사실상 무한 반경 재타깃을 하므로, 워프가 나중이면 재아그로가 「런이 끝난 자리(= PC 옆)」에서 일어난다(D-39 원인 ①). "
+ "끄면 813w3 순서 그대로(워프가 나중).")]
public bool nextRunWarpBeforeHeal = true;
[Tooltip("보스를 되돌릴 때 PC 쪽 락타깃(813w Actor.m_IsLockTarget)도 풀고, PC 가 그 보스를 타깃 중이면 해제한다. "
+ "끄면 보스 쪽 타깃만 해제한다(813w3 그대로).")]
public bool nextRunClearsPlayerTarget = true;
[Tooltip("🔴 런 밖(결과창·다음 런 대기)에서도 「거리 판정만」 유지한다 — 아레나를 벗어나면 회복 없이 중심으로 되돌린다. "
+ "끄면 813w3 그대로 = 런 밖에서는 리쉬가 아예 돌지 않아 보스가 사냥터까지 걸어 나간다(D-39). "
+ "회복은 하지 않으므로 D-27 ③(런 밖 회복으로 HP 가 되돌아옴)은 재발하지 않는다.")]
public bool leashOutsideRunDistanceOnly = true;
// ─────────────────────────────────────────── 813w5 §1-2 게이트 조기 개방 방지(D-39)
[Header("게이트 조기 개방 방지 (813w5 · D-39 t≈19.6 개방)")]
[Tooltip("🔴 런 시작 뒤 이 시간(초) 전에는 보스 게이트를 열지 않는다(보스 교전·이미 등장 경로). "
+ "0 이면 무제한 = 813p/813w3 그대로. 런 축이 꺼졌거나 런이 한 번도 시작되지 않았으면 무동작.")]
public float gateMinSeconds = 60f;
[Tooltip("위 시간 전이라도 「진짜 존 전멸」(RunGateReason.ZonesCleared)이면 게이트를 연다. "
+ "끄면 존을 다 밀어도 gateMinSeconds 전에는 안 열린다.")]
public bool gateMinAllowsZonesCleared = true;
}
}