Switch to Svelte

This commit is contained in:
2026-06-11 14:39:41 -07:00
parent 29860061c4
commit 58a7e4d4f1
74 changed files with 7144 additions and 2379 deletions

View File

@@ -0,0 +1,527 @@
<script>
import { apiRequest } from '../lib/api';
import { getSuggestion } from '../lib/suggestions';
export let state;
// Form states
let basicDetails = {
avatar_look: state.player.avatar_look || '',
avatar_smell: state.player.avatar_smell || '',
first_words: state.player.first_words || '',
good_at_math: state.player.good_at_math || ''
};
let savingBasic = false;
let delegating = false;
async function saveBasicDetails() {
savingBasic = true;
try {
await apiRequest(`/game/${state.game.id}/player/${state.player.id}/save-basic`, 'POST', basicDetails);
} catch(e) {
console.error(e);
} finally {
savingBasic = false;
}
}
async function autoDelegate() {
delegating = true;
try {
await apiRequest(`/game/${state.game.id}/player/${state.player.id}/delegate/auto`, 'POST');
} catch(e) {
console.error(e);
} finally {
delegating = false;
}
}
// A helper to get player name
function getPlayerName(id) {
if (!id) return "None";
const p = state.players.find(x => x.id === id);
return p ? p.name : "Unknown";
}
// Techniques
let tech1 = '', tech2 = '', tech3 = '';
let savingTechs = false;
async function submitTechniques() {
savingTechs = true;
try {
await apiRequest(`/game/${state.game.id}/player/${state.player.id}/submit-techniques`, 'POST', {
tech1, tech2, tech3
});
} catch(e) {
console.error(e);
} finally {
savingTechs = false;
}
}
// Face Techniques
let jackTech = '', queenTech = '', kingTech = '';
let assigningFace = false;
let faceError = '';
async function assignFaceTechniques() {
assigningFace = true;
faceError = '';
try {
await apiRequest(`/game/${state.game.id}/player/${state.player.id}/assign-face-techniques`, 'POST', {
jack: jackTech,
queen: queenTech,
king: kingTech
});
} catch(e) {
faceError = e.message;
} finally {
assigningFace = false;
}
}
// Submit delegated answers
let answerLike = '', answerHate = '';
async function submitDelegatedAnswer(type, targetId, answer) {
try {
await apiRequest(`/game/${state.game.id}/player/${state.player.id}/submit-delegate/${targetId}/${type}`, 'POST', { answer });
} catch(e) {
console.error(e);
}
}
// Check completion status
$: createdTechniques = state.player.created_techniques ? JSON.parse(state.player.created_techniques) : [];
$: swappedTechniques = state.player.swapped_techniques ? JSON.parse(state.player.swapped_techniques) : [];
$: basicComplete = !!(state.player.avatar_look && state.player.avatar_smell && state.player.first_words && state.player.good_at_math);
$: delegateComplete = !!(state.player.other_like_from_player_id && state.player.other_hate_from_player_id);
$: techniquesComplete = !!(state.player.tech_jack && state.player.tech_queen && state.player.tech_king);
// Dev Mode
let devMode = false;
let autoFilling = false;
async function autoFillAll() {
autoFilling = true;
try {
// 1. Basic details
if (!basicComplete) {
basicDetails = {
avatar_look: 'A scruffy rat',
avatar_smell: 'Like old cheese',
first_words: 'Give me the loot!',
good_at_math: 'No'
};
await saveBasicDetails();
}
// 2. Delegate
if (!delegateComplete && state.players.length >= 2) {
await autoDelegate();
}
// 3. Answer questions (might need a quick refresh to get the questions, but we can do our best with current state)
// Wait, state needs to be re-fetched to get the assigned questions. We can just fill what's currently in inbox.
for (const p of state.players) {
if (p.other_like_from_player_id === state.player.id && !p.other_like) {
await submitDelegatedAnswer('like', p.id, 'They are sneaky');
}
if (p.other_hate_from_player_id === state.player.id && !p.other_hate) {
await submitDelegatedAnswer('hate', p.id, 'They snore loud');
}
}
// 4. Techniques
if (createdTechniques.length === 0) {
tech1 = 'Pocket Sand';
tech2 = 'Tail Whip';
tech3 = 'Cheese Decoy';
await submitTechniques();
}
// 5. Face Techniques - we need the swapped ones. We can't do this synchronously without waiting for the server
// to process the state, because swapped_techniques comes from the server once everyone is done.
if (!techniquesComplete && swappedTechniques.length === 3) {
jackTech = swappedTechniques[0];
queenTech = swappedTechniques[1];
kingTech = swappedTechniques[2];
await assignFaceTechniques();
}
} catch (e) {
console.error(e);
} finally {
autoFilling = false;
}
}
</script>
<div class="character-creation-view">
<div class="view-header text-center">
<h2>Character Sheet Creation</h2>
<p>Fill in your Rat Records, delegate questions to your crew, and prepare your Math Magic Techniques.</p>
<div class="flex justify-center items-center gap-2 text-sm text-gray-500 mt-2">
<input type="checkbox" id="devModeToggle" bind:checked={devMode} class="accent-gold"/>
<label for="devModeToggle">Dev Mode</label>
</div>
</div>
{#if devMode}
<div class="bg-dark-900 border border-gold p-4 rounded-lg flex justify-between items-center mb-6 max-w-4xl mx-auto">
<span class="text-gold font-bold">🛠 Dev Mode Tools</span>
<button
class="btn btn-primary"
on:click={autoFillAll}
disabled={autoFilling}>
{autoFilling ? 'Auto-Filling...' : 'Auto-Fill Available Fields'}
</button>
</div>
{/if}
<!-- MAIN PANEL: Grid for Layout -->
<div class="creation-grid max-w-5xl mx-auto">
<!-- SECTION 1: Rat Records (Basic Info) -->
<div class="card glass-panel section-basic">
<h3>I. Basic Rat Records {basicComplete ? '✅' : '❌'}</h3>
{#if basicComplete}
<!-- Saved View -->
<div class="read-only-sheet">
<div class="record-line"><strong>Look:</strong> {state.player.avatar_look}</div>
<div class="record-line"><strong>Smell:</strong> {state.player.avatar_smell}</div>
<div class="record-line"><strong>First Words:</strong> "{state.player.first_words}"</div>
<div class="record-line"><strong>Good at Math?</strong> {state.player.good_at_math}</div>
</div>
{:else}
<!-- Form View -->
<form on:submit|preventDefault={saveBasicDetails} class="creation-form">
<div class="form-group">
<label for="avatar_look">What does your Pi-Rat look like?</label>
<div class="input-row">
<input type="text" id="avatar_look" placeholder="e.g. A blue bandana, scarred snout" bind:value={basicDetails.avatar_look} required class="input-field">
<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.avatar_look = getSuggestion('look')}>Suggest</button>
</div>
</div>
<div class="form-group">
<label for="avatar_smell">What does your Pi-Rat smell like?</label>
<div class="input-row">
<input type="text" id="avatar_smell" placeholder="e.g. Damp gunpowder, salty cheese" bind:value={basicDetails.avatar_smell} required class="input-field">
<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.avatar_smell = getSuggestion('smell')}>Suggest</button>
</div>
</div>
<div class="form-group">
<label for="first_words">What were your first words after transforming?</label>
<div class="input-row">
<input type="text" id="first_words" placeholder="e.g. Where is my gat?!" bind:value={basicDetails.first_words} required class="input-field">
<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.first_words = getSuggestion('first_words')}>Suggest</button>
</div>
</div>
<div class="form-group">
<label for="good_at_math">Is your Pi-Rat good at Math?</label>
<div class="input-row">
<input type="text" id="good_at_math" placeholder="e.g. Yes, but only geometry; No, thinks Pi is a dessert" bind:value={basicDetails.good_at_math} required class="input-field">
<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.good_at_math = getSuggestion('good_at_math')}>Suggest</button>
</div>
</div>
<button type="submit" class="btn btn-primary btn-full" disabled={savingBasic}>Save Records</button>
</form>
{/if}
</div>
<!-- SECTION 2: Delegated Questions (Like/Hate) -->
<div class="card glass-panel section-delegations">
<div class="card-header-flex" style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem; border-bottom:1px solid rgba(212,175,55,0.2); padding-bottom:0.5rem;">
<h3 style="border:none; margin:0; padding:0; color:var(--gold); font-family:var(--font-heading);">II. Crew Delegations {delegateComplete ? '✅' : '❌'}</h3>
{#if !delegateComplete}
<button on:click={autoDelegate} disabled={delegating || state.players.length < 2} class="btn btn-secondary btn-small">
🎲 Auto-Assign Tasks
</button>
{/if}
</div>
<p class="section-desc">You must let other players decide what they like and hate about your character.</p>
<div class="delegation-list">
{#if delegateComplete}
<div class="delegation-box glass-panel">
<h4>What does another rat LIKE about you?</h4>
<div class="answer-box">
{state.player.other_like ? `"${state.player.other_like}"` : '(Waiting for answer...)'}
</div>
<div class="author-label"> {getPlayerName(state.player.other_like_from_player_id)}</div>
</div>
<div class="delegation-box glass-panel">
<h4>What does another rat HATE about you?</h4>
<div class="answer-box">
{state.player.other_hate ? `"${state.player.other_hate}"` : '(Waiting for answer...)'}
</div>
<div class="author-label"> {getPlayerName(state.player.other_hate_from_player_id)}</div>
</div>
{:else}
<p class="mb-4 text-sm text-gray-400">Randomly assign crewmates to write your relationships by clicking "Auto-Assign Tasks".</p>
{/if}
</div>
</div>
<!-- SECTION 3: Answer Delegations for Others -->
<div class="card glass-panel section-inbox" style="grid-column: span 2;">
<h3>III. Your Inbox (Task List)</h3>
<p class="section-desc">Answer these questions for your crewmates. Be creative and have fun with it!</p>
<div class="inbox-list">
{#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>
<div class="input-row">
<input type="text" class="input-field" bind:value={answerLike} placeholder="e.g. They always share their cheese"/>
<button class="btn btn-primary" on:click={() => submitDelegatedAnswer('like', p.id, answerLike)}>Submit</button>
</div>
</div>
{/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>
<div class="input-row">
<input type="text" class="input-field" bind:value={answerHate} placeholder="e.g. They snore too loudly"/>
<button class="btn btn-primary" on:click={() => submitDelegatedAnswer('hate', p.id, answerHate)}>Submit</button>
</div>
</div>
{/if}
{/each}
{#if state.players.filter(p => (p.other_like_from_player_id === state.player.id && !p.other_like) || (p.other_hate_from_player_id === state.player.id && !p.other_hate)).length === 0}
<p class="inbox-empty-message text-gray-400 italic">No pending tasks! Wait for other players to assign you tasks.</p>
{/if}
</div>
</div>
<!-- SECTION 4: Techniques -->
<div class="card glass-panel section-techniques" id="techniques-card" style="grid-column: span 2;">
<h3>IV. Secret Techniques {techniquesComplete ? '✅' : '❌'}</h3>
{#if createdTechniques.length === 0}
<p class="section-desc">Create 3 techniques that you will swap with other players. Make them cool or ridiculous!</p>
<form on:submit|preventDefault={submitTechniques} id="tech-form">
<div class="form-group">
<label for="tech1">Technique #1:</label>
<div class="input-row">
<input type="text" id="tech1" placeholder="e.g. Carlo's cheese shield" bind:value={tech1} required class="input-field">
<button type="button" class="btn btn-secondary btn-small" on:click={() => tech1 = getSuggestion('techniques')}>Suggest</button>
</div>
</div>
<div class="form-group">
<label for="tech2">Technique #2:</label>
<div class="input-row">
<input type="text" id="tech2" placeholder="e.g. Parabolic boarding bounce" bind:value={tech2} required class="input-field">
<button type="button" class="btn btn-secondary btn-small" on:click={() => tech2 = getSuggestion('techniques')}>Suggest</button>
</div>
</div>
<div class="form-group">
<label for="tech3">Technique #3:</label>
<div class="input-row">
<input type="text" id="tech3" placeholder="e.g. Look, a three-headed gull!" bind:value={tech3} required class="input-field">
<button type="button" class="btn btn-secondary btn-small" on:click={() => tech3 = getSuggestion('techniques')}>Suggest</button>
</div>
</div>
<button type="submit" class="btn btn-primary btn-full glow-effect" disabled={savingTechs}>Submit Techniques to Swap Pool</button>
</form>
{:else if createdTechniques.length === 3 && swappedTechniques.length < 3}
<div class="submitted-techniques text-center mt-4">
<div class="tech-chip">#1: {createdTechniques[0]}</div>
<div class="tech-chip">#2: {createdTechniques[1]}</div>
<div class="tech-chip">#3: {createdTechniques[2]}</div>
<p class="waiting-box margin-top flex justify-center mt-4">
<span class="spinner-small"></span>
Techniques submitted! Waiting for other players to submit so we can shuffle and swap them.
</p>
</div>
{:else if !techniquesComplete}
{#if swappedTechniques.length === 3}
<div class="max-w-4xl mx-auto space-y-6">
<p class="text-gray-300">Drag and drop your assigned techniques onto your Face Cards:</p>
{#if faceError}
<div class="alert alert-danger">{faceError}</div>
{/if}
<div class="available-techniques-container">
<h4>Available Swapped Techniques</h4>
<p class="instruction-help">Drag a technique onto a card slot below to assign it.</p>
<div class="techniques-drag-pool">
{#each swappedTechniques as tech}
<div
draggable={tech !== jackTech && tech !== queenTech && tech !== kingTech}
on:dragstart={(e) => e.dataTransfer.setData('text/plain', tech)}
class="draggable-tech-chip {tech === jackTech || tech === queenTech || tech === kingTech ? 'assigned-hidden' : ''}"
>
<span class="drag-icon">⋮⋮</span> {tech}
</div>
{/each}
</div>
</div>
<!-- Visual Card Slots Grid -->
<div class="face-cards-slots-grid">
<!-- JACK SLOT -->
<div class="face-card-slot-wrapper">
<div class="face-card-slot {jackTech ? 'has-assignment' : ''}"
on:dragover|preventDefault
on:drop={(e) => {
const tech = e.dataTransfer.getData('text/plain');
if (tech) {
if (queenTech === tech) queenTech = '';
if (kingTech === tech) kingTech = '';
jackTech = tech;
}
}}>
<div class="card-bg-letter">J</div>
<div class="card-slot-header">
<span class="rank">J</span>
</div>
<div class="card-slot-body">
<div class="card-title">Jack</div>
<div class="drop-zone-placeholder">Drop Technique Here</div>
<div class="assigned-tech-content">
{#if jackTech}
<div class="assigned-tech-chip">
{jackTech}
<span class="remove-tech-btn" on:click={() => jackTech = ''}>×</span>
</div>
{/if}
</div>
</div>
<div class="card-slot-footer">
<span></span>
<span class="rank">J</span>
</div>
</div>
</div>
<!-- QUEEN SLOT -->
<div class="face-card-slot-wrapper">
<div class="face-card-slot {queenTech ? 'has-assignment' : ''}"
on:dragover|preventDefault
on:drop={(e) => {
const tech = e.dataTransfer.getData('text/plain');
if (tech) {
if (jackTech === tech) jackTech = '';
if (kingTech === tech) kingTech = '';
queenTech = tech;
}
}}>
<div class="card-bg-letter">Q</div>
<div class="card-slot-header">
<span class="rank">Q</span>
</div>
<div class="card-slot-body">
<div class="card-title">Queen</div>
<div class="drop-zone-placeholder">Drop Technique Here</div>
<div class="assigned-tech-content">
{#if queenTech}
<div class="assigned-tech-chip">
{queenTech}
<span class="remove-tech-btn" on:click={() => queenTech = ''}>×</span>
</div>
{/if}
</div>
</div>
<div class="card-slot-footer">
<span></span>
<span class="rank">Q</span>
</div>
</div>
</div>
<!-- KING SLOT -->
<div class="face-card-slot-wrapper">
<div class="face-card-slot {kingTech ? 'has-assignment' : ''}"
on:dragover|preventDefault
on:drop={(e) => {
const tech = e.dataTransfer.getData('text/plain');
if (tech) {
if (jackTech === tech) jackTech = '';
if (queenTech === tech) queenTech = '';
kingTech = tech;
}
}}>
<div class="card-bg-letter">K</div>
<div class="card-slot-header">
<span class="rank">K</span>
</div>
<div class="card-slot-body">
<div class="card-title">King</div>
<div class="drop-zone-placeholder">Drop Technique Here</div>
<div class="assigned-tech-content">
{#if kingTech}
<div class="assigned-tech-chip">
{kingTech}
<span class="remove-tech-btn" on:click={() => kingTech = ''}>×</span>
</div>
{/if}
</div>
</div>
<div class="card-slot-footer">
<span></span>
<span class="rank">K</span>
</div>
</div>
</div>
</div>
<div class="mt-6">
<button
class="btn btn-primary btn-large w-full"
disabled={assigningFace || !jackTech || !queenTech || !kingTech}
on:click={assignFaceTechniques}
>
{assigningFace ? 'Assigning...' : 'Assign to Cards'}
</button>
</div>
</div>
{:else}
<div class="text-center italic text-gray-400">
Waiting for all players to submit techniques so they can be shuffled...
</div>
{/if}
{:else}
<div class="assigned-techs text-center mt-4">
<div class="tech-assignment-pill"><span class="card-rank">J</span> {state.player.tech_jack}</div>
<div class="tech-assignment-pill"><span class="card-rank">Q</span> {state.player.tech_queen}</div>
<div class="tech-assignment-pill"><span class="card-rank">K</span> {state.player.tech_king}</div>
<p class="waiting-box margin-top flex justify-center mt-4">
<span class="spinner-small"></span>
Ready! Waiting for other players to finish J/Q/K assignment...
</p>
</div>
{/if}
</div>
<!-- SECTION 5: Crew Creation Progress -->
<div class="card glass-panel section-crew-status" style="grid-column: span 2;">
<h3>V. Crew Creation Status</h3>
<p class="section-desc">See the creation progress of the entire crew in real time.</p>
<div class="crew-status-list">
<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 class="text-sm">
{#if p.tech_jack}
<span class="text-green-400">Ready</span>
{:else if p.created_techniques && JSON.parse(p.created_techniques).length === 3}
<span class="text-yellow-400">Assigning Face Cards</span>
{:else}
<span class="text-gray-400">Writing Sheet</span>
{/if}
</span>
</li>
{/each}
</ul>
</div>
</div>
</div>
</div>