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
karti db074e9cf7 feat: tone-mapped render rig, studio devices, LA fidelity pass, UI overhaul
The build the studios needed, across eight workstreams and one strict file
partition.

**The render rig was the quality ceiling.** The renderer ran three's
NoToneMapping default while atmosphere drove the sun to 2.35 and assets set
emissives to 3.2, so every value above 1.0 hard-clipped to flat white — which is
why walls blew out and every fitting looked like a white rectangle. ACES filmic
tone mapping and an explicit output colour space land in `stage.ts`, and the
atmosphere intensity table and palette headroom are re-tuned against the new
curve rather than left tuned for the clipping we removed.

`engine/environmentRig.ts` builds a PMREM environment at runtime, procedurally,
so nothing binary is committed. There was no environment map anywhere before, so
every `metalness > 0` role had nothing to reflect and rendered dull grey — a
defect the code already documented against itself in `office/optimus.ts`, where a
whole material role was abandoned over it, and worked around in `modelX.ts` with
a fake emissive that this change deletes. Atmosphere remains the sole light
owner; the rig derives from the `LightingState` it already produced.

**Studio hardware exists.** There was no device concept anywhere in the product:
no type, no route, no state. `devices/types.ts` fixes a declaration/state/
capability/command contract that a smart light, a thermostat, a door sensor and a
charger all fit without a schema change, and both studios now carry a desk mic
and a computer speaker with deterministic simulated behaviour behind an adapter
seam a real API can occupy later. Reads are the demo and are open; commands are a
signed-in action and are kept off the read body entirely, because a shared cache
replaying a GET that turned a microphone on is exactly what the fail-closed
cache default exists to prevent.

**The ADS-B licence hole is closed.** `TERA_ADSB_ENDPOINT` accepted any URL, the
response was served publicly cacheable, and the attribution hardcoded adsb.lol
regardless of where the endpoint pointed — one env var away from republishing
non-redistributable data under an open-terms credit. The host is now allowlisted,
the credit is derived from the host actually configured, public cacheability is
conditional on redistributability, and a refused endpoint demotes to simulated
flights and says so in `degraded[]`. The gate is on the source, not the feature:
live aircraft and their detail cards stay open to anonymous visitors.

**The LA studio was never the smaller pack** — 16 rooms and 248 props against
SF's 4 and 28. Its deficit was fidelity per square metre: 98 of those props were
ceiling troffers, it bound no props to seats, placed none of the habitat kit, and
12 of its 16 rooms had no viewpoint. Density comes from new asset kinds rather
than more instances, because `furnish.ts` draws once per kind and folds colour
into the batch key, so repeat instances add nothing the eye can read.

**The interface stops being forty imperative mutations.** Every visibility
decision moves into a pure, tested `ui/chromeState.ts` and one applier, so the
chrome has coverage for the first time. Deleted: ~100 lines of CSS and two
bindings targeting elements that no longer exist, and a `body:has()` rule that
shifted the desktop layout by 160px for touch controls hidden there. Fixed: the
office picker tabs that drew their label and their badge on top of each other.
Added: a first-run flow, because the product is two verbs and neither was ever
stated on screen. Mobile is designed on its own terms instead of being the
desktop with things hidden — the plan view comes back, and the keyboard-only
shortcuts button is replaced by touch controls.

`arena/studioOps.ts` frames the whole thing as the multi-variable environment it
is, wrapping the same simulators the renderer drives rather than a headless copy.

Also removed `input/vehicle.ts`, which nothing but its own test imported.

Tests 385 -> 961, all passing. Typecheck, build, performance budgets across six
matrix cells, no-binaries, provenance, dependency licences, zero-config boot and
arena source hashes all green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 19:44:24 -07:00

4244 lines
170 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* The demo: two cities under a real sun and moon, and two studios 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, the studio hardware is a
* fixed-step simulator running in this tab. Clone it and it works.
*
* ### What this file is, after the interface moved out
*
* This file used to hold about forty imperative `element.hidden = condition`
* decisions with the condition and the write on the same line, spread through a
* 655-line "Chrome" section, and none of it was testable without a WebGL
* context. That is now two modules: `ui/chromeState.ts` decides and `ui/mount.ts`
* writes, and what is left here is the one call between them —
* `chrome.apply(chromeState(chromeInputs()))` — plus the thing this file is
* actually for, which is **assembly**: it owns the clock, the access tier, which
* board is mounted, which building is open, and which of the delivered modules
* is handed to which scene.
*
* The rule that keeps it that way: nothing below this line may write to a DOM
* node that `mount.ts` owns. Where you want a piece of chrome to change, add a
* field to `ChromeInputs` and call `renderChrome()`.
*/
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 { createEnvironmentRig } from "./engine/environmentRig.ts";
import {
aircraftDetail,
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 { Aircraft, 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 {
aircraftActionsFromPlay,
cameraRelativePlanar,
crowActionsFromPlay,
groundActorActionsFromPlay,
PlayInputRouter,
sampleStandardPlayGamepad,
vehicleActionsFromPlay,
type StandardPlayGamepadButtons,
} from "./input/play.ts";
import {
createTeraClient,
type PresenceWatch,
type TrafficSource,
type WeatherWatch,
} from "./adapters/http.ts";
import {
SAMPLE_MARKERS,
SAMPLE_PALETTE,
SAMPLE_PRESENCE_PALETTE,
samplePresenceForOfficeAt,
sampleRoutesFor,
} from "./adapters/sample.ts";
import { OFFICE_SITES, type ShippedOfficeId } 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,
} from "./aircraft/index.ts";
import {
cityControlMode,
createControlModeState,
transitionControlMode,
type ControlMode,
} from "./play/controlMode.ts";
import {
createOfficeScreenPanel,
type MediaSurfaceDescriptor,
type OfficeScreenPanel,
type OfficeScreenRemoteStatus,
} from "./media/index.ts";
import type { RemoteMediaState, RemoteOfficeMedia } from "./media/remoteMedia.ts";
/**
* The interface, as two modules and one call.
*
* `chromeState` is pure — no DOM, no THREE, no clock — and `mountChrome` is the
* only thing in the product that writes to the page. Both are value imports and
* both belong in the entry chunk: the chrome is on screen before anything else
* is, including the boot card's own successor.
*/
import {
chromeState,
seedPanelOpen,
seedPlanOpen,
type ChromeDetail,
type ChromeInputs,
type ChromeLayout,
} from "./ui/chromeState.ts";
import { mountChrome, type ChromeHandle } from "./ui/mount.ts";
import { controlForKey, edgeForKey } from "./ui/shortcuts.ts";
import { browserStorage, hasSeenOnboarding } from "./ui/onboarding.ts";
import { playHudKindFor, type AircraftDetailInput, type PlayTelemetry } from "./ui/hud.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 sees it, so none of these files is an
* edge in the module graph. The office arrives through `loadOffice()`, the tools
* through the `await import()` in `boot()`. Turning any of them into a value
* import silently undoes the split and nothing fails — the bundle just gets big
* again. The device and vehicle simulators travel with the office for the same
* reason: renderer-free, but with nothing to say until you are in a studio.
*/
import type { OfficeScene } from "./interiors/officeScene.ts";
import type { Office, Presence } from "./interiors/types.ts";
import type { RobotOperationsDefinition } from "./interiors/robotOperations.ts";
import type { MaterialQuality, MaterialRegistry } from "./assets/materials.ts";
import type { DeviceSource } from "./devices/adapter.ts";
import type { DeviceCommand, DeviceDeclaration, DeviceState } from "./devices/types.ts";
import type { SimulatedVehicleTelemetry } from "./transport/vehicleTelemetry.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.
*
* Three, deliberately unalike — a tower above Transbay, a hangar at Alameda
* Point, an Arts District courtyard — because the thing worth showing is that
* one engine and one format render all three, and that `OfficeSite` is what
* makes them feel like different places rather than the same room with
* different furniture.
*
* The loaders stay lazy: a pack eagerly imported would put its furniture in the
* entry bundle for every visitor who never opens that door.
*/
const OFFICE_LOADERS: Readonly<Record<ShippedOfficeId, () => Promise<{ default: Office }>>> = {
"lumbridge-hq": () => import("./offices/lumbridge-hq.ts"),
"frontier-valley": () => import("./offices/frontier-valley.ts"),
"mateo-court": () => import("./offices/mateo-court.ts"),
};
const OFFICE_OPERATION_LOADERS: Readonly<Partial<Record<
ShippedOfficeId,
() => Promise<{ default: RobotOperationsDefinition }>
>>> = {
"lumbridge-hq": async () => ({
default: (await import("./offices/operations/lumbridge-hq.ts")).LUMBRIDGE_HQ_ROBOT_OPERATIONS,
}),
"mateo-court": async () => ({
default: (await import("./offices/operations/mateo-court.ts")).MATEO_COURT_ROBOT_OPERATIONS,
}),
};
const OFFICES = OFFICE_SITES.map((entry) => ({
...entry,
label: entry.name,
load: OFFICE_LOADERS[entry.id],
loadOperations: OFFICE_OPERATION_LOADERS[entry.id],
}));
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 abandoned a renderer on the one GL context this page
* has — and `WebGLRenderer.dispose()` frees no texture at all. `stage.ts` has
* the measurement and the reading of three's source it comes from.
*
* Nothing disposes this: it outlives every city and every office on the page,
* the same arrangement as `officeMaterials` below.
*/
const stage = createStage(canvas);
/**
* One environment map for the page, beside the one renderer, for the same
* reason there is one renderer.
*
* A `PMREMGenerator` compiles three shader programs and each cached environment
* is a half-float render target; both are properties of the GL context, not of
* the board drawn on it. Building a rig inside `createScene` would allocate a
* fresh chain per city and orphan the previous one on every switch, which is
* precisely the arithmetic `stage.ts` records for the renderer itself. So it is
* built here, handed to both scenes, and each scene calls `release()` on its way
* out so the rig's ledger does not retain a floor plate nobody can see.
*
* Nothing about it is eager: it allocates on the first `apply()`, and the two
* kinds — a sky for the city, a room for an office — are cached separately, so a
* page that never opens a door never builds the office blur chain.
*/
const environment = createEnvironmentRig(stage.renderer);
// `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 controlModeState = createControlModeState();
const playInput = new PlayInputRouter();
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.
*
* The one thing that makes Tera and Spaces read as one product rather than two
* views sharing a bundle: both packs carry a real `site`, and until these
* existed that coordinate was known to the lighting and to nothing else.
*
* `OFFICE_SITES` rather than the packs, deliberately — a pack is a lazy chunk
* and the city wants these the instant the board appears. `colorKey` and `glyph`
* stay opaque to the engine, so a door looks different from a company without
* the renderer knowing what either means.
*/
const OFFICE_MARKERS: Marker[] = OFFICE_SITES.map((entry) => ({
id: `office:${entry.id}`,
label: entry.name,
colorKey: "office",
blurb: `${entry.entryCopy} · ${entry.status}`,
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.
*
* 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 must still get doors it can see. Amber, so a door reads as this
* application's navigation rather than as another marker hue.
*/
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.
*
* The asymmetry with every other feed here 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. So the elements are shared — and the *catalogue*
* is not, because a `SatelliteCatalogue` is built around an observer, and
* reusing one would compute the Southland's sky from San Francisco with nothing
* on screen to say so.
*
* A promise rather than a value, so a second city mounted while the first fetch
* is in the air waits for it instead of starting another. `null` forever on the
* majority of deployments: `TERA_SATELLITES_SOURCE` is off by default.
*/
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: string = 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.
*
* Leaving the office used to swap the scene and leave the address bar saying
* `office.`, so the URL you would copy and send disagreed with what you were
* looking at. The city door is therefore this hostname with its first label
* swapped — a guess only ever made when that label is literally `office`, which
* is the same comparison that made this the office door. Same registrable
* domain, same static root, same API; a deployment serving one name and not the
* other has half-configured itself, and a 404 is the honest failure for that.
*
* Anything else — a bare domain, `?view=office` on the city door — derives
* nothing and keeps the in-page swap.
*/
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;
}
// ---- The interface's own state --------------------------------------------
/**
* The applier, once the page has one.
*
* `null` for the handful of statements between this module's first line and
* `boot()`, which is why every call site is optional-chained rather than
* asserted: a keystroke that lands in that window should do nothing, not throw.
*/
let chrome: ChromeHandle | null = null;
/**
* 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 or the button the viewport stops having an opinion.
*
* The seeds themselves live in `ui/chromeState.ts` next to the breakpoints they
* read, because that is where the reasoning about what a phone can afford is —
* and because the old inline `window.innerWidth > 600` for the plan was the
* whole of live defect 10, "the minimap disappears entirely" on a phone.
*/
let panelOpen = seedPanelOpen(window.innerWidth);
let planOpen = seedPlanOpen(window.innerWidth);
let panelChosen = false;
let planChosen = false;
/**
* Whether this browser has been offered the first-run coach.
*
* Read **once**, here, before anything mounts: `mountOnboarding` marks the flag
* as it appears rather than as it completes, so asking `localStorage` a second
* time after the coach is up would answer "seen" and take it straight back off
* the screen. The write belongs to the coach; this is the read, and it is the
* only one.
*/
let firstVisit = !hasSeenOnboarding(browserStorage());
/**
* What the detail card is showing: an authored sentence, an observed aircraft,
* or nothing.
*
* A union rather than a string, because the two are not the same kind of thing
* and flattening the aircraft to a sentence is exactly what used to happen —
* `showDetail(\`${m.label} — ${m.blurb}\`)` for everything, including a track
* with five fields and a licence credit on it.
*/
let detail: ChromeDetail | null = null;
// ---- Studio hardware -------------------------------------------------------
/**
* Where the readings for the room you are standing in come from.
*
* One at a time and it belongs to the visit, like `presenceWatch` above it. The
* choice between the deployment's own device route and the fixed-step simulator
* in this tab is made inside `devices/adapter.ts` and deliberately not here —
* that is the one place the anon-first fallback decision lives, and a second
* copy of it in the app is how the two drift apart.
*/
let deviceSource: DeviceSource | null = null;
/** The latest readings, so the panel and the hardware in the room agree. */
let deviceStates: readonly DeviceState[] = [];
/**
* The car outside, as a state machine.
*
* Seeded from the office id, so a studio's Model X has the same charge, the same
* cabin temperature and the same parking jitter on every machine and on every
* reload — a car whose battery was different every time you opened the door
* would read as noise rather than as a vehicle.
*/
let vehicleTelemetry: SimulatedVehicleTelemetry | null = null;
/**
* The clock line, already formatted, because this file owns the clock.
*
* `instantOverride` is the one thing on the page that can make the rendered
* instant disagree with the wall clock, and it lives here — so a formatter
* downstream would have to be handed the override as well as the time, which is
* two facts to keep in step for one string. Written by `updateSun`, which is
* also the only thing that reads the sun.
*/
let clockLabel = "";
// ---- 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);
/**
* `atmosphere.cloudCover(env)` and **not** `currentWeather()?.cloudCover ?? 0`.
*
* `null` weather is "nobody was asked", which is not an edge case — it is the
* default deployment and the configuration this repo is held to. Falling back
* to zero meant a clean clone's sky was permanently, silently empty.
* `atmosphere` models a sky when nobody has observed one, and an observed
* cover still wins outright when there is one.
*/
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 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)}%` : "";
// Formatted here rather than in the chrome, because the clock is this file's
// — `instantOverride` is the one thing on the page that can make it disagree
// with the wall clock, and a formatter downstream would have to be told.
clockLabel = `${time} · sun ${el >= 0 ? "+" : ""}${el.toFixed(1)}° · ${daylightPhase(el)}${moon}`;
renderChrome();
}
// ---- 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;
clearPublishedPlayInput();
controlModeState = createControlModeState();
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.
*
* Two gates and only one of them is about the visitor. `can.liveEnvironment`
* is true for everybody — the sky is not a thing an account can grant. What is
* load-bearing is `feeds`, the *deployment*, which stops the ordinary box
* where every source is `none` polling two endpoints forever for a 404.
*
* These 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,
// The page's one rig, shared with the office. Handed in rather than built
// per board; see the note where it is constructed.
environment,
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);
},
/**
* The other thing worth pointing at, and the one the owner asked for by
* name: a card per aeroplane, for anybody who lands on this page.
*
* Hover opens it and hover away closes it, which is the same contract the
* marker card has had since picking existed — there is no click handler
* here because there is nothing extra a click could mean. See
* `showAircraftDetail` for where the provenance comes from.
*/
onAircraftPick: (a) => showAircraftDetail(a),
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();
renderChrome();
})
: 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(() => renderChrome());
city.onControlModeChange((mode) => adoptCityControlMode(mode));
/**
* 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 || city.controlMode() !== "overview") 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();
renderChrome();
}
/**
* The app's own frame pump, beside the renderer's.
*
* `Stage` owns the render loop and neither scene handle exposes a per-frame
* hook, so the alternative is adding an `onTick` to both for a handful of call
* sites. 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 scene at all.
*
* What lives on it, and why each is here rather than in a scene:
*
* - the two plan widgets, which are 2-D canvases the engine draws;
* - the pose editor, which is `null` for everyone who is not god;
* - the studio hardware and the car outside, which are **simulations** rather
* than layers — both are fixed-step state machines that the arena also drives,
* and putting their clocks in a scene would mean the arena and the renderer
* stepping two different copies;
* - the chrome, but only while a body is under control. See below.
*/
let lastPumpAt = performance.now();
requestAnimationFrame(function pump(now) {
requestAnimationFrame(pump);
/**
* Clamped, because a backgrounded tab comes back with a `dt` of minutes and
* neither simulator should be asked to catch up on a room nobody was in. Both
* of them drop the excess internally as well; this is the cheaper half of the
* same decision, made before the call rather than inside it.
*/
const dt = Math.min(0.1, Math.max(0, (now - lastPumpAt) / 1000));
lastPumpAt = now;
syncJourneyVehicle(now);
updateLocalPlayerMaps();
// 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();
if (inside) {
// A no-op on the API strategy, which is driven by its own poll — see
// `devices/adapter.ts`, which owns that choice and nothing above it knows
// which of the two it got.
deviceSource?.tick(dt);
stepVehicleTelemetry(dt);
}
poseEditor?.tick();
publishRealtimePresence(now);
pollLiveness();
/**
* The chrome, on the frames where something in it actually moves.
*
* `chromeState` is pure and `mount.apply` compares before it writes, so
* calling this unconditionally would be correct — and it would also rebuild
* nothing, sixty times a second, for the ninety-odd percent of a session
* spent in an orbit camera. The one part of the interface that changes per
* frame is the play HUD's speed and altitude, and `playHudKindFor` is exactly
* the test for "is a controller reporting numbers right now". Everything else
* calls `renderChrome()` at the moment it changes, which is what makes this
* an optimisation rather than a second source of truth.
*/
if (playHudKindFor(controlModeState.mode) !== null) renderChrome();
});
function updateLocalPlayerMaps(): void {
if (!city) return;
if (inside) {
minimap?.setPlayer(null);
const walker = controlModeState.mode === "office-walk" ? office?.walker?.state() : null;
officePlan?.setPlayer(walker
? {
levelId: walker.levelId,
x: walker.position.x,
z: walker.position.z,
headingRad: Math.atan2(-walker.facing.x, -walker.facing.z),
kind: walker.actor,
}
: null);
return;
}
officePlan?.setPlayer(null);
if (city.controlMode() === "drive") {
const state = city.vehicleState();
minimap?.setPlayer(state
? { lat: state.lat, lng: state.lng, headingDeg: state.headingDeg, kind: "vehicle" }
: null);
return;
}
if (city.controlMode() === "aircraft") {
const state = city.aircraftState();
minimap?.setPlayer(state
? { lat: state.lat, lng: state.lng, headingDeg: state.headingDeg, kind: "aircraft" }
: null);
return;
}
if (city.controlMode() === "actor") {
const state = city.actorState();
const pack = CITIES.find((candidate) => candidate.id === cityId)?.city;
if (!state || !pack) {
minimap?.setPlayer(null);
return;
}
const anchor = cityId === "california" ? { lat: 35.5, lng: -119.5 } : pack.center;
const [originX, originZ] = city.world.project(anchor.lat, anchor.lng);
const [lat, lng] = city.world.unproject(
originX + state.x / city.world.metresPerUnit,
originZ + state.z / city.world.metresPerUnit,
);
minimap?.setPlayer({
lat,
lng,
headingDeg: ((-state.yaw * 180 / Math.PI) % 360 + 360) % 360,
kind: "actor",
});
return;
}
minimap?.setPlayer(null);
}
/**
* 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;
renderChrome();
}
/** 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, robotOperations } = 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,
// The page's one environment map, the same object the city holds. Indoors
// it is the difference between `deviceMesh`, `chairBase` and the Model X's
// clearcoat reading as metal and reading as grey plastic.
environment,
/**
* The same aeroplanes the board outside is drawing.
*
* `dial.source` rather than the raw feed, so a sky somebody turned up in
* godmode is the sky in both places; and the *city's* source rather than a
* second subscription, so the office is a second reader of one feed rather
* than a second poller of a volunteer-funded API. The office never
* disposes it — see the note on `OfficeSceneOptions.flights`.
*/
...(trafficDial ? { flights: trafficDial.source } : {}),
/**
* The car on the apron. `corridor` detail because it is parked and the
* arrival viewpoint looks at it from across a courtyard: the 22 extra draw
* calls `follow` buys are mirrors, glass frames and brake calipers, none
* of which resolve at that range.
*
* The seed is the office id rather than a constant, so the two studios do
* not park identical cars at identical angles — which is the single
* clearest tell that a scene was generated.
*/
exteriorVehicle: { detail: "corridor", seed: seedForOffice(officeId) },
// 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 } } : {}),
// Only offices with explicit, validated simulated operations get robots.
// No geometry-derived random errands and no implication of live work.
...(robotOperations ? { robotOperations } : {}),
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(() => renderChrome());
officePlan = buildOfficePlan(createOfficeMinimap, office);
startDeviceFeed(built.createDeviceSource, office.devices);
startVehicleTelemetry(built.createSimulatedVehicleTelemetry);
}
requestControlMode("overview");
city.stage.setScene(office);
inside = true;
controlModeState = {
mode: "office-overview",
revision: controlModeState.revision + 1,
};
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();
renderChrome();
// 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) {
if (controlModeState.mode !== "office-overview") return;
/**
* 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();
clearPublishedPlayInput();
office?.walker?.setActive(false);
dispatchJourney({ type: "leave-office" });
city.stage.setScene(city.stageScene);
inside = false;
controlModeState = {
mode: "overview",
revision: controlModeState.revision + 1,
};
city.setControlMode("overview");
attachCurrentWebcamFace();
moveRealtimePresence();
showPlan();
showDetail(null);
refreshGodmodePlace();
renderChrome();
}
/** Dispose everything whose coordinates or subscriptions belong to one office. */
function disposeLoadedOffice(): void {
stopWatchingOccupancy();
disposeOfficeScreenUi();
// Before the scene, so a reading in flight cannot land on a disposed layer.
// `stop()` is idempotent and is the only thing that owns a timer down there.
deviceSource?.stop();
deviceSource = null;
deviceStates = [];
vehicleTelemetry = null;
officePlan?.dispose();
officePlan = null;
office?.dispose();
office = null;
builtOfficeId = null;
officeAtmosphere = null;
}
/**
* A stable integer per building, for the things that must look the same twice.
*
* FNV-1a over the id, which is the same hash the arena's checksums use and is
* chosen for the same property: it is short, it has no dependencies, and it
* gives two ids that differ by one character completely different seeds — so
* `lumbridge-hq` and `mateo-court` genuinely park different cars rather than
* two that differ in the third decimal.
*/
function seedForOffice(id: string): number {
let hash = 0x811c9dc5;
for (let i = 0; i < id.length; i += 1) {
hash ^= id.charCodeAt(i);
hash = Math.imul(hash, 0x01000193);
}
return hash >>> 0;
}
/**
* Point the hardware in this room at whatever will answer for it.
*
* The choice — the deployment's device route, or the fixed-step simulator in
* this tab — is made inside `createDeviceSource` and deliberately not here.
* That is the one place the anon-first fallback lives, and the reason it is not
* a branch in this file is that the two halves would drift: the route refuses an
* anonymous read, which is correct, and an anonymous visitor is the audience
* this product is designed for, so the refusal must produce a working
* instrument rather than a dead one.
*
* `serverHasDevices` is the same short-circuit `access.feeds` performs for
* weather and markers: a box with no device source answers with an empty body,
* and asking it once per TTL per tab forever to be told nothing is a request
* nobody needs.
*/
function startDeviceFeed(
create: typeof import("./devices/adapter.ts").createDeviceSource,
declarations: readonly DeviceDeclaration[],
): void {
deviceSource?.stop();
deviceStates = [];
if (declarations.length === 0) {
deviceSource = null;
return;
}
deviceSource = create({
declarations,
client: tera,
officeId: officePack?.id ?? officeId,
serverHasDevices: access.feeds?.devices !== false,
seed: seedForOffice(officeId),
onReading: (reading) => {
deviceStates = reading.states;
// The hardware in the room and the panel beside it, from one reading, so
// a lit indicator and a lit control can never disagree about a mic.
office?.setDeviceStates(reading.states);
chrome?.applyDeviceStates(reading.states);
},
});
const opening = deviceSource.current();
deviceStates = opening.states;
office?.setDeviceStates(opening.states);
chrome?.applyDeviceStates(opening.states);
}
/**
* Send one command, and say nothing if it is refused.
*
* `normalizeDeviceCommand` has already run inside the panel, so anything that
* reaches here is well formed for a device this office declared. A `null` back
* is a *deployment* refusal — an anonymous caller writing to a real room — and
* the panel is left showing the last reading rather than a control that appears
* to have worked. The refusal is not narrated because the sign-in offer is
* already on the same screen.
*/
function sendDeviceCommand(command: DeviceCommand): void {
void deviceSource?.command(command).then((state) => {
if (state === null) return;
deviceStates = deviceStates.map((current) => (current.id === state.id ? state : current));
office?.setDeviceStates(deviceStates);
chrome?.applyDeviceStates(deviceStates);
});
}
/** Bring the studio hardware panel into view, opening the column if it is shut. */
function openDevicePanel(): void {
panelOpen = true;
panelChosen = true;
renderChrome();
document.querySelector<HTMLElement>("#device-section")?.scrollIntoView({
block: "nearest",
// A jump rather than a glide, and not only for `prefers-reduced-motion`: the
// panel it is scrolling inside has just been opened in the same frame.
behavior: "auto",
});
}
/**
* The car outside, as its own fixed-step simulation.
*
* Seeded per office and stepped from the frame pump rather than from the office
* scene, because this is the *same* module the arena drives — a state machine
* with a snapshot and a restore, not a render layer. Keeping its clock here is
* what lets the renderer and the arena hold two independent instances of one
* implementation instead of two implementations.
*
* The ambient temperature is a constant and that is a gap rather than a choice:
* `WeatherObservation` carries cloud, precipitation, visibility and wind, and no
* temperature at all, so there is nothing live to couple to. Inventing one from
* the sun's elevation would be publishing a reading nobody took, which is the
* one thing the whole telemetry layer is arranged not to do.
*/
function startVehicleTelemetry(
create: typeof import("./transport/vehicleTelemetry.ts").createSimulatedVehicleTelemetry,
): void {
vehicleTelemetry = create({
seed: seedForOffice(officeId),
fixedStepSeconds: VEHICLE_STEP_SECONDS,
ambientC: DEFAULT_AMBIENT_C,
});
office?.setVehicleTelemetry(vehicleTelemetry.current());
}
/** Seconds of simulated vehicle time per fixed step. */
const VEHICLE_STEP_SECONDS = 0.5;
/**
* The ambient temperature the parked car sits in, in Celsius.
*
* A mild Californian afternoon, which is what both studios stand in. See
* `startVehicleTelemetry` for why it is not read off the weather feed.
*/
const DEFAULT_AMBIENT_C = 19;
let vehicleStepDebt = 0;
/**
* Advance the car by whole fixed steps, and only ever by whole ones.
*
* A fixed-step simulator that is handed a variable `dt` stops being
* reproducible, and reproducibility is the entire reason this module is shared
* with the arena. So the frame's elapsed time is accumulated and spent in whole
* steps, with the remainder carried; `apply` is signature-guarded downstream, so
* the frames that spend no step cost a comparison.
*/
function stepVehicleTelemetry(dt: number): void {
const source = vehicleTelemetry;
if (!source || !office) return;
vehicleStepDebt = Math.min(vehicleStepDebt + dt, VEHICLE_STEP_SECONDS * 4);
while (vehicleStepDebt >= VEHICLE_STEP_SECONDS) {
vehicleStepDebt -= VEHICLE_STEP_SECONDS;
source.stepFixed();
}
office.setVehicleTelemetry(source.current());
}
/**
* Fetch Spaces.
*
* The office is the largest thing in this build that most visitors never open,
* so it is a chunk the door fetches on the way through rather than bytes every
* map visitor downloads. Measured entry chunk: 780.18 kB before the split,
* 720.89 after — smaller than the chunks themselves because three.js is shared
* and stays where it was.
*
* One `Promise.all` because they are one arrival: a pack without its builder is
* a data file nobody can draw, so the fetches overlap rather than queue.
*
* No retry and no cache-busting, deliberately. 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 can, 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;
createDeviceSource: typeof import("./devices/adapter.ts").createDeviceSource;
createSimulatedVehicleTelemetry:
typeof import("./transport/vehicleTelemetry.ts").createSimulatedVehicleTelemetry;
pack: Office;
materials: MaterialRegistry;
robotOperations: RobotOperationsDefinition | null;
} | null> {
try {
// More modules 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, operations, devices, telemetry] = await Promise.all([
import("./interiors/officeScene.ts"),
entry.load(),
import("./assets/materials.ts"),
import("./engine/officeMinimap.ts"),
entry.loadOperations?.() ?? Promise.resolve(null),
// Neither of these imports THREE, the DOM or the network, and neither has
// anything to say until somebody is standing in a studio — so both travel
// with the furniture rather than in the bundle every map visitor fetches.
import("./devices/adapter.ts"),
import("./transport/vehicleTelemetry.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: officeMaterialQuality() });
return {
createOfficeScene: interiors.createOfficeScene,
createOfficeMinimap: plan.createOfficeMinimap,
createDeviceSource: devices.createDeviceSource,
createSimulatedVehicleTelemetry: telemetry.createSimulatedVehicleTelemetry,
pack: officePack,
materials: officeMaterials,
robotOperations: operations?.default ?? null,
};
} catch {
showDetail("The office did not load. Check the connection and try the door again.");
return null;
}
}
/**
* How much texture a studio may draw, decided by the device rather than by a
* constant.
*
* This said `"high"` for every visitor, which meant `materials.ts` had a fully
* implemented `low` and `medium` path — different roughness handling, a cheaper
* glazing fallback, and 256px textures instead of 512 — that **no caller could
* ever reach**. The documented mobile escape hatch had never actually existed,
* and a phone was being handed desktop-grade texture memory for a canvas at
* 1.5× pixel ratio.
*
* `medium` rather than `low` on a handheld: `low` drops to flat Lambert with no
* maps at all, which is the setting that makes an office open on an integrated
* GPU and is far more than a modern phone needs. `medium` is the same physically
* shaded materials at half the texture resolution, which is the actual
* difference a phone can feel.
*
* `deviceProfile()` is the one definition of "phone" on this page — the pixel
* ratio cap, the shadow map size and now the texture budget all read it, so they
* cannot disagree about what a phone is.
*/
function officeMaterialQuality(): MaterialQuality {
return deviceProfile().handheld ? "medium" : "high";
}
/** 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, so there is nothing to populate
* and the request would be one the session is going to be refused anyway.
*
* The fallback is `markers`' fallback, one room in. An API that answers 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 liven up the demo 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 = false;
// 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();
renderChrome();
});
}
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 fallback = samplePresenceForOfficeAt(officePack?.id ?? officeId, currentInstant());
const people = livePresence ?? fallback;
presenceIsSample = livePresence === null && fallback.length > 0;
office.setPresence(people);
officePlan?.setPresence(people);
}
// ---- Chrome ---------------------------------------------------------------
//
// Six DOM handles and no more. Everything else on this page belongs to
// `ui/mount.ts`, which is the only module in the product that writes to it —
// these six are the hosts of things `mount` deliberately does not own: two
// widgets built by the engine, one overlay built by `profile/`, one by
// `media/`, and one indicator built by `realtime/`.
const minimapFrame = document.querySelector<HTMLElement>("#minimap .minimap-frame");
const minimapReadout = document.querySelector<HTMLElement>("#minimap-readout");
const presenceHost = document.querySelector<HTMLElement>("#presence-host");
const webcamFaceIndicator = document.querySelector<HTMLElement>("#webcam-face-indicator");
const profileOverlay = document.querySelector<HTMLElement>("#profile-overlay");
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;
/** Whether hosted presence has a widget on screen; the chrome makes room for it. */
let presenceMounted = false;
function availableControlModes() {
const route = city?.current();
return {
insideOffice: inside,
drive: !inside && city?.vehicleState() !== null &&
(route === "la-sf-us-101" || route === "la-sf-i-5"),
actor: !inside && city?.actorState() !== null,
aircraft: !inside && city?.aircraftState() !== null,
officeWalk: inside && office?.walker !== null && office?.walker !== undefined,
};
}
/**
* The same availability, as the list the chrome takes.
*
* Two shapes for one fact is a smell and is the lesser one here:
* `transitionControlMode` was written against the record and is tested against
* it, and `chromeState` takes a list because a list is what a dock renders. This
* is the one conversion, in one place, rather than each caller deriving its own.
*/
function availableModes(): ControlMode[] {
const can = availableControlModes();
const modes: ControlMode[] = [inside ? "office-overview" : "overview"];
if (can.drive) modes.push("drive");
if (can.actor) modes.push("actor");
if (can.aircraft) modes.push("aircraft");
if (can.officeWalk) modes.push("office-walk");
return modes;
}
function clearPublishedPlayInput(): void {
playInput.clearAll();
city?.setVehicleActions({});
city?.setActorActions({});
city?.setAircraftActions({});
office?.walker?.setAction({ x: 0, z: 0 });
}
function adoptCityControlMode(mode: ReturnType<typeof cityControlMode>): void {
if (inside) return;
const previous = controlModeState.mode;
if (controlModeState.mode !== mode) {
clearPublishedPlayInput();
controlModeState = { mode, revision: controlModeState.revision + 1 };
}
if (previous === "overview" && mode !== "overview") closePanelForPlay();
renderChrome();
}
/** One transaction updates Journey, simulation ownership, input and chrome. */
function requestControlMode(requested: ControlMode): boolean {
const transition = transitionControlMode(controlModeState, requested, availableControlModes());
if (transition.changed) clearPublishedPlayInput();
controlModeState = transition.state;
const next = transition.state.mode;
if (inside) {
city?.setControlMode("overview");
const walking = next === "office-walk";
office?.walker?.setActive(walking);
if (!walking) office?.walker?.setAction({ x: 0, z: 0 });
} else {
office?.walker?.setActive(false);
city?.setControlMode(cityControlMode(next));
}
if (next === "drive") {
const routeId = city?.current();
if (routeId === "la-sf-us-101" || routeId === "la-sf-i-5") {
dispatchJourney({ type: "set-mode", mode: "play" });
if (journey.route?.routeId !== routeId) {
dispatchJourney({ type: "select-route", routeId, direction: 1 });
}
if (!journey.vehicle) dispatchJourney({ type: "enter-vehicle", vehicleId: "model-x-black" });
}
} else if (journey.vehicle) {
dispatchJourney({ type: "exit-vehicle" });
}
if (next !== "overview" && next !== "office-overview") {
dispatchJourney({ type: "set-mode", mode: "play" });
} else if (!journey.vehicle) {
dispatchJourney({ type: "set-mode", mode: "observe" });
}
if (
transition.changed &&
(transition.previous === "overview" || transition.previous === "office-overview") &&
next !== "overview" && next !== "office-overview"
) {
closePanelForPlay();
}
renderChrome();
publishPlayActions();
return transition.accepted;
}
/** One authored sentence in the detail card, or nothing. */
function showDetail(text: string | null) {
detail = text === null ? null : { kind: "text", text };
renderChrome();
}
/**
* One aeroplane in the detail card, for anybody at all.
*
* Owner decision 2: the flight card is part of the demo an anonymous visitor
* gets, and there is nothing here an account could grant — an ADS-B position is
* broadcast unencrypted by the aircraft to anybody with a receiver. What the
* *deployment* knows and the engine does not is provenance, which is why the
* live source is asked first: `TrafficSource.detail` carries the transponder
* address off the wire body and the credit line the feed asks to be shown
* beside its data, and neither is recoverable from the position alone.
*
* The fallback is not a degraded case. It is the zero-config clone, the
* California corridor board and every aircraft the godmode dial invented, and
* for all three the honest answer is a card that says "simulated track" rather
* than a hex address nobody transmitted. `aircraftDetail` defaults `observed` to
* false precisely so that forgetting to say so cannot produce the other claim.
*/
function showAircraftDetail(aircraft: Aircraft | null): void {
if (aircraft === null) {
if (detail?.kind === "aircraft") showDetail(null);
return;
}
const centre = CITIES.find((c) => c.id === cityId)?.city.center;
const resolved = cityFlights?.detail(aircraft.id)
?? aircraftDetail(aircraft, { observed: false, ...(centre ? { from: centre } : {}) });
const card: AircraftDetailInput = {
// The hex when the feed gave one; the source's own id otherwise, which the
// card only ever shows for a track it has already labelled as simulated.
id: resolved.icao24 ?? resolved.id,
callsign: resolved.callsign,
lat: resolved.lat,
lng: resolved.lng,
altitude: resolved.altitudeM,
heading: resolved.headingDeg,
synthetic: !resolved.observed,
attribution: resolved.attribution.length > 0 ? resolved.attribution.join(" · ") : null,
};
detail = { kind: "aircraft", aircraft: card };
renderChrome();
}
/** The board strip: worlds outside a building, buildings inside one. */
function boardTabs() {
return inside
? OFFICES.map((o) => ({ id: o.id, label: o.label, status: o.status }))
: CITIES.map((c) => ({ id: c.id, label: c.label, status: null }));
}
/**
* Whatever the mode's controller is reporting, in the shape the HUD formats.
*
* Keyed off `playHudKindFor` rather than off a second `switch` on the mode, so
* the two orbit states are excluded once, in the module that also decides the
* card's visibility. `null` whenever the controller has nothing to say — a
* scene that has not built an aircraft cannot report on one — and the HUD is
* hidden rather than blank in that case.
*/
function playTelemetry(): PlayTelemetry | null {
switch (playHudKindFor(controlModeState.mode)) {
case "drive": {
const state = city?.vehicleState();
if (!state) return null;
return {
kind: "drive",
speedMps: state.speedMps,
roadName: state.roadName,
driveMode: state.mode,
progress: state.progress,
camera: city?.vehicleCamera() ?? "chase",
guardrailContact: state.guardrailContact,
collisionRisk: state.collisionRisk,
};
}
case "actor": {
const state = city?.actorState();
if (!state) return null;
return {
kind: "actor",
actor: state.kind,
speedMps: state.speedMps,
altitudeM: Math.max(0, state.y),
distanceM: state.distanceM,
pose: state.kind === "crow" ? state.crowPose : state.mode,
flightEnergy: state.flightEnergy,
displayName: state.identity.displayName,
atAltitudeBound: state.altitudeBoundContact !== "none",
};
}
case "aircraft": {
const state = city?.aircraftState();
if (!state) return null;
return {
kind: "aircraft",
speedMps: state.speedMps,
altitudeM: state.altitudeM,
flightMode: state.mode,
batteryWh: state.batteryWh,
stalled: state.stalled,
hardLanding: state.hardLanding,
envelopeContact: state.envelopeContact,
};
}
case "office-walk": {
const state = office?.walker?.state();
if (!state) return null;
return {
kind: "office-walk",
officeLabel: officeName(),
distanceM: state.distance,
x: state.position.x,
z: state.position.z,
};
}
default:
return null;
}
}
/** The body under control, for the labels that name it. */
function chromeActor() {
if (inside) {
const walker = office?.walker?.state();
if (!walker) return {};
return { kind: walker.actor === "anonymous-dog" ? ("dog" as const) : ("humanoid" as const) };
}
if (controlModeState.mode === "aircraft") return { flying: true };
const state = city?.actorState();
if (!state) return {};
return { kind: state.kind, flying: state.mode === "flight" };
}
/**
* Everybody who is owed a credit for what is currently on screen.
*
* MET Norway and Open-Meteo publish under CC BY 4.0 and the ADS-B feeds ask to
* be named for the positions; all of it arrived, and until the `?` sheet started
* printing it, all of it was read by nobody — a licence obligation plumbed to
* within one line of being met. The weather override is deliberately excluded:
* a sky somebody typed is not MET Norway's sky and must not be attributed to
* them.
*/
function creditLines(): string[] {
const lines: string[] = [];
if (weatherOverride === null) lines.push(...(weatherWatch?.current().attribution ?? []));
lines.push(...(cityFlights?.attribution() ?? []));
return lines.filter((line) => line.trim() !== "");
}
/**
* Everything the interface needs to know, as one plain object.
*
* This function is the whole of the coupling between the application and its
* chrome. It reads module state and returns data; it writes nothing and it
* decides nothing — every "should this be visible" question in the product is
* answered by `chromeState`, and every answer is applied by `mount`. Adding a
* piece of chrome is a field here, a decision there and a write in the applier,
* in that order, and never a fortieth `element.hidden =` in this file.
*/
function chromeInputs(): ChromeInputs {
const entry = CITIES.find((c) => c.id === cityId);
const selectedOffice = OFFICES.find((o) => o.id === officeId) ?? OFFICES[0];
const mediaSurfaces = inside && office?.depth === "full" ? office.listMediaSurfaces() : [];
const robotActivity = inside ? office?.robotActivityInfo() ?? null : null;
const views: View[] = inside && office ? office.views : city?.chapters ?? [];
const activeViewId = inside && office ? office.current() : city?.current() ?? null;
return {
mode: controlModeState.mode,
available: availableModes(),
access: {
tier: access.tier,
signInUrl: access.signInUrl,
subject: access.subject,
displayName: localProfile?.displayName ?? null,
hasProfile: localProfile !== null,
},
inside,
officeDepth: inside ? office?.depth ?? null : null,
viewport: {
width: window.innerWidth,
height: window.innerHeight,
// The class is set by the first touch this page ever sees and never
// removed, because a laptop with a touchscreen has both and the one the
// person actually used is the one worth believing.
coarsePointer:
document.body.classList.contains("touch-capable") ||
window.matchMedia?.("(pointer: coarse)").matches === true,
},
feeds: {
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,
weatherOverridden: weatherOverride !== null,
sampleOccupancy: presenceIsSample,
credits: creditLines(),
},
degraded: access.degraded ?? [],
devices: inside ? office?.devices ?? [] : [],
firstVisit,
panelOpen,
planOpen,
board: {
cityId,
cityLabel: entry?.city.name ?? "",
officeId,
officeLabel: inside ? officeName() : selectedOffice?.label ?? "the studio",
officeStatus: selectedOffice?.status ?? "building",
isCalifornia: cityId === "california",
},
actor: chromeActor(),
office: {
mediaSurfaceCount: mediaSurfaces.length,
mediaSurfaceActiveCount: mediaSurfaces.filter((surface) => surface.bound).length,
robotDisclosure: robotActivity?.disclosure ?? null,
walkable: office?.walker != null,
},
cameraLive: webcamCapture?.status() === "active",
presenceVisible: presenceMounted,
boards: boardTabs(),
views: views.map((view) => ({
id: view.id,
number: view.number ?? null,
shortLabel: view.shortLabel,
...(view.description === undefined ? {} : { description: view.description }),
})),
activeViewId,
telemetry: playTelemetry(),
detail,
clockLabel,
};
}
/**
* Draw the interface. One call, and it replaces forty.
*
* Cheap enough to run from the frame pump — `chromeState` is pure arithmetic and
* string building, and the applier compares before it writes and rebuilds a
* subtree only when that subtree's signature moves — but it is still called on
* *changes* rather than unconditionally, and the pump only joins in while a body
* is under control. See `pumpChrome`.
*
* The two plan widgets are updated here rather than inside the applier because
* neither is chrome: they are canvases the engine draws, holding a live camera,
* and `mount.ts` is deliberately ignorant of THREE.
*/
function renderChrome(): void {
// The one place the mode the app *thinks* it is in is reconciled with the mode
// the scene is actually in. A scene can change it on its own — a chapter that
// is a driving route puts the city into `drive` — and a dock that disagreed
// with the camera was the oldest bug in this file.
if (city) {
const walking = inside && (office?.walker?.active() ?? false);
const actual: ControlMode = inside
? walking ? "office-walk" : "office-overview"
: city.controlMode();
if (controlModeState.mode !== actual) {
controlModeState = { mode: actual, revision: controlModeState.revision + 1 };
}
}
chrome?.apply(chromeState(chromeInputs()));
// 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.
if (city) minimap?.setChapters(city.chapters, city.current());
officePlan?.setActiveView(office?.current() ?? null);
}
/**
* 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, the light rig, the hardware on the desks and the car
* on the apron. 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, hardware feed, plan and scene all belong to the
// building being 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();
renderChrome();
}
// ---- 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;
// The class that makes room for it is `chromeState`'s to add, from
// `presenceVisible` — one place decides what the top-right column contains.
presenceMounted = true;
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;
// The one thing on this page that owns GPU memory neither scene created. It
// is disposed by whoever owns the `Stage`, which is this file, and never by a
// scene — the two kinds of environment are shared between every city and every
// office that ever existed on this page.
environment.dispose();
});
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);
renderChrome();
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);
renderChrome();
}
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() ?? []);
renderChrome();
}
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());
renderChrome();
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();
}
// The `Office screens →` button is bound by `mount`; this is the overlay's own
// backdrop, which is not chrome and stays here with the panel it dismisses.
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) {
requestControlMode("office-overview");
office.flyTo(view.id);
renderChrome();
}
else {
const destination = cityId === "california" ? CALIFORNIA_DESTINATIONS.get(view.id) : undefined;
if (destination) {
requestControlMode("overview");
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") {
city?.flyTo(view.id);
requestControlMode("drive");
closePanelForPlay();
renderChrome();
return;
}
requestControlMode("overview");
city?.flyTo(view.id);
renderChrome();
}
}
/**
* 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) {
requestControlMode(inside ? "office-overview" : "overview");
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;
try {
/*
* The boot card is the whole of the "something is happening" story now, and
* it is enough: `building()` puts it up before the fetch and takes it down
* only once a frame of the new scene is on the glass. The old arrangement
* also wrote "Opening the office…" onto the door itself, which meant this
* file reaching into a label `mount.ts` now owns to say something the card
* in front of it was already saying.
*/
await building("Fetching the office…", () => enterOffice());
} finally {
entering = false;
// Writes the real label whichever way it went — "← Back to the city" if we
// are in, the door again if the chunk fetch failed.
renderChrome();
}
}
/** Switch between the authored dollhouse camera and the local possessed actor. */
function toggleOfficeWalk(): boolean {
if (!inside) {
if (!city?.actorState()) return false;
const active = city.controlMode() !== "actor";
requestControlMode(active ? "actor" : "overview");
if (active) closePanelForPlay();
renderChrome();
return true;
}
const walker = office?.walker;
if (!walker) return false;
const active = !walker.active();
requestControlMode(active ? "office-walk" : "office-overview");
if (active) closePanelForPlay();
renderChrome();
return true;
}
function toggleAircraft(): boolean {
if (inside || cityId !== "california" || !city?.aircraftState()) return false;
const active = city.controlMode() !== "aircraft";
requestControlMode(active ? "aircraft" : "overview");
if (active) closePanelForPlay();
renderChrome();
return true;
}
/**
* 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;
renderChrome();
});
});
// ---- Panels, plan and overlays ----------------------------------------------
/**
* Get the left column out of the way when a body takes over.
*
* Only on a phone, where `#panel` is a bottom sheet covering a third of the
* screen: possessing an actor and then being unable to see it is not a trade
* anybody would make, and the sheet has a toggle to bring it back. Above that
* width the column is furniture beside the map rather than on top of it, and
* closing it would be taking away a thing that costs nothing.
*
* `panelChosen` is set for the same reason a keypress sets it: once anything has
* had an opinion about this panel, the viewport stops having one.
*
* The plan view is deliberately **not** closed here any more. It used to be, and
* the reason was that on a phone it was a bottom sheet in exactly the place the
* joystick now occupies; `ui/chromeState.ts` moved it to a glanceable corner and
* closing it is now taking the map away at the moment somebody is navigating.
*/
function closePanelForPlay(): void {
if (window.innerWidth > 600) return;
panelOpen = false;
panelChosen = true;
}
/**
* 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:
* a phone starts without `M` and had no button. `#plan-toggle` and the touch
* pad's Map button are that way in, and both arrive here through `mount`'s
* `onTogglePlan`, so the key and the buttons cannot disagree about the state.
*/
function togglePlan(): void {
setPlanOpen(!planOpen);
}
function setPlanOpen(open: boolean): void {
planOpen = open;
planChosen = true;
renderChrome();
}
function setPanelOpen(open: boolean): void {
panelOpen = open;
panelChosen = true;
renderChrome();
}
/**
* The viewport crossed a published breakpoint — a rotation, a window drag, a
* tablet turning over.
*
* Both seeds are re-run, and that "both" is the fix: `resize` used to re-seed
* the plan and, for no stated reason, not the panel, so rotating a tablet from
* portrait to landscape left the sheet closed over a layout wide enough that the
* toggle which would reopen it is not drawn. Either is skipped once its own
* control has been touched.
*/
function adoptLayout(_layout: ChromeLayout): void {
if (!panelChosen) panelOpen = seedPanelOpen(window.innerWidth);
if (!planChosen) planOpen = seedPlanOpen(window.innerWidth);
renderChrome();
}
function routeDriveIsActive(): boolean {
const state = !inside ? city?.vehicleState() : null;
return state !== null && state !== undefined && city?.controlMode() === "drive" &&
city.current() === state.routeId;
}
function toggleVehicleCamera(): boolean {
if (!routeDriveIsActive() || !city) return false;
city.setVehicleCamera(city.vehicleCamera() === "driver" ? "chase" : "driver");
return true;
}
function cameraForward(
camera: { position: { x: number; z: number } },
controls: { target: { x: number; z: number } },
) {
return { x: controls.target.x - camera.position.x, z: controls.target.z - camera.position.z };
}
function publishPlayActions(): boolean {
const input = playInput.snapshot();
const requests = playInput.consumeRequests();
if (routeDriveIsActive() && city) {
city.setVehicleActions({
...vehicleActionsFromPlay(input),
modeRequest: requests.has("assist") ? "assisted" : "none",
reset: requests.has("reset"),
});
if (requests.has("camera")) toggleVehicleCamera();
return true;
}
const walker = inside && controlModeState.mode === "office-walk" ? office?.walker : null;
if (walker?.active() && office) {
walker.setAction(cameraRelativePlanar(input, cameraForward(office.camera, office.controls)));
return true;
}
if (!inside && city?.controlMode() === "actor") {
const state = city.actorState();
if (!state) return false;
if (state.kind === "crow" && state.mode === "flight") {
city.setActorActions({
...crowActionsFromPlay(input), modeRequest: "none", kindRequest: "none", reset: false,
});
} else {
city.setActorActions({
...groundActorActionsFromPlay(
input,
state.yaw,
cameraForward(city.stageScene.camera, city.stageScene.controls),
),
pitch: 0, climb: 0, glide: false, modeRequest: "none", kindRequest: "none", reset: false,
});
}
return true;
}
if (!inside && city?.controlMode() === "aircraft") {
city.setAircraftActions({
...aircraftActionsFromPlay(input),
modeRequest: requests.has("assist") ? "assisted" : "none",
reset: requests.has("reset"),
});
return true;
}
return false;
}
window.addEventListener("keyup", (event) => {
const control = controlForKey(event.key);
if (!control) return;
playInput.setDigital("keyboard", control, false);
if (publishPlayActions()) event.preventDefault();
});
function releaseAllPlayInput(): void {
clearPublishedPlayInput();
publishPlayActions();
}
window.addEventListener("blur", releaseAllPlayInput);
document.addEventListener("visibilitychange", () => {
if (document.visibilityState !== "visible") releaseAllPlayInput();
});
let gamepadButtons: StandardPlayGamepadButtons = { assist: false, reset: false, camera: false };
function pollPlayGamepad() {
try {
const pad = navigator.getGamepads?.().find((candidate) => candidate !== null);
if (pad) {
const sample = sampleStandardPlayGamepad(pad, gamepadButtons);
gamepadButtons = sample.buttons;
playInput.clearSource("gamepad");
playInput.setAxes("gamepad", sample.axes);
for (const control of sample.digital) playInput.setDigital("gamepad", control, true);
for (const request of sample.requests) playInput.request(request);
publishPlayActions();
} else {
playInput.clearSource("gamepad");
gamepadButtons = { assist: false, reset: false, camera: 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(pollPlayGamepad);
}
requestAnimationFrame(pollPlayGamepad);
window.addEventListener("pointerdown", (event) => {
if (event.pointerType === "touch") document.body.classList.add("touch-capable");
}, { capture: true });
/**
* Keyboard access to everything the pointer 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.
*
* Every binding below is resolved through `ui/shortcuts.ts`, which is also what
* renders the `?` sheet — so a key that works and a key that is documented are
* now the same list. They were not: the sheet had no row for `G` while `g` was
* bound to glide *and* inserted at runtime meaning godmode, and Space was
* documented as "Handbrake while driving" while being the generic primary in
* every mode.
*/
window.addEventListener("keydown", (event) => {
if (event.defaultPrevented || 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") {
// Through the applier rather than at `#shortcuts.hidden`, so the focus
// return and the `onShortcuts` callback stay correct whichever way the
// sheet was opened.
if (chrome?.shortcutsOpen() === true) chrome.closeShortcuts();
else if (inside) {
if (!leaveToCity()) leaveOffice();
}
else showDetail(null);
return;
}
if (event.key === "?") {
if (chrome?.shortcutsOpen() === true) chrome.closeShortcuts();
else chrome?.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();
const control = controlForKey(event.key);
if (control) {
playInput.setDigital("keyboard", control, true);
if (publishPlayActions()) {
event.preventDefault();
return;
}
}
/**
* The three one-shot requests — assist, reset, camera — from the same table
* the held controls come from.
*
* This was three hand-written `lower === "p"`-shaped branches, each carrying
* its own copy of "which modes is this meaningful in". The mode test is still
* here, because it is a fact about the simulation rather than about the
* keyboard: only a drive has a camera to swap and only a drive or a flight has
* an assist to resume, and a `P` that silently queued a request in overview
* would be a key that does nothing while claiming to do something.
*/
const edge = edgeForKey(event.key);
if (edge !== null) {
const driving = routeDriveIsActive();
const flying = city?.controlMode() === "aircraft";
const meaningful = edge === "camera" ? driving : driving || flying;
if (meaningful) {
playInput.request(edge);
publishPlayActions();
event.preventDefault();
return;
}
}
if (lower === "v" && toggleOfficeWalk()) {
event.preventDefault();
return;
}
if (lower === "m") {
togglePlan();
return;
}
if (lower === "o") void toggleOffice();
});
// ---- Time -------------------------------------------------------------------
/**
* One writer for one override.
*
* The `#hour` slider is gone: `capabilitiesFor` hands `timeControl` and `debug`
* to the same tier, so the only person who could see the scrubber was the person
* who can open the godmode panel — and it wrote an hour onto *today*, silently
* discarding whatever date the panel had set.
*
* The clock line stays visible to everyone; a map that will not say what time it
* is showing is worse than one you cannot scrub. This gate is belt and braces:
* "no control" and "no override" are two different facts, and the renderer
* depends on the second.
*/
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 anyone else the result 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.
*/
/** 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. The stage travels with the place even though
* there is only ever one, because what the panel is told is *which board these
* counters are about* — handing it 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`,
* and the panel prints "no hook wired" rather than a guess.
*/
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.
renderChrome();
},
/**
* 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);
// The godmode row in the `?` sheet is no longer inserted from here: it is a
// `KEYMAP` entry scoped to the god tier and rendered by `renderShortcutSheet`,
// which is the same table the key handler reads. The hand-written row said
// `G`, and `G` is glide.
renderChrome();
}
/**
* 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. Its geometry used to be set here as
* element styles rather than as a rule, because every visitor downloads
* `index.html` and a `#pose-dock { … }` sitting in it that can never match is
* the one trace this arrangement would otherwise leave behind. That reason still
* holds and the styles have moved anyway: `tools/godmode.ts` injects its own
* stylesheet, that stylesheet is fetched only behind `access.can.debug`, and a
* rule in it can carry the `env(safe-area-inset-bottom)` and the phone
* breakpoint an inline style could not.
*/
function instrumentDock(): HTMLElement {
if (poseDockBody) return poseDockBody;
const dock = document.createElement("aside");
dock.id = "pose-dock";
dock.setAttribute("aria-label", "Pose editor");
const toggle = document.createElement("button");
toggle.type = "button";
toggle.className = "help";
toggle.setAttribute("aria-expanded", "true");
const body = document.createElement("div");
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;
}
// ---- 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: the heightfield builds in a Worker, so
* `createScene` resolves while the main thread is idle and two animation frames
* go by in 33 ms — long before the first WebGL draw. The card faded on an empty
* canvas. `renderer.info.render.frame` is the renderer counting its own draws
* and is the one witness a fast frame cannot fool.
*
* The timeout bounds a promise that would otherwise never settle: an abandoned
* build never draws, and the boot card must not outlive 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 -----------------------------------------------------------------
/**
* Bind the interface to the page, once.
*
* Every handler is one line and every one of them ends in a function that
* already existed — that is the measure of whether the chrome extraction
* worked. `mount` owns the listeners, the joystick, the shortcuts dialog, the
* onboarding coach and the device panel from here on; this file owns what the
* buttons mean.
*/
function mountInterface(): ChromeHandle {
return mountChrome(document, {
onEnter: () => void toggleOffice(),
onWalk: () => { toggleOfficeWalk(); },
onFly: () => { toggleAircraft(); },
onScreens: () => openOfficeScreens(),
onDevices: () => openDevicePanel(),
onDeviceCommand: (command) => sendDeviceCommand(command),
onSelectBoard: (id) => {
if (inside) void switchOffice(id);
else switchCity(id);
},
onSelectView: (_id, index) => flyToIndex(index),
onMode: (mode) => {
requestControlMode(mode);
if (mode !== "overview" && mode !== "office-overview") closePanelForPlay();
renderChrome();
},
onTogglePanel: (open) => setPanelOpen(open),
onTogglePlan: (open) => setPlanOpen(open),
onDismissDetail: () => showDetail(null),
onCharacter: () => openProfileEditor(),
// A held touch button is one source per control rather than one per pointer:
// `mount` captures the pointer and guarantees the matching release, so the
// per-pointer bookkeeping this file used to do has an owner now.
onTouchHold: (control, pressed) => {
playInput.setDigital(`touch:${control}`, control, pressed);
publishPlayActions();
},
onTouchEdge: (edge) => {
playInput.request(edge);
publishPlayActions();
},
onStick: (axes) => {
if (axes === null) playInput.clearSource("stick");
else playInput.setAxes("stick", axes);
publishPlayActions();
},
onOnboardingFinished: () => {
// The coach has recorded itself as seen; this is the app agreeing, so the
// next `renderChrome` does not put it straight back up.
firstVisit = false;
renderChrome();
},
onLayoutChange: (layout) => adoptLayout(layout),
});
}
/**
* 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, the local hardware simulator, and a label at the
* bottom of the screen saying which of them you are looking at.
*/
async function boot() {
chrome = mountInterface();
renderChrome();
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();
renderChrome();
// 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();