Players can choose their own name

An inline editor in the standfirst; the name is remembered in the
browser, applied to the current duel, and used for every duel after.
The opponent is drawn from the list avoiding the player's name.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-09-22 15:48:47 -04:00
co-authored by Claude Fable 5.1
parent d40734590b
commit 3716f49bbb
2 changed files with 103 additions and 7 deletions
+31 -3
View File
@@ -98,9 +98,21 @@ function blankSets(): Record<SetName, Record<Hand, HandDraft>> {
return { main: { left: blankDraft(), right: blankDraft() }, haste: { left: blankDraft(), right: blankDraft() } }; return { main: { left: blankDraft(), right: blankDraft() }, haste: { left: blankDraft(), right: blankDraft() } };
} }
function pickNames(): { A: string; B: string } { const NAME_KEY = 'wh:name';
const pool = [...NAMES]; export const NAME_MAX = 24;
const a = pool.splice(Math.floor(Math.random() * pool.length), 1)[0];
function readName(): string {
try {
return (localStorage.getItem(NAME_KEY) ?? '').trim().slice(0, NAME_MAX);
} catch {
return '';
}
}
/** The player's chosen name if they have one, and an opponent who is not called the same. */
function pickNames(player = readName()): { A: string; B: string } {
const pool = NAMES.filter((n) => n.toLowerCase() !== player.toLowerCase());
const a = player || pool.splice(Math.floor(Math.random() * pool.length), 1)[0];
const b = pool[Math.floor(Math.random() * pool.length)]; const b = pool[Math.floor(Math.random() * pool.length)];
return { A: a, B: b }; return { A: a, B: b };
} }
@@ -227,6 +239,22 @@ export class Duel {
} }
} }
/** Rename the player for this duel and every duel after it. An empty name goes back to a drawn one. */
setName(raw: string): void {
const name = raw.trim().slice(0, NAME_MAX);
try {
if (name) localStorage.setItem(NAME_KEY, name);
else localStorage.removeItem(NAME_KEY);
} catch {
// The name still applies to this duel.
}
const next = name || pickNames('').A;
if (next.toLowerCase() === this.foe.name.toLowerCase()) {
this.state.wizards[FOE].name = pickNames(next).B;
}
this.state.wizards[YOU].name = next;
}
/** Write the duel and the half-written turn to local storage. Reads everything it saves, so an effect can track it. */ /** Write the duel and the half-written turn to local storage. Reads everything it saves, so an effect can track it. */
persist(): void { persist(): void {
const bundle: SavedDuel = { const bundle: SavedDuel = {
+72 -4
View File
@@ -6,11 +6,24 @@
import SpellSheet from '$lib/components/SpellSheet.svelte'; import SpellSheet from '$lib/components/SpellSheet.svelte';
import TurnSummary from '$lib/components/TurnSummary.svelte'; import TurnSummary from '$lib/components/TurnSummary.svelte';
import WizardStatus from '$lib/components/WizardStatus.svelte'; import WizardStatus from '$lib/components/WizardStatus.svelte';
import { duel, FOE, YOU, castKey, tokensText } from '$lib/game/duel.svelte'; import { duel, FOE, YOU, NAME_MAX, castKey, tokensText } from '$lib/game/duel.svelte';
import { GESTURE_SHORT, glyph } from '$lib/game/glyphs'; import { GESTURE_SHORT, glyph } from '$lib/game/glyphs';
import { MAGIC_GESTURES } from '$lib/game/spells'; import { MAGIC_GESTURES } from '$lib/game/spells';
let confirmingNew = $state(false); 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() { function startNew() {
if (duel.inProgress && !confirmingNew) { if (duel.inProgress && !confirmingNew) {
@@ -45,10 +58,21 @@
<header class="masthead"> <header class="masthead">
<div> <div>
<h1>Waving Hands</h1> <h1>Waving Hands</h1>
{#if duel.state.turn === 0 && !duel.timeStopped} {#if renaming}
<p class="standfirst">You are <em>{duel.you.name}</em>, duelling <em>{duel.foe.name}</em>. Each turn, both wizards choose one gesture per hand and reveal them together. The right run of gestures on one hand is a spell.</p> <form class="rename" onsubmit={saveName}>
<label>
<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} {:else}
<p class="standfirst">You are <em>{duel.you.name}</em>, duelling <em>{duel.foe.name}</em>.</p> <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 && !duel.timeStopped}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} {/if}
</div> </div>
<nav class="actions"> <nav class="actions">
@@ -272,6 +296,50 @@
color: var(--bone); color: var(--bone);
} }
.link {
background: none;
border: 0;
padding: 0;
margin-left: 0.5em;
font-size: 0.8rem;
color: var(--frost);
text-decoration: underline;
vertical-align: baseline;
}
.rename {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem 0.75rem;
margin-top: 0.4rem;
}
.rename label {
display: inline-flex;
align-items: baseline;
gap: 0.5rem;
color: var(--bone-dim);
font-style: italic;
}
.rename input {
font: inherit;
font-style: normal;
color: var(--bone);
background: var(--slate-deep);
border: 1px solid var(--rule-strong);
border-radius: 4px;
padding: 0.3rem 0.6rem;
width: 14em;
max-width: 100%;
}
.rename input:focus-visible {
outline: 2px solid var(--frost);
outline-offset: 1px;
}
.actions { .actions {
display: flex; display: flex;
gap: 0.5rem; gap: 0.5rem;