feat: add deterministic office robot jobs
This commit is contained in:
+39
-19
@@ -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}`));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user