---
id: world-map-and-portals/002
title: The two map surfaces — a standalone map page on the website and a map screen in the game
epic: world-map-and-portals
state: done
priority: 2
blocked_by: []
estimate: L
created: 2026-08-12
updated: 2026-08-15
claimed_by: null
claimed_at: null
delivers: []
review_artifact: null
---

## What

Build the two places a player looks at the map. Both render the *same* world graph — one
source of truth, two surfaces — which is why this task waits on 001 settling whether that
graph is authored content or a live projection.

### Surface 1 — the public site, standalone

A page on lor.kris.ai-task-runner.com showing the public world: the village of Lor and
what it links to, laid out so someone who has never played can see how the world hangs
together. Above it, a **server dropdown** — there will be more than one server, and the
map is per-server.

Constraints that already apply and are not up for renegotiation here:
- Site code is agent-only, human-directed (workspace `CLAUDE.md`, "Who edits what").
- `brand/design.md` (storybook-cover direction, palette, typefaces) and `brand/voice.md`
  are read first, before any pixel or any sentence.
- The site is a thin renderer over markdown built by `npm run build` into `../public`.
  If 001 decides the map is *live* rather than authored, that collides with "no backend,
  no database — if it can't be a markdown file read at build time it's out of scope"
  (`public-site-and-comms` non-goals). Resolving that collision is part of this task:
  either the map is authored content, or the site fetches it client-side from the
  director, or a build step snapshots it. Pick one, and say why.

### Surface 2 — in the game

A map screen reachable from the game interface, showing the same public graph **plus the
player's private layer**: their own home area and portals to their friends' realms. Built
as native Godot Control-node scenes alongside the existing UI in
`land-of-lor/game/world/client/ui/`, per `game-interface/004`'s resolution — one
implementation on web and Android, no DOM shell UI.

`player_state.gd` is the documented seam where per-account data plugs in
("when the server starts owning this data, this is the ONE file that changes"). The
private map layer is exactly that kind of data, so it arrives through that seam and not
by a second mechanism.

## Definition of done

- The public map is live on the site with a working server picker, and looks like it
  belongs to the rest of the site.
- The in-game map screen opens from the interface, shows public and private layers, and
  ships identically on web and Android.
- Both read the same source of truth, whatever 001 decided that is.

## Blocked by

`world-map-and-portals/001` — naming, the authored-vs-derived call, what a server means,
and what the private layer reveals all have to be settled before either surface can be
built without guessing.

## Input Kris still owes

The paper sketch of the Lor map (see 001) — it drives the visual layout of both surfaces.


---

## Superseded, 2026-08-15

Both surfaces were built under **world-map-and-portals/006**, against the approved Claude
Design output rather than the sketch this task was waiting on. Closed here so the work isn't
tracked in two places; 006 carries what actually landed and what was deliberately left.

The collision this task flagged — a live map versus `public-site-and-comms`' "no backend, no
database, if it can't be a markdown file read at build time it's out of scope" — was resolved
the third way it offered: the site stays a static build and fetches a public read-only JSON
endpoint on the director at runtime. Kris's call. It is the site's first runtime fetch, which
is why the page has a real loading state and a real failure state rather than rendering an
empty world when the game server is unreachable.