Since 6e49fc9. Engine: the teleport BFS in the view and the engine's
wallIgnoringDistance share teleportStep; the counter trail is weighed
between steps instead of on six arms; the bush attack takes its
adjacency from SIDES like the wall does; a dead redirected clause, a
one-member set, an identity ternary, and a stray try/catch are gone;
two tests now assert what their names promise. Server: KEEPER and
MAX_SEATS live in rooms.ts, the three Sentry bells ring through one
helper, the unread reply field is dropped, actingSeat comes from the
engine, and the protocol header names every message. Web: ReplayStep
is one type, cardName one helper, the say box and the reference section
one snippet each, the keeper's name arrives from the server rather than
the source, and the lifesaver ruling that ruled nothing is out.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
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">The whole tale{data.actor ? ` — ${data.actor} triumphant` : ""}</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 — {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 — two to six wizards enter 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
|
|
transcribed, every wall verified, replays 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>
|