1
0

The city points at its own buildings, and the sky stops depending on an API

**Clouds were invisible to everyone who had not wired up NWS.** The layer
took `currentWeather()?.cloudCover ?? 0`, and `currentWeather()` is null on
any deployment without a weather source — which is the default, and the
exact configuration this repo is held to: a stranger clones it, runs one
command, and gets a city with no account and no key. Their sky was
permanently, silently empty. `atmosphere.ts` already models a sky when
nobody has observed one; it now models cover too, an observed reading
still wins outright, and the clouds are there on a bare clone.

**Both offices are pins on the city, and clicking one walks you in.** Each
pack has carried a real `site` since the sun needed one, and that
coordinate was known to the lighting and to nothing else — a visitor
looking at the board had no way to tell that two of those buildings are
ones they can go inside. The coordinates move to a tiny eagerly-imported
`offices/sites.ts` that the packs import *from*, because a pack is a 25 kB
lazy chunk and the board wants its pins long before anybody opens a door.
A test asserts the pack and the table hold the **same object**, not merely
equal values: a drifted coordinate would put the marker on one building
and the sun on another and both would look entirely plausible.

**Aircraft bank into their turns.** The roll channel existed and was never
written, so every turn was flat. Bank comes from the coordinated-turn
relation against the measured turn rate, damped by a first-order lag so it
settles rather than oscillates, and clamped at 30° like a real limiter.
Six regression tests, because roll is the one channel that feeds itself —
position and heading are recomputed from the last two observations and
wash out a bad value, while a NaN in the roll would persist for the life
of the track.

That fed straight into a real defect: `AdsbFlights` substituted
`heading: 0` for records with no `track` field, which is harmless for a
symmetrical dart and is a **sustained full-scale artefact** once aircraft
bank — a target whose real heading is 200° reported as 0° reads as a 160°
turn and pins the roll at its limiter for as long as it is in the feed.
Those records are dropped now. An aeroplane the feed will not give a
heading for is one this layer cannot draw honestly.

**The office empties out overnight.** A full complement of seated people
at one in the morning, under house lights that came on because the sun is
down, was the least believable thing left in the room once the clock
became real. A live roster always wins — an API that says the building is
empty is telling the truth about the building.

**Robots go somewhere.** They pick real addresses — a seat, a room — and
turn to face the seat when they arrive, rather than stopping at a random
angle. Godmode gets an office section: house lights forced on or off or
following the sun, robots and ceilings toggled, with a readout.

**The bundle is split.** Entry chunk 758 kB to 208 kB, with three.js and
satellite.js in a vendor chunk that survives an app deploy instead of
being re-downloaded on every one. Rollup's 500 kB warning still fires and
should — it now points at three.js, where it is true, instead of at our
code, where it was pointing at three.js all along.

Reviewers caught two false geography claims in the new prose ("both
shipped buildings stand in San Francisco" — one is across the estuary at
Alameda Point) and several miscounted figures. Fixed. In a codebase where
the comments are the design record, those are defects.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-07 04:18:00 -07:00
parent 51979feea0
commit 2d87d9f354
14 changed files with 2304 additions and 72 deletions
+220 -18
View File
@@ -44,10 +44,11 @@ import {
import {
SAMPLE_MARKERS,
SAMPLE_PALETTE,
SAMPLE_PRESENCE,
SAMPLE_PRESENCE_PALETTE,
samplePresenceAt,
sampleRoutesFor,
} from "./adapters/sample.ts";
import { OFFICE_SITES } from "./offices/sites.ts";
import { authFetch } from "./session.ts";
import { capabilitiesFor, resolveAccess, type Access } from "./access.ts";
import { createMinimap, type Minimap } from "./engine/minimap.ts";
@@ -73,7 +74,7 @@ import type { MaterialRegistry } from "./assets/materials.ts";
// protect. It arrives with the office, in `loadOffice()`, because it is only
// ever drawn once you are standing in one.
import type { OfficeMinimap } from "./engine/officeMinimap.ts";
import type { Godmode, GodmodePlace } from "./tools/index.ts";
import type { Godmode, GodmodeHouseLights, GodmodePlace } from "./tools/index.ts";
import type { PoseEditor } from "./tools/poseEditor.ts";
const CITIES: { id: string; label: string; city: City }[] = [
@@ -147,7 +148,73 @@ let wantedCity = "sf";
let office: OfficeScene | null = null;
let inside = false;
let markers: Marker[] = SAMPLE_MARKERS;
let palette: MarkerPalette = SAMPLE_PALETTE;
/**
* The buildings you can walk into, as pins on the city.
*
* This is the one thing that makes Tera and Spaces feel like one product rather
* than two views sharing a bundle. Both packs carry a real `site` — it is what
* puts the sun in the right place — and until now that coordinate was known to
* the lighting and to nothing else. A stranger looking at the board had no way
* to tell that two of those buildings are ones they can go inside.
*
* `OFFICE_SITES` rather than the packs themselves, deliberately: a pack is a
* lazy chunk worth tens of kilobytes and the city wants these the instant the
* board appears, long before anybody opens a door. See `offices/sites.ts`.
*
* `colorKey` is opaque to the engine, as every `Pin.colorKey` is — `SAMPLE_PALETTE`
* resolves it, and giving these their own key is what lets a door look different
* from a company.
*/
const OFFICE_MARKERS: Marker[] = OFFICE_SITES.map((entry) => ({
id: `office:${entry.id}`,
label: entry.name,
colorKey: "office",
blurb: `${entry.site.label ?? "An office"} — click to walk in`,
lat: entry.site.lat,
lng: entry.site.lng,
// Hand-typed from the street grid, like every other coordinate here. Not a
// placeholder, so it is drawn as a real address.
located: true,
}));
/**
* Whether a marker is a door rather than a company.
*
* The id prefix is the whole test, and it is deliberately something no marker
* off the wire can collide with: `markers/gate.ts` serves rows from a synced
* database and none of them are namespaced this way.
*/
function officeIdOf(marker: Marker): string | null {
return marker.id.startsWith("office:") ? marker.id.slice("office:".length) : null;
}
/**
* Whatever the pointer is over, so a click knows what it clicked.
*
* The engine reports picks by hover rather than by click — that is what drives
* the detail card — so the click handler has no argument of its own and reads
* this instead. `null` whenever the pointer is over open ground, which is what
* makes a click on the terrain do nothing.
*/
let hoveredMarker: Marker | null = null;
/**
* The colour a door is drawn in, which no marker feed knows about.
*
* `colorKey` is opaque to the engine and resolved by the consuming app, so the
* palette is this file's business. The office key is merged in **here** rather
* than added to `SAMPLE_PALETTE`, because it is not a sample of anything: a
* deployment that replaces the whole marker feed with its own palette
* (`feed.palette`, further down) must still get doors it can see, and folding
* this into the sample set would lose it the moment real markers arrived.
*
* Amber, to sit with the chapter list and the "Enter the office" button rather
* than with the marker hues — a door is a piece of this application's
* navigation, and it should read as one.
*/
const OFFICE_PALETTE: MarkerPalette = { office: 0xf5b53f };
let palette: MarkerPalette = { ...SAMPLE_PALETTE, ...OFFICE_PALETTE };
let liveData = false;
/**
* What this visitor may do. Resolved once in `boot()`; every gate below reads
@@ -290,6 +357,16 @@ let officePlan: OfficeMinimap | null = null;
* possible to take it without the caption.
*/
let presenceIsSample = false;
/**
* The deployment's own roster, or `null` while the sample one stands in for it.
*
* Held separately from what is on screen because the two are refreshed by
* different things: a real roster arrives from the API on its own timer, and the
* sample one is a function of the clock and has to be recomputed whenever the
* clock moves. Collapsing them would mean either re-rendering a live roster on
* every scrub or freezing the sample one.
*/
let livePresence: Presence[] | null = null;
/**
* The running poll of who is in, or `null` when nobody is standing in the room.
*
@@ -369,6 +446,28 @@ function cityDoorUrl(cityWanted?: string): string | null {
* override, one writer, one type that can carry everything the sun depends on.
*/
let instantOverride: Date | null = null;
/**
* Where the office's house lights take their level from. Godmode's switch, and
* it survives the office being rebuilt because the panel re-asserts it.
*/
let houseLights: GodmodeHouseLights = "sun";
/**
* The sun's height as the *fittings* are told it, which is the real one unless
* somebody has a hand on the switch.
*
* Forcing the level by lying about the elevation rather than opening a second
* path into `luminaires.ts`, and that is the design rather than a shortcut:
* `OfficeScene.setSolarElevation` feeds the fittings and nothing else, while
* the rig outside is computed from the real `env` a line later. So this moves
* the *interior* and leaves the sky, the key light and the fog exactly where the
* clock put them — which is the whole point of a control that lets you look at
* the night office in daylight. ±90° is far outside the 0°–6° ramp in
* `luminaires.ts`, and stays outside it if that band is ever widened.
*/
function houseElevation(actual: number): number {
return houseLights === "sun" ? actual : houseLights === "on" ? -90 : 90;
}
/**
* A fabricated sky, or `null` for whatever the deployment reports.
*
@@ -432,8 +531,8 @@ function officeLighting(site: NonNullable<Office["site"]>) {
* ramp so the first frame is already correct rather than a lit room fading
* down or a dark one fading up.
*/
office?.setSolarElevation(env.sun.elevation);
const house = office?.houseLevel() ?? houseLevelFor(env.sun.elevation);
office?.setSolarElevation(houseElevation(env.sun.elevation));
const house = office?.houseLevel() ?? houseLevelFor(houseElevation(env.sun.elevation));
return withHouseLights(officeDaylight(state, site), house);
}
@@ -462,6 +561,9 @@ function updateSun() {
if (state) office.setLighting(state);
}
// The roster follows the same clock the sun does. Never over a live answer.
if (livePresence === null) applyPresence();
if (!city || !atmosphere) return;
const env = observe(active.center.lat, active.center.lng, currentInstant(), currentWeather());
city.setLighting(atmosphere.apply(env));
@@ -470,12 +572,20 @@ function updateSun() {
* The sky's own cover, which is a different question from what it does to the
* light and is why the scene takes it separately.
*
* `null` weather is "nobody was asked" — the state `currentWeather` is careful
* to preserve — and for cloud the honest reading of that is a clear sky rather
* than an invented overcast. The modelled marine layer already reaches the rig
* through `observe`; this is the *observed* cover when a station reported one.
* `atmosphere.cloudCover(env)` and **not** `currentWeather()?.cloudCover ?? 0`,
* and the difference is the whole point of the layer existing. `null` weather
* is "nobody was asked", which is not an edge case — it is the *default*
* deployment and the exact configuration this repo is held to: a stranger
* clones it, runs one command, and gets a city with no account and no key.
* Falling back to zero meant that stranger's sky was permanently, silently
* empty, and the cloud layer only ever appeared for somebody who had wired up
* NWS.
*
* `atmosphere` models a sky when nobody has observed one — it already does
* exactly that for the marine layer — and an observed cover still wins
* outright when there is one. See `cloudCover` in `atmosphere.ts`.
*/
city.setCloudCover(currentWeather()?.cloudCover ?? 0);
city.setCloudCover(atmosphere.cloudCover(env));
city.setWind(currentWeather()?.windKph ?? null, currentWeather()?.windDirDeg ?? null);
// The override itself, not `currentInstant()`. Handing over a resolved date
// would peg the sky to whatever second this ran in, and this runs about once a
@@ -611,7 +721,19 @@ async function mountCity(id: string) {
markerPalette: palette,
flights: dial.source,
...(catalogue ? { satellites: catalogue } : {}),
onMarkerPick: (m) => showDetail(m ? `${m.label}${m.blurb ? `${m.blurb}` : ""}` : null),
/**
* A pin is a hover *and* a click, and an office pin is a door.
*
* `onMarkerPick` fires for both — `scenekit`'s picking calls it on hover
* with the marker and on leave with `null` — so this cannot simply open a
* building on every call or the office would fly open the moment the pointer
* crossed a tower. The hover shows the card; the click is a separate
* listener below, which reads whatever the hover last resolved.
*/
onMarkerPick: (m) => {
hoveredMarker = m;
showDetail(m ? `${m.label}${m.blurb ? `${m.blurb}` : ""}` : null);
},
signal: mount.signal,
// An abandoned build keeps its worker running for a tick or two after the
// abort; its percentages must not land on the card the new city is using.
@@ -695,7 +817,11 @@ async function mountCity(id: string) {
// not a decoration. LA gets its own weather, not San Francisco's fog.
marineLayer: id === "sf" ? PACIFIC_MARINE_LAYER : null,
});
city.setMarkers(id === "sf" ? markers : []);
// The offices are in the Bay Area, so they ride along with that board's
// markers and are absent from the Southland's — the same rule the sample set
// already follows, for the same reason: a pin for a building six hundred
// kilometres off the board is a pin in the wrong place.
city.setMarkers(id === "sf" ? [...markers, ...OFFICE_MARKERS] : []);
city.onChapterChange(() => renderLegend());
/**
@@ -745,7 +871,7 @@ async function mountCity(id: string) {
},
});
showPlan();
minimap.setMarkers(id === "sf" ? markers : []);
minimap.setMarkers(id === "sf" ? [...markers, ...OFFICE_MARKERS] : []);
// The instruments, for the one visitor in a deployment who has them. The pose
// editor holds a `World`, a camera and a controls, so it belongs to the board
@@ -1098,8 +1224,8 @@ function watchOccupancy() {
const scene = office;
if (!scene || scene.depth !== "full") return;
presenceWatch = tera.watchPresence(officePack?.id ?? "lumbridge-hq", (body) => {
const people: Presence[] = body?.people ?? SAMPLE_PRESENCE;
presenceIsSample = body === null;
livePresence = body?.people ?? null;
presenceIsSample = livePresence === null;
// The scene may have been torn down between a request going out and coming
// back — a city switch disposes the office — and writing people into a
// disposed layer is a use-after-free with a friendly name. The watch's own
@@ -1107,8 +1233,7 @@ function watchOccupancy() {
// because the office can also be *replaced* (signing in rebuilds it at full
// depth) without the watch having been stopped in between.
if (office !== scene) return;
scene.setPresence(people);
officePlan?.setPresence(people);
applyPresence();
renderOfficeBadge();
renderSource();
});
@@ -1117,6 +1242,29 @@ function watchOccupancy() {
function stopWatchingOccupancy() {
presenceWatch?.stop();
presenceWatch = null;
// Or the next building — or the next entry into this one — opens wearing the
// previous deployment's roster while its own request is still in the air.
livePresence = null;
}
/**
* Put whoever is in the building on screen.
*
* The live roster if there is one, and otherwise the sample one **as it would be
* at the instant being rendered**. That second half is what stops the office
* showing a full complement of seated people at one in the morning, under house
* lights that came on because the sun is down — which was the least believable
* thing left in the room once the clock became real.
*
* A live answer always wins. An API that says the building is empty is telling
* the truth about the building, and dressing it with invented people would be
* the one lie this whole layer is arranged to avoid.
*/
function applyPresence() {
if (!office || office.depth !== "full") return;
const people = livePresence ?? samplePresenceAt(currentInstant());
office.setPresence(people);
officePlan?.setPresence(people);
}
// ---- Chrome ---------------------------------------------------------------
@@ -1572,6 +1720,29 @@ async function toggleOffice() {
enterButton?.addEventListener("click", () => void toggleOffice());
/**
* Clicking a building on the city walks into it.
*
* On the canvas rather than on anything the engine owns, because the engine
* reports picks by *hover* — `onMarkerPick` fires as the pointer crosses a pin
* and again with `null` as it leaves — so there is no click event to hang this
* on down there. `hoveredMarker` is whatever that hover last resolved, which is
* exactly what a click on the same pixel means.
*
* Guarded on not already being inside: the city's canvas is the office's canvas
* too, they share one renderer, and a stray click on the floor of a room should
* not re-enter the building you are standing in.
*/
canvas.addEventListener("click", () => {
if (inside) return;
const marker = hoveredMarker;
if (!marker) return;
const id = officeIdOf(marker);
if (id === null) return;
officeId = id;
void building(`Opening ${marker.label}`, () => enterOffice());
});
// ---- Panels, plan and overlays ----------------------------------------------
/**
@@ -1840,6 +2011,36 @@ async function mountGodmode() {
* over the board that was current when the panel opened would go on driving
* a disposed scene after the first city switch.
*/
/**
* Read through the module-level `office`, never closed over — the panel is
* mounted once and every `switchOffice` replaces the scene underneath it.
*/
office: {
onHouseLights(mode) {
houseLights = mode;
// `officeLighting` is the only caller of `setSolarElevation` and
// `updateSun` is the only caller of that, so this is the whole apply.
updateSun();
},
onRobotsVisible(visible) {
office?.setRobotsVisible(visible);
},
onCeilingsVisible(visible) {
office?.setCeilingsVisible(visible);
},
read() {
// Only while a room is actually on the stage. Out in the city the office
// is paused and kept, and a dimmer aimed at a scene nobody is rendering
// is exactly the dead panel this section is arranged to avoid.
if (!inside || !office) return null;
return {
id: officeId,
depth: office.depth,
houseLevel: office.houseLevel(),
robots: office.robots().length,
};
},
},
sky: {
onExtraTraffic(count) {
trafficDial?.setExtra(count);
@@ -2129,7 +2330,8 @@ async function boot() {
try {
const feed = await tera.markers();
markers = feed.value;
palette = feed.palette;
// The caller's palette, plus the door colour it cannot know about.
palette = { ...feed.palette, ...OFFICE_PALETTE };
liveData = feed.live;
} catch {
// A missing API is the self-host default, not an error.