Files
hnefatafl/src/lib/components/Lobby.svelte
T

188 lines
6.7 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import TableTalk from './TableTalk.svelte';
import type { Room } from '$lib/net/room.svelte';
let { room, link, copied, copyLink }: { room: Room; link: string; copied: boolean; copyLink: () => void } = $props();
const v = $derived(room.view);
/** The host asked to begin while a seat is still held for someone on their way. */
let beginAsked = $state(false);
const hostName = $derived(v.seats.find((s) => s.id === v.host)?.name ?? 'the host');
const keeperSeated = $derived(v.seats.some((s) => s.name.toLowerCase() === v.keeper.toLowerCase()));
/** The hour where the keeper lives, in words, so a caller knows whether to wait. */
let minute = $state(0);
$effect(() => {
const id = setInterval(() => (minute += 1), 60_000);
return () => clearInterval(id);
});
const keeperHour = $derived.by(() => {
void minute;
try {
const hour = Number(new Intl.DateTimeFormat('en-US', { hour: 'numeric', hour12: false, timeZone: v.keeperZone }).format(new Date())) % 24;
if (hour === 0) return 'midnight';
if (hour === 12) return 'noon';
const word = ['twelve', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'eleven'][hour % 12];
const part = hour < 5 ? 'in the small hours' : hour < 12 ? 'in the morning' : hour < 18 ? 'in the afternoon' : 'in the evening';
return `${word} ${part}`;
} catch {
return '';
}
});
</script>
<section class="lobby">
{#if room.spectating}
<h2>At the table</h2>
{#if room.seatEmpty}
<p>{hostName} is gathering players. Sit down above, or watch from the gallery until the game begins.</p>
{:else}
<p>The table is full, so you watch from the Peanut Gallery. The game begins when {hostName} says.</p>
{/if}
{:else}
<p class="eyebrow">room {v.roomId}</p>
<h2>The table is laid</h2>
<p>Send this link, or the code. Whoever opens it takes a seat, and the game begins when you say. Anyone who arrives once it has begun watches from the Peanut Gallery.</p>
<p class="link-row"><code>{link}</code><button type="button" class="quiet" onclick={copyLink}>{copied ? 'Copied' : 'Copy link'}</button></p>
{/if}
<ul class="roster">
{#each v.seats as s (s.id)}
<li>
<span class="seat-name">{s.name}</span>
<span class="muted">{s.id === v.host ? 'opened the table' : s.bot ? 'a housecarl' : 'seated'}{s.id === v.me ? ', you' : ''}{#if s.bot && room.isHost}
<button type="button" class="dismiss" title="Send this housecarl away" aria-label="Send {s.name} away" onclick={() => room.unseat(s.id)}>×</button>{/if}</span>
</li>
{/each}
{#each v.expected as name (name)}
<li>
<span class="seat-name">{name}</span>
<span class="muted">{v.challenge && name.toLowerCase() === v.keeper.toLowerCase() ? 'called to the table; a seat is held' : 'from the last table; a seat is held'}</span>
</li>
{/each}
<li class="empty muted">{v.size - v.seats.length - v.expected.length} of {v.size} seats empty{#if v.audience > 0}; {v.audience} in the gallery{v.galleryTalk ? ', allowed to talk' : ''}{/if}</li>
</ul>
{#if v.rematchOf}
<p class="muted">This is a rematch of table {v.rematchOf}, with seats held for those not yet back.</p>
{/if}
{#if !room.spectating}
<div class="ways">
{#if room.seatEmpty && room.isHost}
<button type="button" class="quiet" onclick={() => room.addBot()}>Seat a housecarl</button>
{/if}
{#if room.isHost && beginAsked && v.expected.length > 0}
<span>A seat is held for {v.expected.join(' and ')}, who {v.expected.length === 1 ? 'has' : 'have'} not arrived. The seat goes once the game begins.</span>
<button type="button" class="commit small" onclick={() => room.begin()}>Begin without them</button>
<button type="button" class="link" onclick={() => (beginAsked = false)}>wait</button>
{:else if room.isHost}
<button type="button" class="commit small" disabled={v.seats.length < 2} onclick={() => (v.expected.length > 0 ? (beginAsked = true) : room.begin())}>Begin</button>
<span class="muted">{v.seats.length < 2 ? 'once a second player is seated' : `with ${v.seats.length} players`}</span>
{:else}
<span class="muted">{hostName} will begin when the table is ready.</span>
{/if}
</div>
{#if room.isHost}
<label class="switch muted">
<input type="checkbox" checked={v.galleryTalk} onchange={(e) => room.setGalleryTalk(e.currentTarget.checked)} />
Let the Peanut Gallery talk at the table
</label>
{/if}
{#if v.keeper && !v.challenge && !keeperSeated && room.seatUnheld}
<div class="keeper">
<button type="button" class="quiet" title="A seat is held for {v.keeper}, who keeps this site, and their phone rings" onclick={() => room.callKeeper()}>Challenge {v.keeper}, the keeper</button>
<p class="muted small note">{v.keeper} keeps this site and answers every call, sometimes within the minute, sometimes after a night's sleep.{#if keeperHour}&nbsp;It is {keeperHour} where {v.keeper} lives.{/if} The seat is held either way.</p>
</div>
{:else if v.challenge}
<p class="muted small note">{v.keeper} has been called to the table by {room.nameOf(v.challenge.by)}.{#if keeperHour}&nbsp;It is {keeperHour} there.{/if} A seat is held for them. If they can come now, they will take it. If they are asleep or away, they will leave a word below when they can. In the meantime you can seat a housecarl, invite a friend, or play another game in a new tab. This table keeps its place in your hall until you come back.</p>
{/if}
{/if}
{#if room.error}<p class="warning">{room.error}</p>{/if}
<div class="lobby-talk"><TableTalk {room} /></div>
</section>
<style>
.lobby {
max-width: 1280px;
margin: 0 auto;
padding: 1rem var(--gutter) 2rem;
}
.lobby h2 {
font-size: 1.4rem;
margin-bottom: 0.4rem;
}
.lobby p + p {
margin-top: 0.6rem;
}
.roster {
list-style: none;
margin: 0.9rem 0 0.6rem;
padding: 0;
max-width: 28em;
}
.roster li {
display: flex;
justify-content: space-between;
gap: 1rem;
padding: 0.3rem 0;
border-bottom: 1px solid var(--rule);
}
.roster li.empty {
border-bottom: 0;
justify-content: flex-start;
font-style: italic;
}
.seat-name {
font-weight: 500;
}
.ways {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem 0.9rem;
margin-top: 0.4rem;
}
.link-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem 0.75rem;
}
code {
font-family: var(--font);
background: var(--slate);
padding: 0.3rem 0.6rem;
border-radius: 4px;
word-break: break-all;
}
.lobby-talk {
max-width: 28em;
margin-top: 1rem;
}
.switch {
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: 0.6rem;
font-size: 0.9rem;
}
.keeper {
margin-top: 0.8rem;
max-width: 38em;
}
.note {
margin-top: 0.4rem;
}
</style>