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

250 lines
12 KiB
C#
Raw Normal View History

// ─────────────────────────────────────────────────────────────────────────────
// 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>런 중 처치 수(보스 포함) · 그중 보스 처치 수.</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;
}
}
}