using System; using System.Collections.Concurrent; namespace AcDream.Core.Combat; /// /// Client-side combat state — tracks per-entity health percent and /// emits typed events when UpdateHealth / Victim / Attacker / Defender /// notifications arrive. Powers target HP bars, damage floaters, combat /// log panel. /// /// /// Retail client-side combat responsibilities (r02 §7): /// /// /// Maintain a cache of "last known health percent" per entity guid. /// UpdateHealth (0x01C0) is sent when the player queries or the /// server broadcasts a change. /// /// /// Convert raw damage events into UI-ready notifications (colored /// floating numbers, "Critical!" flashes, body-part locations). /// /// /// Track self-centered notifications (you hit / you got hit / you /// evaded / you were evaded) so the log panel can format them /// correctly. /// /// /// /// /// /// The server is authoritative: this class does NOT simulate damage /// locally (exception: a predictive-ish "estimated damage" display for /// the attack bar UI, which can use ). /// /// public sealed class CombatState { private readonly ConcurrentDictionary _healthByGuid = new(); public CombatMode CurrentMode { get; private set; } = CombatMode.NonCombat; /// Fires when a target's health percent changes (from UpdateHealth). public event Action? HealthChanged; /// You (the player) got hit for some damage. public event Action? DamageTaken; /// You (the player) dealt some damage. public event Action? DamageDealtAccepted; /// You (the player) evaded an incoming hit. public event Action? EvadedIncoming; /// The target evaded your hit. public event Action? MissedOutgoing; /// An attack commit completed (0x01A7). WeenieError = 0 on success. public event Action? AttackDone; /// The server accepted the attack and the power bar/animation can begin. public event Action? AttackCommenced; /// The locally requested or server-confirmed combat mode changed. public event Action? CombatModeChanged; /// /// Fires when the server confirms the player landed a killing blow /// (GameEvent KillerNotification (0x01AD)). Event payload is /// the victim's display name + their server GUID. Used by killfeed UI /// (future panel) and any plugin scoring kill counts. /// public event Action? KillLanded; public readonly record struct DamageIncoming( string AttackerName, uint AttackerGuid, uint DamageType, uint Damage, uint HitQuadrant, bool Critical, uint AttackType); public readonly record struct DamageDealt( string DefenderName, uint DamageType, uint Damage, float DamagePercent); /// Retrieve last known health percent for a guid, or 1.0 if unknown. public float GetHealthPercent(uint guid) => _healthByGuid.TryGetValue(guid, out var pct) ? pct : 1f; public int TrackedTargetCount => _healthByGuid.Count; // ── Inbound handlers (wired from WorldSession.GameEvents) ──────────────── public void OnUpdateHealth(uint targetGuid, float healthPercent) { _healthByGuid[targetGuid] = healthPercent; HealthChanged?.Invoke(targetGuid, healthPercent); } public void SetCombatMode(CombatMode mode) { if (CurrentMode == mode) return; CurrentMode = mode; CombatModeChanged?.Invoke(mode); } public void OnVictimNotification( string attackerName, uint attackerGuid, uint damageType, uint damage, uint hitQuadrant, uint critical, uint attackType) { DamageTaken?.Invoke(new DamageIncoming( attackerName, attackerGuid, damageType, damage, hitQuadrant, critical != 0, attackType)); } public void OnDefenderNotification( string attackerName, uint attackerGuid, uint damageType, uint damage, uint hitQuadrant, uint critical) { // DefenderNotification is semantically the same as VictimNotification // from the defender's POV — the client log merges them. DamageTaken?.Invoke(new DamageIncoming( attackerName, attackerGuid, damageType, damage, hitQuadrant, critical != 0, 0)); } public void OnAttackerNotification( string defenderName, uint damageType, uint damage, float damagePercent) { DamageDealtAccepted?.Invoke(new DamageDealt( defenderName, damageType, damage, damagePercent)); } public void OnEvasionAttackerNotification(string defenderName) => MissedOutgoing?.Invoke(defenderName); /// /// Server confirmation that the player landed a killing blow on a /// target. Wire source: GameEvent KillerNotification (0x01AD) /// — the parser at GameEvents.ParseKillerNotification shipped /// alongside victim/defender notifications but was never registered /// for dispatch until 2026-04-25 (per ISSUES.md #10). /// public void OnKillerNotification(string victimName, uint victimGuid) => KillLanded?.Invoke(victimName, victimGuid); public void OnEvasionDefenderNotification(string attackerName) => EvadedIncoming?.Invoke(attackerName); public void OnAttackDone(uint attackSequence, uint weenieError) => AttackDone?.Invoke(attackSequence, weenieError); public void OnCombatCommenceAttack() => AttackCommenced?.Invoke(); public void Clear() => _healthByGuid.Clear(); }