Project_WL/Assets/WL/Combat/Run/RunEvents.cs

259 lines
13 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.

// ─────────────────────────────────────────────────────────────────────────────
// RunEvents.cs — WL 3분 런 통지 허브(정적) · 813q(결과 화면 UI)의 유일한 구독 지점
//
// PD 지시 #813 · 발주서 WL-813p §1 (2026-09-09)
//
// ■ 왜 별도 파일인가
// 코어 허브 `Assets/WL/Combat/Core/CombatEvents.cs` 는 811d~j 가 동시에 물고 있어 수정 금지다.
// 「런」은 811 12종·813h BossEvents·813k GrowthEvents 에 없던 새 축이므로 같은 규약으로 따로 둔다.
// · 정적 클래스(새 Manager/Singleton 0) · 구조체 페이로드를 `in` 으로 전달(복사·박싱 0)
// · 구독자 = 사전 할당 배열(코어의 CombatEventList<T> 재사용 — Assembly-CSharp 단일 어셈블리)
// · 구독자 예외는 try/catch 로 격리(CombatEventList.Dispatch 안) → 런 흐름을 끊지 않는다
// · 발행은 전부 RunDirector 안(이 파일은 계약 + 구독자 목록만 들고 있다)
//
// ■ 813q(UI)가 구독할 것 — 발주서 §4 계약표와 1:1
// RunStarted 런 시작(HUD 타이머 리셋 · 존 게이지 리셋)
// RunTick 1 Hz(SO tickIntervalSeconds) — 남은 시간 · 현재 존 진행률
// ZoneCleared 존 1개 클리어(존 게이지 완료 연출)
// BossGateOpened 보스 게이트 개방(배너 · "보스 등장" 안내)
// BossStarted 보스가 실제로 월드에 등장(보스 HP 바 · 보스 타이머 시작)
// RunEnded 런 종료 — RunResult 1개로 결과 화면 전부를 그린다
//
// 🔴 RunResult 안의 배열(zoneClearSec · zoneKills · lootByGrade)은 RunDirector 가 **재사용**한다.
// 구독자는 값을 즉시 읽어 자기 버퍼에 복사할 것 — 참조를 보관하면 다음 런에서 덮어써진다(GC 0 대가).
//
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것.
// ─────────────────────────────────────────────────────────────────────────────
using UnityEngine;
using WL.Combat.Core;
namespace WL.Combat.Run
{
/// <summary>런 종료 사유.</summary>
public enum RunOutcome
{
/// <summary>아직 끝나지 않았다(진행 중 스냅샷).</summary>
None = 0,
/// <summary>보스(Anubis) 처치 — 승리.</summary>
Win = 1,
/// <summary>runSeconds 초과 — 보스 미처치.</summary>
Timeout = 2,
/// <summary>런 미완 이탈(로비 복귀 · 맵 이동 · PC 소실).</summary>
Abandon = 3,
}
/// <summary>런 진행 단계.</summary>
public enum RunPhase
{
/// <summary>런 없음(타이틀 · 로비 · 종료 후 대기).</summary>
Idle = 0,
/// <summary>존 진행 중(게이트 미개방).</summary>
Zones = 1,
/// <summary>게이트는 열렸으나 보스가 아직 월드에 없다.</summary>
BossGate = 2,
/// <summary>보스전 진행 중.</summary>
Boss = 3,
/// <summary>종료(RunEnded 발행 완료 · 다음 런 대기).</summary>
Ended = 4,
}
/// <summary>보스 게이트를 연 이유.</summary>
public enum RunGateReason
{
None = 0,
/// <summary>존을 전부 클리어했다(openGateOnZonesCleared).</summary>
ZonesCleared = 1,
/// <summary>openGateAtSec 경과(존이 남았어도 연다).</summary>
TimeLimit = 2,
/// <summary>외부에서 RunDirector.OpenBossGate() 를 직접 불렀다(QA·치트·813q 버튼).</summary>
External = 3,
/// <summary>보스가 게이트보다 먼저 월드에 있었다(BossArena off · 813e 상시 배치).</summary>
AlreadySpawned = 4,
}
// ───────────────────────────────────────────── 페이로드 (struct · 발생 시점 스냅샷 · GC 0)
/// <summary>런 시작.</summary>
public struct RunStartedEvent
{
/// <summary>이 세션에서 몇 번째 런인가(1부터). Restart 마다 +1.</summary>
public int runIndex;
/// <summary>런이 시작된 시각(Time.unscaledTime).</summary>
public float startTime;
/// <summary>제한 시간(초 · SO runSeconds) — UI 타이머 최대값.</summary>
public float runSeconds;
/// <summary>이 런의 존 수(SO zoneSpawnerIds 길이) — UI 존 게이지 개수.</summary>
public int zoneCount;
/// <summary>시작 사유("map 1" · "pc" · "restart" · "manual").</summary>
public string reason;
public float time;
public int frame;
}
/// <summary>존 1개 클리어(누적 처치가 임계에 도달한 순간 · 존당 1회).</summary>
public struct ZoneClearedEvent
{
/// <summary>zoneSpawnerIds 안의 인덱스(0부터).</summary>
public int zoneIndex;
/// <summary>그 존의 SpawnerID(813001~).</summary>
public int spawnerId;
/// <summary>런 시작 후 경과 초.</summary>
public float elapsedSec;
/// <summary>런 시작 시점 대비 그 존의 처치 수 · 임계값.</summary>
public int kills, required;
/// <summary>이번 클리어로 모든 존이 끝났는가.</summary>
public bool allZonesCleared;
public float time;
public int frame;
}
/// <summary>보스 게이트 개방(런당 1회).</summary>
public struct BossGateOpenedEvent
{
/// <summary>런 시작 후 경과 초.</summary>
public float elapsedSec;
/// <summary>왜 열렸는가.</summary>
public RunGateReason reason;
/// <summary>보스 스포너 ID(SO bossSpawnerId).</summary>
public int bossSpawnerId;
/// <summary>BossArena.ForceOpenGate() 가 실제로 먹혔는가(게이트가 무장돼 있었는가). false 면 통지만 났다.</summary>
public bool arenaGateArmed;
public float time;
public int frame;
}
/// <summary>보스가 월드에 등장(런당 1회).</summary>
public struct BossStartedEvent
{
/// <summary>런 시작 후 경과 초.</summary>
public float elapsedSec;
/// <summary>등장한 보스의 SpawnerID.</summary>
public int spawnerId;
/// <summary>등장한 보스(BossArena.Boss · 폴백 경로로 잡았으면 null 일 수 있다 — 반드시 null 검사).</summary>
public Actor boss;
public float time;
public int frame;
}
/// <summary>런 타이머 틱(기본 1 Hz · UI 타이머·존 게이지 갱신용).</summary>
public struct RunTickEvent
{
/// <summary>런 시작 후 경과 초 · 남은 초(0 미만으로 내려가지 않는다).</summary>
public float elapsedSec, remainingSec;
/// <summary>현재 존 인덱스 = 아직 클리어하지 않은 첫 존(전부 클리어면 zoneCount).</summary>
public int zoneIndex;
/// <summary>현재 존의 처치 수 · 임계값(전부 클리어면 0/0).</summary>
public int zoneKills, zoneRequired;
/// <summary>지금까지 클리어한 존 수 · 전체 존 수.</summary>
public int zonesCleared, zoneCount;
/// <summary>현재 단계.</summary>
public RunPhase phase;
/// <summary>이 세션에서 몇 번째 런인가.</summary>
public int runIndex;
public float time;
public int frame;
}
/// <summary>
/// 런 결과 — 813q 결과 화면이 이 하나로 전부를 그린다.
/// 🔴 배열 3종은 RunDirector 가 재사용한다. 값을 즉시 읽어 복사할 것(참조 보관 금지).
/// </summary>
public struct RunResult
{
/// <summary>종료 사유.</summary>
public RunOutcome outcome;
/// <summary>이 세션에서 몇 번째 런인가(1부터).</summary>
public int runIndex;
/// <summary>런 전체 경과 초(pauseTimerWhileWorldHeld 면 부활 팝업 시간은 뺀다).</summary>
public float totalSec;
/// <summary>제한 시간(초 · SO runSeconds).</summary>
public float runSeconds;
/// <summary>존별 클리어 시각(런 시작 후 초). 미클리어는 1. 길이 = zoneCount.</summary>
public float[] zoneClearSec;
/// <summary>존별 처치 수(런 시작 시점 대비). 길이 = zoneCount.</summary>
public int[] zoneKills;
/// <summary>존 수 · 클리어한 존 수.</summary>
public int zoneCount, zonesCleared;
/// <summary>보스 게이트 개방 시각 · 보스 등장 시각 · 보스 처치 시각(런 시작 후 초 · 미발생 1).</summary>
public float bossGateSec, bossStartSec, bossClearSec;
/// <summary>
/// 🔴 813w6(D-44) — 런 종료 순간의 보스 HP 비율(0~1). 보스가 없거나 못 읽으면 **1**.
/// **BossArena.ParkBoss(만피 복구) 이전** 값이다(순서 = 스냅샷 → RunEnded 발행 → Park).
/// 결과창 「보스 처치 : 미처치 (HP 남음 N %)」는 이 값을 쓰면 된다(813q2 는 UI 자체 실측이라 만피를 읽었다).
/// </summary>
public float bossHpRatio;
/// <summary>런 종료 순간의 보스 HP · 최대 HP(못 읽으면 0 · 813w6).</summary>
public double bossHp, bossHpMax;
/// <summary>런 중 처치 수(보스 포함) · 그중 보스 처치 수.</summary>
public int kills, bossKills;
/// <summary>런 중 최대 연쇄 처치 수(813e KillChain).</summary>
public int maxChain;
/// <summary>단일 타격 최대 피해 · 합산(DamageMergeEvent.total) 최대 피해.</summary>
public double maxDamage, maxDamageBurst;
/// <summary>런 시작·종료 시점 레벨(813k GrowthEvents · 플레이 모드가 아니면 0).</summary>
public int levelFrom, levelTo;
/// <summary>등급별 획득 수 — **인덱스 = 등급(1~WLLootSettings.GradeSlots)** · [0] 미사용. 길이 = GradeSlots+1.</summary>
public int[] lootByGrade;
/// <summary>획득 총 개수(골드 제외) · 획득 골드 총량.</summary>
public int lootTotal;
public long gold;
/// <summary>사망 수(813i DeathFlow · 런을 끝내지 않는다) · 물약 사용 수 · 스킬 시전 수(메인 PC).</summary>
public int deaths, potionsUsed, skillCasts;
public float time;
public int frame;
}
/// <summary>런 이벤트 허브. 정적 · GC 0 · 구독은 각 목록의 Add / Remove.</summary>
public static class RunEvents
{
/// <summary>런 시작.</summary>
public static readonly CombatEventList<RunStartedEvent> RunStarted = new CombatEventList<RunStartedEvent>(4);
/// <summary>존 클리어(존당 1회).</summary>
public static readonly CombatEventList<ZoneClearedEvent> ZoneCleared = new CombatEventList<ZoneClearedEvent>(4);
/// <summary>보스 게이트 개방(런당 1회).</summary>
public static readonly CombatEventList<BossGateOpenedEvent> BossGateOpened = new CombatEventList<BossGateOpenedEvent>(4);
/// <summary>보스 등장(런당 1회).</summary>
public static readonly CombatEventList<BossStartedEvent> BossStarted = new CombatEventList<BossStartedEvent>(4);
/// <summary>런 종료 — 페이로드가 곧 RunResult.</summary>
public static readonly CombatEventList<RunResult> RunEnded = new CombatEventList<RunResult>(4);
/// <summary>런 타이머 틱(기본 1 Hz).</summary>
public static readonly CombatEventList<RunTickEvent> RunTick = new CombatEventList<RunTickEvent>(4);
/// <summary>런 축이 살아 있는가 = 에셋 · enabled · 런타임 스위치 · 코어(C8).</summary>
public static bool Enabled { get { return WLRunSettings.Enabled; } }
// ── 진단(프로브가 읽는다)
public static int TotalRaised;
public static string LastEvent = "";
public static float LastEventTime;
internal static void Stamp(string name)
{
TotalRaised++;
LastEvent = name;
LastEventTime = Time.unscaledTime;
}
/// <summary>프로브용: 카운터 초기화(구독은 유지).</summary>
public static void ResetDiagnostics()
{
TotalRaised = 0; LastEvent = ""; LastEventTime = 0f;
RunStarted.Raised = RunStarted.Dispatched = 0;
ZoneCleared.Raised = ZoneCleared.Dispatched = 0;
BossGateOpened.Raised = BossGateOpened.Dispatched = 0;
BossStarted.Raised = BossStarted.Dispatched = 0;
RunEnded.Raised = RunEnded.Dispatched = 0;
RunTick.Raised = RunTick.Dispatched = 0;
}
}
}