Refresh the scene dashboard around a crew bubble column

Replace the roster banner and the standalone character-sheet panel with
a left-hand column of crew bubbles. Each bubble shows the player's role
icon, name, rank, current hand size, and a 🛞 for the Captain; clicking
one pops out that player's character sheet as a modal.

- New CrewColumn.svelte: bubbles (Pi-Rats first, Deep last) + an
  Objectives button that opens a crew-objectives popup.
- CharacterSheet.svelte is now a target-driven modal: description,
  likes/hates, and personal objectives for anyone; secret J/Q/K only on
  your own sheet; the Duel UI (no "Challenge whom?" — target is fixed)
  only when a living Pi-Rat views another living Pi-Rat.
- ScenePhase.svelte → 3-column layout (crew | obstacles | hand/deep);
  crew column collapses to a horizontal strip on narrow screens.
- Shared .modal-backdrop/.modal-box/.modal-close moved into components.css;
  removed the now-dead roster-banner and captain-badge styles.

Backend unchanged — the state blob already carries hands, roles, ranks,
objectives, and the captain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-14 20:03:41 -07:00
parent 8614a6c820
commit 2ea91c066a
6 changed files with 367 additions and 236 deletions

View File

@@ -0,0 +1,81 @@
<script>
import CharacterSheet from './CharacterSheet.svelte';
export let state;
let openTargetId = null;
let showObjectives = false;
// Pi-Rats first, the Deep last; otherwise keep join order.
$: crew = [...state.players].sort((a, b) => (a.role === 'deep' ? 1 : 0) - (b.role === 'deep' ? 1 : 0));
$: openTarget = openTargetId ? state.players.find(p => p.id === openTargetId) : null;
function handSize(p) {
try { return JSON.parse(p.hand_cards || '[]').length; } catch { return 0; }
}
function roleIcon(p) {
if (p.role === 'deep') return '🌊';
if (p.is_ghost) return '👻';
if (p.is_dead) return '💀';
return '🐀';
}
function onKeydown(e) {
// CharacterSheet handles its own Escape; this closes the objectives popup.
if (e.key === 'Escape') showObjectives = false;
}
</script>
<svelte:window on:keydown={onKeydown} />
<div class="crew-column">
<button class="crew-objectives-btn" on:click={() => showObjectives = true}>🎯 Objectives</button>
<div class="crew-bubbles">
{#each crew as p (p.id)}
<button
class="crew-bubble"
class:is-you={p.id === state.player.id}
class:is-deep={p.role === 'deep'}
class:is-dead={p.is_dead}
class:is-ghost={p.is_ghost}
title="View {p.name}'s character sheet"
on:click={() => openTargetId = p.id}
>
<span class="bubble-icon">{roleIcon(p)}</span>
<span class="bubble-name">{p.name}{#if p.id === state.game.captain_player_id}&nbsp;🛞{/if}</span>
<span class="bubble-meta">
{#if p.role === 'deep'}Deep{:else}Rank {p.rank}{/if} · 🃏 {handSize(p)}
</span>
</button>
{/each}
</div>
</div>
{#if openTarget}
<CharacterSheet {state} target={openTarget} on:close={() => openTargetId = null} />
{/if}
{#if showObjectives}
<div class="modal-backdrop" on:click|self={() => showObjectives = false}>
<div class="modal-box glass-panel" style="text-align: left;">
<button class="modal-close" on:click={() => showObjectives = false} aria-label="Close">×</button>
<h3>🎯 Crew Objectives</h3>
<p class="info-text" style="font-size: 0.85rem;">The whole crew works toward these. The Deep marks them off as you accomplish them.</p>
<div class="objectives-checklist">
<label class="checkbox-label">
<input type="checkbox" checked={state.game.completed_crew_1} disabled>
<span>Steal a Ship</span>
</label>
<label class="checkbox-label">
<input type="checkbox" checked={state.game.completed_crew_2} disabled>
<span>Choose a Captain</span>
</label>
<label class="checkbox-label">
<input type="checkbox" checked={state.game.completed_crew_3} disabled>
<span>Commit Piracy</span>
</label>
</div>
</div>
</div>
{/if}