feat(gameplay): stamina — running drains it, exhaustion forces a walk (#178) #185

Merged
panda merged 6 commits from feat/stamina into main 2026-08-02 00:56:42 +02:00
Collaborator

Summary

Makes running cost something, which is what turns the run gait (#177/#180) and the marsh move-cost (#138) into an actual decision instead of "marsh is just slower". Implements #178 to the design agreed and persisted on that issue.

Stacked on #180 — based on feat/run-walk, not main, so this diff is only the stamina delta. Merge #180 first.

What changed

  • The vital — the Mobile.Stamina => Dex stub becomes MaxStam => Dex, with a real pool held in tenths (StamTenths, MaxStamTenths => Dex * 10) and a whole-point Stam view. Intrinsic clamped mutators live on the entity next to SetHits/SetMana: SetStam, SpendStaminaTenths, RecoverStaminaTenths. A DEX change re-clamps it like the other vitals.
  • Drain — only on run steps; walking is free. The cost is scaled by the same terrain multiplier as the cooldown (reusing TileMap.MoveCostTicks on the drain), so sprinting a marsh burns ~1.6x what open ground does. Tenths are what keep that 1.6x from rounding into 2x.
  • Exhaustion — the pool emptying latches Exhausted; while spent, CanRun is false and MovementSystem applies the walk cooldown and skips the drain whatever gait the client asked for. The latch clears only once the pool climbs back past StamResumePercent (15%), so the gait can't flip on every regen tick.
  • Regen — MovementSystem.RegenerateStamina, on a tick cadence mirroring the mana regen, for everyone who did not run that tick. Deliberately runs after ApplyPending, otherwise a sprinting player would still regenerate.
  • Feedback — new SystemMessageId.TooWindedToRun + catalog template, sent (throttled, 2s) when a player asks to run while spent. ProtocolVersion 13 → 14. Read together with the overweight check in a single sim invoke.
  • Tunables — StamRegenTicks, StamRegenTenths, RunDrainTenths, StamResumePercent on GameOptions. Defaults give roughly a 12s sprint and a ~20s recovery at DEX 50.
  • Transient — stamina is not serialized; you start and relog rested. No save-format change.

How it was tested

  • New StaminaTests against World: running drains while walking is free; sustained running empties the pool and drops CanRun; an exhausted player still sending Running moves at the walk cadence (the trust-boundary case); stamina recovers while not running; the resume threshold prevents gait flapping; a marsh run costs more than a grass run.
  • Two of those deliberately disable regen to isolate the drain — with it on, the forced walk after exhaustion immediately starts refilling, which is correct behaviour and is covered by its own test.
  • Whole solution builds 0 warnings; all tests green (7/7 projects, 520 tests).

Notes

  • No HUD: vitals are still invisible to the player (there is no wire message for hits/mana either). The throttled cue is the MVP feedback; the real fix is the vitals HUD tracked in #181.
  • Balance numbers are a first pass and deliberately all tunable.
## Summary Makes running **cost** something, which is what turns the run gait (#177/#180) and the marsh move-cost (#138) into an actual decision instead of "marsh is just slower". Implements #178 to the design agreed and persisted on that issue. **Stacked on #180** — based on `feat/run-walk`, not `main`, so this diff is only the stamina delta. Merge #180 first. ## What changed - **The vital** — the `Mobile.Stamina => Dex` stub becomes `MaxStam => Dex`, with a real pool held in **tenths** (`StamTenths`, `MaxStamTenths => Dex * 10`) and a whole-point `Stam` view. Intrinsic clamped mutators live on the entity next to `SetHits`/`SetMana`: `SetStam`, `SpendStaminaTenths`, `RecoverStaminaTenths`. A DEX change re-clamps it like the other vitals. - **Drain** — only on **run** steps; walking is free. The cost is scaled by the same terrain multiplier as the cooldown (reusing `TileMap.MoveCostTicks` on the drain), so sprinting a marsh burns ~1.6x what open ground does. Tenths are what keep that 1.6x from rounding into 2x. - **Exhaustion** — the pool emptying latches `Exhausted`; while spent, `CanRun` is false and `MovementSystem` applies the **walk** cooldown and skips the drain *whatever gait the client asked for*. The latch clears only once the pool climbs back past `StamResumePercent` (15%), so the gait can't flip on every regen tick. - **Regen** — `MovementSystem.RegenerateStamina`, on a tick cadence mirroring the mana regen, for everyone who did **not** run that tick. Deliberately runs *after* `ApplyPending`, otherwise a sprinting player would still regenerate. - **Feedback** — new `SystemMessageId.TooWindedToRun` + catalog template, sent (throttled, 2s) when a player asks to run while spent. `ProtocolVersion` 13 → **14**. Read together with the overweight check in a **single** sim invoke. - **Tunables** — `StamRegenTicks`, `StamRegenTenths`, `RunDrainTenths`, `StamResumePercent` on `GameOptions`. Defaults give roughly a 12s sprint and a ~20s recovery at DEX 50. - **Transient** — stamina is not serialized; you start and relog rested. No save-format change. ## How it was tested - New `StaminaTests` against `World`: running drains while walking is free; sustained running empties the pool and drops `CanRun`; **an exhausted player still sending `Running` moves at the walk cadence** (the trust-boundary case); stamina recovers while not running; the resume threshold prevents gait flapping; **a marsh run costs more than a grass run**. - Two of those deliberately disable regen to isolate the drain — with it on, the forced walk after exhaustion immediately starts refilling, which is correct behaviour and is covered by its own test. - Whole solution builds **0 warnings**; all tests green (7/7 projects, 520 tests). ## Notes - No HUD: vitals are still invisible to the player (there is no wire message for hits/mana either). The throttled cue is the MVP feedback; the real fix is the vitals HUD tracked in **#181**. - Balance numbers are a first pass and deliberately all tunable.
feat(gameplay): stamina — running drains it, exhaustion forces a walk (#178)
All checks were successful
ci / Lint & Test (pull_request) Successful in 2m22s
8c2b0b02d2
panda changed target branch from feat/run-walk to main 2026-08-02 00:20:57 +02:00
Merge origin/main into feat/stamina: reconcile stamina with run/walk-on-main + terrain-Z + vitals/inventory (protocol 19)
All checks were successful
ci / Lua content lint (pull_request) Successful in 9s
ci / Lint & Test (pull_request) Successful in 5m29s
334e2b2e52
Author
Collaborator

Retargeted base to main (its old base feat/run-walk was merged & deleted) and merged current main in. Conflicts resolved:

  • ProtocolVersion -> 19 (stamina's TooWindedToRun stacks on run/walk 18 now on main).
  • MovementSystem -> (map, walk, run, maxStepZ, StaminaOptions); keeps stamina's sprinting = running && CanRun gating and the terrain-scaled run drain (SpendStaminaTenths(MoveCostTicks(RunDrainTenths,...))).
  • World ctor gains stamina alongside main's maxStepZ/itemCatalog/runCooldownTicks.
  • PlayerMobile.ToVitals now feeds the real drainable pool: Stam (=StamTenths/10) + MaxStam (the #181 stub's predicted one-liner).
  • GameSessionHandler keeps stamina's (Overweight, Winded) move result -> TooWindedToRun notice.
  • client Mobile / assets.isoa — took main's (run/walk gait already reconciled with terrain there).

Gates green in a worktree off current main: 0 warnings, 774 tests, CSharpier clean.

Retargeted base to `main` (its old base `feat/run-walk` was merged & deleted) and merged current `main` in. Conflicts resolved: - **ProtocolVersion** -> 19 (stamina's `TooWindedToRun` stacks on run/walk 18 now on main). - **MovementSystem** -> `(map, walk, run, maxStepZ, StaminaOptions)`; keeps stamina's `sprinting = running && CanRun` gating and the terrain-scaled run drain (`SpendStaminaTenths(MoveCostTicks(RunDrainTenths,...))`). - **World** ctor gains `stamina` alongside main's `maxStepZ`/`itemCatalog`/`runCooldownTicks`. - **PlayerMobile.ToVitals** now feeds the real drainable pool: `Stam` (=StamTenths/10) + `MaxStam` (the #181 stub's predicted one-liner). - **GameSessionHandler** keeps stamina's `(Overweight, Winded)` move result -> `TooWindedToRun` notice. - **client Mobile / assets.isoa** — took main's (run/walk gait already reconciled with terrain there). Gates green in a worktree off current main: **0 warnings**, **774 tests**, CSharpier clean.
Merge origin/main into feat/stamina: reconcile with combat formulas (#217) — World ctor takes both stamina + combatFormulas
All checks were successful
ci / Lua content lint (pull_request) Successful in 16s
ci / Lint & Test (pull_request) Successful in 6m6s
8299562c27
panda merged commit adbfb7724f into main 2026-08-02 00:56:42 +02:00
panda deleted branch feat/stamina 2026-08-02 00:56:43 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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!185
No description provided.