fix(client): draw entities at the mesh display-Z (P2a — kill the float) #270

Merged
panda merged 2 commits from feat/render-unify-entity-height into main 2026-08-03 22:18:08 +02:00
Collaborator

Summary

P2a of the multi-level world design (docs/superpowers/specs/2026-08-03-multi-level-world-model-design.md, epic #268): unify the surface height so entities stop floating.

The ground mesh draws at the smoothed display-Z (GroundBlendRenderer, the #247 cliff-smoothing that rounds a cliff into a ramp), but entities (player, mobiles, corpses, statics, ground items) were drawn at the raw GroundZAt. Two different heights for one cell, and with IsoProjection.ZScale = 4 (~4 px per Z) the gap detaches the sprite from the ground it stands on — the character "flies and climbs better than Messner" on cliffs and carved roads.

Fix: entities are drawn at the same height the mesh uses for their cell. One seam — ClientWorld.DisplayZAt (renamed from GroundZAt) is now wired to GroundBlendRenderer.DisplayZ (a new public rounded-int accessor over the existing display-Z window, with a raw fallback outside it). Every entity-draw height routes through it: the mobile/corpse GroundZ injection, the static draw (GridRenderer.DrawStatic now takes the resolved Z instead of reading raw GroundZAt), and the ground-item projection. The raw GroundZAt stays only for the admin HUD readout and cursor picking (they must show the real terrain Z). Client-only display change: no protocol/persistence/server/gameplay change — walkability still reads the authoritative raw GroundZAt server-side.

Scope note: this is the prerequisite height fix. The dramatic, user-visible win — the character correctly behind a cliff and revealed by an anti-occlusion fade (Diablo/Fallout-style) — is P2b (§ Relief anti-occlusion in the design), a separate PR. P2a is what puts the character at the right depth for P2b to occlude/reveal it correctly.

Screenshots / recording

Piloted the client from a fresh DB (debug-harness): login test, /tp 6195 2757 onto the saved carved road, screenshot. The character renders on the carved-road surface at its display height (render_after.png, harness screenshot at tile 6195,2757, "here 50"). Honest note: at this particular cell the display-Z and raw-Z are close, so the delta vs the raw build (the owner's earlier float screenshots) is subtle here — the mismatch is largest, and the fix most visible, in a narrow trench / right at a tall cliff base, and the dramatic fix is P2b's anti-occlusion. Verified the fix compiles, runs, and draws the character glued to the mesh surface; happy to capture a higher-divergence spot on request.

How it was tested

  • dotnet build IsoMmo.slnx0 warnings / 0 errors.
  • dotnet test IsoMmo.slnxall green (Shared 177, GameServer 346, Client.Core 196, Auth 19, Scripting 39, MapEditor.Core 66, others — full solution).
  • dotnet csharpier check . → clean (515 files).
  • Debug-harness in-world capture as above (fresh DB).
  • Grep-swept the client for stray raw-Z entity draws; the only remaining raw GroundZAt reads are the admin HUD readout and cursor picking (intentional).

Invariants Check

Client-only display change; most gameplay invariants N/A (each falsifiable).

  • Scope ✓ the P2a slice the owner approved; the anti-occlusion (P2b) is deliberately separate. · Server-authoritative ✓ display only — walkability/movement still read the raw authoritative GroundZAt server-side; the client sends no Z. · GM auth / Identity N/A. · Protocol versioned ✓ N/A — no wire shape/enum/DTO changed, so no ProtocolVersion bump. · String catalog N/A. · Single-threaded sim N/A (client render). · World.cs gate N/A. · Screen HARD GATE ✓ the height logic lives in GroundBlendRenderer + the ClientWorld seam; GameScreen only wires it. · Client engine-independence ✓ the smoothing (TerrainDisplayHeight, Client.Core, unit-tested) is unchanged; this is MonoGame draw glue routing its result. · Gameplay/Networking N/A. · Act on the instance ✓ no side-collection; a single DisplayZAt provider. · Extend by type N/A. · Persistence (GameServer/Auth) N/A — no save/schema change. · Process separation N/A. · Typed options N/A. · Broadcasts/AoI N/A. · Multi-platform ✓ pure .NET/MonoGame. · Assets N/A. · ModernUO/D4 N/A for this slice (the surface model is the epic; this is only the height). · Docs & DoD ✓ the design doc (#269) carries P2a's DoD.

No .

Definition of Done

  • Entities (player, mobiles, corpses, statics, ground items) draw at the mesh display-Z; the character stays glued to the surface it stands on, no float/sink (harness capture).
  • Raw GroundZAt remains only for the HUD readout + cursor picking; no other entity draw reads it.
  • No protocol/persistence/server change; walkability unaffected.
  • Whole solution builds 0-warning and all tests pass.

Checklist

  • just lint passes (CSharpier clean + zero-warning build)
  • just test green (whole solution)
  • The whole solution builds (client and tools included)
  • Multi-platform preserved (pure .NET/MonoGame)
  • Tests added/updated — N/A: render wiring has no unit surface (the smoothing it routes is already unit-tested in TerrainDisplayHeight); verified by build + harness
  • Linked the related issue (#268) — P2a slice; DoD in the design (#269)

Prerequisite slice of #268 (design #269). Anti-occlusion is P2b, separate.

## Summary **P2a of the multi-level world design** (`docs/superpowers/specs/2026-08-03-multi-level-world-model-design.md`, epic #268): *unify the surface height so entities stop floating*. The ground mesh draws at the smoothed **display-Z** (`GroundBlendRenderer`, the #247 cliff-smoothing that rounds a cliff into a ramp), but entities (player, mobiles, corpses, statics, ground items) were drawn at the **raw `GroundZAt`**. Two different heights for one cell, and with `IsoProjection.ZScale = 4` (~4 px per Z) the gap detaches the sprite from the ground it stands on — the character "flies and climbs better than Messner" on cliffs and carved roads. Fix: entities are drawn at the **same height the mesh uses** for their cell. One seam — `ClientWorld.DisplayZAt` (renamed from `GroundZAt`) is now wired to `GroundBlendRenderer.DisplayZ` (a new public rounded-int accessor over the existing display-Z window, with a raw fallback outside it). Every entity-draw height routes through it: the mobile/corpse `GroundZ` injection, the static draw (`GridRenderer.DrawStatic` now takes the resolved Z instead of reading raw `GroundZAt`), and the ground-item projection. The **raw** `GroundZAt` stays only for the admin HUD readout and cursor picking (they must show the real terrain Z). Client-only display change: **no** protocol/persistence/server/gameplay change — walkability still reads the authoritative raw `GroundZAt` server-side. **Scope note:** this is the *prerequisite* height fix. The dramatic, user-visible win — the character correctly *behind* a cliff and revealed by an **anti-occlusion fade** (Diablo/Fallout-style) — is **P2b** (`§ Relief anti-occlusion` in the design), a separate PR. P2a is what puts the character at the right depth for P2b to occlude/reveal it correctly. ## Screenshots / recording Piloted the client from a fresh DB (debug-harness): `login test`, `/tp 6195 2757` onto the saved carved road, `screenshot`. The character renders **on** the carved-road surface at its display height (`render_after.png`, harness `screenshot` at tile 6195,2757, "here 50"). Honest note: at this particular cell the display-Z and raw-Z are close, so the delta vs the raw build (the owner's earlier float screenshots) is *subtle here* — the mismatch is largest, and the fix most visible, in a narrow trench / right at a tall cliff base, and the *dramatic* fix is P2b's anti-occlusion. Verified the fix compiles, runs, and draws the character glued to the mesh surface; happy to capture a higher-divergence spot on request. ## How it was tested - `dotnet build IsoMmo.slnx` → **0 warnings / 0 errors**. - `dotnet test IsoMmo.slnx` → **all green** (Shared 177, GameServer 346, Client.Core 196, Auth 19, Scripting 39, MapEditor.Core 66, others — full solution). - `dotnet csharpier check .` → clean (515 files). - Debug-harness in-world capture as above (fresh DB). - Grep-swept the client for stray raw-Z entity draws; the only remaining raw `GroundZAt` reads are the admin HUD readout and cursor picking (intentional). ## Invariants Check Client-only display change; most gameplay invariants N/A (each falsifiable). - **Scope** ✓ the P2a slice the owner approved; the anti-occlusion (P2b) is deliberately separate. · **Server-authoritative** ✓ display only — walkability/movement still read the raw authoritative `GroundZAt` server-side; the client sends no Z. · **GM auth / Identity** N/A. · **Protocol versioned** ✓ N/A — no wire shape/enum/DTO changed, so **no** `ProtocolVersion` bump. · **String catalog** N/A. · **Single-threaded sim** N/A (client render). · **`World.cs` gate** N/A. · **`Screen` HARD GATE** ✓ the height logic lives in `GroundBlendRenderer` + the `ClientWorld` seam; `GameScreen` only wires it. · **Client engine-independence** ✓ the smoothing (`TerrainDisplayHeight`, Client.Core, unit-tested) is unchanged; this is MonoGame draw glue routing its result. · **Gameplay/Networking** N/A. · **Act on the instance** ✓ no side-collection; a single `DisplayZAt` provider. · **Extend by type** N/A. · **Persistence (GameServer/Auth)** N/A — no save/schema change. · **Process separation** N/A. · **Typed options** N/A. · **Broadcasts/AoI** N/A. · **Multi-platform** ✓ pure .NET/MonoGame. · **Assets** N/A. · **ModernUO/D4** N/A for this slice (the surface model is the epic; this is only the height). · **Docs & DoD** ✓ the design doc (#269) carries P2a's DoD. No `✗`. ## Definition of Done - Entities (player, mobiles, corpses, statics, ground items) draw at the mesh display-Z; the character stays glued to the surface it stands on, no float/sink (harness capture). - Raw `GroundZAt` remains only for the HUD readout + cursor picking; no other entity draw reads it. - No protocol/persistence/server change; walkability unaffected. - Whole solution builds 0-warning and all tests pass. ## Checklist - [x] `just lint` passes (CSharpier clean + zero-warning build) - [x] `just test` green (whole solution) - [x] The whole solution builds (client and tools included) - [x] Multi-platform preserved (pure .NET/MonoGame) - [ ] Tests added/updated — N/A: render wiring has no unit surface (the smoothing it routes is already unit-tested in `TerrainDisplayHeight`); verified by build + harness - [x] Linked the related issue (#268) — P2a slice; DoD in the design (#269) Prerequisite slice of #268 (design #269). Anti-occlusion is P2b, separate.
fix(client): draw entities at the mesh display-Z so they stop floating over cliffs/carved roads
Some checks failed
ci / Lint & Test (pull_request) Failing after 9s
ci / Lua content lint (pull_request) Successful in 10s
1799a4af87
docs(plan): reference the multi-level design by issue/PR, not a cross-branch file path (fixes check-doc-refs)
All checks were successful
ci / Lua content lint (pull_request) Successful in 50s
ci / Lint & Test (pull_request) Successful in 9m42s
c80291ceb2
panda merged commit cf8ef93527 into main 2026-08-03 22:18:08 +02:00
panda deleted branch feat/render-unify-entity-height 2026-08-03 22:18:08 +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!270
No description provided.