1
0

feat: tone-mapped render rig, studio devices, LA fidelity pass, UI overhaul

The build the studios needed, across eight workstreams and one strict file
partition.

**The render rig was the quality ceiling.** The renderer ran three's
NoToneMapping default while atmosphere drove the sun to 2.35 and assets set
emissives to 3.2, so every value above 1.0 hard-clipped to flat white — which is
why walls blew out and every fitting looked like a white rectangle. ACES filmic
tone mapping and an explicit output colour space land in `stage.ts`, and the
atmosphere intensity table and palette headroom are re-tuned against the new
curve rather than left tuned for the clipping we removed.

`engine/environmentRig.ts` builds a PMREM environment at runtime, procedurally,
so nothing binary is committed. There was no environment map anywhere before, so
every `metalness > 0` role had nothing to reflect and rendered dull grey — a
defect the code already documented against itself in `office/optimus.ts`, where a
whole material role was abandoned over it, and worked around in `modelX.ts` with
a fake emissive that this change deletes. Atmosphere remains the sole light
owner; the rig derives from the `LightingState` it already produced.

**Studio hardware exists.** There was no device concept anywhere in the product:
no type, no route, no state. `devices/types.ts` fixes a declaration/state/
capability/command contract that a smart light, a thermostat, a door sensor and a
charger all fit without a schema change, and both studios now carry a desk mic
and a computer speaker with deterministic simulated behaviour behind an adapter
seam a real API can occupy later. Reads are the demo and are open; commands are a
signed-in action and are kept off the read body entirely, because a shared cache
replaying a GET that turned a microphone on is exactly what the fail-closed
cache default exists to prevent.

**The ADS-B licence hole is closed.** `TERA_ADSB_ENDPOINT` accepted any URL, the
response was served publicly cacheable, and the attribution hardcoded adsb.lol
regardless of where the endpoint pointed — one env var away from republishing
non-redistributable data under an open-terms credit. The host is now allowlisted,
the credit is derived from the host actually configured, public cacheability is
conditional on redistributability, and a refused endpoint demotes to simulated
flights and says so in `degraded[]`. The gate is on the source, not the feature:
live aircraft and their detail cards stay open to anonymous visitors.

**The LA studio was never the smaller pack** — 16 rooms and 248 props against
SF's 4 and 28. Its deficit was fidelity per square metre: 98 of those props were
ceiling troffers, it bound no props to seats, placed none of the habitat kit, and
12 of its 16 rooms had no viewpoint. Density comes from new asset kinds rather
than more instances, because `furnish.ts` draws once per kind and folds colour
into the batch key, so repeat instances add nothing the eye can read.

**The interface stops being forty imperative mutations.** Every visibility
decision moves into a pure, tested `ui/chromeState.ts` and one applier, so the
chrome has coverage for the first time. Deleted: ~100 lines of CSS and two
bindings targeting elements that no longer exist, and a `body:has()` rule that
shifted the desktop layout by 160px for touch controls hidden there. Fixed: the
office picker tabs that drew their label and their badge on top of each other.
Added: a first-run flow, because the product is two verbs and neither was ever
stated on screen. Mobile is designed on its own terms instead of being the
desktop with things hidden — the plan view comes back, and the keyboard-only
shortcuts button is replaced by touch controls.

`arena/studioOps.ts` frames the whole thing as the multi-variable environment it
is, wrapping the same simulators the renderer drives rather than a headless copy.

Also removed `input/vehicle.ts`, which nothing but its own test imported.

Tests 385 -> 961, all passing. Typecheck, build, performance budgets across six
matrix cells, no-binaries, provenance, dependency licences, zero-config boot and
arena source hashes all green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-21 19:44:24 -07:00
parent 8738367258
commit db074e9cf7
150 changed files with 36237 additions and 2586 deletions
+807
View File
@@ -0,0 +1,807 @@
/**
* Every visibility decision this interface makes, as one pure function.
*
* `renderLegend()` in `main.ts` made about forty of them — `walkButton.hidden =
* inside ? office?.walker === null : city.actorState() === null`, and thirty-nine
* more like it — interleaved with the DOM writes that acted on them, reading
* eight pieces of module-level mutable state, and with **zero** test assertions
* behind any of it. On a product whose entire pitch is the interface, the
* interface was the only subsystem in the repo with no coverage at all. It was
* not testable in principle either: every decision needed a live `Stage`, a built
* heightfield and a WebGL context to reach.
*
* This module is the decisions and nothing else. No DOM, no THREE, no import of
* `main.ts`, no clock, no `localStorage`, no network — hand it a plain object,
* get a plain object back. `mount.ts` is the only thing that writes to the page,
* and it writes exactly what this returned.
*
* Two properties fall out of that split and both are the point:
*
* - **The matrix is enumerable.** Three access tiers × six control modes ×
* inside/outside × coarse/fine pointer is 72 states, and
* `src/test/ui/chromeState.test.ts` walks all of them. That is a thing you can
* only do to a function.
* - **The integration workstream makes one call.** `apply(chromeState(inputs))`
* replaces forty scattered mutations, so a new piece of chrome is a field here
* and a write there, rather than a fortieth line in a function nobody wants to
* touch.
*
* ### On the input shape
*
* The twelve top-level keys are fixed by the build spec and are the contract with
* `integration`; they are not negotiable and are not reordered. Everything past
* them is optional, defaulted, and exists because the forty decisions genuinely
* depend on more than twelve facts — the label on the Fly button needs to know
* which board you are on, and the label on the touch action button needs to know
* whether the crow is currently in the air. Making those optional rather than
* required is deliberate: an integration stub that passes only the twelve
* documented keys type-checks and returns a sane, if plainer, chrome.
*/
import type { Tier } from "../access.ts";
import type { DeviceDeclaration } from "../devices/types.ts";
import type { ControlMode } from "../play/controlMode.ts";
import { formatAircraftDetail, formatPlayHud } from "./hud.ts";
import type { AircraftDetailInput, AircraftDetailView, PlayHudView, PlayTelemetry } from "./hud.ts";
import { railHints } from "./shortcuts.ts";
import { BREAKPOINT } from "./tokens.ts";
// ---- Inputs ---------------------------------------------------------------
/** What this visitor is, as the chrome needs to know it. */
export interface ChromeAccess {
tier: Tier;
/** `null` when this deployment has no door to knock on — a clean self-hosted clone. */
signInUrl: string | null;
/** The auth subject, when there is one. */
subject: string | null;
/** The name this person chose, which outranks the subject in the badge. */
displayName?: string | null;
/** True once a local profile exists, which is what the Character button edits. */
hasProfile?: boolean;
}
export interface ChromeViewport {
width: number;
height: number;
/**
* A finger, not a mouse.
*
* Keyed off the pointer rather than the width, because the defect this decides
* is about fingers: an 820px iPad has no keyboard either, and it lands on the
* desktop layout by design (see `deviceProfile` in `engine/stage.ts`).
*/
coarsePointer: boolean;
}
/** Which of the three feeds is answering, and what the page may therefore claim. */
export interface ChromeFeeds {
markers: boolean;
weather: boolean;
flights: boolean;
/** A sky somebody typed retires the live-weather claim for as long as it is up. */
weatherOverridden?: boolean;
/** The people in this office are invented, and the caption must say so. */
sampleOccupancy?: boolean;
/** Licence lines from whichever live sources are answering. Shown in the `?` card. */
credits?: readonly string[];
}
/** Which board and which building, for the labels that name them. */
export interface ChromeBoard {
cityId?: string;
cityLabel?: string;
officeId?: string;
officeLabel?: string;
officeStatus?: "active" | "building";
/** The aircraft route only exists on the California board. */
isCalifornia?: boolean;
}
/**
* One tab in the board strip — a world outside a building, a building inside one.
*
* The lists live in the inputs rather than being fetched by the applier, so that
* `apply(chromeState(inputs))` really is *one* call: a second `setBoards()`
* method on the handle would be a second place for the chrome to be half
* updated, which is the class of bug this whole module exists to remove.
*/
export interface ChromeBoardTab {
id: string;
label: string;
/** "active" or "building" for an office; `null` for a city, which has no status. */
status?: "active" | "building" | null;
}
/** One row of the chapter list: a city chapter and an office viewpoint are both this. */
export interface ChromeView {
id: string;
/** The authored number, or `null` to take the ordinal. */
number?: string | null;
shortLabel: string;
description?: string;
}
/**
* What the detail card is showing.
*
* Two shapes, because there are two things worth picking: a marker, which is one
* authored sentence, and an aircraft, which is five observed fields and is the
* moment the map stops looking like a diagram. Modelled as a union rather than a
* string so the aircraft case cannot be flattened back into one on its way here,
* which is exactly what used to happen.
*/
export type ChromeDetail =
| { kind: "text"; text: string }
| { kind: "aircraft"; aircraft: AircraftDetailInput };
/** The body currently under control, for the labels that describe it. */
export interface ChromeActor {
kind?: "crow" | "dog" | "humanoid" | null;
/** A crow on the wing takes a different action pad from one on the ground. */
flying?: boolean;
}
/** Facts about the building you are standing in. */
export interface ChromeOffice {
/** Screens authored in this pack that a full-depth viewer may drive. */
mediaSurfaceCount?: number;
/** How many of them are showing something right now. */
mediaSurfaceActiveCount?: number;
/** `resolveRobotOperations`' one-sentence disclosure, when the pack has robots. */
robotDisclosure?: string | null;
/** The pack could not give this office a walker — a dollhouse-only building. */
walkable?: boolean;
}
export interface ChromeInputs {
mode: ControlMode;
/** Every mode reachable right now, per `controlModeAvailable`. */
available: readonly ControlMode[];
access: ChromeAccess;
inside: boolean;
/** `null` outside a building. "public" is the shell; "full" is the people too. */
officeDepth: "public" | "full" | null;
viewport: ChromeViewport;
feeds: ChromeFeeds;
/** One sentence per demotion the server reported. Admin-visible. */
degraded: readonly string[];
/** Authored hardware in this room. Empty outside one. */
devices: readonly DeviceDeclaration[];
/** Nothing in `localStorage` says this browser has been here before. */
firstVisit: boolean;
panelOpen: boolean;
planOpen: boolean;
// ---- Optional refinements; see the header ----
board?: ChromeBoard;
actor?: ChromeActor;
office?: ChromeOffice;
/** A marker or aircraft is picked, so the detail card has something to show. */
detailOpen?: boolean;
/** A camera is live in this tab, which raises the top-right column over modals. */
cameraLive?: boolean;
/** Hosted presence is connected, which adds a row to the top-right column. */
presenceVisible?: boolean;
/** The board strip's entries: cities outside a building, offices inside one. */
boards?: readonly ChromeBoardTab[];
/** The chapter list: city chapters outside, authored viewpoints inside. */
views?: readonly ChromeView[];
activeViewId?: string | null;
/** Whatever the current mode's controller is reporting, for the play HUD. */
telemetry?: PlayTelemetry | null;
/** The picked marker or aircraft, or `null`. Supersedes `detailOpen`. */
detail?: ChromeDetail | null;
/** The clock line, already formatted by whoever owns the clock. */
clockLabel?: string;
}
// ---- Output ---------------------------------------------------------------
/**
* Three layouts, not two breakpoints' worth of tweaks.
*
* `phone` is a different design and not a narrow desktop: no hover, so nothing
* may depend on it; no keyboard, so a card explaining the keyboard is furniture;
* and one thumb, which reaches the bottom third of the screen and not the top
* corners.
*/
export type ChromeLayout = "phone" | "compact" | "desktop";
export interface ChromeModeButton {
mode: ControlMode;
label: string;
visible: boolean;
pressed: boolean;
}
export interface ChromeTouchAction {
/** The DOM id in `index.html`, so the applier needs no second table. */
id: string;
label: string;
visible: boolean;
/** The held control it feeds, for the buttons that hold one. */
control: string | null;
/** The one-shot request it fires, for the buttons that fire one. */
edge: string | null;
}
export interface ChromeRailHint {
id: string;
keys: readonly string[];
label: string;
}
export interface ChromeBoardButton extends ChromeBoardTab {
active: boolean;
}
export interface ChromeViewRow {
id: string;
/** Always a string by the time it reaches the applier: authored, or the ordinal. */
number: string;
label: string;
active: boolean;
}
/** The detail card, resolved. `null` when nothing is picked. */
export type ChromeDetailView =
| { kind: "text"; text: string }
| { kind: "aircraft"; view: AircraftDetailView };
export interface ChromeState {
// ---- Frame ----
layout: ChromeLayout;
/** A finger drives this session, so on-screen controls are the primary path. */
touchFirst: boolean;
/** Standing in a building rather than looking at a board. */
inside: boolean;
/** Every class the applier sets on `<body>`, computed once so nothing drifts. */
bodyClasses: readonly string[];
// ---- The left column ----
panelVisible: boolean;
panelOpen: boolean;
/**
* Carried through so the applier can compute the *next* value for a toggle
* without keeping its own copy of the state — which is how a toggle ends up
* disagreeing with the thing it toggles.
*/
planOpen: boolean;
panelToggleVisible: boolean;
panelToggleLabel: string;
scrimVisible: boolean;
panelTitle: string;
panelSubtitle: string;
/** Which list the board strip is pointed at: worlds outside, buildings inside. */
boardPicker: "city" | "office";
boards: readonly ChromeBoardButton[];
clockLabel: string;
enterVisible: boolean;
enterLabel: string;
walkVisible: boolean;
walkPressed: boolean;
walkLabel: string;
flyVisible: boolean;
flyPressed: boolean;
flyLabel: string;
screensVisible: boolean;
screensLabel: string;
devicePanelVisible: boolean;
devicePanelLabel: string;
/**
* The declarations themselves, carried through so the applier can build the
* panel without a second call. Pass-through, not a decision — but it belongs in
* the state for the same reason the board and view lists do: one call, one
* consistent frame of chrome.
*/
deviceDeclarations: readonly DeviceDeclaration[];
chaptersVisible: boolean;
views: readonly ChromeViewRow[];
blurbVisible: boolean;
blurbText: string;
// ---- Top right ----
tierVisible: boolean;
tierTone: Tier;
tierLabel: string;
/**
* What signing in **adds**, for a visitor who has not.
*
* `null` for anyone it would not help. The badge used to read "Public view"
* and stop there, which names the visitor rather than the offer and is the one
* place in the product where the anon-first promise reads as a demotion.
*/
tierAdds: string | null;
whoLabel: string | null;
characterVisible: boolean;
signInVisible: boolean;
signInLabel: string;
signInHref: string | null;
presenceVisible: boolean;
cameraAlert: boolean;
minimapVisible: boolean;
minimapReadoutVisible: boolean;
// ---- The dock and the HUD ----
modeDockVisible: boolean;
modeButtons: readonly ChromeModeButton[];
playHudVisible: boolean;
/** The three strings and the warning flag, or `null` in the orbit modes. */
playHud: PlayHudView | null;
// ---- The rail ----
railVisible: boolean;
detailVisible: boolean;
detail: ChromeDetailView | null;
helpVisible: boolean;
planToggleVisible: boolean;
planTogglePressed: boolean;
railHintsVisible: boolean;
railHints: readonly ChromeRailHint[];
// ---- Touch ----
touchControlsVisible: boolean;
stickVisible: boolean;
stickLabel: string;
touchActions: readonly ChromeTouchAction[];
// ---- Honesty, and the office ----
sourceVisible: boolean;
sourceLabel: string;
sourceLive: boolean;
officeInviteVisible: boolean;
officeInviteText: string;
officeInviteLinkLabel: string | null;
officeNoteVisible: boolean;
officeNoteText: string;
degradedVisible: boolean;
degradedLines: readonly string[];
/**
* The credit lines the live sources ask for, shown in the `?` card.
*
* MET Norway and Open-Meteo publish under CC BY 4.0 and the API forwards the
* line each of them asks for; adsb.lol asks to be named for the positions. All
* of it arrived, was parsed, and was then read by nobody — a licence
* obligation plumbed to within one line of being met. Empty when nothing live
* is on screen, because a credit for data nobody is looking at is noise and
* the zero-config build owes nobody anything.
*/
credits: readonly string[];
// ---- First run ----
onboardingVisible: boolean;
canvasLabel: string;
}
// ---- The dock's five buttons ---------------------------------------------
const DOCK_MODES: readonly { mode: ControlMode; label: string }[] = [
{ mode: "overview", label: "View" },
{ mode: "drive", label: "Drive" },
{ mode: "actor", label: "Explore" },
{ mode: "aircraft", label: "Fly" },
{ mode: "office-walk", label: "Walk" },
];
/**
* Modes in which a body is under control and the touch surface applies.
*
* `office-overview` is not one of them, and neither is `overview`: both are
* orbit-camera states where the joystick would move nothing.
*/
const PLAY_MODES: readonly ControlMode[] = ["drive", "actor", "aircraft", "office-walk"];
function isPlayMode(mode: ControlMode): boolean {
return PLAY_MODES.includes(mode);
}
/**
* The liveness phrase, restated here rather than imported.
*
* `describeLiveness` lives in `adapters/http.ts`, which cannot be imported under
* Node's type stripping today and which pulls a network client behind it. The
* wording is the same and the rule is the same: all three live is the only case
* that may say "live data", because "live data" printed over invented companies
* because a weather station answered is precisely the claim the flag exists to
* prevent.
*/
function describeFeeds(feeds: ChromeFeeds): string {
const weather = feeds.weather && feeds.weatherOverridden !== true;
const parts: string[] = [];
if (feeds.markers && weather && feeds.flights) return "live data";
if (feeds.markers) parts.push("live markers");
if (weather) parts.push("live weather");
if (feeds.flights) parts.push("live traffic");
return parts.join(" · ");
}
function layoutFor(viewport: ChromeViewport): ChromeLayout {
const width = Number.isFinite(viewport.width) ? viewport.width : BREAKPOINT.compact + 1;
if (width <= BREAKPOINT.phone) return "phone";
if (width <= BREAKPOINT.compact) return "compact";
return "desktop";
}
function actorNoun(actor: ChromeActor | undefined): string {
const kind = actor?.kind ?? null;
if (kind === "crow") return "your crow";
if (kind === "dog") return "your dog";
if (kind === "humanoid") return "your humanoid";
return "a body";
}
// ---- Seeds ----------------------------------------------------------------
/**
* Should the left column start open at this width?
*
* The width only *seeds* these two; the moment somebody presses the toggle the
* viewport stops having an opinion. That rule was already right and is kept —
* what was wrong was that the seed lived inline in `main.ts` next to the
* `resize` listener that re-ran one of the two and not the other, so a rotation
* from portrait to landscape re-seeded the plan and left the panel sheet closed
* over a desktop-width layout.
*/
export function seedPanelOpen(width: number): boolean {
return !(Number.isFinite(width) && width <= BREAKPOINT.compact);
}
/**
* Should the plan start visible at this width?
*
* **Yes, at every width, and that is the change.** It used to be
* `window.innerWidth > 600`, which meant a phone started with the plan off — and
* a phone has no `M`. Every visible control at 390px was enumerated once and
* none of them could turn it on, so the bottom-sheet plan layout `index.html`
* had carried for months was unreachable on the device it was drawn for. The
* `#plan-toggle` button closed the "no way to turn it on" half; this closes the
* "it was off to begin with" half, and the phone stylesheet gives the plan a
* glanceable 8.5rem home in the top-right column rather than a sheet that eats
* the map.
*/
export function seedPlanOpen(_width: number): boolean {
return true;
}
// ---- The function ---------------------------------------------------------
/**
* Resolve the whole interface for one instant. Pure, total, and cheap enough to
* call on every frame if the caller wants to.
*/
export function chromeState(inputs: ChromeInputs): ChromeState {
const layout = layoutFor(inputs.viewport);
const phone = layout === "phone";
const touchFirst = inputs.viewport.coarsePointer;
const inside = inputs.inside;
const mode = inputs.mode;
const available = inputs.available;
const board = inputs.board ?? {};
const office = inputs.office ?? {};
const actor = inputs.actor ?? {};
const tier = inputs.access.tier;
const can = (candidate: ControlMode): boolean => available.includes(candidate);
const walking = mode === "office-walk";
const exploring = mode === "actor";
const flying = mode === "aircraft";
const driving = mode === "drive";
const playing = isPlayMode(mode);
const crowFlying = exploring && actor.kind === "crow" && actor.flying === true;
const cityLabel = board.cityLabel ?? "";
const officeLabel = board.officeLabel ?? "the studio";
const officeStatus = board.officeStatus ?? "building";
// ---- The dock -----------------------------------------------------------
//
// Hidden entirely when `overview` is the only thing available. On two of the
// three city boards this rendered as a lit pill holding one already-pressed
// button that did nothing — a control whose only state is its current one is
// not a control, it is a label pretending to be one.
const modeButtons: ChromeModeButton[] = DOCK_MODES.map(({ mode: dockMode, label }) => {
// The "View" button stands for both orbit states: `overview` outside a
// building and `office-overview` inside one. They are one idea — the camera
// is yours and nothing else is — and two buttons for it would swap under the
// viewer as they walked through a door.
const visible =
dockMode === "overview" ? can("overview") || can("office-overview") : can(dockMode);
const pressed =
dockMode === "overview" ? mode === "overview" || mode === "office-overview" : mode === dockMode;
return { mode: dockMode, label, visible, pressed };
});
const modeDockVisible = modeButtons.filter((button) => button.visible).length > 1;
// ---- The touch action pad ----------------------------------------------
//
// Built from the mode rather than from a pile of independent `hidden`
// assignments, which is what this was: eight `if (touchX) touchX.hidden = …`
// lines whose conditions overlapped and could not all be true or all be false.
const touchActions: readonly ChromeTouchAction[] = [
{
id: "touch-primary",
label: driving ? "Handbrake" : flying ? "Throttle" : crowFlying ? "Climb" : "Sprint",
visible: playing && !walking,
control: crowFlying ? "ascend" : "primary",
edge: null,
},
{
id: "touch-secondary",
label: "Glide",
visible: crowFlying,
control: "secondary",
edge: null,
},
{ id: "touch-pitch-up", label: "Pitch +", visible: crowFlying || flying, control: "pitch-up", edge: null },
{ id: "touch-pitch-down", label: "Pitch ", visible: crowFlying || flying, control: "pitch-down", edge: null },
{ id: "touch-assist", label: "Assist", visible: driving || flying, control: null, edge: "assist" },
{ id: "touch-reset", label: "Reset", visible: driving || flying, control: null, edge: "reset" },
{ id: "touch-camera", label: "Camera", visible: driving, control: null, edge: "camera" },
{ id: "touch-map", label: "Map", visible: true, control: null, edge: null },
];
/**
* The joystick, and the defect it fixes.
*
* `pointerStick.ts` has carried a real analogue stick since the play modes
* landed, and it was shown only while a body was already under control — which
* on a phone in VIEW mode meant the screen offered no movement control at all,
* and no way to discover that one existed. It is now visible in every play
* mode *and* announced by the onboarding coach, so the first thing a thumb
* finds is the thing that moves.
*/
const touchControlsVisible = touchFirst && playing;
// ---- The rail -----------------------------------------------------------
//
// `?` is a keyboard reference. On a device with no keyboard it was still being
// drawn, which is the interface offering a person a list of keys they cannot
// press. On a coarse pointer the same button remains — the sheet now carries
// the touch and on-screen half of every row — but it is labelled for what it
// is there.
const hints = railHints(mode, touchFirst).map((entry) => ({
id: entry.id,
keys: entry.keys,
label: entry.meaning,
}));
// ---- The honesty line ---------------------------------------------------
const sampleOccupancy = inside && inputs.feeds.sampleOccupancy === true;
const liveness = describeFeeds(inputs.feeds);
const sourceLabel = sampleOccupancy
? "sample occupancy · these people are invented"
: liveness;
// ---- The tier badge -----------------------------------------------------
//
// The label names what you get; the second line names what you would gain.
// "Public view" said neither, and on a product designed anon-first it was the
// one string that framed the default visitor as a lesser one.
const tierLabel = tier === "god" ? "Godmode" : tier === "member" ? "Full view" : "Open demo";
const tierAdds =
tier === "anon" && inputs.access.signInUrl !== null
? "Sign in adds the people: who is in the room, your own character on the floor, and the studio desk."
: null;
// ---- The studio's one invitation ---------------------------------------
//
// It used to live on `#office-badge` inside `#panel`, and on a phone `#panel`
// is a bottom sheet that starts closed — so the only in-office call to sign in
// was behind a hamburger on the device least likely to open one. This is the
// same move `renderSource` already made for the occupancy disclosure, applied
// to the message that is an offer rather than a disclosure.
const publicOffice = inside && inputs.officeDepth === "public";
const officeInviteText = publicOffice
? inputs.access.signInUrl !== null
? "You are seeing the building, not the people."
: "This deployment has no sign-in, so the floor is always the building alone."
: "";
const mediaCount = office.mediaSurfaceCount ?? 0;
const mediaActive = office.mediaSurfaceActiveCount ?? 0;
const robotDisclosure = office.robotDisclosure ?? null;
const mediaNote =
mediaCount === 0
? ""
: mediaActive > 0
? `${mediaActive} of ${mediaCount} ${mediaCount === 1 ? "screen" : "screens"} active · stop control in Office screens.`
: `${mediaCount} ${mediaCount === 1 ? "screen" : "screens"} ready · media stays off until you opt in.`;
const officeNoteText = [mediaNote, robotDisclosure ?? ""].filter((part) => part !== "").join(" ");
// ---- Onboarding ---------------------------------------------------------
//
// Not while a modal-ish state is up, not inside a building, and not while a
// body is already under control — a coach that teaches "drag to orbit" over a
// scene somebody is already driving is a coach interrupting the thing it was
// written to cause.
const onboardingVisible = inputs.firstVisit && !inside && mode === "overview";
// ---- The two lists -------------------------------------------------------
//
// Resolved here rather than in the applier so the applier has no decisions
// left to make. The board strip is one control pointed at whichever list is
// currently the answer to "which of these am I in"; a second, separate office
// strip was the obvious alternative and is worse, because it would sit dead
// and greyed out for the whole time anybody is looking at the city.
const activeBoardId = inside ? board.officeId : board.cityId;
const boards: readonly ChromeBoardButton[] = (inputs.boards ?? []).map((tab) => ({
...tab,
active: tab.id === activeBoardId,
}));
const views: readonly ChromeViewRow[] = (inputs.views ?? []).map((view, i) => ({
id: view.id,
number: view.number ?? String(i + 1).padStart(2, "0"),
label: view.shortLabel,
active: view.id === inputs.activeViewId,
}));
const activeView = (inputs.views ?? []).find((view) => view.id === inputs.activeViewId);
const blurbText = activeView?.description ?? "";
const detailInput = inputs.detail ?? null;
const detail: ChromeDetailView | null =
detailInput === null
? null
: detailInput.kind === "text"
? { kind: "text", text: detailInput.text }
: { kind: "aircraft", view: formatAircraftDetail(detailInput.aircraft) };
const playHud = formatPlayHud(inputs.telemetry ?? null);
const canvasLabel = inside
? walking
? `${officeLabel}, following ${actorNoun(actor)}. Use W A S D to move, or the joystick.`
: `${officeLabel}, seen from above. Drag to orbit, scroll to zoom.`
: driving
? `${cityLabel}, following your car. Use W A S D to drive.`
: flying
? `${cityLabel}, following your electric aircraft. Use W A S D to fly, or P to resume assisted flight.`
: exploring
? `${cityLabel}, following ${actorNoun(actor)}. Use W A S D to move, Q and E for altitude, G to glide.`
: `Map of ${cityLabel}, seen from above. Drag to orbit, scroll to zoom.`;
const bodyClasses: string[] = [];
if (!inputs.panelOpen) bodyClasses.push("panel-closed");
if (!inputs.planOpen) bodyClasses.push("minimap-off");
if (sampleOccupancy) bodyClasses.push("sample-occupancy");
if (inputs.presenceVisible === true) bodyClasses.push("presence-on");
if (touchFirst) bodyClasses.push("touch-first");
if (inside) bodyClasses.push("inside");
if (playing) bodyClasses.push("playing");
if (inputs.cameraLive === true) bodyClasses.push("camera-live");
bodyClasses.push(`layout-${layout}`);
return {
layout,
touchFirst,
inside,
bodyClasses,
panelVisible: true,
panelOpen: inputs.panelOpen,
planOpen: inputs.planOpen,
// Above 900 the column is furniture and always there, so the toggle would be
// a button that hides the thing it is standing on top of.
panelToggleVisible: layout !== "desktop",
panelToggleLabel: inside ? officeLabel : cityLabel,
scrimVisible: phone && inputs.panelOpen,
panelTitle: inside ? officeLabel : cityLabel,
panelSubtitle: inside
? `Tera · ${officeStatus} environment`
: "Tera · Lumbridge Simulate",
boardPicker: inside ? "office" : "city",
boards,
clockLabel: inputs.clockLabel ?? "",
enterVisible: true,
enterLabel: inside
? "← Back to the city"
: officeStatus === "active"
? `Open ${officeLabel}`
: `Preview ${officeLabel} · building →`,
walkVisible: inside ? office.walkable !== false && can("office-walk") : can("actor"),
walkPressed: walking || exploring,
walkLabel: inside
? walking
? "Return to overview ↑"
: `Walk as ${actorNoun(actor)}`
: exploring
? "Return to flyover ↑"
: `Explore as ${actorNoun(actor)}`,
flyVisible: !inside && board.isCalifornia === true && can("aircraft"),
flyPressed: flying,
flyLabel: flying ? "Return to flyover ↑" : "Fly the California route →",
screensVisible: inside && inputs.officeDepth === "full" && mediaCount > 0,
screensLabel: "Office screens →",
// Anonymous visitors see the hardware too. The declarations are authored into
// the pack and are therefore public by construction; what an account buys is
// the live *readings*, and the panel says which it is showing.
devicePanelVisible: inside && inputs.devices.length > 0,
devicePanelLabel:
inputs.devices.length === 1 ? "Studio hardware →" : `Studio hardware · ${inputs.devices.length}`,
deviceDeclarations: inputs.devices,
chaptersVisible: views.length > 0,
views,
// On a phone the panel is a bottom sheet with a thumb-sized list in it; a
// paragraph of description under that list pushes the door off the screen.
blurbVisible: !phone && blurbText !== "",
blurbText,
tierVisible: true,
tierTone: tier,
tierLabel,
tierAdds,
whoLabel: inputs.access.displayName ?? inputs.access.subject,
characterVisible: inputs.access.hasProfile === true,
signInVisible: inputs.access.subject === null && inputs.access.signInUrl !== null,
signInLabel: "Sign in",
signInHref: inputs.access.signInUrl,
presenceVisible: inputs.presenceVisible === true,
cameraAlert: inputs.cameraLive === true,
minimapVisible: inputs.planOpen,
// Hover-only text on a device with no hover.
minimapReadoutVisible: !touchFirst,
modeDockVisible,
modeButtons,
playHudVisible: playing && playHud !== null,
playHud,
railVisible: true,
detailVisible: detail !== null || inputs.detailOpen === true,
detail,
helpVisible: true,
// For pointers, and only for pointers: where there is a keyboard the `M`
// hint is the affordance and a button saying the same thing is a second row
// of glass for nothing.
planToggleVisible: touchFirst || phone,
planTogglePressed: inputs.planOpen,
railHintsVisible: hints.length > 0,
railHints: hints,
touchControlsVisible,
stickVisible: touchControlsVisible,
stickLabel: walking
? "Walk"
: driving
? "Steer"
: flying
? "Fly"
: crowFlying
? "Bank"
: "Move",
touchActions,
sourceVisible: sourceLabel !== "",
sourceLabel,
sourceLive: !sampleOccupancy && liveness !== "",
officeInviteVisible: publicOffice,
officeInviteText,
officeInviteLinkLabel:
publicOffice && inputs.access.signInUrl !== null ? "Sign in for the live floor" : null,
officeNoteVisible: inside && officeNoteText !== "",
officeNoteText,
// The demotions are an operator's diagnostic, not a visitor's caption. They
// are shown in the `?` card to the tier that could act on them.
degradedVisible: tier === "god" && inputs.degraded.length > 0,
degradedLines: inputs.degraded,
// The weather override is somebody's invention; it is not MET Norway's sky
// and must not be attributed to them.
credits: [...new Set((inputs.feeds.credits ?? []).filter((line) => line.trim() !== ""))],
onboardingVisible,
canvasLabel,
};
}