feat: add Arknights: Endfield, and stop passing off requirements as summaries

Endfield was previously written off as undatable. That was wrong: a first
pass only inspected its Duration rows (all "Permanently Available") and its
year-less version grid, and missed an "Event | Schedule & Summary" table
whose cells read "Period: 08/09/26 - 08/30/26 During the event...". Two real
events, with a year.

Adds MM/DD/YY range parsing for that shape. Month-first ordering is not
assumed lightly — Endfield's own version grid reads 01/22, 04/17, 07/16 for
versions 1.0, 1.2 and 1.4, chronological only if the month comes first.

Also stops presenting unlock conditions as descriptions. Several templates
put "Reach Union Level 8" where a blurb would go; filling the summary slot
with text that never says what the event is is worse than leaving it empty.
Wuthering Waves correctly drops to zero summaries as a result.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Lucas Winther
2026-08-15 01:07:39 +02:00
co-authored by Claude Opus 5
parent 65f1660718
commit ad45b52c84
11 changed files with 237 additions and 19 deletions
+56
View File
@@ -0,0 +1,56 @@
import { useCallback, useEffect, useState } from "react";
import { readJson, writeJson } from "./storage.ts";
export interface Mark {
/** When the reader made this mark. */
at: string;
}
export type Marks = Record<string, Mark>;
/**
* A set of per-event marks, keyed by event ID and persisted locally.
*
* Completions ("I finished this") and ignores ("stop showing me this") are the
* same shape and want the same guarantees, so they share one implementation.
* They stay separate stores because they mean different things: an ignored
* event is hidden, a completed one is dimmed but still counted.
*/
export function useMarkSet(storageKey: string) {
const [marks, setMarks] = useState<Marks>(() =>
readJson<Marks>(storageKey, {}),
);
useEffect(() => {
writeJson(storageKey, marks);
}, [storageKey, marks]);
const toggle = useCallback((id: string) => {
setMarks((prev) => {
if (prev[id] !== undefined) {
const { [id]: _removed, ...rest } = prev;
return rest;
}
return { ...prev, [id]: { at: new Date().toISOString() } };
});
}, []);
const clear = useCallback(() => setMarks({}), []);
/**
* Union merge, keeping the earlier mark. Never removes: nothing else holds a
* copy of these, so a silent deletion would be unrecoverable.
*/
const merge = useCallback((incoming: Marks) => {
setMarks((prev) => {
const next = { ...prev };
for (const [id, value] of Object.entries(incoming)) {
const existing = next[id];
next[id] =
existing === undefined || value.at < existing.at ? value : existing;
}
return next;
});
}, []);
return { marks, toggle, merge, clear };
}