Biomes C: terrain move-cost (marsh slow) for all movers #138

Closed
opened 2026-07-22 13:29:06 +02:00 by panda · 5 comments
Collaborator

Depends on #136. Adds the marsh slow as a server-authoritative per-terrain move-cost, applied to every grounded mover. Design: docs/game-design/systems/biomes.md (parent #135).

Scope

  • Shared: TileMap.MoveCostTicks(from, into) = base * max(cost(from), cost(into)) (direction-independent; no free exit step).
  • MovementSystem: scale the player move cooldown by the transition cost (today's gate: currentTick - LastMoveTick < moveCooldownTicks). Logic stays in MovementSystem (World-owned component), not World.cs.
  • CreatureSystem: scale the creature chase/wander cadence by the same helper (today a fixed cadence) -> marsh chases are symmetric.
  • Add an IgnoresTerrainMoveCost mover capability (flying/floating), default false; Teleport//tp stays cost-exempt.
  • Client: mirror moveCost locally for movement feel (authority stays server-side).
  • Feedback: per-step marsh ripple VFX (existing VFX system) + a one-time SystemNotice on first marsh step -- new SystemMessageId + SystemMessageCatalog template (no loose strings).

Definition of Done (delta)

  • A timed straight run through marsh takes longer than the same run on grassland.
  • A creature chasing a player through marsh is slowed by the same factor (timed run, equal cadence) -- marsh is not a mob-favored trap.
  • A modified/fast client cannot cross marsh at normal speed (server rejects the early move).
  • First marsh step shows the one-time notice + per-step ripple; the slow never reads as lag.
Depends on #136. Adds the marsh **slow** as a server-authoritative per-terrain move-cost, applied to every grounded mover. Design: `docs/game-design/systems/biomes.md` (parent #135). ## Scope - `Shared`: `TileMap.MoveCostTicks(from, into)` = `base * max(cost(from), cost(into))` (direction-independent; no free exit step). - `MovementSystem`: scale the player move cooldown by the transition cost (today's gate: `currentTick - LastMoveTick < moveCooldownTicks`). Logic stays in `MovementSystem` (World-owned component), not `World.cs`. - `CreatureSystem`: scale the creature chase/wander cadence by the same helper (today a **fixed** cadence) -> marsh chases are symmetric. - Add an `IgnoresTerrainMoveCost` mover capability (flying/floating), default false; `Teleport`/`/tp` stays cost-exempt. - Client: mirror `moveCost` locally for movement feel (authority stays server-side). - Feedback: per-step marsh **ripple VFX** (existing VFX system) + a **one-time `SystemNotice`** on first marsh step -- new `SystemMessageId` + `SystemMessageCatalog` template (no loose strings). ## Definition of Done (delta) - A timed straight run through marsh takes **longer** than the same run on grassland. - A **creature chasing a player through marsh is slowed by the same factor** (timed run, equal cadence) -- marsh is not a mob-favored trap. - A modified/fast client **cannot** cross marsh at normal speed (server rejects the early move). - First marsh step shows the one-time notice + per-step ripple; the slow never reads as lag.
Owner

⏭️ Heads-up @andrea: #143 (full tiledata data-file) is being implemented now as the foundation for the #113 map editor (owner's call). It absorbs TerrainInfo + BiomeVisuals + StaticCatalog + the terrain legend into one content/tiledata.txt and replaces the TerrainType enum with string ids. This touches the #142 foundation this issue builds on — when you pick it up you'll rebase onto the new model (terrain = string id, flags/art/tint read from TileData). No ProtocolVersion change. Design + invariants recorded on #143.

⏭️ Heads-up @andrea: #143 (full tiledata data-file) is being implemented now as the foundation for the #113 map editor (owner's call). It **absorbs `TerrainInfo` + `BiomeVisuals` + `StaticCatalog` + the terrain legend into one `content/tiledata.txt`** and **replaces the `TerrainType` enum with string ids**. This touches the #142 foundation this issue builds on — when you pick it up you'll rebase onto the new model (terrain = string id, flags/art/tint read from `TileData`). No `ProtocolVersion` change. Design + invariants recorded on #143.
Owner

⏭️ Post-#143 note: the terrain foundation changed under this. TerrainType/TerrainInfo are gone — terrain is now a string id defined in content/tiledata.txt, read as a TerrainDef via TileData. So the marsh move-cost becomes a column on the terrain row (a TerrainDef field), not a TerrainInfo change. The server-side MoveCostTicks/MovementSystem/CreatureSystem scaling still applies; just source the per-terrain cost from TileData. No ProtocolVersion change (terrain isn't wire).

⏭️ Post-#143 note: the terrain foundation changed under this. `TerrainType`/`TerrainInfo` are **gone** — terrain is now a string id defined in **`content/tiledata.txt`**, read as a `TerrainDef` via `TileData`. So the **marsh move-cost becomes a column on the `terrain` row** (a `TerrainDef` field), not a `TerrainInfo` change. The server-side `MoveCostTicks`/`MovementSystem`/`CreatureSystem` scaling still applies; just source the per-terrain cost from `TileData`. No `ProtocolVersion` change (terrain isn't wire).
Author
Collaborator

Technical design review — agreed (go-with-changes)

Ran critical-design-review before implementation (branch feat/biomes-art).

Design: TerrainDef gains a MoveCost float (default 1); a trailing optional column in
content/tiledata.txt (marsh 1.6). TileMap.MoveCostTicks(baseTicks, from, into) = ceil(baseTicks * max(cost(from), cost(into))) — direction-independent, so any step touching marsh is
slow (no free exit step). MovementSystem.ApplyPending and CreatureSystem.Step replace the fixed
cooldown/cadence with the scaled one. All server-side.

Decisions: (1) add the IgnoresTerrainMoveCost exemption hook now (on the creature, default
false; players always affected). (2) #138 = server core (players + creatures + tests) + verify/fix
rubber-band; the marsh ripple VFX + one-time SystemNotice are a separate follow-up.

Invariants Check (vs CLAUDE.md ## Design checklist)

  1. Scope — ✓ exactly #138; no extra system.
  2. Server-authoritative — ✓ cost scales the gate in MovementSystem.ApplyPending (authoritative tick); client sends only MoveRequest; early move rejected.
  3. GM authorization — N/A — no admin action; /tp stays cost-exempt.
  4. Identity model — N/A — no account/identity change.
  5. Protocol versioned — ✓ move-cost is a tiledata terrain property, not on the wire; surfaces via existing PlayerMoved/MobileState → no bump.
  6. String catalog — N/A for the server core (the deferred first-marsh notice will be a SystemMessageId).
  7. Single-threaded sim — ✓ cost read on the sim thread; TileMap.MoveCostTicks is a pure read of immutable terrain; no new lock/state.
  8. World.cs HARD GATE — ✓ logic in MovementSystem/CreatureSystem + TileMap; World keeps delegating.
  9. Screen HARD GATE — N/A (deferred client VFX goes in a component, not the Screen).
  10. Client engine-independence — N/A for the core; any client mirroring reuses TileMap.MoveCostTicks (Shared).
  11. Gameplay/Networking separation — ✓ rule in Gameplay/ + TileMap; networking untouched.
  12. Act on the instance — ✓ no side-collection; derived from terrain data + the entity's own LastMoveTick.
  13. Extend by type, not switch — ✓ move-cost is a TerrainDef data field; MoveCostTicks is a max() formula, no switch.
  14. Server-paced actions — ✓ the slow advances on ticks; a modified client can't compress it; publicly observable.
  15. Persistence (GameServer) — N/A — terrain property from tiledata, not saved per-entity state.
  16. Persistence (Auth) — N/A.
  17. Process separation — N/A.
  18. Typed options — ✓ base moveCooldownTicks is a bound GameOptions; multipliers live in tiledata, no IConfiguration read.
  19. Broadcasts absolute / AoI — ✓ surfaces via existing absolute-position broadcasts; no new observable entity/event in the core.
  20. Multi-platform — ✓ pure .NET.
  21. Assets required — N/A — the core adds no rendered content (marsh art already landed #137).
  22. Asset naming HARD GATE — N/A — no new asset/tiledata id (a column on existing rows).
  23. ModernUO — ✓ follows ModernUO per-tile TileData movement flags; diverges with a simple max(from,into) cooldown scale (no full stamina/movement system).
  24. Docs & DoD same change — ✓ update the tiledata format header/comment + the biomes design "marsh slow" note; DoD below.

No ✗, no HARD GATE tripped.

Definition of Done (delta)

  • Crossing marsh is measurably slower than grassland (timed straight run of N tiles).
  • A creature chasing a player through marsh is slowed by the same factor (timed run) — marsh is not a mob-favoured trap.
  • A modified/fast client cannot cross marsh at normal speed (server rejects the early move).
  • A mover flagged IgnoresTerrainMoveCost crosses marsh at normal speed (unit test; no such creature ships yet).
  • No rubber-banding on the client at the marsh border.
## Technical design review — agreed (go-with-changes) Ran `critical-design-review` before implementation (branch `feat/biomes-art`). **Design:** `TerrainDef` gains a `MoveCost` float (default 1); a trailing optional column in `content/tiledata.txt` (marsh `1.6`). `TileMap.MoveCostTicks(baseTicks, from, into) = ceil(baseTicks * max(cost(from), cost(into)))` — direction-independent, so any step touching marsh is slow (no free exit step). `MovementSystem.ApplyPending` and `CreatureSystem.Step` replace the fixed cooldown/cadence with the scaled one. All server-side. **Decisions:** (1) add the `IgnoresTerrainMoveCost` exemption hook now (on the creature, default false; players always affected). (2) #138 = server core (players + creatures + tests) + verify/fix rubber-band; the marsh ripple VFX + one-time `SystemNotice` are a **separate follow-up**. ### Invariants Check (vs CLAUDE.md ## Design checklist) 1. Scope — ✓ exactly #138; no extra system. 2. Server-authoritative — ✓ cost scales the gate in `MovementSystem.ApplyPending` (authoritative tick); client sends only `MoveRequest`; early move rejected. 3. GM authorization — N/A — no admin action; `/tp` stays cost-exempt. 4. Identity model — N/A — no account/identity change. 5. Protocol versioned — ✓ move-cost is a tiledata terrain property, not on the wire; surfaces via existing `PlayerMoved`/`MobileState` → no bump. 6. String catalog — N/A for the server core (the deferred first-marsh notice will be a `SystemMessageId`). 7. Single-threaded sim — ✓ cost read on the sim thread; `TileMap.MoveCostTicks` is a pure read of immutable terrain; no new lock/state. 8. World.cs HARD GATE — ✓ logic in `MovementSystem`/`CreatureSystem` + `TileMap`; World keeps delegating. 9. Screen HARD GATE — N/A (deferred client VFX goes in a component, not the Screen). 10. Client engine-independence — N/A for the core; any client mirroring reuses `TileMap.MoveCostTicks` (Shared). 11. Gameplay/Networking separation — ✓ rule in `Gameplay/` + `TileMap`; networking untouched. 12. Act on the instance — ✓ no side-collection; derived from terrain data + the entity's own `LastMoveTick`. 13. Extend by type, not switch — ✓ move-cost is a `TerrainDef` data field; `MoveCostTicks` is a `max()` formula, no switch. 14. Server-paced actions — ✓ the slow advances on ticks; a modified client can't compress it; publicly observable. 15. Persistence (GameServer) — N/A — terrain property from tiledata, not saved per-entity state. 16. Persistence (Auth) — N/A. 17. Process separation — N/A. 18. Typed options — ✓ base `moveCooldownTicks` is a bound `GameOptions`; multipliers live in tiledata, no `IConfiguration` read. 19. Broadcasts absolute / AoI — ✓ surfaces via existing absolute-position broadcasts; no new observable entity/event in the core. 20. Multi-platform — ✓ pure .NET. 21. Assets required — N/A — the core adds no rendered content (marsh art already landed #137). 22. Asset naming HARD GATE — N/A — no new asset/tiledata id (a column on existing rows). 23. ModernUO — ✓ follows ModernUO per-tile `TileData` movement flags; diverges with a simple `max(from,into)` cooldown scale (no full stamina/movement system). 24. Docs & DoD same change — ✓ update the tiledata format header/comment + the biomes design "marsh slow" note; DoD below. No ✗, no HARD GATE tripped. ### Definition of Done (delta) - Crossing marsh is measurably slower than grassland (timed straight run of N tiles). - A creature chasing a player through marsh is slowed by the **same** factor (timed run) — marsh is not a mob-favoured trap. - A modified/fast client cannot cross marsh at normal speed (server rejects the early move). - A mover flagged `IgnoresTerrainMoveCost` crosses marsh at normal speed (unit test; no such creature ships yet). - No rubber-banding on the client at the marsh border.
Author
Collaborator

#138 — Done (server move-cost + client feedback)

Landed on feat/biomes-art (PR #155), CI green. Whole solution builds 0 warnings; 507 tests green.

  • Server (move-cost): TileMap.MoveCostTicks(base, from, into) = ceil(base * max(cost(from), cost(into))) (symmetric — no free exit step). MovementSystem scales the player cooldown; CreatureSystem scales the creature cadence; BaseCreature.IgnoresTerrainMoveCost (default false) exempts flying kinds; teleport stays exempt. Marsh moveCost = 1.6 via a new optional column in content/tiledata.txt.
  • Client (feedback): derived purely from the authoritative PlayerMoved + the shared TileMap (no wire event). MarshFeedback.Decide(destMoveCost, isSelf, hintShown) (pure, Client.Core) → per-step ground ripple (RippleEffect, authored effect/marsh_ripple) for any mover, plus a one-time SystemMessageId.MarshSlowsYou for the local player. ProtocolVersion 11→12 (new protocol enum value).

Note / divergence from the scope checklist: the "mirror move-cost client-side for feel" item was dropped as unnecessary — the client does not predict movement (it renders authoritative positions from PlayerMoved), so there is no rubber-band to correct. Confirmed by inspection of GameInputController/ServerMessageDispatcher.

Invariants Check (client-feedback delta — server part reviewed when the move-cost landed)

  • Scope ✓ only DoD item 4 (feedback), no new system.
  • Server-authoritative ✓ client only reacts to authoritative PlayerMoved; the slow itself stays server-enforced.
  • GM auth N/A — no admin action.
  • Identity model N/A — no account/claim change.
  • Protocol versionedProtocolVersion 11→12 for the new SystemMessageId.MarshSlowsYou (protocol enum).
  • String catalog ✓ new SystemMessageId + catalog template; no loose literal.
  • Single-threaded sim N/A — client only, no World.
  • World.cs HARD GATE N/A — untouched.
  • Screen HARD GATE ✓ logic in ServerMessageDispatcher (component) + a pure Client.Core helper; nothing added to GameScreen; latch on ClientWorld.
  • Client engine-independence ✓ decision in pure MarshFeedback (Client.Core, unit-tested); only the draw (RippleEffect/Journal) lives in the Client project.
  • Gameplay/Networking separation N/A — no new rule/transport.
  • Act on the instance ✓ latch is a bool on ClientWorld (session state), not a side-collection mirroring entity state.
  • Extend by type, not switch ✓ keyed on the MoveCost > 1 flag, not a terrain-type check — every future slow terrain inherits the feedback.
  • Server-paced actions N/A — no timed action added.
  • Persistence (GameServer) ✓ latch transient, never serialized.
  • Persistence (Auth) N/A.
  • Process separation N/A — client only.
  • Typed options ✓ no new tunable (RippleEffect constants local; tint is Color.White).
  • Broadcasts absolute / AoI ✓ no new event; derived from PlayerMoved (already AoI-filtered, absolute positions).
  • Multi-platform ✓ pure .NET/MonoGame, no OS-specific dependency.
  • Assets required ✓ ripple uses real authored art effect/marsh_ripple; missing art no-ops (no PlaceholderArt shape); notice needs no art.
  • Asset naming (HARD GATE)effect/marsh_ripple is descriptive <material>_<kind>.
  • ModernUO reference N/A — presentation VFX, no architectural call.
  • Docs & DoD in same changecontent/tiledata.txt header + CLAUDE.md tiledata line updated with the moveCost column; this DoD confirmation.

Definition of Done

  • Timed run through marsh slower than grassland — MarshSlowTests.
  • Creature chasing/moving through marsh slowed by the same factor (flying exempt) — MarshSlowCreatureTests.
  • Modified/fast client can't cross marsh at normal speed (server rejects the early move) — FastClientCannotBeatTheMarshCooldown.
  • First marsh step shows the one-time notice + per-step ripple; the slow reads as terrain, not lag — MarshFeedbackTests + screenshots on PR #155.
## #138 — Done (server move-cost + client feedback) Landed on `feat/biomes-art` (PR #155), CI green. Whole solution builds 0 warnings; 507 tests green. - **Server (move-cost):** `TileMap.MoveCostTicks(base, from, into) = ceil(base * max(cost(from), cost(into)))` (symmetric — no free exit step). `MovementSystem` scales the player cooldown; `CreatureSystem` scales the creature cadence; `BaseCreature.IgnoresTerrainMoveCost` (default false) exempts flying kinds; teleport stays exempt. Marsh `moveCost = 1.6` via a new optional column in `content/tiledata.txt`. - **Client (feedback):** derived purely from the authoritative `PlayerMoved` + the shared `TileMap` (no wire event). `MarshFeedback.Decide(destMoveCost, isSelf, hintShown)` (pure, Client.Core) → per-step ground ripple (`RippleEffect`, authored `effect/marsh_ripple`) for any mover, plus a one-time `SystemMessageId.MarshSlowsYou` for the local player. `ProtocolVersion` 11→12 (new protocol enum value). **Note / divergence from the scope checklist:** the "mirror move-cost client-side for feel" item was dropped as unnecessary — the client does **not** predict movement (it renders authoritative positions from `PlayerMoved`), so there is no rubber-band to correct. Confirmed by inspection of `GameInputController`/`ServerMessageDispatcher`. ### Invariants Check (client-feedback delta — server part reviewed when the move-cost landed) - **Scope** ✓ only DoD item 4 (feedback), no new system. - **Server-authoritative** ✓ client only reacts to authoritative `PlayerMoved`; the slow itself stays server-enforced. - **GM auth** N/A — no admin action. - **Identity model** N/A — no account/claim change. - **Protocol versioned** ✓ `ProtocolVersion` 11→12 for the new `SystemMessageId.MarshSlowsYou` (protocol enum). - **String catalog** ✓ new `SystemMessageId` + catalog template; no loose literal. - **Single-threaded sim** N/A — client only, no `World`. - **`World.cs` HARD GATE** N/A — untouched. - **`Screen` HARD GATE** ✓ logic in `ServerMessageDispatcher` (component) + a pure Client.Core helper; nothing added to `GameScreen`; latch on `ClientWorld`. - **Client engine-independence** ✓ decision in pure `MarshFeedback` (Client.Core, unit-tested); only the draw (`RippleEffect`/`Journal`) lives in the `Client` project. - **Gameplay/Networking separation** N/A — no new rule/transport. - **Act on the instance** ✓ latch is a `bool` on `ClientWorld` (session state), not a side-collection mirroring entity state. - **Extend by type, not switch** ✓ keyed on the `MoveCost > 1` flag, not a terrain-type check — every future slow terrain inherits the feedback. - **Server-paced actions** N/A — no timed action added. - **Persistence (GameServer)** ✓ latch transient, never serialized. - **Persistence (Auth)** N/A. - **Process separation** N/A — client only. - **Typed options** ✓ no new tunable (`RippleEffect` constants local; tint is `Color.White`). - **Broadcasts absolute / AoI** ✓ no new event; derived from `PlayerMoved` (already AoI-filtered, absolute positions). - **Multi-platform** ✓ pure .NET/MonoGame, no OS-specific dependency. - **Assets required** ✓ ripple uses real authored art `effect/marsh_ripple`; missing art no-ops (no PlaceholderArt shape); notice needs no art. - **Asset naming (HARD GATE)** ✓ `effect/marsh_ripple` is descriptive `<material>_<kind>`. - **ModernUO reference** N/A — presentation VFX, no architectural call. - **Docs & DoD in same change** ✓ `content/tiledata.txt` header + CLAUDE.md tiledata line updated with the `moveCost` column; this DoD confirmation. ### Definition of Done - ✅ Timed run through marsh slower than grassland — `MarshSlowTests`. - ✅ Creature chasing/moving through marsh slowed by the same factor (flying exempt) — `MarshSlowCreatureTests`. - ✅ Modified/fast client can't cross marsh at normal speed (server rejects the early move) — `FastClientCannotBeatTheMarshCooldown`. - ✅ First marsh step shows the one-time notice + per-step ripple; the slow reads as terrain, not lag — `MarshFeedbackTests` + screenshots on PR #155.
Owner

Completata: move-cost per-terrain server-authoritative — TileMap.MoveCostTicks(base, from, into) con max(cost(from),cost(into)), applicato a player e creature (test MarshSlowTests + MarshSlowCreatureTests), feedback client marsh_ripple + hint one-time (MarshFeedback). La parità creature/player è stata finalizzata da #182 (5313140). Chiudo — completata.

Completata: move-cost per-terrain server-authoritative — `TileMap.MoveCostTicks(base, from, into)` con `max(cost(from),cost(into))`, applicato a player e creature (test `MarshSlowTests` + `MarshSlowCreatureTests`), feedback client `marsh_ripple` + hint one-time (`MarshFeedback`). La parità creature/player è stata finalizzata da **#182** (`5313140`). Chiudo — completata.
marco closed this issue 2026-07-25 19:06:18 +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#138
No description provided.