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:
co-authored by
Claude Fable 5
parent
ac82c28750
commit
98d7dee5c7
+65
-12
@@ -14,7 +14,8 @@
|
|||||||
let joinCode = $state("");
|
let joinCode = $state("");
|
||||||
let claimPhrase = $state("");
|
let claimPhrase = $state("");
|
||||||
let showHelp = $state(false);
|
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. */
|
/** Tap-to-inspect: hide the enlarged card for the current selection. */
|
||||||
let inspectorHidden = $state(false);
|
let inspectorHidden = $state(false);
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -497,7 +498,31 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/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"
|
<div class="scrim-handoff" role="button" tabindex="0"
|
||||||
onclick={() => local.takeSeat()} onkeydown={(e) => e.key === "Enter" && local.takeSeat()}>
|
onclick={() => local.takeSeat()} onkeydown={(e) => e.key === "Enter" && local.takeSeat()}>
|
||||||
<div class="handoff-card">
|
<div class="handoff-card">
|
||||||
@@ -529,14 +554,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hotseat-row">
|
<div class="hotseat-row">
|
||||||
<input class="claim-input wide" bind:value={hotseatNames}
|
<span class="hotseat-label">or play here, passing the device:</span>
|
||||||
placeholder="hotseat: names, comma-separated" aria-label="hotseat player names" />
|
<div class="count-picker" role="group" aria-label="number of wizards">
|
||||||
<button class="stamp tiny" disabled={hotseatNames.split(",").filter((n) => n.trim()).length < 2}
|
{#each [2, 3, 4, 5, 6] as n (n)}
|
||||||
onclick={() => {
|
<button class="count-btn" class:current={hotseatCount === n}
|
||||||
const err = local.start(hotseatNames.split(","), withExpansion);
|
onclick={() => (hotseatCount = n)}>{n}</button>
|
||||||
if (err) net.error = err;
|
{/each}
|
||||||
}}>
|
</div>
|
||||||
Play here
|
<button class="stamp tiny" onclick={() => { setupName = ""; local.beginSetup(hotseatCount, withExpansion); }}>
|
||||||
|
Gather {hotseatCount} wizards
|
||||||
</button>
|
</button>
|
||||||
{#if local.hasSave()}
|
{#if local.hasSave()}
|
||||||
<button class="stamp tiny" onclick={() => local.resume()}>Resume hotseat</button>
|
<button class="stamp tiny" onclick={() => local.resume()}>Resume hotseat</button>
|
||||||
@@ -865,7 +891,6 @@
|
|||||||
}
|
}
|
||||||
.mast-leave:hover { color: #d8d2c0; }
|
.mast-leave:hover { color: #d8d2c0; }
|
||||||
.mast-help-solo { margin-left: auto; }
|
.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-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; }
|
.hotseat-exp { margin-top: 0.4rem; margin-bottom: 0; font-size: 0.85rem; }
|
||||||
|
|
||||||
@@ -899,6 +924,34 @@
|
|||||||
line-height: 1.15;
|
line-height: 1.15;
|
||||||
}
|
}
|
||||||
.handoff-hint { font-size: 0.85rem; color: #6b5a41; margin-top: 0.4rem; }
|
.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 {
|
.toast {
|
||||||
background: #6d2119;
|
background: #6d2119;
|
||||||
@@ -1277,7 +1330,7 @@
|
|||||||
.boxlid { padding: 1rem 0; }
|
.boxlid { padding: 1rem 0; }
|
||||||
.boxlid-inner { padding: 1.5rem 1.2rem 1.4rem; }
|
.boxlid-inner { padding: 1.5rem 1.2rem 1.4rem; }
|
||||||
.boxlid-title { font-size: 2.3rem; }
|
.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; }
|
.hotseat-row .stamp, .claim-row .stamp { flex: 0 0 auto; }
|
||||||
.mast-title { font-size: 1.05rem; white-space: nowrap; }
|
.mast-title { font-size: 1.05rem; white-space: nowrap; }
|
||||||
.mast-sub { display: none; }
|
.mast-sub { display: none; }
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ function actorId(state: GameState): PlayerId {
|
|||||||
|
|
||||||
class LocalGame {
|
class LocalGame {
|
||||||
active = $state(false);
|
active = $state(false);
|
||||||
|
/** Hotseat setup round: players name themselves one by one. */
|
||||||
|
setup = $state<{ count: number; expansion: boolean; names: PlayerId[] } | null>(null);
|
||||||
gameState = $state<GameState | null>(null);
|
gameState = $state<GameState | null>(null);
|
||||||
viewerId = $state<PlayerId | null>(null);
|
viewerId = $state<PlayerId | null>(null);
|
||||||
/** Set while the device should be handed to the named player. */
|
/** Set while the device should be handed to the named player. */
|
||||||
@@ -51,6 +53,31 @@ class LocalGame {
|
|||||||
return localStorage.getItem(SAVE_KEY) !== null;
|
return localStorage.getItem(SAVE_KEY) !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
beginSetup(count: number, expansion: boolean): void {
|
||||||
|
this.setup = { count, expansion, names: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelSetup(): void {
|
||||||
|
this.setup = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One wizard names themselves; the last name starts the game. */
|
||||||
|
submitName(raw: string): string | null {
|
||||||
|
if (!this.setup) return "no setup in progress";
|
||||||
|
const name = raw.trim();
|
||||||
|
if (!name) return "every wizard needs a name";
|
||||||
|
if (this.setup.names.some((n) => n.toLowerCase() === name.toLowerCase())) {
|
||||||
|
return "that name is already taken at this table";
|
||||||
|
}
|
||||||
|
this.setup = { ...this.setup, names: [...this.setup.names, name] };
|
||||||
|
if (this.setup.names.length === this.setup.count) {
|
||||||
|
const { names, expansion } = this.setup;
|
||||||
|
this.setup = null;
|
||||||
|
return this.start(names, expansion);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
start(names: PlayerId[], expansion: boolean): string | null {
|
start(names: PlayerId[], expansion: boolean): string | null {
|
||||||
const cleaned = [...new Set(names.map((n) => n.trim()).filter(Boolean))];
|
const cleaned = [...new Set(names.map((n) => n.trim()).filter(Boolean))];
|
||||||
if (cleaned.length < 2 || cleaned.length > 6) return "two to six wizards, each with a name";
|
if (cleaned.length < 2 || cleaned.length > 6) return "two to six wizards, each with a name";
|
||||||
|
|||||||
Reference in New Issue
Block a user