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

240 lines
18 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.

// ─────────────────────────────────────────────────────────────────────────────
// 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;
// ─────────────────────────────────────────── 보스 연타 대비 피격 후 무적 (Q10 D-21 · WL-813i2)
// 🔴 실측 근거 = qa/W2-10_재채점_v8.md §6(`death_*.txt` 14건 전수)
// · 사망 14/14 가 전부 `boss/813010 Anubis(Clone)` — 마지막 5~6행이 전부 보스 타격
// · 보스 1타 원 피해 133.8~547.2 → ×0.15 를 거쳐도 **PC 최대 HP(310)의 6~26 %**
// · 타격 간격 🔴 **0.52 s 등간격** · 🔴 **5대 / 약 2.6 초에 사망**(122→92→63→34→4→사망)
// · 도트(Unscaled) 기여 **0**(14건 전부 0건 0 HP) — 813m3 가설은 기각됐다
// → 배율(F-17 로 0.15→0.1)만으로는 「한 방에 26 %」가 안 바뀐다. 연타의 **횟수**를 줄이는 축이 필요하다.
// 0.6 s 창이면 0.52 s 간격 5연타에서 1·3·5 타만 들어온다(3/5) → 창을 0.6 s 로 두면 실측 2~3타.
// 창 안의 피격은 **부활 무적 창과 같은 원본 경로**(playerInvincible 유지 → base 가 피해를 건너뛰고
// RaiseDamaged(invincible=true) 만 낸다)로 흘려보낸다 → 피격 비네트(WLSurvivalUiSettings
// `ignoreInvincibleHits=1`)·타격 반응(WLImpactTierSettings `pcHitSkipWhenInvincible=1`)이
// 스스로 걸러 준다 = **UI 무수정으로 연출 1회만**.
[Header("보스 연타 대비 피격 후 무적 (Q10 D-21 · WL-813i2)")]
[Tooltip("피해를 실제로 입은 직후 짧은 무적 창을 연다(보스 연타 완화). C8 롤백 = 0(813i 동작 100%).")]
public bool hitInvincibleEnabled = true;
[Tooltip("보스 피격으로 HP 가 실제로 깎인 직후 무적 창 길이(초 · 게임 시간). "
+ "0 이면 창 없음 = C8. 실측 보스 타격 간격 0.52 s 기준 0.6 이면 연타 5타 중 2~3타만 들어온다.")]
public float bossHitInvincibleSeconds = 0.6f;
[Tooltip("잡몹 피격에도 같은 창을 여는가. 실측상 잡몹은 한 방에 0.3~0.4 % 라 기본은 끈다(0) — "
+ "켜면 잡몹 밭에서도 무적이 상시로 켜져 난이도가 통째로 사라진다.")]
public bool mobHitInvincible = false;
[Tooltip("잡몹 피격 후 무적 창 길이(초 · mobHitInvincible 가 켜졌을 때만).")]
public float mobHitInvincibleSeconds = 0.2f;
[Tooltip("창을 여는 최소 실피해(HP). 이보다 적게 깎였으면 창을 열지 않는다(스침 타격으로 창이 열려 "
+ "보스 본타를 통째로 흘리는 것을 막는다). 0 이면 피해가 조금이라도 있으면 연다.")]
public float hitInvincibleMinHpLoss = 1f;
[Tooltip("창을 열 때 Console 에 1줄 남긴다(verboseLog 와 별개 · 창 1개당 1줄).")]
public bool hitInvincibleLog = 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;
// ─────────────────────────────────────────── 사망 원인 진단 · 배율 미적용 경로 가드 (Q9 D-21 · WL-813m3)
// 실측 근거(2026-09-09 · 원본 읽기만): 813i 게이트는 PCActor.Get_Damage 한 곳뿐인데
// Actor.Co_DotDamage:1536(독/화상/출혈) · Check_CC_byAttack:1116(스턴 추가타) ·
// Check_Extra:1191·1197·1202·1222·1229 · Check_EnchantOption:1402·1412·1453 가
// **Get_Damage 를 거치지 않고 Cal_Damage 로 곧장 HP 를 깎는다**(배율 0 · 방어 공식 0 · PC 무적조차 무시).
// → Q9 D-21 "배율을 0.1/0.15 로 내려도 사망이 안 줄었다" 와 일치한다.
[Header("사망 원인 진단 · 배율 미적용 경로 가드 (Q9 D-21 · WL-813m3)")]
[Tooltip("PC 피해를 링버퍼 16칸에 기록하고 사망마다 파일 1개 + 콘솔 1줄을 남긴다(verboseLog 와 무관). C8 롤백 = 0.")]
public bool deathDiagEnabled = true;
[Tooltip("사망 로그 폴더(프로젝트 루트 기준 · Assets 밖 · 미추적). 빌드에서는 persistentDataPath 아래.")]
public string deathDiagDir = "Screenshots_WL/WL813m3";
[Tooltip("훅(PCActor.Get_Damage)을 거치지 않고 깎인 HP 를 매 프레임 정산해 같은 배율만큼 되돌린다(도트·추가타·인챈트 경로). 배율은 mobDamageMultiplier 를 재사용한다. C8 롤백 = 0.")]
public bool unscaledDamageGuardEnabled = true;
[Tooltip("가드가 배율 미적용 피해를 잡을 때마다 Console 에 1줄 남긴다(도트는 초당 1~2건이라 기본 off · 사망 파일에는 항상 남는다).")]
public bool unscaledDamageGuardLog = false;
// ─────────────────────────────────────────── 사망 · 부활
[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;
}
}