From 35d805fda394187da92c68d48408cddd317023db Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Wed, 23 Sep 2026 08:49:37 -0400 Subject: [PATCH] The lobby tells a caller what hour it is where the keeper lives, and that other games can go on in another tab WIZWAR_KEEPER_TZ names the zone (Eastern by default); the room carries it and the client words the hour by the minute. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG --- deploy/wizwar.service | 1 + packages/server/src/index.ts | 3 +++ packages/web/src/App.svelte | 25 ++++++++++++++++++++++--- packages/web/src/net.svelte.ts | 3 +++ 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/deploy/wizwar.service b/deploy/wizwar.service index 1b4c0c3..4ebf397 100644 --- a/deploy/wizwar.service +++ b/deploy/wizwar.service @@ -11,6 +11,7 @@ Environment=PORT=8787 Environment=HOST=127.0.0.1 Environment=WIZWAR_DATA_DIR=/var/lib/wizwar/rooms Environment=WIZWAR_STATIC_DIR=/opt/wizwar/packages/web/dist +Environment=WIZWAR_KEEPER_TZ=America/New_York Environment=SENTRY_DSN=https://a47ea012dff08b52dd230a95ccbe7414@o4509525984149504.ingest.us.sentry.io/4512011462049793 ExecStart=/opt/wizwar/node_modules/.bin/tsx src/index.ts Restart=always diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 14af39b..d02a211 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -147,6 +147,8 @@ function receiveFeedbackImage(req: IncomingMessage, res: ServerResponse, reportI /** Calls to the keeper: a real person's phone rings for each. */ const challengesPerAddress = new SlidingLimit(3, 60 * 60 * 1000); +/** Where the keeper sleeps, so a caller can tell four in the morning from four in the afternoon. */ +const KEEPER_ZONE = process.env.WIZWAR_KEEPER_TZ ?? "America/New_York"; const PUBLIC_URL = (process.env.WIZWAR_PUBLIC_URL ?? "https://wizwar.kestrelsnest.social").replace(/\/$/, ""); const reportsPerAddress = new SlidingLimit(6, 60 * 60 * 1000); const MAX_COMMAND_BYTES = 16384; // serialized game command @@ -594,6 +596,7 @@ function roomInfo(room: Room) { expansion: room.expansion, challenge: room.challenge ?? null, keeper: KEEPER, + keeperZone: KEEPER_ZONE, colors: Object.fromEntries(room.colorChoices), bots: Object.fromEntries( // A mystery machine keeps its mood only while the game lives: once it diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index ae7cc73..891f068 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -1723,6 +1723,23 @@ /** The attack on the stack draws its sight line; a cast that resolved * at once (a creation, a curse) draws the line it was accepted on for a * few seconds, so the table can see how the aim was legal. */ + /** The hour where the keeper lives, in words, kept current by the minute. */ + let minuteTick = $state(0); + $effect(() => { + const id = setInterval(() => { minuteTick++; }, 60_000); + return () => clearInterval(id); + }); + const keeperHour = $derived.by(() => { + void minuteTick; + if (!net.keeperZone) return null; + try { + const hour = Number(new Intl.DateTimeFormat("en-US", { hour: "numeric", hour12: false, timeZone: net.keeperZone }).format(new Date())) % 24; + const word = ["twelve", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven"][hour % 12]; + const part = hour < 5 ? "in the small hours" : hour < 12 ? "in the morning" : hour < 18 ? "in the afternoon" : "in the evening"; + return `${word} ${part}`; + } catch { return null; } + }); + let castTrace = $state.raw(null); let castTraceTimer: ReturnType | null = null; const sightTrace = $derived((view ? stackSightTrace(view) : null) ?? castTrace); @@ -2739,13 +2756,15 @@ title="a raven flies to {net.keeper}, who keeps this table"> ⚔ Challenge {net.keeper}, the keeper
{net.keeper} keeps this table and answers every raven — sometimes within the minute, - sometimes after a night's sleep. The seat is held either way.
+ sometimes after a night's sleep. {keeperHour ? `It is ${keeperHour} where ${net.keeper} lives.` : ""} The seat is held either way. {:else if net.challenge} -
The raven has flown to {net.keeper}, called by {net.challenge.by}. A seat is held for them: +
The raven has flown to {net.keeper}, called by {net.challenge.by}. + {keeperHour ? `It is ${keeperHour} there.` : ""} A seat is held for them: if they can come now, they will take it; if they are at supper, or asleep, or off in the world, they will leave a word below when they can. Meanwhile the table is yours — seat a clockwork wizard, invite a friend, - or leave the room on the ledger and come back to it later.
+ or open another game in a new tab and play on; this room keeps its place on your ledger until you come + back, as long as nobody abandons it.
{/if} {@const lobbyTalk = net.log.filter((l) => l.text.startsWith("\u{1F4AC}")).slice(-6)} diff --git a/packages/web/src/net.svelte.ts b/packages/web/src/net.svelte.ts index ac1a51c..a84783c 100644 --- a/packages/web/src/net.svelte.ts +++ b/packages/web/src/net.svelte.ts @@ -383,6 +383,8 @@ class Net { challenge = $state<{ by: string; at: string } | null>(null); /** The keeper's name, once the room has said it. */ keeper = $state(null); + /** The keeper's time zone (IANA), for telling a caller what hour it is there. */ + keeperZone = $state(null); view = $state(null); log = $state([]); error = $state(null); @@ -541,6 +543,7 @@ class Net { this.expected = msg.expected ?? []; this.challenge = msg.challenge ?? null; if (typeof msg.keeper === "string") this.keeper = msg.keeper; + if (typeof msg.keeperZone === "string") this.keeperZone = msg.keeperZone; if (this.you && this.token) { const seat: Seat = { name: this.you, roomId: msg.roomId, token: this.token }; localStorage.setItem(SEAT_KEY, JSON.stringify(seat));