Begin Wyrm from the game kit
This commit is contained in:
commit
d91b95cfab
73 files changed
+10509
No files matched your search
@@ -0,0 +1,154 @@
|
||||
<script lang="ts">
|
||||
// The host's hand on the other seats during a game: a transfer phrase for a
|
||||
// player whose browser lost their seat, and a bot to play the seat of one
|
||||
// who cannot go on, until a phrase gives it back.
|
||||
import type { SeatId } from '$lib/game/spec';
|
||||
import type { Room } from '$lib/net/room.svelte';
|
||||
|
||||
let { room, open = $bindable(false) }: { room: Room; open?: boolean } = $props();
|
||||
|
||||
const people = $derived(room.view.seats.filter((s) => s.id !== room.me && (!s.bot || s.standIn)));
|
||||
let asked = $state<SeatId | null>(null);
|
||||
let phrase = $state<{ name: string; phrase: string; expiresAt: number } | null>(null);
|
||||
let error = $state('');
|
||||
|
||||
function close() {
|
||||
open = false;
|
||||
asked = null;
|
||||
error = '';
|
||||
}
|
||||
|
||||
async function makePhrase(seat: SeatId, name: string) {
|
||||
error = '';
|
||||
try {
|
||||
const made = await room.hostPhrase(seat);
|
||||
phrase = { name, ...made };
|
||||
setTimeout(() => {
|
||||
if (phrase?.phrase === made.phrase) phrase = null;
|
||||
}, Math.max(0, made.expiresAt - Date.now()));
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'The phrase could not be made.';
|
||||
}
|
||||
}
|
||||
|
||||
async function standIn(seat: SeatId) {
|
||||
asked = null;
|
||||
if (!(await room.standIn(seat))) error = room.error;
|
||||
}
|
||||
|
||||
function phraseTitle(name: string, standIn: boolean): string {
|
||||
const why = standIn ? `Brings ${name} back from the bot.` : `For when ${name}'s browser lost the seat, or they need to play from another device.`;
|
||||
return `${why} You get four words to pass to them. Within ten minutes, they enter them in the hall, or in the claim box while watching this table, and sit back down as ${name}. The phrase works once, and the browser they used before stops opening the seat.`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={(e) => open && e.key === 'Escape' && close()} />
|
||||
|
||||
{#if open}
|
||||
<div class="scrim" role="presentation" onclick={(ev) => ev.target === ev.currentTarget && close()}>
|
||||
<div class="slip" role="dialog" aria-labelledby="seats-heading">
|
||||
<h2 id="seats-heading">The other seats</h2>
|
||||
<p class="muted small">As the host, you can bring a player back to a seat their browser lost, or give the seat of one who cannot go on to a bot, which plays it at once.</p>
|
||||
{#if people.length === 0}
|
||||
<p class="muted">Nobody else at this table is a person.</p>
|
||||
{/if}
|
||||
<ul>
|
||||
{#each people as s (s.id)}
|
||||
<li>
|
||||
<span class="name">{s.name}{#if s.standIn}<span class="muted note">a bot plays for them</span>{/if}</span>
|
||||
<span class="ways">
|
||||
<button type="button" class="quiet" title={phraseTitle(s.name, !!s.standIn)} onclick={() => makePhrase(s.id, s.name)}>{s.standIn ? 'Transfer phrase to take it back' : 'Make a transfer phrase'}</button>
|
||||
{#if !s.standIn && !room.view.over}
|
||||
{#if asked === s.id}
|
||||
<button type="button" class="commit small" onclick={() => standIn(s.id)}>Yes, a bot plays for {s.name}</button>
|
||||
<button type="button" class="link" onclick={() => (asked = null)}>no</button>
|
||||
{:else}
|
||||
<button type="button" class="quiet" title="A bot plays {s.name}'s seat from their next move, and their browser stops opening it. A transfer phrase gives it back." onclick={() => (asked = s.id)}>Give to a bot</button>
|
||||
{/if}
|
||||
{/if}
|
||||
</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{#if phrase}
|
||||
<p class="made">Give {phrase.name} this phrase to claim within ten minutes, in the hall or while watching this table: <b class="phrase">{phrase.phrase}</b>. It works once, and their old link stops opening the seat.</p>
|
||||
{/if}
|
||||
{#if error}<p class="warning">{error}</p>{/if}
|
||||
<div class="actions"><button type="button" class="link" onclick={close}>close</button></div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.scrim {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
display: grid;
|
||||
grid-template-rows: minmax(0, 1fr);
|
||||
place-items: center;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.slip {
|
||||
width: min(100%, 34rem);
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
background: var(--slate);
|
||||
border: 1px solid var(--rule-strong);
|
||||
border-radius: 6px;
|
||||
padding: 1rem 1.2rem 1.2rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0.8rem 0 0;
|
||||
}
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.4rem 1rem;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
}
|
||||
|
||||
.note {
|
||||
margin-left: 0.5em;
|
||||
font-style: italic;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.ways {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.made {
|
||||
margin-top: 0.8rem;
|
||||
}
|
||||
|
||||
.phrase {
|
||||
letter-spacing: 0.06em;
|
||||
user-select: all;
|
||||
}
|
||||
|
||||
.warning {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user