The keeper can bring back a seat a browser lost: a reseat line on the ledger gives that wizard's seat a new token and the old one opens nothing after, wizwar-reseat.sh on the host asks for it through a keeper door that listens on the loopback alone and opens only to a key the deploy makes once, and the link it prints carries the seat in the fragment, which leaves the address bar at once and is kept only when no seat held in this browser still opens and the server takes it; a gallery watching a game in play is told both ways back, the front page asks before forgetting a game not yet finished, and the server's tests now run with the rest
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MbtUPJ24P6vc6tE1x6RS3m
This commit is contained in:
1 parent
1b9acfd7c4
commit
5ea0d4fdec
16 files changed
+423
-16
No files matched your search
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { motion } from "./motion";
|
||||
import { attentionLabel, net, SIDE_NAMES, spellName } from "./net.svelte";
|
||||
import { attentionLabel, net, seatInLink, SIDE_NAMES, spellName } from "./net.svelte";
|
||||
import { MediaQuery } from "svelte/reactivity";
|
||||
import { FISTS } from "./fpv/paintedFx";
|
||||
import Board from "./Board.svelte";
|
||||
@@ -37,14 +37,29 @@
|
||||
* seat or the gallery, a started game is watched from the gallery. */
|
||||
const inviteCode = location.pathname.match(/^\/join\/([A-Za-z0-9]{4})$/)?.[1]?.toUpperCase() ?? null;
|
||||
if (inviteCode) joinCode = inviteCode;
|
||||
/** A link from the keeper carries a seat in its fragment. It leaves the
|
||||
* address bar at once, is tried only when no seat held here still opens,
|
||||
* and is kept only once the server takes it, so a link someone else
|
||||
* sends cannot take a working seat away. */
|
||||
const linkedSeat = inviteCode ? seatInLink(location.hash, inviteCode) : null;
|
||||
if (location.hash.startsWith("#seat=")) history.replaceState(history.state, "", location.pathname + location.search);
|
||||
let inviteFollowed = false;
|
||||
$effect(() => {
|
||||
if (!inviteCode || inviteFollowed || net.status !== "connected" || net.roomId) return;
|
||||
inviteFollowed = true;
|
||||
const seat = net.seats.find((s) => s.roomId === inviteCode);
|
||||
if (seat) net.resume(seat);
|
||||
else net.watch(inviteCode);
|
||||
const code = inviteCode;
|
||||
const fromLink = linkedSeat && (() => net.resume(linkedSeat, () => {
|
||||
net.watch(code);
|
||||
net.flash("The keeper's link no longer opens that seat. Write again for a fresh one.");
|
||||
}));
|
||||
const seat = net.seats.find((s) => s.roomId === code);
|
||||
if (seat) net.resume(seat, fromLink || undefined);
|
||||
else if (fromLink) fromLink();
|
||||
else net.watch(code);
|
||||
});
|
||||
/** A game not yet finished asks before it is forgotten: this browser may
|
||||
* be all that holds the seat. */
|
||||
let forgetting = $state<string | null>(null);
|
||||
/** The address bar holds the table's invite link while you sit at one,
|
||||
* so recruiting is a copy away. */
|
||||
$effect(() => {
|
||||
@@ -2701,8 +2716,15 @@
|
||||
<button class="stamp tiny" title="take your seat at the rematch"
|
||||
onclick={() => { net.you = seat.name; net.spectating = false; net.join(g.rematch!.roomId, seat.name); }}>rematch</button>
|
||||
{/if}
|
||||
<button class="ledger-forget" title="forget this game"
|
||||
onclick={() => net.forgetSeat(seat.roomId)}>×</button>
|
||||
{#if forgetting === seat.roomId}
|
||||
<span class="forget-ask">This browser may be all that holds your seat.
|
||||
<button class="hint-cancel" onclick={() => { forgetting = null; net.forgetSeat(seat.roomId); }}>forget it</button>
|
||||
<button class="hint-cancel" onclick={() => (forgetting = null)}>keep it</button>
|
||||
</span>
|
||||
{:else}
|
||||
<button class="ledger-forget" title="forget this game"
|
||||
onclick={() => { if (g?.finished) net.forgetSeat(seat.roomId); else forgetting = seat.roomId; }}>×</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{#if net.feedbackReports.length > 0}
|
||||
@@ -3731,6 +3753,9 @@
|
||||
<div class="hand" bind:this={handEl} role="list" class:spent={actionsSpent && !discardMode && discardSelection.size === 0} aria-label="your hand">
|
||||
{#if net.spectating}
|
||||
<span class="gallery-note">👁 You watch from the Peanut Gallery, and every hand stays secret, even from you.</span>
|
||||
{#if view.phase !== "finished"}
|
||||
<span class="gallery-note">Had a seat at this table on another device? Choose <em>transfer seat</em> there and claim the phrase on the front page, or <a href="mailto:eric@ericwagoner.com?subject={encodeURIComponent(`My seat in room ${net.roomId}`)}">write to the keeper</a> for a link that brings it back.</span>
|
||||
{/if}
|
||||
{/if}
|
||||
{#each view.phase === "finished" ? [] : orderedHand as card (card.instanceId)}
|
||||
<div class="hand-slot"
|
||||
@@ -4399,6 +4424,7 @@
|
||||
padding: 0 0.3rem;
|
||||
}
|
||||
.ledger-forget:hover { color: #b3372b; }
|
||||
.forget-ask { font-size: 0.8rem; color: #8d8672; flex: none; }
|
||||
|
||||
.stamp {
|
||||
font-family: "Oswald", sans-serif;
|
||||
|
||||
Reference in new issue
Block a user