// ───────────────────────────────────────────────────────────────────────────── // BossEvents.cs — WL 전투 액션 · 보스 페이즈 통지 허브(정적) // // PD 지시 #813 · 발주서 WL-813h §1-1 (2026-09-09) // // ■ 왜 별도 파일인가 // 코어 허브 `Assets/WL/Combat/Core/CombatEvents.cs` 는 811d~j 가 동시에 물고 있어 수정 금지다. // 보스 페이즈(BossPhase)는 811 12종 이벤트에 없던 새 축이므로 같은 규약으로 이 파일에 따로 둔다. // · 정적 클래스(새 Manager/Singleton 0) · 구조체 페이로드를 `in` 으로 전달(복사·박싱 0) // · 구독자 = 사전 할당 배열(코어의 CombatEventList 재사용 — Assembly-CSharp 단일 어셈블리) // · 구독자 예외는 try/catch 로 격리(코어 CombatEventList.Dispatch 안) → 원본 전투 흐름을 끊지 않는다 // · WLCombatCoreSettings.enabled / RuntimeDisabled 를 그대로 준수(C8 롤백 1순위 스위치 공유) // // ■ 발생 지점 (원본 훅은 BossMobActor.cs 1줄뿐 — 실제 판정은 BossPatternTable 안) // BossMobActor.On_Regen ─ BossPatternTable.Apply → 페이즈 1 진입 통지(스폰) // BossMobActor.Get_Damage ─ BossPatternTable.UpdatePhase → HP% 가 임계를 내려갈 때만 통지 // 페이즈는 단조 증가(회복해도 되돌아가지 않는다) — 연출·UI 가 페이즈를 왕복하지 않게 한다. // // ■ 임계값의 출처 (C45) // 코드 상수 0. 임계는 `Assets/WL/Combat/Settings/Resources/WL/WL813h_BossPattern.json` // (보스별 phaseThresholds · 없으면 테이블 기본 [1.0, 0.7, 0.4]). // JSON 이 없거나 코어가 꺼져 있으면 Raise 자체가 일어나지 않는다 = 원본 100%. // // 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것(BossMobActor 가 Assembly-CSharp 에 있다). // ───────────────────────────────────────────────────────────────────────────── using UnityEngine; using WL.Combat.Core; namespace WL.Combat.Boss { /// 보스 페이즈 전환 페이로드(발생 시점 스냅샷 · struct · GC 0). public struct BossPhaseEvent { /// 페이즈가 바뀐 보스(에디트 모드 프로브에서는 프리팹 인스턴스일 수 있다). public BossMobActor boss; /// MonsterList 의 n_MonsterID (Anubis = 10006). public int monsterId; /// 이전 페이즈(스폰 통지는 0) · 새 페이즈(1부터). public int prevPhase, phase; /// 이 보스의 총 페이즈 수(임계 배열 길이). public int phaseCount; /// 전환 시점의 HP 비율(0~1) · 이 페이즈에 들어오게 한 임계값. public float hpPercent, threshold; /// 스폰(페이즈 1 진입) 통지면 true — 피해로 인한 전환과 구분한다. public bool isSpawn; public float time; public int frame; } /// 보스 이벤트 허브. 정적 · GC 0 · 구독은 BossPhase.Add / Remove. public static class BossEvents { /// 페이즈 전환 구독자 목록(코어와 같은 구현 · 예외 격리 · Raised/Dispatched 카운터). public static readonly CombatEventList BossPhase = new CombatEventList(4); /// 코어 활성 여부 = WLCombatCoreSettings.Enabled(에셋 · enabled · RuntimeDisabled). public static bool Enabled { get { return WLCombatCoreSettings.Enabled; } } // ── 진단(프로브가 읽는다) public static int TotalRaised; public static string LastEvent = ""; public static float LastEventTime; public static int LastPhase, LastPrevPhase, LastMonsterId; public static float LastHpPercent, LastThreshold; /// /// 페이즈 전환 통지. 호출측(BossPatternTable)이 이미 "바뀌었다"를 판정한 뒤에만 부른다. /// 코어가 꺼져 있으면 즉시 반환(C8) — 구독자도 카운터도 움직이지 않는다. /// public static void RaiseBossPhase(BossMobActor boss, int monsterId, int prevPhase, int phase, int phaseCount, float hpPercent, float threshold, bool isSpawn) { if (!Enabled) return; var e = new BossPhaseEvent { boss = boss, monsterId = monsterId, prevPhase = prevPhase, phase = phase, phaseCount = phaseCount, hpPercent = hpPercent, threshold = threshold, isSpawn = isSpawn, time = Time.unscaledTime, frame = Time.frameCount }; TotalRaised++; LastEvent = "BossPhase"; LastEventTime = e.time; LastPrevPhase = prevPhase; LastPhase = phase; LastMonsterId = monsterId; LastHpPercent = hpPercent; LastThreshold = threshold; BossPhase.Dispatch(in e); var c = WLCombatCoreSettings.Instance; if (c != null && c.verboseLog) Debug.Log("[BossEvents] Phase " + prevPhase + "→" + phase + " id=" + monsterId + " hp=" + hpPercent.ToString("F3") + " th=" + threshold.ToString("F3")); } /// 프로브용: 카운터 초기화(구독은 유지). public static void ResetDiagnostics() { TotalRaised = 0; LastEvent = ""; LastEventTime = 0f; LastPhase = LastPrevPhase = LastMonsterId = 0; LastHpPercent = LastThreshold = 0f; BossPhase.Raised = BossPhase.Dispatched = 0; } } }