438 lines
27 KiB
C#
438 lines
27 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// Survival.cs — 무적 해제 게이트(받는 피해 배율 · 잡몹/보스 분리) + 부활 무적 창 + 생존 축 공용 틱 러너
|
||
//
|
||
// PD 지시 #813 · 발주서 WL-813i §1-1 · 기준서 v1 §B 요소 8 · §F-2 · 설계안 WL-811a §A 7행 (2026-09-09)
|
||
//
|
||
// ■ 왜 PCActor.Get_Damage 훅인가 (실측 · 발주서 "실측 후 최소 침습")
|
||
// PC 피해 경로 = PCActor.Get_Damage :264 → base = MyActor.Get_Damage :301
|
||
// → `wlInvincible = IsRole(PC) && WLGameplaySettings.PlayerInvincible` 이면 Actor.Get_Damage(방어 공식 · Cal_Damage · Set_Die)
|
||
// 를 **아예 부르지 않고** RaiseDamaged(invincible=true) 만 낸다.
|
||
// · Damaged 이벤트 구독으로는 피해를 넣을 수 없다 — 그 시점엔 이미 base 가 건너뛰어졌고, 다시 Get_Damage 를 부르면 같은 게이트에 막힌다.
|
||
// · Actor.Cal_Damage 직접 호출은 방어 공식(:400-462) · 관통 · 감쇠 · 마나실드 · CC · HitConfirmed 를 전부 건너뛴다(스탯 의미 상실).
|
||
// · MyActor.cs(무적 분기) 수정 금지 · WLGameplaySettings.asset 값 변경 금지.
|
||
// → PCActor.Get_Damage 의 `base.Get_Damage(_dinfo)` 1줄을 `Survival.TakeDamage(this, _dinfo, base.Get_Damage)` 로 바꾼다.
|
||
// base 메서드 그룹은 C# 이 **비가상**으로 MyActor.Get_Damage 에 묶어 준다. 이 안에서
|
||
// ① 부활 무적 창이면 원본 그대로(무적 플래그 유지 → 피해 0) ② 잡몹/보스 스위치가 꺼져 있으면 원본 그대로
|
||
// ③ 아니면 원 피해 × 배율 → WLGameplaySettings.Instance.playerInvincible 을 **이 호출 동안만** false 로 내리고 base 호출 → finally 로 복원.
|
||
// 비활성(에셋 없음 · enabled=false)이면 base 만 부른다 = 원본 100%(C8).
|
||
// 비용: 훅 줄이 base 델리게이트 1개를 만든다(피격 1회당 ≈ 64 B · 원본이 피격마다 new DamageInfo 를 만드는 것과 같은 자릿수). 완료보고에 기재.
|
||
//
|
||
// ■ 배율은 방어 공식 **전**(원 피해)에 곱한다 — Actor.Get_Damage 뒤에는 PC 에 훅 지점이 없다. 방어 공식은 피해에 선형이라
|
||
// "받는 피해 0.5" 의 체감은 같고, 고정 피해(ATK_FIXED) 항만 배율 밖이다.
|
||
//
|
||
// ■ 러너(SurvivalRunner · 숨은 오브젝트 1개 · DontDestroyOnLoad · 코루틴 없음)는 SO 가 켜져 있을 때만 만든다(C8: 꺼져 있으면 오브젝트 0).
|
||
// 틱: Survival(게임 시간) · PotionUse(게임 시간) · AutoPotion(게임 시간 · 813m2) · DeathFlow(unscaled · 세계를 세워도 흘러야 한다) · 런 경계 폴링(맵/모드/PC 변화).
|
||
//
|
||
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것.
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// WL-813i2 (2026-09-09 · PD 지시 #813) — 보스 연타 대비 **피격 후 짧은 무적 창**
|
||
//
|
||
// 🔴 실측 근거 = 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** — 813m3 의 「도트가 범인」 가설은 기각됐다
|
||
// → 배율(F-17 0.15→0.1)은 「한 방에 26 %」를 못 바꾼다. **연타의 횟수**를 줄이는 축이 이 창이다.
|
||
//
|
||
// ■ 어떻게 (새 코드 경로 0 · 813i 무적 API 재사용)
|
||
// 실피해가 들어간 직후 `SetHitInvincible(pc, bossHitInvincibleSeconds)` 로 창을 연다.
|
||
// 창 안의 피격은 **부활 무적 창과 똑같은 원본 경로**를 탄다 — `playerInvincible` 을 내리지 않으므로
|
||
// MyActor.Get_Damage(:308)가 `base.Get_Damage` 를 건너뛰고 `RaiseDamaged(invincible=true)` 만 낸다.
|
||
// · 피해 0 · 방어 공식 0 · 사망 판정 0
|
||
// · 연출은 **UI 무수정으로 1회만** — `WLSurvivalUiSettings.ignoreInvincibleHits=1`(HitVignette:87)과
|
||
// `WLImpactTierSettings.pcHitSkipWhenInvincible=1`(PcHitReaction:89)이 invincible 타를 스스로 거른다.
|
||
// · 잡몹에는 기본 미적용(`mobHitInvincible=0`) — 실측상 잡몹은 한 방에 0.3~0.4 % 라 창이 상시로 열리면
|
||
// 난이도가 통째로 사라진다.
|
||
// C8 롤백 = `bossHitInvincibleSeconds = 0` 또는 `hitInvincibleEnabled = 0` → 813i 동작 100 %.
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
using System;
|
||
using UnityEngine;
|
||
|
||
namespace WL.Combat.Survival
|
||
{
|
||
/// <summary>무적 해제 게이트 + 부활 무적 창 + 런 경계. 정적 · GC 0(verboseLog 때만 문자열).</summary>
|
||
public static class Survival
|
||
{
|
||
// ── 진단(프로브가 읽는다)
|
||
public static int Applied; // 배율 적용 후 base 호출(무적 해제) 횟수
|
||
public static int Lifted; // 그중 playerInvincible 을 실제로 내렸다 되돌린 횟수
|
||
public static int PassDisabled; // SO off → 원본 그대로
|
||
public static int PassNotMainPC; // 메인 PC 아님(적 PC 등) → 원본 그대로
|
||
public static int BlockedReviveInvincible; // 부활 무적 창 → 원본 그대로(피해 0)
|
||
public static int SkippedMobSwitch, SkippedBossSwitch;
|
||
public static double LastRawDamage, LastScaledDamage, LastFinalDamage;
|
||
public static bool LastWasBoss;
|
||
public static bool LastFlagDuringCall; // base 호출 직전 PlayerInvincible 값(기대 false)
|
||
|
||
// ── WL-813i2 진단(프로브가 읽는다)
|
||
/// <summary>피격 후 무적 창이 막은 피격 수(= 연타에서 덜 맞은 타수).</summary>
|
||
public static int BlockedHitInvincible;
|
||
/// <summary>피격 후 무적 창을 연 횟수 · 그중 보스 피격이 연 횟수.</summary>
|
||
public static int HitInvincibleOpened, HitInvincibleOpenedByBoss;
|
||
/// <summary>마지막으로 연 창의 길이(초).</summary>
|
||
public static float LastHitInvincibleSeconds;
|
||
|
||
// ── WL-813w8 진단(프로브가 읽는다) — 결과창(런 종료) 정지 중 무적 래치
|
||
/// <summary>정지 무적이 막은 피격 수(D-46 = 결과창 중 초당 3~5대).</summary>
|
||
public static int BlockedHoldInvincible;
|
||
/// <summary>래치를 건 횟수 · 푼 횟수.</summary>
|
||
public static int HoldInvincibleOn, HoldInvincibleOff;
|
||
/// <summary>마지막으로 걸거나 푼 사유(진단).</summary>
|
||
public static string LastHoldInvincibleReason = "";
|
||
|
||
/// <summary>
|
||
/// WL-813m3 — 이 훅을 거쳐 실제로 깎인 PC HP 누계(중첩 Cal_Damage 포함 · base 호출 전후 실측).
|
||
/// DamageGuard 가 "총 감소량 − 이 누계" 로 **훅을 안 거친 피해**(배율 미적용 경로)를 뽑는 기준선이다.
|
||
/// </summary>
|
||
public static double HookHpLoss;
|
||
|
||
static float s_now; // 게임 시간(러너 Tick · 프로브는 직접 넣는다)
|
||
static float s_reviveInvincibleUntil = -1f;
|
||
static Actor s_invinciblePc;
|
||
|
||
// WL-813i2 — 피격 후 무적 창(부활 창과 별도 축: 부활 창은 런/부활 경계가, 이 창은 피격이 연다)
|
||
static float s_hitInvincibleUntil = -1f;
|
||
static Actor s_hitInvinciblePc;
|
||
|
||
// WL-813w8 — 결과창 정지 무적 「래치」(창이 아니다 = 시간으로 만료되지 않는다).
|
||
// 거는 쪽(RunGuide.BeginRunEndHold)이 풀 때까지 유지된다 — 결과창을 얼마나 오래 띄워도 피해 0.
|
||
static Actor s_holdInvinciblePc;
|
||
|
||
/// <summary>마지막 틱의 게임 시간(Time.time · 프로브 클록).</summary>
|
||
public static float Now { get { return s_now; } }
|
||
public static float ReviveInvincibleUntil { get { return s_reviveInvincibleUntil; } }
|
||
public static float ReviveInvincibleRemaining { get { return Mathf.Max(0f, s_reviveInvincibleUntil - s_now); } }
|
||
|
||
static WLSurvivalSettings St { get { return WLSurvivalSettings.Instance; } }
|
||
static bool Verbose { get { var st = St; return st != null && st.verboseLog; } }
|
||
|
||
/// <summary>부활 무적 창 안인가(게임 시간 · 813j HP 바 연출이 읽어도 된다).</summary>
|
||
public static bool IsReviveInvincible(Actor pc)
|
||
{
|
||
return pc != null && ReferenceEquals(pc, s_invinciblePc) && s_now < s_reviveInvincibleUntil;
|
||
}
|
||
|
||
/// <summary>DeathFlow 가 부활 직후 부른다. seconds ≤ 0 이면 창 없음.</summary>
|
||
public static void SetReviveInvincible(Actor pc, float seconds)
|
||
{
|
||
DamageGuard.Rebase(); // WL-813m3 — 부활·런 경계로 HP 가 튀면 폴링 기준선을 다시 잡는다(오검출 0)
|
||
SetHitInvincible(null, 0f); // WL-813i2 — 부활·런 경계는 피격 창도 함께 닫는다
|
||
if (pc == null || seconds <= 0f) { s_invinciblePc = null; s_reviveInvincibleUntil = -1f; return; }
|
||
s_invinciblePc = pc;
|
||
s_reviveInvincibleUntil = s_now + seconds;
|
||
}
|
||
|
||
// ───────────────────────────────────────── WL-813i2 피격 후 무적 창 (Q10 D-21)
|
||
|
||
/// <summary>피격 후 무적 창 종료 시각(게임 시간 · 창 없으면 −1).</summary>
|
||
public static float HitInvincibleUntil { get { return s_hitInvincibleUntil; } }
|
||
/// <summary>피격 후 무적 창 잔여(초 · 813j UI 가 읽어도 된다).</summary>
|
||
public static float HitInvincibleRemaining { get { return Mathf.Max(0f, s_hitInvincibleUntil - s_now); } }
|
||
|
||
/// <summary>피격 후 무적 창 안인가(부활 창과 별개).</summary>
|
||
public static bool IsHitInvincible(Actor pc)
|
||
{
|
||
return pc != null && ReferenceEquals(pc, s_hitInvinciblePc) && s_now < s_hitInvincibleUntil;
|
||
}
|
||
|
||
/// <summary>피격 후 무적 창을 연다(프로브도 직접 부른다). seconds ≤ 0 이면 창을 닫는다.</summary>
|
||
public static void SetHitInvincible(Actor pc, float seconds)
|
||
{
|
||
if (pc == null || seconds <= 0f) { s_hitInvinciblePc = null; s_hitInvincibleUntil = -1f; return; }
|
||
s_hitInvinciblePc = pc;
|
||
s_hitInvincibleUntil = s_now + seconds;
|
||
LastHitInvincibleSeconds = seconds;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 이 피격이 창을 열 자격이 있는가(순수 함수 · Actor 접근 0 · 프로브가 가짜 값으로 그대로 부른다 · C8 증거).
|
||
/// 반환 = 창 길이(초 · 0 이면 열지 않는다).
|
||
/// </summary>
|
||
public static float HitInvincibleWindow(WLSurvivalSettings st, bool boss, double hpLost)
|
||
{
|
||
if (st == null || !st.hitInvincibleEnabled) return 0f;
|
||
if (hpLost < (double)Mathf.Max(0f, st.hitInvincibleMinHpLoss)) return 0f;
|
||
if (boss) return Mathf.Max(0f, st.bossHitInvincibleSeconds);
|
||
if (!st.mobHitInvincible) return 0f;
|
||
return Mathf.Max(0f, st.mobHitInvincibleSeconds);
|
||
}
|
||
|
||
// ─────────────────────────── WL-813w8 결과창 정지 중 무적 래치 (Q11 D-46)
|
||
//
|
||
// 🔴 근거 = qa/W2-11_재채점_v9.md D-46 — 813w7 정지(RunGuide.BeginRunEndHold)는 **PC 의 공격만** 막는다.
|
||
// 몹은 그대로 때려 **초당 3.4~4.9 HP** 가 깎이고 **55~80 초에 사망**(3판 3/3 · 사망 파일 3건).
|
||
// `mobHitInvincible=0`(813i2 설계값)이라 잡몹 연타를 끊는 창이 없다.
|
||
//
|
||
// ■ 왜 「창」이 아니라 「래치」인가 — 결과창을 얼마나 오래 켜 두는지는 PD 손에 달렸다(Q11: 82 s · 94 s).
|
||
// 시간 창을 갱신하는 방식은 갱신이 한 번만 끊겨도 죽는다. 거는 쪽이 풀 때까지 유지되는 래치가 맞다.
|
||
// ■ 피해를 어떻게 0 으로 만드나 = 813i2 피격 창과 **같은 원본 경로**다 — `playerInvincible` 을 이 호출
|
||
// 동안만 올려 `MyActor.Get_Damage`(:308)가 base 를 건너뛰고 `RaiseDamaged(invincible=true)` 만 내게 한다.
|
||
// → 피해 0 · 비네트(`WLSurvivalUiSettings.ignoreInvincibleHits=1`)·타격 반응
|
||
// (`WLImpactTierSettings.pcHitSkipWhenInvincible=1`)이 스스로 걸러 준다(UI 무수정).
|
||
// ■ 값·스위치는 거는 쪽 SO(`WLRunGuideSettings.invincibleWhileRunEndHold`) — 여기는 API 만 둔다(C8).
|
||
|
||
/// <summary>결과창 정지 무적이 걸려 있는가(대상 무관 · DamageGuard·프로브가 읽는다).</summary>
|
||
public static bool HoldInvincibleActive { get { return s_holdInvinciblePc != null; } }
|
||
|
||
/// <summary>이 PC 에 결과창 정지 무적이 걸려 있는가.</summary>
|
||
public static bool IsHoldInvincible(Actor pc)
|
||
{
|
||
return pc != null && ReferenceEquals(pc, s_holdInvinciblePc);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 결과창 정지 무적을 걸거나 푼다(RunGuide 가 부른다 · 프로브도 직접 부른다).
|
||
/// on=false 이거나 pc 가 null 이면 해제. 같은 상태로 다시 불러도 카운터는 한 번만 움직인다.
|
||
/// </summary>
|
||
public static void SetHoldInvincible(Actor pc, bool on, string reason)
|
||
{
|
||
if (!on || pc == null)
|
||
{
|
||
if (s_holdInvinciblePc == null) return;
|
||
s_holdInvinciblePc = null;
|
||
HoldInvincibleOff++;
|
||
LastHoldInvincibleReason = reason;
|
||
return;
|
||
}
|
||
if (ReferenceEquals(pc, s_holdInvinciblePc)) return;
|
||
s_holdInvinciblePc = pc;
|
||
HoldInvincibleOn++;
|
||
LastHoldInvincibleReason = reason;
|
||
}
|
||
|
||
/// <summary>
|
||
/// PCActor.Get_Damage 훅(1줄). baseCall = base.Get_Damage(MyActor · 비가상). 비활성이면 baseCall 만 = 원본 100%.
|
||
/// </summary>
|
||
public static void TakeDamage(PCActor pc, DamageInfo dinfo, Action<DamageInfo> baseCall)
|
||
{
|
||
if (baseCall == null) return;
|
||
if (!WLSurvivalSettings.Enabled || pc == null || dinfo == null) { PassDisabled++; baseCall(dinfo); return; }
|
||
if (!pc.IsMainPC()) { PassNotMainPC++; baseCall(dinfo); return; }
|
||
EnsureRunner();
|
||
|
||
if (IsReviveInvincible(pc)) { BlockedReviveInvincible++; CallBase(pc, dinfo, baseCall, DamageVia.ReviveInv, dinfo.Damage, 0d); return; } // 원본 무적 경로(피해 0 · Damaged(invincible=true))
|
||
|
||
// 🔴 WL-813w8 (Q11 D-46) — 결과창(런 종료) 정지 중에는 피해 0. 813i2 피격 창과 **같은 원본 경로**
|
||
// (플래그를 이 호출 동안만 올린다 → base 가 건너뛰고 Damaged(invincible=true) 만 나간다).
|
||
// 부활 무적 창보다 **뒤**에 둔다 — 사망/부활 중이면 그쪽이 우선이다(발주서 §1-1 「부활 우선」).
|
||
if (IsHoldInvincible(pc))
|
||
{
|
||
BlockedHoldInvincible++;
|
||
var gsr = WL.Settings.WLGameplaySettings.Instance;
|
||
bool raiseHold = gsr != null && !gsr.playerInvincible;
|
||
if (raiseHold) gsr.playerInvincible = true;
|
||
try { CallBase(pc, dinfo, baseCall, DamageVia.HoldInv, dinfo.Damage, 0d); }
|
||
finally { if (raiseHold) gsr.playerInvincible = false; }
|
||
return;
|
||
}
|
||
|
||
// 🔴 WL-813i2 (Q10 D-21) — 피격 후 무적 창. 부활 창과 **같은 원본 경로**로 흘려보낸다:
|
||
// playerInvincible 을 내리지 않으므로 MyActor.Get_Damage 가 base 를 건너뛰고
|
||
// RaiseDamaged(invincible=true) 만 낸다 → 피해 0 · 비네트/타격반응은 각 SO 의
|
||
// `ignoreInvincibleHits` / `pcHitSkipWhenInvincible` 이 걸러 준다(UI 무수정 · 연출 1회).
|
||
// 🔴 그 경로는 `WLGameplaySettings.playerInvincible` 이 **true 일 때만** 성립한다(MyActor.cs:308).
|
||
// PD 가 그 값을 해제하면 창이 있어도 원 피해가 그대로 들어간다 → 이 호출 동안만 플래그를
|
||
// 올렸다 되돌린다(Scaled 경로가 내렸다 되돌리는 것의 정확한 대칭 · 디스크 값 무변경).
|
||
if (IsHitInvincible(pc))
|
||
{
|
||
BlockedHitInvincible++;
|
||
var gsi = WL.Settings.WLGameplaySettings.Instance;
|
||
bool raise = gsi != null && !gsi.playerInvincible;
|
||
if (raise) gsi.playerInvincible = true;
|
||
try { CallBase(pc, dinfo, baseCall, DamageVia.HitInv, dinfo.Damage, 0d); }
|
||
finally { if (raise) gsi.playerInvincible = false; }
|
||
return;
|
||
}
|
||
|
||
var st = St;
|
||
bool boss = IsBossSource(dinfo, st);
|
||
if (boss ? !st.takeBossDamage : !st.takeMobDamage)
|
||
{
|
||
if (boss) SkippedBossSwitch++; else SkippedMobSwitch++;
|
||
CallBase(pc, dinfo, baseCall, DamageVia.SwitchOff, dinfo.Damage, 0d); // 스위치 off = 그 진영엔 원본 무적 그대로
|
||
return;
|
||
}
|
||
|
||
double mult = boss ? st.bossDamageMultiplier : st.mobDamageMultiplier;
|
||
if (mult < 0d) mult = 0d;
|
||
double raw = dinfo.Damage;
|
||
dinfo.Damage = raw * mult;
|
||
LastRawDamage = raw; LastScaledDamage = dinfo.Damage; LastWasBoss = boss;
|
||
|
||
var gs = WL.Settings.WLGameplaySettings.Instance;
|
||
bool lift = gs != null && gs.playerInvincible; // 이미 꺼져 있으면(PD 가 에셋에서 해제) 손대지 않는다
|
||
if (lift) { gs.playerInvincible = false; Lifted++; }
|
||
LastFlagDuringCall = WL.Settings.WLGameplaySettings.PlayerInvincible;
|
||
double lost = 0d;
|
||
try { lost = CallBase(pc, dinfo, baseCall, DamageVia.Scaled, raw, mult); }
|
||
finally { if (lift) gs.playerInvincible = true; } // 디스크 값 무변경 · 예외가 나도 되돌린다
|
||
Applied++;
|
||
LastFinalDamage = dinfo.Damage; // Actor.Get_Damage 가 dinfo 를 제자리에서 갱신 → 방어 후 최종 피해
|
||
|
||
// 🔴 WL-813i2 — **실제로 깎였을 때만** 창을 연다(방어로 0 이 된 타격은 창을 열지 않는다).
|
||
float win = HitInvincibleWindow(st, boss, lost);
|
||
if (win > 0f)
|
||
{
|
||
SetHitInvincible(pc, win);
|
||
HitInvincibleOpened++;
|
||
if (boss) HitInvincibleOpenedByBoss++;
|
||
if (st.hitInvincibleLog)
|
||
Debug.Log("[WL813i2] " + (boss ? "boss" : "mob") + " hit −" + lost.ToString("F0")
|
||
+ " hp → 무적 " + win.ToString("F2") + "s (hp=" + pc.Get_HP().ToString("F0")
|
||
+ "/" + pc.Get_MaxHP().ToString("F0") + " 막은타 " + BlockedHitInvincible + ")");
|
||
}
|
||
|
||
if (Verbose)
|
||
Debug.Log("[Survival] " + (boss ? "boss" : "mob") + " dmg " + raw.ToString("F0") + " ×" + mult.ToString("F2")
|
||
+ " → " + LastScaledDamage.ToString("F0") + " → final " + dinfo.Damage.ToString("F0")
|
||
+ " hp=" + pc.Get_HP().ToString("F0") + "/" + pc.Get_MaxHP().ToString("F0") + (lift ? " (lifted)" : ""));
|
||
}
|
||
|
||
/// <summary>
|
||
/// WL-813m3 — base 호출을 HP 실측으로 감싼다. 호출 전후 차이가 "훅이 책임지는 감소량" 이고
|
||
/// (원본 Get_Damage 안의 중첩 Cal_Damage · 스턴 추가타 · Extra 까지 포함한다) 그 누계가
|
||
/// DamageGuard 의 기준선이 된다. 같은 자리에서 사망 직전 로그 1건도 남긴다(GC 0).
|
||
/// </summary>
|
||
/// <returns>WL-813i2 — 이 호출이 실제로 깎은 HP(피격 후 무적 창을 열 자격 판정에 쓴다).</returns>
|
||
static double CallBase(PCActor pc, DamageInfo dinfo, Action<DamageInfo> baseCall, DamageVia via, double raw, double mult)
|
||
{
|
||
double before = pc.Get_HP();
|
||
double lost = 0d;
|
||
try { baseCall(dinfo); }
|
||
finally
|
||
{
|
||
double after = pc.Get_HP();
|
||
lost = before - after;
|
||
if (lost < 0d) lost = 0d;
|
||
HookHpLoss += lost;
|
||
DeathDiag.Record(via, dinfo != null ? dinfo.Beater : null, raw, mult, lost, after, pc.Get_MaxHP());
|
||
}
|
||
return lost;
|
||
}
|
||
|
||
static bool IsBossSource(DamageInfo dinfo, WLSurvivalSettings st)
|
||
{
|
||
var beater = dinfo.Beater;
|
||
if (beater == null) return false; // 가해자 없음(함정 등) = 잡몹 취급
|
||
if (beater.IsSubRole(eSubRol.Boss)) return true;
|
||
return st.eliteCountsAsBoss && beater.IsSubRole(eSubRol.Elite);
|
||
}
|
||
|
||
// ───────────────────────────────────────── 틱 · 런 경계
|
||
|
||
/// <summary>러너(또는 프로브)가 게임 시간을 넣는다.</summary>
|
||
public static void Tick(float gameNow) { s_now = gameNow; }
|
||
|
||
static PCActor s_lastPc;
|
||
static int s_lastMapId = int.MinValue;
|
||
static int s_lastMode = int.MinValue;
|
||
public static int RunStarts;
|
||
public static string LastRunReason = "";
|
||
|
||
/// <summary>러너가 매 프레임 부른다. 메인 PC 교체 · Stage/Dungeon 맵 진입을 런 시작으로 본다(원본 훅 0 · 폴링).</summary>
|
||
internal static void PollRunBoundary()
|
||
{
|
||
var st = St;
|
||
if (st == null) return;
|
||
|
||
var pc = MyValue.MyPC; // 파괴된 참조는 Unity != 로 걸러진다
|
||
if (!ReferenceEquals(pc, s_lastPc))
|
||
{
|
||
s_lastPc = pc;
|
||
if (pc != null) OnRunStart("pc");
|
||
else DeathFlow.Abort("pc lost");
|
||
}
|
||
|
||
var info = InGameInfo.Ins;
|
||
if (info == null) return;
|
||
int mode = (int)info.Get_GameMode();
|
||
int map = MyValue.MyChoiceMapData != null ? MyValue.MyChoiceMapData.n_MapID : -1;
|
||
if (mode != s_lastMode || map != s_lastMapId)
|
||
{
|
||
bool first = s_lastMode == int.MinValue;
|
||
s_lastMode = mode; s_lastMapId = map;
|
||
if (first) return; // 부팅 직후 첫 관측은 경계가 아니다
|
||
var m = (eGameMode)mode;
|
||
if (m == eGameMode.Lobby) DeathFlow.Abort("lobby");
|
||
else if ((m == eGameMode.Stage || m == eGameMode.Dungeon) && st.potionResetOnMapLoad) OnRunStart("map " + map);
|
||
}
|
||
}
|
||
|
||
static void OnRunStart(string reason)
|
||
{
|
||
RunStarts++; LastRunReason = reason;
|
||
DeathFlow.Abort(reason);
|
||
PotionUse.ResetRun(reason);
|
||
SetReviveInvincible(null, 0f);
|
||
SetHoldInvincible(null, false, "런 경계(" + reason + ")"); // 813w8 — 맵/PC 가 바뀌면 정지 무적도 반드시 푼다(RunGuide 해제와 이중 안전)
|
||
if (Verbose) Debug.Log("[Survival] run start (" + reason + ")");
|
||
}
|
||
|
||
internal static void EnsureRunner()
|
||
{
|
||
if (Application.isPlaying) SurvivalRunner.Ensure();
|
||
}
|
||
|
||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
||
static void Boot()
|
||
{
|
||
if (WLSurvivalSettings.Enabled) SurvivalRunner.Ensure(); // 꺼져 있으면 오브젝트 0(C8)
|
||
}
|
||
|
||
/// <summary>프로브용: 카운터·창 초기화.</summary>
|
||
public static void ResetDiagnostics()
|
||
{
|
||
Applied = Lifted = PassDisabled = PassNotMainPC = BlockedReviveInvincible = SkippedMobSwitch = SkippedBossSwitch = 0;
|
||
LastRawDamage = LastScaledDamage = LastFinalDamage = 0d; LastWasBoss = false; LastFlagDuringCall = false;
|
||
s_reviveInvincibleUntil = -1f; s_invinciblePc = null;
|
||
RunStarts = 0; LastRunReason = "";
|
||
HookHpLoss = 0d; // 813m3
|
||
// 813i2
|
||
BlockedHitInvincible = HitInvincibleOpened = HitInvincibleOpenedByBoss = 0;
|
||
LastHitInvincibleSeconds = 0f;
|
||
s_hitInvincibleUntil = -1f; s_hitInvinciblePc = null;
|
||
// 813w8
|
||
BlockedHoldInvincible = HoldInvincibleOn = HoldInvincibleOff = 0;
|
||
LastHoldInvincibleReason = "";
|
||
s_holdInvinciblePc = null;
|
||
}
|
||
}
|
||
|
||
/// <summary>생존 축 틱 러너(숨은 오브젝트 1개 · DontDestroyOnLoad · 코루틴 없음). 파괴·비활성·앱 종료 시 세계 정지를 반드시 푼다.</summary>
|
||
public sealed class SurvivalRunner : MonoBehaviour
|
||
{
|
||
static SurvivalRunner s_runner;
|
||
|
||
public static bool Exists { get { return s_runner != null; } }
|
||
|
||
public static void Ensure()
|
||
{
|
||
if (s_runner != null) return;
|
||
var go = new GameObject("__WLSurvival");
|
||
DontDestroyOnLoad(go);
|
||
s_runner = go.AddComponent<SurvivalRunner>();
|
||
}
|
||
|
||
void Update()
|
||
{
|
||
Survival.Tick(Time.time);
|
||
PotionUse.Tick(Time.time);
|
||
AutoPotion.Tick(Time.time); // 813m2 · PotionUse.Tick 뒤(쿨 잔량이 먼저 갱신돼야 IsReady 가 맞다)
|
||
DeathFlow.Tick(Time.unscaledTime);
|
||
Survival.PollRunBoundary();
|
||
DeathDiag.EnsureSubscribed(); // 813m3 · 첫 호출에만 DeathFlow.Died 구독
|
||
DamageGuard.Poll(); // 813m3 · 훅을 안 거친 HP 감소 정산
|
||
}
|
||
|
||
/// <summary>
|
||
/// 813m3 — 도트 코루틴(`WaitForSeconds`)은 Update **뒤**에 재개된다. 같은 프레임에 정산하려면
|
||
/// LateUpdate 에서 한 번 더 봐야 한다(지연 ≈ 0). 비활성이면 Poll 이 즉시 반환한다(C8).
|
||
/// </summary>
|
||
void LateUpdate() { DamageGuard.Poll(); }
|
||
|
||
void OnDisable() { DeathFlow.ReleaseWorld(); }
|
||
void OnApplicationQuit() { DeathFlow.ReleaseWorld(); }
|
||
}
|
||
}
|