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