feat(games): track Reverse: 1999, which resets at 05:00

Adds the r1999 GameId and its metadata, plus GameMeta.resetHourLocal for
the fact that makes it unlike every other game here: it rolls its day at
05:00 server time rather than 04:00, on a single global UTC-5 server, so
its day-key boundary is 10:00 UTC.

resetOffsets could not carry that. Landing 10:00 UTC through the offset
would mean claiming a UTC-6 server, and serverOffsetUtc answers more
questions than this one. The new field overrides RESET_HOUR_LOCAL per game
and is absent everywhere else, so no existing reader's logged day moved —
a test pins that for the games that already had readers.

Both facts are read off the source, not assumed: all 154 rows of the
wiki's event list state (UTC-5) and run 05:00 to 04:59, an event ending
one minute before the reset the next one begins on.

No adapter yet, so the game has no events until one lands.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Lucas Winther
2026-08-17 22:45:40 +02:00
co-authored by Claude Opus 5
parent 7926a70850
commit 1a65a1f06c
6 changed files with 105 additions and 3 deletions
+47
View File
@@ -8,6 +8,8 @@ import {
isDaily,
msUntilReset,
nextResetMs,
resetHourFor,
RESET_HOUR_LOCAL,
resolveDaily,
streakOf,
} from "../src/shared/daily.ts";
@@ -358,3 +360,48 @@ describe("dailiesId", () => {
expect(dailiesId("genshin").split(":")).toHaveLength(2);
});
});
describe("a game whose day rolls on a different hour", () => {
// Reverse: 1999 runs one global server on UTC-5 and resets at 05:00 rather
// than 04:00, so its day rolls at 10:00 UTC. Both facts are read off the
// source: every row of its event list states (UTC-5) and runs 05:00 → 04:59.
test("rolls on 10:00 UTC, not the 09:00 a UTC-5 game would", () => {
expect(dayKey(at("2026-08-16T09:59:59Z"), "europe", "r1999")).toBe(
"2026-08-15",
);
expect(dayKey(at("2026-08-16T10:00:00Z"), "europe", "r1999")).toBe(
"2026-08-16",
);
expect(nextResetMs(at("2026-08-16T08:00:00Z"), "europe", "r1999")).toBe(
at("2026-08-16T10:00:00Z"),
);
});
test("every region gets the same answer, because there is one server", () => {
const instant = at("2026-08-16T09:30:00Z");
const keys = (["asia", "america", "europe"] as const).map((r) =>
dayKey(instant, r, "r1999"),
);
expect(new Set(keys).size).toBe(1);
});
test("resetHourFor defaults to 04:00 for every other game", () => {
// Adding the field must not have moved a single existing reader's day key,
// which is why it is an override rather than a per-game table.
expect(resetHourFor("r1999")).toBe(5);
for (const game of ["genshin", "endfield", "arknights", undefined] as const) {
expect(resetHourFor(game)).toBe(RESET_HOUR_LOCAL);
}
// A lane the reader invented has no server map and takes the default too.
expect(resetHourFor("mygame:my-own-game")).toBe(RESET_HOUR_LOCAL);
});
test("an event's checklist counts days on the 05:00 clock", () => {
const start = at("2026-08-13T10:00:00Z"); // an r1999 reset
expect(dailyDays(start, start + 3 * DAY, "europe", "r1999")).toEqual([
"2026-08-13",
"2026-08-14",
"2026-08-15",
]);
});
});