diff --git a/deploy/deploy.sh b/deploy/deploy.sh
index aba9e20..3562aa1 100755
--- a/deploy/deploy.sh
+++ b/deploy/deploy.sh
@@ -3,6 +3,10 @@
set -euo pipefail
HOST="${1:?usage: deploy.sh }"
+# The family list is the kit's; the site deploys with the kit's latest copy when the kit is beside this checkout.
+KIT_FAMILY="$(dirname "$0")/../../game-kit/family.json"
+[ -f "$KIT_FAMILY" ] && cp "$KIT_FAMILY" packages/web/public/family.json
+
npm run build --workspace=@wizwar/web
# Old hashed chunks stay a week: a page loaded before this deploy can still
# fetch the module it was built against instead of failing mid-game.
diff --git a/packages/web/public/family.json b/packages/web/public/family.json
new file mode 100644
index 0000000..10d4a2e
--- /dev/null
+++ b/packages/web/public/family.json
@@ -0,0 +1,21 @@
+{
+ "family": "the Kestrel's Nest",
+ "games": [
+ {
+ "slug": "wizwar",
+ "name": "Wiz-War",
+ "url": "https://wizwar.kestrelsnest.social",
+ "pitch": "Steal treasure. Blast through walls. Outwit your friends.",
+ "players": "2 to 6",
+ "origin": "Tom Jolly's board game, sixth edition, 1993"
+ },
+ {
+ "slug": "waving-hands",
+ "name": "Waving Hands",
+ "url": "https://hands.kestrelsnest.social",
+ "pitch": "Duelling wizards. Two hands each. The right run of gestures is a spell.",
+ "players": "2 to 12",
+ "origin": "Richard Bartle's pencil-and-paper duel, 1977"
+ }
+ ]
+}
diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte
index 20053f2..e63e1c6 100644
--- a/packages/web/src/App.svelte
+++ b/packages/web/src/App.svelte
@@ -10,6 +10,7 @@
import Card from "./Card.svelte";
import Help from "./Help.svelte";
import { HELP_TABS, type HelpTab } from "./help-state";
+ import { otherGames, type FamilyGame } from "./family";
import { summarizeMissed } from "./catchup";
import Replay from "./Replay.svelte";
import FxGallery from "./FxGallery.svelte";
@@ -26,6 +27,11 @@
net.startGamePolling();
let name = $state(prefs.wizardName);
+ /** The other games of the Kestrel's Nest, for the front page. */
+ let family = $state([]);
+ $effect(() => {
+ void otherGames("wizwar").then((g) => (family = g));
+ });
let joinCode = $state("");
let claimPhrase = $state("");
/** A /join/ recruiting link. The visitor lands looking at the
@@ -2561,6 +2567,10 @@
watch clips from the labyrinth
+ {#if family.length}
+ Also at the Kestrel's Nest:
+ {#each family as g, i (g.slug)}{i > 0 ? ", " : " "}{g.name}{/each}
+ {/if}
@@ -4108,6 +4118,8 @@
margin-bottom: 1.2rem;
}
.about-link:hover { color: #43331f; }
+ .family-line { margin: 0.5rem 0 0; font-size: 0.85rem; color: #7b6a51; }
+ .family-line a { color: #5a4630; }
.clips-link { display: inline-block; margin: 0 0 0.9rem; }
.field { display: block; text-align: left; margin-bottom: 1rem; }
.field span {
diff --git a/packages/web/src/Help.svelte b/packages/web/src/Help.svelte
index 8bd11e1..9931037 100644
--- a/packages/web/src/Help.svelte
+++ b/packages/web/src/Help.svelte
@@ -3,6 +3,7 @@
// body, the card library with proper entries, the house rulings, the
// story of the table, and the tally. Every section and card has a link
// a player can send; the desk remembers where the reader was.
+ import { otherGames, type FamilyGame } from "./family";
import { cardDef } from "@wizwar/engine";
import { tick } from "svelte";
import Card from "./Card.svelte";
@@ -127,6 +128,11 @@
if (initialAnchor) void jumpTo(initialAnchor);
else if (bodyEl) bodyEl.scrollTop = helpState.scroll[tab] ?? 0;
});
+ /** The other games of the Kestrel's Nest, for the about page. */
+ let family = $state([]);
+ $effect(() => {
+ void otherGames("wizwar").then((g) => (family = g));
+ });
{#snippet section(s: RefSection, withSource: boolean)}
@@ -311,6 +317,14 @@
The keeper of this table roosts at
kestrelsnest.social.
+ {#if family.length}
+ More games at the Kestrel's Nest
+
+ {#each family as g (g.slug)}
+ - {g.name}: {g.pitch} {g.players} players; {g.origin}.
+ {/each}
+
+ {/if}
Wiz-War © Tom Jolly. Sixth edition published by Chessex, 1993.
Built by Eric and a very enthusiastic AI, 2026.
@@ -691,4 +705,7 @@
.entry-card :global(.card:hover) { transform: none; }
.contents { columns: 1; }
}
+ .family { margin: 0.4rem 0 0.8rem 1.2rem; padding: 0; }
+ .family li + li { margin-top: 0.3rem; }
+ .family-note { color: #7b6a51; }
diff --git a/packages/web/src/family.ts b/packages/web/src/family.ts
new file mode 100644
index 0000000..c18054f
--- /dev/null
+++ b/packages/web/src/family.ts
@@ -0,0 +1,23 @@
+// The other games of the Kestrel's Nest, from the family list the kit keeps
+// (family.json, one copy shipped with every game), so a player who liked one
+// table can find the next. This game is left out of its own list.
+
+export interface FamilyGame {
+ slug: string;
+ name: string;
+ url: string;
+ pitch: string;
+ players: string;
+ origin: string;
+}
+
+export async function otherGames(self: string): Promise {
+ try {
+ const res = await fetch('/family.json', { cache: 'no-cache' });
+ if (!res.ok) return [];
+ const data = (await res.json()) as { games?: FamilyGame[] };
+ return (data.games ?? []).filter((g) => g.slug !== self);
+ } catch {
+ return [];
+ }
+}