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
+217 -15
View File
@@ -4,8 +4,11 @@ An **office pack** is one JSON-shaped object describing the inside of a
building: floor slabs, walls, holes in the walls, furniture, seats, and a few
camera poses. `src/interiors/types.ts` is the contract — it is short, it is
commented, and it wins any argument with this document. `lumbridge-hq.ts` in
this directory is a worked example of every feature described here, and copying
it is the intended way to start.
this directory is the small worked example — one storey, four rooms, every core
feature — and copying it is the intended way to start. `mateo-court.ts` is the
large one: two storeys, sixteen rooms, seat bindings, device declarations and a
courtyard, and it is the file to read when you want to see a feature used in
anger rather than demonstrated.
Nothing in a pack requires an account, a key or a network. If you can run the
repo you can author an office, and if you can author an office you can hand
@@ -34,6 +37,7 @@ export const ACME_HQ: Office = {
deskBanks: [...],
seats: [...],
zones: [...],
devices: [...], // smart hardware; see "Devices" below
},
},
],
@@ -91,14 +95,88 @@ walls the light actually comes through. `0` means your "north" really is north.
`elevation` is the other one worth thinking about, because it is what the horizon
is measured from — the difference between an office on the 48th floor and a shed
on an airfield is one number, and it is this one. Both shipped packs are worked
examples: `lumbridge-hq.ts` is 188 m up and rotated 205°, `frontier-valley.ts` is
4 m up and square to the compass.
on an airfield is one number, and it is this one. All three shipped packs are
worked examples: `lumbridge-hq.ts` is 188 m up and rotated 205°,
`frontier-valley.ts` is 4 m up and square to the compass, and `mateo-court.ts` is
1.2 m up — a loading dock — and turned 36° onto the 1781 pueblo grid that
downtown Los Angeles still follows.
### Where the car goes
A sited building may also say where a vehicle stands outside it:
```ts
arrival: {
levelId: "level-1",
position: { x: 22.6, z: -3.4 }, // metres, in YOUR plan frame
rotation: -Math.PI / 2, // Yaw: which way the car points
kind: "vehicle-stall",
label: "Mateo Street kerb",
},
```
`src/engine/officeExterior.ts` builds an apron and a vehicle there. The position
is in **the pack's own frame**, the same one the walls are in — which is why a
stall on the street side of a façade authored at `z = 0` has a *negative* `z`,
and why you can see it is outside the building without converting anything.
Put it on ground that is genuinely outside: `Plan` checks the level, the kind and
that the numbers are numbers, and deliberately does **not** rule on whether the
stall is inside the footprint, because a covered undercroft and a courtyard are
things a pack might legitimately mean. `src/test/packs/arrivalAnchors.test.ts`
is where the three shipped packs assert that theirs are on the street, the
podium kerb and the apron.
One trap, and it is `lumbridge-hq`'s: a pack whose level-0 floor is 188 m above
the ground outside has no pavement to park on at all. Its stall is authored
beside its own front door because the plan frame is the only frame a pack has,
and what "outside" means *vertically* for a tower is the exterior layer's
decision. If your building is up in the air, say so in a comment where the next
person will find it.
Nothing here is geocoded and nothing can be. These are numbers you type, like
every other number in a pack — see CONTRACT.md §8 for why a coordinate's
provenance is a licensing question in this repo.
## Levels, and the one thing you cannot do with a second one
A `Level` is a storey: an `elevation` (floor-to-**floor**, not floor-to-ceiling),
a default wall height, and its own floorplan in its own frame with its slab at
zero. `Plan` adds the elevation to every coordinate on the level exactly once, so
you author an upper floor without holding 5 m in your head.
> ### ⚠️ A walker cannot change levels. Do not author a staircase.
>
> **This is the single most expensive thing to discover by building it.** The
> walk controller is hard-locked to the level it spawned on, in two places:
>
> - `src/interiors/walker.ts` rejects any state whose `levelId` differs from the
> spawn level — a restored or proposed state on another storey is refused as
> *"walker snapshot is incompatible or invalid"*.
> - `src/interiors/officeWalker.ts` **throws** if the level it is on stops
> resolving — *"office walker lost level"*.
>
> There is no vertical transition anywhere in the engine: no stair traversal, no
> lift, no level handoff, and nothing that changes `levelId` after a spawn. So a
> flight of stairs you author is a flight of stairs the collider will never let
> anybody climb, however carefully you model it. `mateo-court.ts` has a real
> dog-leg stair standing in its courtyard, drawn as a floor finish with a 1.2 m
> gap in the balustrade at the head of it, and **its upper floor is unreachable
> on foot**. That is a known, accepted state and not a bug in the pack.
>
> Fixing it is an **engine** change — a cross-level walker state, a transition
> volume, and a collider that knows about both storeys — and it is out of scope
> for a pack author. Until it lands:
>
> - Author a second storey for what it *is*: a place the camera flies to, which
> viewpoints do perfectly well. `mateo-court` gives its upper floor five of
> them.
> - Give each level a viewpoint of its own so nothing up there is unreachable by
> every means at once.
> - Do not spend a day on treads. `viewpoints[0]` is the walk spawn, and it is on
> exactly one level; everything else on that level is walkable and everything
> on any other level is not.
## Rooms are slabs. Walls are segments.
This is the load-bearing idea, and the thing most people get backwards on the
@@ -117,9 +195,10 @@ Consequences worth stating out loud:
id if you ever want to name it.
- Rooms may overlap and may leave gaps. `Plan.roomAt` resolves *later* rooms
first, so a room declared after another wins the lookup where they cross.
Overlapping is legal but the reference pack avoids it — two coplanar slabs at
the same height is a z-fight waiting for the wrong GPU, so the open floor is
notched around the focus booths rather than passing underneath them.
Overlapping is legal but every shipped pack avoids it — two coplanar slabs at
the same height is a z-fight waiting for the wrong GPU, so `mateo-court`
notches its courtyard around the stair standing in it rather than laying one
slab over the other.
Rooms and walls should be authored against the **same numbers**. A wall is
centred on its line and straddles the boundary between the two slabs meeting
@@ -133,10 +212,18 @@ means **no ceiling at all** — an atrium, a void, or a room you want to look do
into. `{ height, surface }` overrides one room.
An office you look down into from an establishing viewpoint cannot have lids on
the rooms you are trying to see, so most of the reference pack declares
`ceiling: null`. The three that keep theirs the focus booths, the server room,
the store — are the three you are never meant to see inside, and a ceiling is a
cheap way of saying so.
the rooms you are trying to see, so most of both large packs declare
`ceiling: null`. The rooms that keep theirs are the ones you are never meant to
see inside — the bath and storage in the SF studio, the equipment store in the LA
one — and a ceiling is a cheap way of saying so.
`mateo-court` uses the field in a third sense that is worth knowing about: its
courtyard and its stair are `ceiling: null` because they are **outside**, and
there is nothing above them at any height. Nothing in the format had to change
for that; the field already said what was needed. One consequence is worth
carrying, though: a room with no ceiling is a room with nothing to hang a light
fitting from, and a ceiling grid over an open courtyard is a lighting plan for a
different building.
## Doors and windows are openings, not props
@@ -339,6 +426,98 @@ only if you place its centre half its own depth off the wall's face:
deep and `tera:whiteboard` is 0.10 m. Name those offsets as constants; you will
use them a dozen times.
## Devices
A pack may declare smart hardware — a mic, a speaker — on its floorplan:
```ts
devices: [
{
id: "la-front-mic",
kind: "mic",
label: "Front desk mic",
assetId: "tera:device.mic.desk",
anchor: { levelId: "level-1", propId: "front-mic", roomId: "lobby", seatId: "front-01" },
capabilities: CANONICAL_CAPABILITIES.mic,
provenance: "simulated",
disclosure: "Simulated studio hardware. These readings are demonstration data…",
},
],
```
`src/devices/types.ts` owns the type and is worth reading; four things about it
matter when you are authoring one.
**A device has no coordinate.** `anchor.propId` is required and it *is* the
position: the device derives its transform from that prop's, plus an optional
`offset` in the prop's own frame for the few centimetres between a desk's origin
and the top of a mic stand. Nudge the desk and the mic goes with it, because
there was never a second number to forget. Same rule `Prop.seat` follows for
chairs, and the same one a pack follows by importing its site instead of
restating the coordinates.
**The anchor prop is the hardware.** Its `kind` is the device asset —
`<namespace>:device.<kind>.<placement>`, so `tera:device.mic.desk` and a
self-hoster's `acme:device.mic.boom` both read as a mic with nothing registered.
`Plan` drops a declaration whose anchor prop is device hardware of the *wrong*
kind: a mic bolted to a speaker is not a rendering mistake, it is a command
routed to the wrong instrument.
**`capabilities` should be `CANONICAL_CAPABILITIES[kind]`.** The device panel
builds its controls by walking that array and the arena's observation width is
the sum of them, so two studios describing a mic differently changes the shape of
an RL observation without anybody editing the arena.
**`disclosure` is mandatory and it is checked.** A declaration with
`provenance: "simulated"` whose disclosure does not contain the word is reported
as a problem and dropped — the same check `RobotOperationsDefinition` gets, for
the same reason. A level meter that moves, with nothing beside it saying where
the number came from, is a claim about a real room.
What a device is *doing* — powered, muted, its level in dBFS — is a `DeviceState`
and **never appears in a pack**. It arrives over the API from a route that can
refuse an anonymous caller, exactly as `Presence` does. A declaration is a
description of a room and is safe to publish; a reading is not, and the split is
the whole design.
## How full a room should be, and the trap in the answer
The number to aim at is **0.26 non-light props per square metre**, building-wide,
with no room over 20 m² below **0.15**. Both shipped studios clear it:
`lumbridge-hq` sits at 0.28 over 100 m², `mateo-court` at 0.29 over 1246 m².
`src/test/packs/mateoContent.test.ts` measures it if you want the exact method —
it counts prop centres by `Plan.roomAt`, and it excludes every `tera:light.*`
because a ceiling grid will satisfy any prop count you like while leaving the
floor bare. Mateo Court's first version proved that: ninety-eight of its props
were troffers, two of the grids hung in rooms declared `ceiling: null`, and it
looked empty from every viewpoint it had.
**But the ratio is the easy half, and on its own it is a lie.** `furnish.ts`
batches props by `(asset, colorKey)` and draws `ctx.rand` **once per batch**, so
every instance of a kind is geometrically identical — the same seeded jitter,
the same books on the same shelf, the same leaves on the same plant. Ten more
shelves in a room are one shelf drawn ten times. So:
> Apparent density is a function of **distinct kinds**, not of prop count.
> A room that looks thin does not get better when you copy what is already in it.
Two consequences for how you fill a room:
- **Reach for a kind you have not used yet before you reach for a second copy.**
Mateo Court's courtyard went from 13 props of 6 kinds to 44 of 13, and it is
the second number that changed what it looks like. Twelve of the assets in
`src/assets/office/studio.ts` exist because of exactly that room.
- **If the kind you need is not in the catalogue, write it.** `defineAsset` plus
a `registerAll` is a smaller change than it looks, `src/assets/office/studio.ts`
is a worked example of a dozen of them, and a `colorKey` on an existing kind
will not stand in for it — the colour is *in* the batch key, so two tints of
one asset are two batches of the same geometry, which is better than one and is
not a new object.
A useful floor for a big room is **seven distinct kinds over 40 m²**, which is
what the reference pack's one large room manages. Below that a room reads as a
pattern rather than a place, whatever the prop count says.
## Zones
A named region of floor with an opaque `colorKey`, no behaviour and no effect on
@@ -450,6 +629,12 @@ without capturing console output. In a dev build it also warns.
- duplicate ids — **ids are unique per kind and building-wide, not per level**,
because a `Presence` binds to a seat id and an occupancy layer dims a prop id,
so both have to mean one thing in the building. Later loses.
- a device whose anchor names a prop that does not exist, sits on another level,
or is device hardware of a different kind; and one whose own declaration is
invalid — no label, a `kind` its `assetId` disagrees with, no capabilities, or
a `simulated` provenance whose disclosure does not say so
- an `arrival` anchor on a level that does not exist, of an unknown `kind`, or
with a position that is not a number
**Repaired** (silently, and recorded):
@@ -457,6 +642,8 @@ without capturing console output. In a dev build it also warns.
- an outline wound the wrong way
- a negative sill clamped to the floor; a head above the wall clamped down
- a prop bound to an unknown seat id — the binding is cleared, the prop stays
- a device naming an unknown room or an unknown seat — the address is cleared,
the device stays and the room is re-derived from where its hardware stands
Missing required arrays read as `[]`, because a pack that arrived over HTTP has
been through no type checker.
@@ -477,12 +664,27 @@ dropping one bad opening does not renumber its siblings.
## A checklist before you call it done
1. `new Plan(office).problems` is empty.
1. `new Plan(office).problems` is empty**at both depths**, `"full"` and
`"public"`. They are different builds and only one of them is what a visitor
gets.
2. Every room you can walk into has a `door` or `arch` with `sill: 0` reaching
at least 1.1 m of head. Walk the graph, or spot-check with `Plan.blocked`.
3. No window opening has `sill: 0` unless you meant a doorway.
4. Seat ids are the ones you are willing to live with for a year.
4. Seat ids are the ones you are willing to live with for a year, and every seat
somebody is meant to occupy has a prop bound to it with `seat:`.
5. Corridors are at least 1.2 m clear, doors 0.9 m, desks 1.41.6 m. Numbers a
person would recognise are the whole difference between a floor plan and a
diagram.
6. Nothing binary landed under `src/`.
6. `JSON.parse(JSON.stringify(office))` **deep-equals** the office. The usual way
to fail this is a helper that writes `elevation: opts.elevation`
unconditionally: `{ elevation: undefined }` and `{}` are different objects and
only one of them survives the wire. Spread optional fields, do not assign
them.
7. Every room worth looking at has a viewpoint whose `focus.at` lands inside it,
and `viewpoints[0]` is somewhere a person can stand — it is the walk spawn as
well as the arrival camera.
8. At least 0.26 non-light props/m² building-wide, no room over 20 m² below
0.15, and — the one that matters — **at least seven distinct kinds in every
room over 40 m²**. See "How full a room should be" above for why the second
number is the real one.
9. Nothing binary landed under `src/`.
+7 -2
View File
@@ -182,8 +182,13 @@ function scatter(
kind,
position,
rotation: opts.rotation ?? NORTH,
elevation: opts.elevation,
colorKey: opts.colorKey,
// Spread rather than assigned, because `elevation: undefined` is not the
// same shape as no `elevation` at all: `JSON.stringify` drops the key and a
// pack that has been through the wire stops deep-equalling the one in the
// bundle. CONTRACT.md §2 says those two have to be literally the same
// thing, and `src/test/packs/packRegression.test.ts` now checks it.
...(opts.elevation === undefined ? {} : { elevation: opts.elevation }),
...(opts.colorKey === undefined ? {} : { colorKey: opts.colorKey }),
}));
}
+77 -2
View File
@@ -12,6 +12,7 @@
* without invalidating those addresses.
*/
import { CANONICAL_CAPABILITIES, type DeviceDeclaration } from "../devices/types.ts";
import type {
AssetId,
DeskBank,
@@ -64,6 +65,8 @@ const PENDANT: AssetId = "tera:light.pendant";
const TROFFER: AssetId = "tera:light.troffer";
const RUG: AssetId = "tera:rug";
const WHITEBOARD: AssetId = "tera:whiteboard";
const MIC: AssetId = "tera:device.mic.desk";
const SPEAKER: AssetId = "tera:device.speaker.desk";
const CONCRETE = "tera:concrete.polished";
const WOOD = "tera:wood.plank";
@@ -230,6 +233,14 @@ const PROPS: Prop[] = [
// screen grants survive this content rewrite.
prop("lobby-monitor", MONITOR, 2.15, 4.45, NORTH, { elevation: 0.73 }),
prop("agent-monitor-b", MONITOR, 3.97, 4.45, NORTH, { elevation: 0.73 }),
// The two pieces of hardware the studio operates, and the only two props in
// this pack that a `DeviceDeclaration` points at: a desk condenser on the
// left-hand workstation and a monitor speaker beside its screen. Both stand on
// the 0.73 m desktop, so both carry the elevation rather than assuming one.
// The declarations are in `DEVICES` at the foot of this file and carry no
// coordinate — they name these ids and take the transform from here.
prop("agent-mic", MIC, 1.62, 4.72, NORTH, { elevation: 0.73, seat: "sf-agent-01" }),
prop("agent-speaker", SPEAKER, 2.72, 4.5, NORTH, { elevation: 0.73 }),
prop("agent-tree", TREE, 5.15, 4.15),
prop("agent-light-a", TROFFER, 2.2, 4.6, NORTH, { elevation: CEILING }),
prop("agent-light-b", TROFFER, 4.0, 4.6, NORTH, { elevation: CEILING }),
@@ -264,6 +275,69 @@ const PROPS: Prop[] = [
prop("lounge-plant", PLANT, 6.4, 7.95),
];
/**
* One sentence, shown to a viewer beside every reading this building publishes.
*
* Mandatory and checked: `validateDeviceDeclaration` refuses a `simulated`
* declaration whose disclosure does not contain the word, exactly as
* `resolveRobotOperations` refuses a robot definition that does not say its
* activity is simulated. A live-looking level meter with no provenance beside it
* is a claim about a real room, and this is the field that stops the pack making
* one by omission.
*/
const DISCLOSURE =
"Simulated studio hardware. These readings are demonstration data, never live " +
"presence data.";
/**
* Two devices: the mic on the desk and the speaker on the computer.
*
* Both anchored to a prop and neither carrying a coordinate — see `DeviceAnchor`
* in `src/devices/types.ts` for why that is the whole design of the type. Move
* `agent-mic` 200 mm and the mic moves with it, because there was never a second
* number to forget.
*
* `capabilities` is `CANONICAL_CAPABILITIES[kind]` rather than a hand-written
* list. Two studios authored months apart should describe the same instrument
* the same way — the device panel builds its controls by walking this array, and
* the arena's observation width is the sum of them.
*/
const DEVICES: DeviceDeclaration[] = [
{
id: "sf-desk-mic",
kind: "mic",
label: "Desk mic",
assetId: MIC,
anchor: {
levelId: "level-1",
propId: "agent-mic",
roomId: "live-work",
// The seat this mic is in front of. An address, like every other seat
// reference in a pack — it is what lets a consumer ask whether anybody is
// sitting where the mic is pointed without being told a coordinate.
seatId: "sf-agent-01",
},
capabilities: CANONICAL_CAPABILITIES.mic,
provenance: "simulated",
disclosure: DISCLOSURE,
},
{
id: "sf-desk-speaker",
kind: "speaker",
label: "Desk speaker",
assetId: SPEAKER,
anchor: {
levelId: "level-1",
propId: "agent-speaker",
roomId: "live-work",
seatId: "sf-agent-01",
},
capabilities: CANONICAL_CAPABILITIES.speaker,
provenance: "simulated",
disclosure: DISCLOSURE,
},
];
const ZONES: Zone[] = [
{ id: "zone-entry", name: "Entry", outline: rect(FACE, 6.7, 1.75, DEPTH - FACE), colorKey: "social" },
{ id: "zone-agent-bench", name: "Agent Bench", outline: rect(1.1, 3.5, 5.15, 5.6), colorKey: "focus" },
@@ -338,6 +412,7 @@ const LEVEL: Level = {
deskBanks: DESK_BANKS,
seats: SEATS,
zones: ZONES,
devices: DEVICES,
},
};
@@ -354,8 +429,8 @@ export const LUMBRIDGE_HQ: Office = {
"A compact 12 × 9 metre San Francisco live/work studio: two agent workstations, a demo lounge, kitchen, sleeping alcove and bath/storage.",
author: "Lumbridge",
license: "CC0-1.0",
version: "2.0.0",
updated: "2026-08-19",
version: "2.1.0",
updated: "2026-08-21",
},
};
+1105 -61
View File
File diff suppressed because it is too large Load Diff
+12 -2
View File
@@ -7,7 +7,13 @@ export const MATEO_COURT_ROBOT_OPERATIONS = Object.freeze({
officeId: "mateo-court",
disclosure: "Seeded robot simulation — no live people, company activity, or operational data.",
stations: [
{ id: "la-l1-dock", label: "Court charge dock", role: "charge", anchor: { kind: "point", levelId: "level-1", position: { x: 18.4, z: 13.2 }, facing: { x: 1, z: 0 } } },
// Against the west wall of the yard, standing on `court-dock` — the
// `tera:dock.robot` prop the pack now places at (10.15, 16.9). This used to
// read (18.4, 13.2), which is underneath the courtyard's long table: fine
// for as long as nothing was drawn there, and a robot standing in the lunch
// the moment something was. The dock faces east out of the pad, which is
// where its `facing` comes from.
{ id: "la-l1-dock", label: "Court charge dock", role: "charge", anchor: { kind: "point", levelId: "level-1", position: { x: 10.9, z: 16.9 }, facing: { x: 1, z: 0 } } },
{ id: "la-l1-paseo", label: "Paseo patrol point", role: "patrol", anchor: { kind: "room", roomId: "paseo" } },
{ id: "la-l1-mess", label: "Mess patrol point", role: "patrol", anchor: { kind: "room", roomId: "mess" } },
{ id: "la-l1-court", label: "Court patrol point", role: "patrol", anchor: { kind: "room", roomId: "court" } },
@@ -16,7 +22,11 @@ export const MATEO_COURT_ROBOT_OPERATIONS = Object.freeze({
{ id: "la-l1-directory", label: "Paseo directory", role: "inspect", anchor: { kind: "prop", propId: "paseo-directory", standoffM: 0.7, side: -1 } },
{ id: "la-l1-display", label: "Works display", role: "inspect", anchor: { kind: "prop", propId: "works-display", standoffM: 0.72, side: -1 } },
{ id: "la-l2-dock", label: "Loft charge dock", role: "charge", anchor: { kind: "point", levelId: "level-2", position: { x: 13.2, z: 3.0 }, facing: { x: 1, z: 0 } } },
// The upper dock moved east out of the new `loft-b` bench, whose three
// columns now occupy x 13.416.8. It stands on the `loft-dock` prop in the
// model bay instead, beside the racks, on the one stretch of the street wall
// upstairs with no window in it. Its bay opens south, hence the facing.
{ id: "la-l2-dock", label: "Loft charge dock", role: "charge", anchor: { kind: "point", levelId: "level-2", position: { x: 22.6, z: 1.5 }, facing: { x: 0, z: 1 } } },
{ id: "la-l2-loft", label: "Loft patrol point", role: "patrol", anchor: { kind: "room", roomId: "loft" } },
{ id: "la-l2-palmetto", label: "Palmetto patrol point", role: "patrol", anchor: { kind: "room", roomId: "palmetto" } },
{ id: "la-l2-loggia", label: "Loggia patrol point", role: "patrol", anchor: { kind: "point", levelId: "level-2", position: { x: 10.0, z: 8.0 } } },
+49
View File
@@ -19,6 +19,22 @@
* pack importing its own site **from here** rather than declaring it inline. One
* source of truth, and the direction of the dependency is the safe one: the
* small thing does not know about the large one.
*
* ### Where the car stands
*
* Each site below carries an `arrival` anchor: one marked stall on the ground
* outside, in **the pack's own plan frame**, which is the only frame a pack has
* and the same one its walls are in. `src/engine/officeExterior.ts` builds the
* apron and the vehicle there.
*
* Two of the three are honest ground. `mateo-court` sits 1.2 m above its street
* and `frontier-valley` 4 m above an airfield, so a stall a few metres outside
* the façade is a stall on the pavement. **`lumbridge-hq` is 188 m up a tower**
* and there is no pavement outside its west wall at all — its anchor is the
* kerb of the podium, authored beside the front door because the pack frame is
* the only place it can be authored, and what "outside" means vertically for a
* tower is the exterior layer's decision and not this file's. It is called out
* here rather than left for somebody to discover from a car parked in the sky.
*/
import type { OfficeSite } from "../interiors/types.ts";
@@ -44,6 +60,17 @@ export const LUMBRIDGE_HQ_SITE: OfficeSite = {
seed: 115,
bodyColor: 0x8799a8,
},
// West of the studio's own front door, which is the doorway 6.8 m along
// `ext-west`. Parallel to the façade and nosed north, the way a kerbside bay
// on a one-way downtown street runs. See the note at the top of this file
// about what 188 m of elevation does to the word "outside".
arrival: {
levelId: "level-1",
position: { x: -4.0, z: 7.4 },
rotation: 0,
kind: "vehicle-stall",
label: "Podium kerb",
},
};
/** A hangar on the old naval air station. See `frontier-valley.ts`. */
@@ -64,6 +91,16 @@ export const FRONTIER_VALLEY_SITE: OfficeSite = {
seed: 2718,
bodyColor: 0x899397,
},
// On the apron, seven metres clear of the twelve-metre hangar door in the east
// gable and centred on it, nosed in. An apron is the one place in these three
// packs where a vehicle is not a visitor but part of the programme.
arrival: {
levelId: "level-1",
position: { x: 61.0, z: 12.0 },
rotation: Math.PI / 2,
kind: "vehicle-stall",
label: "Hangar apron",
},
};
/**
@@ -89,6 +126,18 @@ export const MATEO_COURT_SITE: OfficeSite = {
seed: 1781,
bodyColor: 0xa87960,
},
// On the street, north of the brick façade and a few metres east of the paseo
// arch, so that it is in frame from the arrival viewpoint and three steps from
// the front door. `z` is negative because the street façade is authored at
// `z = 0` and the pavement is on the other side of it, which is the whole
// reason the anchor is in the plan's frame rather than the world's.
arrival: {
levelId: "level-1",
position: { x: 22.6, z: -3.4 },
rotation: -Math.PI / 2,
kind: "vehicle-stall",
label: "Mateo Street kerb",
},
};
/**