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:
co-authored by
Claude Fable 5.1
parent
d40734590b
commit
3716f49bbb
@@ -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
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user