Project_WL/Assets/WL/Combat/Survival/WLSurvivalSettings.cs

187 lines
13 KiB
C#

// ─────────────────────────────────────────────────────────────────────────────
// WLSurvivalSettings.cs — "위험과 회복"(무적 해제 게이트 · 받는 피해 배율 · 물약 · 사망/부활) 값의 단일 출처(SOT · C45)
//
// PD 지시 #813 · 발주서 WL-813i §1-4 · 기준서 v1 §B 요소 8 · §D-1 813i 행 · §F-2 (2026-09-09)
//
// 에셋 경로(고정): Assets/WL/Combat/Settings/Resources/WL/WLSurvivalSettings.asset
// → Resources.Load<WLSurvivalSettings>("WL/WLSurvivalSettings") 로 1회 로드 후 캐시(코어 설정 SO 와 같은 관례).
// 에셋이 없거나 enabled = false 면 Enabled == false → Survival · PotionUse · DeathFlow 가 아무 것도 하지 않는다
// = 원본 + 기존 WL 동작 100%(플레이어 무적 유지 · 물약 없음 · After_Die 3 s 자동 부활). C8 롤백 1순위 스위치.
//
// 🔴 기본 enabled = false — 813j(물약 버튼 · 부활 팝업 UI)와 **동시 병합**할 때 Lead 가 켠다(기준서 §F-2).
// WLGameplaySettings.playerInvincible 은 손대지 않는다 — 이 SO 가 켜지면 Survival.TakeDamage 가 피격 1회 동안만
// 그 플래그를 내렸다가 되돌린다(디스크 값 무변경).
//
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것(PCActor 가 Assembly-CSharp 에 있다).
// ─────────────────────────────────────────────────────────────────────────────
using UnityEngine;
namespace WL.Combat.Survival
{
[CreateAssetMenu(fileName = "WLSurvivalSettings", menuName = "WL/Survival Settings", order = 325)]
public sealed class WLSurvivalSettings : ScriptableObject
{
public const string ResourcesPath = "WL/WLSurvivalSettings";
static WLSurvivalSettings s_cached;
static bool s_lookupDone;
/// <summary>설정 에셋(1회 로드 후 캐시). 없으면 null.</summary>
public static WLSurvivalSettings Instance
{
get
{
if (!s_lookupDone)
{
s_cached = Resources.Load<WLSurvivalSettings>(ResourcesPath);
s_lookupDone = true;
}
return s_cached;
}
}
/// <summary>
/// QA·A/B 전용 런타임 스위치(에셋 무수정 · Play 종료 시 도메인 리로드로 자동 0).
/// 0 = 에셋의 enabled 를 따른다 · 1 = 에셋이 꺼져 있어도 켠다(813j 병합 전 QA) · -1 = 강제 off.
/// </summary>
public static int RuntimeOverride;
/// <summary>생존 축이 살아 있는가 = 에셋 존재 · (enabled 또는 강제 on) · 강제 off 아님. false 면 모든 경로가 원본 동작이다.</summary>
public static bool Enabled
{
get
{
var i = Instance;
if (i == null || RuntimeOverride < 0) return false;
return RuntimeOverride > 0 || i.enabled;
}
}
/// <summary>테스트용: 캐시를 비워 다음 접근 때 다시 Resources.Load 한다.</summary>
public static void ClearCache() { s_cached = null; s_lookupDone = false; }
// ─────────────────────────────────────────── 전체 스위치
[Header("전체 스위치")]
[Tooltip("끄면 무적 해제 · 물약 · 사망/부활 흐름이 전부 비활성 — 플레이어 무적(WLGameplaySettings.playerInvincible) 그대로 · 원본 After_Die 3 s 자동 부활 그대로. 롤백 1순위. 813j UI 와 동시 병합 때 Lead 가 켠다.")]
public bool enabled = false;
[Tooltip("피해 배율 · 물약 · 사망/부활 결정을 Console 에 남긴다(검증용 · 기본 off · 문자열 GC 발생).")]
public bool verboseLog = false;
// ─────────────────────────────────────────── 받는 피해
[Header("받는 피해 (기준서 §B 요소 8 위험 · §F-2 · 잡몹/보스 분리)")]
[Tooltip("잡몹(Boss 가 아닌 가해자 · 정예 포함)의 피해를 받을지. 끄면 잡몹에게는 원본 무적 그대로.")]
public bool takeMobDamage = true;
[Tooltip("잡몹 피해에 곱하는 받는 피해 배율. 원본 Actor.Get_Damage(방어 공식) 진입 **전** 원 피해에 곱한다. 기준서 제안 0.5 로 시작.")]
public float mobDamageMultiplier = 0.5f;
[Tooltip("보스(eSubRol.Boss 가해자)의 피해를 받을지. 끄면 보스에게는 원본 무적 그대로.")]
public bool takeBossDamage = true;
[Tooltip("보스 피해에 곱하는 받는 피해 배율. 기준서 제안 0.5 로 시작.")]
public float bossDamageMultiplier = 0.5f;
[Tooltip("켜면 정예(eSubRol.Elite) 가해자를 보스 배율·스위치로 다룬다. 기본은 잡몹 취급.")]
public bool eliteCountsAsBoss = false;
// ─────────────────────────────────────────── 물약
[Header("물약 (기준서 §B 요소 8 회복 · 3개/런 · 40 % · 쿨 5 s)")]
[Tooltip("런(맵 진입)당 물약 개수.")]
public int potionCountPerRun = 3;
[Tooltip("1회 회복량(최대 HP 비율 · Actor.Heal(rate)). 0.4 = 40 %.")]
public float potionHealRate = 0.4f;
[Tooltip("사용 후 재사용 대기(초 · 게임 시간 = 일시정지 중 정지).")]
public float potionCooldownSeconds = 5f;
[Tooltip("회복 수치를 원본 데미지 텍스트(HUDDMGUI · 초록)로 띄운다.")]
public bool potionShowHealNumber = true;
[Tooltip("HP 가 가득 차 있으면 사용을 거절한다(개수 낭비 방지 · Actor.Heal 도 만충이면 아무 것도 안 한다).")]
public bool potionRefuseAtFullHp = true;
[Tooltip("Stage/Dungeon 맵에 들어갈 때(맵 ID·게임 모드 변화) 물약 개수를 런 시작값으로 되돌린다. 끄면 PotionUse.ResetRun() 호출(813m 런 시작)로만 리셋된다.")]
public bool potionResetOnMapLoad = true;
// ─────────────────────────────────────────── 자동 물약 (Q5 D-21 · WL-813m2)
// 실측 근거: Q5 결과 화면 「사망 / 물약 = 3 / 0」 — 자동전투(813x)로 도는 런에서는 플레이어가
// 물약 버튼(813j/813y)을 누르지 않아 런당 3개가 통째로 남은 채 3번 죽는다.
[Header("자동 물약 (Q5 D-21 · 자동전투 중 HP 임계 이하 자동 사용 · WL-813m2)")]
[Tooltip("자동전투 중 HP 가 임계 이하로 떨어지면 물약을 자동으로 쓴다. C8 롤백 = 0(코드 무수정 · 물약은 버튼으로만).")]
public bool autoPotionEnabled = true;
[Tooltip("자동 사용 임계(최대 HP 대비 비율). 0.35 = 35 % 이하. 회복량(potionHealRate)과 합이 1 을 넘지 않아야 회복이 낭비되지 않는다(0.35 + 0.4 = 0.75 → 낭비 0).")]
[Range(0.05f, 0.9f)]
public float autoPotionHpRatio = 0.35f;
[Tooltip("자동전투(eCommon.Auto)일 때만 자동 사용한다. 끄면 수동 전투에서도 자동으로 쓴다(플레이어의 물약 타이밍을 뺏는다).")]
public bool autoPotionRequireAutoCombat = true;
[Tooltip("HP 를 확인하는 주기(초 · 게임 시간). 짧을수록 반응이 빠르고 폴링 비용이 는다. 0.02 미만은 0.02 로 잘린다.")]
public float autoPotionCheckSeconds = 0.25f;
[Tooltip("자동 사용 때 Console 에 1줄 남긴다(런당 최대 potionCountPerRun 회 · verboseLog 와 별개로 켤 수 있다).")]
public bool autoPotionLog = true;
// ─────────────────────────────────────────── 사망 · 부활
[Header("사망 · 부활 (기준서 사망 연출 2 s → 부활 요청 → 존 시작점 On_Regen · 부활 후 무적 1.5 s)")]
[Tooltip("사망 연출 길이(초 · unscaled). 이 시간이 지나면 ReviveRequested 를 낸다(813j 팝업).")]
public float deathDisplaySeconds = 2f;
[Tooltip("부활 요청을 낸 뒤 응답까지 Time.timeScale = 0 으로 세계를 세운다(원본 PauseUI 선례). 원본 After_Die(사망 3 s 뒤 자동 부활 · 코루틴 · 게임 시간)를 붙잡아 두는 수단이기도 하다 — 끄면 팝업 응답이 3 s 를 넘길 때 원본이 먼저 부활시킨다(external 부활로 통보).")]
public bool holdWorldWhileReviving = true;
[Tooltip("ReviveRequested 구독자가 0 이면(813j 미병합 · QA) 요청 즉시 자동 부활한다. 끄면 구독자가 없어도 기다린다(autoReviveTimeoutSeconds 참고).")]
public bool autoReviveWhenNoListener = true;
[Tooltip("부활 요청 뒤 이 시간(초 · unscaled) 안에 응답이 없으면 자동 부활. 0 이면 무제한 대기.")]
public float autoReviveTimeoutSeconds = 0f;
[Tooltip("부활 직후 무적 시간(초 · 게임 시간). 이 동안 PC 피격은 원본 무적 경로(피해 0 · Damaged(invincible=true) 발생).")]
public float reviveInvincibleSeconds = 1.5f;
[Tooltip("부활 시 회복 비율(On_Regen healrate · 1 = 전회복 · 원본 After_Die 와 동일).")]
public float reviveHealRate = 1f;
[Tooltip("부활 시 소환수·펫·일꾼도 On_Regen 한다(원본 After_Die 의 PC 분기와 동일).")]
public bool reviveCompanions = true;
// ─────────────────────────────────────────── 사망 중 영구 정지 방지 (Q4-a D-3 · WL-813z)
// 실측 근거(2026-09-09 · Popup.cs 전문 54줄 · ErrorLogHookManager.cs:33)
// 공용 Popup 은 노드 1개짜리 싱글턴이고 Set() 이 m_ok 를 **덮어쓴다**.
// ErrorLogHookManager 가 Error/Exception 마다 Popup.Ins.Set(One, 174, null, null, log) 을 부르면
// 부활 팝업이 넘긴 확인 콜백(m_ok)이 null 로 지워진다 → 확인을 눌러도 부활이 일어나지 않고
// holdWorldWhileReviving 이 세운 timeScale=0 이 영영 안 풀린다(Q4-a 실측 2분 30초).
// → 아래 워치독이 **마지막 방어선**이다. autoReviveTimeoutSeconds 는 정상 흐름의 응답 상한이고,
// 이 값은 "무슨 일이 있어도 세계는 다시 흐른다"를 보장하는 안전장치라 별도 필드로 둔다.
[Header("사망 중 영구 정지 방지 (Q4-a D-3 · WL-813z)")]
[Tooltip("AwaitingRevive(부활 팝업 대기)가 이 시간(초 · unscaled)을 넘기면 자동 부활하고 세계 정지를 푼다. 0 = 워치독 없음(C8 롤백 = 0).")]
public float reviveWatchdogSeconds = 20f;
[Tooltip("켜면 이 SO 의 enabled·RuntimeOverride 가 꺼져도 워치독만은 계속 돈다(이미 세계를 세워 둔 상태에서 스위치가 꺼져 잠기는 것을 막는다).")]
public bool reviveWatchdogIgnoresDisable = true;
[Tooltip("워치독이 발동하면 Console 에 Log 1줄을 남긴다(Warning/Error 가 아니라 ErrorLogHookManager 모달을 부르지 않는다).")]
public bool reviveWatchdogLog = true;
// ─────────────────────────────────────────── 부활 팝업 최상단 보장 (UI · ReviveDialog 가 읽는다)
// 🔴 UI 값이지만 이번 발주(WL-813z §2)가 허용한 UI 파일 목록에 WLSurvivalUiSettings.cs 가 없어
// 813q 와의 충돌 0 을 지키려고 사망/부활 소유 SO 인 여기에 둔다(Lead 판단으로 이관 가능).
[Header("부활 팝업 최상단 보장 (D-3 · ReviveDialog 가 읽는다 · WL-813z)")]
[Tooltip("끄면 최상단 보장·재보장·승격을 전부 하지 않는다(C8 롤백 = 0 · 813y 동작 100%).")]
public bool reviveTopmostEnabled = true;
[Tooltip("부활 팝업 전용 캔버스의 sortingOrder. 공용 팝업 프리팹 SortOrder_5 의 Canvas.m_SortingOrder = 5 (실측) 보다 커야 한다.")]
public int reviveSortingOrder = 60;
[Tooltip("표시 중 최상단·레이캐스트를 다시 보장하는 주기(초 · unscaled · 0 이면 표시 순간 1회만). GC 0 경로.")]
public float reviveTopmostIntervalSeconds = 0.5f;
[Tooltip("공용 Popup 이 다른 팝업(에러 모달 등)에 덮인 것을 감지하면 전용 폴백 팝업으로 승격해 확인 버튼을 되살린다.")]
public bool reviveEscalateOnHijack = true;
}
}