Screenplays: authored scenes the engine performs and the reel films
?fpv&script=<name> plays a screenplay — a seeded deal, cards slipped into hands as props, then a command list run through applyCommand, so every event on screen is a legal move under the real rules. Ten scenes ship in the catalog: wormhole heists, a fireball threaded through a warp, an ambush that watches its corridor through a mouth, the leap over a conjured pit, and the wall between two homes coming down. The server grows a public gallery to hang them in: /clips lists the catalog, /clips/<name> pages each scene in two takes — through the wizard's eyes and from the board — with og:video unfurls and Range-served mp4s (Safari refuses video without it). Files live in the clip vault beside the rooms, shipped by deploy/clips-publish.sh; the name gate is the security. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF
This commit is contained in:
co-authored by
Claude Fable 5
parent
7c201554f5
commit
6c48174376
@@ -216,3 +216,39 @@ export function archiveRoomFile(roomId: string): void {
|
||||
mkdirSync(graveyard, { recursive: true });
|
||||
renameSync(src, join(graveyard, `${roomId}.${Date.now()}.jsonl`));
|
||||
}
|
||||
|
||||
// --- The clip vault. ------------------------------------------------------
|
||||
// Showcase clips live beside the rooms as plain files — <slug>-fpv.mp4,
|
||||
// <slug>-board.mp4, <slug>.jpg — described by clips.json, all published by
|
||||
// deploy/clips-publish.sh. The server only ever reads them.
|
||||
|
||||
export interface ClipMeta {
|
||||
name: string;
|
||||
title: string;
|
||||
blurb: string;
|
||||
seconds: number;
|
||||
}
|
||||
|
||||
const clipsDir = () => join(DATA_DIR, "..", "clips");
|
||||
|
||||
export function readClips(): ClipMeta[] {
|
||||
const file = join(clipsDir(), "clips.json");
|
||||
if (!existsSync(file)) return [];
|
||||
try {
|
||||
const parsed = JSON.parse(readFileSync(file, "utf8")) as ClipMeta[];
|
||||
return parsed.filter((c) => /^[a-z0-9-]{1,60}$/.test(c.name));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolve a clip asset request to its path — or null for any name that
|
||||
* is not exactly a published clip file shape. The gate IS the security:
|
||||
* nothing outside <slug>(-fpv|-board).mp4 / <slug>.jpg can be named. */
|
||||
export function clipAssetPath(file: string): string | null {
|
||||
if (!/^[a-z0-9-]{1,60}(-fpv|-board)\.mp4$/.test(file) && !/^[a-z0-9-]{1,60}\.jpg$/.test(file)) {
|
||||
return null;
|
||||
}
|
||||
const path = join(clipsDir(), file);
|
||||
return existsSync(path) ? path : null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user