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
79
src/AcDream.Core/Player/LocalPlayerState.cs
Normal file
79
src/AcDream.Core/Player/LocalPlayerState.cs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
namespace AcDream.Core.Player;
|
||||
|
||||
/// <summary>
|
||||
/// Cache of the local player's stamina + mana absolute values, populated
|
||||
/// from the <c>CreatureProfile</c> blob inside <c>PlayerDescription
|
||||
/// (0x0013)</c>. Health stays in <see cref="AcDream.Core.Combat.CombatState"/>
|
||||
/// because it has its own delta opcode (<c>UpdateHealth 0x01C0</c>); stam
|
||||
/// and mana don't, so without this cache <c>VitalsVM</c> can't surface
|
||||
/// percent-of-max bars even though the parser already decodes the
|
||||
/// absolute values.
|
||||
///
|
||||
/// <para>
|
||||
/// Filed by issue <c>#5</c> in <c>docs/ISSUES.md</c>. Once future delta
|
||||
/// opcodes for stam/mana arrive (or are recognised in the existing event
|
||||
/// stream) they update through the same <see cref="OnPlayerDescription"/>
|
||||
/// surface — keep the cache the single point of truth so VM consumers
|
||||
/// don't have to subscribe to multiple sources.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class LocalPlayerState
|
||||
{
|
||||
/// <summary>Current stamina (absolute), or <c>null</c> if never received.</summary>
|
||||
public uint? CurrentStamina { get; private set; }
|
||||
|
||||
/// <summary>Max stamina (absolute), or <c>null</c> if never received.</summary>
|
||||
public uint? MaxStamina { get; private set; }
|
||||
|
||||
/// <summary>Current mana (absolute), or <c>null</c> if never received.</summary>
|
||||
public uint? CurrentMana { get; private set; }
|
||||
|
||||
/// <summary>Max mana (absolute), or <c>null</c> if never received.</summary>
|
||||
public uint? MaxMana { get; private set; }
|
||||
|
||||
/// <summary>Fires after any field update via <see cref="OnPlayerDescription"/>.</summary>
|
||||
public event Action<LocalPlayerState>? Changed;
|
||||
|
||||
/// <summary>
|
||||
/// Stamina as a 0..1 fraction, or <c>null</c> when either current or max
|
||||
/// is missing or max is zero. Clamps to 1.0 if current > max (which
|
||||
/// the server can briefly report during buff transitions).
|
||||
/// </summary>
|
||||
public float? StaminaPercent => Percent(CurrentStamina, MaxStamina);
|
||||
|
||||
/// <summary>
|
||||
/// Mana as a 0..1 fraction, or <c>null</c> when either current or max
|
||||
/// is missing or max is zero. Same clamp rules as
|
||||
/// <see cref="StaminaPercent"/>.
|
||||
/// </summary>
|
||||
public float? ManaPercent => Percent(CurrentMana, MaxMana);
|
||||
|
||||
/// <summary>
|
||||
/// Apply a slice of the latest <c>CreatureProfile</c>. Each field is
|
||||
/// nullable: <c>null</c> means "no information" — preserve any previously
|
||||
/// known good value rather than wipe it. Fires <see cref="Changed"/> once
|
||||
/// after the update.
|
||||
/// </summary>
|
||||
public void OnPlayerDescription(
|
||||
uint? currentStamina,
|
||||
uint? maxStamina,
|
||||
uint? currentMana,
|
||||
uint? maxMana)
|
||||
{
|
||||
if (currentStamina.HasValue) CurrentStamina = currentStamina.Value;
|
||||
if (maxStamina.HasValue) MaxStamina = maxStamina.Value;
|
||||
if (currentMana.HasValue) CurrentMana = currentMana.Value;
|
||||
if (maxMana.HasValue) MaxMana = maxMana.Value;
|
||||
Changed?.Invoke(this);
|
||||
}
|
||||
|
||||
private static float? Percent(uint? current, uint? max)
|
||||
{
|
||||
if (current is not uint c) return null;
|
||||
if (max is not uint m || m == 0) return null;
|
||||
float r = (float)c / m;
|
||||
if (r < 0f) r = 0f;
|
||||
else if (r > 1f) r = 1f;
|
||||
return r;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue