# Ground types

## overview.md
```yaml
id: ground-types
title: Ground types
area: client
summary: Every cell of every Area becomes one of the 64 hexagrams — the ground it is made of — stored, sent, rendered with soft edges, sunken water and lava, standing mountains, painted into every existing Area, and editable in an area map editor.
repo: land-of-lor (game + director + tools/world-editor) and this workspace (deliverables, site/play)
depends_on: ["world-map-and-portals"]
```

## What this is

Kris wrote the spec on 2026-08-17: `land-of-lor/docs/specs/GROUND_TYPES.md`. Read it first;
this overview does not restate it. The short version: a cell's ground is a hexagram. The
**lower** trigram says what the ground *is* — Lake is deep water, Mountain is a mountain,
Earth is grass, Wind is sand, Fire is lava, Water is water, Thunder is snow and ice, Heaven is
stone, tiles and decorative floors. The **upper** trigram is one of eight flavours of that
type. Water and lava sit lower than the ground around them; mountains stand up out of the
cell (taller when ringed by eight more); a cell holds exactly one type but the picture shows
soft transitions to its neighbours rather than hard block edges.

This epic is the implementation of that spec, end to end: the lookup that puts the hexagram at
the centre of the code, the storage and the wire, the rendering, the eight looks and their
variations, painting every existing Area (plain grass everywhere; a river through the Village;
every type on show in the Badlands), and an area map editor reachable from the world editor.

## Where it started from

Until this epic there was no per-cell data anywhere: the floor of every Area was one flat green
plane, the server grid held only walkable / wall / portal flags, and three places in the code
carried an explicit "ground types don't exist yet" placeholder waiting for exactly this —
`grid.gd`'s `build_for_area`, `area_server.gd`'s `_area_payload` ("when authored terrain
exists it gets a `cells` key right here"), and the disabled *Edit the cell map · later*
button in the world editor's inspector. The parked `world-traversal-and-ground` epic was
waiting on the spec that now exists; its one task is answered by it, and that epic stays
parked for the deeper traversal questions (fertile ground, what a player does to ground on
purpose) which this spec deliberately does not settle.

## Decisions taken with Kris (2026-08-17, planning session)

- **Mountain and Deep Water block movement now**, on the server and in client prediction
  through the one shared `is_walkable`. Water and Lava stay open — creatures don't have a
  nature yet, and the spec says not to restrict them until they do.
- **Placeholder looks are procedural**, generated in the engine from data in `elements.js`;
  a folder convention lets a real texture override any of the 64 later without code.
- **The area editor is built directly** in the world editor's own visual language — it is
  Kris's local tool, not a public surface — rather than going through a design brief first.
- **A new epic**, not the parked one: `world-traversal-and-ground` was scoped as spec-only,
  and this is a build.

## Design decisions the tasks are built on

Written down once so sessions don't re-derive them; the full reasoning is in the planning
session's plan and in each task file.

- Ground data lives **inside** the two existing globals in `game/shell/elements.js` —
  `LOR_TRIGRAMS[<lower>].ground` (the eight types) and `LOR_ELEMENTS[kw].groundVariant`
  (the 64 flavours) — because the card editor regenerates that file from exactly those two
  objects; a third global would be lost on Kris's next export.
- Storage is a new `areas.ground SMALLINT[]` column, its own column on purpose: the `state`
  blob belongs to the world process's snapshots and the loader must be able to write ground.
- The wire carries ground as a plain array on `hello_ack.area.ground`; **the protocol version
  moves to 4** because a client that can't see a mountain would walk into an invisible wall.
- Authored ground is a per-area file, `game/content/areas/<slug>.ground.json`, one row of
  cells per line, applied by `load-world` like the world file — never embedded in
  `spellgrove.world.json`.
- Portal cells, the hub entry cell, the default spawn (1,1) and the home landing cell must
  stay passable and mutually reachable; the loader, the editor and the generators all refuse
  paint that breaks this.
- The floor is one procedurally built mesh per Area on an inset lattice (so a one-cell river
  is a trough, not a row of dimples); transitions ship first as vertex-colour blends, then as
  a textured shader; mountains are one MultiMesh.
- Unpainted ground is hexagram 2, Earth over Earth.

## Non-goals

- Restricting Water and Lava by creature nature (no natures yet).
- Ground that changes during play (terraforming) — the storage leaves room for it, nothing
  here builds it.
- An area map *viewer* on the site or in the game.
- The fertile-ground rule and the rest of `world-traversal-and-ground`'s questions.
- Real art. Everything here is a placeholder that looks like one, by the project's stated
  policy.

## Relevant docs

- `land-of-lor/docs/specs/GROUND_TYPES.md` — the spec (human-owned; agents read only).
- `land-of-lor/docs/specs/CORE_ENTITIES.md` — Area / Portal; explicitly left ground undefined.
- `land-of-lor/docs/README.md` §3, §7 — the floor slot, "a lava field is Mountain-over-Fire as
  ground", changing lines. One tension to keep in view rather than resolve: §3 says "stone is a
  Mountain-faced hexagram"; the spec makes Heaven-lower the stone and Mountain-lower the mountain.
- `land-of-lor/game/README.md` — the world map, its editor, `load-world`, the droplet deploy.
- `epics/world-map-and-portals/` — the Area/Portal model this builds on.

## status.md
```yaml
updated: 2026-08-18
parked: false
tasks: {"backlog":0,"needs-input":0,"ready":0,"doing":0,"review":0,"done":8}
open_questions: []
tag: done
next: Done — all eight signed off 2026-08-18. The look itself continues in ground-look-and-levels.
```

# Task summary

8 tasks, all created 2026-08-17 from the planning session that followed Kris's spec, and all built the same day.
Sequencing: 001 first; then 002 (storage and wire) alongside 003 (rendering, stage 1) and
the scaffold of 007 (editor); 004 (textured transitions, phone check) after 003; 005
(content pipeline, painted areas) after 002; 006 (looks) after 004; 008 (deploy, ship,
bookkeeping) last.

- **001 — the model** — `done`. Ground-type and variant definitions in `elements.js`,
  synced to the director; `ground_types.gd`; `AreaGrid.ground` and terrain passability.
  Starts with the world-file fix.
- **002 — storage, hydrate, wire, protocol 4** — `done`. Built 2026-08-17.
- **003 — client rendering, stage 1** — `done`. Built 2026-08-17. Lattice mesh, sunken cells, mountains,
  entities sinking, vertex-colour transitions.
- **004 — client rendering, stage 2** — `done`. Built 2026-08-17; the phone check is Kris's. Textured transitions via shader + atlas;
  the renderer check on web and phone.
- **005 — content pipeline** — `done`. Built 2026-08-17. Ground files, loader, presets, the six painted
  Areas.
- **006 — the looks** — `done`. A placeholder pass; Kris's eye decides. Tune the eight types and 64 variants with the game
  running.
- **007 — area map editor** — `done`. Built 2026-08-17.
- **008 — verify, deploy, ship** — `done`. Live on the droplet, /play and APK v10; deliverable 010 delivered.

## Open questions

None.

## Next

Nothing. All eight: Signed off by Kris 2026-08-18 (blanket approval of everything then in review). Follow-on look work lives in `ground-look-and-levels`.

## tasks/ (8)

### ground-types/001 — The model — a hexagram is a ground type, in every runtime
```yaml
id: ground-types/001
title: The model — a hexagram is a ground type, in every runtime
epic: ground-types
state: done
priority: 1
blocked_by: []
estimate: M
created: 2026-08-17
updated: 2026-08-18
claimed_by: null
claimed_at: null
delivers: []
review_artifact: land-of-lor/game/world/ground_types.gd
```

## What to do

Put the hexagram at the centre of the code, the way the spec asks, before any storage or
rendering exists. Read `land-of-lor/docs/specs/GROUND_TYPES.md` first (it is human-owned;
read, don't edit).

**Step 0 — fix the world file.** `game/content/spellgrove.world.json` is invalid today: a
world-editor session on 2026-08-16 (`05fcdd0`) shrank `beginner-arena` to 20×10 and left two
portals out of bounds (`village-to-arena` toCell (0,10), `arena-to-badlands` fromCell (19,19)).
`validateWorldContent` returns two errors and `npm run load-world` refuses the file. Everything
in this epic rides on the loader. Assumption: keep the 20×10 and move both portals to the
bottom edge — (0,9) and (19,9). Say so in the session report.

**Then the model, in three runtimes from one table:**

1. `game/shell/elements.js` — add `ground` to each of the eight `LOR_TRIGRAMS` entries
   (`{ name, kind: "flat"|"sunken"|"mountain", passable, colour, pattern }`; earth = Grass,
   wind = Sand, thunder = Snow/Ice, water = Water (sunken), fire = Lava (sunken), mountain =
   Mountain (impassable), lake = Deep Water (impassable), heaven = Stone/tiles) and
   `groundVariant` (`{ label, colour, pattern }`) to each of the 64 `LOR_ELEMENTS` (the
   Earth-upper one is the plain variant; kw 56, Fire over Mountain, is the volcano). Inside
   the two existing globals, not a third one — `card-editor.html` regenerates this file from
   exactly `LOR_TRIGRAMS` and `LOR_ELEMENTS`. Placeholder colours are fine; task 006 tunes them.
2. `game/sync-elements.sh` also writes `game/director/src/data/elements.json`; the director's
   `build` script copies `src/data` into `dist/` the way it already copies `schema.sql`.
3. New `game/world/ground_types.gd` at the project root (pure `RefCounted`, `preload`-only,
   loads the same `elements.json` via `load()` — no hand-copied tables): `kw_of(upper, lower)`,
   `lower_of(kw)`, `variant_index(kw)`, `is_passable(kw)`, `is_sunken(kw)`, `is_mountain(kw)`,
   `DEFAULT_KW = 2`, `plain_kw_for(lower)`. `client/ui/element_db.gd` gains
   `ground_type(kw)` / `ground_variant(kw)` and documented stubs for the spec's other getters
   (`getCreatureModel`, `getSize`, `getColour`, `getBrain`, `getNature`, `howManyStackInCell`,
   `getSpellKind`, `getPowerValue`, `rarity`) — the shape is visible, no values are invented.
4. New `game/director/src/groundTypes.ts` with the same API, reading `data/elements.json`.
5. `game/world/grid.gd` — `CELL_TERRAIN_BLOCK = 4`; `ground: PackedInt32Array` filled with 2
   in `_init`; `ground_at()`, `apply_ground(PackedInt32Array) -> bool` (length must match;
   sets `CELL_TERRAIN_BLOCK` for impassable hexagrams but never overwrites `CELL_PORTAL`);
   `decode_ground()` as the one codec function; `from_area_payload` reads `ground`;
   `permits_squeeze()` is false for a terrain block; `set_portal` warns if it lands on one.
   Retire the "an authored area is open ground (no ground types yet)" test line.
6. Write the ground content-file format into `game/README.md` now, so tasks 005–007 can start
   from it: `game/content/areas/<slug>.ground.json` = `{ "$comment", "slug", "width",
   "height", "layers", "rows": ["02 02 20 02 …", …] }` — `height × layers` row strings,
   two-character zero-padded, space-separated, one row per line.

## Definition of done

- `test_runner.gd`: kw ↔ (upper, lower) is a bijection matching `elements.json`;
  `apply_ground` respects portal precedence and rejects a wrong length; `is_walkable` /
  `permits_squeeze` on terrain blocks; `from_area_payload` with, without and with a short
  `ground`.
- `npm test`: `groundTypes.test.ts` (64 → 8 × 8, kind rules) and the loader suite green again
  after step 0.
- `sync-elements.sh` writes both JSON copies; both are committed.
- Committed and pushed in `land-of-lor`.

## Where the work lands

`land-of-lor` — `game/shell/`, `game/director/src/`, `game/world/`, `game/README.md`.

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

- Step 0: `spellgrove.world.json` — beginner-arena's two portals moved to (0,5) and (19,9);
  `validateWorldContent` is clean again. **Assumption:** the 20×10 resize was intended; the
  portals were moved, not the size restored.
- `elements.js`: `ground` on all eight trigrams, `groundVariant` on all 64 elements
  (placeholder colours; task 006 tunes them). `sync-elements.sh` writes the director copy;
  `npm run build` carries it into `dist/`.
- `director/src/groundTypes.ts` + `test/groundTypes.test.ts` (5 tests).
- `world/ground_types.gd` (root, pure), `element_db.gd` getters and stubs, `grid.gd`
  (`ground`, `CELL_TERRAIN_BLOCK`, `set_ground`, `apply_ground`, `decode_ground`,
  `ground_at`, `is_sunken`, `is_mountain`; `from_area_payload` reads `ground` before
  portals). `test_runner.gd`: 143 checks green.
- `game/README.md`: "The ground — every cell is a hexagram", incl. the content-file format.

### ground-types/002 — Storage, hydrate, wire — areas.ground, the hydrate file, hello_ack, protocol 4
```yaml
id: ground-types/002
title: Storage, hydrate, wire — areas.ground, the hydrate file, hello_ack, protocol 4
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/director/src/groundPaint.ts
```

## What to do

Give ground a home in the database and carry it to the world process and the client.

- `game/director/src/schema.sql`: `ALTER TABLE areas ADD COLUMN IF NOT EXISTS ground SMALLINT[];`
  — nullable (NULL = never painted), row-major `(layer*h + y)*w + x`, the same indexing as
  `AreaGrid`. Range and length are checked in TypeScript; this schema bans bare
  `ADD CONSTRAINT`.
- `areas.ts`: `getAreaGround(pool, id)` and `setAreaGround(pool, id, cells)` (1..64, length
  `w*h*layers`). **Not** added to `AREA_COLUMNS` / `rowToArea` — every Area query would drag
  ~10 KB per row. `createArea` writes a plain fill; `ensureHomeArea` writes the seeded home
  paint in its size `UPDATE`.
- New `groundPaint.ts` (seeded PRNG, e.g. mulberry32, seed = hash of the area id):
  `paintPlain`, `sprinkleVariants(cells, w, h, seed, density, lower)`, `paintHome`;
  `paintUnpaintedAreas(pool)` called from `db.ts migrate()` — NULL *or wrong length* → homes
  sprinkled grass, everything else plain; log `[migrate] painted ground for N area(s)`;
  idempotent (a second boot logs nothing).
- `areaInstance.ts spawnInstance()`: `ground` in the hydrate object (dropped with a warning if
  the length is wrong, so the world process falls back to plain rather than crashing).
- `area_server.gd`: `apply_ground` right after `build_for_area` and **before** `_hydrate`
  (portals must win over paint); cache the marshalled array once (`_ground_wire`) rather than
  rebuilding it per peer; `_area_payload()` gains `ground`. Annotate the "when authored terrain
  exists it gets a `cells` key right here" comment as closed — don't delete it.
- **PROTOCOL_VERSION 3 → 4** in `protocol.ts`, `protocol.gd` and
  `test/helpers/gameClient.ts` — an old client can't see mountains and would walk into
  invisible walls; the 015 "update needed" path makes the bump survivable. Deploy order (task
  008): director, then web export, then APK.
- `client_main.gd _apply_area`: the dedupe key gains `hash(ground)` — otherwise a same-size
  repaint keeps stale terrain across a respawn.

## Definition of done

- `portal-authored.test.ts`: `hello_ack.area.ground.length == w*h`; an intent into a Mountain
  cell is `intent_rejected`.
- `home-portal.test.ts`: a home's ground has more than one distinct hexagram, all Earth-lower;
  spawn (1,1) and the landing cell (w/2, h−2) are passable.
- `schema.test.ts`: column present; migrate paints NULLs and re-paints wrong lengths; a second
  run is a no-op.
- `stale-client.test.ts` still green at version 4.
- Committed and pushed in `land-of-lor`.

## Where the work lands

`land-of-lor/game/director/`, `game/world/`.

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

- `schema.sql`: `areas.ground SMALLINT[]` (nullable; range/length checked in TS).
- `areas.ts`: `getAreaGround` / `setAreaGround` / `ensureAreaGround` / `paintUnpaintedAreas`
  (not in `AREA_COLUMNS`); `ensureHomeArea` paints the home after sizing it. New
  `groundPaint.ts`: mulberry32 over FNV-1a, `paintPlain`, `sprinkleVariants`, `paintHome`,
  `defaultPaintFor`, `encodeGround` (the one wire codec). `db.ts migrate()` →
  `[migrate] painted ground for N area(s)`. `areaInstance.ts spawnInstance` puts `ground` in
  the hydrate file via `ensureAreaGround`; the loader re-ensures after its size UPDATE.
- `area_server.gd`: `apply_ground` before `_hydrate`, `_ground_wire` cached,
  `_area_payload().ground`; the old "gets a `cells` key right here" comment annotated as
  closed. `client_main.gd`: `hash(ground)` in the `_apply_area` key.
- **PROTOCOL_VERSION 4** in `protocol.ts`, `protocol.gd`, `test/helpers/gameClient.ts`.
- Tests: `schema.test.ts` (column, migrate paints NULL and re-paints a resize, idempotent,
  `setAreaGround` refuses bad input), `portal-authored.test.ts` (ground on hello_ack; a step
  into a Mountain is `BLOCKED`, into Water lands), `home-portal.test.ts` (home scatter,
  deterministic per id); `stale-client`, `loadWorldContent`, `groundTypes` still green.
- **Assumption:** the wire array is plain JSON (~10 KB for 60×60) — `encodeGround` /
  `decode_ground` are the two places to change if that ever matters.

### ground-types/003 — Client rendering, stage 1 — the lattice mesh, sunken cells, mountains, and people standing in water
```yaml
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).

### ground-types/004 — Client rendering, stage 2 — textured transitions, and the renderer check on web and phone
```yaml
id: ground-types/004
title: Client rendering, stage 2 — textured transitions, and the renderer check on web and phone
epic: ground-types
state: done
priority: 1
blocked_by: []
estimate: M
created: 2026-08-17
updated: 2026-08-18
claimed_by: null
claimed_at: null
delivers: []
review_artifact: land-of-lor/docs/handoff/ground-types.md
```

## What to do

The spec's "its own special task": the ground shows real (placeholder) textures with soft edges
between any two types, corners included, and the same picture on Chrome and on the phone.

- `game/world/client/ground.gdshader` — `shader_type spatial`. Inputs: a nearest-sampled RGBA8
  data texture the size of the grid (R = lower-trigram index, G = upper index, B = flags;
  decode with `int(round(c * 255.0))` — no integer samplers) and an **8×8 tile atlas** (one
  `Texture2D`, gutter, mipmaps off). Per fragment: the four nearest cell centres, bilinear
  weights sharpened with `smoothstep` (optional value-noise wobble), each cell's tile sampled at
  the same world UV so like types are seamless — four data reads, four atlas reads. Try
  `sampler2DArray` only if the atlas look is unacceptable.
- `client/ground_atlas.gd` — the 64 tiles generated procedurally once from the ground and
  variant definitions (plain, speckle, stripes, dots, flowers for grass, cracks for lava,
  tiles / bricks / paths for Heaven); if `res://client/ground/<lower>/<upper>.png` exists it wins.
- `area_view.gd`: swap the material behind a toggle; the vertex-colour material stays as the
  fallback / debug view.
- `game/world/project.godot`: add `renderer/rendering_method.mobile="gl_compatibility"` (the
  POCs have it, the game doesn't, so the APK is very likely running the Vulkan Mobile renderer
  today) and fix the stale `config/features` "Forward Plus" entry. Re-check the two UI canvas
  shaders on the device afterwards.
- Verify on desktop Compatibility, on the **web export in Chrome**, and on **an APK on Kris's
  phone**: no shader compile errors in the console / logcat; frame time on the 60×60 Badlands at
  maximum zoom-out. The phone check needs the laptop — everything else can run from the task
  runner.

## Definition of done

Screenshots from all three, the numbers, and the atlas-vs-Texture2DArray decision written into
`docs/handoff/0NN-ground-rendering.md`. Committed and pushed.

## Where the work lands

`land-of-lor/game/world/client/`, `game/world/project.godot`, `docs/handoff/`.

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

- `client/ground.gdshader` (spatial; four-cell blend, `band` + value-noise `wobble`, cell
  data as a nearest RGBA8 texture decoded `floor(c*255+0.5)`, no integer samplers / texelFetch)
  and `client/ground_atlas.gd` (8×8 atlas of 32 px tiles drawn from each hexagram's colour
  and pattern — plain, speckle, streaks, dapple, cracks, pebbles, ripples, flowers, tiles;
  `res://client/ground/<lower>/<upper>.png` overrides a tile). `area_view.gd` swaps the
  material behind `USE_TEXTURED_GROUND`; stage 1 stays as fallback.
- `project.godot`: `renderer/rendering_method.mobile="gl_compatibility"`; features string
  fixed. **Decision:** atlas as primary; `Texture2DArray` not needed, not tried.
- Verified on desktop Compatibility (`_shots.gd`) and on the **exported web build in
  headless Chrome (WebGL2)** through the real signup → Start Game flow against a throwaway
  local director carrying the six ground files — no shader errors, textured home rendered.
  Screenshots + reasoning in `docs/handoff/ground-types.md` and `docs/handoff/ground-types/`.
- **Not done — needs Kris and a phone:** the on-device check and the 60×60 frame-time
  number. The `.mobile` override puts the phone on the backend the web check just passed;
  the APK itself is built in 008.

### ground-types/005 — Content pipeline — ground files, the loader, presets, and the six painted Areas
```yaml
id: ground-types/005
title: Content pipeline — ground files, the loader, presets, and the six painted Areas
epic: ground-types
state: done
priority: 1
blocked_by: []
estimate: M
created: 2026-08-17
updated: 2026-08-18
claimed_by: null
claimed_at: null
delivers: []
review_artifact: land-of-lor/game/content/areas/the-badlands.ground.json
```

## What to do

Get real ground into the authored Areas through the same reviewable-file path the world map uses.

- New `game/director/src/groundContent.ts`: zod schema for the ground file (format in
  `game/README.md`, from task 001), `parseRows` / `formatRows`, and
  `validateGround(area, portals, ground)` — dimensions equal the world file's, values 1..64,
  passable at (1,1), the hub `entryCell`, every portal from/to cell (a two-way portal's return
  leg included) and the home landing cell; 8-way reachability from (1,1) and the entry cell to
  every portal cell using `movement.gd`'s corner rule. Errors, not warnings.
- `worldContent.ts loadWorldContent(pool, content, { ground })` → `setAreaGround`;
  `loadWorldCli.ts` reads sibling `areas/<slug>.ground.json` files; a missing file leaves the
  database as it is. The loader is strict on dimensions — cropping / padding is the editor's job.
- `groundPaint.ts` presets: `river(seed)` — a winding line of Water through the middle plus
  sprinkled grass — for the Village of Lor; a **deterministic, hand-laid Badlands** — patches of
  all eight types: a Mountain cluster with a peak core, a volcano, a lava pool, deep water, snow,
  sand, a Heaven-tiled plaza around the portals — keeping (1,1), (0,0), (2,4), (58,31) and the
  portal cells passable and reachable (deterministic beats random: reviewable, no re-rolls);
  `sprinkle` for the other four.
- New CLI `npm run paint-ground -- <slug> --preset plain|sprinkle|river|badlands [--seed N]`
  (`src/paintGroundCli.ts`) writes the file. Commit the six files under `game/content/areas/`;
  `load-world` applies them locally.

## Definition of done

- Tests: the six committed files validate; loader round-trip; the validator rejects a mountain
  under a portal and an unreachable portal.
- Local game: the Village shows sprinkled grass and a river; the Badlands shows every type.
- Committed and pushed in `land-of-lor`.

## Where the work lands

`land-of-lor/game/director/`, `game/content/areas/`.

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

- `director/src/groundContent.ts`: file schema, `parseRows` / `formatRows` /
  `makeGroundContent`, `protectedCells`, `reachableFrom` (8-way with the corner rule),
  `validateGround`, `resizeGround`, `readGroundFiles`. `loadWorldContent(…, { ground })`
  validates the files with the graph before writing and applies them after
  `ensureAreaGround`; `loadWorldCli` reads `areas/*.ground.json` beside the world file.
- `groundPaint.ts` presets `river` / `paintVillage` / `paintBadlands` (hand-laid) + `rect` /
  `ellipse`; new `npm run paint-ground`. The six files under `game/content/areas/` committed
  (Village: sprinkled grass + river; Badlands: every type incl. a peak-cored range and a
  volcano; the other four sprinkled). Rendered and eyeballed via `_shots.gd`.
- Tests: `test/groundContent.test.ts` (10) — codec, each refusal, the corner rule, resize,
  the six files validate against the world file, presets deterministic, load round-trip and
  refusal before write; `loadWorldContent.test.ts` still green.

### ground-types/007 — The area map editor — paint an Area's ground from the world editor
```yaml
id: ground-types/007
title: The area map editor — paint an Area's ground from the world editor
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/tools/world-editor/area-editor.js
```

## What to do

The disabled *Edit the cell map · later* button in the world editor's inspector
(`tools/world-editor/editor.js`) becomes real. Built directly in the world editor's own visual
language — Kris's local tool, never deployed.

- `tools/world-editor/server.mjs`: `GET /elements.js` (serves `game/shell/elements.js`; the page
  `<script src>`s it), `GET` / `PUT /areas/<slug>/ground.json` (slug regex; 404 when absent;
  PUT validates shape, dimensions against `spellgrove.world.json`, values 1..64, then writes
  the row-string format). Still bound to 127.0.0.1, no auth.
- New `area.html`, `area-editor.js`, `area-editor.css` (reusing `editor.css` tokens). The
  inspector button opens it in a **new window** — `window.open('area.html?slug=…')` — and is
  disabled with a note until an unsaved new Area has been saved.
- The painter: a canvas at about 12 px per cell (60×60 = 720 px); eight base-type swatches
  (names and colours from the table); a variant picker per type including **"random mix"**
  (Earth-upper by default with the other seven sprinkled in); brush sizes 1 / 3 / 5; rectangle;
  flood fill; eyedropper; undo / redo (client-side). Overlays for portal cells, the entry cell,
  the (1,1) spawn and the home landing cell — painting an impassable type onto any of them is
  refused inline. A "loose ends" strip like the world editor's: impassable under a door, a door
  that can't be reached. If the Area was resized since the file was written, crop / pad with
  hexagram 2 on load and say so.
- Save = `PUT`, then the same three steps as the world map: read the diff, `npm run load-world`,
  commit and push. Document in `tools/world-editor/README.md` and a "The ground" subsection of
  `game/README.md`'s world-map section. Optional: the world editor's own `cellPicker` shows the
  ground colours behind its ruled grid.

The scaffold (server routes, page, palette) can be built before task 005 lands; the end-to-end
check needs 005's loader.

## Definition of done

Paint the Village river by hand → save → `git diff game/content/areas/village-of-lor.ground.json`
reads as row changes → `load-world` accepts → visible in the client. Committed and pushed.

## Where the work lands

`land-of-lor/tools/world-editor/`, `game/README.md`.

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

- `tools/world-editor/area.html` + `area-editor.js` + `area-editor.css`; `server.mjs`
  serves `/elements.js` and `GET`/`PUT /areas/<slug>/ground.json` (PUT checks shape, dims
  against the world file, 1..64; writes the row-per-line format). The inspector's
  placeholder became **Edit the ground →** (disabled until the world is saved).
- Painter: 8 type swatches, flavour picker + Random mix, brush 1/3/5, rect, fill (same
  type), pick, undo/redo, keys; portal / entry / spawn overlays with refusal; loose-ends
  strip (impassable under a door, door unreachable — the loader's rules); crop/pad on a
  resize with a note. Driven end to end in headless Chrome (paint, refusal, loose end, undo,
  save → a 4-line diff → reverted). Docs in `tools/world-editor/README.md` and
  `game/README.md`.
- Not done: the optional ground colours behind the world editor's `cellPicker`.

### ground-types/008 — Verify, deploy, ship — the ground goes live, on the web and on the phone
```yaml
id: ground-types/008
title: Verify, deploy, ship — the ground goes live, on the web and on the phone
epic: ground-types
state: done
priority: 1
blocked_by: []
estimate: M
created: 2026-08-17
updated: 2026-08-18
claimed_by: null
claimed_at: null
delivers: [10]
review_artifact: deliverables/010-the-ground-has-types.md
```

## What to do

- New `game/director/cli-test-client/verify-ground.ts` (`npm run verify-ground`, reusing the
  durable probe account via `probeSession.ts`): the Village's `ground` has 1600 cells and
  contains Water; the Badlands has all eight lower trigrams; a step into a Mountain is rejected.
- Deploy to the droplet per `game/README.md`: back the database up first, bundle, build,
  restart (watch for `[migrate] painted ground for N area(s)` in the service log), then
  `load-world` with the authored files, then `verify-ground`, `verify-world-map`,
  `verify-home-door`, `verify-portal-destinations`, `verify-accounts-e2e` — all green — then
  clear the verifier bodies. Add the entry to `Dev/CLAUDE.md` (backup name; reverse: the column
  is additive, and the protocol bump means old clients see "update needed" until they update).
- Ship the clients, in this order: `export-web.sh` and commit `site/public/embeds/lor-game`;
  `APK_NAME` → `land-of-lor-v10` and `site/src/pages/play.astro`'s `apkUrl` together;
  `export-android.sh --upload`. The "does it run on the phone" check is Kris's.
- Flip `deliverables/010-the-ground-has-types.md` to delivered (`cta: url` = /play, screenshots).
- `docs/handoff/0NN-ground-types.md`; annotate — never delete — the three placeholder comments
  (`grid.gd`, `area_server.gd`, `editor.js`); `game/README.md` layout rows for
  `content/areas/`, `paint-ground` and the editor; note for Kris the `docs/README.md` §3
  "stone is a Mountain-faced hexagram" tension with the spec (not resolved by an agent).
- Epic bookkeeping: regenerate `ground-types/status.md`; move
  `world-traversal-and-ground/001` to `review` with `review_artifact:
  land-of-lor/docs/specs/GROUND_TYPES.md` (Kris wrote the spec that task asked for).

## Definition of done

Live verifiers green; `/play` serves the new web build; the v10 APK is on the droplet;
deliverable 10 is `delivered`. Everything committed and pushed in both repos, and the site
rebuild confirmed from outside the repo.

## Where the work lands

`land-of-lor/` and this workspace (`deliverables/`, `site/`, `epics/`).

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

- `cli-test-client/verify-ground.ts` (`npm run verify-ground`): 15/15 live.
- Droplet: backup, bundle deploy of `b8dbdb1`, restart (`[migrate] painted ground for 18
  area(s)`), `load-world` (6 painted from files), all five verifiers green, verifier bodies
  cleared, `purge-probes --yes`. Recorded in `Dev/CLAUDE.md` ("Ground Types deploy").
- Web build `b8dbdb1` on `/play` (`06f25df`); `APK_NAME` → v10, exported, uploaded, linked
  from `/play` (`dfb8374`, `c2d8d9e`).
- Deliverable 010 delivered (`cta` = /play). `docs/handoff/ground-types.md` "Shipped";
  `game/README.md` known-limits entry; the three placeholder comments were annotated in
  001/002/007.
- **Not done — needs Kris:** the on-device look and the Badlands frame time on the phone.
- The `docs/README.md` §3 "stone is a Mountain-faced hexagram" tension is noted in the
  handoff for Kris, not resolved.

### ground-types/006 — The looks — tune the eight ground types and their 64 variations
```yaml
id: ground-types/006
title: The looks — tune the eight ground types and their 64 variations
epic: ground-types
state: done
priority: 2
blocked_by: []
estimate: M
created: 2026-08-17
updated: 2026-08-18
claimed_by: null
claimed_at: null
delivers: []
review_artifact: land-of-lor/docs/handoff/ground-types/badlands-textured.png
```

## What to do

Spec items 3 and 4 as a content pass with the game running, separate from the rendering
plumbing. Set the placeholder colours and patterns in `game/shell/elements.js` (`ground` on the
eight trigrams, `groundVariant` on the 64 elements) until each of the eight reads at a glance —
grass green, sand yellow (some variants dirt), snow and ice, lava red-orange, water and deep
water blues, mountain grey, Heaven as eight distinct decorative floors (tiles, bricks, paths) —
and each variant is visibly a flavour of its base. Write into `elements.js`'s header that ground
colours are **not** the trigram identity colours (Earth's identity colour is brown; grass is
green). Re-run `sync-elements.sh`.

## Definition of done

Screenshots of all eight types (and the Badlands showcase) in `docs/handoff/`; Kris reviews.
Committed and pushed in `land-of-lor`.

## Where the work lands

`land-of-lor/game/shell/elements.js`, `docs/handoff/`.

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

- Base albedos retuned for how they light (grass `#467a31`, sand `#c2a866` + two dirt
  flavours, snow `#a9b6c0` + two ice flavours, water, lava, mountain, deep water, stone
  `#9c9081`); Stone's eight became named floors (Stone tiles, Plank floor, Gravel path,
  Mosaic, Red bricks, Cobbles, Blue tiles, Gold flagstones) with `bricks` / `planks` /
  `checks` patterns added to the atlas; the scene's ambient 0.6 → 0.45 and sun 1.0 → 0.9 so
  light types stop clipping (both in `client_main.gd` and `_shots.gd`). Header of
  `elements.js` says why.
- Judged in `_shots.gd` (a new `g-ground-corner` close-up) — screenshots in
  `docs/handoff/ground-types/`. Snow still reads bright, deliberately; its flavours survive.
- **This is a placeholder pass; Kris's eye decides.** Every value is one line in
  `elements.js` and re-syncs with `sync-elements.sh`.

## 2026-08-18 — the brief for real art

`land-of-lor/docs/design/briefs/GROUND_TILES_BRIEF.md` — for Claude Design: 64 seamless tiles, the
constraints the engine imposes (repeat per cell, the shader does the transitions, flat-lit),
the storybook direction, and the full table of the 64. Kris pastes it in; the PNGs drop into
`world/client/ground/<lower>/<upper>.png` and the engine uses them over its drawn tiles.

## questions/ (0)

## deliverables (3)

- #13 — The first painted tiles — sixty-four textures land on the ground of Lor (2026-08-18) — /deliverables/13
- #12 — The Village before the real tiles — what the ground looked like on 18 August 2026 (2026-08-18) — /deliverables/12
- #10 — The ground has types — 64 kinds of ground, a river through the Village, and mountains in the Badlands (2026-08-17) — /deliverables/10