1
0
This repository has been archived on 2026-08-25. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
tera/src/main.ts
T

3867 lines
158 KiB
TypeScript

/**
* The demo: two cities under a real sun and moon, and one office you can step
* into.
*
* It ships **no real company data**. The markers are fabricated — see
* `src/adapters/sample.ts`, which says so loudly — because real positions are
* geocoded and real pipeline status is private, and neither belongs in this
* repo. When a Tera API is present the same markers arrive from it instead, and
* the UI says which of the two it is showing.
*
* It also runs with no server at all: the sun and moon are computed locally,
* the traffic is simulated, the office is a data file. Clone it and it works.
*/
import {
createAtmosphere,
observe,
PACIFIC_MARINE_LAYER,
type Atmosphere,
type WeatherObservation,
} from "./engine/atmosphere.ts";
import { officeDaylight, withHouseLights } from "./interiors/daylight.ts";
import { createScene, type SceneHandle } from "./engine/scene.ts";
import {
regionOf,
SimulatedFlights,
withTrafficDial,
type TrafficDial,
} from "./engine/flights.ts";
import { SatelliteCatalogue, type SatelliteElements } from "./engine/satellites.ts";
import type { Pose } from "./engine/scenekit.ts";
import { createStage, deviceProfile } from "./engine/stage.ts";
import { daylightPhase } from "./engine/solar.ts";
import type { City, Marker, MarkerPalette, View } from "./engine/types.ts";
import CALIFORNIA from "./cities/california.ts";
import SAN_FRANCISCO from "./cities/sf.ts";
import SOCAL from "./cities/socal.ts";
import CALIFORNIA_TRANSPORT from "./transport/california.ts";
import {
mergeVehicleActions,
sampleStandardGamepad,
type GamepadButtonState,
} from "./input/vehicle.ts";
import type { VehicleActionSnapshot } from "./transport/vehicleController.ts";
import {
createTeraClient,
describeLiveness,
type PresenceWatch,
type TrafficSource,
type WeatherWatch,
} from "./adapters/http.ts";
import {
SAMPLE_MARKERS,
SAMPLE_PALETTE,
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";
import {
createJourney,
journeyReducer,
loadJourneySession,
saveJourneySession,
type JourneyCity,
type JourneyEvent,
type JourneyState,
} from "./journey/index.ts";
import {
actorKindForPresence,
createProfileEditor,
createDefaultLocalProfile,
createWebcamCapture,
createWebcamFaceConsent,
createWebcamFacePanel,
createWebcamFaceTexture,
loadLocalProfile,
resolveHumanoidAppearance,
saveLocalProfile,
type LocalProfile,
type ProfileEditor,
type WebcamCaptureController,
type WebcamFacePanel,
type WebcamFaceTextureAdapter,
} from "./profile/index.ts";
import type { ActorIdentity } from "./actors/controller.ts";
import { SRGBColorSpace, VideoTexture } from "three";
import {
CALIFORNIA_AIR_ROUTE,
createAircraftPoseSnapshot,
type AircraftActionSnapshot,
} from "./aircraft/index.ts";
import {
createOfficeScreenPanel,
type MediaSurfaceDescriptor,
type OfficeScreenPanel,
type OfficeScreenRemoteStatus,
} from "./media/index.ts";
import type { RemoteMediaState, RemoteOfficeMedia } from "./media/remoteMedia.ts";
/**
* Three type-only imports and not one value among them, which is what keeps the
* office and the instruments out of the entry chunk.
*
* `import type` is erased before Rollup ever sees it, so none of these three
* files is an edge in the module graph and none of them lands in the 780 kB
* everybody downloads. The office arrives through the `await import()` in
* `loadOffice()`, the tools through the one in `boot()`, and the rule that says
* so for `src/tools/` is written out at the top of `tools/index.ts`. Turning any
* of these into a value import silently undoes the split, and nothing fails —
* the bundle just gets big again.
*/
import type { OfficeScene } from "./interiors/officeScene.ts";
import type { Office, Presence } from "./interiors/types.ts";
import type { MaterialRegistry } from "./assets/materials.ts";
// Type-only for the reason above, and it matters more here than it looks:
// `officeMinimap.ts` imports the asset registry as a *value*, to read prop
// footprints, so a value import of it here would drag the furniture catalogue
// back into the entry chunk and quietly undo the split this block exists to
// 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, GodmodeHouseLights, GodmodePlace } from "./tools/index.ts";
import type { PoseEditor } from "./tools/poseEditor.ts";
import type {
EntityPoseSnapshot,
InterestCell,
RealtimeClient,
ServerRealtimeMessage,
} from "./realtime/index.ts";
import type { PresenceIndicator } from "./realtime/presenceIndicator.ts";
const CITIES: { id: string; label: string; city: City }[] = [
{ id: "california", label: "California", city: CALIFORNIA },
{ id: "sf", label: "Bay Area", city: SAN_FRANCISCO },
{ id: "socal", label: "SoCal", city: SOCAL },
];
/** The corridor's scale doors, and the office each detailed board arrives near. */
const CALIFORNIA_DESTINATIONS = new Map<string, { cityId: string; officeId: string }>([
["los-angeles", { cityId: "socal", officeId: "mateo-court" }],
["san-francisco", { cityId: "sf", officeId: "lumbridge-hq" }],
]);
const JOURNEY_SESSION_KEY = "tera:journey:v1";
const restoredJourney = loadJourneySession(sessionStorage, JOURNEY_SESSION_KEY);
let journey: JourneyState = restoredJourney.status === "loaded"
? restoredJourney.state
: createJourney({
actor: {
id: "anonymous",
kind: "crow",
signedIn: false,
profile: { displayName: "Guest" },
},
});
const LOCAL_PROFILE_KEY = "tera:profile:v2";
let localProfile: LocalProfile | null = null;
function humanoidAppearance() {
return localProfile ? resolveHumanoidAppearance(localProfile.appearance) : null;
}
function signedInActorIdentity(profile = localProfile): ActorIdentity {
const appearance = profile ? resolveHumanoidAppearance(profile.appearance) : null;
return {
id: access.subject ?? "anonymous",
displayName: profile?.displayName ?? access.subject ?? "Guest",
authenticated: access.subject !== null,
profile: {
appearance: appearance
? {
skinTone: appearance.skinTone,
primaryColor: appearance.outfitColor,
accentColor: appearance.accentColor,
hairColor: appearance.hairColor,
bodyShape: appearance.bodyShape,
}
: { primaryColor: "#151a20", accentColor: "#f2b134" },
},
};
}
function dispatchJourney(event: JourneyEvent): boolean {
const next = journeyReducer(journey, event);
if (next === journey) return false;
journey = next;
saveJourneySession(sessionStorage, JOURNEY_SESSION_KEY, journey);
return true;
}
function journeyToCity(city: JourneyCity): void {
if (journey.location.scale === "office") dispatchJourney({ type: "leave-office" });
if (journey.location.scale !== "california") dispatchJourney({ type: "return-to-california" });
dispatchJourney({ type: "navigate-to-city", city });
}
/**
* The buildings this page can walk into.
*
* Two of them, and the second one is why this is a table rather than the single
* hardcoded `import("./offices/lumbridge-hq.ts")` it replaces. They are
* deliberately unalike — a two-storey tower floor 188 m above Transbay, and a
* hangar four metres above reclaimed ground at Alameda Point — because the
* thing worth showing is that one engine and one format render both, and that
* `OfficeSite` is what makes them feel like different places rather than the
* same room with different furniture.
*
* The loaders stay lazy. Every pack is a chunk this page does not fetch until
* somebody opens that door, which is the arithmetic `loadOffice` explains — and
* a second pack eagerly imported would put its furniture in the entry bundle
* for every visitor who never opens it.
*/
const OFFICES: { id: string; label: string; load: () => Promise<{ default: Office }> }[] = [
{ id: "lumbridge-hq", label: "Lumbridge HQ", load: () => import("./offices/lumbridge-hq.ts") },
{ id: "frontier-valley", label: "Frontier Valley", load: () => import("./offices/frontier-valley.ts") },
{ id: "mateo-court", label: "Mateo Court", load: () => import("./offices/mateo-court.ts") },
];
const canvas = document.querySelector<HTMLCanvasElement>("#scene");
if (!canvas) throw new Error("#scene canvas missing");
/**
* One renderer, one loop, for as long as this page is open.
*
* Built here rather than inside `createScene` because a `WebGLRenderer` is a
* property of the *canvas* and not of the city drawn on it. When each city
* built its own, every switch between the Bay Area and SoCal abandoned a
* renderer on the one GL context this page has, and abandoned renderers do not
* give their textures back: `WebGLRenderer.dispose()` frees no texture at all,
* so ten switches measured 88 live GPU textures against zero `deleteTexture`
* calls, 16.8 MB of orphaned shadow map at a time. `stage.ts` has the numbers
* and the reading of three's source that they come from.
*
* Nothing disposes this. It outlives every city and every office on the page,
* and the page unload takes it — the same arrangement, and the same reasoning,
* as `officeMaterials` below.
*/
const stage = createStage(canvas);
// `authFetch` so the private-office pack (`/api/v1/offices/:id`, which answers
// 404 rather than 403 to anyone who may not see it) is requested as the signed-in
// viewer. On a `password`-mode or open deployment it is an ordinary fetch.
const tera = createTeraClient({ fetch: authFetch });
let city: SceneHandle | null = null;
let cityId = "california";
/**
* The city the user last *asked* for, which is not the same as the one that is
* mounted or even the one that is being built.
*
* `building()` defers its work by two animation frames so the boot card can
* paint, and a frame under load is not 16 ms — measured at 250 ms on a software
* rasteriser. Clicking SoCal and then changing your mind inside that window
* used to hit `if (id === cityId) return` against a `cityId` the deferred
* `mountCity` had not written yet, so the second click was discarded as
* redundant and you arrived at the city you had just cancelled. The guard has
* to be against the intention, and the intention is recorded synchronously in
* the click handler.
*/
let wantedCity = "california";
let office: OfficeScene | null = null;
/** The pack used by `office`; kept separate from the currently selected door. */
let builtOfficeId: string | null = null;
let inside = false;
let markers: Marker[] = SAMPLE_MARKERS;
let realtimeClient: RealtimeClient | null = null;
let presenceIndicator: PresenceIndicator | null = null;
let stopRealtimeSubscription: (() => void) | null = null;
let realtimeOperation = 0;
let lastRealtimePublishAt = 0;
let realtimePageActive = true;
/** Page-scoped wire identities; auth subject never enters the spatial protocol. */
const realtimeActorId = crypto.randomUUID();
const realtimeVehicleId = crypto.randomUUID();
const realtimeAircraftId = crypto.randomUUID();
/**
* The buildings you can walk into, as procedural glyphs 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 — the palette
* resolves it, and giving these their own key is what lets a door look different
* from a company. `glyph` is equally literal: dimensions and a silhouette, with
* no office semantics in the renderer.
*/
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,
...(entry.site.exterior ? { glyph: entry.site.exterior } : {}),
}));
/**
* 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
* `access.can.*` and nothing else.
*
* The pre-boot value is the **closed** one, deliberately. A handler that
* somehow fires before `resolveAccess()` has settled — a keystroke on a slow
* connection, a click on a control that is in the document from first paint —
* should offer a visitor less than they are entitled to and never more. The
* rule that produces this value, and the SSO bug that once produced it wrongly,
* are written out in `src/access.ts`.
*/
let access: Access = {
tier: "anon",
subject: null,
signInUrl: null,
can: capabilitiesFor("anon"),
feeds: null,
};
let atmosphere: ReturnType<typeof createAtmosphere> | null = null;
let minimap: Minimap | null = null;
/**
* The sky over the city currently on screen, polled while it is on screen.
*
* One watch at a time and it belongs to the board, not to the page. Stopping it
* at the top of `mountCity` is the whole of the cancel-on-switch rule: a
* `/weather` request for San Francisco that lands after the user has moved to
* SoCal would otherwise put the marine layer over Long Beach, and it is a
* request in flight for most of the second in which somebody clicks.
*/
let weatherWatch: WeatherWatch | null = null;
/**
* The live traffic source, when there is one, kept so the corner label can ask
* it whether the aircraft on screen were observed. `null` means the simulator,
* which is never live and does not need asking.
*/
let cityFlights: TrafficSource | null = null;
/**
* The satellite element sets, fetched once for the page rather than once per city.
*
* Every other feed here is per-city and is torn down on a switch. These are not,
* and the asymmetry is the physics: an aircraft at 10,000 m is visible for tens
* of kilometres and the two boards are six hundred apart, but a satellite at
* 550 km is above the horizon for a circle two thousand kilometres across. The
* same element sets serve both cities and would serve a continent.
*
* **The elements are shared and the catalogue is not.** A `SatelliteCatalogue`
* is built around an observer, and the observer is the city centre: reusing one
* across a city switch would compute the Southland's sky from San Francisco and
* put every look angle several degrees out, with nothing on screen to say so.
* So the expensive, universal half is cached here and the cheap, local half is
* rebuilt per board.
*
* `null` until the first fetch is started, and on the overwhelming majority of
* deployments forever: `TERA_SATELLITES_SOURCE` is off by default. A promise
* rather than a value so that a second city mounted while the first fetch is
* still in the air waits for it instead of starting another.
*/
let satelliteElements: Promise<SatelliteElements[]> | null = null;
/**
* The fabricated-traffic dial for the board on screen, rebuilt with every city.
*
* Held here rather than inside the godmode closure because the panel is mounted
* once and the board is not: a dial captured when the panel opened would keep
* pushing aircraft at a scene that had been disposed two city switches ago.
*/
let trafficDial: TrafficDial | null = null;
/**
* Whether the satellite layer is drawn, remembered across city switches.
*
* A new board builds a new layer, which starts visible, so a god who turned the
* sky off and then changed city would have it come back on — a setting that
* quietly undoes itself is worse than one that is not there.
*/
let satellitesVisible = true;
/**
* The build in progress. Aborting it is what makes a second click on the other
* city cheap: `createScene` drops the heightfield, resolves `null`, and has
* allocated no WebGL context for the abandoned board.
*/
let mounting: AbortController | null = null;
let godmode: Godmode | null = null;
let poseEditor: PoseEditor | null = null;
/**
* The office, once somebody has asked for it. Both are `null` until the first
* `loadOffice()` because both live in a chunk this page does not fetch until
* then — see `loadOffice` for the arithmetic, and the import block above for
* what keeps them out of the entry chunk.
*
* The registry is one texture set for every office this page ever builds.
* Signing in while standing in the public office is a `dispose()` and a second
* `createOfficeScene` at `"full"` — cheap only if both are handed the same
* registry, because drawing the textures is the expensive part and a registry
* draws them once. Owned here and disposed nowhere: it outlives every scene
* that borrows it, and the page teardown takes the process with it.
*/
let officePack: Office | null = null;
let officeMaterials: MaterialRegistry | null = null;
/**
* Which building the door leads to. Changed by the picker while you are inside.
*
* `officePack` is the pack for *this* id and is rebuilt on a switch rather than
* memoised forever, which is the one-line difference from the arrangement that
* could only ever hold one office.
*/
let officeId = OFFICES[0]?.id ?? "lumbridge-hq";
/**
* The office's own sky, when its pack says where it stands.
*
* A second `Atmosphere` rather than the city's, because the two are at different
* scales and in different places: the city's is built with `metresPerUnit` near
* 94 and a fog measured in board spans, and an office runs at 1 m per unit
* fifty metres across. `null` for a pack with no `site`, which keeps the fixed
* interior rig and is a supported state rather than a gap.
*/
let officeAtmosphere: Atmosphere | null = null;
let officeScreenPanel: OfficeScreenPanel | null = null;
let sharedScreen: {
screenId: string;
stream: MediaStream;
video: HTMLVideoElement;
texture: VideoTexture;
remote: RemoteOfficeMedia | null;
} | null = null;
let remoteViewedScreen: {
screenId: string;
officeId: string;
video: HTMLVideoElement;
texture: VideoTexture;
remote: RemoteOfficeMedia;
bound: boolean;
} | null = null;
let remoteMediaOperation = 0;
let remoteViewerRequestScreenId: string | null = null;
/**
* The plan panel's other occupant.
*
* There are two of these because there are two places, and the panel shows the
* one you are standing in. Holding the city plan up while the scene in front of
* it is a floor plate was the bug this pair exists to fix: the single piece of
* chrome whose whole job is "where am I" was answering about another county.
*
* It is built with the office and dies with it, so the office's `Plan`, camera
* and controls — all three of which it holds by reference — cannot outlive the
* scene they came from. Which of the two is *mounted* is a separate question
* from which exist, and `showPlan()` is the only thing that answers it.
*/
let officePlan: OfficeMinimap | null = null;
/**
* Whether the people currently on the floor came from the deployment or from
* `sample.ts`.
*
* Kept because the difference has to be *said*. Every other gap on this page is
* a silence — an empty office, a missing scrubber — and a silence you cannot
* attribute is indistinguishable from a fault; here the failure is the opposite
* and worse, because fabricated people are not silent. Twenty-five invented
* names at real desks is a screenshot somebody will take, and it must not be
* 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.
*
* It belongs to the visit and not to the page, which is the same rule
* `weatherWatch` follows one line up and for a sharper reason: a roster is
* requested with a credential and describes people, so a watch left running
* after somebody stepped back out to the city is a page quietly asking about a
* room nobody is looking at. Started by `enterOffice`, stopped by `leaveOffice`
* and by `mountCity`, and there is never more than one.
*/
let presenceWatch: PresenceWatch | null = null;
/**
* `office.lumbridgecorp.com` and `tera.lumbridgecorp.com` are one bundle behind
* two names, and which name you arrived at is the whole difference: one is a
* map with an office in it, the other is an office with a map behind it. The
* city is still built underneath either way — that is what makes "← Back to the
* city" work from the office front door — so this is a policy about where boot
* *stops*, not about what boot builds.
*/
const OFFICE_HOST = location.hostname.split(".")[0] === "office";
const OPENS_IN_OFFICE =
OFFICE_HOST || new URLSearchParams(location.search).get("view") === "office";
/**
* Where the city is, when this page is the office front door.
*
* `office.lumbridgecorp.com` and `tera.lumbridgecorp.com` are one bundle behind
* two names, and until now leaving the office from the office door swapped the
* scene and left the address bar saying `office.` — so the site's own URL
* disagreed with the site. That is not a cosmetic complaint: it is the thing you
* copy, bookmark and send to someone, and it took them somewhere other than what
* you were looking at.
*
* So the city door is this hostname with its first label swapped, and the guess
* is exactly as safe as the one directly above it: it is only ever consulted
* when that label is literally `office`, which is the same string comparison
* that made this the office door in the first place. Same registrable domain,
* same origin policy, same static root, same API — a deployment that serves one
* of these names and not the other has half-configured itself, and the honest
* failure for that is a 404 on a name it chose not to serve rather than a silent
* lie in the address bar.
*
* Anything else — a bare domain, `spaces.example.com`, `?view=office` on the
* city door — derives nothing and keeps the in-page swap, which is what the
* whole scene-swap design was for.
*/
function cityDoorUrl(cityWanted?: string): string | null {
if (!OFFICE_HOST) return null;
const url = new URL(location.href);
const labels = url.hostname.split(".");
labels[0] = "tera";
url.hostname = labels.join(".");
url.search = "";
url.hash = "";
// Carried so that stepping out of the office into SoCal lands in SoCal. The
// page load is the cost of telling the truth about where you are; arriving in
// the wrong metro on top of it would not be.
if (cityWanted !== undefined && cityWanted !== CITIES[0]?.id) {
url.searchParams.set("city", cityWanted);
}
return url.href;
}
// ---- Time -----------------------------------------------------------------
/**
* `null` follows the wall clock. The override exists because the honest answer
* at 2 a.m. is a very dark city — correct, and not what you want to be looking
* at while judging whether the sun is in the right place.
*
* A whole `Date` and not an hour, which is the change the godmode panel forced
* and the right shape anyway. The old scrubber wrote an hour onto *today*, so
* there was no way to ask for the December solstice, and any control that could
* set a date would have had it silently discarded on the next scrub. One
* 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.
*
* Kept next to the instant because it is the same kind of thing — a god-only
* lie about the inputs, told to see what the renderer does with it — and it
* takes precedence over the live observation for exactly as long as it is set.
*/
let weatherOverride: WeatherObservation | null = null;
function currentInstant(): Date {
return instantOverride ?? new Date();
}
/**
* What the sky is doing, in the order the answers are trusted.
*
* The override wins because somebody typed it. Otherwise the live observation,
* and `null` — nobody was asked — when there is no watch or it has not landed
* yet. `null` is not "clear": `atmosphere.ts` treats a *reported* clear sky as
* authority that suppresses the modelled marine layer, so handing it an
* invented clear day on every failed poll would permanently kill San
* Francisco's fog on the zero-config box where the local model is all there is.
* See `WeatherFeed` in `adapters/http.ts`, which is careful about the same
* distinction from the other side.
*/
function currentWeather(): WeatherObservation | null {
return weatherOverride ?? weatherWatch?.current().value ?? null;
}
/**
* The office's rig for the instant being rendered, in the building's own frame.
*
* Same clock, same weather and same `Atmosphere` machinery the city runs on —
* which is the entire point. An office that dimmed on a schedule of its own
* would be a second sun, and CONTRACT.md §4 exists to say there is one.
*
* `officeDaylight` does the two things a room needs and a map does not: it turns
* the sun into the building's frame using `site.heading`, so the light comes
* through the windows the pack actually has, and it moves the fog outdoors.
*/
function officeLighting(site: NonNullable<Office["site"]>) {
const env = observe(site.lat, site.lng, currentInstant(), currentWeather());
// `officeAtmosphere` is built alongside the pack; falling back to the fixed
// rig here would be a flicker rather than a fix, so this is only ever called
// where one exists.
const state = officeAtmosphere?.apply(env);
if (!state) return undefined;
/**
* The building's own lights, on top of whatever is left of the sun.
*
* The order matters and is the only subtle thing here: the daylight
* adaptation runs first, because it is about the *sun* — which way the
* building faces and where the weather starts — and the house lights are
* added to the result, because they are about the building. Doing it the
* other way round would rotate the interior lighting by the building's
* heading, which is meaningless: a ceiling does not face a compass point.
*
* `office` may not exist yet — this is called once at construction, before
* there is a scene to ask — in which case the elevation is fed straight to the
* ramp so the first frame is already correct rather than a lit room fading
* down or a dark one fading up.
*/
office?.setSolarElevation(houseElevation(env.sun.elevation));
const house = office?.houseLevel() ?? houseLevelFor(houseElevation(env.sun.elevation));
return withHouseLights(officeDaylight(state, site), house);
}
/**
* The lights-on ramp, for the one moment there is no office to ask.
*
* Duplicating the two bounds from `luminaires.ts` is a smell and is the lesser
* of the two available ones: the alternative is building the office scene with
* a rig computed from a light level it cannot report yet, which means the room
* is visibly wrong for exactly one frame at every entry. Kept in step by being
* four lines long and named after the thing it mirrors.
*/
function houseLevelFor(solarElevationDeg: number): number {
return 1 - Math.min(1, Math.max(0, solarElevationDeg / 6));
}
function updateSun() {
const active = CITIES.find((c) => c.id === cityId)?.city ?? SAN_FRANCISCO;
// The office follows the same clock, and follows it whether or not you are
// standing in it — walking back in to a room lit for an hour ago is the
// failure this avoids.
const site = officePack?.site;
if (office && site && officeAtmosphere) {
const state = officeLighting(site);
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));
city.setSolarElevation(env.sun.elevation);
/**
* 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.
*
* `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(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
// second — so an unscrubbed sky would advance in visible steps while the
// aircraft beside it moved smoothly. `null` means "read the clock yourself".
city.setSkyInstant(instantOverride);
// The plan view follows the same day the map does. It computes its own
// palette from this one number rather than reading the rig, because a rig is
// a set of three.js lights and the minimap has none.
minimap?.setSolarElevation(env.sun.elevation);
const clock = document.querySelector<HTMLElement>("#clock");
if (!clock) return;
const el = env.sun.elevation;
const time = env.time.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
const moon = env.moon ? ` · moon ${Math.round(env.moon.illuminated * 100)}%` : "";
clock.textContent = `${time} · sun ${el >= 0 ? "+" : ""}${el.toFixed(1)}° · ${daylightPhase(el)}${moon}`;
}
// ---- Cities ---------------------------------------------------------------
/**
* Switching city tears the old one down completely.
*
* Unlike the city↔office move — where the city is paused and kept, because you
* are coming straight back — nobody flips between metros often enough to
* justify holding two heightfields and 140k building instances at once.
*/
async function mountCity(id: string) {
const entry = CITIES.find((c) => c.id === id);
if (!entry || !canvas) return;
/**
* Abandon whatever is still building before touching anything else.
*
* Two clicks on the city buttons a second apart used to run two heightfields
* to completion and race to assign `city`; now the first `createScene` sees
* its signal go and resolves `null` without ever allocating a renderer. The
* controller is replaced rather than reused because an aborted signal stays
* aborted, and the new build must not be born cancelled.
*/
mounting?.abort();
const mount = new AbortController();
mounting = mount;
wantedCity = id;
weatherWatch?.stop();
weatherWatch = null;
poseEditor?.destroy();
poseEditor = null;
disposeLoadedOffice();
inside = false;
minimap?.dispose();
minimap = null;
city?.dispose();
// Not merely tidy. `city` is read by the frame pump, by `updateSun` and by
// every render function, and the gap between the dispose above and the
// assignment below is now an `await` wide rather than a statement — long
// enough for all three to run against a torn-down scene.
city = null;
cityId = id;
/**
* The sky and the traffic are per-city and are chosen here, before the build,
* because `flights` is fixed at scene construction.
*
* Still two gates, but only one of them is about the visitor now.
*
* `can.liveEnvironment` is true for everybody — the reasoning is in
* `access.ts`, and it comes down to the sky not being a thing an account can
* grant you. What remains load-bearing is `feeds`, the *deployment*: it is
* what stops the ordinary box, where every source is `none`, from polling two
* endpoints forever for a 404 on every tab that is open.
*
* The other half of the old comment is still worth keeping, because it was a
* real bug: the flights used to be gated on the *markers* flag, which is a
* different feed entirely, so a deployment with a real ADS-B receiver and no
* marker file flew the simulator.
*/
const region = regionOf(entry.city);
// The hand-authored corridors for *this* city. `SAMPLE_ROUTES` was passed
// unconditionally and all of it is over San Francisco, so the SoCal board's
// entire sky projected ~590 km off the world and rendered as nothing at all.
const routes = sampleRoutesFor(entry.city);
// The public traffic API is region-oriented and intentionally capped around
// one metro. A state-wide request would either be rejected or become a data
// vacuum, so California keeps the honest deterministic sky while its two
// detailed boards continue to use live ADS-B when available.
const traffic =
id !== "california" && access.can.liveEnvironment && access.feeds?.flights
? tera.flights(region, routes)
: null;
cityFlights = traffic;
/**
* Started here and awaited below, so the fetch overlaps the heightfield build
* rather than following it. Gated on the deployment for the same reason the
* traffic is: a box with no satellite source answers with an empty catalogue,
* and asking it once per page load for that is a request nobody needs.
*
* Not gated on the visitor. There is no `can.` check because there is nothing
* to grant — the objects in this catalogue broadcast their positions to
* anybody with a radio, and every element set in it is a US Government work.
*/
if (satelliteElements === null && access.feeds?.satellites) {
satelliteElements = tera.satellites();
}
const elements = (await satelliteElements) ?? [];
// Rebuilt per board: the observer is this city's centre. See the note on
// `satelliteElements` for why only the elements are shared.
const catalogue =
elements.length === 0 ? undefined : new SatelliteCatalogue(elements, entry.city.center);
// The build may have been abandoned while that was in the air.
if (mount.signal.aborted) {
traffic?.dispose();
if (cityFlights === traffic) cityFlights = null;
return;
}
// Wrapped, not replaced: the dial passes the real sky through untouched and
// concatenates fabricated aircraft after it, so it composes with a live ADS-B
// feed as readily as with the simulator. `cityFlights` stays the unwrapped
// source — the corner label asks it whether what is on screen was observed,
// and the answer is about the feed rather than about the dial.
const dial = withTrafficDial(traffic ?? new SimulatedFlights(routes), region);
// Carried across the switch, so a dial somebody set on the last board is still
// set on this one.
dial.setExtra(trafficDial?.extra() ?? 0);
trafficDial = dial;
/**
* Stable destinations are handed to the scene at construction time, not a
* frame later. A building glyph needs that head start so `blocks.ts` can
* reserve its footprint before the anonymous one-draw-call skyline is
* emitted; otherwise both buildings occupy the same address and the useful
* one is usually hidden inside the random one.
*
* A door belongs to the board it stands on. This used to be gated on a
* hard-coded `id === "sf"`; using the board's own bounds is what keeps Mateo
* Court on the Southland board and off the Bay Area one. Sample companies
* stay SF-only because that sample feed is about one city and always was.
*/
const bounds = entry.city.bounds;
const doors = OFFICE_MARKERS.filter(
(m) =>
m.lat >= bounds.minLat &&
m.lat <= bounds.maxLat &&
m.lng >= bounds.minLng &&
m.lng <= bounds.maxLng,
);
const initialMarkers = id === "sf" ? [...markers, ...doors] : doors;
const handle = await createScene(stage, {
city: entry.city,
actor: {
kind: actorKindForPresence(access.subject !== null, "outdoors"),
identity: access.subject === null
? {
id: "anonymous",
displayName: "Guest",
authenticated: false,
profile: { appearance: { primaryColor: "#11151a", accentColor: "#f2b134" } },
}
: signedInActorIdentity(),
mode: access.subject === null ? "flight" : "ground",
position: access.subject === null ? { y: 1_200 } : { y: 0 },
minFlightAltitude: 20,
maxFlightAltitude: 1_500,
...(access.subject === null
? { camera: { distance: 2.55, height: 1.2, targetHeight: 0.1, lookAhead: 1.3 } }
: {}),
},
...(id === "california"
? { actorAnchor: { lat: 35.5, lng: -119.5 } }
: {}),
markerPalette: palette,
markers: initialMarkers,
...(id === "california"
? {
roadTraffic: {
pack: CALIFORNIA_TRANSPORT,
routeId: "la-sf-us-101",
count: 14,
seed: 115,
},
aircraft: {
route: CALIFORNIA_AIR_ROUTE,
initialPosition: CALIFORNIA_AIR_ROUTE[0],
initialAltitudeM: CALIFORNIA_AIR_ROUTE[0]?.altitudeM ?? 1_350,
initialHeadingDeg: 320,
assistedAltitudeM: 1_500,
// Corridor altitude is an atlas glyph just like the black cars:
// literal metres would put the chase camera inside the coarse hills.
altitudeSceneUnitsPerMetre: 0.01,
visualSceneUnitsPerMetre: 0.22,
camera: { distance: 52, height: 18, lookAhead: 18 },
},
}
: {}),
flights: dial.source,
...(catalogue ? { satellites: catalogue } : {}),
/**
* 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.
onProgress: (p) => {
if (!mount.signal.aborted) bootProgress(entry.label, p.fraction, p.onMainThread);
},
...(access.subject !== null
? { realtimePeers: { localSceneUnitsPerMetre: entry.city.latScale / 111_320 } }
: {}),
});
if (!handle) {
/**
* Superseded. `scene.dispose()` is what normally cancels the traffic
* source, and there is no scene — so the polling this call started would
* otherwise outlive the board it was started for, and keep a request in
* the air for a city nobody is looking at.
*/
traffic?.dispose();
if (cityFlights === traffic) cityFlights = null;
return;
}
city = handle;
attachCurrentWebcamFace();
moveRealtimePresence();
// A new board builds a new layer, and a new layer starts visible. Reapply
// whatever the panel last said, or the setting silently undoes itself on the
// first city switch.
handle.setSatellitesVisible(satellitesVisible);
/**
* The weather, started only now that the board exists.
*
* Deliberately after the build rather than beside it: a poll issued at the
* top of a two-second heightfield is a request for a city the user may
* already have left, and the first thing `stop()` would do is throw the
* answer away. Nothing on screen is waiting for it — `updateSun` runs
* immediately with `null`, which is climatology, and the observation
* replaces it when it lands.
*/
// Weather observations are metro-scoped for the same reason live aircraft
// are: one honest station cannot describe a 600 km corridor. California uses
// the local climatology model; the detailed SF and SoCal boards keep their
// live observations.
weatherWatch =
id !== "california" && access.can.liveEnvironment && access.feeds?.weather
? tera.watchWeather(entry.city.center, () => {
updateSun();
renderSource();
})
: null;
// Fog distances are scene units, so they have to follow the board — 210/460
// was tuned for a 230-unit San Francisco and fogs out most of a 1000-unit
// Bay Area. They also have to clear the CAMERA, which sits about 0.6 spans
// out on the whole-board view: a fog starting nearer than that is behind the
// viewer's own shoulder, and every pixel in frame is then at full fog.
//
// That last failure used to be catastrophic and is now only bad, and the
// difference is worth recording because the comment used to claim the worse
// version. The night fog colour is derived from the sky, the night sky was
// nearly black, and so a fog plane behind the camera turned the entire map
// off. `atmosphere.ts` now floors the night ground rig and stops the
// obscuration convergence subtracting it again, and the night fog here lands
// around #16203a — aerial perspective that lifts distance rather than a
// blackout. The clearance is still required: a board flattened to one uniform
// value is unreadable at any brightness. It is no longer the difference
// between a map and a black rectangle.
const [wx, nz] = city.world.project(entry.city.bounds.maxLat, entry.city.bounds.minLng);
const [ex, sz] = city.world.project(entry.city.bounds.minLat, entry.city.bounds.maxLng);
const span = Math.max(Math.abs(ex - wx), Math.abs(sz - nz));
atmosphere = createAtmosphere({
lng: entry.city.center.lng,
metresPerUnit: city.world.metresPerUnit,
// Pushed out with the camera. `scene.ts` now lets the orbit reach 2.0 spans
// so the viewer can get above the satellite dome, and at the old far of 2.8
// the board sat at 51% fog from that pose — the whole city washing out at
// exactly the moment the shot is meant to be the city under the
// constellation. 3.9 keeps it near a fifth, which is the haze it had at the
// old limit.
clearFog: { near: span * 1.15, far: span * 3.9 },
// The floor on how far you can see, and it has to know how big the board
// is. `minVisibilityM` defaults to 4.5 km, which is honest weather and
// completely wrong here: this board is ninety-four kilometres across, so
// real visibility correctly hides three quarters of it and the night view
// renders as a black rectangle. A map is looked at from outside the
// atmosphere it is depicting.
minVisibilityM: span * city.world.metresPerUnit * 1.6,
// The marine layer is a fact about the eastern Pacific at this latitude,
// not a decoration. LA gets its own weather, not San Francisco's fog.
marineLayer: id === "sf" ? PACIFIC_MARINE_LAYER : null,
});
city.onChapterChange(() => renderLegend());
/**
* The plan view, built last, because it reads the finished `World` — the
* heightfield the terrain has already paid for — and the live camera and
* controls the scene has just made. It is torn down and rebuilt with the
* city for the same reason the city is: nothing in it survives a change of
* board, and it holds a `World` that would otherwise leak.
*/
minimap = createMinimap({
world: city.world,
city: entry.city,
camera: city.stageScene.camera,
controls: city.stageScene.controls,
markerPalette: palette,
// The plan view is a 2D canvas the same size as a phone's thumb, and on a
// handheld it is drawn at the same ceiling the WebGL renderer uses. One
// definition of "phone", in `stage.ts`, read by both.
maxPixelRatio: deviceProfile().maxPixelRatio,
onSeek(lat, lng) {
if (!city) return;
/**
* Slide the orbit target and carry the camera with it, keeping the offset
* between them. A seek is "look over there", not "go to chapter three":
* snapping to a chapter pose throws away the angle and the distance the
* user spent the last minute choosing, and doing it from a click on a map
* is the kind of surprise that stops people clicking on the map.
*
* No easing, deliberately. `flyTo` would need a pose, which is the thing
* being avoided, and an instant move is also the correct answer under
* `prefers-reduced-motion`.
*/
const { camera, controls } = city.stageScene;
const [x, z] = city.world.project(lat, lng);
const y = city.world.groundAt(lat, lng);
const dx = camera.position.x - controls.target.x;
const dy = camera.position.y - controls.target.y;
const dz = camera.position.z - controls.target.z;
controls.target.set(x, y, z);
camera.position.set(x + dx, y + dy, z + dz);
},
onHover(info) {
if (!minimapReadout) return;
minimapReadout.textContent = info
? `${info.lat.toFixed(4)}, ${info.lng.toFixed(4)}${info.district ? ` · ${info.district}` : ""}`
: "";
},
});
showPlan();
minimap.setMarkers(initialMarkers);
// 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
// and dies with it — the same reason the plan view does.
mountPoseEditor(city);
refreshGodmodePlace();
updateSun();
renderLegend();
}
/**
* The minimap's own frame pump.
*
* `Stage` owns the render loop and `SceneHandle` exposes no per-frame hook, so
* the alternative is adding an `onTick` to the scene handle for exactly one
* call site. This is the smaller change and it costs nothing measurable: an
* idle `tick()` is a timestamp comparison and a dirty flag, 0.0002 ms, and the
* loop keeps running unchanged across a city swap, across the office swap, and
* during the window where there is no minimap at all.
*/
requestAnimationFrame(function pumpMinimap() {
requestAnimationFrame(pumpMinimap);
syncJourneyVehicle(performance.now());
// Only the mounted one. The other is still constructed and still holds a live
// camera, but its canvas is out of the document, so `clientWidth` is 0, every
// `resize()` puts it back to `ready = false`, and ticking it would be a
// function call that exists to do nothing sixty times a second.
if (inside) officePlan?.tick();
else minimap?.tick();
// The pose editor is on the same pump for the same reasons, and is `null` for
// everyone who is not god, so this is one property read per frame on a
// public page.
poseEditor?.tick();
publishRealtimePresence(performance.now());
pollLiveness();
});
/**
* Whether the corner label is still telling the truth, once a second.
*
* The two live feeds settle on their own schedule and neither has an event to
* subscribe to: `TrafficSource.live()` flips when a `/flights` body lands
* inside the region, which is somewhere in the first fifteen seconds, and the
* weather watch fires its own callback but only when a *poll* settles. A
* one-second sample is late by nothing anybody can perceive and costs a
* subtraction on the frames it skips.
*/
let livenessCheckedAt = 0;
let journeySyncedAt = 0;
let previousVehicleProgress: number | null = null;
function pollLiveness() {
const now = performance.now();
if (now - livenessCheckedAt < 1000) return;
livenessCheckedAt = now;
renderSource();
}
/** Persist route progress cheaply and turn the hero's wrap into a real scale door. */
function syncJourneyVehicle(now: number): void {
if (!routeDriveIsActive() || !city || journey.route === null || journey.vehicle === null) {
previousVehicleProgress = null;
return;
}
const vehicle = city.vehicleState();
if (!vehicle) return;
const progress = vehicle.progress;
const arrived = journey.route.direction === 1
? previousVehicleProgress !== null && previousVehicleProgress > 0.94 && progress < 0.06
: previousVehicleProgress !== null && previousVehicleProgress < 0.06 && progress > 0.94;
previousVehicleProgress = progress;
if (arrived) {
const endpoint = journey.route.direction === 1 ? "san-francisco" : "los-angeles";
dispatchJourney({ type: "reach-route-endpoint", endpoint });
dispatchJourney({ type: "exit-vehicle" });
switchCity(endpoint === "san-francisco" ? "sf" : "socal");
return;
}
if (now - journeySyncedAt < 500) return;
journeySyncedAt = now;
dispatchJourney({ type: "update-route-progress", progress });
}
// ---- Office ---------------------------------------------------------------
/**
* Everyone gets in. The tier picks which building they get, not whether the
* door opens.
*
* The office used to be members-only, and the anonymous view of this site was a
* map with a greyed-out button on it — the single most interesting thing the
* project does, visible only as something you cannot have. At `"public"` depth
* the same shell, the same furniture and the same named viewpoints are built,
* and the only thing missing is the people. That is withheld because the API
* refuses occupancy to an anonymous caller, not because this function declined
* to draw it.
*/
async function enterOffice() {
if (!city) return;
// A city can have more than one authored door. Leaving one office parks its
// scene for a cheap re-entry, but clicking a different door must not reopen
// that cached building under the new office id. Tear down every resource
// owned by the old room before loading the requested one; the parked city
// actor, journey identity and local profile deliberately live elsewhere.
if (office && builtOfficeId !== officeId) disposeLoadedOffice();
if (!office) {
const built = await loadOffice();
// The chunk arrived after the user had already left for the other city, or
// it did not arrive at all. Either way there is no room to walk into and
// `loadOffice` has already said so on the button.
if (!built || !city) return;
const { createOfficeScene, createOfficeMinimap, pack, materials } = built;
const depth = access.can.officeDepth;
// Keep the remote-avatar renderer behind the authenticated boundary. The
// public Office door can build its full local scene without downloading it.
const createOfficePeers = access.subject !== null
? (await import("./realtime/scenePeers.ts")).createScenePeers
: null;
/**
* The building's own sky, built before the scene because the scene wants the
* opening rig and the horizon drop at construction.
*
* `metresPerUnit: 1` — an office is authored in metres, and this is the only
* thing `Atmosphere` needs to know about scale. The fog it computes from
* that is then thrown away and replaced by `officeDaylight`, because a fog
* sized for a fifty-metre room is a fog inside the room.
*/
officeAtmosphere = pack.site
? createAtmosphere({
lng: pack.site.lng,
metresPerUnit: 1,
/**
* No marine layer, deliberately — unlike the city, which gets one.
*
* The model is a fact about sea level on this coast, and both of these
* buildings look *out over* it: one from 188 m up a tower, one across
* an estuary. Handing it to the office put the observer inside the fog
* it describes, which greyed the sky to near-white at one in the
* afternoon and took the view with it. The city keeps its fog; the
* rooms that look at the city do not stand in it.
*/
marineLayer: null,
})
: null;
office = createOfficeScene(pack, {
dom: city.stage.renderer.domElement,
// A visitor owns one local actor. Anonymous visitors are the promised
// office dog; a signed-in visitor gets the procedural humanoid. The
// arrival viewpoint is already a pack-authored clear point on a floor,
// which makes it the honest spawn and keeps coordinates out of the app.
walker: {
levelId: pack.viewpoints[0]?.levelId ?? pack.levels[0]?.id ?? "level-1",
position: pack.viewpoints[0]?.focus.at ?? { x: 0, z: 0 },
facing: pack.viewpoints[0]
? {
x: -Math.sin(pack.viewpoints[0].focus.rotation),
z: -Math.cos(pack.viewpoints[0].focus.rotation),
}
: { x: 0, z: -1 },
actor: actorKindForPresence(access.subject !== null, "office") === "dog"
? { kind: "anonymous-dog", coatColor: 0x17191c, collarColor: 0xf2b134 }
: { kind: "humanoid", ...(humanoidAppearance() ?? { outfitColor: 0x151a20, accentColor: 0xf2b134 }) },
},
// Only when there is no sky to put behind it. A sited office computes a
// gradient and a horizon; painting the old flat colour over that is the
// bug that looks exactly like the sky not working.
...(pack.site ? {} : { background: 0x11161c }),
...(pack.site ? { lighting: officeLighting(pack.site) } : {}),
...(pack.site ? { horizon: { drop: pack.site.elevation } } : {}),
// Two per floor, whatever floors this pack has — so the two-storey tower
// gets four and the single-storey hangar gets two, without either pack
// having to know about robots. Derived from the levels rather than
// written down, because a pack that gains a storey should not need an
// edit here to be staffed.
robots: pack.levels.flatMap((level) => [
{ levelId: level.id, id: `${level.id}-a` },
{ levelId: level.id, id: `${level.id}-b` },
]),
depth,
materials,
// Ignored entirely at `"public"` depth, where no layer is built to colour.
presencePalette: SAMPLE_PRESENCE_PALETTE,
// Two different questions, so two different callbacks. `onPresencePick`
// answers "who is at this desk"; `onPlacePick` answers only "this is a
// desk, and it is the fourteenth one" — which is all a stranger is told.
...(depth === "full"
? { onPresencePick: (p) => showDetail(p ? p.label : null) }
: { onPlacePick: (place) => showDetail(place ? place.label : null) }),
...(createOfficePeers ? { realtimePeers: { create: createOfficePeers } } : {}),
});
builtOfficeId = officeId;
office.onViewChange(() => renderLegend());
officePlan = buildOfficePlan(createOfficeMinimap, office);
}
city.stage.setScene(office);
inside = true;
attachCurrentWebcamFace();
moveRealtimePresence();
const desiredCity: JourneyCity = officeId === "mateo-court" ? "socal" : "bay-area";
journeyToCity(desiredCity);
dispatchJourney({ type: "enter-office", officeId: officeId as "lumbridge-hq" | "frontier-valley" | "mateo-court" });
showPlan();
showDetail(null);
refreshGodmodePlace();
renderLegend();
// After the room is on screen, not before. The first ask is a second request
// and the building is worth looking at while it is in flight; awaiting it here
// would hold the door shut on a network round trip to draw people into a scene
// the user cannot see yet.
watchOccupancy();
}
/**
* The plan panel, for the office.
*
* Built once alongside the scene rather than on every entry, because it reads
* that scene's camera and controls by reference — a second one built against a
* disposed office would draw a footprint from a camera nobody is moving.
*
* The constructor is passed in rather than imported, for the reason written at
* the import block: naming the module here as a value would put the furniture
* catalogue back in the entry chunk.
*/
function buildOfficePlan(
create: typeof import("./engine/officeMinimap.ts").createOfficeMinimap,
scene: OfficeScene,
): OfficeMinimap {
const panel = create({
plan: scene.plan,
camera: scene.camera,
controls: scene.controls,
// The same ceiling the city plan takes, from the same place. Two 2D canvases
// in one corner of one page disagreeing about what a phone can afford is the
// exact drift `deviceProfile` exists in one file to prevent.
maxPixelRatio: deviceProfile().maxPixelRatio,
onSeek(x, z) {
/**
* The same move the city plan makes, and for the same reason: slide the
* orbit target and carry the camera with it, keeping the offset between
* them. A seek is "look over there", not "go to viewpoint three".
*
* The height is deliberately untouched. Indoors that matters more than it
* does outdoors — the difference between a viewpoint two metres off the
* floor and one fourteen metres up is the difference between standing in
* the room and looking down into it, and clicking the plan should not
* silently move you between those two.
*/
const { camera, controls } = scene;
const dx = camera.position.x - controls.target.x;
const dy = camera.position.y - controls.target.y;
const dz = camera.position.z - controls.target.z;
controls.target.set(x, controls.target.y, z);
camera.position.set(x + dx, controls.target.y + dy, z + dz);
},
onHover(info) {
if (!minimapReadout) return;
if (!info) {
minimapReadout.textContent = "";
return;
}
// Metres, not degrees. The city widget prints a latitude and a longitude
// because that is the address of a place on a map; the address of a place
// in a building is where it is on the floor, in the units the pack was
// authored in, so a self-hoster can read a number off this and type it
// into their own office file.
const where = `${info.x.toFixed(1)}, ${info.z.toFixed(1)} m`;
// The storey is named only when there is more than one to be on. On a
// single-level pack — which is the reference office and will be most
// packs — "Level 1" on every hover is a word that never changes and
// therefore never informs.
const level = scene.plan.levels.length > 1 && info.level ? ` · ${info.level}` : "";
// The person last, and after the room, because the readout is read left to
// right as an address getting more specific: the floor, then the room,
// then who is in it.
const room = info.room ? ` · ${info.room}` : "";
const person = info.person ? ` · ${info.person}` : "";
minimapReadout.textContent = `${where}${level}${room}${person}`;
},
});
/**
* Once, not per frame.
*
* `scene.robots()` hands back a stable array of vectors the robot layer
* mutates in place, so the plan reads this frame's positions through a
* reference taken here — the same handshake `officeScene` already makes with
* `luminaires.setWalkers`. Polling it every frame would allocate nothing and
* would imply the array were a snapshot, which it is not.
*/
panel.setRobots(scene.robots());
return panel;
}
/**
* Put the plan for wherever you are into the panel.
*
* One function rather than a `replaceChildren` at each of the call sites,
* because the failure it prevents is silent: the panel keeps showing a widget
* that is still ticking and still correct *about somewhere else*, which looks
* exactly like a working map.
*/
function showPlan() {
const widget = inside ? officePlan : minimap;
if (!minimapFrame || !widget) return;
minimapFrame.replaceChildren(widget.canvas);
// The canvas has just re-entered the document with a real box for the first
// time, or the first time since it left it. Nothing else tells it that: the
// `ResizeObserver` fires on a change of size, and going from detached to
// attached at the same size is not one.
widget.resize();
}
function leaveOffice() {
if (!city) return;
// Before the scene swap, so the last thing the watch can do is abort a request
// rather than publish into a room the user has already left.
stopWatchingOccupancy();
disposeOfficeScreenUi();
office?.walker?.setActive(false);
office?.walker?.setAction({ x: 0, z: 0 });
dispatchJourney({ type: "leave-office" });
city.stage.setScene(city.stageScene);
inside = false;
attachCurrentWebcamFace();
moveRealtimePresence();
showPlan();
showDetail(null);
refreshGodmodePlace();
renderLegend();
}
/** Dispose everything whose coordinates or subscriptions belong to one office. */
function disposeLoadedOffice(): void {
stopWatchingOccupancy();
disposeOfficeScreenUi();
officePlan?.dispose();
officePlan = null;
office?.dispose();
office = null;
builtOfficeId = null;
officeAtmosphere = null;
}
/**
* Fetch Spaces.
*
* The office is the largest thing in this build that most visitors never open:
* the interior, the furniture catalogue, the material registry and the
* floorplan are 67 kB of chunk — 22 kB across the wire — and they used to be
* downloaded, parsed and executed on every load of a map page by people who
* came to look at a city. Behind these three `await import()`s Vite gives them
* chunks of their own and the door fetches them on the way through. Measured,
* entry chunk: 780.18 kB / 216.89 kB gzipped before, 720.89 / 198.33 after —
* the difference is smaller than the chunks because three.js is shared and
* stays where it was.
*
* All three in one `Promise.all` because they are one arrival: the pack without
* the builder is a data file nobody can draw, so the fetches overlap rather
* than queue. Rollup happens to emit them as three chunks the browser asks for
* together; awaiting them in sequence would make that three round trips on a
* slow link for no reason at all.
*
* There is deliberately no retry and no cache-busting. A failed chunk fetch is
* a deploy that moved the file under an open tab; the honest answer is to say
* the door did not open and let the next click try again, which it will,
* because a rejected dynamic import is not memoised by the browser.
*/
async function loadOffice(): Promise<{
createOfficeScene: typeof import("./interiors/officeScene.ts").createOfficeScene;
createOfficeMinimap: typeof import("./engine/officeMinimap.ts").createOfficeMinimap;
pack: Office;
materials: MaterialRegistry;
} | null> {
try {
// A fourth import and still one arrival. The plan renderer reads prop
// footprints off the asset registry, so it is already downstream of the
// furniture catalogue this chunk exists to hold back — asking for it here
// costs nothing beyond the module itself, and asking for it anywhere else
// would cost the whole catalogue in the entry chunk.
const entry = OFFICES.find((o) => o.id === officeId) ?? OFFICES[0];
if (!entry) return null;
const [interiors, pack, assets, plan] = await Promise.all([
import("./interiors/officeScene.ts"),
entry.load(),
import("./assets/materials.ts"),
import("./engine/officeMinimap.ts"),
]);
// Assigned rather than memoised with `??=`: the memo was what made this
// single-office forever, quietly serving the first pack fetched for every
// later request whatever id was asked for.
officePack = pack.default;
officeMaterials ??= new assets.MaterialRegistry({ quality: "high" });
return {
createOfficeScene: interiors.createOfficeScene,
createOfficeMinimap: plan.createOfficeMinimap,
pack: officePack,
materials: officeMaterials,
};
} catch {
showDetail("The office did not load. Check the connection and try the door again.");
return null;
}
}
/** The office's name, for the two bits of chrome that say where you are. */
function officeName(): string {
return officePack?.name ?? "Spaces";
}
/**
* Who is in the building, asked for once per entry.
*
* Only at full depth, and that is not an optimisation. `createOfficeScene` at
* `"public"` builds **no presence layer at all** — not an empty one, not a
* hidden one — so there is nothing here to populate and the request would be one
* this visitor's session is going to be refused anyway. `presence.ts` explains
* at length why the layer is absent rather than emptied; this is the call site
* that would otherwise quietly reintroduce it.
*
* The fallback is `markers`' fallback, one room in: an API that does not answer
* gets the fabricated roster, because a clone with no server is the flagship
* case and a member shown the same empty room as a stranger has been told the
* tier means something when it does not. An API that *does* answer is believed,
* including when it answers with nobody — an office where everyone has gone home
* is a real fact about an office, and overwriting it with invented people to
* make the demo livelier is the one thing this file must never do.
*/
function watchOccupancy() {
stopWatchingOccupancy();
const scene = office;
if (!scene || scene.depth !== "full") return;
presenceWatch = tera.watchPresence(officePack?.id ?? "lumbridge-hq", (body) => {
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
// `stop()` already drops late answers; this is the belt to that brace,
// 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;
applyPresence();
renderOfficeBadge();
renderSource();
});
}
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 ---------------------------------------------------------------
const nav = document.querySelector<HTMLElement>("#chapters");
const blurb = document.querySelector<HTMLElement>("#blurb");
const title = document.querySelector<HTMLElement>("#title");
const subtitle = document.querySelector<HTMLElement>("#subtitle");
const enterButton = document.querySelector<HTMLButtonElement>("#enter");
const cityNav = document.querySelector<HTMLElement>("#cities");
const source = document.querySelector<HTMLElement>("#source");
const minimapFrame = document.querySelector<HTMLElement>("#minimap .minimap-frame");
const minimapReadout = document.querySelector<HTMLElement>("#minimap-readout");
const tierBadge = document.querySelector<HTMLElement>("#tier");
const presenceHost = document.querySelector<HTMLElement>("#presence-host");
const officeBadge = document.querySelector<HTMLElement>("#office-badge");
const panelToggle = document.querySelector<HTMLButtonElement>("#panel-toggle");
const panelToggleLabel = document.querySelector<HTMLElement>("#panel-toggle-label");
const shortcutsCard = document.querySelector<HTMLElement>("#shortcuts");
const helpButton = document.querySelector<HTMLButtonElement>("#help");
const planToggle = document.querySelector<HTMLButtonElement>("#plan-toggle");
const credits = document.querySelector<HTMLElement>("#credits");
const driveControls = document.querySelector<HTMLElement>("#drive-controls");
const driveHint = document.querySelector<HTMLElement>("#drive-hint");
const walkButton = document.querySelector<HTMLButtonElement>("#walk");
const flyButton = document.querySelector<HTMLButtonElement>("#fly");
const screensButton = document.querySelector<HTMLButtonElement>("#screens");
const walkControls = document.querySelector<HTMLElement>("#walk-controls");
const walkHint = document.querySelector<HTMLElement>("#walk-hint");
const profileOverlay = document.querySelector<HTMLElement>("#profile-overlay");
const webcamFaceIndicator = document.querySelector<HTMLElement>("#webcam-face-indicator");
const screensOverlay = document.querySelector<HTMLElement>("#screens-overlay");
let profileEditor: ProfileEditor | null = null;
let webcamFacePanel: WebcamFacePanel | null = null;
let webcamFaceTexture: WebcamFaceTextureAdapter | null = null;
let webcamFaceConsent = createWebcamFaceConsent();
let webcamCapture: WebcamCaptureController | null = null;
function showDetail(text: string | null) {
const card = document.querySelector<HTMLElement>("#detail");
const body = document.querySelector<HTMLElement>("#detail-text");
if (!card || !body) return;
card.hidden = text === null;
// The text, and not the card: the card also holds the dismiss button, which
// `textContent` on the card would delete the first time a marker was picked.
body.textContent = text ?? "";
}
/**
* The board strip above the legend: world scales outside, buildings inside.
*
* One control that answers "which of these am I in", pointed at whichever list
* is currently the answer. A second, separate office strip was the obvious
* alternative and is worse: it would sit dead and greyed out for the entire time
* anybody is looking at the city, which is most of the time.
*/
function renderCityPicker() {
if (!cityNav) return;
cityNav.replaceChildren();
const entries = inside
? OFFICES.map((o) => ({ id: o.id, label: o.label, active: o.id === officeId }))
: CITIES.map((c) => ({ id: c.id, label: c.label, active: c.id === cityId }));
for (const entry of entries) {
const b = document.createElement("button");
// `aria-pressed` rather than a class, because these buttons choose exactly
// one active board. The stylesheet keys off the attribute
// so the visual state and the announced state cannot drift apart.
b.className = "city";
b.type = "button";
b.setAttribute("aria-pressed", String(entry.active));
b.textContent = entry.label;
b.addEventListener("click", () => {
if (inside) void switchOffice(entry.id);
else switchCity(entry.id);
});
cityNav.append(b);
}
}
/**
* Walk out of one building and into another.
*
* A full teardown and rebuild rather than a swap, because everything an office
* scene holds is derived from its pack: the shell, the plan panel, the camera
* limits, the horizon drop and the light rig. The expensive part — the texture
* registry — is deliberately *not* rebuilt, which is the same trick that makes
* signing in cheap: `officeMaterials` outlives every scene that borrows it, so
* a switch costs geometry and not the thing that draws the wood grain.
*/
async function switchOffice(id: string) {
// `entering` is the same guard `toggleOffice` uses, and this has to share it.
// Without it, two clicks inside the loading window build two office scenes and
// the first is parked on the stage with nothing holding a reference to it —
// a whole `OfficeScene`, its geometry and its plan panel, leaked per click.
if (entering || id === officeId || !OFFICES.some((o) => o.id === id)) return;
const previous = officeId;
officeId = id;
// The roster, screen session, plan and scene all belong to the building left.
disposeLoadedOffice();
entering = true;
try {
await building("Opening the door…", () => enterOffice());
} finally {
entering = false;
}
/**
* The old room is already gone by the time we find out whether the new one
* arrived, and a failed chunk fetch is a real case — `loadOffice` says so on
* the card and returns null.
*
* So there is no room on the stage and `inside` still claims there is. Put the
* viewer back in the city rather than in an empty scene, and put the door back
* on the building they came from, or the picker is wedged pointing at an
* office that will not open.
*/
if (office || !city) return;
officeId = previous;
inside = false;
city.stage.setScene(city.stageScene);
showPlan();
renderLegend();
}
/** One legend for both places — a city chapter and an office viewpoint are both `View`s. */
function renderLegend() {
renderCityPicker();
if (!nav || !city) return;
const views: View[] = inside && office ? office.views : city.chapters;
const activeId = inside && office ? office.current() : city.current();
nav.replaceChildren();
views.forEach((view, i) => {
const button = document.createElement("button");
button.className = "chapter";
button.type = "button";
button.setAttribute("aria-pressed", String(view.id === activeId));
const number = view.number ?? String(i + 1).padStart(2, "0");
button.innerHTML = `<span class="num">${number}</span><span>${view.shortLabel}</span>`;
button.addEventListener("click", () => flyToIndex(i));
nav.append(button);
});
const active = views.find((v) => v.id === activeId);
if (blurb) {
blurb.textContent = active?.description ?? "";
blurb.hidden = !active?.description;
}
const cityLabel = CITIES.find((c) => c.id === cityId)?.city.name ?? "";
if (title) title.textContent = inside ? officeName() : cityLabel;
if (subtitle) {
subtitle.textContent = inside ? "Spaces · a Lumbridge office" : "Tera · Lumbridge Simulate";
}
if (enterButton) {
// One label for everyone. The door is open at both tiers; what differs is
// what is behind it, and that is the badge's job to say, not the button's.
enterButton.textContent = inside ? "← Back to the city" : "Enter the office →";
}
const walking = inside && (office?.walker?.active() ?? false);
const exploring = !inside && (city.actorActive() ?? false);
const flying = !inside && (city.aircraftActive() ?? false);
if (walkButton) {
walkButton.hidden = inside ? office?.walker === null : city.actorState() === null;
walkButton.setAttribute("aria-pressed", String(walking || exploring));
if (inside && office?.walker) {
const actor = office.walker.state().actor === "anonymous-dog" ? "your dog" : "your humanoid";
walkButton.textContent = walking ? "Return to overview ↑" : `Walk as ${actor}`;
} else if (city.actorState()) {
const actor = city.actorState()?.kind === "crow" ? "your crow" : "your humanoid";
walkButton.textContent = exploring ? "Return to flyover ↑" : `Explore as ${actor}`;
}
}
if (flyButton) {
flyButton.hidden = inside || cityId !== "california" || city.aircraftState() === null;
flyButton.setAttribute("aria-pressed", String(flying));
flyButton.textContent = flying ? "Return to flyover ↑" : "Fly the California route →";
}
if (screensButton) {
const canManage = inside && office?.depth === "full" && (office.listMediaSurfaces().length > 0);
screensButton.hidden = !canManage;
}
renderSource();
if (panelToggleLabel) panelToggleLabel.textContent = inside ? "Office" : cityLabel;
if (canvas) {
canvas.setAttribute(
"aria-label",
inside
? walking
? `${officeName()}, following your ${office?.walker?.state().actor === "anonymous-dog" ? "dog" : "humanoid"}. Use W A S D to move.`
: `${officeName()}, seen from above. Drag to orbit, scroll to zoom.`
: flying
? `${cityLabel}, following your electric aircraft. Use W A S D to fly or P to resume assisted flight.`
: exploring
? `${cityLabel}, following your ${city.actorState()?.kind ?? "actor"}. Use W A S D to move, Q and E for altitude, and G to glide.`
: `Map of ${cityLabel}, seen from above. Drag to orbit, scroll to zoom.`,
);
}
// Each plan rings the entry its own legend is showing as current. Both are
// updated whichever one is mounted, so the hidden one is already right when it
// comes back rather than correcting itself a frame after it appears.
minimap?.setChapters(city.chapters, city.current());
officePlan?.setActiveView(office?.current() ?? null);
renderOfficeBadge();
if (driveControls) driveControls.hidden = !routeDriveIsActive();
if (walkControls) {
walkControls.hidden = !(walking || exploring || flying);
walkControls.setAttribute(
"aria-label",
flying
? "Aircraft flight controls"
: exploring && city.actorState()?.kind === "crow"
? "Crow flight controls"
: "Walking controls",
);
}
for (const control of walkControls?.querySelectorAll<HTMLButtonElement>("[data-walk-key]") ?? []) {
control.textContent = flying
? control.dataset.aircraftLabel ?? control.textContent
: control.dataset.walkLabel ?? control.textContent;
}
for (const control of walkControls?.querySelectorAll<HTMLElement>(".flight-only") ?? []) {
control.hidden = inside || (!flying && city.actorState()?.kind !== "crow");
}
for (const control of walkControls?.querySelectorAll<HTMLElement>(".aircraft-only") ?? []) {
control.hidden = !flying;
}
for (const control of walkControls?.querySelectorAll<HTMLElement>(".crow-only") ?? []) {
control.hidden = inside || flying || city.actorState()?.kind !== "crow";
}
if (driveHint) driveHint.hidden = inside || cityId !== "california" || flying;
if (walkHint) {
walkHint.hidden = !(inside || city.actorState() || city.aircraftState());
walkHint.textContent = flying
? "WASD fly · P assisted · R reset"
: inside
? "V walk · WASD move"
: "V explore · WASD · Q/E altitude · G glide";
}
}
/**
* The corner label, which now names the parts rather than claiming the whole.
*
* Only the *positive* case gets a permanent label. This used to read "sample
* data · fabricated, not real companies" on every frame of every load, which is
* the overwhelmingly common case — no deployment has a markers source wired by
* default — so the disclosure was on screen approximately always and had become
* furniture. A caption nobody reads is not disclosure, it is a watermark. The
* fact still has to be somewhere on the same screen as the map, so it is stated
* on the boot card everyone passes through and again in the `?` card, one
* keypress away and permanently reachable.
*
* What is left is the informative signal, and it is three signals rather than
* one. The markers, the weather and the traffic arrive from three different
* places and every combination of them is a deployment that exists; a single
* flag has to pick one to be about and then lie about the other two. The
* particular lie this closes is "live data" printed over invented companies
* because a weather station answered — which is precisely the claim the `live`
* flag was introduced to prevent. `describeLiveness` in `adapters/http.ts` owns
* the wording; all three live is the only case that still says "live data".
*
* Called from `renderLegend` and once a second from the frame pump, because the
* feeds settle after the legend has been drawn.
*/
function renderSource() {
if (!source) return;
/**
* The office's provenance line, which outranks the city's.
*
* It used to live on `#office-badge` inside `#panel`, and on a phone `#panel`
* is a bottom sheet that starts closed — so the one sentence standing between
* twenty-five invented people at real desks and a screenshot presented as a
* staff list was behind a hamburger, on the device most likely to take the
* screenshot. This slot is the right home for it anyway and not merely a
* visible one: `#source` is where this page already says what on screen is and
* is not real, it is fixed and always drawn, and the phone stylesheet calls it
* "the one caption that is never allowed to be dropped for space".
*
* It replaces the city's label rather than joining it, because while you are
* standing in the office the markers, the sky and the traffic are facts about
* a board behind you — and "live data" printed over invented colleagues is the
* exact species of lie the liveness wording was rewritten to stop telling.
*/
const sample = inside && presenceIsSample;
// A class rather than an inline style, so the stylesheet keeps the decision
// about how a phone lays this out and this keeps the decision about what it
// says. The rule it turns on is the one that stops the sentence being
// ellipsised down to the half that reads as reassuring.
document.body.classList.toggle("sample-occupancy", sample);
if (sample) {
const caption = "sample occupancy · these people are invented";
if (source.textContent !== caption) source.textContent = caption;
source.hidden = false;
return;
}
const label = describeLiveness({
markers: liveData,
// An override is a sky somebody invented, so it retires the claim for as
// long as it is up — the label is about what is on screen, not about what
// the deployment could have shown.
weather: weatherOverride === null && (weatherWatch?.current().live ?? false),
flights: cityFlights?.live() ?? false,
});
if (source.textContent !== label) source.textContent = label;
source.hidden = label === "";
/**
* The green. `.source.live` in `index.html` is the whole visual difference
* between this line and the rest of the chrome, and the rewrite that replaced
* `source.className = "source live"` with a `textContent`/`hidden` pair
* dropped it — so every live label rendered at `--ink-3`, the same muted grey
* as a key hint, and the stylesheet rule could no longer match anything. This
* label is only ever on screen when it has something to say; the colour is
* how it says it is worth reading.
*/
source.classList.toggle("live", label !== "");
renderCredits();
}
/**
* Who to thank for what is on screen, in the `?` card.
*
* MET Norway and Open-Meteo publish under CC BY 4.0 and the server emits the
* credit line each of them asks for — `server/README.md` says in as many words
* that the consumer is expected to display it — and adsb.lol asks to be named
* for the positions. All of it arrived, was parsed into `WeatherFeed.attribution`
* and `FlightsBody.attribution`, and was then read by nobody: a licence
* obligation plumbed to within one line of being met.
*
* It goes in the `?` card rather than on the `#source` line, and that is a
* choice rather than convenience. The corner label is one short phrase and on a
* phone it is explicitly clamped to a single ellipsised line, so a licence
* sentence appended to it would be *truncated* — the one outcome worse than
* putting it a keypress away. The `?` card is reachable from every state the
* app can be in, on both layouts, and already carries the sentence about the
* markers being fabricated; the provenance of the map belongs in one place.
*
* Empty when nothing live is on screen, because a credit for data nobody is
* looking at is noise, and because the zero-config build owes nobody anything.
*/
function renderCredits() {
if (!credits) return;
const lines: string[] = [];
// The weather override is somebody's invention; it is not MET Norway's sky
// and must not be attributed to them.
if (weatherOverride === null) lines.push(...(weatherWatch?.current().attribution ?? []));
lines.push(...(cityFlights?.attribution() ?? []));
const unique = [...new Set(lines.filter((line) => line !== ""))];
credits.textContent = unique.join(" · ");
credits.hidden = unique.length === 0;
}
/**
* The one thing a public visitor is actually missing, said in the place where
* they would notice it missing.
*
* An empty office with no explanation reads as a bug — a floor that failed to
* load — and the fix for that is a sentence, not a disabled button. The
* sign-in link is offered *beside* the office rather than in front of it, so it
* is an upgrade and never a toll gate.
*/
function renderOfficeBadge() {
if (!officeBadge) return;
const publicOffice = inside && office !== null && office.depth === "public";
const mediaSurfaces = inside && office !== null && office.depth === "full"
? office.listMediaSurfaces()
: [];
// The fabricated-occupancy caption used to be here too and is now on
// `#source` — see `renderSource`. This badge keeps the message that is a call
// to action rather than a disclosure, because that one belongs beside the
// office controls and survives being missed; the other one does not.
officeBadge.hidden = !publicOffice && mediaSurfaces.length === 0;
if (!publicOffice) {
if (mediaSurfaces.length === 0) return;
const noun = mediaSurfaces.length === 1 ? "screen" : "screens";
const active = mediaSurfaces.filter((surface) => surface.bound).length;
officeBadge.textContent = active > 0
? `${active} of ${mediaSurfaces.length} ${noun} active · stop control in Office screens.`
: `${mediaSurfaces.length} ${noun} ready · media stays off until you opt in.`;
return;
}
officeBadge.replaceChildren(
document.createTextNode("Public view — the building, not the people. "),
);
if (access.signInUrl !== null) {
const link = document.createElement("a");
link.href = access.signInUrl;
link.textContent = "Sign in for the live floor";
officeBadge.append(link, document.createTextNode("."));
} else {
officeBadge.append(document.createTextNode("Sign in to see who's in."));
}
}
/**
* Who the site thinks you are, in the corner, always. Three words and a name.
*
* It is here rather than buried in a menu because every other difference on
* this page — an empty office, sample markers, no godmode tab — is a
* *silence*, and a silence you cannot attribute is indistinguishable from a
* fault. This is the line that tells you which of the two you are looking at.
*/
function renderTierBadge() {
if (!tierBadge) return;
tierBadge.className = `card tier ${access.tier}`;
const label = document.createElement("span");
/**
* The label names what you *get*, not who you are, and that is deliberate.
* "Signed in" was the first draft and it is a lie in the commonest case:
* a clean clone with no API at all resolves to `member`, and telling someone
* they are signed in to a server that does not exist is the sort of small
* dishonesty that makes the rest of the interface untrustworthy. "Full view"
* is true whether the tier came from a session or from there being nothing to
* have a session with; the subject, when there is one, says the rest.
*/
label.textContent =
access.tier === "god" ? "Godmode" : access.tier === "member" ? "Full view" : "Public view";
tierBadge.replaceChildren(label);
if (access.subject !== null) {
const who = document.createElement("span");
who.className = "who";
who.textContent = localProfile?.displayName ?? access.subject;
tierBadge.append(who);
if (localProfile) {
const customize = document.createElement("button");
customize.type = "button";
customize.className = "profile-trigger";
customize.textContent = "Character";
customize.addEventListener("click", openProfileEditor);
tierBadge.append(customize);
}
} else if (access.signInUrl !== null) {
const link = document.createElement("a");
link.href = access.signInUrl;
link.textContent = "Sign in";
tierBadge.append(link);
}
tierBadge.hidden = false;
}
// ---- Hosted realtime presence --------------------------------------------
function realtimeInterest(): InterestCell {
if (inside) return { kind: "office", officeId };
if (cityId === "california") return { kind: "california-tile", x: 0, y: 0, level: 0 };
return { kind: "city", cityId: cityId === "socal" ? "socal" : "bay-area" };
}
function degrees(radians: number): number {
return ((radians * 180 / Math.PI + 540) % 360) - 180;
}
function realtimePose(): EntityPoseSnapshot | null {
if (access.subject === null) return null;
const timestampMs = Date.now();
if (inside) {
const walker = office?.walker;
if (!walker) return null;
const state = walker.state();
const headingDeg = -degrees(Math.atan2(-state.facing.x, -state.facing.z));
return {
entity: "actor",
actorId: realtimeActorId,
kind: state.actor === "anonymous-dog" ? "dog" : "humanoid",
sequence: 0,
timestampMs,
pose: {
space: "local",
cell: { kind: "office", officeId },
xM: state.position.x,
yM: office?.plan.level(state.levelId)?.floorY ?? 0,
zM: state.position.z,
headingDeg,
pitchDeg: 0,
},
velocity: {
xMps: state.action.x * 1.6,
yMps: 0,
zMps: state.action.z * 1.6,
yawDegPerSec: 0,
},
};
}
const aircraft = city?.aircraftActive() ? city.aircraftState() : null;
if (aircraft) {
return createAircraftPoseSnapshot(
{ aircraftId: realtimeAircraftId, pilotActorId: realtimeActorId },
aircraft,
0,
timestampMs,
);
}
const vehicle = journey.vehicle !== null ? city?.vehicleState() : null;
if (vehicle) {
const heading = vehicle.headingDeg * Math.PI / 180;
return {
entity: "vehicle",
vehicleId: realtimeVehicleId,
kind: "model-x",
driverActorId: realtimeActorId,
sequence: 0,
timestampMs,
pose: {
space: "geographic",
lat: vehicle.lat,
lng: vehicle.lng,
altitudeM: 0,
headingDeg: vehicle.headingDeg,
pitchDeg: 0,
},
velocity: {
xMps: Math.sin(heading) * vehicle.speedMps,
yMps: 0,
zMps: Math.cos(heading) * vehicle.speedMps,
yawDegPerSec: 0,
},
steering: vehicle.steering,
wheelRadians: vehicle.wheelRadians,
};
}
const actor = city?.actorState();
if (!actor || !city) return null;
const headingDeg = -degrees(actor.yaw);
const velocity = {
xMps: -Math.sin(actor.yaw) * actor.speedMps,
yMps: actor.verticalSpeedMps,
// Geographic velocity is east/up/north; scene +Z is south.
zMps: Math.cos(actor.yaw) * actor.speedMps,
yawDegPerSec: 0,
};
const origin = cityId === "california" ? { lat: 35.5, lng: -119.5 } : city.world.city.center;
const [originX, originZ] = city.world.project(origin.lat, origin.lng);
const [lat, lng] = city.world.unproject(
originX + actor.x / city.world.metresPerUnit,
originZ + actor.z / city.world.metresPerUnit,
);
return {
entity: "actor",
actorId: realtimeActorId,
kind: actor.kind,
sequence: 0,
timestampMs,
pose: {
space: "geographic",
lat,
lng,
altitudeM: actor.y,
headingDeg,
pitchDeg: degrees(actor.pitch),
},
velocity,
};
}
function ownRealtimeEntity(snapshot: EntityPoseSnapshot): boolean {
if (snapshot.entity === "actor") return snapshot.actorId === realtimeClient?.state().actorId;
if (snapshot.entity === "vehicle") {
return snapshot.driverActorId === realtimeClient?.state().actorId || snapshot.vehicleId === realtimeVehicleId;
}
return snapshot.pilotActorId === realtimeClient?.state().actorId ||
snapshot.aircraftId === realtimeAircraftId;
}
function clearRealtimePeers(): void {
city?.clearRemoteEntities();
office?.clearRemoteEntities();
updatePresenceIndicator();
}
function applyRealtimeMessage(message: ServerRealtimeMessage): void {
if (message.type === "membership-revoked") {
stopWebcamFace(true);
clearRealtimePeers();
return;
}
const updates = message.type === "pose-delta"
? message.updates
: message.type === "join-grant"
? message.initial
: message.type === "resume-grant"
? message.snapshot
: [];
const target = inside ? office : city;
for (const snapshot of updates) {
if (!ownRealtimeEntity(snapshot)) target?.upsertRemoteSnapshot(snapshot);
}
if (message.type === "pose-delta") {
for (const id of message.removedEntityIds) {
if (
id !== `actor:${realtimeClient?.state().actorId}` &&
id !== `vehicle:${realtimeVehicleId}` &&
id !== `aircraft:${realtimeAircraftId}`
) {
target?.removeRemoteEntity(id);
}
}
}
updatePresenceIndicator();
}
function updatePresenceIndicator(state = realtimeClient?.state()): void {
if (!presenceIndicator || access.subject === null) return;
const connection: "connecting" | "live" | "reconnecting" | "offline" =
state?.status === "streaming" ? "live"
: state?.status === "reconnecting" ? "reconnecting"
: state?.status === "left" || state?.status === "disposed" ? "offline"
: "connecting";
const nearbyPeerCount = connection === "live"
? (inside ? office?.remoteEntityCount() : city?.remoteEntityCount()) ?? 0
: 0;
presenceIndicator.update({ signedIn: true, connection, nearbyPeerCount });
}
async function joinRealtimePresence(operation: number): Promise<void> {
const client = realtimeClient;
const pose = realtimePose();
if (!client || !pose) return;
try {
const grant = await client.join(realtimeInterest(), pose);
if (operation !== realtimeOperation) return;
applyRealtimeMessage(grant);
} catch {
if (operation === realtimeOperation) {
presenceIndicator?.update({ signedIn: true, connection: "offline", nearbyPeerCount: 0 });
}
}
}
function moveRealtimePresence(): void {
clearRealtimePeers();
const client = realtimeClient;
if (!client || access.subject === null) return;
const operation = ++realtimeOperation;
if (client.state().sessionId === null) {
void joinRealtimePresence(operation);
return;
}
updatePresenceIndicator({ ...client.state(), status: "reconnecting" });
void client.moveInterest(realtimeInterest())
.then((grant) => {
if (operation === realtimeOperation) applyRealtimeMessage(grant);
})
.catch(() => {
if (operation === realtimeOperation) {
presenceIndicator?.update({ signedIn: true, connection: "offline", nearbyPeerCount: 0 });
}
});
}
function publishRealtimePresence(now: number): void {
if (!realtimeClient || now - lastRealtimePublishAt < 100) return;
const pose = realtimePose();
if (!pose || realtimeClient.state().sessionId === null) return;
lastRealtimePublishAt = now;
try {
realtimeClient.publishPose(pose);
} catch {
presenceIndicator?.update({ signedIn: true, connection: "offline", nearbyPeerCount: 0 });
}
}
async function initializeRealtimePresence(): Promise<void> {
if (!realtimePageActive || access.subject === null || !presenceHost || realtimeClient) return;
const [{ createRealtimeClient }, { createPresenceIndicator }] = await Promise.all([
import("./realtime/client.ts"),
import("./realtime/presenceIndicator.ts"),
]);
// `pagehide` can overtake the lazy chunks. Never construct a fresh client
// after the page has already run its terminal cleanup.
if (!realtimePageActive || access.subject === null || realtimeClient) return;
presenceHost.hidden = false;
document.body.classList.add("presence-on");
presenceIndicator = createPresenceIndicator({
container: presenceHost,
onRetry: () => moveRealtimePresence(),
});
presenceIndicator.update({ signedIn: true, connection: "connecting", nearbyPeerCount: 0 });
realtimeClient = createRealtimeClient({
actorId: realtimeActorId,
authenticatedFetch: authFetch,
sendIntervalMs: 100,
});
stopRealtimeSubscription = realtimeClient.subscribe({
onMessage: applyRealtimeMessage,
onStateChange: updatePresenceIndicator,
onError: () => presenceIndicator?.update({ signedIn: true, connection: "offline", nearbyPeerCount: 0 }),
});
moveRealtimePresence();
}
window.addEventListener("pagehide", () => {
realtimePageActive = false;
stopWebcamFace(true);
disposeOfficeScreenUi();
realtimeOperation += 1;
stopRealtimeSubscription?.();
stopRealtimeSubscription = null;
clearRealtimePeers();
// Page teardown cannot await the leave round trip. Contain a rotated-token
// or offline failure so navigation never produces an unhandled rejection;
// the server's disconnect grace still removes the ephemeral session.
void realtimeClient?.dispose().catch(() => undefined);
realtimeClient = null;
presenceIndicator?.dispose();
presenceIndicator = null;
});
function applyProfilePreview(profile: LocalProfile): void {
if (access.subject === null) return;
if (inside) {
const appearance = resolveHumanoidAppearance(profile.appearance);
office?.walker?.setAppearance({
kind: "humanoid",
skinTone: appearance.skinTone,
outfitColor: appearance.outfitColor,
accentColor: appearance.accentColor,
hairColor: appearance.hairColor,
bodyShape: appearance.bodyShape,
});
} else {
city?.setActorIdentity(signedInActorIdentity(profile));
}
}
function cameraSupported(): boolean {
return typeof navigator.mediaDevices?.getUserMedia === "function";
}
/** Attach the one ephemeral texture to whichever signed-in humanoid is current. */
function attachCurrentWebcamFace(): void {
const texture = webcamFaceTexture?.texture() ?? null;
if (!texture || access.subject === null) return;
city?.clearActorFaceTexture();
office?.walker?.clearFaceTexture();
if (inside) office?.walker?.attachFaceTexture(texture);
else city?.attachActorFaceTexture(texture);
}
/**
* Stop app-owned capture. The texture adapter deliberately never owns tracks,
* so this boundary must stop every one before dropping the stream reference.
*/
function stopWebcamFace(revoke = false): void {
city?.clearActorFaceTexture();
office?.walker?.clearFaceTexture();
webcamFaceTexture?.clear();
webcamCapture?.stop();
if (revoke) webcamFaceConsent.revoke();
else webcamFaceConsent.stop();
webcamFaceTexture?.sync();
webcamFacePanel?.update(cameraSupported() ? "off" : "unsupported");
}
async function startWebcamFace(): Promise<void> {
if (access.subject === null || !localProfile || !cameraSupported() || webcamCapture?.status() === "active" ||
webcamCapture?.status() === "requesting") return;
webcamFacePanel?.update("requesting");
webcamFaceConsent.requestStart();
webcamCapture ??= createWebcamCapture({
acquire: () => navigator.mediaDevices.getUserMedia({
audio: false,
video: { facingMode: "user", width: { ideal: 640 }, height: { ideal: 640 } },
}),
createVideo: () => {
const video = document.createElement("video");
video.muted = true;
video.playsInline = true;
video.autoplay = true;
video.hidden = true;
document.body.append(video);
return video;
},
});
let binding: Awaited<ReturnType<WebcamCaptureController["start"]>>;
try {
binding = await webcamCapture.start();
} catch (error) {
webcamFaceConsent.stop();
const denied = error instanceof DOMException && (error.name === "NotAllowedError" || error.name === "SecurityError");
webcamFacePanel?.update(
"error",
denied ? "Camera permission was denied. Nothing was captured." : "Camera could not start. Check the device and try again.",
);
return;
}
// Null means Stop/pagehide overtook an asynchronous permission or play step.
if (!binding) return;
if (access.subject === null) {
stopWebcamFace(true);
return;
}
try {
const consent = webcamFaceConsent.start(true);
if (!consent.accepted) throw new Error("camera consent was no longer active");
webcamFaceTexture ??= createWebcamFaceTexture({ consent: webcamFaceConsent });
webcamFaceTexture.bind(binding);
for (const track of binding.stream.getVideoTracks()) {
track.addEventListener("ended", () => {
if (webcamCapture?.binding()?.stream === binding.stream) stopWebcamFace();
}, { once: true });
}
attachCurrentWebcamFace();
webcamFacePanel?.update("active");
// Native permission UI can restore focus to the page after the promise
// resolves. Put it back inside the still-open modal on the next frame.
requestAnimationFrame(() => {
if (profileEditor?.state().open) webcamFacePanel?.focusStop();
});
} catch {
stopWebcamFace();
webcamFacePanel?.update("error", "Camera preview could not start. Nothing was retained.");
}
}
function ensureProfileEditor(): ProfileEditor | null {
if (profileEditor) return profileEditor;
if (!profileOverlay || !localProfile || access.subject === null) return null;
profileEditor = createProfileEditor({
container: profileOverlay,
profile: localProfile,
identityId: access.subject,
onPreview: applyProfilePreview,
onSave(profile) {
localProfile = profile;
saveLocalProfile(sessionStorage, LOCAL_PROFILE_KEY, profile);
dispatchJourney({
type: "sign-in-actor-swap",
actor: {
id: access.subject ?? "anonymous",
kind: "humanoid",
signedIn: true,
profile: { displayName: profile.displayName, color: humanoidAppearance()?.accentColor },
},
});
city?.setActorIdentity(signedInActorIdentity(profile));
applyProfilePreview(profile);
renderTierBadge();
profileOverlay.hidden = true;
},
onCancel() {
profileOverlay.hidden = true;
},
});
profileEditor.root.addEventListener("keydown", (event) => event.stopPropagation());
if (webcamFaceIndicator) {
const host = document.createElement("div");
profileEditor.root.insertBefore(host, profileEditor.root.querySelector("form")?.nextSibling ?? null);
webcamFacePanel = createWebcamFacePanel({
container: host,
indicatorContainer: webcamFaceIndicator,
supported: cameraSupported(),
onStart: startWebcamFace,
onStop: () => stopWebcamFace(),
});
if (webcamCapture?.status() === "active") webcamFacePanel.update("active");
profileEditor.registerFocusables(webcamFacePanel.focusables);
}
return profileEditor;
}
function openProfileEditor(): void {
if (!localProfile || !profileOverlay) return;
const editor = ensureProfileEditor();
if (!editor) return;
editor.update(localProfile);
profileOverlay.hidden = false;
editor.open();
}
function screenBinding(surface: MediaSurfaceDescriptor) {
return {
officeId: surface.officeId,
levelId: surface.levelId,
roomId: surface.roomId,
screenId: surface.screenId,
};
}
function remotePanelStatus(screenId: string, status: OfficeScreenRemoteStatus): void {
officeScreenPanel?.setRemoteStatus(screenId, status);
renderOfficeBadge();
}
function isRemoteAuthorizationError(error: unknown): boolean {
return error instanceof Error && /\((?:401|403)\)/.test(error.message);
}
function releaseRemoteViewer(
status: OfficeScreenRemoteStatus = "off",
action: "revoke" | "dispose" = "revoke",
): void {
const viewing = remoteViewedScreen;
const pendingScreenId = remoteViewerRequestScreenId;
remoteMediaOperation += 1;
remoteViewerRequestScreenId = null;
if (pendingScreenId && pendingScreenId !== viewing?.screenId) {
remotePanelStatus(pendingScreenId, status);
}
if (!viewing) return;
remoteViewedScreen = null;
office?.clearMediaSurface(viewing.screenId);
// `OfficeScreenPanel` owns defensive descriptor snapshots rather than a
// live view of OfficeScene. Refresh after clear just as the bind path does,
// otherwise the panel can keep saying "Media active" after remote teardown.
officeScreenPanel?.update(office?.listMediaSurfaces() ?? []);
viewing.texture.dispose();
viewing.video.pause();
viewing.video.srcObject = null;
remotePanelStatus(viewing.screenId, status);
const finish = action === "revoke" ? viewing.remote.revoke() : viewing.remote.dispose();
void finish.catch(() => undefined);
}
function syncRemoteViewerState(
viewing: NonNullable<typeof remoteViewedScreen>,
state: RemoteMediaState,
): void {
if (remoteViewedScreen !== viewing) return;
if (state.status === "reconnecting") remotePanelStatus(viewing.screenId, "reconnecting");
else if (state.status === "connecting") remotePanelStatus(viewing.screenId, "connecting");
else if (state.status === "live") remotePanelStatus(viewing.screenId, "live");
if (state.hasRemoteVideo && !viewing.bound) {
// Playback is a consequence of the user's screen-specific opt-in click;
// the transport itself deliberately never calls `play()`.
void viewing.video.play().then(() => {
if (remoteViewedScreen !== viewing || !office || !inside) return;
viewing.bound = office.bindMediaSurface(
viewing.screenId,
{ canView: true, optedIn: true },
viewing.texture,
);
officeScreenPanel?.update(office.listMediaSurfaces());
remotePanelStatus(viewing.screenId, viewing.bound ? "live" : "error");
}).catch(() => {
if (remoteViewedScreen === viewing) {
showDetail("The remote screen arrived, but this browser could not start video playback.");
releaseRemoteViewer("error", "dispose");
}
});
}
if (state.status === "stopped" || state.status === "revoked") {
showDetail(state.status === "revoked" ? "Remote screen access was revoked." : "The remote screen share stopped.");
releaseRemoteViewer("stopped", "dispose");
}
}
async function startRemoteViewer(surface: MediaSurfaceDescriptor): Promise<void> {
if (access.subject === null || !inside || !office || office.depth !== "full") return;
releaseRemoteViewer("off");
const operation = ++remoteMediaOperation;
remoteViewerRequestScreenId = surface.screenId;
remotePanelStatus(surface.screenId, "connecting");
showDetail(`Connecting to ${surface.screenId}`);
try {
const { createRemoteOfficeMedia } = await import("./media/remoteMedia.ts");
const optedIn = officeScreenPanel?.state().optedInScreenIds.includes(surface.screenId) ?? false;
if (operation !== remoteMediaOperation || access.subject === null || !inside || !office ||
office.depth !== "full" || officeId !== surface.officeId || !optedIn) return;
const video = document.createElement("video");
video.muted = true;
video.playsInline = true;
video.autoplay = false;
const texture = new VideoTexture(video);
texture.colorSpace = SRGBColorSpace;
texture.generateMipmaps = false;
let viewing: NonNullable<typeof remoteViewedScreen>;
const remote = createRemoteOfficeMedia({
role: "viewer",
binding: screenBinding(surface),
authenticatedFetch: authFetch,
onStateChange: (state) => { if (viewing) syncRemoteViewerState(viewing, state); },
onError: (error) => {
if (remoteViewedScreen !== viewing) return;
if (isRemoteAuthorizationError(error)) {
showDetail("Remote screen authorization ended.");
releaseRemoteViewer("unavailable", "dispose");
} else {
remotePanelStatus(surface.screenId, "reconnecting");
}
},
});
viewing = { screenId: surface.screenId, officeId: surface.officeId, video, texture, remote, bound: false };
remoteViewerRequestScreenId = null;
remoteViewedScreen = viewing;
await remote.startViewer({ viewerOptIn: true, video });
if (remoteViewedScreen === viewing) showDetail(`Waiting for ${surface.screenId} remote video…`);
} catch (error) {
if (operation !== remoteMediaOperation) return;
remoteViewerRequestScreenId = null;
releaseRemoteViewer("unavailable", "dispose");
remotePanelStatus(surface.screenId, "unavailable");
showDetail(isRemoteAuthorizationError(error)
? "Remote screen authorization ended. Sign in again to reconnect."
: "No authorized remote share is available for that screen.");
}
}
async function startRemotePresenter(
surface: MediaSurfaceDescriptor,
shared: NonNullable<typeof sharedScreen>,
): Promise<void> {
if (access.subject === null || sharedScreen !== shared || !inside || office?.depth !== "full") return;
remotePanelStatus(surface.screenId, "connecting");
try {
const { createRemoteOfficeMedia } = await import("./media/remoteMedia.ts");
if (access.subject === null || sharedScreen !== shared || !inside || office?.depth !== "full") return;
const remote = createRemoteOfficeMedia({
role: "presenter",
binding: screenBinding(surface),
authenticatedFetch: authFetch,
onStateChange(state) {
if (sharedScreen !== shared) return;
if (state.status === "live") remotePanelStatus(surface.screenId, "live");
else if (state.status === "reconnecting") remotePanelStatus(surface.screenId, "reconnecting");
else if (state.status === "connecting") remotePanelStatus(surface.screenId, "connecting");
else if (state.status === "stopped" || state.status === "revoked") {
showDetail("The hosted screen share ended.");
queueMicrotask(() => { if (sharedScreen === shared) stopLocalScreenShare(); });
}
},
onError: (error) => {
if (sharedScreen !== shared) return;
if (isRemoteAuthorizationError(error)) {
showDetail("Screen sharing stopped because authorization ended.");
queueMicrotask(() => { if (sharedScreen === shared) stopLocalScreenShare(); });
} else {
remotePanelStatus(surface.screenId, "reconnecting");
}
},
});
shared.remote = remote;
await remote.startPresenter({ consent: { authorized: true, optedIn: true }, stream: shared.stream, video: shared.video });
if (sharedScreen === shared) showDetail(`Sharing ${surface.screenId} locally and to authorized remote viewers.`);
} catch (error) {
if (sharedScreen !== shared) return;
if (isRemoteAuthorizationError(error)) {
showDetail("Screen sharing stopped because authorization ended. Sign in again to share.");
stopLocalScreenShare();
return;
}
void shared.remote?.dispose().catch(() => undefined);
shared.remote = null;
remotePanelStatus(surface.screenId, "unavailable");
showDetail(`Sharing locally to ${surface.screenId}; hosted sharing is unavailable.`);
}
}
function stopLocalScreenShare(): void {
const shared = sharedScreen;
if (!shared) return;
sharedScreen = null;
remoteMediaOperation += 1;
office?.clearMediaSurface(shared.screenId);
remotePanelStatus(shared.screenId, "stopped");
void shared.remote?.stop().catch(() => undefined);
for (const track of shared.stream.getTracks()) track.stop();
shared.texture.dispose();
shared.video.pause();
shared.video.srcObject = null;
officeScreenPanel?.update(office?.listMediaSurfaces() ?? []);
renderOfficeBadge();
}
function disposeOfficeScreenUi(): void {
stopLocalScreenShare();
releaseRemoteViewer("off", "dispose");
officeScreenPanel?.dispose();
officeScreenPanel = null;
if (screensOverlay) screensOverlay.hidden = true;
}
async function startLocalScreenShare(surface: MediaSurfaceDescriptor): Promise<void> {
if (!office || !inside || office.depth !== "full") return;
// One authored surface role at a time. A viewer texture must not survive
// behind a new presenter preview or be cleared later over the presenter.
releaseRemoteViewer("off");
if (!navigator.mediaDevices?.getDisplayMedia) {
showDetail("This browser does not provide tab or window sharing.");
return;
}
let pendingStream: MediaStream | null = null;
let pendingVideo: HTMLVideoElement | null = null;
let pendingTexture: VideoTexture | null = null;
try {
showDetail("Choose a tab or window. Nothing is captured until you approve the browser prompt.");
const stream = await navigator.mediaDevices.getDisplayMedia({
video: {
displaySurface: "browser",
width: { ideal: 1_280, max: 1_280 },
height: { ideal: 720, max: 720 },
frameRate: { ideal: 15, max: 15 },
},
audio: false,
// Chromium honours these as chooser preferences. Other browsers ignore
// unknown dictionary members and still require the same explicit prompt.
monitorTypeSurfaces: "exclude",
selfBrowserSurface: "exclude",
surfaceSwitching: "include",
} as DisplayMediaStreamOptions);
pendingStream = stream;
const video = document.createElement("video");
pendingVideo = video;
video.muted = true;
video.playsInline = true;
video.srcObject = stream;
await video.play();
const texture = new VideoTexture(video);
pendingTexture = texture;
texture.colorSpace = SRGBColorSpace;
texture.generateMipmaps = false;
stopLocalScreenShare();
if (!office?.bindMediaSurface(surface.screenId, { canView: true, optedIn: true }, texture)) {
for (const track of stream.getTracks()) track.stop();
texture.dispose();
video.pause();
video.srcObject = null;
showDetail("That screen is no longer available.");
return;
}
const shared = { screenId: surface.screenId, stream, video, texture, remote: null };
sharedScreen = shared;
pendingStream = null;
pendingVideo = null;
pendingTexture = null;
stream.getVideoTracks()[0]?.addEventListener("ended", () => stopLocalScreenShare(), { once: true });
officeScreenPanel?.update(office.listMediaSurfaces());
renderOfficeBadge();
showDetail(`Sharing locally to ${surface.screenId}. Use Office screens to stop.`);
// Anonymous/self-host-only behavior ends here exactly as before. The
// transport chunk is fetched only for a signed-in explicit share.
if (access.subject !== null) void startRemotePresenter(surface, shared);
} catch (error) {
for (const track of pendingStream?.getTracks() ?? []) track.stop();
pendingTexture?.dispose();
if (pendingVideo) {
pendingVideo.pause();
pendingVideo.srcObject = null;
}
if ((error as DOMException)?.name !== "NotAllowedError") {
showDetail("The screen preview could not start. Try again from Office screens.");
}
}
}
function ensureOfficeScreenPanel(): OfficeScreenPanel | null {
if (!screensOverlay || !inside || !office || office.depth !== "full") return null;
if (officeScreenPanel) {
officeScreenPanel.update(office.listMediaSurfaces());
return officeScreenPanel;
}
officeScreenPanel = createOfficeScreenPanel({
container: screensOverlay,
surfaces: office.listMediaSurfaces(),
onRequestShare: (surface) => { void startLocalScreenShare(surface); },
onSelect(surface) {
if ((remoteViewedScreen && remoteViewedScreen.screenId !== surface.screenId) ||
(remoteViewerRequestScreenId !== null && remoteViewerRequestScreenId !== surface.screenId)) {
releaseRemoteViewer("off");
}
},
onStopShare: (surface) => {
if (sharedScreen?.screenId === surface.screenId) stopLocalScreenShare();
if (remoteViewedScreen?.screenId === surface.screenId || remoteViewerRequestScreenId === surface.screenId) {
releaseRemoteViewer("stopped");
}
},
onViewerOptIn(surface, optedIn) {
if (!optedIn) {
if (remoteViewedScreen?.screenId === surface.screenId || remoteViewerRequestScreenId === surface.screenId) {
releaseRemoteViewer("off");
} else {
remotePanelStatus(surface.screenId, "off");
}
} else if (access.subject !== null) {
void startRemoteViewer(surface);
}
},
});
const syncOverlay = () => queueMicrotask(() => {
if (officeScreenPanel && !officeScreenPanel.state().open) screensOverlay.hidden = true;
});
officeScreenPanel.root.addEventListener("click", syncOverlay);
officeScreenPanel.root.addEventListener("keydown", (event) => {
event.stopPropagation();
syncOverlay();
});
return officeScreenPanel;
}
function openOfficeScreens(): void {
const panel = ensureOfficeScreenPanel();
if (!panel || !screensOverlay) return;
screensOverlay.hidden = false;
panel.open();
}
screensButton?.addEventListener("click", openOfficeScreens);
screensOverlay?.addEventListener("click", (event) => {
if (event.target !== screensOverlay) return;
officeScreenPanel?.close();
screensOverlay.hidden = true;
});
// ---- Navigation -------------------------------------------------------------
/** The views on offer right now — city chapters, or office viewpoints inside. */
function currentViews(): View[] {
if (inside && office) return office.views;
return city?.chapters ?? [];
}
function flyToIndex(index: number) {
const view = currentViews()[index];
if (!view) return;
if (inside && office) office.flyTo(view.id);
else {
const destination = cityId === "california" ? CALIFORNIA_DESTINATIONS.get(view.id) : undefined;
if (destination) {
officeId = destination.officeId;
journeyToCity(destination.cityId === "socal" ? "socal" : "bay-area");
switchCity(destination.cityId);
return;
}
if (view.id === "la-sf-us-101" || view.id === "la-sf-i-5") {
dispatchJourney({ type: "set-mode", mode: "play" });
dispatchJourney({ type: "select-route", routeId: view.id, direction: 1 });
dispatchJourney({ type: "enter-vehicle", vehicleId: "model-x-black" });
}
city?.flyTo(view.id);
}
}
/**
* Step out of the office to the city, by whichever route keeps the URL honest.
*
* Returns true when it has started a navigation and the caller should stop — the
* page is on its way out, and anything after this runs against a document that
* is about to be replaced.
*
* The `?view=office` case is the cheap half of the same fix. There is no other
* host to go to, so the scene swap stands, but the query that put us in the
* office is dropped from the address bar on the way out. `replaceState` rather
* than `pushState`: the office and the city are one page, and a back button that
* walks a history of scene swaps is a back button that does not leave.
*/
function leaveToCity(cityWanted?: string): boolean {
const url = cityDoorUrl(cityWanted);
if (url !== null) {
location.assign(url);
return true;
}
const here = new URL(location.href);
if (here.searchParams.get("view") === "office") {
here.searchParams.delete("view");
history.replaceState(null, "", `${here.pathname}${here.search}${here.hash}`);
}
return false;
}
function switchCity(id: string) {
if (inside && leaveToCity(id)) return;
if (inside) leaveOffice();
if (id === wantedCity) return;
// Arriving at a detailed board should put its local front door under the
// existing Office button. The California overview keeps whichever building
// the traveller last visited; it is a scale, not a fourth office location.
if (id === "socal") officeId = "mateo-court";
else if (id === "sf" && officeId === "mateo-court") officeId = "lumbridge-hq";
if (id === "california") {
if (journey.vehicle) dispatchJourney({ type: "exit-vehicle" });
if (journey.location.scale === "office") dispatchJourney({ type: "leave-office" });
if (journey.location.scale !== "california") dispatchJourney({ type: "return-to-california" });
} else {
journeyToCity(id === "socal" ? "socal" : "bay-area");
}
wantedCity = id;
const label = CITIES.find((c) => c.id === id)?.label ?? id;
void building(`Building ${label}`, () => mountCity(id));
}
function stepCity(delta: number) {
const at = CITIES.findIndex((c) => c.id === wantedCity);
const next = CITIES[(at + delta + CITIES.length) % CITIES.length];
if (next) switchCity(next.id);
}
/**
* Guards the door against the second click.
*
* Entering now begins with a network fetch for the Spaces chunk, so the window
* between the click and the room is wide enough to click in again — and two
* `enterOffice()` calls in that window build two office scenes, park the second
* on the stage and leak the first, textures and all. One flag, cleared in a
* `finally` so a failed fetch does not wedge the door shut.
*/
let entering = false;
async function toggleOffice() {
if (inside) {
if (leaveToCity()) return;
leaveOffice();
return;
}
if (entering) return;
// Only the first entry fetches or builds anything; after that the office is
// parked in memory next to the paused city and the swap is a pointer.
if (office) {
void enterOffice();
return;
}
entering = true;
/**
* Say so on the button before anything else happens.
*
* The boot card comes up too, but it comes up on the *next* frame at the
* earliest, and on a slow connection the chunk is the long pole rather than
* the build. A door that does nothing visible for half a second gets clicked
* again; a door that says "Opening…" gets waited for.
*/
if (enterButton) {
enterButton.textContent = "Opening the office…";
enterButton.setAttribute("aria-busy", "true");
}
try {
await building("Fetching the office…", () => enterOffice());
} finally {
entering = false;
enterButton?.removeAttribute("aria-busy");
// `renderLegend` writes the real label whichever way it went — "← Back to
// the city" if we are in, the door again if the fetch failed.
renderLegend();
}
}
enterButton?.addEventListener("click", () => void toggleOffice());
/** Switch between the authored dollhouse camera and the local possessed actor. */
function toggleOfficeWalk(): boolean {
if (!inside) {
if (!city?.actorState()) return false;
const active = !city.actorActive();
city.setActorActive(active);
if (!active) city.setActorActions({});
if (active && window.innerWidth <= 600) {
panelOpen = false;
applyPanel();
}
renderLegend();
return true;
}
const walker = office?.walker;
if (!walker) return false;
const active = !walker.active();
walker.setActive(active);
if (!active) walker.setAction({ x: 0, z: 0 });
if (active && window.innerWidth <= 600) {
panelOpen = false;
applyPanel();
}
renderLegend();
return true;
}
walkButton?.addEventListener("click", () => toggleOfficeWalk());
function toggleAircraft(): boolean {
if (inside || cityId !== "california" || !city?.aircraftState()) return false;
const active = !city.aircraftActive();
city.setAircraftActive(active);
if (!active) city.setAircraftActions({});
if (active && window.innerWidth <= 600) {
panelOpen = false;
applyPanel();
}
renderLegend();
return true;
}
flyButton?.addEventListener("click", () => toggleAircraft());
/**
* 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 || entering) return;
entering = true;
officeId = id;
void building(`Opening ${marker.label}`, () => enterOffice()).finally(() => {
entering = false;
renderLegend();
});
});
// ---- Panels, plan and overlays ----------------------------------------------
/**
* Two pieces of chrome are a *user* decision rather than a media query, and the
* distinction matters: a media query that hides the plan below 600px also makes
* `M` do nothing there, which is the width where a plan view is most useful and
* least affordable. So the width only seeds the initial state, and the moment
* someone presses the key the viewport stops having an opinion.
*/
let panelOpen = window.innerWidth > 900;
let planOpen = window.innerWidth > 600;
let planChosen = false;
function applyPanel() {
document.body.classList.toggle("panel-closed", !panelOpen);
panelToggle?.setAttribute("aria-expanded", String(panelOpen));
}
function applyPlan() {
document.body.classList.toggle("minimap-off", !planOpen);
planToggle?.setAttribute("aria-pressed", String(planOpen));
}
/**
* One body, two ways in, and the second one is the point.
*
* This lived inline in the `M` branch of the keydown handler and was reachable
* from nowhere else, which made the plan view **unreachable on any touch
* device**: `planOpen` is seeded `window.innerWidth > 600`, so a phone starts
* with it off, and a phone has no `M`. Every visible control at 390px was
* enumerated and none of them could turn it on. `index.html` has carried a
* designed phone layout for `.corner` — a bottom sheet above the rail, at
* `min(38dvh, 18rem)` — that no visitor to that layout could ever see, under a
* comment saying it "costs nothing until it is asked for". There was no way to
* ask. `#plan-toggle` is that way, shown wherever the pointer is coarse.
*
* So the key and the button call this, and `planChosen` is set by both for the
* same reason it always was: once somebody has an opinion, the viewport stops
* having one.
*/
function togglePlan() {
planOpen = !planOpen;
planChosen = true;
applyPlan();
}
planToggle?.addEventListener("click", () => togglePlan());
panelToggle?.addEventListener("click", () => {
panelOpen = !panelOpen;
applyPanel();
});
/**
* The scrim behind the phone's panel sheet. It is `display: none` above 600px,
* so this listener is only ever reachable where the sheet exists.
*/
document.querySelector<HTMLElement>("#scrim")?.addEventListener("click", () => {
panelOpen = false;
applyPanel();
});
document.querySelector<HTMLElement>("#detail-close")?.addEventListener("click", () => {
showDetail(null);
});
window.addEventListener("resize", () => {
if (!planChosen) {
planOpen = window.innerWidth > 600;
applyPlan();
}
});
function openShortcuts() {
if (!shortcutsCard || !shortcutsCard.hidden) return;
shortcutsCard.hidden = false;
document.querySelector<HTMLButtonElement>("#shortcuts-close")?.focus();
}
function closeShortcuts() {
if (!shortcutsCard || shortcutsCard.hidden) return;
shortcutsCard.hidden = true;
helpButton?.focus();
}
helpButton?.addEventListener("click", () => openShortcuts());
document.querySelector<HTMLElement>("#shortcuts-close")?.addEventListener("click", closeShortcuts);
shortcutsCard?.addEventListener("click", (event) => {
// The backdrop, not the sheet. Clicking the card itself must not close it.
if (event.target === shortcutsCard) closeShortcuts();
});
/** Held keyboard state translated into the same snapshot a gamepad/touch UI uses. */
const heldDriveKeys = new Set<string>();
function routeDriveIsActive(): boolean {
const state = !inside ? city?.vehicleState() : null;
return state !== null && state !== undefined && !city?.actorActive() &&
!city?.aircraftActive() && city?.current() === state.routeId;
}
function publishVehicleActions(
supplement: Partial<VehicleActionSnapshot> = {},
): boolean {
if (!routeDriveIsActive() || !city) return false;
const left = heldDriveKeys.has("a");
const right = heldDriveKeys.has("d");
city.setVehicleActions(
mergeVehicleActions(
{
throttle: heldDriveKeys.has("w") ? 1 : 0,
brake: heldDriveKeys.has("s") ? 1 : 0,
steering: (right ? 1 : 0) - (left ? 1 : 0),
handbrake: heldDriveKeys.has(" "),
},
supplement,
),
);
return true;
}
function publishOfficeWalkActions(): boolean {
const walker = inside ? office?.walker : null;
if (!walker?.active()) return false;
walker.setAction({
x: (heldDriveKeys.has("d") ? 1 : 0) - (heldDriveKeys.has("a") ? 1 : 0),
z: (heldDriveKeys.has("s") ? 1 : 0) - (heldDriveKeys.has("w") ? 1 : 0),
});
return true;
}
function publishCityActorActions(): boolean {
if (inside || !city?.actorActive()) return false;
city.setActorActions({
forward: (heldDriveKeys.has("w") ? 1 : 0) - (heldDriveKeys.has("s") ? 1 : 0),
right: (heldDriveKeys.has("d") ? 1 : 0) - (heldDriveKeys.has("a") ? 1 : 0),
turn: (heldDriveKeys.has("d") ? 1 : 0) - (heldDriveKeys.has("a") ? 1 : 0),
sprint: heldDriveKeys.has(" "),
climb: (heldDriveKeys.has("e") || heldDriveKeys.has(" ") ? 1 : 0) -
(heldDriveKeys.has("q") ? 1 : 0),
glide: heldDriveKeys.has("g"),
});
return true;
}
function publishAircraftActions(
supplement: Partial<AircraftActionSnapshot> = {},
): boolean {
if (inside || !city?.aircraftActive()) return false;
city.setAircraftActions({
throttle: heldDriveKeys.has(" ") ? 1 : 0,
pitch: (heldDriveKeys.has("w") ? 1 : 0) - (heldDriveKeys.has("s") ? 1 : 0),
roll: (heldDriveKeys.has("d") ? 1 : 0) - (heldDriveKeys.has("a") ? 1 : 0),
yaw: (heldDriveKeys.has("e") ? 1 : 0) - (heldDriveKeys.has("q") ? 1 : 0),
modeRequest: supplement.modeRequest ?? "none",
reset: supplement.reset ?? false,
});
return true;
}
function toggleVehicleCamera(): boolean {
if (!routeDriveIsActive() || !city) return false;
city.setVehicleCamera(city.vehicleCamera() === "driver" ? "chase" : "driver");
return true;
}
for (const button of driveControls?.querySelectorAll<HTMLButtonElement>("[data-drive-key]") ?? []) {
const key = button.dataset.driveKey;
if (key === undefined) continue;
const release = (event: PointerEvent) => {
heldDriveKeys.delete(key);
button.setAttribute("aria-pressed", "false");
publishVehicleActions();
event.preventDefault();
};
button.addEventListener("pointerdown", (event) => {
button.setPointerCapture(event.pointerId);
heldDriveKeys.add(key);
button.setAttribute("aria-pressed", "true");
publishVehicleActions();
event.preventDefault();
});
button.addEventListener("pointerup", release);
button.addEventListener("pointercancel", release);
button.addEventListener("lostpointercapture", release);
}
for (const button of walkControls?.querySelectorAll<HTMLButtonElement>("[data-walk-key]") ?? []) {
const key = button.dataset.walkKey;
if (key === undefined) continue;
const release = (event: PointerEvent) => {
heldDriveKeys.delete(key);
button.setAttribute("aria-pressed", "false");
publishOfficeWalkActions();
publishAircraftActions();
publishCityActorActions();
event.preventDefault();
};
button.addEventListener("pointerdown", (event) => {
button.setPointerCapture(event.pointerId);
heldDriveKeys.add(key);
button.setAttribute("aria-pressed", "true");
publishOfficeWalkActions();
publishAircraftActions();
publishCityActorActions();
event.preventDefault();
});
button.addEventListener("pointerup", release);
button.addEventListener("pointercancel", release);
button.addEventListener("lostpointercapture", release);
}
driveControls?.querySelector<HTMLButtonElement>("[data-drive-action='assist']")
?.addEventListener("click", () => publishVehicleActions({ modeRequest: "assisted" }));
driveControls?.querySelector<HTMLButtonElement>("[data-drive-action='reset']")
?.addEventListener("click", () => publishVehicleActions({ reset: true }));
driveControls?.querySelector<HTMLButtonElement>("[data-drive-action='camera']")
?.addEventListener("click", () => toggleVehicleCamera());
walkControls?.querySelector<HTMLButtonElement>("[data-aircraft-action='assist']")
?.addEventListener("click", () => publishAircraftActions({ modeRequest: "assisted" }));
walkControls?.querySelector<HTMLButtonElement>("[data-aircraft-action='reset']")
?.addEventListener("click", () => publishAircraftActions({ reset: true }));
window.addEventListener("keyup", (event) => {
const key = event.key.length === 1 ? event.key.toLowerCase() : event.key;
if (!heldDriveKeys.delete(key)) return;
if (
publishVehicleActions() || publishOfficeWalkActions() ||
publishAircraftActions() || publishCityActorActions()
) event.preventDefault();
});
window.addEventListener("blur", () => {
heldDriveKeys.clear();
publishVehicleActions();
publishOfficeWalkActions();
publishAircraftActions();
publishCityActorActions();
});
let gamepadButtons: GamepadButtonState = { assist: false, reset: false };
function pollDriveGamepad() {
try {
const pad = navigator.getGamepads?.().find((candidate) => candidate !== null);
if (pad && routeDriveIsActive()) {
const sample = sampleStandardGamepad(pad, gamepadButtons);
gamepadButtons = sample.buttons;
publishVehicleActions(sample.actions);
} else {
gamepadButtons = { assist: false, reset: false };
}
} catch {
// Some privacy-hardened browsers expose the method but throw until a pad
// has produced a trusted event. Keyboard/touch remain fully functional.
}
requestAnimationFrame(pollDriveGamepad);
}
requestAnimationFrame(pollDriveGamepad);
/**
* Keyboard access to everything the mouse can reach.
*
* Bound to `window` rather than to the canvas, because the canvas is only
* focusable by accident and a shortcut that stops working when you tab to the
* legend is worse than no shortcut. The guard is the usual one: a keystroke
* that lands in a text field or on the plan view's own arrow-key handler
* belongs to that control, not to this.
*/
window.addEventListener("keydown", (event) => {
if (event.metaKey || event.ctrlKey || event.altKey) return;
const target = event.target;
if (
target instanceof HTMLInputElement ||
target instanceof HTMLSelectElement ||
target instanceof HTMLTextAreaElement ||
(target instanceof HTMLElement && target.isContentEditable)
) {
return;
}
if (event.key === "Escape") {
if (shortcutsCard && !shortcutsCard.hidden) closeShortcuts();
else if (inside) {
if (!leaveToCity()) leaveOffice();
}
else showDetail(null);
return;
}
if (event.key === "?") {
if (shortcutsCard && !shortcutsCard.hidden) closeShortcuts();
else openShortcuts();
event.preventDefault();
return;
}
if (event.key >= "1" && event.key <= "9") {
flyToIndex(Number(event.key) - 1);
return;
}
if (event.key === "[") {
stepCity(-1);
return;
}
if (event.key === "]") {
stepCity(1);
return;
}
const lower = event.key.toLowerCase();
if (
lower === "w" || lower === "a" || lower === "s" || lower === "d" ||
lower === "q" || lower === "e" || lower === "g" || event.key === " "
) {
heldDriveKeys.add(event.key === " " ? " " : lower);
if (publishVehicleActions()) {
event.preventDefault();
return;
}
if (publishOfficeWalkActions()) {
event.preventDefault();
return;
}
if (publishAircraftActions()) {
event.preventDefault();
return;
}
if (publishCityActorActions()) {
event.preventDefault();
return;
}
}
if (lower === "p" && publishVehicleActions({ modeRequest: "assisted" })) {
event.preventDefault();
return;
}
if (lower === "p" && publishAircraftActions({ modeRequest: "assisted" })) {
event.preventDefault();
return;
}
if (lower === "r" && publishVehicleActions({ reset: true })) {
event.preventDefault();
return;
}
if (lower === "r" && publishAircraftActions({ reset: true })) {
event.preventDefault();
return;
}
if (lower === "c" && toggleVehicleCamera()) {
event.preventDefault();
return;
}
if (lower === "v" && toggleOfficeWalk()) {
event.preventDefault();
return;
}
if (lower === "m") {
togglePlan();
return;
}
if (lower === "o") void toggleOffice();
});
// ---- Time -------------------------------------------------------------------
/**
* The `#hour` slider and its `now` button are gone, replaced rather than kept.
*
* They were a second writer for one override, and the weaker of the two:
* `capabilitiesFor` hands `timeControl` and `debug` to exactly the same tier, so
* there was never an audience for the simple case — the only person who could
* see the scrubber was the same person who can open the godmode panel. Keeping
* both meant the slider wrote an hour onto *today* and silently discarded
* whatever date the panel had set, which is a bug with no upside.
*
* `#clock` stays exactly as it was, and stays visible to everyone: a map that
* will not say what time it is showing is worse than one you cannot scrub.
*
* What is left of the gate is one line, and it is belt and braces — the only
* writer of `instantOverride` is the panel, and the panel is not constructed
* unless `can.debug`. It stays because "no control" and "no override" are two
* different facts, and the second is the one the renderer depends on.
*/
function applyTimeControl() {
if (!access.can.timeControl) instantOverride = null;
}
// ---- Instruments ------------------------------------------------------------
/**
* The godmode panel and the pose editor, for the one visitor in a deployment
* who has them.
*
* **Constructed, not hidden.** Everything in this section is behind
* `access.can.debug`, and for a member or an anonymous visitor the result is
* not a panel with `display: none` on it — it is no element, no `<style>`, no
* key binding, and no bytes: `src/tools/` is reached only through the
* `await import()` below, so a non-god browser never fetches the chunk. A
* hidden instrument is still an instrument you shipped to a stranger, and the
* tier that gets these is the tier that can already read the source.
*
* That rule is also why the dock's geometry is set as element styles instead of
* a rule in `index.html`. Every visitor downloads that stylesheet; a
* `#pose-dock { … }` sitting in it that can never match is the one trace this
* arrangement would otherwise leave behind, in the bytes if not in the DOM.
*/
/** Cached across boards: the module is fetched once, the tool is rebuilt per city. */
let poseEditorFactory: typeof import("./tools/poseEditor.ts").createPoseEditor | null = null;
let poseDockBody: HTMLElement | null = null;
let poseDockOpen = true;
/**
* Where the panel thinks it is standing.
*
* `null` while there is no board — between a `dispose()` and the next
* `createScene`, which is now an `await` wide. The stage travels with the place
* even though there is only ever one of them, because what the panel is being
* told is *which board these counters are about*: `label`, the centre, the
* scale and the renderer's ledger are one reading, and handing the panel the
* stage separately would let it print SoCal's draw calls under the Bay Area's
* name for the length of a switch.
*
* `marineStrength` is deliberately not wired. It is private to `atmosphere.ts`
* today, and the panel prints "no hook wired" rather than a guess — see the
* handoff note; one `export` keyword upstream turns the readout on.
*/
function godmodePlace(): GodmodePlace | null {
if (!city) return null;
const entry = CITIES.find((c) => c.id === cityId);
const active = entry?.city ?? SAN_FRANCISCO;
const world = city.world;
const label = entry?.label ?? cityId;
return {
// The office is a different board for accounting: it is the ledger key, and
// its draw calls are not the city's.
label: inside ? `${label} · office` : label,
lat: active.center.lat,
lng: active.center.lng,
stage: city.stage,
metresPerUnit: world.metresPerUnit,
unproject: (x, z) => world.unproject(x, z),
};
}
/** `setPlace` when there is a place. Called on every board and room change. */
function refreshGodmodePlace() {
const place = godmodePlace();
if (place) godmode?.setPlace(place);
}
async function mountGodmode() {
if (!access.can.debug || godmode) return;
const place = godmodePlace();
if (!place) return;
const [tools, poses] = await Promise.all([
import("./tools/index.ts"),
import("./tools/poseEditor.ts"),
]);
poseEditorFactory = poses.createPoseEditor;
godmode = tools.createGodmode({
container: document.body,
initial: { instant: currentInstant(), place },
onTimeChange(instant) {
instantOverride = instant;
updateSun();
},
onWeatherOverride(w) {
weatherOverride = w;
updateSun();
// The corner label has to stop claiming live weather the moment the sky
// on screen is one somebody typed.
renderSource();
},
/**
* Both dials read through the module-level handles rather than closing over
* a board, because the panel outlives the city: it is mounted once and every
* later `mountCity` swaps `city` and `trafficDial` underneath it. A closure
* 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);
},
onSatellitesVisible(visible) {
satellitesVisible = visible;
city?.setSatellitesVisible(visible);
},
read() {
const counts = city?.satelliteCounts();
return {
extraTraffic: trafficDial?.extra() ?? 0,
trafficIsLive: cityFlights?.live() ?? false,
// `total: 0` is a board with no catalogue, which is not the same as a
// catalogue with nothing above the horizon — the panel says so.
satellites: counts && counts.total > 0 ? counts : null,
};
},
},
});
// The city was built before this chunk arrived, so its pose editor is built
// here; every later board gets one from `mountCity`.
if (city) mountPoseEditor(city);
addGodmodeShortcut();
}
/**
* The pose editor, rebuilt with the board.
*
* It holds a `World`, a camera and a controls, all three of which die with the
* city — the same reason the plan view is rebuilt rather than re-pointed.
*/
function mountPoseEditor(handle: SceneHandle) {
if (!access.can.debug || !poseEditorFactory) return;
const dock = instrumentDock();
poseEditor = poseEditorFactory({
container: dock,
allowed: access.can.debug,
world: handle.world,
camera: handle.stageScene.camera,
controls: handle.stageScene.controls,
/**
* A cut rather than a flight, for now.
*
* The tool is forbidden from writing the camera itself and asks for
* `SceneKit.flyTo`, which `SceneHandle` does not expose — it offers
* `flyTo(chapterId)` and nothing that takes a pose. Moving both ends of the
* orbit at once is exactly what the plan view's `onSeek` does a few hundred
* lines up and it lands on the right pose; what it does not do is ease, so
* re-flying a captured chapter jumps. `scene.ts` is another agent's file
* this week; the two-line addition that upgrades this is in the handoff.
*/
flyTo: (pose: Pose) => {
handle.stageScene.camera.position.copy(pose.position);
handle.stageScene.controls.target.copy(pose.target);
handle.stageScene.controls.update();
},
existingChapters: handle.chapters,
});
poseEditor.setVisible(poseDockOpen);
}
/**
* The dock, built once and reused across boards.
*
* Bottom right, above the key-hint rail and below the plan view, which is the
* one column of this layout with room in it: the left panel owns the left edge
* top to bottom, the godmode drawer opens bottom *centre*, and the plan view
* stops around 20rem down. The height is capped against both neighbours rather
* than at a flat `dvh`, so a short window shrinks the dock instead of sliding
* it under the plan view.
*/
function instrumentDock(): HTMLElement {
if (poseDockBody) return poseDockBody;
const dock = document.createElement("aside");
dock.id = "pose-dock";
dock.setAttribute("aria-label", "Pose editor");
Object.assign(dock.style, {
position: "fixed",
right: "var(--s4, 16px)",
bottom: "calc(var(--s4, 16px) + 5rem)",
zIndex: "5",
width: "min(20rem, calc(100vw - var(--s4, 16px) * 2))",
maxHeight: "min(52dvh, calc(100dvh - 26rem))",
display: "flex",
flexDirection: "column",
alignItems: "stretch",
gap: "var(--s1, 4px)",
});
const toggle = document.createElement("button");
toggle.type = "button";
toggle.className = "help";
toggle.setAttribute("aria-expanded", "true");
Object.assign(toggle.style, { alignSelf: "flex-end", fontFamily: "inherit" });
const body = document.createElement("div");
Object.assign(body.style, { flex: "1", minHeight: "0", display: "flex" });
function applyDock() {
toggle.textContent = poseDockOpen ? "poses ▾" : "poses ▴";
toggle.setAttribute("aria-expanded", String(poseDockOpen));
body.hidden = !poseDockOpen;
// Collapsed is genuinely idle: `setVisible(false)` is what stops the tool
// reading the camera on every frame of a map nobody is authoring against.
poseEditor?.setVisible(poseDockOpen);
}
toggle.addEventListener("click", () => {
poseDockOpen = !poseDockOpen;
applyDock();
});
dock.append(toggle, body);
document.body.append(dock);
poseDockBody = body;
applyDock();
return body;
}
/**
* `G` in the `?` card, added from here rather than typed into `index.html`.
*
* The card is the one place a visitor goes to find out what this page can do,
* so a key that exists must be in it — and a key that does not exist must not.
* Hard-coding the row would advertise an instrument nine visitors in ten have
* no way to open.
*/
function addGodmodeShortcut() {
const keys = document.querySelector<HTMLElement>("#shortcuts .keys");
const before = document.querySelector<HTMLElement>("#key-overlays");
if (!keys || document.querySelector("#key-godmode")) return;
const dt = document.createElement("dt");
dt.id = "key-godmode";
dt.innerHTML = "<kbd>G</kbd>";
const dd = document.createElement("dd");
dd.textContent = "Godmode: the clock, the weather and the counters";
keys.insertBefore(dt, before);
keys.insertBefore(dd, before);
}
// ---- The boot card ----------------------------------------------------------
const bootCard = document.querySelector<HTMLElement>("#boot");
const bootStep = document.querySelector<HTMLElement>("#boot-step");
/**
* Wait until the browser has actually put pixels on the glass.
*
* Writing to `textContent` and then immediately building a heightfield paints
* nothing: the style change and the two seconds of synchronous work are in the
* same task, so the frame the user sees is the one *after* the work. Two
* `requestAnimationFrame`s straddle a paint, which is the whole trick — a
* single one still runs before it.
*/
function painted(): Promise<void> {
return new Promise((resolve) => {
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
});
}
/**
* Wait until the renderer has actually drawn the thing that was being built.
*
* `painted()` is not enough any more and the difference is visible. The
* heightfield now builds in a Worker, so `createScene` resolves at a moment
* when the main thread is *idle* and two animation frames go by in 33 ms —
* long before the terrain mesh, the 140k building instances and the first
* WebGL draw have happened. The card faded on an empty canvas.
*
* `renderer.info.render.frame` is the renderer counting its own draws, which is
* the only witness that cannot be fooled by a fast frame: it advances exactly
* once per `render()`, so an increment means a frame of the new scene has been
* submitted, and the extra `painted()` after it puts that frame on the glass
* before the fade starts.
*
* The timeout is not a fallback to a timer, it is a bound on a promise that
* would otherwise never settle — an abandoned build never draws anything, and
* the boot card must not be the thing that outlives it.
*/
function drawn(timeoutMs = 4000): Promise<void> {
const from = stage.renderer.info.render.frame;
const deadline = performance.now() + timeoutMs;
return new Promise((resolve) => {
requestAnimationFrame(function wait() {
if (stage.renderer.info.render.frame > from || performance.now() > deadline) {
void painted().then(resolve);
return;
}
requestAnimationFrame(wait);
});
});
}
/**
* Which `building()` call the boot card belongs to.
*
* A generation counter rather than a boolean because two builds can overlap:
* clicking SoCal while the Bay Area is still building leaves the first
* `building()` running, and it must not fade a card the second one is using.
* Last one in owns it.
*/
let bootGeneration = 0;
/**
* Run something slow with the boot card up and a sentence saying what it is.
*
* Naming the work is most of the value: a blank page for two seconds reads as
* broken, and "Building the Bay Area…" for two seconds reads as busy. The
* percentage on top of it comes from `bootProgress`, which the heightfield
* drives directly from the Worker.
*
* `work` may be synchronous or not. It became "or not" when `createScene` did,
* and the flattening matters: without the `await` here the card faded about
* 800 ms in, while the city was still building, because a promise is a truthy
* value that returns instantly.
*/
async function building<T>(label: string, work: () => T | Promise<T>): Promise<T> {
const generation = ++bootGeneration;
if (bootStep) bootStep.textContent = label;
if (bootCard) {
bootCard.hidden = false;
bootCard.classList.remove("done");
}
await painted();
const result = await work();
await drawn();
// Superseded while we were building: the card belongs to a later call now and
// fading it would uncover a city that does not exist yet.
if (generation !== bootGeneration) return result;
bootCard?.classList.add("done");
window.setTimeout(() => {
if (bootCard?.classList.contains("done")) bootCard.hidden = true;
}, 300);
return result;
}
/**
* The heightfield's progress, on the card that is already on screen.
*
* Nothing is written while the build is on the main thread, and that is not an
* oversight: `onMainThread` means the page is frozen for the duration, so every
* one of these writes would land in the same task and exactly one of them —
* the last — would ever be seen. The fallback build is the case where the
* static label is all the honesty available.
*
* The caller is responsible for not reporting an abandoned build; see the
* `onProgress` passed by `mountCity`.
*/
function bootProgress(label: string, fraction: number, onMainThread: boolean) {
if (!bootStep || onMainThread || bootCard?.hidden !== false) return;
bootStep.textContent = `Building ${label}… terrain ${Math.round(fraction * 100)}%`;
}
// ---- Boot -----------------------------------------------------------------
/**
* Access first, then data, then the board.
*
* The order is load-bearing in both directions and it used to be wrong. Markers
* were fetched before the tier was known, which is a request an anonymous
* visitor should not be making; and both decisions have to be settled before
* the *first* `mountCity`, because `markerPalette` is fixed at scene
* construction — the sample palette's keys are not the API's — and the flight
* source is chosen in the same call.
*
* Everything about the API remains optional. No server means the bundled sample
* set, the simulated traffic, and a label at the bottom of the screen saying
* which of the two you are looking at.
*/
async function boot() {
applyPanel();
applyPlan();
if (bootStep) bootStep.textContent = "Asking the deployment who you are…";
access = await resolveAccess();
if (access.subject !== null) {
const loadedProfile = loadLocalProfile(sessionStorage, LOCAL_PROFILE_KEY);
localProfile = loadedProfile.status === "loaded"
? loadedProfile.profile
: createDefaultLocalProfile(access.subject, access.subject);
if (loadedProfile.status !== "loaded" || loadedProfile.migrated) {
saveLocalProfile(sessionStorage, LOCAL_PROFILE_KEY, localProfile);
}
} else {
localProfile = null;
}
dispatchJourney({
type: "sign-in-actor-swap",
actor: access.subject === null
? {
id: "anonymous",
kind: "crow",
signedIn: false,
profile: { displayName: "Guest" },
}
: {
id: access.subject,
kind: "humanoid",
signedIn: true,
profile: {
displayName: localProfile?.displayName ?? access.subject,
color: humanoidAppearance()?.accentColor,
},
},
});
applyTimeControl();
renderTierBadge();
// Both gates, for the reason `mountCity` gives at length: the tier says
// whether this visitor may ask, `feeds` says whether there is anything to
// ask. A box with `markers: "none"` serves the public empty body to everyone,
// so the request buys a round trip and lands on the same sample set.
if (access.can.liveMarkers && access.feeds?.markers) {
try {
const feed = await tera.markers();
markers = feed.value;
// 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.
}
}
/**
* Which metro to build, from the URL if it names one this build has.
*
* The parameter exists to make leaving the office lossless — `office.` sends
* you to `tera.?city=socal` rather than dropping you in the Bay Area — but it
* is read unconditionally, because a deep link to a city is a reasonable thing
* to want on its own and a parameter that only works when it arrives from one
* particular page is a trap. An unknown id falls back rather than failing:
* `?city=paris` on a build with two cities in it should show a city, not a
* black screen.
*/
const wanted = new URLSearchParams(location.search).get("city");
const first = CITIES.find((c) => c.id === wanted) ?? CITIES[0];
await building(`Building ${first?.label ?? "the city"}`, () =>
mountCity(first?.id ?? "california"),
);
// There must be a mounted scene before remote snapshots have anywhere to go.
// Anonymous and zero-server builds retain their existing local-only path.
// Hosted presence is an enhancement: a failed lazy chunk or unavailable API
// must not stop profile/tools initialization after the world is already live.
await initializeRealtimePresence().catch(() => undefined);
// The instruments, after the first board, because the panel reads a live
// stage and there is not one before this line.
await mountGodmode();
// The `office.` front door. The city is already standing behind this, so the
// back button is a scene swap and not a rebuild.
if (OPENS_IN_OFFICE) await building("Fetching the office…", () => enterOffice());
/**
* The wall clock, once a minute.
*
* Skipped entirely while an override is up — the whole point of an override
* is that the map has stopped following the clock — and the panel is told the
* new instant only when it is not the one choosing it, so its own readouts
* stay pinned to what is being rendered rather than fighting it.
*/
window.setInterval(() => {
if (instantOverride !== null) return;
updateSun();
godmode?.setInstant(new Date());
}, 60_000);
}
void boot();