194 lines
9.0 KiB
Svelte
194 lines
9.0 KiB
Svelte
<script>
|
|
import { apiRequest } from '../../lib/api';
|
|
import { getCardDisplay, isJoker, displayName, cardObstacleInfo, obstacleTable } from '../../lib/cards';
|
|
|
|
export let state;
|
|
|
|
let error = '';
|
|
let newNameInput = '';
|
|
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 submitNewName() {
|
|
if (!newNameInput.trim()) return;
|
|
error = '';
|
|
try {
|
|
await apiRequest(`/game/${state.game.id}/player/${state.player.id}/set-name`, 'POST', {
|
|
new_name: newNameInput.trim()
|
|
});
|
|
newNameInput = '';
|
|
} catch(e) {
|
|
error = e.message;
|
|
}
|
|
}
|
|
|
|
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} — 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.needs_name}
|
|
<div class="prompt-box accent">
|
|
<h4>🏴☠️ You Earned a Name!</h4>
|
|
<p class="info-text" style="margin-bottom: 0.5rem;">You completed your second objective and ranked up! What is your new Pirate Name?</p>
|
|
<div class="input-row">
|
|
<input type="text" bind:value={newNameInput} class="input-field" placeholder="Enter new name...">
|
|
<button class="btn btn-gold" on:click={submitNewName} disabled={!newNameInput.trim()}>Claim Name</button>
|
|
</div>
|
|
</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>
|
|
</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>
|