Files
pirats/frontend/src/components/scene/CharacterSheet.svelte
Tim McCarthy 44bbb88836 Add gat-description modal when a Pi-Rat earns a Gat
Mirrors the name modal: earning the Gat objective sets
Player.needs_gat_description, and GatModal.svelte (rendered by Dashboard
across all phases) prompts for a one-of-a-kind Gat description, stored in
Player.gat_description and shown on the character sheet. Like a stolen
Name, the Gat's description travels with it through Gat Tax transfers.

- Player.gat_description / needs_gat_description (+ migration)
- toggle_objective personal_1 sets/clears the prompt
- crud_challenge gat-tax paths move the description with the Gat
- set-gat-description route; CharacterSheet displays it

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 09:46:02 -07:00

172 lines
8.1 KiB
Svelte

<script>
import { apiRequest } from '../../lib/api';
import { getCardDisplay, isJoker, displayName, cardObstacleInfo, obstacleTable } from '../../lib/cards';
export let state;
let error = '';
let pvpOpponentId = '';
let pvpCard = '';
$: hand = state.player.hand_cards ? JSON.parse(state.player.hand_cards) : [];
$: scenePirats = state.players.filter(p => p.role === 'pirat');
function getPlayerName(id) {
if (!id) return 'a crewmate';
const p = state.players.find(pl => pl.id === id);
return p ? displayName(p) : 'Unknown';
}
$: duelTargets = scenePirats.filter(p => p.id !== state.player.id && !p.is_dead);
// What the chosen duel card becomes as a temporary Obstacle for the defender
$: pvpCardObstacle = cardObstacleInfo(pvpCard, $obstacleTable);
async function createPvp() {
error = '';
try {
await apiRequest(`/game/${state.game.id}/player/${state.player.id}/challenge/pvp/create`, 'POST', {
defender_id: pvpOpponentId,
card_code: pvpCard
});
pvpOpponentId = '';
pvpCard = '';
} catch(e) {
error = e.message;
}
}
</script>
<div class="card glass-panel sheet-card">
<h3>🐀 {state.player.name}'s Character Sheet</h3>
{#if state.player.player_name && state.player.player_name !== state.player.name}
<p class="info-text" style="margin-top: -0.5rem; font-size: 0.85rem;">
Played by {state.player.player_name}{#if !state.player.completed_personal_2}&nbsp;— they'll go by their smell until they earn a Name!{/if}
</p>
{/if}
<div class="character-sheet-details">
{#if error}
<div class="alert alert-danger">{error}</div>
{/if}
{#if state.player.needs_reroll}
<div class="prompt-box accent text-center">
<h4>🐀 Recruit Incoming!</h4>
<p class="info-text" style="margin-bottom: 0; font-size: 0.9rem;">You don't have a Pi-Rat in this scene. Sit back, enjoy the chaos, and you'll create your recruit with the crew between scenes.</p>
</div>
{/if}
{#if state.player.is_dead}
<div class="prompt-box danger text-center">
<h4>💀 You have Died / Retired!</h4>
<p class="info-text" style="margin-bottom: 0; font-size: 0.9rem;">Your story arc is complete. You will choose to become a Ghost or roll a new character during the upkeep phase between scenes.</p>
</div>
{/if}
{#if state.player.is_ghost}
<div class="prompt-box ghostly text-center">
<h4>👻 Playing as a Ghost</h4>
<p class="info-text" style="margin-bottom: 0; font-size: 0.9rem;">You are in the Ghost World (grayscale view). You cannot interact well with the living but you can still help them resolve challenges!</p>
</div>
{/if}
{#if state.player.tax_banned}
<div class="prompt-box danger text-center">
<p class="info-text" style="margin: 0; font-size: 0.9rem;">🚫 You failed a refused Tax — no more Gat/Name Taxes for you this scene.</p>
</div>
{/if}
<div class="sheet-group">
<h4>Description:</h4>
<p><strong>Look:</strong> {state.player.avatar_look}</p>
<p><strong>Smell:</strong> {state.player.avatar_smell}</p>
<p><strong>First Words:</strong> "{state.player.first_words}"</p>
{#if state.player.gat_description}
<p><strong>🔫 Gat:</strong> {state.player.gat_description}</p>
{/if}
</div>
{#if state.player.other_like || state.player.other_hate}
<div class="sheet-group margin-top">
<h4>What the Crew Thinks:</h4>
{#if state.player.other_like}
<p><strong>👍 Likes:</strong> "{state.player.other_like}" <span class="text-muted">{getPlayerName(state.player.other_like_from_player_id)}</span></p>
{/if}
{#if state.player.other_hate}
<p><strong>👎 Hates:</strong> "{state.player.other_hate}" <span class="text-muted">{getPlayerName(state.player.other_hate_from_player_id)}</span></p>
{/if}
</div>
{/if}
<div class="sheet-group margin-top">
<h4>Assigned Secret Techniques (J/Q/K):</h4>
<ul class="techniques-list-sheet">
<li><strong>Jack (J):</strong> "{state.player.tech_jack}"</li>
<li><strong>Queen (Q):</strong> "{state.player.tech_queen}"</li>
<li><strong>King (K):</strong> "{state.player.tech_king}"</li>
</ul>
</div>
<!-- Duel another Pi-Rat -->
{#if !state.player.is_dead && !state.player.needs_reroll && duelTargets.length > 0}
<div class="sheet-group margin-top">
<h4>⚔️ Duel a Pi-Rat</h4>
<p class="info-text" style="font-size: 0.85rem;">Challenge a crewmate (e.g. for the Captaincy)! Your card becomes a temporary Obstacle they must beat. Both cards are discarded afterwards.</p>
<select class="select-field" bind:value={pvpOpponentId} style="width: 100%; margin-bottom: 0.5rem;">
<option value="">Challenge whom?</option>
{#each duelTargets as p}
<option value={p.id}>{displayName(p)} (Rank {p.rank})</option>
{/each}
</select>
<select class="select-field" bind:value={pvpCard} style="width: 100%; margin-bottom: 0.5rem;">
<option value="">Throw which card?</option>
{#each hand.filter(c => !isJoker(c)) as card}
<option value={card} title={cardObstacleInfo(card, $obstacleTable) ? `${cardObstacleInfo(card, $obstacleTable).title} ${cardObstacleInfo(card, $obstacleTable).description}` : ''}>{getCardDisplay(card)}</option>
{/each}
</select>
{#if pvpCardObstacle}
<p class="info-text" style="font-size: 0.8rem; margin: -0.25rem 0 0.5rem 0;">
As an Obstacle, {getCardDisplay(pvpCard)} is <strong>{pvpCardObstacle.title}</strong>: {pvpCardObstacle.description}
</p>
{/if}
<button class="btn btn-secondary btn-full" on:click={createPvp} disabled={!pvpOpponentId || !pvpCard}>Throw Down!</button>
</div>
{/if}
<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} disabled>
<span>Steal a Ship</span>
</label>
<label class="checkbox-label">
<input type="checkbox" checked={state.game.completed_crew_2} disabled>
<span>Choose a Captain</span>
</label>
<label class="checkbox-label">
<input type="checkbox" checked={state.game.completed_crew_3} disabled>
<span>Commit Piracy</span>
</label>
</div>
</div>
<div class="sheet-group margin-top">
<h4>Personal Objectives</h4>
<div class="objectives-checklist">
<label class="checkbox-label">
<input type="checkbox" checked={state.player.completed_personal_1} disabled>
<span>1. Gat</span>
</label>
<label class="checkbox-label">
<input type="checkbox" checked={state.player.completed_personal_2} disabled>
<span>2. Name</span>
</label>
<label class="checkbox-label">
<input type="checkbox" checked={state.player.completed_personal_3} disabled>
<span>3. Die/Retire</span>
</label>
</div>
</div>
</div>
</div>