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