# Tera

The map view of **Lumbridge Simulate** — California from above, in three.js.
Its other half, **Spaces**, is the offices you walk into: one engine and one
asset library, seen from outside and from inside.

Apache 2.0. Runs at [tera.lumbridgecorp.com](https://tera.lumbridgecorp.com).

![San Francisco](docs/sf.jpg)

---

## What it is

An engine plus data packs. The default board joins Los Angeles and San Francisco
with live deterministic traffic on US-101 and on the honest I-5 → I-580 → I-80
approach. Choose either route chapter to follow the procedural black Model X.

The engine renders terrain, coastline, built cities on authored street grids,
bridges, roads, markers, road traffic and air traffic. A *city pack* is
pure data — coastlines, hills, districts, landmarks, camera chapters — so adding
a city is a data contribution anyone can review, not a fork.

California, the detailed Bay Area, and Los Angeles / Orange County / Riverside
ship today. The corridor is intentionally sparse; detailed cities remain their
own boards rather than forcing a 600 km world into one full-resolution mesh.

A **plan view** sits top right: the board drawn flat, with the footprint of the
camera's own frustum on it, so you can see where you are looking from outside
the shot. Click or drag it to move the camera; scroll it to dolly. It is a 2D
canvas rather than a second WebGL context, drawn from the same city pack, and it
follows the sun into the night along with everything else.

## Who sees what

Three tiers, resolved once at boot by `src/access.ts`:

| | anonymous | signed in | admin |
|---|---|---|---|
| the map, the plan view, the named chapters | ✅ | ✅ | ✅ |
| observed weather and live aircraft | ✅ | ✅ | ✅ |
| the office | public depth — shell, furniture, viewpoints, nobody home | full depth, with presence | full depth |
| the marker feed | per `TERA_MARKERS_ACCESS` | ✅ | ✅ |
| the godmode panel (`G`) — date, season, weather override, counters, pose editor | — | — | ✅ |

The sky is public on purpose. Cloud cover over San Francisco is a government
sensor reading, and the aircraft are broadcasting their positions unencrypted to
anyone with a receiver; neither is something an account can grant you access to.
Gating them cost the only moment that makes this project land — real fog rolling
off the Pacific onto a city you recognise, at the real time of day, on a first
visit.

The **markers** are the one feed that can carry something private, so the server
decides. `TERA_MARKERS_ACCESS` is `members` by default and an operator has to
say `public` out loud, which `/api/v1/health` then announces in `degraded[]`.
The default is the safe answer rather than the common one, because the failure
mode is silent: nothing errors, nothing looks broken, the data is just readable
by the internet.

**These are drawing decisions, not a security boundary**, and `src/access.ts`
says so at length. Live data and office presence are withheld by the *API*, from
a caller it does not recognise; the client tier stops the app asking for
something it will not get. Admin is granted only by `TERA_ADMIN_SUBJECTS` on the
server — never inferred in the browser, and never from an API that failed to
answer. A deployment with no API at all is open, because "clone it and it works"
is the promise; it is not "clone it and you are an administrator".

## Quick start

```bash
npm install
npm run dev
```

## Play controls

The bottom mode dock is the local-player source of truth: View, Drive, Explore,
Fly, or office Walk. A transition clears stale held input and atomically hands
the follow camera to one subsystem. `WASD` is movement; `Q/E` is vertical or
yaw, `I/K` pitches the crow, Space is the primary action, `G` glides, `P`
resumes assistance, `R` resets, and `C` switches the driving camera. A standard
gamepad maps both sticks, triggers, shoulders, and rising-edge action buttons.

Touch play uses a pointer-ID analogue stick at lower left and only the actions
that apply to the current mode at lower right. The Map button remains available
during possession. Touch, keyboard, and gamepad state are independent, so a
released or cancelled finger cannot clear another source that is still held.
The UI and follow camera are presentation adapters only; they never enter Arena
observations, rewards, snapshots, traces, or simulator hashes.

## Headless RL environments

Tera also exports a versioned, renderer-independent Arena contract with five
deterministic environments: US-101/I-5 driving, Frontier Valley office
navigation, seeded SF/LA office robot jobs, crow waypoint flight, and California
electric-aircraft flight.
They share the client controllers and office plan, but require no canvas, DOM,
Three.js scene, network service, or new runtime dependency.

Import them from `@lumbridge/tera/arena`. Seeded train/dev scenarios,
component rewards, safety terminals, maximum steps, snapshots, checksummed
traces, exact replay and executable inaction/scripted baseline proofs are
documented in [ARENA.md](ARENA.md).

The visible SF and LA office robots use that same fixed-step job state. Their
patrol, parcel, inspection, and charging loops are authored demonstration
scenarios—not presence, telemetry, or evidence of real company work—and the UI
labels them as a seeded simulation.

## Using the engine

```ts
import { createScene } from "@lumbridge/tera/engine/scene.ts";
import { createStage } from "@lumbridge/tera/engine/stage.ts";
import SAN_FRANCISCO from "@lumbridge/tera/cities/sf.ts";

// One stage per canvas, for the life of the page. Cities are put on it and
// taken off again; a renderer per city leaks its shadow map on every switch.
const stage = createStage(canvas);

const scene = await createScene(stage, {
  city: SAN_FRANCISCO,
  markerPalette: { hiring: 0x4ade80, closed: 0xef4444 },
});

scene?.setMarkers([
  { id: "1", lat: 37.7765, lng: -122.4241, label: "Somewhere", colorKey: "hiring" },
]);
```

`createScene` is async because the heightfield is built in a Worker — half a
million samples, about 730 ms on the Bay Area, and not on the main thread. It
resolves to `null` if the build was abandoned through `options.signal`, which is
what makes switching city mid-build cheap.

The engine renders `Marker[]` and looks colours up by `colorKey` in a palette
you supply. It does not know what your markers *mean* — that mapping lives in
your adapter. This is what lets one renderer serve a private map coloured by
one scheme and a public map coloured by another, without either being a fork.

## Adding a city

Write `src/cities/<id>.ts` exporting a `City`. Trace the coastline and parks by
hand, place hills as radial peaks, and give each district its street bearing.

Two rules, and they are not stylistic:

- **Do not import geometry from OpenStreetMap.** OSM and Nominatim output is
  ODbL — share-alike, and incompatible with this repo's licence.
- **Do not commit logos or brand assets.** They are trademarks, not code.

See [ARCHITECTURE.md](ARCHITECTURE.md) §3 for the full reasoning, and
[NOTICE](NOTICE) for the attribution and data-provenance statement, and
[PROVENANCE.json](PROVENANCE.json) for the machine-checked shipped-artifact and
original procedural-lineage ledger. Run `npm run provenance`, `npm run licenses`,
and `npm run sbom` before accepting assets or dependencies.

## Aircraft

The engine takes a `FlightSource`. Two ship here: `SimulatedFlights` (original,
flies real approach and departure corridors) and `AdsbFlights` (open community
ADS-B feeds such as adsb.lol).

FlightRadar24 is deliberately absent — their terms forbid scraping and forbid
redistributing their data, so a client for it cannot live in an Apache-2.0
repository. Commercial sources belong in private deployments. The best long-term
answer is an RTL-SDR receiver: first-party data with nothing to comply with.

## Layout

```
src/engine/    renderer — terrain, blocks, structures, markers, flights, scene, minimap
src/cities/    data packs — pure geography, no code
src/transport/ serializable route packs and renderer-independent simulation
src/arena/     versioned headless RL contract, scenarios, traces and environments
src/assets/    original procedural asset library
src/adapters/  where outside data plugs in
src/tools/     instruments — god-only, dynamically imported, never statically
```

`engine` never imports `cities`; neither imports `adapters`.

Nothing under `src/tools/` may be reached by a static import from the app. It is
loaded by one `await import()` behind `access.can.debug`, so a visitor who is
not an admin does not download the code at all — which is the strongest
available reading of "nothing here runs for a non-god visitor": not a hidden
panel, not a disabled panel, no panel. `src/tools/index.ts` states the rule and
what silently undoes it.

## Licence

Apache License 2.0 — see [LICENSE](LICENSE) and [NOTICE](NOTICE).

The ordered build plan and parallel work lanes live in [BUILD_PLAN.md](BUILD_PLAN.md).
