1
0

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>
This commit is contained in:
2026-08-07 04:18:00 -07:00
parent 51979feea0
commit 2d87d9f354
14 changed files with 2304 additions and 72 deletions
+118
View File
@@ -568,3 +568,121 @@ describe("more aircraft than the trail buffer was sized for", () => {
);
});
});
// ---- Banking ---------------------------------------------------------------
/**
* Roll is the one channel that feeds itself.
*
* Position, heading, pitch and altitude are all recomputed from the last two
* observations every time, so a bad value washes out on the next poll. The bank
* is a first-order lag on its own previous value — that is what makes it settle
* smoothly instead of stepping — and the price of that is that a `NaN`, or a
* sign error, or a failure to reset, persists for the life of the track rather
* than for one frame. These are the cases where that would bite.
*
* Read through `mesh.rotation.z`, which needs no GL context.
*/
describe("aircraft banking", () => {
/** Fly `headings` in order, one distinct observation per refresh. */
function fly(f: Fixture, headings: number[]): THREE.Mesh {
let lng = -122.4;
let t = 0;
for (const heading of headings) {
at(t);
// A real step each time, or the repeat-skip correctly ignores the sample
// and the heading never lands.
lng += 0.02;
f.layer.update([{ ...jet("bank", 37.77, lng), heading }]);
t += REFRESH;
}
at(t);
f.layer.tick();
const mesh = f.meshes()[0];
assert.ok(mesh, "no aircraft");
return mesh;
}
it("stays dead level on a straight leg", () => {
const f = fixture();
const mesh = fly(f, [90, 90, 90, 90, 90]);
assert.equal(mesh.rotation.z, 0, "a straight leg should have no bank at all");
});
it("banks into a sustained turn, and not past the limiter", () => {
const f = fixture();
const mesh = fly(f, [90, 105, 120, 135, 150, 165]);
assert.ok(mesh.rotation.z !== 0, "a turning aircraft should be banked");
// 30° is the stated ceiling; anything past it is a knife-edge airliner.
assert.ok(
Math.abs(mesh.rotation.z) <= (30 * Math.PI) / 180 + 1e-9,
`banked ${((mesh.rotation.z * 180) / Math.PI).toFixed(1)}°, past the limiter`,
);
});
/**
* The sign, which is the half nobody can check by reading.
*
* A left turn and a right turn of the same size must produce equal and
* opposite rolls. That does not prove the absolute direction is right — the
* geometry argument in `flights.ts` does that — but it does catch the whole
* class of errors where the roll is derived from something that is not the
* signed turn, which would break the symmetry.
*/
it("rolls opposite ways for opposite turns", () => {
const right = fly(fixture(), [90, 105, 120, 135]).rotation.z;
const left = fly(fixture(), [90, 75, 60, 45]).rotation.z;
assert.ok(Math.abs(right) > 1e-3, "the right turn produced no bank");
assert.ok(Math.abs(right + left) < 1e-6, `${right} and ${left} are not mirrored`);
});
/**
* The 0/360 wrap, which is where a naive `to - from` produces a 350° turn out
* of a 10° one and rolls the aircraft onto its back.
*/
it("does not flick as a track crosses north", () => {
const f = fixture();
const mesh = fly(f, [340, 350, 0, 10, 20]);
const degrees = (mesh.rotation.z * 180) / Math.PI;
assert.ok(Number.isFinite(degrees), "the bank went non-finite across the wrap");
// A steady 10°-per-refresh right turn. If the wrap were mishandled this
// would be pinned at the limiter with the opposite sign.
assert.ok(degrees > 0, `crossing north banked ${degrees.toFixed(1)}°, the wrong way`);
assert.ok(degrees <= 30 + 1e-9, `crossing north banked ${degrees.toFixed(1)}°`);
});
/**
* A looping simulator route teleports, and the teleport branch clears the
* samples. It must clear the roll too — a track that starts its next leg still
* banked has no observation pair to wash it out, so it would simply stay that
* way.
*/
it("comes level again when a route wraps", () => {
const f = fixture();
fly(f, [90, 105, 120, 135]);
// Half a degree of longitude in one refresh: hundreds of units, well past
// the teleport ceiling.
at(REFRESH * 5);
f.layer.update([{ ...jet("bank", 37.77, -121.9), heading: 135 }]);
at(REFRESH * 5);
f.layer.tick();
const mesh = f.meshes()[0];
assert.ok(mesh);
assert.equal(mesh.rotation.z, 0, "a wrapped route kept its bank");
});
it("stays finite when a feed reports a nonsense heading", () => {
const f = fixture();
fly(f, [90, 105, 120]);
at(REFRESH * 4);
f.layer.update([{ ...jet("bank", 37.77, -122.3), heading: Number.NaN }]);
at(REFRESH * 4);
f.layer.tick();
const mesh = f.meshes()[0];
assert.ok(mesh);
assert.ok(
Number.isFinite(mesh.rotation.z),
"one bad heading poisoned the roll for the life of the track",
);
});
});
+29
View File
@@ -27,6 +27,7 @@ import { describe, it } from "node:test";
import { Plan } from "../interiors/plan.ts";
import LUMBRIDGE_HQ from "../offices/lumbridge-hq.ts";
import FRONTIER_VALLEY from "../offices/frontier-valley.ts";
import { OFFICE_SITES } from "../offices/sites.ts";
const plan = new Plan(LUMBRIDGE_HQ, { warn: false });
@@ -415,3 +416,31 @@ describe("the sites", () => {
}
});
});
/**
* `offices/sites.ts` and the packs must not drift.
*
* The city draws its doors from `OFFICE_SITES` because a pack is a lazy chunk
* and the board wants the pins before anybody opens one. That means two places
* name the same building, and nothing in the type system ties them together —
* a coordinate edited in the pack and not in the table would put the marker on
* one building and the sun on another, and both would look entirely plausible.
*/
describe("the office site table", () => {
it("lists exactly the packs this build ships", () => {
assert.deepEqual(
OFFICE_SITES.map((e) => e.id).sort(),
[FRONTIER_VALLEY.id, LUMBRIDGE_HQ.id].sort(),
);
});
it("hands each pack the very same site object it publishes", () => {
for (const pack of [LUMBRIDGE_HQ, FRONTIER_VALLEY]) {
const entry = OFFICE_SITES.find((e) => e.id === pack.id);
assert.ok(entry, `${pack.id} is missing from OFFICE_SITES`);
// Identity, not equality: the packs import from the table, so anything
// less than the same reference means somebody has restated a coordinate.
assert.equal(entry.site, pack.site, `${pack.id} has a site of its own`);
}
});
});