feat(player): #5 LocalPlayerState — Stam/Mana wired through PlayerDescription
Closes ISSUES.md #5. The Vitals devtools window now draws three bars (HP / Stamina / Mana) once the server sends the first PlayerDescription (0x0013), instead of HP only. Built test-first per CLAUDE.md TDD rule — 16 new tests went red before the implementation went in. New AcDream.Core.Player.LocalPlayerState (cache): - {CurrentStamina, MaxStamina, CurrentMana, MaxMana} as uint? — null until first received. - StaminaPercent / ManaPercent: 0..1 fraction or null when either field is missing or max is zero. Clamps to 1.0 if current > max (server can briefly report this during buff transitions). - OnPlayerDescription preserves any previously known good value when an incoming field is null — partial profiles don't wipe state. - Changed event for future subscribers. GameEventWiring.WireAll: - New optional 6th parameter: LocalPlayerState? localPlayer = null. Existing 5-arg call sites still work; without the parameter the new PlayerDescription handler still parses + feeds the spellbook but skips the cache update. - PlayerDescription (0x0013) shares AppraiseInfo wire format with IdentifyObjectResponse (0x00C9) per AppraiseInfoParser docstring, so the new handler reuses the existing parser and pulls CreatureProfile.{Stamina, StaminaMax, Mana, ManaMax}. - Player's full learned spellbook also lands here (previously only item-scoped Identify responses fed the spellbook). VitalsVM: - Constructor adds optional LocalPlayerState? parameter (default null keeps every existing caller compiling). - StaminaPercent / ManaPercent now read through to LocalPlayerState every access — no VM-side caching, so a server-side delta to the cache surfaces next frame without any explicit refresh. GameWindow: - Public readonly LocalPlayer field alongside Combat / Chat / Items / SpellBook so plugins + future panels can bind directly. - WireAll call updated to pass LocalPlayer. - VitalsVM construction passes LocalPlayer so the existing VitalsPanel automatically picks up the two new bars. Test counts: - AcDream.Core.Tests: 550 → 561 (+11 LocalPlayerStateTests) - AcDream.UI.Abstractions.Tests: 23 → 26 (+3 VitalsVM through-cache) - AcDream.Core.Net.Tests: 192 → 194 (+2 PlayerDescription wiring) - Total: 765 → 781 Build: 0 warnings, 0 errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9faf9d7e3a
commit
d42bf5735d
8 changed files with 436 additions and 50 deletions
|
|
@ -1,4 +1,5 @@
|
|||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Player;
|
||||
|
||||
namespace AcDream.UI.Abstractions.Panels.Vitals;
|
||||
|
||||
|
|
@ -8,17 +9,18 @@ namespace AcDream.UI.Abstractions.Panels.Vitals;
|
|||
/// <c>UpdateHealth (0x01C0)</c> GameEvent).
|
||||
///
|
||||
/// <para>
|
||||
/// <b>D.2a scope limits:</b>
|
||||
/// <b>Sources:</b>
|
||||
/// </para>
|
||||
///
|
||||
/// <list type="bullet">
|
||||
/// <item>HP comes from <see cref="CombatState"/> and is <b>percent-only</b>
|
||||
/// (0..1). Absolute current/max HP is not wired yet.</item>
|
||||
/// <item>Stamina / Mana are always <c>null</c> — those values live in
|
||||
/// <c>AppraiseInfoParser.CreatureProfile</c> (parsed from
|
||||
/// <c>PlayerDescription (0x0013)</c>) but the parsed record is
|
||||
/// currently discarded. Wiring a <c>LocalPlayerState</c> cache is
|
||||
/// a separate follow-up; see <c>docs/ISSUES.md</c>.</item>
|
||||
/// <item>HP — <see cref="CombatState.GetHealthPercent"/> (percent-only,
|
||||
/// updated by <c>UpdateHealth (0x01C0)</c>).</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>
|
||||
|
|
@ -33,16 +35,20 @@ namespace AcDream.UI.Abstractions.Panels.Vitals;
|
|||
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"/> instance. The
|
||||
/// GUID starts at 0; call <see cref="SetLocalPlayerGuid"/> once the
|
||||
/// live session assigns it.
|
||||
/// 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)
|
||||
public VitalsVM(CombatState combat, LocalPlayerState? localPlayer = null)
|
||||
{
|
||||
_combat = combat ?? throw new ArgumentNullException(nameof(combat));
|
||||
_local = localPlayer;
|
||||
_localPlayerGuid = 0;
|
||||
}
|
||||
|
||||
|
|
@ -61,15 +67,16 @@ public sealed class VitalsVM
|
|||
public float HealthPercent => _combat.GetHealthPercent(_localPlayerGuid);
|
||||
|
||||
/// <summary>
|
||||
/// Stamina percent (0..1) or <c>null</c> when absolute values aren't wired.
|
||||
/// D.2a always returns <c>null</c>; to be populated by a future
|
||||
/// <c>LocalPlayerState</c> that caches <c>PlayerDescription (0x0013)</c>.
|
||||
/// 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 => null;
|
||||
public float? StaminaPercent => _local?.StaminaPercent;
|
||||
|
||||
/// <summary>
|
||||
/// Mana percent (0..1) or <c>null</c> when absolute values aren't wired.
|
||||
/// Same status as <see cref="StaminaPercent"/>.
|
||||
/// Mana percent (0..1), or <c>null</c> under the same conditions as
|
||||
/// <see cref="StaminaPercent"/>.
|
||||
/// </summary>
|
||||
public float? ManaPercent => null;
|
||||
public float? ManaPercent => _local?.ManaPercent;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue