280 lines
19 KiB
C#
280 lines
19 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 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;
|
||
}
|
||
}
|