107 lines
4.8 KiB
Svelte
107 lines
4.8 KiB
Svelte
<script>
|
|
import PlayerBadges from './PlayerBadges.svelte';
|
|
import { displayName, crewLabel } from '../lib/cards';
|
|
import CharacterSheet from './scene/CharacterSheet.svelte';
|
|
|
|
export let state;
|
|
|
|
let openTargetId = null;
|
|
|
|
$: captainId = state.game.captain_player_id;
|
|
$: 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 iconFor(p) {
|
|
if (p.role === 'deep') return '🌊';
|
|
if (p.is_ghost) return '👻';
|
|
if (p.is_dead) return '💀';
|
|
return '🐀';
|
|
}
|
|
|
|
function safeArray(value) {
|
|
try { return JSON.parse(value || '[]'); } catch { return []; }
|
|
}
|
|
|
|
function voteStatus(p) {
|
|
if ((state.votes || []).some(v => v.voter_player_id === p.id)) return { icon: '✓', label: 'Voted', done: true };
|
|
const canVote = state.players.some(candidate => candidate.id !== p.id && candidate.role !== 'deep' && !candidate.is_dead && !candidate.needs_reroll);
|
|
return canVote ? { icon: '○', label: 'Choosing a nominee', done: false } : { icon: '—', label: 'No eligible nominee', done: true };
|
|
}
|
|
|
|
function statusFor(p) {
|
|
const phase = state.game.phase;
|
|
if (phase === 'lobby') {
|
|
if (p.is_creator) return 'Creator';
|
|
if (p.is_admin) return 'Admin';
|
|
return 'Crew member';
|
|
}
|
|
if (phase === 'character_creation') {
|
|
if (p.needs_reroll) return 'Spectating';
|
|
return safeArray(p.created_techniques).length === 3 ? 'Techniques submitted' : 'Writing sheet';
|
|
}
|
|
if (phase === 'swap_techniques') {
|
|
if (p.is_ready) return 'Done swapping';
|
|
const ownLeft = safeArray(p.held_techniques).filter(t => t.creator_id === p.id).length;
|
|
return `Swapping · ${ownLeft} own left`;
|
|
}
|
|
if (phase === 'assign_techniques') return p.tech_jack ? 'Ready' : 'Assigning face cards';
|
|
if (phase === 'scene_setup') {
|
|
if (p.needs_reroll) return 'Spectating';
|
|
if (p.role === 'deep') return 'The Deep';
|
|
if (p.role === 'pirat') return `Pi-Rat · Rank ${p.rank}`;
|
|
return 'Choosing a role…';
|
|
}
|
|
if (phase === 'between_scenes') return `Rank ${p.rank}`;
|
|
if (phase === 'deep_upkeep') return `Rank ${p.rank} · ${p.is_ready ? 'Ready' : 'Finishing upkeep…'}`;
|
|
if (phase === 'recruit_creation') return p.needs_reroll ? 'Creating a new Pi-Rat' : `Rank ${p.rank} · Helping recruits`;
|
|
if (phase === 'ended') {
|
|
const story = p.completed_personal_3 ? 'Story complete' : 'Still sailing';
|
|
return `Rank ${p.rank} · ${story}`;
|
|
}
|
|
return `Rank ${p.rank}`;
|
|
}
|
|
</script>
|
|
|
|
<aside class="crew-column phase-crew-column" aria-label="Crew roster">
|
|
<div class="crew-bubbles">
|
|
{#each crew as p (p.id)}
|
|
{#if state.game.phase === 'lobby'}
|
|
<div
|
|
class="crew-bubble is-static"
|
|
class:is-you={p.id === state.player.id}
|
|
>
|
|
<span class="bubble-icon">{iconFor(p)}</span>
|
|
<span class="bubble-name">{displayName(p)} <PlayerBadges player={p} {state} /></span>
|
|
<span class="bubble-meta">{statusFor(p)}</span>
|
|
</div>
|
|
{:else}
|
|
<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}
|
|
>
|
|
{#if state.game.phase === 'between_scenes'}
|
|
<span class="vote-status" class:done={voteStatus(p).done} title={voteStatus(p).label} aria-label={voteStatus(p).label}>{voteStatus(p).icon}</span>
|
|
{/if}
|
|
<span class="bubble-icon">{iconFor(p)}</span>
|
|
<span class="bubble-name">{crewLabel(p, captainId)} <PlayerBadges player={p} {state} /></span>
|
|
<span class="bubble-meta">{statusFor(p)}</span>
|
|
</button>
|
|
{/if}
|
|
{/each}
|
|
</div>
|
|
</aside>
|
|
|
|
{#if openTarget}
|
|
<CharacterSheet {state} target={openTarget} on:close={() => openTargetId = null} />
|
|
{/if}
|
|
|
|
<style>
|
|
.vote-status { position: absolute; top: 8px; right: 10px; display: grid; place-items: center; width: 1.5rem; height: 1.5rem; border: 1px solid currentColor; border-radius: 50%; color: var(--text-muted); font-size: 1rem; }
|
|
.vote-status.done { color: var(--success); }
|
|
</style>
|