c0c7fcf974340c6fb9c4cc52e2dae5395e625784
12 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c0c7fcf974 | feat: add character studio and aircraft foundation | ||
|
|
1c37ef8f3e | feat: add private profile media and realtime contracts | ||
|
|
a2a52bfdae | feat: add playable actors and seamless journey state | ||
|
|
fe58290728 | California gets roads, traffic, and a car to follow | ||
|
|
9c9e78f6f9 |
The Southland gets a door, and the people in it move
**Mateo Court**, a courtyard block in the Arts District — a third pack and a third *kind* of building. Two office floors and a shed already existed, so this is the plan the format had not been shown: a ring of rooms round an open-air yard with **no corridor anywhere**. Every door opens onto the yard, and the yard does the job a corridor does in the other two. It is also the first pack whose `ceiling: null` means there is genuinely no ceiling at any height rather than "take the lid off so the shot can see in", and the first sited off the Bay Area board at all. That last part needed a fix, not just a coordinate. `OFFICE_MARKERS` was gated on `id === "sf"`, which was correct for exactly as long as every office was in the Bay Area — it would have kept the Los Angeles building off the Los Angeles board and pinned it to San Francisco's. The gate is the board's own bounds now, which is the same question asked honestly. Every door through the 0.25 m courtyard skin is 1.2 m rather than the usual 0.9, and that is not a style choice: `blocked` inflates by the walker's radius *and* the wall's thickness, so a 0.9 m leaf through a wall that thick seals the room behind it while rendering perfectly. Two rooms were sealed exactly that way on the first pass. The test says so, because a well-meaning edit back to 0.9 for consistency would do it again. **People move.** `samplePresenceAt` thinned the roster by hour, which fixed a building that was full at 1 a.m., but everybody was still pinned to their own desk all day. Occupancy is a hand-written booking table now: meetings fill a room for a plausible length, the kitchen island fills at lunch, and a `Presence` binds to a seat id — so "in a meeting" means occupying a meeting-room seat, and the whole thing is choosing seat ids rather than inventing positions. The flood fill that checks a pack is walkable existed three times over. One copy now, since the third is where a divergence lives. **Films.** A new `office-dusk` reel — six hours over Lumbridge HQ catching the moment the house lights take over from the sun, which is the one thing only a time-lapse can show and which did not exist. The three existing reels are re-rendered rather than re-captioned: `films.ts` in v4 named a commit eleven behind HEAD, so the published reels were shot before there were any clouds and before the city drew office pins. Reviewers caught roughly a dozen false statements in the new prose across these files — a room census that did not add up, a wall-thickness count, a claim that every room has daylight when one does not, and a cost figure saying `updateSun` runs once a second when it runs once a minute. The consequential ones are fixed. In a codebase where the comments are the design record, a confident wrong number is a defect. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2d87d9f354 |
The city points at its own buildings, and the sky stops depending on an API
**Clouds were invisible to everyone who had not wired up NWS.** The layer
took `currentWeather()?.cloudCover ?? 0`, and `currentWeather()` is null on
any deployment without a weather source — which is the default, and the
exact configuration this repo is held to: a stranger clones it, runs one
command, and gets a city with no account and no key. Their sky was
permanently, silently empty. `atmosphere.ts` already models a sky when
nobody has observed one; it now models cover too, an observed reading
still wins outright, and the clouds are there on a bare clone.
**Both offices are pins on the city, and clicking one walks you in.** Each
pack has carried a real `site` since the sun needed one, and that
coordinate was known to the lighting and to nothing else — a visitor
looking at the board had no way to tell that two of those buildings are
ones they can go inside. The coordinates move to a tiny eagerly-imported
`offices/sites.ts` that the packs import *from*, because a pack is a 25 kB
lazy chunk and the board wants its pins long before anybody opens a door.
A test asserts the pack and the table hold the **same object**, not merely
equal values: a drifted coordinate would put the marker on one building
and the sun on another and both would look entirely plausible.
**Aircraft bank into their turns.** The roll channel existed and was never
written, so every turn was flat. Bank comes from the coordinated-turn
relation against the measured turn rate, damped by a first-order lag so it
settles rather than oscillates, and clamped at 30° like a real limiter.
Six regression tests, because roll is the one channel that feeds itself —
position and heading are recomputed from the last two observations and
wash out a bad value, while a NaN in the roll would persist for the life
of the track.
That fed straight into a real defect: `AdsbFlights` substituted
`heading: 0` for records with no `track` field, which is harmless for a
symmetrical dart and is a **sustained full-scale artefact** once aircraft
bank — a target whose real heading is 200° reported as 0° reads as a 160°
turn and pins the roll at its limiter for as long as it is in the feed.
Those records are dropped now. An aeroplane the feed will not give a
heading for is one this layer cannot draw honestly.
**The office empties out overnight.** A full complement of seated people
at one in the morning, under house lights that came on because the sun is
down, was the least believable thing left in the room once the clock
became real. A live roster always wins — an API that says the building is
empty is telling the truth about the building.
**Robots go somewhere.** They pick real addresses — a seat, a room — and
turn to face the seat when they arrive, rather than stopping at a random
angle. Godmode gets an office section: house lights forced on or off or
following the sun, robots and ceilings toggled, with a readout.
**The bundle is split.** Entry chunk 758 kB to 208 kB, with three.js and
satellite.js in a vendor chunk that survives an app deploy instead of
being re-downloaded on every one. Rollup's 500 kB warning still fires and
should — it now points at three.js, where it is true, instead of at our
code, where it was pointing at three.js all along.
Reviewers caught two false geography claims in the new prose ("both
shipped buildings stand in San Francisco" — one is across the estuary at
Alameda Point) and several miscounted figures. Fixed. In a codebase where
the comments are the design record, those are defects.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
51979feea0 |
Shadows land on the building, and the sky layers stop repeating themselves
**Shadows were the right size and pointed at nothing.** Last round fixed the missing `updateProjectionMatrix()`, so the frustum finally became the size every caller asks for — but nothing aimed it, and `sun.target` sits at the world origin. A pack's origin is the **north-west corner of its slab**, so for lumbridge-hq the box was off-centre by half the building: 14.4 m of a 48 m plate, about a third of the floor, fell outside the frustum and neither cast nor received. Invisible while three's broken ±5 default made shadows useless everywhere; obvious the moment they started working. `SceneKitOptions` takes a `shadowTarget` now, both callers pass one, and the light's target is added to the scene — which is the part that actually matters, because `LightShadow.updateMatrices` reads `target.matrixWorld` and an unparented `Object3D` is never reached by the traversal that updates it. The sun is also placed relative to the target rather than the origin, so light-to-target is exactly `sunDistance` for every direction, which is the invariant each caller's `shadowNear`/`shadowFar` were chosen against. **`flights.ts` could not be tested, and that is why it was untested.** It used a TypeScript parameter property — the one piece of TS syntax that *emits code* rather than annotating a type — so Node's type stripping refused the whole module. The bundler never cared, so nobody found out until the first `node --test` file tried to import it. The module carrying the worst bug this project has shipped was, by construction, the one module that could not have a test. It has eleven now, including one that fails if the live-aircraft repeat-skip is removed. **Robots are on the plan panel** — a turned marker with a bow for heading, in the one hue left that is neither the people-blue nor the camera-amber. Review findings cleared across the four new sky/robot modules: a real 24 mm void at the ankle and an 8 mm hole through each forearm, a per-frame allocation in the robot heading picker, a per-frame sort in the starlink ranking, `uTime` growing unbounded until the cloud breath quantises, and `DAY_REFERENCE`'s derivation which did not reproduce. `createStarlinkMeshLayer` now takes a **board** radius — the same unit its sibling takes — instead of a dome radius with nothing in the types to tell them apart. That is the exact confusion that has already caused one real bug here. `DOME_RADIUS_FACTOR` has one owner and is imported, not copied: the points and the meshes must be on the same dome or a satellite that grows geometry also jumps. Several comments were wrong rather than merely stale — a fabricated claim about `Object3D.clone`, a fabricated attribution to `Plan`, an inverted `DoubleSide` argument, a triangle ledger citing a function that no longer exists, and a defensive-call rationale that contradicted the paragraph above it. In a codebase where the comments are the design record, those are defects. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
06455f7424 |
The office learns where it stands, and the sky stops being a backdrop
**Aircraft actually move now, and the reason they did not is the headline.**
`HttpFlights` holds a frozen snapshot between network refreshes and is
polled at 1 Hz, so a live feed handed the layer the same position five to
fifteen times and then jumped. `span` therefore measured the poll interval
rather than the gap between the two positions that differ, the teleport
test saw an airliner covering 36 units in a "second" against a ceiling of
8, and **every live track's history was wiped on every refresh** — so no
aircraft on the deployed site could ever grow a trail, however long
TRAIL_POINTS was set. Skipping the repeat fixes the motion and the trail
at once. Trails then go to 72 points / 240 s, which is about seventy
seconds of flying.
Three more defects in the same file, found while looking: trail
truncation dropped the segments nearest the aircraft (leaving a streak
with no aeroplane attached), MAX_TRACKS was declared and never enforced,
and one missing target deleted its whole trail. The buffer now uploads
only what it wrote, rather than 46 MB/s of untouched array.
**You can get above the constellation.** Dome to 1.05 board *radii* and
the orbit to 2.0 spans. Radii, not spans: scene space is centred on the
city and the Bay Area board runs forty kilometres down the peninsula, so
the furthest corner is 0.94 spans out where the half-diagonal is 0.65 —
sized off the half-diagonal the dome sat inside its own city. The far
plane goes to 4 spans to stop clipping the sky from off-centre chapters,
and `PointsMaterial` defaults `fog: true`, which was quietly dimming the
whole constellation with the city's haze.
**An office can say where it stands.** `Office.site` — lat, lng, height
above the ground outside, and the compass bearing the pack's −Z points
along — and with one it gets the same sun the city does, a sky, and a
horizon at `-elevation`. CONTRACT §4 reserved this as "a later
refinement"; it is taken up rather than overturned, and `daylight.ts`
computes no light of its own. It does the two things a room needs that a
map does not: turn the sun into the building's frame, and move the fog
outdoors before it greys out the far wall.
Two buildings now, and they are deliberately unalike: Lumbridge HQ 188 m
up a Transbay tower facing 205°, and **Frontier Valley**, a startup in a
hangar at Alameda Point — one room, 54 x 30 m, nine metres to the
trusses, four metres above reclaimed ground.
Floor-to-floor in the reference pack is now 16.8 m: the interstitial is
ten times a real one, so the space between the slabs is somewhere things
can hang. It is frankly not architecture, `PLENUM` is the one number to
change, and the file says so.
Also fixed, all found by review rather than by looking at the screen:
- `sun.shadow.camera.updateProjectionMatrix()` was never called, so
three's default ±5 unit box has been in force this whole time and
every `shadowExtent` this repo passes — including the city's ±752 —
has been silently ignored.
- A missing aircraft was kept alive by the new grace period and *drawn*,
so it froze in mid-air at full opacity for 32 s.
- Frontier Valley's mezzanine was a `Room`, which carries no height: its
slab lay on the concrete, its chairs floated 4.4 m over it, and its
balustrade fenced off a patch of ground floor. It is a `Level`.
- Overlapping floor slabs z-fought. The format permits overlap and
resolves later-first, so `shell.ts` now lifts a slab a hair per
earlier slab it overlaps — and by nothing at all in a pack, like the
reference office, whose rooms only ever abut.
- `switchOffice` bypassed the `entering` guard (leaking a whole scene
per double-click) and tore down the old room before knowing the new
one would load, with no way back.
Known and not fixed: raising MAX_SPAN to 30 s doubles the worst-case
re-base snap when a feed's gap shortens. It is bounded, pre-existing in
kind, and the fix wants carrying the live head into the next leg.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
d8afc42d15 |
The office gets a room you cannot see all of from a doorway
Lumbridge HQ was 34 x 18 m, 612 m², one storey, fifteen rooms. It is now
48 x 18 m over two storeys, twenty-six rooms and a hundred and
twenty-six seats — and the fourteen metres that were added are one room
rather than a second row of cellular offices, because area is not the
thing the building was short of.
**The commons** is the east wing: one volume, open all the way to the
roof at 7.0 m, with the block's old east curtain wall now looking into it
instead of onto a street. Two archways through that glass, four trees,
one refectory table on the axis of the corridor, and nothing along the
glazing.
**Level 2** sits at 4.2 m — floor to floor, not floor to ceiling, which
is the number people get wrong. A studio with nineteen metres of
unbroken north-lit floor, a library, two meeting rooms, a project room,
and a gallery that reaches 4.2 m out over the void so the commons can be
looked down into rather than merely walked through.
`WIDTH` became `BLOCK_E`: every room, wall and seat authored before the
wing existed is measured against the old east line, and folding the two
into one number would have stretched fifteen rooms sideways.
Three things worth knowing:
- Every existing seat id is unchanged. The README says to keep them
stable for the same reason street numbers survive repainting, and a
`Presence` binds to one.
- The balustrade is a wall and not a prop. `Plan` derives the walk
collider from the wall list, so a rail authored as furniture is a
balcony you can walk off, and the drop is a storey onto terrazzo.
- The wing's walls carry an explicit `height`, because "double height"
is a property of the walls and `ceiling: null` alone would leave a
lid you cannot see but the light rig can.
`src/test/office.test.ts` is new and is the README's own checklist, which
nothing had ever run: problems empty, seat ids unique building-wide, the
balustrade impassable from all three open edges, and a 0.1 m flood fill
proving every room on both levels is reachable on foot from the way in.
That last one caught a door authored 200 mm past the end of its wall —
silently dropped, room sealed, and invisible from every camera angle.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
a229fb2721 |
The sky gets the things above the aeroplanes
Satellites, end to end: CelesTrak element sets behind the same TTL cache
the weather and the flights use, served as TLEs rather than as positions,
and propagated in the browser with SGP4.
Sending elements is the same trick `flights/plan.ts` plays and it has a
better excuse here — a TLE *is* the closed form, valid for days either
side of its epoch, so one cacheable fetch every six hours replaces a poll
and every viewer agrees about where everything is.
Two things are worth knowing about the shape of it:
- There is no region parameter. An aeroplane at 10,000 m is local and
a satellite at 550 km is above the horizon for a circle two thousand
kilometres across, so one catalogue serves both boards and the client
decides what is above its own horizon. Only the observer is per-city,
which is why `main.ts` shares the elements and rebuilds the catalogue.
- The layer draws on a dome, because it cannot draw anywhere else.
`world.metres(550_000)` is 21,000 scene units against a far plane at
3,000. Azimuth and elevation are real; the radius carries nothing.
Off by default: a clone that started pulling CelesTrak on `npm run dev`
would have volunteered somebody else's bandwidth for its onboarding.
Godmode gets the two dials that point at the sky rather than at the
light — fabricated traffic, which composes with a live ADS-B feed instead
of replacing it, and a switch for the satellite layer with a count beside
it. Both are god-only lies about the inputs, in the manner of the weather
override.
`satellite.js` is the second runtime dependency this package has taken.
Its entry point star-exports an Emscripten build that cannot be shaken
out, so `noWasmPropagator` in the Vite config cuts it: 308 kB of WASM
loader for a bulk propagator nothing calls, against 26 kB for the SGP4
that does the work.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
902481a03d |
The access test follows the sky out from behind the sign-in
Written against a `Capabilities` that had `liveData` on it, and landing on a
master where it does not:
|
||
|
|
aab58a1c24 |
The client gets tests, starting with the two files that most needed them
Nine test files on the server, none on the client, and no test script in the root package at all — so CI's only gate on the half of this project that runs in a stranger's browser was `tsc --noEmit`, which will tell you the types line up and nothing about whether an anonymous visitor is handed the private office. Both files here were built to be tested and never were. `access.ts` is the only module in the bundle whose output is a set of decisions about what a stranger may see, and its own header carries the reason a test is owed: a shipped line read `canEnterOffice = s.authenticated || !s.passwordLogin`, which is true for `auth: none` and dangerously false for `sso` — where `POST /session` answers 404 precisely *because* credentials are issued elsewhere — so on an SSO deployment every anonymous visitor got the private view while the config still said the box was private. There is now a test named after that bug. The assertions are weighted toward the closed direction on purpose: one that a member gets what a member is owed, and half a dozen that nobody gets more than nothing, because showing a member the public office is a bad afternoon and showing a stranger the private one is what the tiers exist to prevent. The 5xx case is in there too — a box mid-restart is `anon`, not `member` — and so is the `javascript:` entry URL, which a CSP of `script-src 'self' 'unsafe-inline'` does not stop from navigating. `plan.ts` says in its own header that it imports no three.js "so the splitting pass is testable without a WebGL context", and that it "drops rather than throws … every one of those is reported through `problems`" — an array whose whole purpose is to be asserted on, which nothing asserted on. So: a pack wrong in five ways still builds and files five reports; a zero-length wall does not put a NaN in the bounds; an office with no levels stays finite so nothing downstream divides by it; a pack with the required arrays missing is taken, because HTTP will send one. And the wall pass gets the check CONTRACT.md §2's argument deserves — one decomposition, two products — by walking a walker through the door and into the wall beside it, and through a window and being stopped. Also the yaw convention, which nothing stated and `officeMinimap.ts` draws straight from: get the sign wrong and every wall mirrors about its own centre, invisible on a square and obvious on anything else. No new dependency. The server already runs `node --test` over `.ts` on native type stripping, so the client does the same — which matters here, because this repo's "no surprise dependencies" check is an allowlist naming why each one is permitted, and a test runner would have needed an entry and an argument. One source change was needed to make any of it possible. `session.ts` read `import.meta.env.VITE_IDENTITY_URL` at module scope, and `access.ts` imports `authFetch` from it — so one property access made the file that decides what an anonymous visitor sees unreachable from a plain test runner, which is most of why it had no tests. It now reads the way `plan.ts` already reads `DEV`, by the idiom that file documents as being there "so this module stays importable from a plain test runner". 31 tests, 9 suites, all passing, wired into `npm test` and into the CI job beside the server's. `vite build` is unchanged and no test code reaches the bundle. |