Streamline dead Pi-Rat rank voting

Dead and awaiting-recruit Pi-Rats can no longer be nominated to rank up
(they'd reset to Rank 1 anyway). submit_rank_vote rejects them, and the
upkeep dropdown only lists living, in-play crewmates via the shared
eligible_vote_nominees helper.

When a voter has no eligible nominees (e.g. a lone survivor whose only
crewmates are the previous Deep and a dead rat) they now skip voting
entirely and can ready up directly, instead of deadlocking — which is
why dead players were previously left nominable. The roster shows such
players as "No vote".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 21:21:38 -07:00
parent 345ef4088f
commit d04e5013fa
4 changed files with 78 additions and 15 deletions

View File

@@ -14,6 +14,19 @@
// Helpers
$: hand = state.player.hand_cards ? JSON.parse(state.player.hand_cards) : [];
// A nominee must be another living, in-play Pi-Rat: not the voter, not a
// previous-scene Deep, and not dead / awaiting-recruit (they'd reset to
// Rank 1 anyway). Mirrors crud_upkeep.is_eligible_nominee.
function eligibleNomineesFor(voter) {
return state.players.filter(p =>
p.id !== voter.id && p.role !== 'deep' && !p.is_dead && !p.needs_reroll
);
}
$: myNominees = eligibleNomineesFor(state.player);
$: hasVoted = !!state.votes.find(v => v.voter_player_id === state.player.id);
// With no eligible crewmates, the voter skips voting entirely (no deadlock).
$: mustVote = myNominees.length > 0;
// Drag & Drop State
let keepCards = [];
let discardCards = [];
@@ -191,19 +204,21 @@
<div class="voted-confirmation-box glass-panel">
<p class="success-text">✔️ Voting complete! Results tallied.</p>
</div>
{:else if state.votes.find(v => v.voter_player_id === state.player.id)}
{:else if hasVoted}
<div class="voted-confirmation-box glass-panel">
<p class="success-text">✔️ Vote submitted! Waiting for other players to submit their nominations.</p>
</div>
{:else if !mustVote}
<div class="voted-confirmation-box glass-panel">
<p class="info-text">No living crewmates left to nominate — you skip voting this round. Go ahead and ready up.</p>
</div>
{:else}
<form on:submit|preventDefault={submitVote} class="vote-form inline-form">
<div class="form-group inline-group">
<select class="select-field" bind:value={selectedVoteId} required>
<option value="">Nominate crewmate...</option>
{#each state.players as p}
{#if p.id !== state.player.id && p.role !== 'deep'}
<option value={p.id}>{displayName(p)} (Rank {p.rank})</option>
{/if}
{#each myNominees as p}
<option value={p.id}>{displayName(p)} (Rank {p.rank})</option>
{/each}
</select>
<button type="submit" class="btn btn-primary" disabled={voting || !selectedVoteId}>Cast Nomination</button>
@@ -218,9 +233,10 @@
<div class="player-chips list-chips" id="voting-roster">
{#each state.players as p}
{@const voted = state.votes.some(v => v.voter_player_id === p.id)}
<div class="player-chip {voted ? 'voted-chip' : 'not-voted-chip'}">
{@const skips = eligibleNomineesFor(p).length === 0}
<div class="player-chip {voted || skips ? 'voted-chip' : 'not-voted-chip'}">
<span class="name">{displayName(p)}</span>
<span class="status-badge">{voted ? 'Done' : 'Voting...'}</span>
<span class="status-badge">{voted ? 'Done' : skips ? 'No vote' : 'Voting...'}</span>
</div>
{/each}
</div>
@@ -325,7 +341,7 @@
</div>
{/if}
{:else}
{#if !state.votes.find(v => v.voter_player_id === state.player.id)}
{#if mustVote && !hasVoted}
<p class="info-text gold-text" style="margin-bottom:0.5rem;">⚠️ You must nominate a crewmate above before you can ready up.</p>
<button class="btn btn-primary btn-large" disabled>Ready for Next Scene</button>
{:else}