Real weather, real aircraft, a heightfield off the main thread, and instruments
Three things that were built and never connected, connected.
**The weather was already there.** `observe()` has always taken a
`WeatherObservation` and `main.ts` has always passed null, so the cloud,
precipitation, visibility and marine-layer paths in atmosphere.ts had never run
outside a test. The server already shipped NWS, met.no and Open-Meteo, all
configured off. What was actually missing was that a single TERA_ORIGIN_LAT/LNG
served one metro and lied to the other — so weather and traffic are per-region
now, derived from the city's own bounds, and the Bay Area gets its fog while
Long Beach gets its own sky. The route takes ?city= or a validated ?lat=&lng=
and refuses to become an open geocoding proxy for the planet.
**The heightfield moved to a Worker.** 2.3 s of blocked main thread at boot, and
another ~950 ms of point-in-polygon on top of it: the park mask is filled in the
worker now, and block placement samples four corners and only runs the exact
test on a cell that straddles an edge — 8 buildings differ out of 185,036.
createScene is async and takes a Stage as a consequence, and there is a
main-thread fallback because "clone it and it works" has no exception clause.
**Spaces is a chunk you fetch when you reach for the door**, not one everybody
downloads. Same for the godmode tools. The entry chunk is 722 kB rather than
772; three.js is most of what is left and splitting it is a different job.
**Godmode is an instrument panel now** rather than one slider: the date and the
season, not just the hour, so the Meeus moon and the sun's seasonal arc become
visible instead of merely correct; a weather override that says on screen when
it is lying; a frame-time and draw-call readout; and a pose editor that emits a
paste-ready Chapter block, which is the thing that makes adding New York cheap.
Two blockers the review caught:
- Every city switch leaked 8 GPU textures — one of them a 2048x2048 shadow map
— and ~10.5 shader programs, and deleteTexture had never been called once in
the app's lifetime. The renderer was being built per scene; it belongs to the
canvas, for the life of the page.
- An upstream fetch that threw rather than returning null skipped the cache
stamp, so the TTL — the only rate limit on outbound calls — collapsed to one
upstream request per inbound request, and the caller got a 500.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,7 +35,7 @@ Three tiers, resolved once at boot by `src/access.ts`:
|
||||
| the map, the plan view, the named chapters | ✅ | ✅ | ✅ |
|
||||
| the office | public depth — shell, furniture, viewpoints, nobody home | full depth, with presence | full depth |
|
||||
| live markers and live traffic | — | ✅ | ✅ |
|
||||
| the time scrubber and debug readouts | — | — | ✅ |
|
||||
| the godmode panel (`G`) — clock, weather override, counters | — | — | ✅ |
|
||||
|
||||
**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
|
||||
@@ -56,18 +56,28 @@ npm run dev
|
||||
|
||||
```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";
|
||||
|
||||
const scene = createScene(canvas, {
|
||||
// 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([
|
||||
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
|
||||
@@ -104,10 +114,18 @@ answer is an RTL-SDR receiver: first-party data with nothing to comply with.
|
||||
src/engine/ renderer — terrain, blocks, structures, markers, flights, scene, minimap
|
||||
src/cities/ data packs — pure geography, no code
|
||||
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).
|
||||
|
||||
Reference in New Issue
Block a user