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

188 lines
10 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.

// ─────────────────────────────────────────────────────────────────────────────
// DamageGuard.cs — "배율을 타지 않는 PC 피해 경로" 에도 같은 배율을 먹인다(원본 훅 0 · 폴링 + 환급)
//
// PD 지시 #813 · 발주서 WL-813m3 §1-3 · 근거 = Q9 D-21(피해 배율을 0.15→0.1 · 0.25→0.15 로 내려도 사망이 안 줄었다)
//
// ■ 왜 필요한가 — 코드 실측(원본 읽기만 · Assets/Script 0줄 수정)
// 813i 게이트는 `PCActor.Get_Damage` **한 곳**에만 있다(Survival.TakeDamage). 그런데 원본에는
// `Get_Damage` 를 **거치지 않고 곧장 `Actor.Cal_Damage(dinfo)` 로 HP 를 깎는** 경로가 있다:
// · `Actor.Co_DotDamage :1536` — 독/화상/출혈 도트. 매 틱 `Beater.Get_DamageInfo()` 로 **새 피해를 다시 만들어**
// `Cal_Damage` 를 직접 부른다 → 배율 0 · 방어 공식 0 · **원본 무적 플래그도 무시**된다.
// · `Actor.Check_CC_byAttack :1116`(스턴 추가타) · `Check_Extra :1191·1197·1202·1222·1229`(빙결/둔화/스턴 추가 피해)
// — 이쪽은 이미 배율이 곱해진 `_dinfo.Damage` 를 다시 쓰므로 **간접 적용**(2차 타격이라 총량은 2배가 된다).
// · `Check_EnchantOption :1402(LUK 저주) · :1412(STR 스플래시 · allies[k].Cal_Damage) · :1453(LUK 크리)`
// — 가해자 스탯에서 **피해를 새로 계산**하므로 배율 0.
// `Cal_Damage` 안에는 `IsDead()` 와 `NoDmgTime`(몹 전용 · PC 는 0) 말고 어떤 가드도 없다 — PC 무적조차 없다.
//
// ■ 어떻게 훅 0 으로 막나 — HP 델타 정산(폴링) + 환급
// ① `Survival.TakeDamage` 가 base 를 부를 때마다 **호출 전후 HP 차이**를 `Survival.HookHpLoss` 에 누계한다
// (중첩 `Cal_Damage` 까지 포함한 "훅이 책임지는 감소량" 실측치).
// ② 이 가드는 매 프레임(Update + **LateUpdate**) 메인 PC 의 HP 를 본다.
// `실제 감소량 훅 누계 증가분` = **훅을 안 거친 감소량**(= 배율 미적용 경로).
// ③ 그 차액에 같은 배율을 적용한 만큼을 되돌린다 — `Heal(차액 × (1 배율), showhud:false)`.
// 결과적으로 그 경로의 순 피해도 `차액 × 배율` 이 된다.
// · LateUpdate 를 함께 도는 이유: `WaitForSeconds` 코루틴(도트 틱)은 Update **뒤**에 재개된다 —
// LateUpdate 폴링이면 **같은 프레임 안**에 정산된다(지연 ≈ 0).
// · 🔴 한계(보고서 「미확인」): 한 틱이 곧바로 치명타여서 `Cal_Damage` 안에서 `Set_Die` 가 먼저 나면
// 환급이 늦다. 이 가드는 **누적 감소 속도**를 배율만큼 낮춰 그 지점에 도달하지 않게 하는 것이 목적이다.
//
// ■ 값은 전부 SO — 배율은 `WLSurvivalSettings.mobDamageMultiplier` **기존 필드 재사용**(발주서 §1-3).
// 가해자를 알 수 없는 폴링이라 잡몹 배율을 쓴다(보스 배율보다 낮아 생존에 보수적) · 스위치 `unscaledDamageGuardEnabled`.
//
// ■ C8 롤백: `unscaledDamageGuardEnabled = 0` 또는 SO off → 폴링 자체를 하지 않는다(코드 무수정).
// ■ GC 0: 문자열·할당 0(로그는 SO 스위치가 켜졌을 때 · 사건당 1줄).
// 🔴 Debug.LogError/LogException 금지(813z).
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것.
// ─────────────────────────────────────────────────────────────────────────────
using UnityEngine;
namespace WL.Combat.Survival
{
/// <summary>훅을 거치지 않은 PC HP 감소를 같은 배율로 되돌린다. 정적 · GC 0.</summary>
public static class DamageGuard
{
/// <summary>이 값보다 작은 감소는 무시한다(부동소수 잡음 · 구조 상수).</summary>
const double Epsilon = 0.5d;
// ── 진단(프로브가 읽는다)
/// <summary>훅을 안 거친 감소를 잡은 횟수.</summary>
public static int DetectedCount;
/// <summary>그 경로로 깎인 HP 누계(환급 전).</summary>
public static double DetectedHp;
/// <summary>실제로 환급(Heal)한 횟수 · HP 누계.</summary>
public static int RefundCount;
public static double RefundedHp;
/// <summary>죽어 있어서 환급하지 못한 횟수.</summary>
public static int SkippedDead;
/// <summary>폴링 횟수 · 기준선 재설정 횟수.</summary>
public static int PollCount, RebaseCount;
public static double LastDetected, LastRefund;
public static float LastDetectTime;
static PCActor s_pc;
static double s_lastHp;
static double s_lastHookLoss;
static bool s_armed;
static WLSurvivalSettings St { get { return WLSurvivalSettings.Instance; } }
/// <summary>가드가 실제로 도는가 = 생존 축 on + 이 스위치 on.</summary>
public static bool Active
{
get { var st = St; return WLSurvivalSettings.Enabled && st != null && st.unscaledDamageGuardEnabled; }
}
/// <summary>폴링에 쓰는 배율(잡몹 배율 재사용 · 0~1 로 자른다).</summary>
public static double Multiplier
{
get
{
var st = St;
if (st == null) return 1d;
double m = st.mobDamageMultiplier;
if (m < 0d) m = 0d;
if (m > 1d) m = 1d;
return m;
}
}
/// <summary>기준선을 지금 HP 로 다시 잡는다(런 시작 · 부활 · PC 교체 · 회복).</summary>
public static void Rebase()
{
s_armed = false; RebaseCount++;
}
/// <summary>러너가 Update · LateUpdate 에서 부른다. 비활성이면 즉시 반환(C8).</summary>
public static void Poll()
{
var st = St;
if (st == null || !WLSurvivalSettings.Enabled || !st.unscaledDamageGuardEnabled) { s_armed = false; return; }
var pc = MyValue.MyPC;
if (pc == null) { s_armed = false; s_pc = null; return; }
if (!ReferenceEquals(pc, s_pc)) { s_pc = pc; s_armed = false; }
PollCount++;
double hp = pc.Get_HP();
double hookLoss = Survival.HookHpLoss;
if (!s_armed)
{
s_lastHp = hp; s_lastHookLoss = hookLoss; s_armed = true;
return;
}
double drop = s_lastHp - hp; // > 0 = 잃었다
double accounted = hookLoss - s_lastHookLoss; // 훅이 책임지는 감소량
if (accounted < 0d) accounted = 0d;
if (drop <= Epsilon) // 회복했거나 변화 없음 → 기준선만 갱신
{
s_lastHp = hp; s_lastHookLoss = hookLoss;
return;
}
double unscaled = drop - accounted;
if (unscaled <= Epsilon) // 전부 훅을 거쳤다 = 정상
{
s_lastHp = hp; s_lastHookLoss = hookLoss;
return;
}
// ── 배율 미적용 경로를 잡았다
DetectedCount++; DetectedHp += unscaled;
LastDetected = unscaled; LastDetectTime = Application.isPlaying ? Time.time : Survival.Now;
// 🔴 WL-813w8 (Q11 D-46) — 결과창 정지 무적 중에는 **전액 환급**한다(배율 0).
// 훅 경로는 Survival 이 피해 0 으로 만들지만, 이 폴링이 잡는 경로(도트 · Cal_Damage 직접 호출)는
// 원본 무적 플래그조차 거치지 않는다 — 정지 중 「피해 0」을 이 축에서도 지킨다.
// 래치가 없으면(기본 · 런 중) 종전 그대로 잡몹 배율을 쓴다(C8).
double mult = Survival.IsHoldInvincible(pc) ? 0d : Multiplier;
double refund = unscaled * (1d - mult);
double maxHp = pc.Get_MaxHP();
if (pc.IsOnlyDead() || hp <= 0d)
{
SkippedDead++; LastRefund = 0d; // 이미 죽었다 — Heal 은 IsDead 로 막힌다
}
else if (refund >= 1d) // Actor.Heal 은 1 미만도 1 로 올린다 → 과환급 방지
{
// Actor.Heal 은 마지막에 HUD(Get_HUDUI)를 건드린다 — 인게임 밖(프로브·전환 중)에서는 그 줄이 던질 수 있다.
// HP 대입은 그보다 앞이므로 실제로 오른 만큼을 실측해 센다(예외가 나도 전투 흐름을 끊지 않는다).
try { pc.Heal(refund, false); }
catch (System.Exception) { } // 🔴 LogError 금지(813z)
double healed = pc.Get_HP() - hp;
if (healed > 0d) { RefundCount++; RefundedHp += healed; }
LastRefund = healed > 0d ? healed : 0d;
}
else LastRefund = 0d;
double hpAfter = pc.Get_HP();
DeathDiag.Record(DamageVia.Unscaled, null, unscaled, mult, unscaled, hpAfter, maxHp);
if (st.unscaledDamageGuardLog)
Debug.Log("[WL813m3] unscaled dmg " + unscaled.ToString("F1") + " ×" + mult.ToString("F2")
+ " → refund " + LastRefund.ToString("F1")
+ " hp=" + hpAfter.ToString("F0") + "/" + maxHp.ToString("F0")); // 🔴 Log 만(813z)
s_lastHp = hpAfter; s_lastHookLoss = hookLoss;
}
/// <summary>프로브용: 카운터 초기화 + 기준선 해제.</summary>
public static void ResetDiagnostics()
{
DetectedCount = RefundCount = SkippedDead = PollCount = RebaseCount = 0;
DetectedHp = RefundedHp = LastDetected = LastRefund = 0d;
LastDetectTime = 0f;
s_pc = null; s_armed = false; s_lastHp = 0d; s_lastHookLoss = 0d;
}
/// <summary>프로브용: 기준선을 직접 주입한다(Play 없이 폴링 로직 검증).</summary>
public static void ProbeArm(PCActor pc, double hp, double hookLoss)
{
s_pc = pc; s_lastHp = hp; s_lastHookLoss = hookLoss; s_armed = true;
}
}
}