Wording throughout the app, the server's messages and the engine's refusals trades its stacked dashes for sentences, while the maze still trusts you to honor its curses, the gallery still listens, and the clockwork still learns its best tricks from the humans at the table. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
161 lines
5.0 KiB
Svelte
161 lines
5.0 KiB
Svelte
<script lang="ts">
|
|
import type { ReplayStep } from "./catchup";
|
|
// The public share page (/watch/<id>): one turn of one game, watchable
|
|
// by anyone, spectator-redacted — the rest of the game stays private.
|
|
// The reel opens in first person through the turn-owner's eyes, framed
|
|
// by enough chrome to explain itself and point the way home.
|
|
import Replay from "./Replay.svelte";
|
|
import type { GameEvent, GameView } from "@wizwar/engine";
|
|
|
|
const id = location.pathname.split("/")[2] ?? "";
|
|
// Under the vite dev server the API lives on the game server's port.
|
|
const API = location.port === "5173" ? `http://${location.hostname}:8787` : "";
|
|
|
|
interface ShareSteps {
|
|
steps: ReplayStep[];
|
|
actor: string;
|
|
round: number;
|
|
whole?: boolean;
|
|
/** The turn's deed in one line, as the share card says it. */
|
|
headline?: string | null;
|
|
}
|
|
let data = $state<ShareSteps | null>(null);
|
|
let failed = $state(false);
|
|
let reelKey = $state(0);
|
|
$effect(() => {
|
|
fetch(`${API}/api/share/${id}`)
|
|
.then((r) => (r.ok ? r.json() : Promise.reject(new Error("gone"))))
|
|
.then((d) => (data = d))
|
|
.catch(() => (failed = true));
|
|
});
|
|
</script>
|
|
|
|
<div class="share-page">
|
|
<header class="share-head">
|
|
<a class="share-brand" href="/">WIZ-WAR</a>
|
|
{#if data}
|
|
<div class="share-title">
|
|
{#if data.whole}
|
|
<span class="share-deed">{data.actor ? `The whole tale of ${data.actor}'s triumph` : "The whole tale"}</span>
|
|
{:else if data.headline}
|
|
<span class="share-deed">{data.headline}</span>
|
|
<span class="share-sub">an instant replay of {data.actor}'s turn{data.round ? `, round ${data.round}` : ""}</span>
|
|
{:else}
|
|
<span class="share-deed">Instant replay of {data.actor}'s turn{data.round ? `, round ${data.round}` : ""}</span>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</header>
|
|
|
|
{#if failed}
|
|
<div class="share-note">
|
|
<p>This replay has wandered off. The link may be old, or the game gone.</p>
|
|
<a class="share-cta" href="/">▶ deal yourself into a fresh game</a>
|
|
</div>
|
|
{:else if !data}
|
|
<div class="share-note"><p>Rebuilding the turn…</p></div>
|
|
{:else}
|
|
<div class="share-stage">
|
|
{#key reelKey}
|
|
<Replay steps={data.steps} moment pov={data.actor} endLabel="⟲ watch it again"
|
|
onclose={() => (reelKey += 1)} />
|
|
{/key}
|
|
</div>
|
|
<div class="share-play">
|
|
<a class="share-cta" href="/">▶ play free, with two to six wizards in the maze</a>
|
|
</div>
|
|
{/if}
|
|
|
|
<footer class="share-foot">
|
|
<p>
|
|
<strong>Wiz-War</strong> is Tom Jolly's 1983 game of magical combat in
|
|
a stone labyrinth. This digital table is an unofficial, non-commercial
|
|
fan re-creation of the sixth edition (Chessex, 1993). Every card is
|
|
transcribed and every wall verified, and each replay is rebuilt move
|
|
by move from the game's own ledger.
|
|
</p>
|
|
</footer>
|
|
</div>
|
|
|
|
<style>
|
|
.share-page {
|
|
/* Safari's text inflation has no business here: the page is laid
|
|
out for the phone already. */
|
|
-webkit-text-size-adjust: 100%;
|
|
text-size-adjust: 100%;
|
|
min-height: 100vh;
|
|
background: #171a20;
|
|
color: #d8d2c0;
|
|
font-family: "Archivo Narrow", system-ui, sans-serif;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding: 1rem;
|
|
}
|
|
.share-head {
|
|
display: flex;
|
|
align-items: baseline;
|
|
flex-wrap: wrap;
|
|
gap: 1rem;
|
|
width: min(46rem, 100%);
|
|
margin-bottom: 0.6rem;
|
|
}
|
|
.share-brand {
|
|
font-family: "Oswald", sans-serif;
|
|
letter-spacing: 0.3em;
|
|
font-size: 1.1rem;
|
|
color: #e0b34a;
|
|
text-decoration: none;
|
|
}
|
|
.share-title {
|
|
font-family: "Oswald", sans-serif;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.12em;
|
|
font-size: 0.85rem;
|
|
color: #e9e1cb;
|
|
}
|
|
.share-deed { display: block; }
|
|
.share-sub { display: block; text-transform: none; letter-spacing: 0.02em; color: #8d8672; font-size: 0.8rem; }
|
|
.share-play { width: min(46rem, 100%); margin-top: 0.8rem; text-align: center; }
|
|
/* The reel is a modal everywhere else; here it stands in the page. */
|
|
.share-stage { width: min(46rem, 100%); }
|
|
.share-stage :global(.replay-scrim) {
|
|
position: relative;
|
|
inset: auto;
|
|
background: none;
|
|
padding: 0;
|
|
display: block;
|
|
z-index: 1;
|
|
}
|
|
.share-stage :global(.replay) { width: 100%; max-height: none; }
|
|
.share-note {
|
|
background: #efe8d4;
|
|
color: #3a2f1f;
|
|
border-radius: 4px;
|
|
padding: 1.2rem 1.5rem;
|
|
margin: 2rem 0;
|
|
font-family: "Courier Prime", monospace;
|
|
text-align: center;
|
|
}
|
|
.share-foot {
|
|
width: min(46rem, 100%);
|
|
margin-top: 1rem;
|
|
color: #8d8672;
|
|
font-size: 0.9rem;
|
|
line-height: 1.5;
|
|
}
|
|
.share-foot strong { color: #c9a72a; }
|
|
.share-cta {
|
|
display: inline-block;
|
|
margin-top: 0.4rem;
|
|
background: #e9e1cb;
|
|
color: #43331f;
|
|
border: 1.5px solid #43331f;
|
|
border-radius: 3px;
|
|
padding: 0.45rem 0.9rem;
|
|
text-decoration: none;
|
|
font-family: "Oswald", sans-serif;
|
|
letter-spacing: 0.06em;
|
|
}
|
|
</style>
|