feat(nikki): track Infinity Nikki
Adds the game and its Game8 events page. One `SOURCES` entry against the existing game8 parser, plus one date shape that page needs: `parseLabelledStartEnd` reads a duration cell holding `Start: <date>` and `End: <date>` split by a `<br>`, which a tag-stripping reader flattens into one run of text. Four of the seven events say `End: Permanent`. That is the source telling us there is no deadline, so they publish with `endsAt: null` and `endPrecision: "unknown"` rather than a plausible date. The labelled cell is also structure end to end, so it never becomes a summary — stripping the leading half would leave "End: Permanent" standing where a description belongs. Verified by re-extracting the page with a throwaway script independent of the parser: 7 rows in, 7 events out, every date matching. Note the source itself is stale — its "Current Events" table still lists January-May 2025 and "Upcoming Events" says there are none. The adapter is right; the page looks abandoned, and Nikki coverage worth relying on needs a second source at a higher priority. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
243c7d70dd
commit
67951e81b4
@@ -66,6 +66,12 @@ const SOURCES: SourceSpec[] = [
|
||||
url: "https://game8.co/games/Neverness-to-Everness/archives/592073",
|
||||
parserId: "game8",
|
||||
},
|
||||
{
|
||||
id: "nikki-game8-events",
|
||||
game: "nikki",
|
||||
url: "https://game8.co/games/Infinity-Nikki/archives/487445",
|
||||
parserId: "game8",
|
||||
},
|
||||
];
|
||||
|
||||
function toAdapter(spec: SourceSpec): Adapter {
|
||||
|
||||
@@ -147,6 +147,34 @@ export function parseShortSlashRange(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* "Start: January 24, 2025 End: Permanent" → the start, and whatever the end
|
||||
* half turns out to be.
|
||||
*
|
||||
* A labelled cell rather than a range: the two halves are separated by a `<br>`
|
||||
* and each carries its own word. The colons are required, which is what stops
|
||||
* the word "end" inside a description from splitting a cell that is really
|
||||
* prose.
|
||||
*
|
||||
* An end half that is not a date ("Permanent", "TBD", "After maintenance")
|
||||
* yields a null end rather than an invented one — the same outcome as
|
||||
* `parseOpenRange`, and for the same reason.
|
||||
*/
|
||||
export function parseLabelledStartEnd(
|
||||
input: string,
|
||||
): { start: ParsedInstant; end: ParsedInstant | null } | null {
|
||||
const m = /^\s*start\s*[::]\s*(.+?)(?:\s+end\s*[::]\s*(.*?))?\s*$/i.exec(
|
||||
input,
|
||||
);
|
||||
if (!m) return null;
|
||||
|
||||
const start = parseMonthDayYear(m[1] ?? "");
|
||||
if (start === null) return null;
|
||||
|
||||
const endHalf = m[2];
|
||||
return { start, end: endHalf === undefined ? null : parseMonthDayYear(endHalf) };
|
||||
}
|
||||
|
||||
/**
|
||||
* A range whose start is a real date but whose end is not: "July 10, 2026 -
|
||||
* Permanent", "Jul. 24, 2026 - End of 4.6", or a lone start date.
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
} from "../../shared/schema.ts";
|
||||
import {
|
||||
parseFullRange,
|
||||
parseLabelledStartEnd,
|
||||
parseMonthDayRange,
|
||||
parseMonthDayYear,
|
||||
parseOpenRange,
|
||||
@@ -313,6 +314,11 @@ function isRequirementOnly(text: string): boolean {
|
||||
|
||||
/** Strip a leading label and date range, leaving any description behind it. */
|
||||
function proseAfterDates(cell: string): string | null {
|
||||
// A fully labelled "Start: … End: …" cell is structure end to end. Stripping
|
||||
// the leading half would leave "End: Permanent" standing where a description
|
||||
// belongs — a date masquerading as a blurb.
|
||||
if (parseLabelledStartEnd(cell) !== null) return null;
|
||||
|
||||
const rest = cell
|
||||
.replace(/^\s*(period|duration|schedule|dates?)\s*[::]\s*/i, "")
|
||||
.replace(RANGE_PREFIX, "")
|
||||
@@ -330,6 +336,7 @@ function parseRange(
|
||||
parseFullRange(value) ??
|
||||
parseShortSlashRange(value) ??
|
||||
parseMonthDayRange(value) ??
|
||||
parseLabelledStartEnd(value) ??
|
||||
parseOpenRange(value)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ export const GAMES: Record<GameId, GameMeta> = {
|
||||
// and `america` already resolves to -5, so Europe is the only real override.
|
||||
endfield: { id: "endfield", name: "Arknights: Endfield", short: "Endfield", hue: "#E8635A" , studio: "Hypergryph", dailyTasks: "Daily missions", resetOffsets: { europe: -5 } },
|
||||
nte: { id: "nte", name: "Neverness to Everness", short: "NTE", hue: "#C77DFF" , studio: "Hotta Studio", dailyTasks: "Daily tasks" },
|
||||
nikki: { id: "nikki", name: "Infinity Nikki", short: "Nikki", hue: "#F27BB0" , studio: "Infold Games", dailyTasks: "Daily tasks, Vital Energy" },
|
||||
};
|
||||
|
||||
export const GAME_LIST: GameMeta[] = Object.values(GAMES);
|
||||
|
||||
@@ -8,6 +8,7 @@ export const GameId = z.enum([
|
||||
"arknights",
|
||||
"endfield",
|
||||
"nte", // Neverness to Everness
|
||||
"nikki", // Infinity Nikki
|
||||
]);
|
||||
export type GameId = z.infer<typeof GameId>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user