---
id: ground-types/003
title: Client rendering, stage 1 — the lattice mesh, sunken cells, mountains, and people standing in water
epic: ground-types
state: done
priority: 1
blocked_by: []
estimate: L
created: 2026-08-17
updated: 2026-08-18
claimed_by: null
claimed_at: null
delivers: []
review_artifact: land-of-lor/game/world/client/ground_mesh.gd
---

## What to do

Replace the one flat green plane (`client_main.gd`'s `_rebuild_area_geometry`) with ground that
is built from the cells. This stage uses no shader: transitions are vertex-colour blends, which
work on every renderer and are an honest placeholder. Runs in parallel with 002 — it can
render from a fake payload.

- New `game/world/client/ground_mesh.gd` — pure logic, no Node dependencies, testable headless:
  from `(ground, width, height)` build an **inset lattice** (lattice lines at cell offsets
  0, 0.25, 0.75, 1) and return `{vertices, indices, colours, uvs}` PackedArrays. One rule for
  every vertex: it is **sunk (y = −0.25·CELL) iff every cell touching it is sunken** — interior
  vertices touch one cell, edge vertices two, corners four. A one-cell-wide river is then a
  flat-bottomed trough with quarter-cell banks (a centre fan would give a row of dimples).
  Vertex colour = the average of the touching cells' base colours. Also `mountain_instances()`
  (a Mountain cell → instance; **peak** when all eight neighbours are Mountain, out-of-bounds
  counting as not-Mountain; variant tint; kw 56 flagged volcano) and `pack_data_image()` for
  task 004. 60×60 → 32,761 vertices, 64,800 triangles, one static mesh.
- New `game/world/client/area_view.gd` — `build(grid)`: the ground `MeshInstance3D`
  (SurfaceTool, `generate_normals()`, `StandardMaterial3D` with `vertex_color_use_as_albedo`),
  a `MultiMeshInstance3D` of low-poly cones for Mountains (taller scale for a peak; a second
  small MultiMesh of red crater caps for volcanoes), a `BoxMesh` only for `CELL_WALL` /
  `CELL_SOFT_BLOCK`, portal rings at their cell's surface height. `client_main.gd`'s
  `_rebuild_area_geometry()` becomes a thin call into it.
- `humanoid_node.gd`: `set_target_cell(cell, y)`; `client_main.gd` passes y = −0.5·CELL on a
  sunken cell (local and remote bodies) — the existing constant-speed glide interpolates the
  dip and the surface at −0.25 hides the lower part. Unify the three `CELL_3D` copies
  (`humanoid_node.gd`, `client_main.gd`, `portal_node.gd`) into one.
- `client/ui/_shots.gd`: `LOR_GROUND_FILE` → build an `AreaGrid` from a ground content file →
  `g-ground.png`. Write a small deterministic fixture file for it; task 005's real files
  replace it.

## Definition of done

- `test_runner.gd`: the vertex-sunk rule on a single cell, a 1-wide river and a 2-wide river;
  the peak rule incl. edges; data packing.
- A screenshot from `_shots.gd` showing grass variants, a river with banks, mountains with a
  peak, reviewed by eye; the web export boots.
- Committed and pushed in `land-of-lor`.

## Where the work lands

`land-of-lor/game/world/client/`.

## What was done (2026-08-17, laptop session)

- `client/world_units.gd` — the one `CELL_3D` (plus `SUNKEN_DEPTH`, `ENTITY_SINK`, mountain
  sizes); `humanoid_node.gd`, `portal_node.gd`, `client_main.gd` use it.
- `client/ground_mesh.gd` (pure): the inset lattice, `vertex_sunk` / `vertex_colour` /
  `build`, `is_peak` / `mountain_instances`, `pack_cells` (for 004), `standing_height` /
  `surface_height`. 26 new `test_runner.gd` checks (169 total) — single cell, 1-wide and
  2-wide rivers, edge of the Area, blends, peaks incl. the corner case, volcano, packing.
- `client/area_view.gd` (Node3D): floor mesh via SurfaceTool + `vertex_color_use_as_albedo`,
  one MultiMesh of cones (peaks taller, tinted per flavour) + red crater caps for kw 56,
  walls only for `CELL_WALL`/`SOFT_BLOCK`, portal rings on the cell's surface.
  `client_main.gd _rebuild_area_geometry()` builds it; entities get `snap_to_cell(cell, y)` /
  `set_target_cell(cell, y)` from `standing_height`.
- `_shots.gd`: `g-ground`, `g-ground-top`, `g-ground-wade` from `LOR_GROUND_FILE` or a
  built-in showcase; reviewed by eye — the river reads as a channel with banks, patches
  blend, the mountain block has a taller tinted peak, the wading body's legs are hidden.
- The headless Godot client harness (`npm run test-client-godot`) still passes every
  movement/prediction check; its two `CLIENT_REMOTE` checks fail because they still expect
  `entity-<accountId>` ids, which 014 replaced — pre-existing, not touched here.
- Not done: the web export itself (task 008 does the export and ships it).