Enforced server-side in lobby/start and confirm_scene_setup via crud_base.has_min_players; Dev Mode bypasses it so local single-player testing still works. Lobby and scene-setup UIs show a player-count message instead of the start button when below the minimum. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
154 lines
6.7 KiB
Svelte
154 lines
6.7 KiB
Svelte
<script>
|
|
import { onMount } from 'svelte';
|
|
import { apiRequest } from '../lib/api';
|
|
import { displayName } from '../lib/cards';
|
|
|
|
export let state;
|
|
|
|
let settingRole = false;
|
|
let starting = false;
|
|
let error = '';
|
|
let allowedRoles = ['pirat', 'deep'];
|
|
|
|
onMount(async () => {
|
|
try {
|
|
const res = await apiRequest(`/game/${state.game.id}/player/${state.player.id}/allowed-roles`, 'GET');
|
|
allowedRoles = res.allowed_roles;
|
|
} catch(e) {
|
|
console.error("Failed to load allowed roles", e);
|
|
}
|
|
});
|
|
|
|
async function setRole(role) {
|
|
settingRole = true;
|
|
try {
|
|
await apiRequest(`/game/${state.game.id}/player/${state.player.id}/set-role`, 'POST', { role });
|
|
} catch(e) {
|
|
console.error(e);
|
|
} finally {
|
|
settingRole = false;
|
|
}
|
|
}
|
|
|
|
async function startScene() {
|
|
starting = true;
|
|
error = '';
|
|
try {
|
|
await apiRequest(`/game/${state.game.id}/scene/start`, 'POST');
|
|
} catch(e) {
|
|
error = e.message;
|
|
starting = false;
|
|
}
|
|
}
|
|
|
|
$: allRolesAssigned = state.players.every(p => p.role !== null || p.needs_reroll);
|
|
$: deepPlayer = state.players.find(p => p.role === 'deep');
|
|
$: piratPlayer = state.players.find(p => p.role === 'pirat');
|
|
$: enoughPlayers = state.players.length >= 3 || state.game.dev_mode;
|
|
$: canStart = enoughPlayers && allRolesAssigned && deepPlayer && piratPlayer && (state.player.is_admin || state.player.role === 'deep');
|
|
</script>
|
|
|
|
<div class="scene-setup-view text-center">
|
|
<h2>Scene Setup (Scene #{state.game.current_scene_number})</h2>
|
|
<p class="description">Select your role for the upcoming scene. There must be at least one Pi-Rat and one Deep player. If you played the Deep in the last scene, you must play a Pi-Rat this scene!</p>
|
|
|
|
{#if state.game.current_scene_number === 1}
|
|
<div class="notice-banner accent">
|
|
<h4>🚢 First Scene Framing</h4>
|
|
<p class="info-text" style="margin: 0; font-size: 0.95rem;">
|
|
The first scene must establish that the Pi-Rats are <strong>on a ship they can steal</strong> (or have an immediate opportunity to steal one). <strong>Don't start far from water!</strong>
|
|
{#if state.player.role === 'deep'}
|
|
<br/><span class="text-accent">You're playing the Deep — once the scene starts, describe where the crew is and what ship is ripe for the taking before calling any Challenges.</span>
|
|
{/if}
|
|
</p>
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="setup-grid">
|
|
<!-- Role Selection Card -->
|
|
<div class="card glass-panel role-selection-card">
|
|
<h3>Select Your Role</h3>
|
|
|
|
{#if state.player.needs_reroll}
|
|
<div class="notice-banner accent">
|
|
<p class="info-text" style="margin: 0;">🐀 You don't have a Pi-Rat yet! You'll spectate this scene and create your recruit with the crew between scenes.</p>
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="role-buttons">
|
|
<button on:click={() => setRole('pirat')}
|
|
disabled={settingRole || !allowedRoles.includes('pirat')}
|
|
class="btn btn-large role-btn pirat-btn {state.player.role === 'pirat' ? 'active' : ''}">
|
|
🐀 Play Pi-Rat
|
|
</button>
|
|
|
|
<button on:click={() => setRole('deep')}
|
|
disabled={settingRole || !allowedRoles.includes('deep')}
|
|
class="btn btn-large role-btn deep-btn {state.player.role === 'deep' ? 'active' : ''}">
|
|
🌊 Play the Deep
|
|
</button>
|
|
</div>
|
|
|
|
<div class="role-restrictions mt-4">
|
|
{#if !allowedRoles.includes('pirat') && !state.player.needs_reroll}
|
|
{#if state.game.current_scene_number === 1}
|
|
<p class="text-danger text-sm italic">You start at Rank 3, so you must play The Deep for the first scene!</p>
|
|
{:else}
|
|
<p class="text-danger text-sm italic">You are the only eligible player for The Deep this scene!</p>
|
|
{/if}
|
|
{/if}
|
|
{#if !allowedRoles.includes('deep') && !state.player.needs_reroll}
|
|
{#if state.game.current_scene_number === 1}
|
|
<p class="text-danger text-sm italic">You start at Rank 1, so you must play your Pi-Rat for the first scene!</p>
|
|
{:else}
|
|
<p class="text-danger text-sm italic">You played The Deep last scene, so you must play a Pi-Rat!</p>
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Live Roster Card -->
|
|
<div class="card glass-panel roster-card">
|
|
<h3>Live Roster Selection</h3>
|
|
<div class="roster-list" id="scene-roster-list">
|
|
{#each state.players as p}
|
|
<div class="roster-item">
|
|
<span class="player-name">{displayName(p)} <span class="text-sm text-muted">(Rank {p.rank})</span></span>
|
|
{#if p.role === 'pirat'}
|
|
<span class="role-badge pirat">Pi-Rat</span>
|
|
{:else if p.role === 'deep'}
|
|
<span class="role-badge deep">The Deep</span>
|
|
{:else}
|
|
<span class="role-badge unassigned">Choosing...</span>
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="action-box margin-top">
|
|
{#if error}
|
|
<div class="alert alert-danger">{error}</div>
|
|
{/if}
|
|
|
|
{#if canStart}
|
|
<button on:click={startScene}
|
|
disabled={starting}
|
|
class="btn btn-primary btn-large glow-effect">
|
|
{starting ? 'Starting Scene...' : 'Confirm Roles & Shuffle Deck'}
|
|
</button>
|
|
{:else if !enoughPlayers}
|
|
<p class="text-danger italic mt-4">You need at least 3 players to start a scene — share the join link!</p>
|
|
{:else if !allRolesAssigned}
|
|
<p class="text-muted italic mt-4">Waiting for all players to choose a role...</p>
|
|
{:else if !deepPlayer}
|
|
<p class="text-danger italic mt-4">Someone must play as The Deep!</p>
|
|
{:else if !piratPlayer}
|
|
<p class="text-danger italic mt-4">Someone must play as a Pi-Rat!</p>
|
|
{:else}
|
|
<p class="text-muted italic mt-4">Waiting for The Deep or Creator to start the scene...</p>
|
|
{/if}
|
|
</div>
|
|
</div>
|