b25f217e3e
The world stops being a simulation of California and starts being California. **THE PROMOTION GATE WAS THE FIRST COMMIT, BEFORE ANY ORANGE PIXEL EXISTED.** On today's live store the SoCal board contains 22 incidents. Every one has NULL acreage and fifteen are nameless LA County dispatch numbers. Drawn naively that is 22 orange marks over Los Angeles on a day nothing is burning — in a frame that contains no other warm colour, so one glyph would be the most salient object on the board and twenty-two would spend its credibility permanently. `acres >= 10 AND contained < 80 AND type != 'RX' AND last_seen = max(last_seen)` returns 0 on SoCal, exactly 5 on California, 0 on the Bay — same body, same day, three correct answers. The empty board is a deliverable, not a fallback: it says "No active fire on this board — CAL FIRE and WFIGS, just now", states that 21 records were gated and why, lists the largest fires burning OUTSIDE the frame with distances, and counts the hot pixels it is deliberately not drawing. **The privacy leak is structurally impossible rather than carefully avoided.** cloud-1 serves a projection; the four home-relative columns never leave that box. `observations.threat` was the one that nearly got through — it is `(16/distance)^2 x log10(acres) x momentum x containment x wind-alignment`, so with acreage and containment public it inverts to a distance circle around a house and three fires give an intersection. A grep of the built bundle for distance_km, bearing_deg, threat, 7762 and the street name returns nothing. **Deliberately not used, and both would have produced a confident wrong answer:** the store's `air` table retains only the last parameter of each poll, so all 78 rows read "Good" while the live feed reports ozone 101 "Unhealthy for Sensitive Groups" — haze driven off it would clear the sky during a smoke event. And `weather` is written only inside the NWS alerts loop, so a quiet day stores no wind at all. Tera's own per-region NWS wind is already correct and already what the clouds drift on. Satellite detections are drawn as evidence and never as incidents. The permanent industrial heat source 4.7 km from the owner's house is flagged persistent and dropped, asserted by a test that first proves it is present in the fixture. MODIS integer confidence and VIIRS string confidence are branched on `sat`. **The LA office is a twin.** Its entire authored second storey — Model Loft, Model Bay, The Materials Room, 430 lines nobody had ever stood in — is reachable on foot: a walker crosses level-1 to level-2 in 73 fixed steps, floorY 0 to 5, verified against the real pack rather than a synthetic plan. Its two studio devices read real hardware through a field-allowlisted bridge: mute, volume and reachability only. Never level, because there is no passive level upstream and obtaining one would record a room with people in it. Never dB, because upstream is gainPct across four different native scales. The bridge refuses all writes. Fixed at its root: an anonymous visitor was getting permanently at-rest instruments backing off against a 401. The tier moves into `createDeviceSource`, so anon gets the living simulator three file headers already promised. **Item 8 is closed, not fixed, and the correction is the point.** The Bay Area "stutter" was GPU power management — the card sat at 500 MHz of 2725 through every run that reproduced it, 4096/2048/1024/256 shadow maps all render in 1.21-1.31 ms, and two consecutive runs over a byte-identical dist gave 33.4 then 16.7. The allowance is removed and the cell is back to 16.7. Geometry is the gate; frame time is advisory. Item 7 was re-scoped after measuring: 1,069,006 of the Bay Area's 2,265,056 triangles were the second submission of the same buildings into the shadow pass. Mobile now has its own triangle caps and bay-area mobile draws 1,266,096. Also: bridges and the freeway corridor light up at night as emission, not lights — 1,614 deck lamps and 18 tower heads on the Bay in two draw calls. The single change that made US-101 legible was moving its edge lines from the lit material to the unlit one: retroreflective paint, the argument the SFO night frame already makes. California went 21,991 lamps to 4,051, clustered at the 17 town districts, because a rural interurban corridor genuinely is unlit. Tests 1137 -> 1340, server 280. All ten budget cells pass on first attempt with no cap raised. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
824 lines
32 KiB
TypeScript
824 lines
32 KiB
TypeScript
/**
|
||
* 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;
|
||
/**
|
||
* `smokeCaption`'s sentence, when the fires on the board have browned this
|
||
* building's sky. `null` on every ordinary day.
|
||
*
|
||
* It sits with the other two disclosures rather than in the fire panel
|
||
* because it is a statement about *this room's* picture: the sky in here is
|
||
* derived from incidents on the map, not measured at this address, and a
|
||
* brown courtyard with nothing beside it saying why is the failure this
|
||
* exists to prevent.
|
||
*/
|
||
smokeNote?: 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,
|
||
// `rail`, not `meaning`: the sheet gets the sentence, the rail gets the
|
||
// glance. See the field's note in `shortcuts.ts` for what reusing the
|
||
// sentence here actually looked like on screen.
|
||
label: entry.rail ?? 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 ?? "", office.smokeNote ?? ""]
|
||
.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,
|
||
};
|
||
}
|