CSS refactor: every color, font, radius, and shadow is now defined once in theme.css; all other styles derive translucent variants with color-mix(), so re-theming means editing one file. Removed the duplicate core.css/style.css import chains, dead styles, and the Bootstrap/Tailwind orphan classes that were silently unstyled (the main reason the splash page looked broken). New "Lantern & Brine" look: brass and parchment on a lantern-lit ink-navy night, Pirata One wordmark with Alegreya SC/Sans body fonts (self-hosted via fontsource — they were previously referenced but never loaded), parchment-faced playing cards, and a rebuilt splash page. Inline color styles in components were replaced with semantic classes (prompt-box, notice-banner, status chips, etc.), the rulebook's embedded palette was updated to match, and the leftover Vite favicon and "frontend" page title were replaced. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
154 lines
6.7 KiB
Svelte
154 lines
6.7 KiB
Svelte
<script>
|
|
import { apiRequest } from '../../lib/api';
|
|
import { displayName } from '../../lib/cards';
|
|
|
|
export let state;
|
|
|
|
let error = '';
|
|
let challengeTargetId = '';
|
|
let challengeStakes = '';
|
|
let selectedObstacles = {};
|
|
let captainSelectId = '';
|
|
|
|
$: openChallenges = (state.challenges || []).filter(c => c.status === 'open');
|
|
$: challengedObstacleIds = new Set(openChallenges.flatMap(c => JSON.parse(c.obstacle_ids || '[]')));
|
|
$: scenePirats = state.players.filter(p => p.role === 'pirat');
|
|
|
|
async function createChallenge() {
|
|
error = '';
|
|
const ids = Object.keys(selectedObstacles).filter(id => selectedObstacles[id]);
|
|
try {
|
|
await apiRequest(`/game/${state.game.id}/player/${state.player.id}/challenge/create`, 'POST', {
|
|
target_player_id: challengeTargetId,
|
|
obstacle_ids: JSON.stringify(ids),
|
|
stakes: challengeStakes
|
|
});
|
|
challengeTargetId = '';
|
|
challengeStakes = '';
|
|
selectedObstacles = {};
|
|
} catch(e) {
|
|
error = e.message;
|
|
}
|
|
}
|
|
|
|
async function setCaptain() {
|
|
error = '';
|
|
try {
|
|
await apiRequest(`/game/${state.game.id}/player/${state.player.id}/set-captain`, 'POST', {
|
|
target_player_id: captainSelectId
|
|
});
|
|
captainSelectId = '';
|
|
} catch(e) {
|
|
error = e.message;
|
|
}
|
|
}
|
|
|
|
async function toggleObjective(targetPlayerId, type) {
|
|
error = '';
|
|
try {
|
|
await apiRequest(`/game/${state.game.id}/player/${targetPlayerId}/objective/toggle`, 'POST', { type });
|
|
} catch(e) {
|
|
error = e.message;
|
|
}
|
|
}
|
|
|
|
async function endScene() {
|
|
error = '';
|
|
try {
|
|
await apiRequest(`/game/${state.game.id}/scene/end`, 'POST');
|
|
} catch(e) {
|
|
error = e.message;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div class="card glass-panel deep-panel-card">
|
|
<h3 class="deep-text text-center">🌊 Deep Control Panel</h3>
|
|
|
|
{#if error}
|
|
<div class="alert alert-danger">{error}</div>
|
|
{/if}
|
|
|
|
<!-- Call a Challenge -->
|
|
<div class="sheet-group margin-top">
|
|
<h4>⚔️ Call a Challenge</h4>
|
|
<p class="info-text" style="font-size: 0.85rem;">Establish the stakes, pick a Pi-Rat, and apply one or more Obstacles. They play one card per Obstacle — beating one passes; each failure breeds a complication.</p>
|
|
<select class="select-field" bind:value={challengeTargetId} style="width: 100%; margin-bottom: 0.5rem;">
|
|
<option value="">Challenge which Pi-Rat?</option>
|
|
{#each scenePirats.filter(p => !p.is_dead) as p}
|
|
<option value={p.id}>{displayName(p)} (Rank {p.rank})</option>
|
|
{/each}
|
|
</select>
|
|
<div style="margin-bottom: 0.5rem;">
|
|
{#each state.obstacles as obs}
|
|
{@const taken = challengedObstacleIds.has(obs.id)}
|
|
<label class="checkbox-label" style="{taken ? 'opacity: 0.5;' : ''}">
|
|
<input type="checkbox" bind:checked={selectedObstacles[obs.id]} disabled={taken}>
|
|
<span>{obs.title}{taken ? ' (in a challenge)' : ''}</span>
|
|
</label>
|
|
{/each}
|
|
</div>
|
|
<input type="text" class="input-field" placeholder="Stakes (optional): what happens on success/failure?" bind:value={challengeStakes} style="width: 100%; margin-bottom: 0.5rem;">
|
|
<button class="btn btn-primary btn-full" on:click={createChallenge}
|
|
disabled={!challengeTargetId || !Object.values(selectedObstacles).some(v => v)}>
|
|
Call Challenge
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Captain Assignment -->
|
|
<div class="sheet-group margin-top">
|
|
<h4>🏴☠️ Captaincy</h4>
|
|
<p class="info-text" style="font-size: 0.85rem;">The Captain holds power until they step down, lose a duel, die, or the ship is lost. Reassign after a leadership duel or resignation.</p>
|
|
<div style="display: flex; gap: 0.5rem;">
|
|
<select class="select-field" bind:value={captainSelectId} style="flex: 1;">
|
|
<option value="">No Captain</option>
|
|
{#each scenePirats.filter(p => !p.is_dead) as p}
|
|
<option value={p.id}>{displayName(p)}</option>
|
|
{/each}
|
|
</select>
|
|
<button class="btn btn-secondary" on:click={setCaptain}>Set</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="sheet-group margin-top">
|
|
<h4>Crew Objectives</h4>
|
|
<div class="objectives-checklist">
|
|
<label class="checkbox-label">
|
|
<input type="checkbox" checked={state.game.completed_crew_1} on:change={() => toggleObjective(state.player.id, 'crew_1')}>
|
|
<span>Steal a Ship</span>
|
|
</label>
|
|
<label class="checkbox-label">
|
|
<input type="checkbox" checked={state.game.completed_crew_2} on:change={() => toggleObjective(state.player.id, 'crew_2')}>
|
|
<span>Choose a Captain</span>
|
|
</label>
|
|
<label class="checkbox-label">
|
|
<input type="checkbox" checked={state.game.completed_crew_3} on:change={() => toggleObjective(state.player.id, 'crew_3')}>
|
|
<span>Commit Piracy</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="sheet-group margin-top">
|
|
<h4>Pi-Rat Personal Objectives</h4>
|
|
<p class="info-text">You control completion. Only mark in sequence (1 -> 2 -> 3).</p>
|
|
{#each scenePirats as p}
|
|
<div class="objective-player-block">
|
|
<strong>{displayName(p)}</strong>
|
|
<div class="objectives-checklist" style="margin-top: 0.25rem;">
|
|
<label class="checkbox-label"><input type="checkbox" checked={p.completed_personal_1} on:change={() => toggleObjective(p.id, 'personal_1')}> <span>1. Gat</span></label>
|
|
<label class="checkbox-label"><input type="checkbox" checked={p.completed_personal_2} on:change={() => toggleObjective(p.id, 'personal_2')}> <span>2. Name</span></label>
|
|
<label class="checkbox-label"><input type="checkbox" checked={p.completed_personal_3} on:change={() => toggleObjective(p.id, 'personal_3')}> <span>3. Die/Retire</span></label>
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
|
|
<!-- End Scene Button -->
|
|
<div class="scene-upkeep-controls text-center">
|
|
<p class="info-text">End the scene once every Pi-Rat has faced a Challenge and had a chance at an Objective, or the Obstacle List is empty.</p>
|
|
<button on:click={endScene} class="btn btn-danger btn-large btn-full glow-effect">
|
|
End Scene & Proceed to Ranking
|
|
</button>
|
|
</div>
|
|
</div>
|