Award personal objectives through group votes
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<script>
|
||||
import Card from './Card.svelte';
|
||||
import ObjectiveVotes from './scene/ObjectiveVotes.svelte';
|
||||
import ChallengePanel from './scene/ChallengePanel.svelte';
|
||||
import ObstacleBoard from './scene/ObstacleBoard.svelte';
|
||||
import CrewColumn from './scene/CrewColumn.svelte';
|
||||
@@ -25,6 +26,7 @@
|
||||
|
||||
<!-- MIDDLE COLUMN: Your hand, the Challenge, the Obstacle list -->
|
||||
<div class="table-column">
|
||||
<ObjectiveVotes {state} />
|
||||
{#if !isDeep}
|
||||
<div class="card glass-panel hand-card">
|
||||
<span class="hand-label">🃏 Your Hand</span>
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
$: isSelf = target.id === viewer.id;
|
||||
$: isDeep = viewer.role === 'deep';
|
||||
$: isCaptain = target.id === state.game.captain_player_id;
|
||||
// The Deep manages captaincy and ticks personal objectives from a Pi-Rat's sheet
|
||||
// (both moved here from the old Deep Control Panel).
|
||||
// Captaincy is managed by the Deep; objectives go to a group vote.
|
||||
$: canManageCaptain = isDeep && target.role === 'pirat' && !target.is_dead;
|
||||
$: canManageObjectives = isDeep && target.role === 'pirat';
|
||||
$: canManageObjectives = !viewer.needs_reroll && target.role === 'pirat' && !target.is_dead && !target.needs_reroll;
|
||||
$: pendingObjective = JSON.parse(state.game.objective_votes || '[]').some(p => p.target_id === target.id);
|
||||
// The viewer throws one of their OWN cards in a duel.
|
||||
$: hand = viewer.hand_cards ? JSON.parse(viewer.hand_cards) : [];
|
||||
$: canDuel = !isSelf
|
||||
@@ -59,10 +59,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleObjective(type) {
|
||||
async function proposeObjective(type) {
|
||||
error = '';
|
||||
try {
|
||||
await apiRequest(`/game/${state.game.id}/player/${target.id}/objective/toggle`, 'POST', { type });
|
||||
await apiRequest(`/game/${state.game.id}/player/${viewer.id}/objective/propose`, 'POST', { type, target_id: target.id });
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
}
|
||||
@@ -190,21 +190,21 @@
|
||||
<div class="sheet-group">
|
||||
<h4>Personal Objectives</h4>
|
||||
{#if canManageObjectives}
|
||||
<p class="info-text" style="font-size: 0.8rem;">As the Deep, tick these off in sequence (1 → 2 → 3).</p>
|
||||
<p class="info-text" style="font-size: 0.8rem;">Propose the next objective for a group vote. Your proposal counts as a Yes vote.</p>
|
||||
{/if}
|
||||
<div class="objectives-checklist">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" checked={target.completed_personal_1} disabled={!canManageObjectives} on:change={() => toggleObjective('personal_1')}>
|
||||
<span>1. Gat</span>
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" checked={target.completed_personal_2} disabled={!canManageObjectives} on:change={() => toggleObjective('personal_2')}>
|
||||
<span>2. Name</span>
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" checked={target.completed_personal_3} disabled={!canManageObjectives} on:change={() => toggleObjective('personal_3')}>
|
||||
<span>3. Die/Retire</span>
|
||||
</label>
|
||||
{#each ['Gat', 'Name', 'Die/Retire'] as label, i}
|
||||
{@const type = `personal_${i + 1}`}
|
||||
{@const completed = target[`completed_${type}`]}
|
||||
<div>
|
||||
<span>{completed ? '✓' : '○'} {i + 1}. {label}</span>
|
||||
{#if canManageObjectives && !completed && (i === 0 || target[`completed_personal_${i}`])}
|
||||
<button class="btn btn-secondary" disabled={pendingObjective} on:click={() => proposeObjective(type)}>
|
||||
{pendingObjective ? 'Vote pending' : 'Propose'}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- end sheet-side-column -->
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<script>
|
||||
import { apiRequest } from '../../lib/api';
|
||||
export let state;
|
||||
let error = '';
|
||||
$: proposals = JSON.parse(state.game.objective_votes || '[]');
|
||||
const labels = { personal_1: 'Gat', personal_2: 'Name', personal_3: 'Die/Retire' };
|
||||
async function vote(proposal_id, approve) {
|
||||
error = '';
|
||||
try {
|
||||
await apiRequest(`/game/${state.game.id}/player/${state.player.id}/objective/vote`, 'POST', { proposal_id, approve });
|
||||
} catch (e) { error = e.message; }
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if error}<div class="alert alert-danger">{error}</div>{/if}
|
||||
{#each proposals as proposal (proposal.id)}
|
||||
<div class="card glass-panel">
|
||||
<h3>Objective Vote: {state.players.find(p => p.id === proposal.target_id)?.name} — {labels[proposal.type]}</h3>
|
||||
<p class="info-text">Majority decides. The Captain breaks ties; a tie without a Captain does not award the objective. Votes close when decided or the scene ends.</p>
|
||||
<p>{Object.values(proposal.ballots).filter(Boolean).length} Yes · {Object.values(proposal.ballots).filter(v => !v).length} No · {state.players.filter(p => !p.needs_reroll && !(p.id in proposal.ballots)).length} waiting</p>
|
||||
{#if !state.player.needs_reroll}
|
||||
<div class="challenge-actions">
|
||||
<button class="btn btn-primary" aria-pressed={proposal.ballots[state.player.id] === true} disabled={proposal.ballots[state.player.id] === true} on:click={() => vote(proposal.id, true)}>Yes{proposal.ballots[state.player.id] === true ? ' ✓' : ''}</button>
|
||||
<button class="btn btn-secondary" aria-pressed={proposal.ballots[state.player.id] === false} disabled={proposal.ballots[state.player.id] === false} on:click={() => vote(proposal.id, false)}>No{proposal.ballots[state.player.id] === false ? ' ✓' : ''}</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
@@ -6,10 +6,11 @@
|
||||
// - Add a CHANGELOG entry only when a commit changes something players can
|
||||
// see. Skip refactors, tests, and tooling. Keep wording player-facing.
|
||||
|
||||
export const VERSION = 34;
|
||||
export const VERSION = 35;
|
||||
|
||||
// Newest first. Each entry: { version, date: 'YYYY-MM-DD', changes: [string, ...] }.
|
||||
export const CHANGELOG = [
|
||||
{ version: 35, date: '2026-09-04', changes: ['Personal objectives now require a group vote. Propose the next objective from a character sheet, then vote Yes or No at the table. A majority approves; the Captain breaks tied votes.'] },
|
||||
{ version: 34, date: '2026-09-04', changes: ['Assistance is available only when a Challenge has multiple active Obstacles. Gat and Name Tax takeovers still work against a single Obstacle.'] },
|
||||
{ version: 33, date: '2026-09-04', changes: ['Hand sizes compare ranks across the crew, including Pi-Rats whose players are the Deep.', 'Discarded cards return to the deck at scene setup. Only the previous Deep can refresh their hand during upkeep, once per scene.'] },
|
||||
{ version: 32, date: '2026-09-04', changes: ['Obstacles are automatically discarded with their columns once they reach one success per player. Unfinished obstacles carry over between scenes.'] },
|
||||
|
||||
Reference in New Issue
Block a user