164 lines
6.1 KiB
C#
164 lines
6.1 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
|
|
namespace AcDream.Core.Combat;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
///
|
|
/// <para>
|
|
/// Retail client-side combat responsibilities (r02 §7):
|
|
/// <list type="bullet">
|
|
/// <item><description>
|
|
/// 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.
|
|
/// </description></item>
|
|
/// <item><description>
|
|
/// Convert raw damage events into UI-ready notifications (colored
|
|
/// floating numbers, "Critical!" flashes, body-part locations).
|
|
/// </description></item>
|
|
/// <item><description>
|
|
/// Track self-centered notifications (you hit / you got hit / you
|
|
/// evaded / you were evaded) so the log panel can format them
|
|
/// correctly.
|
|
/// </description></item>
|
|
/// </list>
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// 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 <see cref="CombatMath"/>).
|
|
/// </para>
|
|
/// </summary>
|
|
public sealed class CombatState
|
|
{
|
|
private readonly ConcurrentDictionary<uint, float> _healthByGuid = new();
|
|
|
|
public CombatMode CurrentMode { get; private set; } = CombatMode.NonCombat;
|
|
|
|
/// <summary>Fires when a target's health percent changes (from UpdateHealth).</summary>
|
|
public event Action<uint /*guid*/, float /*percent*/>? HealthChanged;
|
|
|
|
/// <summary>You (the player) got hit for some damage.</summary>
|
|
public event Action<DamageIncoming>? DamageTaken;
|
|
|
|
/// <summary>You (the player) dealt some damage.</summary>
|
|
public event Action<DamageDealt>? DamageDealtAccepted;
|
|
|
|
/// <summary>You (the player) evaded an incoming hit.</summary>
|
|
public event Action<string>? EvadedIncoming;
|
|
|
|
/// <summary>The target evaded your hit.</summary>
|
|
public event Action<string>? MissedOutgoing;
|
|
|
|
/// <summary>An attack commit completed (0x01A7). WeenieError = 0 on success.</summary>
|
|
public event Action<uint /*attackSeq*/, uint /*weenieError*/>? AttackDone;
|
|
|
|
/// <summary>The server accepted the attack and the power bar/animation can begin.</summary>
|
|
public event Action? AttackCommenced;
|
|
|
|
/// <summary>The locally requested or server-confirmed combat mode changed.</summary>
|
|
public event Action<CombatMode>? CombatModeChanged;
|
|
|
|
/// <summary>
|
|
/// Fires when the server confirms the player landed a killing blow
|
|
/// (GameEvent <c>KillerNotification (0x01AD)</c>). Event payload is
|
|
/// the victim's display name + their server GUID. Used by killfeed UI
|
|
/// (future panel) and any plugin scoring kill counts.
|
|
/// </summary>
|
|
public event Action<string /*victimName*/, uint /*victimGuid*/>? 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);
|
|
|
|
/// <summary>Retrieve last known health percent for a guid, or 1.0 if unknown.</summary>
|
|
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);
|
|
|
|
/// <summary>
|
|
/// Server confirmation that the player landed a killing blow on a
|
|
/// target. Wire source: GameEvent <c>KillerNotification (0x01AD)</c>
|
|
/// — the parser at <c>GameEvents.ParseKillerNotification</c> shipped
|
|
/// alongside victim/defender notifications but was never registered
|
|
/// for dispatch until 2026-04-25 (per ISSUES.md #10).
|
|
/// </summary>
|
|
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();
|
|
}
|