Hotseat setup round: pick a count, then each wizard names themselves

The comma-separated name field is gone. Hotseat now starts with a
2-6 count picker and "Gather N wizards", which opens a naming round
in the pass-the-device style: "Wizard 1 of 3 — what is your name?",
type it, pass the device on; the last wizard's button reads "Flip the
boards" and starts the game. Duplicate and empty names are refused
in place, the roster-so-far shows beneath, and "never mind" backs
out. Verified end to end: three names entered, game started, opening
handoff went to the die-roll winner.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-16 01:12:13 -04:00
co-authored by Claude Fable 5
parent ac82c28750
commit 98d7dee5c7
2 changed files with 92 additions and 12 deletions
+65 -12
View File
@@ -14,7 +14,8 @@
let joinCode = $state("");
let claimPhrase = $state("");
let showHelp = $state(false);
let hotseatNames = $state("");
let hotseatCount = $state(2);
let setupName = $state("");
/** Tap-to-inspect: hide the enlarged card for the current selection. */
let inspectorHidden = $state(false);
$effect(() => {
@@ -497,7 +498,31 @@
</div>
{/if}
{#if local.handoffTo}
{#if local.setup}
<div class="scrim-handoff">
<div class="handoff-card">
<div class="handoff-eyebrow">wizard {local.setup.names.length + 1} of {local.setup.count}</div>
<div class="handoff-name setup-title">What is your name?</div>
<form class="setup-form" onsubmit={(e) => {
e.preventDefault();
const err = local.submitName(setupName);
if (err) net.error = err;
else setupName = "";
}}>
<!-- svelte-ignore a11y_autofocus -->
<input class="setup-input" bind:value={setupName} maxlength="20"
placeholder="e.g. Mordecai" autofocus />
<button class="stamp" type="submit" disabled={!setupName.trim()}>
{local.setup.names.length + 1 === local.setup.count ? "Flip the boards" : "Pass the device on"}
</button>
</form>
{#if local.setup.names.length > 0}
<div class="setup-roster">so far: {local.setup.names.join(", ")}</div>
{/if}
<button class="hint-cancel setup-cancel" onclick={() => local.cancelSetup()}>never mind</button>
</div>
</div>
{:else if local.handoffTo}
<div class="scrim-handoff" role="button" tabindex="0"
onclick={() => local.takeSeat()} onkeydown={(e) => e.key === "Enter" && local.takeSeat()}>
<div class="handoff-card">
@@ -529,14 +554,15 @@
</div>
<div class="hotseat-row">
<input class="claim-input wide" bind:value={hotseatNames}
placeholder="hotseat: names, comma-separated" aria-label="hotseat player names" />
<button class="stamp tiny" disabled={hotseatNames.split(",").filter((n) => n.trim()).length < 2}
onclick={() => {
const err = local.start(hotseatNames.split(","), withExpansion);
if (err) net.error = err;
}}>
Play here
<span class="hotseat-label">or play here, passing the device:</span>
<div class="count-picker" role="group" aria-label="number of wizards">
{#each [2, 3, 4, 5, 6] as n (n)}
<button class="count-btn" class:current={hotseatCount === n}
onclick={() => (hotseatCount = n)}>{n}</button>
{/each}
</div>
<button class="stamp tiny" onclick={() => { setupName = ""; local.beginSetup(hotseatCount, withExpansion); }}>
Gather {hotseatCount} wizards
</button>
{#if local.hasSave()}
<button class="stamp tiny" onclick={() => local.resume()}>Resume hotseat</button>
@@ -865,7 +891,6 @@
}
.mast-leave:hover { color: #d8d2c0; }
.mast-help-solo { margin-left: auto; }
.claim-input.wide { width: 17rem; }
.hotseat-row { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; justify-content: center; margin-top: 1.3rem; }
.hotseat-exp { margin-top: 0.4rem; margin-bottom: 0; font-size: 0.85rem; }
@@ -899,6 +924,34 @@
line-height: 1.15;
}
.handoff-hint { font-size: 0.85rem; color: #6b5a41; margin-top: 0.4rem; }
.setup-title { font-size: 2.2rem; }
.setup-form { display: flex; flex-direction: column; gap: 0.7rem; margin-top: 0.8rem; }
.setup-input {
background: #f4eede;
border: 1px solid #b3a687;
border-radius: 4px;
padding: 0.6rem 0.8rem;
font-family: "Archivo Narrow", sans-serif;
font-size: 1.1rem;
color: #43331f;
text-align: center;
}
.setup-roster { margin-top: 0.8rem; font-size: 0.85rem; color: #6b5a41; }
.setup-cancel { margin: 0.6rem auto 0; display: block; }
.hotseat-label { flex-basis: 100%; font-size: 0.85rem; color: #6b5a41; }
.count-picker { display: flex; gap: 0.3rem; }
.count-btn {
width: 2.1rem;
height: 2.1rem;
border-radius: 50%;
border: 1.5px solid #6b5a41;
background: none;
color: #43331f;
font-family: "Oswald", sans-serif;
font-size: 0.95rem;
cursor: pointer;
}
.count-btn.current { background: #43331f; color: #e9e1cb; }
.toast {
background: #6d2119;
@@ -1277,7 +1330,7 @@
.boxlid { padding: 1rem 0; }
.boxlid-inner { padding: 1.5rem 1.2rem 1.4rem; }
.boxlid-title { font-size: 2.3rem; }
.claim-input.wide, .claim-input { width: 100%; flex: 1 1 100%; }
.claim-input { width: 100%; flex: 1 1 100%; }
.hotseat-row .stamp, .claim-row .stamp { flex: 0 0 auto; }
.mast-title { font-size: 1.05rem; white-space: nowrap; }
.mast-sub { display: none; }