Dead Pi-Rat reroll rework
This commit is contained in:
309
frontend/src/components/RecruitPhase.svelte
Normal file
309
frontend/src/components/RecruitPhase.svelte
Normal file
@@ -0,0 +1,309 @@
|
||||
<script>
|
||||
import { apiRequest } from '../lib/api';
|
||||
import { getSuggestion, getTechniqueSuggestion } from '../lib/suggestions';
|
||||
import { displayName } from '../lib/cards';
|
||||
import FaceCardAssigner from './FaceCardAssigner.svelte';
|
||||
|
||||
export let state;
|
||||
|
||||
function getPlayerName(id) {
|
||||
if (!id) return 'None';
|
||||
const p = state.players.find(x => x.id === id);
|
||||
return p ? displayName(p) : 'Unknown';
|
||||
}
|
||||
|
||||
function incomingTechs(p) {
|
||||
return p.incoming_techniques ? JSON.parse(p.incoming_techniques) : [];
|
||||
}
|
||||
|
||||
$: me = state.player;
|
||||
$: recruits = state.players.filter(p => p.needs_reroll);
|
||||
$: iAmRecruit = me.needs_reroll;
|
||||
|
||||
// --- My recruit sheet (only if I'm a pending recruit) ---
|
||||
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;
|
||||
|
||||
async function saveBasicDetails() {
|
||||
savingBasic = true;
|
||||
try {
|
||||
await apiRequest(`/game/${state.game.id}/player/${me.id}/save-basic`, 'POST', basicDetails);
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
savingBasic = false;
|
||||
}
|
||||
}
|
||||
|
||||
$: basicComplete = !!(me.avatar_look && me.avatar_smell && me.first_words && me.good_at_math);
|
||||
$: likeDone = !me.other_like_from_player_id || !!me.other_like;
|
||||
$: hateDone = !me.other_hate_from_player_id || !!me.other_hate;
|
||||
$: myTechs = incomingTechs(me);
|
||||
$: myTechTexts = myTechs.map(s => s.text);
|
||||
$: techsDone = myTechTexts.length === 3 && myTechTexts.every(t => t);
|
||||
|
||||
// J/Q/K assignment + finalize
|
||||
let jackTech = '', queenTech = '', kingTech = '';
|
||||
let finalizing = false;
|
||||
let finalizeError = '';
|
||||
|
||||
async function finalizeRecruit() {
|
||||
finalizing = true;
|
||||
finalizeError = '';
|
||||
try {
|
||||
await apiRequest(`/game/${state.game.id}/player/${me.id}/finalize-recruit`, 'POST', {
|
||||
jack: jackTech, queen: queenTech, king: kingTech
|
||||
});
|
||||
} catch(e) {
|
||||
finalizeError = e.message;
|
||||
} finally {
|
||||
finalizing = false;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Helping other recruits ---
|
||||
// Like/Hate answers (same endpoint as character creation)
|
||||
let inboxAnswers = {};
|
||||
async function submitDelegatedAnswer(type, targetId) {
|
||||
const answer = (inboxAnswers[`${targetId}:${type}`] || '').trim();
|
||||
if (!answer) return;
|
||||
try {
|
||||
await apiRequest(`/game/${state.game.id}/player/${me.id}/submit-delegate/${targetId}/${type}`, 'POST', { answer });
|
||||
delete inboxAnswers[`${targetId}:${type}`];
|
||||
inboxAnswers = inboxAnswers;
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Technique contributions: one draft per (recruit, slot)
|
||||
let techDrafts = {};
|
||||
let techErrors = {};
|
||||
async function contributeTechnique(recruitId, slot) {
|
||||
const text = (techDrafts[`${recruitId}:${slot}`] || '').trim();
|
||||
if (!text) return;
|
||||
try {
|
||||
await apiRequest(`/game/${state.game.id}/player/${me.id}/contribute-technique`, 'POST', {
|
||||
recruit_id: recruitId, slot, text
|
||||
});
|
||||
delete techDrafts[`${recruitId}:${slot}`];
|
||||
techDrafts = techDrafts;
|
||||
delete techErrors[`${recruitId}:${slot}`];
|
||||
techErrors = techErrors;
|
||||
} catch(e) {
|
||||
techErrors[`${recruitId}:${slot}`] = e.message;
|
||||
techErrors = techErrors;
|
||||
}
|
||||
}
|
||||
|
||||
async function suggestTechnique(recruitId, slot) {
|
||||
techDrafts[`${recruitId}:${slot}`] = await getTechniqueSuggestion(Object.values(techDrafts));
|
||||
techDrafts = techDrafts;
|
||||
}
|
||||
|
||||
// My outstanding tasks for other recruits
|
||||
$: myTasks = recruits.filter(p => p.id !== me.id).map(p => ({
|
||||
recruit: p,
|
||||
like: p.other_like_from_player_id === me.id && !p.other_like,
|
||||
hate: p.other_hate_from_player_id === me.id && !p.other_hate,
|
||||
techSlots: incomingTechs(p).map((s, i) => ({ ...s, slot: i })).filter(s => s.from_id === me.id)
|
||||
})).filter(t => t.like || t.hate || t.techSlots.length > 0);
|
||||
|
||||
function recruitProgress(p) {
|
||||
const techs = incomingTechs(p);
|
||||
const parts = [];
|
||||
parts.push(p.avatar_look && p.avatar_smell && p.first_words && p.good_at_math ? '✅ Records' : '❌ Records');
|
||||
const likeOk = !p.other_like_from_player_id || p.other_like;
|
||||
const hateOk = !p.other_hate_from_player_id || p.other_hate;
|
||||
parts.push(likeOk && hateOk ? '✅ Like/Hate' : '❌ Like/Hate');
|
||||
parts.push(`${techs.filter(s => s.text).length}/3 Techniques`);
|
||||
return parts.join(' · ');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="recruit-phase-view">
|
||||
<div class="view-header text-center">
|
||||
<h2>🐀 New Recruits</h2>
|
||||
<p>Fresh rats are washing aboard! Recruits fill in their Rat Records while the crew answers their Like/Hate questions and writes their Secret Techniques.</p>
|
||||
</div>
|
||||
|
||||
<div class="creation-grid max-w-5xl mx-auto">
|
||||
{#if iAmRecruit}
|
||||
<!-- MY NEW RECRUIT SHEET -->
|
||||
<div class="card glass-panel section-basic">
|
||||
<h3>I. Your New Rat Records {basicComplete ? '✅' : '❌'}</h3>
|
||||
{#if basicComplete}
|
||||
<div class="read-only-sheet">
|
||||
<div class="record-line"><strong>Look:</strong> {me.avatar_look}</div>
|
||||
<div class="record-line"><strong>Smell:</strong> {me.avatar_smell}</div>
|
||||
<div class="record-line"><strong>First Words:</strong> "{me.first_words}"</div>
|
||||
<div class="record-line"><strong>Good at Math?</strong> {me.good_at_math}</div>
|
||||
</div>
|
||||
{:else}
|
||||
<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? (Determines name)</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. 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>
|
||||
|
||||
<div class="card glass-panel section-delegations">
|
||||
<h3>II. Crew Delegations {likeDone && hateDone ? '✅' : '❌'}</h3>
|
||||
<p class="section-desc">Crewmates decide what their rats like and hate about your new recruit.</p>
|
||||
<div class="delegation-list">
|
||||
<div class="delegation-box glass-panel">
|
||||
<h4>What does another rat LIKE about you?</h4>
|
||||
<div class="answer-box">
|
||||
{me.other_like ? `"${me.other_like}"` : '(Waiting for answer...)'}
|
||||
</div>
|
||||
<div class="author-label">— {getPlayerName(me.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">
|
||||
{me.other_hate ? `"${me.other_hate}"` : '(Waiting for answer...)'}
|
||||
</div>
|
||||
<div class="author-label">— {getPlayerName(me.other_hate_from_player_id)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card glass-panel section-techniques" style="grid-column: span 2;">
|
||||
<h3>III. Your New Secret Techniques {techsDone ? '✅' : '❌'}</h3>
|
||||
{#if !techsDone}
|
||||
<p class="section-desc">Your crewmates are writing 3 fresh techniques for you:</p>
|
||||
<div class="submitted-techniques text-center mt-4">
|
||||
{#each myTechs as slot, i}
|
||||
<div class="tech-chip">#{i + 1}: {slot.text ? slot.text : `Waiting for ${getPlayerName(slot.from_id)}...`}</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="max-w-4xl mx-auto space-y-6">
|
||||
<p>Drag and drop your new techniques onto your Face Cards:</p>
|
||||
{#if finalizeError}
|
||||
<div class="alert alert-danger">{finalizeError}</div>
|
||||
{/if}
|
||||
<FaceCardAssigner techniques={myTechTexts} bind:jack={jackTech} bind:queen={queenTech} bind:king={kingTech} />
|
||||
<div class="mt-6">
|
||||
<button
|
||||
class="btn btn-primary btn-large w-full glow-effect"
|
||||
disabled={finalizing || !basicComplete || !likeDone || !hateDone || !jackTech || !queenTech || !kingTech}
|
||||
on:click={finalizeRecruit}
|
||||
>
|
||||
{finalizing ? 'Joining the Crew...' : '🐀 Join the Crew!'}
|
||||
</button>
|
||||
{#if !basicComplete || !likeDone || !hateDone}
|
||||
<p class="info-text text-center" style="margin-top: 0.5rem;">Finish your Rat Records and wait for your Like/Hate answers before joining.</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- TASKS FOR OTHER RECRUITS -->
|
||||
<div class="card glass-panel section-inbox" style="grid-column: span 2;">
|
||||
<h3>{iAmRecruit ? 'IV.' : 'I.'} Your Inbox (Help the Recruits)</h3>
|
||||
<p class="section-desc">Answer questions and write Secret Techniques for the fresh recruits. Be creative and have fun with it!</p>
|
||||
<div class="inbox-list">
|
||||
{#each myTasks as task (task.recruit.id)}
|
||||
{#if task.like}
|
||||
<div class="inbox-item">
|
||||
<label>What do you LIKE about {displayName(task.recruit)}?</label>
|
||||
<div class="input-row">
|
||||
<input type="text" class="input-field" bind:value={inboxAnswers[`${task.recruit.id}:like`]} placeholder="e.g. They always share their cheese"/>
|
||||
<button type="button" class="btn btn-secondary btn-small" on:click={() => inboxAnswers[`${task.recruit.id}:like`] = getSuggestion('like')}>Suggest</button>
|
||||
<button class="btn btn-primary" disabled={!(inboxAnswers[`${task.recruit.id}:like`] || '').trim()} on:click={() => submitDelegatedAnswer('like', task.recruit.id)}>Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if task.hate}
|
||||
<div class="inbox-item">
|
||||
<label>What do you HATE about {displayName(task.recruit)}?</label>
|
||||
<div class="input-row">
|
||||
<input type="text" class="input-field" bind:value={inboxAnswers[`${task.recruit.id}:hate`]} placeholder="e.g. They snore too loudly"/>
|
||||
<button type="button" class="btn btn-secondary btn-small" on:click={() => inboxAnswers[`${task.recruit.id}:hate`] = getSuggestion('hate')}>Suggest</button>
|
||||
<button class="btn btn-primary" disabled={!(inboxAnswers[`${task.recruit.id}:hate`] || '').trim()} on:click={() => submitDelegatedAnswer('hate', task.recruit.id)}>Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#each task.techSlots as slot}
|
||||
<div class="inbox-item">
|
||||
<label>
|
||||
Write a Secret Technique for {displayName(task.recruit)}
|
||||
{#if slot.text}<span class="text-success">(submitted: "{slot.text}" — you can revise it)</span>{/if}
|
||||
</label>
|
||||
{#if techErrors[`${task.recruit.id}:${slot.slot}`]}
|
||||
<div class="alert alert-danger">{techErrors[`${task.recruit.id}:${slot.slot}`]}</div>
|
||||
{/if}
|
||||
<div class="input-row">
|
||||
<input type="text" class="input-field" bind:value={techDrafts[`${task.recruit.id}:${slot.slot}`]} placeholder="e.g. Parabolic boarding bounce"/>
|
||||
<button type="button" class="btn btn-secondary btn-small" on:click={() => suggestTechnique(task.recruit.id, slot.slot)}>Suggest</button>
|
||||
<button class="btn btn-primary" disabled={!(techDrafts[`${task.recruit.id}:${slot.slot}`] || '').trim()} on:click={() => contributeTechnique(task.recruit.id, slot.slot)}>Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/each}
|
||||
{#if myTasks.length === 0}
|
||||
<p class="inbox-empty-message text-muted italic">
|
||||
{#if iAmRecruit}
|
||||
No tasks — your crewmates are hard at work on your new rat.
|
||||
{:else}
|
||||
Nothing left to write. Waiting for the recruits to finish their sheets...
|
||||
{/if}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RECRUIT PROGRESS -->
|
||||
<div class="card glass-panel section-crew-status" style="grid-column: span 2;">
|
||||
<h3>{iAmRecruit ? 'V.' : 'II.'} Recruit Progress</h3>
|
||||
<div class="crew-status-list">
|
||||
<ul class="space-y-2 mt-4">
|
||||
{#each recruits as p}
|
||||
<li class="status-row">
|
||||
<span>{displayName(p)}</span>
|
||||
<span class="text-sm text-muted">{recruitProgress(p)}</span>
|
||||
</li>
|
||||
{/each}
|
||||
{#if recruits.length === 0}
|
||||
<li class="status-row"><span class="text-success">All recruits are aboard! Heading to the next scene...</span></li>
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user