Project_WL/Assets/WL/Combat/Growth/GrowthEvents.cs

166 lines
8.7 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.

// ─────────────────────────────────────────────────────────────────────────────
// GrowthEvents.cs — WL 성장(레벨업) 통지 허브(정적)
//
// PD 지시 #813 · 발주서 WL-813k §1-1 (2026-09-09)
//
// ■ 왜 별도 파일인가 (813h BossEvents 선례)
// 코어 허브 `Assets/WL/Combat/Core/CombatEvents.cs` 는 811d~j 가 동시에 물고 있어 수정 금지다.
// 811b 가 둔 `CombatEvents.LevelUp` 페이로드는 `pc · newLevel` 2필드뿐이라 813g 팝업이 읽어야 할
// 「이전/새 스탯 3개」를 담을 수 없다. 그래서 같은 규약으로 이 파일에 성장 축을 따로 둔다.
// · 정적 클래스(새 Manager/Singleton 0) · 구조체 페이로드를 `in` 으로 전달(복사·박싱 0)
// · 구독자 = 사전 할당 배열(코어의 CombatEventList<T> 재사용 — Assembly-CSharp 단일 어셈블리)
// · 구독자 예외는 try/catch 로 격리(코어 CombatEventList.Dispatch 안) → 원본 Add_Exp 흐름을 끊지 않는다
// · 코어 스위치(WLCombatCoreSettings)와 성장 스위치(WLGrowthSettings) 둘 다 준수 = C8 롤백
// 코어를 대체하지 않는다 — forwardToCombatEvents 가 켜져 있으면 811b 의 `CombatEvents.RaiseLevelUp`
// 도 그대로 호출해 코어 구독자(있다면)를 잃지 않는다.
//
// ■ 발생 지점 (원본 훅 1줄)
// ServerClass.cs `SD_PC.Add_Exp` 레벨업 while 루프 안 `++Lv;` 직후 — 오른 레벨 1 당 1회.
// (한 번의 Add_Exp 로 여러 레벨이 오르면 그 횟수만큼 발생하고 levelsGained 가 1,2,3… 으로 센다)
//
// ■ 구독
// WL.Combat.Growth.GrowthEvents.LevelUp.Add(handler) / Remove(handler)
// 연출 = LevelUpBurst(G · 이 폴더) · 팝업 = 813g(U). 둘 다 여기를 구독만 한다.
//
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것(SD_PC 가 Assembly-CSharp 에 있다).
// ─────────────────────────────────────────────────────────────────────────────
using UnityEngine;
using WL.Combat.Core;
namespace WL.Combat.Growth
{
/// <summary>PC 레벨업 페이로드(발생 시점 스냅샷 · struct · GC 0).</summary>
public struct PlayerLevelUpEvent
{
/// <summary>레벨업한 서버 데이터(ServerData.PC). 항상 non-null.</summary>
public SD_PC data;
/// <summary>월드의 메인 PC. 로비·오프라인 보상 등 월드에 PC 가 없으면 null — 구독자는 반드시 null 검사할 것.</summary>
public Actor pc;
/// <summary>레벨업 전/후 레벨.</summary>
public int prevLevel, newLevel;
/// <summary>같은 Add_Exp 호출(같은 프레임·같은 SD_PC)에서 이번이 몇 번째 레벨업인가(1부터).</summary>
public int levelsGained;
/// <summary>레벨업 뒤 남은 미사용 스탯 포인트(SD_PC.Get_Point()). 원본 = (Lv-1)*5 사용분.</summary>
public int statPointTotal;
/// <summary>813g 팝업용 스탯 3종의 종류(WLGrowthSettings.statA/B/C · 기본 MaxHP · MaxMP · ATK).</summary>
public eStat statAKind, statBKind, statCKind;
/// <summary>스탯 3종의 이전/새 값. refreshStatOnLevelUp 이 꺼져 있으면(기본) 원본대로 prev == new 다 — 원본은 레벨업으로 스탯이 바로 오르지 않고 포인트만 준다.</summary>
public double prevStatA, newStatA;
public double prevStatB, newStatB;
public double prevStatC, newStatC;
/// <summary>전회복 직전의 HP/MP 와 최대치(연출·로그·UI 용 · PC 가 없으면 0).</summary>
public double hpBefore, maxHp, mpBefore, maxMp;
/// <summary>연출 스폰 기준 위치(PC 가 없으면 Vector3.zero).</summary>
public Vector3 position;
public float time;
public int frame;
}
/// <summary>성장 이벤트 허브. 정적 · GC 0 · 구독은 LevelUp.Add / Remove.</summary>
public static class GrowthEvents
{
/// <summary>레벨업 구독자 목록(코어와 같은 구현 · 예외 격리 · Raised/Dispatched 카운터).</summary>
public static readonly CombatEventList<PlayerLevelUpEvent> LevelUp = new CombatEventList<PlayerLevelUpEvent>(4);
/// <summary>성장 축이 살아 있는가 = 코어 스위치 · 성장 스위치 둘 다 켜짐. 어느 쪽을 꺼도 원본 동작(C8).</summary>
public static bool Enabled
{
get { return WLCombatCoreSettings.Enabled && WLGrowthSettings.Enabled; }
}
// ── 진단(프로브가 읽는다)
public static int TotalRaised;
public static string LastEvent = "";
public static float LastEventTime;
public static int LastLevel, LastPrevLevel, LastLevelsGained, LastStatPoint;
// ── 연속 레벨업 카운트(같은 프레임 · 같은 SD_PC 면 이어서 센다)
static SD_PC s_lastData;
static int s_lastFrame = -1;
static int s_chain;
/// <summary>
/// 원본 훅 1줄이 부른다 — `ServerClass.cs` `SD_PC.Add_Exp` 의 `++Lv;` 직후.
/// 코어/성장 스위치가 꺼져 있거나 에셋이 없으면 즉시 반환한다(원본 100%).
/// </summary>
public static void RaiseLevelUp(SD_PC data, int newLevel)
{
if (!Enabled) return;
var s = WLGrowthSettings.Instance;
// 월드의 메인 PC. 로비·결과창·오프라인 보상 경로에서는 없을 수 있다(파괴된 참조도 Unity != 로 걸러진다).
var mypc = MyValue.MyPC;
Actor pc = mypc != null ? mypc : null;
int frame = Time.frameCount;
if (ReferenceEquals(data, s_lastData) && s_lastFrame == frame) s_chain++;
else s_chain = 1;
s_lastData = data; s_lastFrame = frame;
var e = new PlayerLevelUpEvent
{
data = data,
pc = pc,
prevLevel = newLevel - 1,
newLevel = newLevel,
levelsGained = s_chain,
statPointTotal = data != null ? data.Get_Point() : 0,
statAKind = s.statA, statBKind = s.statB, statCKind = s.statC,
position = pc != null ? pc.Get_position() : Vector3.zero,
time = Time.unscaledTime,
frame = frame
};
var info = pc != null ? pc.Get_StatInfo() : null;
if (info != null)
{
e.prevStatA = info.Get_Stat(s.statA);
e.prevStatB = info.Get_Stat(s.statB);
e.prevStatC = info.Get_Stat(s.statC);
e.hpBefore = info.Get_Stat(eStat.HP); e.maxHp = info.Get_Stat(eStat.MaxHP);
e.mpBefore = info.Get_Stat(eStat.MP); e.maxMp = info.Get_Stat(eStat.MaxMP);
// 기본 off — 원본은 레벨업으로 스탯이 즉시 오르지 않는다(포인트만 지급). 켜면 재계산 후 새 값을 담는다.
if (s.refreshStatOnLevelUp)
MyValue.Get_ActorStatInfoorNull(eRole.PC, false, "0", 0, true); // showchange=false(StatChangeUI 미호출) · keephpmp=true
e.newStatA = info.Get_Stat(s.statA);
e.newStatB = info.Get_Stat(s.statB);
e.newStatC = info.Get_Stat(s.statC);
}
TotalRaised++;
LastEvent = "LevelUp";
LastEventTime = e.time;
LastPrevLevel = e.prevLevel; LastLevel = e.newLevel;
LastLevelsGained = e.levelsGained; LastStatPoint = e.statPointTotal;
LevelUp.Dispatch(in e);
// 811b 가 API 만 둔 코어 이벤트에도 그대로 흘린다(코어가 꺼져 있으면 내부에서 즉시 반환).
if (s.forwardToCombatEvents) CombatEvents.RaiseLevelUp(pc, newLevel);
if (s.verboseLog)
Debug.Log("[GrowthEvents] LevelUp " + e.prevLevel + "→" + e.newLevel +
" chain=" + e.levelsGained + " point=" + e.statPointTotal +
" pc=" + (pc != null ? pc.name : "none"));
}
/// <summary>프로브용: 카운터 초기화(구독은 유지).</summary>
public static void ResetDiagnostics()
{
TotalRaised = 0; LastEvent = ""; LastEventTime = 0f;
LastLevel = LastPrevLevel = LastLevelsGained = LastStatPoint = 0;
LevelUp.Raised = LevelUp.Dispatched = 0;
s_lastData = null; s_lastFrame = -1; s_chain = 0;
}
}
}