tower / commit
feat(calendar): Tower keeps Pisa's days (D-089)
README.md | 17 +++++++++-------- content/world.json | 8 ++++---- decisions.md | 33 +++++++++++++++++++++++++++++++-- design.md | 21 +++++++++++---------- src/tour/interior-legs.ts | 8 ++++++-- src/world/celestial.ts | 2 +- src/world/schema.ts | 6 +++--- test/sky.test.ts | 43 ++++++++++++++++++++++++++++++++++++++----- tools/screenshots.mjs | 2 +- 9 files changed, 104 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index f50c07b..279cafd 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ nothing passive would stay in the beam past mid-afternoon. It is a person. Built: the unrolled surface map, Tower from outside, standing inside it and flying over it, a detail card for every place, Tower's published weather, a sun that crosses -the world each day on Sydney's clock and a moon by night with Earth's phases, two railways, a scenic tour that flies the world's +the world each day on Pisa's clock and a moon by night with Earth's phases, two railways, a scenic tour that flies the world's paragliders, aircraft, airships, trains and spacecraft inside and out, permalinks, and the sightline probe. @@ -417,10 +417,11 @@ where it goes out: that is nightfall. Through the night the beam is banked to a and the next morning the sun lights at the sun cap again. So Tower's sun moves from one end of the world to the other, every day. -**Tower keeps Sydney's days.** Its clock is Sydney's, daylight saving included, and its sun -comes on at Sydney's sunrise and goes out at Sydney's sunset, so the days lengthen and -shorten with Sydney's seasons — about 45 km/h at the equinoxes, when day 0 falls. The city -and the date of day 0 are the `calendar` in `content/world.json`. +**Tower keeps Pisa's days.** Its clock is Pisa's — Italian time, daylight saving included — +and its sun comes on at Pisa's sunrise and goes out at Pisa's sunset, so the days lengthen and +shorten with Pisa's seasons: about 44 km/h at the equinoxes, when day 0 falls, 61 km/h at +midwinter and 35 km/h at midsummer. The city and the date of day 0 are the `calendar` in +`content/world.json`. (Until D-089 it kept Sydney's.) **Every place has its own noon.** Tower aims the swarm so the sun is as bright seen from anywhere as seen from under it, so the ground is lit by how high the sun stands, as it is @@ -439,13 +440,13 @@ toward the far cap while it waxes and toward the sun cap while it wanes, or noth a path of light across the water toward it. The Time section says where it is and how lit, and the map draws a pale line where it is overhead. -**The clock opens at the time in Sydney now, and runs.** It starts from Sydney's current +**The clock opens at the time in Pisa now, and runs.** It starts from Pisa's current date and time and goes forward at whatever speed it was last run at — 900x until you choose -another, and 1x keeps it in step with Sydney. Add **`?clock=<hours>`** to the address to +another, and 1x keeps it in step with Pisa. Add **`?clock=<hours>`** to the address to hold it at a moment instead, stopped: `?clock=12` is noon on day 0. That is how the screenshot tour gets the same sky every run. -**Time**, in the panel, works in every view: the **Day** (with its date in Sydney), the +**Time**, in the panel, works in every view: the **Day** (with its date in Pisa), the **Time** of day, **Run the clock** and its rate, and the **Light** — how bright Tower runs its day. It says when the sun rises and sets and where it is now, and the map marks it. **Dark** shuts the 54 TW beam down entirely and is the only setting that is genuinely black — diff --git a/content/world.json b/content/world.json index 25836fd..bf3d1a4 100644 --- a/content/world.json +++ b/content/world.json @@ -2,10 +2,10 @@ "version": 2, "seed": 1729, "calendar": { - "city": "Sydney", - "timeZone": "Australia/Sydney", - "latitude": -33.8688, - "longitude": 151.2093, + "city": "Pisa", + "timeZone": "Europe/Rome", + "latitude": 43.7228, + "longitude": 10.4017, "day0": "2026-09-23" }, "ground": { diff --git a/decisions.md b/decisions.md index 237f638..d50ebc3 100644 --- a/decisions.md +++ b/decisions.md @@ -2450,6 +2450,7 @@ and the build — but it means a remembered view or camera follows you between b ## D-074 — Tower has a sun, and keeps Sydney's days *Amended by D-079: the rod no longer stops at the sun; it runs on to the far cap.* +*Amended by D-089: Tower keeps Pisa's days now, not Sydney's.* **Decided** by the author, 2026-09-27. Built. Supersedes D-053; amends D-008. @@ -2947,8 +2948,8 @@ moon either. **Its phase is Earth's Moon's**, worked out for the moment on the clock (`world/celestial.ts` `moonPhase`, Meeus's low-precision phase angle, checked against the almanac's new, full and -quarter moons). Day 0's night, 23 September 2026, has an 89% waxing moon; it is full on the 27th -and new on 11 October. The disc is drawn with its terminator, lit toward the far cap while it +quarter moons). Day 0's night, 23 September 2026, has an 89% waxing moon; it is full on the 26th +in Pisa (the 27th in Sydney, which Tower kept when this was written) and new on 11 October. The disc is drawn with its terminator, lit toward the far cap while it waxes and toward the sun cap while it wanes — where the sun set and where it will rise, as on Earth. @@ -2968,3 +2969,31 @@ moonlight, on the first night of the month with the moon at least 60% lit. **A canon question it raises, not answered here:** whether the moon takes its light from the beam as the sun does — the rod dimmer past it — or is fed by Tower lifting the banked beam, as written above. The second is what is drawn. + +## D-089 — Tower keeps Pisa's days + +*Amends D-074.* + +**Decided** by the author, 2026-09-28: "for reasons, Tower has actually decided to set its +internal 'time' to match that of Pisa, Italy." Built. + +**What changed.** The `calendar` in `content/world.json` is Pisa's: `Europe/Rome`, 43.7228° N, +10.4017° E. Tower's clock is Pisa's local time, Italian daylight saving included (CEST, +2, until +25 October 2026; CET, +1, after), the app opens at the time in Pisa now, and the sun rises and +sets with Pisa's sunrises and sunsets. The fallback calendar in `src/world/schema.ts`, used when +the world file's is missing or unusable, is Pisa's too. + +**The clock and the days stay together.** D-074 makes Tower keep one city's clock *and* its +sunrises and sunsets; changing the city changes both. So Tower's seasons turned over: day 0 — +still 23 September 2026 — is now the northern autumn equinox, and the days shorten toward +December instead of lengthening. The day runs 12 h 11 min at the equinox (the sun at 44 km/h), +8 h 55 min at midwinter (61 km/h) and 15 h 27 min at midsummer (35 km/h), against Sydney's +9 h 54 min and 14 h 24 min. + +**Tests.** Sydney's published sunrise tables still check the solar maths, on a Sydney calendar +of their own; Pisa's day lengths, daylight saving and clock have tests of their own. + +**One knock-on in the tour.** The ship leg starts in the morning over its sea, and the morning +moved; at the new hour the director looked up at the sun, which put the chase camera below the +ship, and the sea pushed it back up beside the ship, looking past it. A ship's chase camera now +looks no more than 20° up (`maxPitch` 0.35 in `shipLeg`). diff --git a/design.md b/design.md index 1bfd8c4..cce3a4d 100644 --- a/design.md +++ b/design.md @@ -271,17 +271,18 @@ mirror is about not wasting the beam; the desert is about where the weather goes Tower's day is **24 hours**, chosen for the people living under it — nothing physical offers one, since the spin is 7.5 minutes and has nothing to do with light. -**Tower keeps an Earth city's days: Sydney's.** Its clock is Sydney's, daylight saving -included, and its sunrise and sunset are Sydney's, so the days lengthen and shorten -with Sydney's seasons. Day 0 is the southern spring equinox, 23 September 2026. The -city and that date are the `calendar` in the world file, and can be changed there. The -app's clock opens at Sydney's time now and runs on at the speed it was last run at. +**Tower keeps an Earth city's days: Pisa's** (D-089; until then, Sydney's). Its clock is +Pisa's — Italian time, daylight saving included — and its sunrise and sunset are Pisa's, +so the days lengthen and shorten with Pisa's seasons. Day 0 is the northern autumn +equinox, 23 September 2026, so the calendar starts on a 12-hour day and shortens toward +December. The city and that date are the `calendar` in the world file, and can be changed +there. The app's clock opens at Pisa's time now and runs on at the speed it was last run at. **The sun crosses the world once a day.** It lights at the **sun cap** at sunrise, travels at a steady speed down the axis, and reaches the **far cap** at sunset, where -it goes out. At the equinox that is 540 km in 12 hours 7 minutes — **45 km/h**, a sun -you can watch move; at midwinter the day is 9 h 54 min and it runs at 55 km/h, and at -midsummer 14 h 24 min and 37 km/h. It takes a quarter of an hour to light and a quarter +it goes out. At the equinox that is 540 km in 12 hours 11 minutes — **44 km/h**, a sun +you can watch move; at midwinter the day is 8 h 55 min and it runs at 61 km/h, and at +midsummer 15 h 27 min and 35 km/h. It takes a quarter of an hour to light and a quarter of an hour to go out, which is the shoal gathering and dispersing. Then it is night (§1.3.2) until the next sunrise. @@ -305,9 +306,9 @@ outside — with the same haze between it and the eye. the size of the sun — 3.2 km, as Earth's Moon and Sun are one size in Earth's sky. It lights at the **far cap** as the sun goes out there at dusk, and crosses the world **the other way**, sunward, reaching the **sun cap** by sunrise, where the sun lights behind it. At the equinox -that is 540 km in 11 h 53 min, some 45 km/h; the long winter nights run it slower. +that is 540 km in about 11 h 50 min, some 46 km/h; the long winter nights run it slower. -**It keeps Earth's Moon's phases**, as the sun keeps Sydney's days. Its disc is lit as much as +**It keeps Earth's Moon's phases**, as the sun keeps Pisa's days. Its disc is lit as much as the Moon is lit that night, and shaped like it: the lit part lies toward the far cap while it waxes — the end the sun went out at, as a waxing moon on Earth is lit on the side of the set sun — and toward the sun cap while it wanes, the end the sun will light at. The dark part is diff --git a/src/tour/interior-legs.ts b/src/tour/interior-legs.ts index ddb953c..d46494a 100644 --- a/src/tour/interior-legs.ts +++ b/src/tour/interior-legs.ts @@ -929,7 +929,11 @@ export const INTERIOR_LEGS: LegSpec[] = [ }, ] -/** A chase camera on a ship, which the fleet draws itself. */ +/** + * A chase camera on a ship, which the fleet draws itself. It looks no more than 20° up: to + * look higher from behind a ship the camera would have to sink below it, into the sea, and + * the sea would push it back up beside the ship, looking past it. + */ function shipLeg(w: TourWorld, o: LegOptions, lane: ShipLane, hours: number, id: string, title: string): Leg { const level = w.compiled.basins.find((b) => b.id === lane.sea)?.levelM ?? 0 const scaleM = Math.max(1, lane.lengthM / 20) @@ -937,7 +941,7 @@ function shipLeg(w: TourWorld, o: LegOptions, lane: ShipLane, hours: number, id: id, vehicle: 'ship', hours, durationS: o.durationS, fovDeg: 55, title, path: (t) => { const f = shipAt(lane, hours + t / 3600); return { z: f.z, a: f.a, altM: level + lane.lengthM * 0.25 } }, heading: (t) => shipAt(lane, hours + t / 3600).headingRad, - rig: { kind: 'chase', back: 60 * scaleM, raise: 12 * scaleM, swingRad: 0.9 }, draw: false, + rig: { kind: 'chase', back: 60 * scaleM, raise: 12 * scaleM, swingRad: 0.9 }, draw: false, maxPitch: 0.35, }) } diff --git a/src/world/celestial.ts b/src/world/celestial.ts index d5225ff..4feaf99 100644 --- a/src/world/celestial.ts +++ b/src/world/celestial.ts @@ -38,7 +38,7 @@ export function sunLongitude(at: Date): number { /** * Earth's Moon's phase at an instant: how much of its disc is lit, 0 at new and 1 at full, - * and whether it is waxing. Tower's moon keeps it (D-088), as its sun keeps Sydney's days. + * and whether it is waxing. Tower's moon keeps it (D-088), as its sun keeps Pisa's days. * * Meeus, *Astronomical Algorithms* ch. 48, low precision: the phase angle from the mean * elongation and the two mean anomalies, with the six largest periodic terms. Good to about diff --git a/src/world/schema.ts b/src/world/schema.ts index 08ca8a2..5fd1239 100644 --- a/src/world/schema.ts +++ b/src/world/schema.ts @@ -282,9 +282,9 @@ export interface Calendar { day0: string } -/** Sydney, from the southern spring equinox of 2026 (author, 2026-09-27). */ +/** Pisa, from the northern autumn equinox of 2026 (author, 2026-09-28; D-089). */ export const defaultCalendar = (): Calendar => ({ - city: 'Sydney', timeZone: 'Australia/Sydney', latitude: -33.8688, longitude: 151.2093, + city: 'Pisa', timeZone: 'Europe/Rome', latitude: 43.7228, longitude: 10.4017, day0: '2026-09-23', }) @@ -476,7 +476,7 @@ export function parseWorld(raw: unknown): World { return world } -/** A calendar with anything unusable in it falls back to Sydney field by field. */ +/** A calendar with anything unusable in it falls back to Pisa field by field. */ function parseCalendar(raw: unknown): Calendar { const d = defaultCalendar() if (!raw || typeof raw !== 'object') return d diff --git a/test/sky.test.ts b/test/sky.test.ts index 05999f6..fec81ba 100644 --- a/test/sky.test.ts +++ b/test/sky.test.ts @@ -3,8 +3,14 @@ import { LENGTH_M } from '../src/canon/constants.js' import { defaultCalendar } from '../src/world/schema.js' import { dateLabel, hourWhenSunAt, hoursAt, skyAt, sunTimes, utcOffsetH } from '../src/world/sky.js' -const sydney = defaultCalendar() -/** The world day on which Sydney has this date (day 0 is 2026-09-23). */ +/** + * Sydney, which Tower kept until D-089: its published sunrise tables check the solar maths, + * whichever city the world file names. + */ +const sydney = { ...defaultCalendar(), city: 'Sydney', timeZone: 'Australia/Sydney', latitude: -33.8688, longitude: 151.2093 } +/** Pisa, which Tower keeps now (D-089). */ +const pisa = defaultCalendar() +/** The world day on which the city has this date (day 0 is 2026-09-23). */ const dayOf = (iso: string) => Math.round((Date.parse(`${iso}T00:00:00Z`) - Date.parse(`${sydney.day0}T00:00:00Z`)) / 86_400_000) const hm = (h: number, m: number) => h + m / 60 @@ -37,6 +43,33 @@ describe("Tower keeps Sydney's days (D-074)", () => { }) }) +describe("Tower keeps Pisa's days (D-089)", () => { + it('starts on the autumn equinox, a twelve-hour day', () => { + const { sunriseH, sunsetH } = sunTimes(pisa, 0) + expect(Math.abs(sunsetH - sunriseH - 12)).toBeLessThan(0.25) + }) + + it('shortens toward December and lengthens toward June', () => { + const len = (iso: string) => { const t = sunTimes(pisa, dayOf(iso)); return t.sunsetH - t.sunriseH } + expect(len('2026-12-21')).toBeLessThan(9.25) + expect(len('2027-06-21')).toBeGreaterThan(15.25) + }) + + it("keeps Italy's daylight saving", () => { + expect(utcOffsetH('Europe/Rome', new Date('2026-09-23T10:00:00Z'))).toBe(2) + expect(utcOffsetH('Europe/Rome', new Date('2026-12-21T11:00:00Z'))).toBe(1) + // Either side of the switch on 25 Oct 2026, sunrise jumps back by about an hour. + const before = sunTimes(pisa, dayOf('2026-10-24')).sunriseH + const after = sunTimes(pisa, dayOf('2026-10-26')).sunriseH + expect(before - after).toBeGreaterThan(0.9) + }) + + it('opens the clock at the time in Pisa now', () => { + // 10:00 UTC on day 0 is noon in Pisa (CEST, +2). + expect(hoursAt(pisa, new Date('2026-09-23T10:00:00Z'))).toBeCloseTo(12) + }) +}) + describe('the sun runs the length of the world, sunrise to sunset', () => { const day = dayOf('2026-12-21') const { sunriseH, sunsetH } = sunTimes(sydney, day) @@ -80,17 +113,17 @@ describe('the sun runs the length of the world, sunrise to sunset', () => { }) describe('the calendar is in the world file', () => { - it('reads the committed one: Sydney, from the equinox', async () => { + it('reads the committed one: Pisa, from the equinox', async () => { const { readFileSync } = await import('node:fs') const { parseWorld } = await import('../src/world/schema.js') const cal = parseWorld(JSON.parse(readFileSync('content/world.json', 'utf8'))).calendar - expect(cal).toEqual(sydney) + expect(cal).toEqual(pisa) }) it('falls back field by field when something in it is unusable', async () => { const { parseWorld } = await import('../src/world/schema.js') const cal = parseWorld({ calendar: { city: 'Perth', timeZone: 'Not/AZone', day0: 'soon', latitude: -31.95 } }).calendar expect(cal.city).toBe('Perth') - expect(cal.timeZone).toBe('Australia/Sydney') + expect(cal.timeZone).toBe('Europe/Rome') expect(cal.day0).toBe('2026-09-23') expect(cal.latitude).toBe(-31.95) }) diff --git a/tools/screenshots.mjs b/tools/screenshots.mjs index fc6040e..0529767 100644 --- a/tools/screenshots.mjs +++ b/tools/screenshots.mjs @@ -430,7 +430,7 @@ async function setTime(page, minutes) { * and all — was a laptop running hot for a minute. With the cache warmed once (below), * no shot compiles at all. */ -// And the clock held at noon on day 0: left alone it opens at Sydney's time now, and runs. +// And the clock held at noon on day 0: left alone it opens at Pisa's time now, and runs. const TOUR_PARAMS = 'workers=2&cache=1&clock=12' const searchFor = () => `?${TOUR_PARAMS}` /** The storage every shot's fresh context starts from: the warm cache, and nothing else. */