198 lines
13 KiB
C#
198 lines
13 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
|
// DeathFlow.cs — PC 사망 → 사망 연출(2 s · 카메라 추가 연출 0 · 디졸브 0 · 조작 차단) → 부활 요청(813j 팝업) → 응답 → 존 시작점 On_Regen → 무적 창
|
|
//
|
|
// PD 지시 #813 · 발주서 WL-813i §1-3 · 기준서 v1 §B 요소 8 회복 · §D-1 813i 행 (2026-09-09)
|
|
//
|
|
// ■ 원본 실측(Actor.cs)
|
|
// Cal_Damage :583 HP ≤ 0 → DeadStatus = true → Set_Die :585 → PCActor.Set_Die :481(All_Stop(true) · Sub_WaveLife · DelLockTarget)
|
|
// Actor.Set_Die :626 → Play_Die · Set_Coroutine(After_Die, 3f)(익명 코루틴 · 게임 시간 · 취소 불가) · 콜라이더/NavMesh off
|
|
// After_Die :640 → Check_Extra(AfterDie)(PC 는 no-op) → `if (!IsDead()) return;` → PC: On_Regen()(즉시 부활 · 연출 0) + 소환수/펫/일꾼 On_Regen
|
|
// InGameInfo.Defeat_byMonster :442 = 전부 주석(빈 함수) — 대체하지 않고 이 파일이 새 흐름을 맡는다.
|
|
// MyActor.On_Regen :388 = base.On_Regen(전회복 · 상태 초기화) + Set_Warp(LoadMapMgr.Get_ReincanationPos) = **존 시작점** ← 부활은 이 공개 API 로.
|
|
//
|
|
// ■ 흐름(훅 = PCActor.Set_Die 끝 1줄 `DeathFlow.OnPCDied(this)`)
|
|
// Dying(deathDisplaySeconds · unscaled) → AwaitingRevive: ReviveRequested 발생 + (holdWorldWhileReviving) Time.timeScale = 0
|
|
// · 세계를 세우는 이유 ① 팝업 동안 몹이 시체를 두들기지 않게 ② 원본 After_Die(3 s · 게임 시간)가 먼저 부활시키지 못하게(붙잡아 둠).
|
|
// · 원본 PauseUI 가 같은 방식(timeScale 0 · 복원 = MyValue.Get_GameSpeed())이라 TimeScaleArbiter 는 이걸 외부 일시정지로 관측한다.
|
|
// 813j 가 Revive() 로 응답 → 세계 복원 → pc.On_Regen(false, reviveHealRate) → 동료 On_Regen → Survival.SetReviveInvincible → Revived.
|
|
// 이후 원본 After_Die 가 도착해도 `!IsDead()` 로 아무 것도 하지 않는다(실측 :643).
|
|
// 구독자 0(813j 미병합)이면 autoReviveWhenNoListener 로 즉시 부활 · autoReviveTimeoutSeconds 로 시간 상한 · 원본이 먼저 부활시키면 external=true 로 통보.
|
|
// ■ 조작 차단 = 원본이 이미 한다(Run_byJoystick/Play_* 의 IsDead 가드 · Set_Die 의 All_Stop(true)) + 세계 정지. 카메라 = 팔로우 대상이 멈추므로 자연 정지(추가 연출 0).
|
|
// ■ 비활성(WLSurvivalSettings off)이면 OnPCDied 가 즉시 반환 = 원본 After_Die 3 s 자동 부활 100%(C8).
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
using System;
|
|
using UnityEngine;
|
|
using WL.Combat.Core;
|
|
|
|
namespace WL.Combat.Survival
|
|
{
|
|
public enum DeathState { None = 0, Dying = 1, AwaitingRevive = 2 }
|
|
|
|
/// <summary>PC 사망 통보(연출 시작 · 813j HUD 가 조작 UI 를 잠글 때).</summary>
|
|
public struct DeathEvent { public PCActor pc; public Vector3 position; public int deathCount; public float displaySeconds; public float time; public int frame; }
|
|
/// <summary>부활 요청(813j 부활 팝업이 띄우고 DeathFlow.Revive() 로 응답한다).</summary>
|
|
public struct ReviveRequestEvent { public PCActor pc; public int deathCount; public bool worldHeld; public float timeoutSeconds; public float time; public int frame; }
|
|
/// <summary>부활 완료(팝업 닫기 · HUD 잠금 해제 · 무적 연출). external = 원본 After_Die 등 바깥이 먼저 살렸다.</summary>
|
|
public struct RevivedEvent { public PCActor pc; public Vector3 position; public int deathCount; public float invincibleSeconds; public bool external; public bool auto; public float time; public int frame; }
|
|
|
|
public static class DeathFlow
|
|
{
|
|
public static readonly CombatEventList<DeathEvent> Died = new CombatEventList<DeathEvent>(4);
|
|
public static readonly CombatEventList<ReviveRequestEvent> ReviveRequested = new CombatEventList<ReviveRequestEvent>(4);
|
|
public static readonly CombatEventList<RevivedEvent> Revived = new CombatEventList<RevivedEvent>(4);
|
|
|
|
static DeathState s_state;
|
|
static PCActor s_pc;
|
|
static float s_now, s_diedAt, s_requestedAt;
|
|
static bool s_worldHeld, s_acceptEarly;
|
|
|
|
// ── 진단(프로브 · 813m 로그가 읽는다)
|
|
public static int DeathCount, ReviveCount, ExternalReviveCount, AutoReviveCount, AbortCount;
|
|
public static float LastDeathToRequest, LastRequestToRevive;
|
|
public static string LastAbortReason = "";
|
|
|
|
/// <summary>프로브 전용 — 부활 실행을 바꿔치기한다(에디트 모드에는 LoadMapMgr·ActorInfo 가 없다). 런타임에서는 null 로 둘 것.</summary>
|
|
public static Action<PCActor, float> ReviveOverride;
|
|
|
|
public static DeathState State { get { return s_state; } }
|
|
public static PCActor Current { get { return s_pc; } }
|
|
public static bool WorldHeld { get { return s_worldHeld; } }
|
|
/// <summary>사망 연출 남은 시간(초 · unscaled · 813j 가 카운트다운에 써도 된다).</summary>
|
|
public static float DisplayRemaining { get { var st = St; return s_state == DeathState.Dying && st != null ? Mathf.Max(0f, st.deathDisplaySeconds - (s_now - s_diedAt)) : 0f; } }
|
|
|
|
static WLSurvivalSettings St { get { return WLSurvivalSettings.Instance; } }
|
|
static bool Verbose { get { var st = St; return st != null && st.verboseLog; } }
|
|
|
|
/// <summary>PCActor.Set_Die 훅(1줄 · base.Set_Die 뒤). 비활성 · 메인 PC 아님 · 진행 중이면 무시.</summary>
|
|
public static void OnPCDied(PCActor pc)
|
|
{
|
|
if (!WLSurvivalSettings.Enabled || pc == null || !pc.IsMainPC()) return;
|
|
if (s_state != DeathState.None) return;
|
|
Survival.EnsureRunner();
|
|
if (Application.isPlaying) s_now = Time.unscaledTime; // 에디트 모드(프로브)는 Tick 이 넣은 클록을 쓴다
|
|
s_pc = pc; s_state = DeathState.Dying; s_diedAt = s_now; s_acceptEarly = false;
|
|
DeathCount++;
|
|
var st = St;
|
|
if (Verbose) Debug.Log("[DeathFlow] died #" + DeathCount + " display=" + st.deathDisplaySeconds.ToString("F1") + "s");
|
|
var e = new DeathEvent { pc = pc, position = pc.Get_position(), deathCount = DeathCount, displaySeconds = st.deathDisplaySeconds, time = Time.unscaledTime, frame = Time.frameCount };
|
|
Died.Dispatch(in e);
|
|
}
|
|
|
|
/// <summary>러너(또는 프로브)가 unscaled 시간을 넣는다.</summary>
|
|
public static void Tick(float unscaledNow)
|
|
{
|
|
s_now = unscaledNow;
|
|
if (s_state == DeathState.None) return;
|
|
var st = St;
|
|
if (st == null || !WLSurvivalSettings.Enabled) { Abort("disabled"); return; }
|
|
if (s_pc == null) { Abort("pc destroyed"); return; }
|
|
if (!s_pc.IsOnlyDead()) { Finish(true, false); return; } // 바깥(원본 After_Die · 리스폰)이 먼저 살렸다
|
|
|
|
if (s_state == DeathState.Dying)
|
|
{
|
|
if (s_now - s_diedAt < st.deathDisplaySeconds) return;
|
|
s_state = DeathState.AwaitingRevive; s_requestedAt = s_now;
|
|
LastDeathToRequest = s_now - s_diedAt;
|
|
if (st.holdWorldWhileReviving) HoldWorld();
|
|
var e = new ReviveRequestEvent { pc = s_pc, deathCount = DeathCount, worldHeld = s_worldHeld, timeoutSeconds = st.autoReviveTimeoutSeconds, time = Time.unscaledTime, frame = Time.frameCount };
|
|
ReviveRequested.Dispatch(in e);
|
|
if (Verbose) Debug.Log("[DeathFlow] revive requested listeners=" + ReviveRequested.Count + " held=" + s_worldHeld);
|
|
if (s_acceptEarly) { Finish(false, false); return; }
|
|
if (ReviveRequested.Count == 0 && st.autoReviveWhenNoListener) { Finish(false, true); return; }
|
|
return;
|
|
}
|
|
|
|
// AwaitingRevive
|
|
if (st.autoReviveTimeoutSeconds > 0f && s_now - s_requestedAt >= st.autoReviveTimeoutSeconds) Finish(false, true);
|
|
}
|
|
|
|
/// <summary>813j 부활 팝업의 응답. AwaitingRevive 면 즉시 부활 · Dying 이면 연출이 끝나는 순간 부활 · None 이면 false.</summary>
|
|
public static bool Revive()
|
|
{
|
|
switch (s_state)
|
|
{
|
|
case DeathState.AwaitingRevive: Finish(false, false); return true;
|
|
case DeathState.Dying: s_acceptEarly = true; return true;
|
|
default: return false;
|
|
}
|
|
}
|
|
|
|
static void Finish(bool external, bool auto)
|
|
{
|
|
var st = St;
|
|
var pc = s_pc;
|
|
ReleaseWorld();
|
|
LastRequestToRevive = s_state == DeathState.AwaitingRevive ? s_now - s_requestedAt : 0f;
|
|
s_state = DeathState.None; s_pc = null; s_acceptEarly = false;
|
|
|
|
float inv = st != null ? st.reviveInvincibleSeconds : 0f;
|
|
if (!external && pc != null)
|
|
{
|
|
try
|
|
{
|
|
if (ReviveOverride != null) ReviveOverride(pc, st.reviveHealRate);
|
|
else pc.On_Regen(false, st.reviveHealRate); // MyActor.On_Regen → 전회복 + 존 시작점 Set_Warp · PCActor.On_Regen → All_Stop(false) 등
|
|
if (st.reviveCompanions && ReviveOverride == null) ReviveCompanions(pc);
|
|
}
|
|
catch (Exception ex) { Debug.LogException(ex); }
|
|
}
|
|
Survival.SetReviveInvincible(pc, inv);
|
|
ReviveCount++; if (external) ExternalReviveCount++; if (auto) AutoReviveCount++;
|
|
if (Verbose) Debug.Log("[DeathFlow] revived external=" + external + " auto=" + auto + " invincible=" + inv.ToString("F2") + "s");
|
|
var e = new RevivedEvent { pc = pc, position = pc != null ? pc.Get_position() : Vector3.zero, deathCount = DeathCount, invincibleSeconds = inv, external = external, auto = auto, time = Time.unscaledTime, frame = Time.frameCount };
|
|
Revived.Dispatch(in e);
|
|
}
|
|
|
|
/// <summary>원본 After_Die 의 PC 분기 재현(소환수 이동 · 펫 부활 · 일꾼 이동) — 전부 공개 API.</summary>
|
|
static void ReviveCompanions(PCActor pc)
|
|
{
|
|
if (pc.IsEnemy()) return;
|
|
var pet = pc.Get_MyPet();
|
|
if (!DSUtil.CheckNull(pet)) pet.On_Regen();
|
|
var info = ActorInfo.Ins;
|
|
if (info == null) return;
|
|
var summ = info.Get_Actor(eRole.Summon);
|
|
if (!DSUtil.CheckNull(summ)) summ.On_Regen();
|
|
var worker = info.Get_Actor(eRole.Worker);
|
|
if (!DSUtil.CheckNull(worker)) worker.On_Regen();
|
|
}
|
|
|
|
/// <summary>흐름 중단(런 경계 · 로비 · PC 소실 · SO off). 세계 정지를 풀고 상태만 비운다(부활 호출 0).</summary>
|
|
public static void Abort(string reason)
|
|
{
|
|
if (s_state == DeathState.None && !s_worldHeld) return;
|
|
ReleaseWorld();
|
|
s_state = DeathState.None; s_pc = null; s_acceptEarly = false;
|
|
AbortCount++; LastAbortReason = reason ?? "";
|
|
if (Verbose) Debug.Log("[DeathFlow] abort (" + reason + ")");
|
|
}
|
|
|
|
static void HoldWorld()
|
|
{
|
|
if (!Application.isPlaying) return; // 에디트 모드에서 timeScale 대입은 TimeManager.asset 에 적힌다(811b-fix 실측) — 프로브 안전
|
|
if (s_worldHeld) return;
|
|
Time.timeScale = 0f; // 원본 PauseUI.Set 과 같은 방식
|
|
s_worldHeld = true;
|
|
}
|
|
|
|
/// <summary>세계 정지 해제(우리가 세웠고 아직 0 일 때만 · 복원값 = 원본 PauseUI 와 같은 MyValue.Get_GameSpeed()).</summary>
|
|
internal static void ReleaseWorld()
|
|
{
|
|
if (!s_worldHeld) return;
|
|
s_worldHeld = false;
|
|
if (Time.timeScale == 0f) Time.timeScale = MyValue.Get_GameSpeed();
|
|
}
|
|
|
|
/// <summary>프로브용: 상태·카운터 초기화(구독은 유지 · 세계 정지 해제).</summary>
|
|
public static void ResetDiagnostics()
|
|
{
|
|
ReleaseWorld();
|
|
s_state = DeathState.None; s_pc = null; s_acceptEarly = false; s_diedAt = s_requestedAt = 0f;
|
|
DeathCount = ReviveCount = ExternalReviveCount = AutoReviveCount = AbortCount = 0;
|
|
LastDeathToRequest = LastRequestToRevive = 0f; LastAbortReason = "";
|
|
Died.Raised = Died.Dispatched = 0; ReviveRequested.Raised = ReviveRequested.Dispatched = 0; Revived.Raised = Revived.Dispatched = 0;
|
|
}
|
|
}
|
|
}
|