1
0

feat: add deterministic office robot jobs

This commit is contained in:
2026-08-19 02:54:49 -07:00
parent 0557a26e6b
commit e378a03740
18 changed files with 1937 additions and 1779 deletions
+39 -19
View File
@@ -57,7 +57,6 @@ import {
sampleRoutesFor,
} from "./adapters/sample.ts";
import { OFFICE_SITES, type ShippedOfficeId } from "./offices/sites.ts";
import { activityRobotsForOffice } from "./offices/runtime.ts";
import { authFetch } from "./session.ts";
import { capabilitiesFor, resolveAccess, type Access } from "./access.ts";
import { createMinimap, type Minimap } from "./engine/minimap.ts";
@@ -115,6 +114,7 @@ import type { RemoteMediaState, RemoteOfficeMedia } from "./media/remoteMedia.ts
*/
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
@@ -201,10 +201,10 @@ function journeyToCity(city: JourneyCity): void {
/**
* The buildings this page can walk into.
*
* Two of them, and the second one is why this is a table rather than the single
* 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 two-storey tower floor 188 m above Transbay, and a
* hangar four metres above reclaimed ground at Alameda Point — because the
* 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.
@@ -220,10 +220,23 @@ const OFFICE_LOADERS: Readonly<Record<ShippedOfficeId, () => Promise<{ default:
"mateo-court": () => import("./offices/mateo-court.ts"),
};
const OFFICE_OPERATION_LOADERS: Readonly<Partial<Record<
ShippedOfficeId,
() => Promise<{ default: RobotOperationsDefinition }>
>>> = {
"lumbridge-hq": async () => ({
default: (await import("./offices/operations/lumbridge-hq.ts")).LUMBRIDGE_HQ_ROBOT_OPERATIONS,
}),
"mateo-court": async () => ({
default: (await import("./offices/operations/mateo-court.ts")).MATEO_COURT_ROBOT_OPERATIONS,
}),
};
const OFFICES = OFFICE_SITES.map((entry) => ({
...entry,
label: entry.name,
load: OFFICE_LOADERS[entry.id],
loadOperations: OFFICE_OPERATION_LOADERS[entry.id],
}));
const canvas = document.querySelector<HTMLCanvasElement>("#scene");
@@ -1210,7 +1223,7 @@ async function enterOffice() {
// it did not arrive at all. Either way there is no room to walk into and
// `loadOffice` has already said so on the button.
if (!built || !city) return;
const { createOfficeScene, createOfficeMinimap, pack, materials } = built;
const { 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.
@@ -1270,10 +1283,9 @@ async function enterOffice() {
...(pack.site ? {} : { background: 0x11161c }),
...(pack.site ? { lighting: officeLighting(pack.site) } : {}),
...(pack.site ? { horizon: { drop: pack.site.elevation } } : {}),
// Environment-authored activity, not inferred headcount. These stable
// specs keep active HQs intentionally sparse and give the robot-jobs
// runtime ids, seeds and job sets it can adopt without a migration.
robots: activityRobotsForOffice(pack),
// 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.
@@ -1446,17 +1458,16 @@ function disposeLoadedOffice(): void {
* the interior, the furniture catalogue, the material registry and the
* floorplan are 67 kB of chunk — 22 kB across the wire — and they used to be
* downloaded, parsed and executed on every load of a map page by people who
* came to look at a city. Behind these three `await import()`s Vite gives them
* 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 three in one `Promise.all` because they are one arrival: the pack without
* 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 happens to emit them as three chunks the browser asks for
* together; awaiting them in sequence would make that three round trips on a
* slow link for no reason at all.
* 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
@@ -1468,20 +1479,22 @@ async function loadOffice(): Promise<{
createOfficeMinimap: typeof import("./engine/officeMinimap.ts").createOfficeMinimap;
pack: Office;
materials: MaterialRegistry;
robotOperations: RobotOperationsDefinition | null;
} | null> {
try {
// A fourth import and still one arrival. The plan renderer reads prop
// 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] = await Promise.all([
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
@@ -1493,6 +1506,7 @@ async function loadOffice(): Promise<{
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.");
@@ -1953,18 +1967,23 @@ function renderOfficeBadge() {
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;
officeBadge.hidden = !publicOffice && mediaSurfaces.length === 0 && robotActivity === null;
if (!publicOffice) {
if (mediaSurfaces.length === 0) return;
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;
officeBadge.textContent = active > 0
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(
@@ -1978,6 +1997,7 @@ function renderOfficeBadge() {
} else {
officeBadge.append(document.createTextNode("Sign in to see who's in."));
}
if (robotActivity) officeBadge.append(document.createTextNode(` ${robotActivity.disclosure}`));
}
/**