1
0
Commit Graph

2 Commits

Author SHA1 Message Date
karti 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>
2026-08-07 04:18:00 -07:00
karti e41c90fe8d Real weather, real aircraft, a heightfield off the main thread, and instruments
Three things that were built and never connected, connected.

**The weather was already there.** `observe()` has always taken a
`WeatherObservation` and `main.ts` has always passed null, so the cloud,
precipitation, visibility and marine-layer paths in atmosphere.ts had never run
outside a test. The server already shipped NWS, met.no and Open-Meteo, all
configured off. What was actually missing was that a single TERA_ORIGIN_LAT/LNG
served one metro and lied to the other — so weather and traffic are per-region
now, derived from the city's own bounds, and the Bay Area gets its fog while
Long Beach gets its own sky. The route takes ?city= or a validated ?lat=&lng=
and refuses to become an open geocoding proxy for the planet.

**The heightfield moved to a Worker.** 2.3 s of blocked main thread at boot, and
another ~950 ms of point-in-polygon on top of it: the park mask is filled in the
worker now, and block placement samples four corners and only runs the exact
test on a cell that straddles an edge — 8 buildings differ out of 185,036.
createScene is async and takes a Stage as a consequence, and there is a
main-thread fallback because "clone it and it works" has no exception clause.

**Spaces is a chunk you fetch when you reach for the door**, not one everybody
downloads. Same for the godmode tools. The entry chunk is 722 kB rather than
772; three.js is most of what is left and splitting it is a different job.

**Godmode is an instrument panel now** rather than one slider: the date and the
season, not just the hour, so the Meeus moon and the sun's seasonal arc become
visible instead of merely correct; a weather override that says on screen when
it is lying; a frame-time and draw-call readout; and a pose editor that emits a
paste-ready Chapter block, which is the thing that makes adding New York cheap.

Two blockers the review caught:

  - Every city switch leaked 8 GPU textures — one of them a 2048x2048 shadow map
    — and ~10.5 shader programs, and deleteTexture had never been called once in
    the app's lifetime. The renderer was being built per scene; it belongs to the
    canvas, for the life of the page.
  - An upstream fetch that threw rather than returning null skipped the cache
    stamp, so the TTL — the only rate limit on outbound calls — collapsed to one
    upstream request per inbound request, and the caller got a 500.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 03:25:31 -07:00