Vitals HUD: show hits / mana / stamina (they are invisible today) #181

Closed
opened 2026-07-24 13:09:38 +02:00 by panda · 3 comments
Collaborator

Surfaced by the stamina design review (#178): the player cannot see any vital. There is no wire message carrying Hits/Mana to the client and no HUD element — they exist only server-side. Today you learn your HP from floating damage numbers and your mana from a "not enough mana" notice.

That is tolerable for HP/mana but becomes a real problem with stamina (#178), which gates running: without a bar the player just mysteriously stops sprinting.

Scope (proposed - needs its own design review)

  • A self-only wire message carrying the player's vitals (hits/maxHits, mana/maxMana, stamina/maxStam), sent on change and/or on a cadence. Self only - other players' vitals stay server-side (they are not observable today and shouldn't become so by accident).
  • A compact HUD (three bars or a small vitals block), drawn by a focused client component - not accreted into GameScreen (Screen HARD GATE).
  • ProtocolVersion bump in the same change.

Definition of Done (delta)

  • The self player's hits, mana and stamina are visible and update as they change (damage, casting, running).
  • Another player's vitals are not sent to observers (verified by inspecting what a second client receives).
  • The HUD lives in its own component; GameScreen only wires it.

Prereq/related: #178 (stamina), #177/#180 (run gait).

Surfaced by the stamina design review (#178): **the player cannot see any vital.** There is no wire message carrying Hits/Mana to the client and no HUD element — they exist only server-side. Today you learn your HP from floating damage numbers and your mana from a "not enough mana" notice. That is tolerable for HP/mana but becomes a real problem with **stamina** (#178), which gates running: without a bar the player just mysteriously stops sprinting. ## Scope (proposed - needs its own design review) - A self-only wire message carrying the player's vitals (hits/maxHits, mana/maxMana, stamina/maxStam), sent on change and/or on a cadence. **Self only** - other players' vitals stay server-side (they are not observable today and shouldn't become so by accident). - A compact HUD (three bars or a small vitals block), drawn by a focused client component - not accreted into `GameScreen` (Screen HARD GATE). - `ProtocolVersion` bump in the same change. ## Definition of Done (delta) - The self player's hits, mana and stamina are visible and update as they change (damage, casting, running). - Another player's vitals are **not** sent to observers (verified by inspecting what a second client receives). - The HUD lives in its own component; `GameScreen` only wires it. Prereq/related: #178 (stamina), #177/#180 (run gait).
Author
Collaborator

Agreed design (reviewed with critical-design-review)

Owner decisions: build on main now (STAM shows the Stamina => Dex stub until #185 lands, then one line points it at StamTenths); procedural HUD first, the gothic frame (from the owner's mockup) as a follow-up skin; no enemy HP bars (self-only).

Design

  • Wire — new self-only VitalsState(Hits, MaxHits, Stam, MaxStam, Mana, MaxMana, Str, Dex, Int) ServerMessage; bump ProtocolVersion. Sent only to the owning connection, like InventoryState — never broadcast, so other players' vitals stay server-side.
  • Change detection — each tick, PlayerRegistry.DrainVitalsChanges() compares each player's current vitals to the last sent (stored on PlayerMobile), emitting only on change. Robust across every mutation site (damage, cast, regen, /set) with no dirty-flag plumbing; the last-sent snapshot lives on the entity (not a side dict). WorldTick sends each to its owner via connections.SendToAsync(id, …), exactly like SkillGains.
  • Client — ClientWorld.Vitals set by the dispatcher; a VitalsHud component drawn by WorldRenderer (Screen HARD GATE: nothing in GameScreen); the pure fill fraction (current/max) in Client.Core, unit-tested. MVP draws three bars (HP red / STAM green / MANA blue) + value text + STR/DEX/INT plates, laid out like the mockup.

Invariants Check

Scope ✓ (only the self vitals HUD). Server-authoritative ✓ (client displays, never computes/sends). GM auth N/A. Identity N/A. Protocol versioned ✓ (new VitalsState → bump). String catalog N/A (static client UI labels, not server copy). Single-threaded sim ✓ (drain in the tick, sim thread). World.cs HARD GATE ✓ (drain delegated to PlayerRegistry). Screen HARD GATE ✓ (VitalsHud component, not GameScreen). Client engine-independence ✓ (fill fraction pure in Client.Core). Gameplay/Networking separation ✓. Act on the instance ✓ (last-sent snapshot on PlayerMobile, no side dict). Extend by type not switch ✓ (a DTO, not a type switch). Server-paced N/A. Persistence (GameServer) ✓ (nothing new persisted; HP/mana already are). Persistence (Auth) N/A. Process separation N/A. Typed options N/A. Broadcasts/AoI ✓ — VitalsState is self-only, never broadcast (other players' vitals stay server-side). Multi-platform ✓ (.NET/MonoGame). Assets ✓ (procedural MVP needs no art; the frame skin will be real gump/* art). Asset naming ✓ (gump/vitals_frame when skinned). ModernUO ✓ (self-only status bar; enemy target-bar deferred). Docs & DoD ✓.

No blocking verdict.

Verification plan

  • Client.Core unit: VitalsBar.Fill(current, max) clamps 0–1.
  • World unit: damaging / spending mana produces a VitalsState for the owner only (trust boundary: an observer does not receive it) — asserted via a WebSocket flow (CreateWebSocketClient) that a second client gets no VitalsState for the first.
  • Screenshot: fresh DB → in world → HUD visible; /set hits 20 (GM) → the HP bar drops.

Definition of Done (delta)

  • The self player sees HP, stamina and mana (current/max) and STR/DEX/INT, updating as they change (damage, casting, /set).
  • Another player's vitals are not sent to observers (verified on the wire).
  • The HUD lives in its own component; GameScreen only wires it.
  • ProtocolVersion bumped in the same change; VitalsState round-trips.
## Agreed design (reviewed with critical-design-review) Owner decisions: **build on `main` now** (STAM shows the `Stamina => Dex` stub until #185 lands, then one line points it at `StamTenths`); **procedural HUD first**, the gothic frame (from the owner's mockup) as a follow-up skin; **no enemy HP bars** (self-only). ### Design - **Wire** — new self-only `VitalsState(Hits, MaxHits, Stam, MaxStam, Mana, MaxMana, Str, Dex, Int)` ServerMessage; **bump `ProtocolVersion`**. Sent only to the owning connection, like `InventoryState` — never broadcast, so other players' vitals stay server-side. - **Change detection** — each tick, `PlayerRegistry.DrainVitalsChanges()` compares each player's current vitals to the last sent (stored on `PlayerMobile`), emitting only on change. Robust across every mutation site (damage, cast, regen, `/set`) with no dirty-flag plumbing; the last-sent snapshot lives on the entity (not a side dict). `WorldTick` sends each to its owner via `connections.SendToAsync(id, …)`, exactly like `SkillGains`. - **Client** — `ClientWorld.Vitals` set by the dispatcher; a `VitalsHud` component drawn by `WorldRenderer` (Screen HARD GATE: nothing in `GameScreen`); the pure fill fraction (`current/max`) in Client.Core, unit-tested. MVP draws three bars (HP red / STAM green / MANA blue) + value text + STR/DEX/INT plates, laid out like the mockup. ### Invariants Check Scope ✓ (only the self vitals HUD). Server-authoritative ✓ (client displays, never computes/sends). GM auth N/A. Identity N/A. Protocol versioned ✓ (new `VitalsState` → bump). String catalog N/A (static client UI labels, not server copy). Single-threaded sim ✓ (drain in the tick, sim thread). `World.cs` HARD GATE ✓ (drain delegated to `PlayerRegistry`). `Screen` HARD GATE ✓ (`VitalsHud` component, not `GameScreen`). Client engine-independence ✓ (fill fraction pure in Client.Core). Gameplay/Networking separation ✓. Act on the instance ✓ (last-sent snapshot on `PlayerMobile`, no side dict). Extend by type not switch ✓ (a DTO, not a type switch). Server-paced N/A. Persistence (GameServer) ✓ (nothing new persisted; HP/mana already are). Persistence (Auth) N/A. Process separation N/A. Typed options N/A. **Broadcasts/AoI ✓ — `VitalsState` is self-only, never broadcast (other players' vitals stay server-side)**. Multi-platform ✓ (.NET/MonoGame). Assets ✓ (procedural MVP needs no art; the frame skin will be real `gump/*` art). Asset naming ✓ (`gump/vitals_frame` when skinned). ModernUO ✓ (self-only status bar; enemy target-bar deferred). Docs & DoD ✓. No blocking verdict. ### Verification plan - Client.Core unit: `VitalsBar.Fill(current, max)` clamps 0–1. - World unit: damaging / spending mana produces a `VitalsState` for the **owner only** (trust boundary: an observer does not receive it) — asserted via a WebSocket flow (`CreateWebSocketClient`) that a second client gets no `VitalsState` for the first. - Screenshot: fresh DB → in world → HUD visible; `/set hits 20` (GM) → the HP bar drops. ## Definition of Done (delta) - The self player sees HP, stamina and mana (current/max) and STR/DEX/INT, updating as they change (damage, casting, `/set`). - Another player's vitals are **not** sent to observers (verified on the wire). - The HUD lives in its own component; `GameScreen` only wires it. - `ProtocolVersion` bumped in the same change; `VitalsState` round-trips.
Author
Collaborator

Implemented in #205 (branch feat/vitals-hud, on top of current main).

What landed (the "1 poi 2" first pass):

  • New self-only VitalsState wire message (hits/maxHits, stam/maxStam, mana/maxMana + str/dex/int); ProtocolVersion.Current → 14.
  • Server emits it only on change, per player, to that player's own connection (snapshot compare on PlayerMobile.LastSentVitals; SendToAsync by id) — scoped exactly like InventoryState/SkillGains, never broadcast, never to observers.
  • Client VitalsHud (bottom-left): STR/DEX/INT plates + HP/STAM/MANA bars with current/max, laid out to the mockup. Thin view owned by WorldRenderer; fill math in Client.Core (VitalsBar, NaN-safe).

DoD status:

  • ✅ Self-only wire message carrying vitals — VitalsState, verified by the WebSocket trust-boundary test (self receives it, a nearby observer never does).
  • ✅ HUD shows all three pools + the three stats, updating live — screenshots in #205 (full → /set hits 18 → HP bar drains to 18/50).
  • ✅ Server-authoritative & change-driven — unit test: emitted on first tick, then only on change, carrying the new value.
  • ✅ Full gate green (CSharpier + analyzers 0 warnings, dotnet test across all projects, whole-solution build).
  • ⏳ STAM is wired but reads full until stamina drain lands (#185) — expected; the bar is ready for it.

Part 2 (deferred, as agreed): the carved gothic frame skin over this procedural layout, once the frame art exists — tracked as a follow-up, not this PR.

Implemented in #205 (branch `feat/vitals-hud`, on top of current `main`). **What landed (the "1 poi 2" first pass):** - New self-only `VitalsState` wire message (hits/maxHits, stam/maxStam, mana/maxMana + str/dex/int); `ProtocolVersion.Current` → **14**. - Server emits it **only on change**, per player, to that player's **own** connection (snapshot compare on `PlayerMobile.LastSentVitals`; `SendToAsync` by id) — scoped exactly like `InventoryState`/`SkillGains`, never broadcast, never to observers. - Client `VitalsHud` (bottom-left): STR/DEX/INT plates + HP/STAM/MANA bars with current/max, laid out to the mockup. Thin view owned by `WorldRenderer`; fill math in `Client.Core` (`VitalsBar`, NaN-safe). **DoD status:** - ✅ Self-only wire message carrying vitals — `VitalsState`, verified by the WebSocket trust-boundary test (self receives it, a nearby observer never does). - ✅ HUD shows all three pools + the three stats, updating live — screenshots in #205 (full → `/set hits 18` → HP bar drains to 18/50). - ✅ Server-authoritative & change-driven — unit test: emitted on first tick, then only on change, carrying the new value. - ✅ Full gate green (CSharpier + analyzers 0 warnings, `dotnet test` across all projects, whole-solution build). - ⏳ **STAM is wired but reads full** until stamina drain lands (#185) — expected; the bar is ready for it. **Part 2 (deferred, as agreed):** the carved gothic frame *skin* over this procedural layout, once the frame art exists — tracked as a follow-up, not this PR.
Owner

Risolta da PR #205 (33fa395): Vitals HUD self-only — VitalsState (hits/mana/stam + str/dex/int) inviato solo alla connessione proprietaria, ProtocolVersion→14, HUD in componente dedicato (VitalsHud.cs/VitalsBar.cs) senza accrescere GameScreen. DoD soddisfatta. Chiudo — completata.

Risolta da **PR #205** (`33fa395`): Vitals HUD self-only — `VitalsState` (hits/mana/stam + str/dex/int) inviato solo alla connessione proprietaria, `ProtocolVersion`→14, HUD in componente dedicato (`VitalsHud.cs`/`VitalsBar.cs`) senza accrescere `GameScreen`. DoD soddisfatta. Chiudo — completata.
marco closed this issue 2026-07-25 19:06:16 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
marco/IsoMmo#181
No description provided.