The hall comes first, in the shape of wizwar's landing
The home page is a centred card: title and pitch, your name, play the bot or continue the duel under way, create or join a duel by code, a sample ledger, the duels this browser holds with whose move it is, and the story of the game. The single-player board lives at /play. The droplet gains the duel server as a systemd service behind Caddy. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
e1bda87987
commit
6c6edca4be
9 files changed
+578
-254
No files matched your search
+21
-188
@@ -1,204 +1,37 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import Board from '$lib/components/Board.svelte';
|
||||
import Hall from '$lib/components/Hall.svelte';
|
||||
import { COMPACT_QUERY, duel, NAME_MAX } from '$lib/game/duel.svelte';
|
||||
|
||||
let confirmingNew = $state(false);
|
||||
let renaming = $state(false);
|
||||
let nameDraft = $state('');
|
||||
|
||||
function startRename() {
|
||||
nameDraft = duel.you.name;
|
||||
renaming = true;
|
||||
}
|
||||
|
||||
function saveName(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
duel.setName(nameDraft);
|
||||
renaming = false;
|
||||
}
|
||||
|
||||
function startNew() {
|
||||
if (duel.inProgress && !confirmingNew) {
|
||||
confirmingNew = true;
|
||||
return;
|
||||
}
|
||||
confirmingNew = false;
|
||||
duel.newGame();
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
duel.persist();
|
||||
});
|
||||
// The store's idea of a narrow screen must agree with the stylesheets' breakpoint.
|
||||
$effect(() => {
|
||||
const query = window.matchMedia(COMPACT_QUERY);
|
||||
const apply = () => (duel.compact = query.matches);
|
||||
apply();
|
||||
query.addEventListener('change', apply);
|
||||
return () => query.removeEventListener('change', apply);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Waving Hands</title>
|
||||
</svelte:head>
|
||||
|
||||
<header class="masthead">
|
||||
<div>
|
||||
<h1>Waving Hands</h1>
|
||||
{#if renaming}
|
||||
<form class="rename" onsubmit={saveName}>
|
||||
<label class="field">
|
||||
<span>Your name</span>
|
||||
<!-- svelte-ignore a11y_autofocus -->
|
||||
<input type="text" bind:value={nameDraft} maxlength={NAME_MAX} autofocus autocomplete="nickname" placeholder="Leave empty for a drawn name" />
|
||||
</label>
|
||||
<button type="submit" class="reveal small">Save</button>
|
||||
<button type="button" class="quiet" onclick={() => (renaming = false)}>Cancel</button>
|
||||
</form>
|
||||
{:else}
|
||||
<p class="standfirst">
|
||||
You are <em>{duel.you.name}</em><button type="button" class="link" onclick={startRename}>change name</button>, duelling <em>{duel.foe.name}</em>.
|
||||
{#if duel.state.turn === 0}Each turn, both wizards choose one gesture per hand and reveal them together. The right run of gestures on one hand is a spell.{/if}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<nav class="actions">
|
||||
<a class="quiet" href="/rules">Rules</a>
|
||||
<button type="button" class="quiet" onclick={startNew}>New duel</button>
|
||||
<a class="quiet" href="#hall">Duel a friend</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
{#if confirmingNew}
|
||||
<div class="confirm" role="alertdialog" aria-labelledby="confirm-heading">
|
||||
<p id="confirm-heading">Abandon the duel against {duel.foe.name}? Turn {duel.turnNumber} will be lost.</p>
|
||||
<div class="choices">
|
||||
<button type="button" class="reveal small" onclick={startNew}>Start a new duel</button>
|
||||
<button type="button" class="quiet" onclick={() => (confirmingNew = false)}>Keep playing</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Board {duel} />
|
||||
|
||||
<Hall name={duel.you.name} />
|
||||
|
||||
<footer class="colophon">
|
||||
<p>After Richard Bartle's <em>Waving Hands</em> (1977), also known as Spellbinder and Spellcaster. <a href="/rules">Read the full rules</a>.</p>
|
||||
</footer>
|
||||
<main class="hall-page">
|
||||
<Hall />
|
||||
<footer class="colophon">
|
||||
<p>After Richard Bartle's <em>Waving Hands</em> (1977), also known as Spellbinder and Spellcaster. <a href="/rules">Read the full rules</a>.</p>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.masthead {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1rem var(--gutter) 0.6rem;
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(2rem, 4vw, 2.7rem);
|
||||
font-weight: 300;
|
||||
font-variation-settings: 'opsz' 144;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.standfirst {
|
||||
max-width: 38em;
|
||||
color: var(--bone-dim);
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
|
||||
.standfirst em {
|
||||
color: var(--bone);
|
||||
}
|
||||
|
||||
.standfirst .link {
|
||||
margin-left: 0.5em;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.rename {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.5rem 0.75rem;
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
|
||||
.rename input {
|
||||
width: 14em;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.quiet {
|
||||
background: none;
|
||||
border: 1px solid var(--rule-strong);
|
||||
border-radius: 4px;
|
||||
padding: 0.4rem 0.9rem;
|
||||
white-space: nowrap;
|
||||
color: var(--bone);
|
||||
text-decoration: none;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.quiet:hover {
|
||||
border-color: var(--bone);
|
||||
}
|
||||
|
||||
.confirm {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto 0.75rem;
|
||||
padding: 0.75rem var(--gutter);
|
||||
}
|
||||
|
||||
.confirm p {
|
||||
color: var(--blood);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.choices {
|
||||
display: flex;
|
||||
gap: 0.6rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.reveal.small {
|
||||
padding: 0.4rem 0.9rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.hall-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: clamp(1rem, 4vh, 3rem) var(--gutter) 2rem;
|
||||
}
|
||||
|
||||
.colophon {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem var(--gutter) 3rem;
|
||||
color: var(--bone-faint);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
max-width: 54rem;
|
||||
width: 100%;
|
||||
padding: 1.5rem 0 0;
|
||||
color: var(--bone-faint);
|
||||
font-size: 0.85rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.colophon a {
|
||||
color: var(--frost);
|
||||
}
|
||||
|
||||
/* Must match COMPACT_QUERY in duel.svelte.ts. */
|
||||
@media (max-width: 860px) {
|
||||
.colophon {
|
||||
padding-bottom: 6rem;
|
||||
}
|
||||
|
||||
.masthead {
|
||||
flex-direction: column;
|
||||
}
|
||||
color: var(--frost);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,201 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import Board from '$lib/components/Board.svelte';
|
||||
import { COMPACT_QUERY, duel, NAME_MAX } from '$lib/game/duel.svelte';
|
||||
|
||||
let confirmingNew = $state(false);
|
||||
let renaming = $state(false);
|
||||
let nameDraft = $state('');
|
||||
|
||||
function startRename() {
|
||||
nameDraft = duel.you.name;
|
||||
renaming = true;
|
||||
}
|
||||
|
||||
function saveName(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
duel.setName(nameDraft);
|
||||
renaming = false;
|
||||
}
|
||||
|
||||
function startNew() {
|
||||
if (duel.inProgress && !confirmingNew) {
|
||||
confirmingNew = true;
|
||||
return;
|
||||
}
|
||||
confirmingNew = false;
|
||||
duel.newGame();
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
duel.persist();
|
||||
});
|
||||
// The store's idea of a narrow screen must agree with the stylesheets' breakpoint.
|
||||
$effect(() => {
|
||||
const query = window.matchMedia(COMPACT_QUERY);
|
||||
const apply = () => (duel.compact = query.matches);
|
||||
apply();
|
||||
query.addEventListener('change', apply);
|
||||
return () => query.removeEventListener('change', apply);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Waving Hands: against the bot</title>
|
||||
</svelte:head>
|
||||
|
||||
<header class="masthead">
|
||||
<div>
|
||||
<h1>Waving Hands</h1>
|
||||
{#if renaming}
|
||||
<form class="rename" onsubmit={saveName}>
|
||||
<label class="field">
|
||||
<span>Your name</span>
|
||||
<!-- svelte-ignore a11y_autofocus -->
|
||||
<input type="text" bind:value={nameDraft} maxlength={NAME_MAX} autofocus autocomplete="nickname" placeholder="Leave empty for a drawn name" />
|
||||
</label>
|
||||
<button type="submit" class="reveal small">Save</button>
|
||||
<button type="button" class="quiet" onclick={() => (renaming = false)}>Cancel</button>
|
||||
</form>
|
||||
{:else}
|
||||
<p class="standfirst">
|
||||
You are <em>{duel.you.name}</em><button type="button" class="link" onclick={startRename}>change name</button>, duelling <em>{duel.foe.name}</em>.
|
||||
{#if duel.state.turn === 0}Each turn, both wizards choose one gesture per hand and reveal them together. The right run of gestures on one hand is a spell.{/if}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<nav class="actions">
|
||||
<a class="quiet" href="/rules">Rules</a>
|
||||
<button type="button" class="quiet" onclick={startNew}>New duel</button>
|
||||
<a class="quiet" href="/">The hall</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
{#if confirmingNew}
|
||||
<div class="confirm" role="alertdialog" aria-labelledby="confirm-heading">
|
||||
<p id="confirm-heading">Abandon the duel against {duel.foe.name}? Turn {duel.turnNumber} will be lost.</p>
|
||||
<div class="choices">
|
||||
<button type="button" class="reveal small" onclick={startNew}>Start a new duel</button>
|
||||
<button type="button" class="quiet" onclick={() => (confirmingNew = false)}>Keep playing</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Board {duel} />
|
||||
|
||||
<footer class="colophon">
|
||||
<p>After Richard Bartle's <em>Waving Hands</em> (1977), also known as Spellbinder and Spellcaster. <a href="/rules">Read the full rules</a>.</p>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
.masthead {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1rem var(--gutter) 0.6rem;
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(2rem, 4vw, 2.7rem);
|
||||
font-weight: 300;
|
||||
font-variation-settings: 'opsz' 144;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.standfirst {
|
||||
max-width: 38em;
|
||||
color: var(--bone-dim);
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
|
||||
.standfirst em {
|
||||
color: var(--bone);
|
||||
}
|
||||
|
||||
.standfirst .link {
|
||||
margin-left: 0.5em;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.rename {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.5rem 0.75rem;
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
|
||||
.rename input {
|
||||
width: 14em;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.quiet {
|
||||
background: none;
|
||||
border: 1px solid var(--rule-strong);
|
||||
border-radius: 4px;
|
||||
padding: 0.4rem 0.9rem;
|
||||
white-space: nowrap;
|
||||
color: var(--bone);
|
||||
text-decoration: none;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.quiet:hover {
|
||||
border-color: var(--bone);
|
||||
}
|
||||
|
||||
.confirm {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto 0.75rem;
|
||||
padding: 0.75rem var(--gutter);
|
||||
}
|
||||
|
||||
.confirm p {
|
||||
color: var(--blood);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.choices {
|
||||
display: flex;
|
||||
gap: 0.6rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.reveal.small {
|
||||
padding: 0.4rem 0.9rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.colophon {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem var(--gutter) 3rem;
|
||||
color: var(--bone-faint);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.colophon a {
|
||||
color: var(--frost);
|
||||
}
|
||||
|
||||
/* Must match COMPACT_QUERY in duel.svelte.ts. */
|
||||
@media (max-width: 860px) {
|
||||
.colophon {
|
||||
padding-bottom: 6rem;
|
||||
}
|
||||
|
||||
.masthead {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user