---
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.