1
0

feat: add active SF and LA HQ environments

This commit is contained in:
2026-08-19 02:51:17 -07:00
parent afa285bd5c
commit 0557a26e6b
14 changed files with 1096 additions and 2312 deletions
+45 -28
View File
@@ -53,10 +53,11 @@ import {
SAMPLE_MARKERS,
SAMPLE_PALETTE,
SAMPLE_PRESENCE_PALETTE,
samplePresenceAt,
samplePresenceForOfficeAt,
sampleRoutesFor,
} from "./adapters/sample.ts";
import { OFFICE_SITES } from "./offices/sites.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";
@@ -213,11 +214,17 @@ function journeyToCity(city: JourneyCity): void {
* a second pack eagerly imported would put its furniture in the entry bundle
* for every visitor who never opens it.
*/
const OFFICES: { id: string; label: string; load: () => Promise<{ default: Office }> }[] = [
{ id: "lumbridge-hq", label: "Lumbridge HQ", load: () => import("./offices/lumbridge-hq.ts") },
{ id: "frontier-valley", label: "Frontier Valley", load: () => import("./offices/frontier-valley.ts") },
{ id: "mateo-court", label: "Mateo Court", load: () => import("./offices/mateo-court.ts") },
];
const OFFICE_LOADERS: Readonly<Record<ShippedOfficeId, () => Promise<{ default: Office }>>> = {
"lumbridge-hq": () => import("./offices/lumbridge-hq.ts"),
"frontier-valley": () => import("./offices/frontier-valley.ts"),
"mateo-court": () => import("./offices/mateo-court.ts"),
};
const OFFICES = OFFICE_SITES.map((entry) => ({
...entry,
label: entry.name,
load: OFFICE_LOADERS[entry.id],
}));
const canvas = document.querySelector<HTMLCanvasElement>("#scene");
if (!canvas) throw new Error("#scene canvas missing");
@@ -299,7 +306,7 @@ const OFFICE_MARKERS: Marker[] = OFFICE_SITES.map((entry) => ({
id: `office:${entry.id}`,
label: entry.name,
colorKey: "office",
blurb: `${entry.site.label ?? "An office"} — click to walk in`,
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
@@ -450,7 +457,7 @@ let officeMaterials: MaterialRegistry | null = null;
* memoised forever, which is the one-line difference from the arrangement that
* could only ever hold one office.
*/
let officeId = OFFICES[0]?.id ?? "lumbridge-hq";
let officeId: string = OFFICES[0]?.id ?? "lumbridge-hq";
/**
* The office's own sky, when its pack says where it stands.
*
@@ -1263,15 +1270,10 @@ async function enterOffice() {
...(pack.site ? {} : { background: 0x11161c }),
...(pack.site ? { lighting: officeLighting(pack.site) } : {}),
...(pack.site ? { horizon: { drop: pack.site.elevation } } : {}),
// Two per floor, whatever floors this pack has — so the two-storey tower
// gets four and the single-storey hangar gets two, without either pack
// having to know about robots. Derived from the levels rather than
// written down, because a pack that gains a storey should not need an
// edit here to be staffed.
robots: pack.levels.flatMap((level) => [
{ levelId: level.id, id: `${level.id}-a` },
{ levelId: level.id, id: `${level.id}-b` },
]),
// 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),
depth,
materials,
// Ignored entirely at `"public"` depth, where no layer is built to colour.
@@ -1527,7 +1529,7 @@ function watchOccupancy() {
if (!scene || scene.depth !== "full") return;
presenceWatch = tera.watchPresence(officePack?.id ?? "lumbridge-hq", (body) => {
livePresence = body?.people ?? null;
presenceIsSample = livePresence === 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
@@ -1564,7 +1566,9 @@ function stopWatchingOccupancy() {
*/
function applyPresence() {
if (!office || office.depth !== "full") return;
const people = livePresence ?? samplePresenceAt(currentInstant());
const fallback = samplePresenceForOfficeAt(officePack?.id ?? officeId, currentInstant());
const people = livePresence ?? fallback;
presenceIsSample = livePresence === null && fallback.length > 0;
office.setPresence(people);
officePlan?.setPresence(people);
}
@@ -1627,18 +1631,26 @@ function renderCityPicker() {
if (!cityNav) return;
cityNav.replaceChildren();
const entries = inside
? OFFICES.map((o) => ({ id: o.id, label: o.label, active: o.id === officeId }))
: CITIES.map((c) => ({ id: c.id, label: c.label, active: c.id === cityId }));
? 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 = "city";
b.className = entry.status ? "city office-choice" : "city";
b.type = "button";
b.setAttribute("aria-pressed", String(entry.active));
b.textContent = entry.label;
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);
@@ -1719,14 +1731,19 @@ function renderLegend() {
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 ? "Spaces · a Lumbridge office" : "Tera · Lumbridge Simulate";
subtitle.textContent = inside
? `Tera · ${selectedOffice?.status ?? "building"} environment`
: "Tera · Lumbridge Simulate";
}
if (enterButton) {
// One label for everyone. The door is open at both tiers; what differs is
// what is behind it, and that is the badge's job to say, not the button's.
enterButton.textContent = inside ? "← Back to the city" : "Enter the office →";
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);