Fable will fix it!

This commit is contained in:
2026-06-11 15:57:08 -07:00
parent 2af98a3eeb
commit 443acd8400
23 changed files with 2151 additions and 557 deletions

View File

@@ -36,6 +36,15 @@
}
}
async function finishGame() {
if (!confirm('End the story for everyone? Best saved for when every player has played the Deep, each Pi-Rat has completed a Personal Objective, and all 3 Crew Objectives are done.')) return;
try {
await apiRequest(`/game/${state.game.id}/finish`, 'POST');
} catch(e) {
console.error(e);
}
}
async function submitReRoll() {
if (!reRollDetails.avatar_look.trim() ||
!reRollDetails.avatar_smell.trim() ||
@@ -115,7 +124,16 @@
}
}
$: maxHandSize = state.player.rank + 1;
// Mirrors the backend hand size table: highest rank -> 4, lowest -> 2, middle -> 3, Captain +1.
// Deep players are ranked against all players.
$: maxHandSize = (() => {
const ranks = state.players.map(p => p.rank);
let base;
if (state.player.rank >= Math.max(...ranks)) base = 4;
else if (state.player.rank <= Math.min(...ranks)) base = 2;
else base = 3;
return base + (state.game.captain_player_id === state.player.id ? 1 : 0);
})();
$: isOverLimit = keepCards.length > maxHandSize;
async function submitVote() {
@@ -180,7 +198,7 @@
</button>
</div>
<p class="info-text" style="margin-top: 1.5rem; font-style: italic; color: var(--text-muted);">
Ghosts look back into the living world, watch over friends, and help their crew. New recruits start fresh with a randomized starting Rank between 1 and 3.
Ghosts look back into the living world, watch over friends, and help their crew. New recruits start fresh at Rank 1 with surprise Secret Techniques and a name based on their smell.
</p>
{:else}
<div class="creation-form-wrapper" style="text-align: left; background: rgba(0,0,0,0.2); padding: 1.5rem; border-radius: 8px; border: 1px solid rgba(255,255,255,0.05);">
@@ -234,8 +252,8 @@
<!-- Ranks and Voting Card -->
<div class="card glass-panel voting-card">
<h3>1. Pirate Ranking (Voting)</h3>
<p class="section-desc">Nominate one crewmate who best exemplified pirate qualities in the previous scene. Previous Deep players are ineligible to receive votes.</p>
<p class="section-desc">Every player (including the Deep) nominates one crewmate who best exemplified pirate qualities in the previous scene. Previous Deep players are ineligible to receive votes.</p>
{#if state.game.phase === 'deep_upkeep'}
<div class="voted-confirmation-box glass-panel">
<p class="success-text">✔️ Voting complete! Results tallied.</p>
@@ -244,10 +262,6 @@
<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 state.player.role === 'deep'}
<div class="voted-confirmation-box glass-panel">
<p class="info-text">The Deep does not vote.</p>
</div>
{:else}
<form on:submit|preventDefault={submitVote} class="vote-form inline-form">
<div class="form-group inline-group">
@@ -270,7 +284,7 @@
<h4>Nomination Progress:</h4>
<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) || p.role === 'deep'}
{@const voted = state.votes.some(v => v.voter_player_id === p.id)}
<div class="player-chip {voted ? 'voted-chip' : 'not-voted-chip'}">
<span class="name">{p.name}</span>
<span class="status-badge">{voted ? 'Done' : 'Voting...'}</span>
@@ -305,26 +319,26 @@
<h4 class="gold-text">Hand (Keep)</h4>
<div class="upkeep-flex">
{#each keepCards as card}
<div class="card-large suit-{card.slice(-1).toLowerCase()} {card === 'JOKER' ? 'joker-card' : ''}"
<div class="card-large suit-{card.slice(-1).toLowerCase()} {card.startsWith('Joker') ? 'joker-card' : ''}"
draggable="true"
on:dragstart={(e) => handleDragStart(e, card)}
on:click={() => moveToDiscard(card)}
style="cursor: pointer;"
title="Click or drag to discard">
<div class="card-corner top-left">
<span class="val">{card === 'JOKER' ? '🃏' : card.slice(0, -1)}</span>
<span class="suit">{card === 'JOKER' ? '' : {'C': '♣️', 'S': '♠️', 'H': '♥️', 'D': '♦️'}[card.slice(-1)]}</span>
<span class="val">{card.startsWith('Joker') ? '🃏' : card.slice(0, -1)}</span>
<span class="suit">{card.startsWith('Joker') ? '' : {'C': '♣️', 'S': '♠️', 'H': '♥️', 'D': '♦️'}[card.slice(-1)]}</span>
</div>
<div class="card-center">
{#if card === 'JOKER'}
{#if card.startsWith('Joker')}
🃏
{:else}
<span class="giant-symbol">{{'C': '♣️', 'S': '♠️', 'H': '♥️', 'D': '♦️'}[card.slice(-1)]}</span>
{/if}
</div>
<div class="card-corner bottom-right">
<span class="val">{card === 'JOKER' ? '🃏' : card.slice(0, -1)}</span>
<span class="suit">{card === 'JOKER' ? '' : {'C': '♣️', 'S': '♠️', 'H': '♥️', 'D': '♦️'}[card.slice(-1)]}</span>
<span class="val">{card.startsWith('Joker') ? '🃏' : card.slice(0, -1)}</span>
<span class="suit">{card.startsWith('Joker') ? '' : {'C': '♣️', 'S': '♠️', 'H': '♥️', 'D': '♦️'}[card.slice(-1)]}</span>
</div>
</div>
{:else}
@@ -340,26 +354,26 @@
<h4 class="gold-text">Discard Pile</h4>
<div class="upkeep-flex">
{#each discardCards as card}
<div class="card-large suit-{card.slice(-1).toLowerCase()} {card === 'JOKER' ? 'joker-card' : ''}"
<div class="card-large suit-{card.slice(-1).toLowerCase()} {card.startsWith('Joker') ? 'joker-card' : ''}"
draggable="true"
on:dragstart={(e) => handleDragStart(e, card)}
on:click={() => moveToKeep(card)}
style="cursor: pointer;"
title="Click or drag to keep">
<div class="card-corner top-left">
<span class="val">{card === 'JOKER' ? '🃏' : card.slice(0, -1)}</span>
<span class="suit">{card === 'JOKER' ? '' : {'C': '♣️', 'S': '♠️', 'H': '♥️', 'D': '♦️'}[card.slice(-1)]}</span>
<span class="val">{card.startsWith('Joker') ? '🃏' : card.slice(0, -1)}</span>
<span class="suit">{card.startsWith('Joker') ? '' : {'C': '♣️', 'S': '♠️', 'H': '♥️', 'D': '♦️'}[card.slice(-1)]}</span>
</div>
<div class="card-center">
{#if card === 'JOKER'}
{#if card.startsWith('Joker')}
🃏
{:else}
<span class="giant-symbol">{{'C': '♣️', 'S': '♠️', 'H': '♥️', 'D': '♦️'}[card.slice(-1)]}</span>
{/if}
</div>
<div class="card-corner bottom-right">
<span class="val">{card === 'JOKER' ? '🃏' : card.slice(0, -1)}</span>
<span class="suit">{card === 'JOKER' ? '' : {'C': '♣️', 'S': '♠️', 'H': '♥️', 'D': '♦️'}[card.slice(-1)]}</span>
<span class="val">{card.startsWith('Joker') ? '🃏' : card.slice(0, -1)}</span>
<span class="suit">{card.startsWith('Joker') ? '' : {'C': '♣️', 'S': '♠️', 'H': '♥️', 'D': '♦️'}[card.slice(-1)]}</span>
</div>
</div>
{:else}
@@ -414,7 +428,7 @@
</div>
{/if}
{:else}
{#if state.player.role === 'pirat' && !state.votes.find(v => v.voter_player_id === state.player.id)}
{#if !state.votes.find(v => v.voter_player_id === state.player.id)}
<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}
@@ -432,6 +446,13 @@
{/if}
{/if}
{/if}
{#if state.player.is_creator && state.game.phase === 'between_scenes'}
<div style="margin-top: 1.5rem; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 1rem;">
<p class="info-text" style="font-size: 0.85rem;">All good stories end. When the crew agrees the legend is complete:</p>
<button on:click={finishGame} class="btn btn-danger">🏴‍☠️ End the Story</button>
</div>
{/if}
</div>
{/if}
</div>