diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index 5274765..1e94c84 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -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 @@ {/if} - {#if local.handoffTo} + {#if local.setup} +
+
+
wizard {local.setup.names.length + 1} of {local.setup.count}
+
What is your name?
+
{ + e.preventDefault(); + const err = local.submitName(setupName); + if (err) net.error = err; + else setupName = ""; + }}> + + + +
+ {#if local.setup.names.length > 0} +
so far: {local.setup.names.join(", ")}
+ {/if} + +
+
+ {:else if local.handoffTo}
local.takeSeat()} onkeydown={(e) => e.key === "Enter" && local.takeSeat()}>
@@ -529,14 +554,15 @@
- - + {/each} +
+ {#if local.hasSave()} @@ -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; } diff --git a/packages/web/src/local.svelte.ts b/packages/web/src/local.svelte.ts index 3a1896b..8e33e9c 100644 --- a/packages/web/src/local.svelte.ts +++ b/packages/web/src/local.svelte.ts @@ -35,6 +35,8 @@ function actorId(state: GameState): PlayerId { class LocalGame { active = $state(false); + /** Hotseat setup round: players name themselves one by one. */ + setup = $state<{ count: number; expansion: boolean; names: PlayerId[] } | null>(null); gameState = $state(null); viewerId = $state(null); /** Set while the device should be handed to the named player. */ @@ -51,6 +53,31 @@ class LocalGame { 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 { 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";