feat(arknights): track Arknights from its own wiki

The most-named game in the release thread, and the cheapest one to add: the
GameId, hue and dailyTasks have been sitting in games.ts since launch with no
source behind them.

arknights.wiki.gg is a better source than Game8 for this game, but it is not the
mp-event shape wikigg.ts reads, so it gets its own parser. Two properties of the
page drive the code:

- Every row lists CN and Global, about five months apart. Only Global is
  published; a row without one yields no event rather than borrowing the CN
  date. A CN date on a Global calendar is a confidently wrong date.
- Only the next boundary carries a machine-readable timer — the end while an
  event runs, the start while it is upcoming — so precision differs per side and
  flips when the event goes live. An exact instant is accepted only when it
  falls on the same UTC day as the date beside it, because startsAt.slice(0, 10)
  is part of the event ID: a start that moved a day would orphan every
  completion mark on the morning the event began.

Also sets resetOffsets for all three regions to UTC-7. Not a blanket per-game
offset — Arknights genuinely runs one Global server for every region we model,
and the page evidences the clock: every ending event ends at 10:59:59Z, which is
03:59:59 at UTC-7, one second before a 04:00 reset.

Six events, counted independently against the page before and after parsing.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Lucas Winther
2026-08-17 18:05:01 +02:00
co-authored by Claude Opus 5
parent 8c2ec19c22
commit 1dd58d3f52
12 changed files with 1824 additions and 9 deletions
+31
View File
@@ -223,6 +223,37 @@ export function parseOpenRange(
return start === null ? null : { start, end: null };
}
/**
* "2026/07/30 2026/08/20" → both instants, day precision.
*
* Year-first, unlike `parseShortSlashRange`'s MM/DD/YY, so there is nothing to
* infer about field order: a four-digit leading number can only be the year.
* The Arknights wiki writes its release windows this way, one line per server.
*
* Anchored on the four-digit year at both ends deliberately. Without it the
* looser MM/DD/YY reader matches the tail of "2026/07/30" as "26/07/30" and
* calls month 26 an invalid date — null either way, but by accident rather than
* by rule.
*/
export function parseYearFirstSlashRange(
input: string,
): { start: ParsedInstant; end: ParsedInstant } | null {
const re =
/(\d{4})\/(\d{1,2})\/(\d{1,2})\s*[-–—]\s*(\d{4})\/(\d{1,2})\/(\d{1,2})/;
const m = re.exec(input);
if (!m) return null;
const n = (i: number) => Number(m[i]);
const startIso = iso(n(1), n(2), n(3));
const endIso = iso(n(4), n(5), n(6));
if (startIso === null || endIso === null) return null;
return {
start: { iso: startIso, precision: "day" },
end: { iso: endIso, precision: "day" },
};
}
/**
* "2021/01/16 04:00 - 2021/01/31 03:59" → both instants, exact precision.
* Trailing prose after the range (e.g. "Currently Unavailable") is ignored.