feat: add active SF and LA HQ environments
This commit is contained in:
+45
-28
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user