A crowded table: keyed threats, a compact roster, a ledger selector, host-only starts
Threats are keyed by seat as well as hand and spell, so two opponents building the same spell no longer crash the board (regression test). Past three wizards the rail lists the others on a line each with health, condition and whether they have moved; a row of names turns the ledger toward any wizard, as do pressing a warning or choosing a target; each seat keeps one colour across the ledger, the warnings and the rail. The result strip folds untouched wizards into one line. A full table no longer begins by itself: the host begins at every size and may send a bot away. Target lists carry health and condition, and an intent sentence says what the reveal will do. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141G6xqLeNRYEtviLWSB5Up
This commit is contained in:
1 parent
c24fccaa35
commit
5c79a65319
19 files changed
+473
-60
No files matched your search
@@ -0,0 +1,145 @@
|
||||
<script lang="ts">
|
||||
// A crowded table's rail: every other wizard on one line, with the one
|
||||
// you are looking at opened out. Your own panel stays in full above.
|
||||
import WizardStatus from './WizardStatus.svelte';
|
||||
import type { Duel } from '$lib/game/duel.svelte';
|
||||
import { seatColor } from '$lib/game/colors';
|
||||
import { MAX_HP, type WizardId } from '$lib/game/state';
|
||||
import { statusWords } from '$lib/game/status';
|
||||
|
||||
let { duel }: { duel: Duel } = $props();
|
||||
|
||||
const waitingOn = $derived(duel.remote?.waitingOn ?? []);
|
||||
|
||||
function toggle(id: WizardId) {
|
||||
if (duel.focus === id) duel.focus = null;
|
||||
else duel.focusOn(id);
|
||||
}
|
||||
|
||||
function stateOf(id: WizardId): string {
|
||||
if (!duel.remote?.started || duel.state.over) return '';
|
||||
const w = duel.state.wizards[id];
|
||||
if (!w.alive || w.surrendered) return 'out';
|
||||
const bot = duel.remote.seats.find((s) => s.id === id)?.bot;
|
||||
if (bot) return '';
|
||||
return waitingOn.includes(id) ? 'writing' : 'moved';
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="roster" aria-label="The other wizards">
|
||||
<h2>At the table</h2>
|
||||
<ul>
|
||||
{#each duel.others as id (id)}
|
||||
{@const w = duel.state.wizards[id]}
|
||||
{@const open = duel.focus === id}
|
||||
{@const words = statusWords(w)}
|
||||
<li class:open class:gone={!w.alive || w.surrendered}>
|
||||
<button type="button" class="row" aria-expanded={open} onclick={() => toggle(id)}>
|
||||
<span class="dot" style="background: {seatColor(duel.state, id)}"></span>
|
||||
<span class="name">{w.name}</span>
|
||||
<span class="hp">{w.hp}/{MAX_HP}</span>
|
||||
<span class="words">{words.join(', ')}</span>
|
||||
<span class="turn muted">{stateOf(id)}</span>
|
||||
</button>
|
||||
{#if open}
|
||||
<div class="detail">
|
||||
<WizardStatus wizard={w} monsters={duel.state.monsters.filter((m) => m.owner === id)} />
|
||||
</div>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.roster {
|
||||
padding: 0.75rem 0 0.4rem;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.1rem;
|
||||
font-style: italic;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
border-top: 1px solid var(--rule);
|
||||
}
|
||||
|
||||
li.gone .name {
|
||||
text-decoration: line-through;
|
||||
color: var(--bone-dim);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto minmax(0, 1.4fr) auto;
|
||||
align-items: baseline;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
padding: 0.35rem 0.2rem;
|
||||
background: none;
|
||||
border: 0;
|
||||
color: var(--bone);
|
||||
text-align: left;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.row:hover,
|
||||
li.open .row {
|
||||
background: var(--slate);
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 0.55rem;
|
||||
height: 0.55rem;
|
||||
border-radius: 50%;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-style: italic;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.hp {
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--bone-dim);
|
||||
}
|
||||
|
||||
.words {
|
||||
color: var(--frost);
|
||||
font-size: 0.82rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.turn {
|
||||
font-size: 0.78rem;
|
||||
font-style: italic;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.detail {
|
||||
padding: 0 0.2rem 0.3rem;
|
||||
}
|
||||
|
||||
.detail :global(.wizard) {
|
||||
border-bottom: 0;
|
||||
padding-top: 0.2rem;
|
||||
}
|
||||
|
||||
.detail :global(h2) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user