feat(custom): model reader-authored games and events
The data layer for PRD F13, with no UI and no behaviour change yet.
src/shared/custom.ts defines the two key spaces, their schemas, and the
projection into what the views read. Two properties are the point of it:
- A reader's event id is random, not derived from their title. They can type a
scraped event's exact name and date, which under ${game}:${slug}:${date} is a
byte-identical key — one completion mark and one streak silently shared by two
events. Randomness also means renaming their own event never moves its id.
- A reader's event carries no sourceUrl, so a hand-entered date can never be
attributed to a source, and claims no region split, because they entered one
instant and inventing three would fabricate two of them.
The rest is widening what was GameId-shaped into a lane that may be one of
theirs: clockFor takes the boundary fields structurally so their events run on
the identical countdown rather than a second one, day keys fall back to the
regional default for a lane with no server map, and gameMeta becomes a context
resolver so metaFor stays pure and total — a lane can outlive its game when an
import carries an event whose game did not come with it.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4029885833
commit
3702ee7a4c
@@ -1,3 +1,4 @@
|
||||
import type { CustomGames, LaneId } from "./custom.ts";
|
||||
import type { GameId, Region } from "./schema.ts";
|
||||
|
||||
export interface GameMeta {
|
||||
@@ -72,3 +73,48 @@ export const GAME_LIST: GameMeta[] = Object.values(GAMES);
|
||||
export function gameMeta(id: GameId): GameMeta {
|
||||
return GAMES[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Meta for any lane, including one the reader invented (PRD F13).
|
||||
*
|
||||
* Pure, and total. Total matters: a lane id can outlive the game it names —
|
||||
* an import can carry an event whose game did not come with it, and a reader
|
||||
* can delete a game a stale render is still holding. Returning a neutral
|
||||
* placeholder keeps that a visible oddity rather than a blank screen, which is
|
||||
* the trade this codebase makes everywhere else in the client.
|
||||
*/
|
||||
export function metaFor(id: LaneId, custom: CustomGames): GameMeta {
|
||||
const tracked = GAMES[id as GameId];
|
||||
if (tracked !== undefined) return tracked;
|
||||
|
||||
const own = custom[id];
|
||||
if (own !== undefined) {
|
||||
return {
|
||||
id: own.id as GameId,
|
||||
name: own.name,
|
||||
short: shortLabel(own.name),
|
||||
hue: own.hue,
|
||||
// Not credited in the colophon and contributing no standing chore: the
|
||||
// colophon lists the sources we fetch, and this game has none. See
|
||||
// docs/DATA-MODEL.md § Reader-authored key spaces.
|
||||
studio: "",
|
||||
dailyTasks: "",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: id as GameId,
|
||||
name: "Unknown game",
|
||||
short: "?",
|
||||
hue: "#9AA3B8",
|
||||
studio: "",
|
||||
dailyTasks: "",
|
||||
};
|
||||
}
|
||||
|
||||
/** A name that still fits a narrow lane label or a chip. */
|
||||
export function shortLabel(name: string): string {
|
||||
if (name.length <= 12) return name;
|
||||
const first = name.split(/\s+/)[0] ?? name;
|
||||
return first.length <= 12 ? first : `${name.slice(0, 11)}…`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user