528 lines
28 KiB
Svelte
528 lines
28 KiB
Svelte
<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>
|