Dead Pi-Rat reroll rework

This commit is contained in:
2026-06-12 11:32:15 -07:00
parent df7a0cc83f
commit f61201144f
18 changed files with 908 additions and 460 deletions

View 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>