acdream/src/AcDream.UI.Abstractions/Panels/Vitals/VitalsVM.cs
Erik 05f6222865 fix(ui): complete live targeted healing flow
Route ACE server commands through the existing chat path, bind the retail paperdoll hit mask instead of its obscured viewport, and prefer authoritative private health vitals. Record the user-confirmed live gate and pin the production DAT widget type in tests.
2026-07-11 07:58:59 +02:00

108 lines
4.6 KiB
C#

using AcDream.Core.Combat;
using AcDream.Core.Player;
namespace AcDream.UI.Abstractions.Panels.Vitals;
/// <summary>
/// ViewModel for the vitals HUD panel. Reads the local player's authoritative
/// private vital snapshot, falling back to world-combat health before that
/// snapshot is available.
///
/// <para>
/// <b>Sources:</b>
/// </para>
///
/// <list type="bullet">
/// <item>HP — <see cref="LocalPlayerState.HealthPercent"/> from
/// <c>PrivateUpdateVital(Current)</c>, with
/// <see cref="CombatState.GetHealthPercent"/> as fallback.</item>
/// <item>Stamina / Mana — <see cref="LocalPlayerState"/>'s
/// <c>StaminaPercent</c> / <c>ManaPercent</c>, populated from the
/// <c>CreatureProfile</c> embedded in <c>PlayerDescription
/// (0x0013)</c>. When no <see cref="LocalPlayerState"/> is wired
/// (older constructor / tests), both stay <c>null</c> and
/// <c>VitalsPanel</c> simply skips those bars.</item>
/// </list>
///
/// <para>
/// <b>GUID timing:</b> the local player's server GUID isn't known at
/// <c>OnLoad</c> (pre-login). Construct with <see cref="SetLocalPlayerGuid"/>
/// left as 0; <c>GameWindow</c> calls the setter when the live session
/// receives its guid at <c>EnterWorld</c>. Before the GUID is set,
/// <see cref="HealthPercent"/> returns 1.0 (via <c>CombatState</c>'s safe
/// default for unknown guids) — the bar reads "full", which is harmless.
/// </para>
/// </summary>
public sealed class VitalsVM
{
private readonly CombatState _combat;
private readonly LocalPlayerState? _local;
private uint _localPlayerGuid;
/// <summary>
/// Build a VitalsVM bound to a <see cref="CombatState"/> and (optionally)
/// a <see cref="LocalPlayerState"/>. The GUID starts at 0; call
/// <see cref="SetLocalPlayerGuid"/> once the live session assigns it.
/// When <paramref name="localPlayer"/> is <c>null</c> (back-compat),
/// stamina + mana stay <c>null</c> and <c>VitalsPanel</c> skips those bars.
/// </summary>
public VitalsVM(CombatState combat, LocalPlayerState? localPlayer = null)
{
_combat = combat ?? throw new ArgumentNullException(nameof(combat));
_local = localPlayer;
_localPlayerGuid = 0;
}
/// <summary>
/// Push the authoritative local-player GUID from <c>WorldSession</c>.
/// One-way setter — only <c>GameWindow</c> should call it, exactly once
/// per live session.
/// </summary>
public void SetLocalPlayerGuid(uint guid) => _localPlayerGuid = guid;
/// <summary>
/// Current health percent (0..1) for the local player. The private vital
/// is authoritative for self; world-combat percentage is the pre-login
/// and legacy fallback.
/// </summary>
public float HealthPercent
=> _local?.HealthPercent ?? _combat.GetHealthPercent(_localPlayerGuid);
/// <summary>
/// Stamina percent (0..1), or <c>null</c> when no
/// <see cref="LocalPlayerState"/> is wired or it hasn't received a
/// <c>PlayerDescription</c> with both current and max yet. Reads
/// through to the cache every access — no VM-side caching.
/// </summary>
public float? StaminaPercent => _local?.StaminaPercent;
/// <summary>
/// Mana percent (0..1), or <c>null</c> under the same conditions as
/// <see cref="StaminaPercent"/>.
/// </summary>
public float? ManaPercent => _local?.ManaPercent;
// ── Absolute values for HUD overlays ──────────────────────────────────
/// <summary>Current health value (server-authoritative absolute) or
/// <c>null</c> if <see cref="LocalPlayerState"/> hasn't received the
/// vital snapshot yet.</summary>
public uint? HealthCurrent => _local?.Get(LocalPlayerState.VitalKind.Health)?.Current;
/// <summary>Max health value, accounting for attribute contribution
/// + active enchantment buffs + vitae. <c>null</c> if no vital
/// snapshot yet.</summary>
public uint? HealthMax => _local?.GetMaxApprox(LocalPlayerState.VitalKind.Health);
/// <summary>Current stamina value.</summary>
public uint? StaminaCurrent => _local?.Get(LocalPlayerState.VitalKind.Stamina)?.Current;
/// <summary>Max stamina including buffs + vitae.</summary>
public uint? StaminaMax => _local?.GetMaxApprox(LocalPlayerState.VitalKind.Stamina);
/// <summary>Current mana value.</summary>
public uint? ManaCurrent => _local?.Get(LocalPlayerState.VitalKind.Mana)?.Current;
/// <summary>Max mana including buffs + vitae.</summary>
public uint? ManaMax => _local?.GetMaxApprox(LocalPlayerState.VitalKind.Mana);
}