feat(movement): run/walk gaits — run by default, hold Shift to walk #180

Merged
panda merged 4 commits from feat/run-walk into main 2026-08-02 00:20:56 +02:00
Collaborator

Summary

Splits the single movement speed into two gaits: a faster run (the default) and a slower, deliberate walk (hold Shift). Movement stays server-authoritative — the client sends the gait as an intent and the server picks the per-step cooldown and enforces it as a floor, so a modified client cannot outrun the run cadence. Implements #177 (design + Invariants Check on the issue). Stamina is deliberately not part of this MVP — tracked in #178.

Reviewed with critical-design-review before any code; the agreed Invariants Check and Verification plan live on #177.

What changed

  • Wire — MoveRequest(Direction, Running); PlayerState/PlayerMoved carry Running so observers glide + animate the right gait. ProtocolVersion.Current 11 → 12 in the same change.
  • Server — two GameOptions: MoveCooldownTicks (walk, now 3 ≈ 300 ms — a touch slower than before) and RunCooldownTicks (1 ≈ 100 ms). MovementSystem keeps the pending intent's gait and gates the step on the matching cooldown; PlayerMobile.Running is the entity's current gait (transient, never serialized) and teleport/resurrect resets it.
  • Client — run by default, hold Shift to walk. The move intent is sent at the run cadence and the server paces the steps, so a walking player never under-sends. Mobile glides for a gait-dependent duration and Player plays the run cycle while running.
  • Art — extracted the real UO human/run_0..4 (body 400, action group 2) into the pack. To avoid regenerating every asset from whichever client is mounted, this adds a surgical extractor command human-anim <client> <action> <name> [fps] [pack-path] that patches one animation into the existing pack in place; pack also learned the run action for future full rebuilds. (UO art remains a temporary placeholder, to be redone with original art.)
  • Harness — move <dir> [run] / goto <x> <y> [run], with the step pacing following the gait so a timed goto reflects real walk vs run speed (docs/debug-harness.md updated).

How it was tested

  • Unit (World) — RunWalkTests: a run step applies in fewer ticks than a walk step; a client spamming the run intent cannot beat the run cooldown; the moved state carries the applied gait; a walk step leaves it false. Client.Core — GaitTimingTests (run glides quicker, run fallback speeds the leg cycle); HumanArtLayoutTests extended for the run_* names.
  • Whole solution builds 0 warnings; all tests green (7/7 projects).
  • Timed in-game (debug harness, fresh DB, same 10-tile path): walk 3.35 s vs run 1.21 s → run is 2.77× faster (≈335 ms/tile vs ≈121 ms/tile, matching the 3-tick and 1-tick cadences).

Screenshots

Mid-stride, walk vs run — the run row is the real UO run cycle (body pitched forward, arms pumping, extended stride), clearly distinct from the upright walk.

walk vs run stride

Run pose in world (first frame is walking for reference):

run pose in game

Notes / follow-ups

  • MoveCooldownTicks is also the creature chase cadence, so creatures now chase at the walk speed (3 ticks) and a running player can outrun them. That's an acceptable MVP consequence (and arguably right — you can sprint away), but if you'd rather creatures kept pace they need their own tunable; say the word and I'll split it.
  • Stamina (running should cost something) is #178.
## Summary Splits the single movement speed into two **gaits**: a faster **run** (the default) and a slower, deliberate **walk** (hold **Shift**). Movement stays server-authoritative — the client sends the gait as an *intent* and the server picks the per-step cooldown and enforces it as a floor, so a modified client cannot outrun the run cadence. Implements #177 (design + Invariants Check on the issue). Stamina is deliberately **not** part of this MVP — tracked in #178. Reviewed with `critical-design-review` before any code; the agreed Invariants Check and Verification plan live on #177. ## What changed - **Wire** — `MoveRequest(Direction, Running)`; `PlayerState`/`PlayerMoved` carry `Running` so observers glide + animate the right gait. `ProtocolVersion.Current` 11 → **12** in the same change. - **Server** — two `GameOptions`: `MoveCooldownTicks` (walk, now **3** ≈ 300 ms — a touch slower than before) and `RunCooldownTicks` (**1** ≈ 100 ms). `MovementSystem` keeps the pending intent's gait and gates the step on the matching cooldown; `PlayerMobile.Running` is the entity's current gait (transient, never serialized) and teleport/resurrect resets it. - **Client** — run by default, **hold Shift to walk**. The move intent is sent at the run cadence and the *server* paces the steps, so a walking player never under-sends. `Mobile` glides for a gait-dependent duration and `Player` plays the run cycle while running. - **Art** — extracted the real UO **`human/run_0..4`** (body 400, action group 2) into the pack. To avoid regenerating every asset from whichever client is mounted, this adds a surgical extractor command **`human-anim <client> <action> <name> [fps] [pack-path]`** that patches one animation into the existing pack in place; `pack` also learned the `run` action for future full rebuilds. (UO art remains a temporary placeholder, to be redone with original art.) - **Harness** — `move <dir> [run]` / `goto <x> <y> [run]`, with the step pacing following the gait so a timed `goto` reflects real walk vs run speed (`docs/debug-harness.md` updated). ## How it was tested - **Unit (`World`)** — `RunWalkTests`: a run step applies in fewer ticks than a walk step; a client spamming the run intent **cannot** beat the run cooldown; the moved state carries the applied gait; a walk step leaves it false. **Client.Core** — `GaitTimingTests` (run glides quicker, run fallback speeds the leg cycle); `HumanArtLayoutTests` extended for the `run_*` names. - **Whole solution builds 0 warnings; all tests green** (7/7 projects). - **Timed in-game** (debug harness, fresh DB, same 10-tile path): **walk 3.35 s vs run 1.21 s → run is 2.77× faster** (≈335 ms/tile vs ≈121 ms/tile, matching the 3-tick and 1-tick cadences). ### Screenshots **Mid-stride, walk vs run** — the run row is the real UO run cycle (body pitched forward, arms pumping, extended stride), clearly distinct from the upright walk. ![walk vs run stride](https://git.homelab.devncode.it/attachments/4935d8ef-dc8a-4235-86c1-fa5173514c3d) **Run pose in world** (first frame is walking for reference): ![run pose in game](https://git.homelab.devncode.it/attachments/46740631-5ca2-41e0-bf44-7e02f819195e) ## Notes / follow-ups - `MoveCooldownTicks` is also the **creature chase** cadence, so creatures now chase at the walk speed (3 ticks) and a running player can outrun them. That's an acceptable MVP consequence (and arguably right — you *can* sprint away), but if you'd rather creatures kept pace they need their own tunable; say the word and I'll split it. - Stamina (running should cost something) is **#178**.
feat(movement): run/walk gaits — run by default, hold Shift to walk (#177)
All checks were successful
ci / Lint & Test (pull_request) Successful in 2m25s
053fe3437f
fix(client): overlays keep pace with the run cycle (hair/beard/gear no longer detach)
All checks were successful
ci / Lint & Test (pull_request) Successful in 1m49s
542d945605
Author
Collaborator

Follow-up fix: overlays detaching while running

Reported after the first pass: the hair "chased" the character when running. Root cause was mine — DriveOverlay only played a layer's animation if (set.Has(anim)), and the overlay art (hair/beard/equip) had no run_* frames, so while the body played the run cycle each overlay was left neither played nor updated: frozen on its last frame, keeping that frame's own anchor, so it visibly drifted off the head.

Fixed properly, two parts:

  • Real run frames for every overlay — extracted run_* (action group 2) for all 7 hair styles, 5 beard styles and 3 equip kinds, so each overlay animates its own run cycle in true lockstep with the body (78 run entries in the pack now). The pack command also learned the run action for equip/hair/beard so future full rebuilds include them.
  • Robustness — DriveOverlay now falls back to the walk cycle when a layer lacks the body's action instead of freezing (a frozen overlay keeps a stale anchor and drifts). Covers e.g. the two beard styles whose client art only has 4 of the 5 stored facings.

The surgical extractor command was generalised from human-anim to body-anim <client> <body> <action> <name> <prefix> [fps] [pack] so any body/namespace can be patched into the existing pack.

Verified in-game: in the running poses the hair now sits on the head and follows the forward-leaning stride. Build 0 warnings, all tests green, CI green.

### Follow-up fix: overlays detaching while running Reported after the first pass: **the hair "chased" the character when running.** Root cause was mine — `DriveOverlay` only played a layer's animation `if (set.Has(anim))`, and the overlay art (hair/beard/equip) had **no `run_*` frames**, so while the body played the run cycle each overlay was left *neither played nor updated*: frozen on its last frame, keeping that frame's own anchor, so it visibly drifted off the head. Fixed properly, two parts: - **Real run frames for every overlay** — extracted `run_*` (action group 2) for all 7 hair styles, 5 beard styles and 3 equip kinds, so each overlay animates its own run cycle in true lockstep with the body (78 run entries in the pack now). The `pack` command also learned the `run` action for equip/hair/beard so future full rebuilds include them. - **Robustness** — `DriveOverlay` now falls back to the walk cycle when a layer lacks the body's action instead of freezing (a frozen overlay keeps a stale anchor and drifts). Covers e.g. the two beard styles whose client art only has 4 of the 5 stored facings. The surgical extractor command was generalised from `human-anim` to **`body-anim <client> <body> <action> <name> <prefix> [fps] [pack]`** so any body/namespace can be patched into the existing pack. Verified in-game: in the running poses the hair now sits on the head and follows the forward-leaning stride. Build 0 warnings, all tests green, CI green.
Merge remote-tracking branch 'origin/main' into feat/run-walk
All checks were successful
ci / Lint & Test (pull_request) Successful in 3m49s
848cb65b15
# Conflicts:
#	client/IsoMmo.Client/Screens/ServerMessageDispatcher.cs
#	client/IsoMmo.Client/assets/assets.isoa
#	src/IsoMmo.GameServer/Gameplay/MovementSystem.cs
#	src/IsoMmo.Shared/Protocol/ProtocolVersion.cs
Author
Collaborator

Realigned on main after #155 merged

Merged the new main (biome art + map + marsh move-cost) into this branch and resolved all four expected conflicts:

  • ProtocolVersion — both branches had claimed 12. Marsh feedback keeps 12 (it landed first, on main); the run/walk gait becomes 13, with both changelog lines preserved.
  • MovementSystem — the substantive one: the two gates are now composed rather than either/or. The gait picks the base cadence and the terrain scales it:
    MoveCostTicks(running ? runCooldownTicks : walkCooldownTicks, from, into) — so marsh slows a runner and a walker alike, using the costlier of from/into, and the result is still the floor the server enforces.
  • ServerMessageDispatcher — combined: MoveTo(tile, moved.Running) and the marsh step feedback fire on the same PlayerMoved.
  • assets.isoa — rebuilt rather than picked: took main's pack (biome land art, dual-grid masks, marsh ripple), re-applied own-art, then re-extracted the run animations. The merged pack carries 78 run entries (human + 7 hair + 5 beard + 3 equip), 50 land tiles, 16 masks and the ripple.

Also added the composition test the owner asked for — marsh × gait: a run step through marsh is slower than one on grass, yet still quicker than walking the marsh (you can sprint the bog, it just costs you).

Whole solution builds 0 warnings; all tests green (7/7 projects, 513 tests, marsh and run/walk suites side by side); CI green; branch mergeable.

### Realigned on `main` after #155 merged Merged the new `main` (biome art + map + marsh move-cost) into this branch and resolved all four expected conflicts: - **`ProtocolVersion`** — both branches had claimed 12. Marsh feedback keeps 12 (it landed first, on `main`); the run/walk gait becomes **13**, with both changelog lines preserved. - **`MovementSystem`** — the substantive one: the two gates are now **composed** rather than either/or. The gait picks the base cadence and the terrain scales it: `MoveCostTicks(running ? runCooldownTicks : walkCooldownTicks, from, into)` — so marsh slows a runner and a walker alike, using the costlier of from/into, and the result is still the floor the server enforces. - **`ServerMessageDispatcher`** — combined: `MoveTo(tile, moved.Running)` *and* the marsh step feedback fire on the same `PlayerMoved`. - **`assets.isoa`** — rebuilt rather than picked: took `main`'s pack (biome land art, dual-grid masks, marsh ripple), re-applied `own-art`, then re-extracted the run animations. The merged pack carries **78 run entries** (human + 7 hair + 5 beard + 3 equip), 50 land tiles, 16 masks and the ripple. Also added the composition test the owner asked for — **marsh × gait**: a run step through marsh is slower than one on grass, yet still quicker than walking the marsh (you *can* sprint the bog, it just costs you). Whole solution builds 0 warnings; all tests green (7/7 projects, 513 tests, marsh and run/walk suites side by side); CI green; branch mergeable.
Merge origin/main into feat/run-walk: reconcile run/walk gait with terrain-Z, chunked world, inventory (protocol 18)
All checks were successful
ci / Lua content lint (pull_request) Successful in 20s
ci / Lint & Test (pull_request) Successful in 5m52s
a1a5780457
Author
Collaborator

Updated to current main (was 43 commits behind → not mergeable). Conflicts resolved:

  • ProtocolVersion → 18 (the run/walk gait stacks on top of scripting/vitals/inventory 13–17).
  • MovementSystem / World — keeps both gait cadences (walk + run) and main's step-height / terrain-cost gate: MoveCostTicks(baseCooldown,...) + CanStep(..., maxStepZ); ctor is now MovementSystem(map, walkCooldownTicks, runCooldownTicks, maxStepZ).
  • Mobile (client) — preserves main's flat-foot/ground-Z heightfield split (glide interpolates both) and layers the gait (Running, per-gait glide duration).
  • assets.isoa — took main's current pack (all terrain/biome art). Run uses the sped-up walk-frame fallback (GaitTiming.FallbackFpsScale, which the feature was designed for); re-baking the real run frames into the current pack is a follow-up.

Gates green in a worktree off current main: whole solution builds with 0 warnings, 763 tests pass (incl. RunWalkTests + the terrain-Z suite), CSharpier clean. mergeable is now true.

Updated to current `main` (was 43 commits behind → not mergeable). Conflicts resolved: - **ProtocolVersion** → 18 (the run/walk gait stacks on top of scripting/vitals/inventory 13–17). - **MovementSystem / World** — keeps both gait cadences (walk + run) **and** main's step-height / terrain-cost gate: `MoveCostTicks(baseCooldown,...)` + `CanStep(..., maxStepZ)`; ctor is now `MovementSystem(map, walkCooldownTicks, runCooldownTicks, maxStepZ)`. - **Mobile** (client) — preserves main's flat-foot/ground-Z heightfield split (glide interpolates both) **and** layers the gait (`Running`, per-gait glide duration). - **assets.isoa** — took **main's** current pack (all terrain/biome art). Run uses the sped-up walk-frame fallback (`GaitTiming.FallbackFpsScale`, which the feature was designed for); re-baking the real run frames into the current pack is a follow-up. Gates green in a worktree off current main: whole solution builds with **0 warnings**, **763 tests** pass (incl. `RunWalkTests` + the terrain-Z suite), CSharpier clean. `mergeable` is now true.
panda scheduled this pull request to auto merge when all checks succeed 2026-08-02 00:17:46 +02:00
panda merged commit ddb4469670 into main 2026-08-02 00:20:56 +02:00
panda deleted branch feat/run-walk 2026-08-02 00:20:57 +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!180
No description provided.