Fable will fix it! Pt 3
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import { SUIT_EMOJI, isJoker, cardValue, cardSuit } from '../lib/cards';
|
||||
import { SUIT_EMOJI, isJoker, cardValue, cardSuit, cardTooltip } from '../lib/cards';
|
||||
|
||||
export let card; // card code, e.g. "10D" or "Joker1"
|
||||
export let size = 'large'; // 'large' | 'medium' | 'mini'
|
||||
@@ -16,17 +16,19 @@
|
||||
$: suit = joker ? '' : SUIT_EMOJI[cardSuit(card)];
|
||||
$: suitClass = joker ? 'joker' : cardSuit(card).toLowerCase();
|
||||
$: techName = techs && !joker ? techs[cardValue(card)] : null;
|
||||
// Tooltip: explicit title wins; otherwise remind the player of the suit's theme
|
||||
$: tooltip = title || cardTooltip(card);
|
||||
</script>
|
||||
|
||||
{#if size === 'mini'}
|
||||
<div class="card-mini {rotated ? 'played-card rotated' : 'base-card'} {success === true ? 'success' : success === false ? 'failure' : ''}" {title}>
|
||||
<div class="card-mini {rotated ? 'played-card rotated' : 'base-card'} {success === true ? 'success' : success === false ? 'failure' : ''}" title={tooltip}>
|
||||
<span class="val">{val}</span>
|
||||
<span class="suit">{suit}</span>
|
||||
{#if owner}<span class="owner">{owner}</span>{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="card-{size} suit-{suitClass} {joker ? 'joker-card' : ''}"
|
||||
{title} {draggable} {style}
|
||||
title={tooltip} {draggable} {style}
|
||||
on:dragstart on:dragend on:click>
|
||||
<div class="card-corner top-left">
|
||||
<span class="val">{val}</span>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { apiRequest } from '../lib/api';
|
||||
import { getSuggestion, getTechniqueSuggestion, getTechniqueSuggestions } from '../lib/suggestions';
|
||||
import { displayName } from '../lib/cards';
|
||||
|
||||
export let state;
|
||||
|
||||
@@ -42,7 +43,7 @@
|
||||
function getPlayerName(id) {
|
||||
if (!id) return "None";
|
||||
const p = state.players.find(x => x.id === id);
|
||||
return p ? p.name : "Unknown";
|
||||
return p ? displayName(p) : "Unknown";
|
||||
}
|
||||
|
||||
// Techniques
|
||||
@@ -274,7 +275,7 @@
|
||||
{#each state.players as p}
|
||||
{#if p.other_like_from_player_id === state.player.id && !p.other_like}
|
||||
<div class="inbox-item">
|
||||
<label>What do you LIKE about {p.name}?</label>
|
||||
<label>What do you LIKE about {displayName(p)}?</label>
|
||||
<div class="input-row">
|
||||
<input type="text" class="input-field" bind:value={inboxAnswers[`${p.id}:like`]} placeholder="e.g. They always share their cheese"/>
|
||||
<button type="button" class="btn btn-secondary btn-small" on:click={() => inboxAnswers[`${p.id}:like`] = getSuggestion('like')}>Suggest</button>
|
||||
@@ -284,7 +285,7 @@
|
||||
{/if}
|
||||
{#if p.other_hate_from_player_id === state.player.id && !p.other_hate}
|
||||
<div class="inbox-item">
|
||||
<label>What do you HATE about {p.name}?</label>
|
||||
<label>What do you HATE about {displayName(p)}?</label>
|
||||
<div class="input-row">
|
||||
<input type="text" class="input-field" bind:value={inboxAnswers[`${p.id}:hate`]} placeholder="e.g. They snore too loudly"/>
|
||||
<button type="button" class="btn btn-secondary btn-small" on:click={() => inboxAnswers[`${p.id}:hate`] = getSuggestion('hate')}>Suggest</button>
|
||||
@@ -512,7 +513,7 @@
|
||||
<ul class="space-y-2 mt-4">
|
||||
{#each state.players as p}
|
||||
<li class="p-3 bg-dark-800 rounded flex justify-between items-center border-l-4 {p.tech_jack ? 'border-green-500' : 'border-gray-500'}">
|
||||
<span>{p.name}</span>
|
||||
<span>{displayName(p)}</span>
|
||||
<span class="text-sm">
|
||||
{#if p.tech_jack}
|
||||
<span class="text-green-400">Ready</span>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script>
|
||||
import { displayName } from '../lib/cards';
|
||||
|
||||
export let state;
|
||||
|
||||
$: pirats = state.players;
|
||||
@@ -33,7 +35,7 @@
|
||||
<div style="background: rgba(0,0,0,0.2); padding: 0.75rem; border-radius: 6px; margin-bottom: 0.5rem; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;">
|
||||
<span>
|
||||
{#if p.is_ghost}👻{:else if p.is_dead}💀{:else}🐀{/if}
|
||||
<strong>{p.name}</strong> (Rank {p.rank})
|
||||
<strong>{displayName(p)}</strong> (Rank {p.rank})
|
||||
{#if p.id === state.game.captain_player_id}<span style="color: var(--gold);"> ⭐ Captain</span>{/if}
|
||||
</span>
|
||||
<span style="font-size: 0.85rem; color: var(--text-muted);">
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script>
|
||||
import Card from './Card.svelte';
|
||||
import { displayName } from '../lib/cards';
|
||||
import ChallengePanel from './scene/ChallengePanel.svelte';
|
||||
import ObstacleBoard from './scene/ObstacleBoard.svelte';
|
||||
import DeepControlPanel from './scene/DeepControlPanel.svelte';
|
||||
import CharacterSheet from './scene/CharacterSheet.svelte';
|
||||
import EventLog from './EventLog.svelte';
|
||||
|
||||
export let state;
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<div>
|
||||
{#if captain}
|
||||
<span class="captain-badge" style="background: rgba(212, 175, 55, 0.15); border: 1px solid var(--gold); color: var(--gold); padding: 0.4rem 1rem; border-radius: 20px; font-size: 1rem; font-weight: 700; font-family: var(--font-heading); display: inline-flex; align-items: center; gap: 0.5rem;">
|
||||
🏴☠️ Captain: {captain.name} (Rank {captain.rank}){isCaptain ? ' — You!' : ''}
|
||||
🏴☠️ Captain: {displayName(captain)} (Rank {captain.rank}){isCaptain ? ' — You!' : ''}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="captain-badge" style="background: rgba(255, 255, 255, 0.05); border: 1px dashed rgba(255, 255, 255, 0.2); color: var(--text-muted); padding: 0.4rem 1rem; border-radius: 20px; font-size: 1rem; display: inline-flex; align-items: center; gap: 0.5rem;">
|
||||
@@ -48,11 +48,11 @@
|
||||
{#each state.players as p}
|
||||
{#if p.role === 'deep'}
|
||||
<span class="player-chip deep-chip" style="font-size: 0.8rem; padding: 0.25rem 0.6rem; border-radius: 12px; margin: 0; display: inline-flex; align-items: center; gap: 0.25rem;">
|
||||
🌊 {p.name} (Deep)
|
||||
🌊 {displayName(p)} (Deep)
|
||||
</span>
|
||||
{:else if p.role === 'pirat'}
|
||||
<span class="player-chip pirat-chip" style="font-size: 0.8rem; padding: 0.25rem 0.6rem; border-radius: 12px; margin: 0; display: inline-flex; align-items: center; gap: 0.25rem; {p.is_ghost ? 'border-color: #7890a8; color: #a0b0c0; background: rgba(120, 150, 180, 0.1);' : ''} {p.is_dead ? 'border-color: var(--red-suit); color: var(--red-suit); background: rgba(255, 42, 95, 0.1);' : ''} {p.id === state.game.captain_player_id ? 'border-color: var(--gold); color: var(--gold); background: rgba(212, 175, 55, 0.1);' : ''}">
|
||||
{#if p.is_ghost}👻{:else if p.is_dead}💀{:else}🐀{/if} {p.name} (Rank {p.rank}){p.id === state.game.captain_player_id ? ' ⭐' : ''}
|
||||
{#if p.is_ghost}👻{:else if p.is_dead}💀{:else}🐀{/if} {displayName(p)} (Rank {p.rank}){p.id === state.game.captain_player_id ? ' ⭐' : ''}
|
||||
</span>
|
||||
{/if}
|
||||
{/each}
|
||||
@@ -99,5 +99,3 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EventLog {state} />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { apiRequest } from '../lib/api';
|
||||
import { displayName } from '../lib/cards';
|
||||
|
||||
export let state;
|
||||
|
||||
@@ -49,7 +50,19 @@
|
||||
<div class="scene-setup-view text-center">
|
||||
<h2>Scene Setup (Scene #{state.game.current_scene_number})</h2>
|
||||
<p class="description">Select your role for the upcoming scene. There must be at least one Pi-Rat and one Deep player. If you played the Deep in the last scene, you must play a Pi-Rat this scene!</p>
|
||||
|
||||
|
||||
{#if state.game.current_scene_number === 1}
|
||||
<div class="glass-panel" style="max-width: 700px; margin: 0 auto 1.5rem; padding: 1rem 1.5rem; border: 1px dashed var(--gold); border-radius: 8px; background: rgba(212, 175, 55, 0.08); text-align: left;">
|
||||
<h4 style="color: var(--gold); margin: 0 0 0.5rem 0; font-family: var(--font-heading);">🚢 First Scene Framing</h4>
|
||||
<p class="info-text" style="margin: 0; font-size: 0.95rem;">
|
||||
The first scene must establish that the Pi-Rats are <strong>on a ship they can steal</strong> (or have an immediate opportunity to steal one). <strong>Don't start far from water!</strong>
|
||||
{#if state.player.role === 'deep'}
|
||||
<br/><span style="color: var(--gold);">You're playing the Deep — once the scene starts, describe where the crew is and what ship is ripe for the taking before calling any Challenges.</span>
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="setup-grid">
|
||||
<!-- Role Selection Card -->
|
||||
<div class="card glass-panel role-selection-card">
|
||||
@@ -93,7 +106,7 @@
|
||||
<div class="roster-list" id="scene-roster-list">
|
||||
{#each state.players as p}
|
||||
<div class="roster-item">
|
||||
<span class="player-name">{p.name} <span class="text-sm text-gray-400">(Rank {p.rank})</span></span>
|
||||
<span class="player-name">{displayName(p)} <span class="text-sm text-gray-400">(Rank {p.rank})</span></span>
|
||||
{#if p.role === 'pirat'}
|
||||
<span class="role-badge pirat">Pi-Rat</span>
|
||||
{:else if p.role === 'deep'}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script>
|
||||
import { apiRequest } from '../lib/api';
|
||||
import { getSuggestion } from '../lib/suggestions';
|
||||
import { displayName } from '../lib/cards';
|
||||
import Card from './Card.svelte';
|
||||
|
||||
export let state;
|
||||
@@ -270,7 +271,7 @@
|
||||
<option value="">Nominate crewmate...</option>
|
||||
{#each state.players as p}
|
||||
{#if p.id !== state.player.id && p.role !== 'deep'}
|
||||
<option value={p.id}>{p.name} (Rank {p.rank})</option>
|
||||
<option value={p.id}>{displayName(p)} (Rank {p.rank})</option>
|
||||
{/if}
|
||||
{/each}
|
||||
</select>
|
||||
@@ -287,7 +288,7 @@
|
||||
{#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'}">
|
||||
<span class="name">{p.name}</span>
|
||||
<span class="name">{displayName(p)}</span>
|
||||
<span class="status-badge">{voted ? 'Done' : 'Voting...'}</span>
|
||||
</div>
|
||||
{/each}
|
||||
@@ -372,7 +373,7 @@
|
||||
<ul class="ranks-ledger">
|
||||
{#each state.players as p}
|
||||
<li>
|
||||
<strong>{p.name}:</strong> Rank {p.rank}
|
||||
<strong>{displayName(p)}:</strong> Rank {p.rank}
|
||||
{#if p.role === 'deep'}<span class="deep-badge">Deep</span>{:else if p.is_ghost}<span class="ghost-badge" style="background: rgba(120, 150, 180, 0.15); border: 1px solid #7890a8; color: #a0b0c0; padding: 0.15rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 700; margin-left: 0.5rem; display: inline-block;">Ghost</span>{:else}<span class="pirat-badge">Pi-Rat</span>{/if}
|
||||
</li>
|
||||
{/each}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import { apiRequest } from '../../lib/api';
|
||||
import { getCardDisplay, isJoker, playerName as lookupName } from '../../lib/cards';
|
||||
import { getCardDisplay, isJoker, displayName, playerName as lookupName } from '../../lib/cards';
|
||||
|
||||
export let state;
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
<select class="select-field" bind:value={taxTargetId} style="max-width: 200px;">
|
||||
<option value="">Pick a crewmate...</option>
|
||||
{#each eligibleTaxTargets() as p}
|
||||
<option value={p.id}>{p.name}</option>
|
||||
<option value={p.id}>{displayName(p)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<button class="btn btn-secondary" on:click={() => requestTax(ch.id)} disabled={!taxTargetId}>Call {taxType(state.player)} Tax</button>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import { apiRequest } from '../../lib/api';
|
||||
import { getCardDisplay, isJoker } from '../../lib/cards';
|
||||
import { getCardDisplay, isJoker, displayName } from '../../lib/cards';
|
||||
|
||||
export let state;
|
||||
|
||||
@@ -43,6 +43,11 @@
|
||||
|
||||
<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}
|
||||
@@ -104,7 +109,7 @@
|
||||
<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}>{p.name} (Rank {p.rank})</option>
|
||||
<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;">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script>
|
||||
import { apiRequest } from '../../lib/api';
|
||||
import { displayName } from '../../lib/cards';
|
||||
|
||||
export let state;
|
||||
|
||||
@@ -75,7 +76,7 @@
|
||||
<select class="select-field" bind:value={challengeTargetId} style="width: 100%; margin-bottom: 0.5rem;">
|
||||
<option value="">Challenge which Pi-Rat?</option>
|
||||
{#each scenePirats.filter(p => !p.is_dead) as p}
|
||||
<option value={p.id}>{p.name} (Rank {p.rank})</option>
|
||||
<option value={p.id}>{displayName(p)} (Rank {p.rank})</option>
|
||||
{/each}
|
||||
</select>
|
||||
<div style="margin-bottom: 0.5rem;">
|
||||
@@ -102,7 +103,7 @@
|
||||
<select class="select-field" bind:value={captainSelectId} style="flex: 1;">
|
||||
<option value="">No Captain</option>
|
||||
{#each scenePirats.filter(p => !p.is_dead) as p}
|
||||
<option value={p.id}>{p.name}</option>
|
||||
<option value={p.id}>{displayName(p)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<button class="btn btn-secondary" on:click={setCaptain}>Set</button>
|
||||
@@ -132,7 +133,7 @@
|
||||
<p class="info-text">You control completion. Only mark in sequence (1 -> 2 -> 3).</p>
|
||||
{#each scenePirats as p}
|
||||
<div style="background: rgba(0,0,0,0.2); padding: 0.5rem; border-radius: 4px; margin-bottom: 0.5rem;">
|
||||
<strong>{p.name}</strong>
|
||||
<strong>{displayName(p)}</strong>
|
||||
<div class="objectives-checklist" style="margin-top: 0.25rem;">
|
||||
<label class="checkbox-label"><input type="checkbox" checked={p.completed_personal_1} on:change={() => toggleObjective(p.id, 'personal_1')}> <span>1. Gat</span></label>
|
||||
<label class="checkbox-label"><input type="checkbox" checked={p.completed_personal_2} on:change={() => toggleObjective(p.id, 'personal_2')}> <span>2. Name</span></label>
|
||||
|
||||
@@ -2,6 +2,21 @@
|
||||
// Codes look like "10D", "KH", "Joker1" (black) / "Joker2" (red).
|
||||
export const SUIT_EMOJI = { C: '♣️', S: '♠️', H: '♥️', D: '♦️' };
|
||||
|
||||
// Suit themes from the rulebook: how a Pi-Rat overcomes an Obstacle with that suit.
|
||||
export const SUIT_THEMES = {
|
||||
C: "♣ Clubs — Shootin' and Stabbin': violence, combat, or direct confrontation",
|
||||
D: "♦ Diamonds — Swimmin' and Sailin': nautical skills, athletics, or mobility",
|
||||
S: "♠ Spades — Sneakin' and Schemin': stealth, deception, or cunning plans",
|
||||
H: "♥ Hearts — Shoutin' and Singin': social skills, performance, or charisma",
|
||||
};
|
||||
|
||||
// Default tooltip for a card: its suit theme (or what a Joker does).
|
||||
export function cardTooltip(code) {
|
||||
if (!code) return '';
|
||||
if (isJoker(code)) return '🃏 Joker — discards an Obstacle outright and draws a replacement';
|
||||
return SUIT_THEMES[cardSuit(code)] || '';
|
||||
}
|
||||
|
||||
export function isJoker(code) {
|
||||
return !!code && code.startsWith('Joker');
|
||||
}
|
||||
@@ -22,6 +37,14 @@ export function getCardDisplay(code) {
|
||||
return `${cardValue(code)}${SUIT_EMOJI[cardSuit(code)] || cardSuit(code)}`;
|
||||
}
|
||||
|
||||
export function playerName(players, id) {
|
||||
return players.find(p => p.id === id)?.name || '???';
|
||||
// "Recruit Cheese (Tim)" — the Pi-Rat's identity with the human player's lobby
|
||||
// name in parentheses (omitted while the two are still identical, e.g. in the lobby).
|
||||
export function displayName(p) {
|
||||
if (!p) return '???';
|
||||
if (p.player_name && p.player_name !== p.name) return `${p.name} (${p.player_name})`;
|
||||
return p.name;
|
||||
}
|
||||
|
||||
export function playerName(players, id) {
|
||||
return displayName(players.find(p => p.id === id));
|
||||
}
|
||||
|
||||
@@ -53,7 +53,8 @@
|
||||
<table class="w-full text-left bg-dark-900 rounded border border-gray-700">
|
||||
<thead>
|
||||
<tr class="border-b border-gray-700">
|
||||
<th class="p-3">Name</th>
|
||||
<th class="p-3">Pi-Rat</th>
|
||||
<th class="p-3">Player</th>
|
||||
<th class="p-3">Role</th>
|
||||
<th class="p-3">Status</th>
|
||||
<th class="p-3">Rank</th>
|
||||
@@ -63,6 +64,7 @@
|
||||
{#each data.players as p}
|
||||
<tr class="border-b border-gray-800">
|
||||
<td class="p-3">{p.name}</td>
|
||||
<td class="p-3">{p.player_name || p.name}</td>
|
||||
<td class="p-3">
|
||||
{#if p.role === 'deep'} 🌊 Deep
|
||||
{:else if p.role === 'pirat'} 🐀 Pi-Rat
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import ScenePhase from '../components/ScenePhase.svelte';
|
||||
import UpkeepPhase from '../components/UpkeepPhase.svelte';
|
||||
import GameOverPhase from '../components/GameOverPhase.svelte';
|
||||
import EventLog from '../components/EventLog.svelte';
|
||||
|
||||
export let params = {};
|
||||
let gameId = params.id;
|
||||
@@ -62,6 +63,12 @@
|
||||
<div class="card p-4">Unknown phase: {state.game.phase}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<EventLog {state} />
|
||||
|
||||
{#if state.player.is_creator}
|
||||
<a class="admin-corner-link" href="#/game/{gameId}/admin" title="Open the Admin Panel">🛠️ Admin</a>
|
||||
{/if}
|
||||
{:else if !error}
|
||||
<div class="loading-container">
|
||||
<p>Loading game state...</p>
|
||||
@@ -69,6 +76,24 @@
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.admin-corner-link {
|
||||
position: fixed;
|
||||
top: 12px;
|
||||
right: 16px;
|
||||
z-index: 1001;
|
||||
padding: 6px 14px;
|
||||
border-radius: 16px;
|
||||
background: rgba(10, 15, 25, 0.9);
|
||||
border: 1px solid var(--gold, #d4af37);
|
||||
color: var(--gold, #d4af37);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
font-family: var(--font-heading);
|
||||
}
|
||||
.admin-corner-link:hover {
|
||||
background: rgba(212, 175, 55, 0.15);
|
||||
}
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -31,7 +31,11 @@
|
||||
</div>
|
||||
|
||||
<div class="card creation-card">
|
||||
<h2>Enter your Pi-Rat name</h2>
|
||||
<h2>Enter your name</h2>
|
||||
<p class="info-text" style="margin-bottom: 1rem;">
|
||||
This is <strong>your</strong> name as a player, not your Pi-Rat's. Your Pi-Rat will be known
|
||||
by their smell (e.g. "Recruit Gunpowder") until they earn a proper Name in play.
|
||||
</p>
|
||||
<form on:submit|preventDefault={joinGame}>
|
||||
<div class="form-group">
|
||||
<label for="playerName">Your Name</label>
|
||||
|
||||
Reference in New Issue
Block a user