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:
co-authored by
Claude Opus 5
parent
65f1660718
commit
ad45b52c84
@@ -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 };
|
||||
}
|
||||
Reference in New Issue
Block a user