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:
1 parent
7c201554f5
commit
6c48174376
6 files changed
+561
-3
No files matched your search
@@ -0,0 +1,106 @@
|
||||
// The public clip gallery: authored screenplay scenes recorded through
|
||||
// the reel, each in two takes — through the wizard's eyes and from the
|
||||
// board above. Standalone pages, no app bundle: a clip link must play
|
||||
// anywhere a browser lands, and unfurl anywhere it's pasted.
|
||||
import type { ClipMeta } from "./store.js";
|
||||
|
||||
const esc = (s: string) =>
|
||||
s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
||||
|
||||
const PAGE_CSS = `
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: #171a21; color: #d6d2c4;
|
||||
font-family: Georgia, "Times New Roman", serif; }
|
||||
a { color: #e8c87a; }
|
||||
.wrap { max-width: 1100px; margin: 0 auto; padding: 2rem 1.25rem 3rem; }
|
||||
h1 { font-size: 1.6rem; letter-spacing: 0.04em; margin: 0 0 0.3rem; }
|
||||
h1 a { color: inherit; text-decoration: none; }
|
||||
p.sub { color: #8b8778; margin: 0 0 1.6rem; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1.4rem; }
|
||||
.card { background: #1f232d; border: 1px solid #2c3140; border-radius: 8px;
|
||||
overflow: hidden; text-decoration: none; color: inherit; display: block; }
|
||||
.card img { width: 100%; display: block; aspect-ratio: 736 / 577; object-fit: cover; }
|
||||
.card .meta { padding: 0.7rem 0.9rem 0.9rem; }
|
||||
.card .t { color: #e8c87a; letter-spacing: 0.03em; }
|
||||
.card .b { font-size: 0.9rem; line-height: 1.45; margin-top: 0.3rem; color: #b5b0a1; }
|
||||
.card .s { float: right; color: #8b8778; font-size: 0.85rem; }
|
||||
video { width: 100%; border-radius: 6px; display: block; background: #000; }
|
||||
.takes { display: grid; gap: 1.5rem; margin-top: 1.4rem; }
|
||||
.take h2 { font-size: 1.05rem; color: #e8c87a; font-weight: normal;
|
||||
letter-spacing: 0.04em; margin: 0 0 0.5rem; }
|
||||
.foot { margin-top: 2.2rem; color: #8b8778; font-size: 0.95rem; }
|
||||
`;
|
||||
|
||||
const shell = (metas: string[], body: string) => `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
${metas.join("\n")}
|
||||
<style>${PAGE_CSS}</style>
|
||||
</head>
|
||||
<body><div class="wrap">${body}</div></body>
|
||||
</html>`;
|
||||
|
||||
export function clipsIndexHtml(clips: ClipMeta[], base: string): string {
|
||||
const metas = [
|
||||
`<title>Wiz-War — clips from the labyrinth</title>`,
|
||||
`<meta property="og:type" content="website"/>`,
|
||||
`<meta property="og:site_name" content="Wiz-War"/>`,
|
||||
`<meta property="og:title" content="Wiz-War — clips from the labyrinth"/>`,
|
||||
`<meta property="og:description" content="Short scenes of magical combat in a stone maze: wormhole heists, warp snipers, ambushes, and the walls coming down — each seen through the wizard's own eyes and from the board above."/>`,
|
||||
`<meta property="og:url" content="${base}/clips"/>`,
|
||||
clips[0] ? `<meta property="og:image" content="${base}/clips/${clips[0].name}.jpg"/>` : "",
|
||||
`<meta name="twitter:card" content="summary_large_image"/>`,
|
||||
].filter(Boolean);
|
||||
const cards = clips.map((c) => `
|
||||
<a class="card" href="/clips/${c.name}">
|
||||
<img src="/clips/${c.name}.jpg" alt="${esc(c.title)}" loading="lazy">
|
||||
<div class="meta">
|
||||
<span class="s">${c.seconds}s</span>
|
||||
<span class="t">${esc(c.title)}</span>
|
||||
<div class="b">${esc(c.blurb)}</div>
|
||||
</div>
|
||||
</a>`).join("\n");
|
||||
const body = `
|
||||
<h1>Clips from the labyrinth</h1>
|
||||
<p class="sub">Scenes of Wiz-War, played by the rules and filmed through the wizards' own eyes.
|
||||
Each clip has a first-person take and a board take of the same moves.</p>
|
||||
<div class="grid">${cards}</div>
|
||||
<p class="foot">Wiz-War is Tom Jolly's classic of magical combat.
|
||||
This is a fan-built table — <a href="/">deal yourself in</a>.</p>`;
|
||||
return shell(metas, body);
|
||||
}
|
||||
|
||||
export function clipPageHtml(clip: ClipMeta, base: string): string {
|
||||
const metas = [
|
||||
`<title>${esc(clip.title)} — a Wiz-War clip</title>`,
|
||||
`<meta property="og:type" content="video.other"/>`,
|
||||
`<meta property="og:site_name" content="Wiz-War"/>`,
|
||||
`<meta property="og:title" content="${esc(clip.title)}"/>`,
|
||||
`<meta property="og:description" content="${esc(clip.blurb)}"/>`,
|
||||
`<meta property="og:url" content="${base}/clips/${clip.name}"/>`,
|
||||
`<meta property="og:image" content="${base}/clips/${clip.name}.jpg"/>`,
|
||||
`<meta property="og:video" content="${base}/clips/${clip.name}-fpv.mp4"/>`,
|
||||
`<meta property="og:video:secure_url" content="${base}/clips/${clip.name}-fpv.mp4"/>`,
|
||||
`<meta property="og:video:type" content="video/mp4"/>`,
|
||||
`<meta name="twitter:card" content="summary_large_image"/>`,
|
||||
`<meta name="twitter:image" content="${base}/clips/${clip.name}.jpg"/>`,
|
||||
];
|
||||
const body = `
|
||||
<h1><a href="/clips">Clips from the labyrinth</a></h1>
|
||||
<p class="sub">${esc(clip.title)} — ${esc(clip.blurb)}</p>
|
||||
<div class="takes">
|
||||
<div class="take">
|
||||
<h2>Through the wizard's eyes</h2>
|
||||
<video src="/clips/${clip.name}-fpv.mp4" poster="/clips/${clip.name}.jpg" controls playsinline></video>
|
||||
</div>
|
||||
<div class="take">
|
||||
<h2>The same scene, from the board</h2>
|
||||
<video src="/clips/${clip.name}-board.mp4" controls playsinline preload="metadata"></video>
|
||||
</div>
|
||||
</div>
|
||||
<p class="foot">Played by the real rules engine — every move on screen is a legal move.
|
||||
<a href="/">Deal yourself in</a>.</p>`;
|
||||
return shell(metas, body);
|
||||
}
|
||||
Reference in new issue
Block a user