Technique swapping implementation

This commit is contained in:
2026-06-12 16:46:03 -07:00
parent 121f2f7498
commit fa37cffe47
10 changed files with 724 additions and 128 deletions

View File

@@ -66,6 +66,41 @@
}
}
// Technique swapping (swap_techniques phase)
let offerTech = '', offerTargetId = '';
let acceptChoice = {};
let swapBusy = false;
let swapError = '';
async function swapApi(path, body) {
swapBusy = true;
swapError = '';
try {
await apiRequest(`/game/${state.game.id}/player/${state.player.id}/${path}`, 'POST', body);
} catch (e) {
swapError = e.message;
} finally {
swapBusy = false;
}
}
function offerSwap() {
swapApi('offer-swap', { target_player_id: offerTargetId, technique: offerTech });
}
function cancelOffer() {
swapApi('cancel-swap-offer');
}
function respondOffer(offererId, accept) {
swapApi('respond-swap-offer', {
offerer_player_id: offererId,
accept,
technique: accept ? (acceptChoice[offererId] || '') : ''
});
}
function setSwapReady(ready) {
swapApi('swap-ready', { ready });
}
// Face Techniques
let jackTech = '', queenTech = '', kingTech = '';
let assigningFace = false;
@@ -103,9 +138,16 @@
}
// Check completion status
$: phase = state.game.phase;
$: createdTechniques = state.player.created_techniques ? JSON.parse(state.player.created_techniques) : [];
$: heldTechniques = state.player.held_techniques ? JSON.parse(state.player.held_techniques) : [];
$: swappedTechniques = state.player.swapped_techniques ? JSON.parse(state.player.swapped_techniques) : [];
$: ownHeldCount = heldTechniques.filter(t => t.creator_id === state.player.id).length;
$: incomingOffers = state.players.filter(p => p.id !== state.player.id && p.swap_offer_to_id === state.player.id);
$: swapTargets = state.players.filter(p => p.id !== state.player.id && !p.needs_reroll);
$: offerTechCreator = (heldTechniques.find(t => t.text === offerTech) || {}).creator_id;
$: offerInvalid = !offerTech || !offerTargetId || offerTechCreator === offerTargetId;
$: 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);
@@ -237,7 +279,9 @@
<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}
{#if state.player.needs_reroll}
<p class="text-muted italic">You're spectating for now — you'll create your Pi-Rat with the crew between scenes.</p>
{:else if phase === 'character_creation' && createdTechniques.length === 0}
<p class="section-desc">Create 3 techniques that you will swap with other players. Make them cool or ridiculous!</p>
{#if techError}
<div class="alert alert-danger">{techError}</div>
@@ -266,42 +310,123 @@
</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}
{:else if phase === 'character_creation'}
<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.
Techniques submitted! Once everyone submits, you'll trade them all away in blind swaps.
</p>
</div>
{:else if !techniquesComplete}
{#if swappedTechniques.length === 3}
<div class="max-w-4xl mx-auto space-y-6">
<p>Drag and drop your assigned techniques onto your Face Cards:</p>
{#if faceError}
<div class="alert alert-danger">{faceError}</div>
{/if}
{:else if phase === 'swap_techniques'}
<p class="section-desc">
Trade away all 3 techniques you created by offering blind swaps to your crewmates.
They only learn that you offered a swap never which technique is on the table!
You can't offer a technique to the rat who wrote it.
</p>
{#if swapError}
<div class="alert alert-danger">{swapError}</div>
{/if}
<FaceCardAssigner techniques={swappedTechniques} bind:jack={jackTech} bind:queen={queenTech} bind:king={kingTech} />
<h4>Your techniques in hand</h4>
<div class="submitted-techniques mt-4">
{#each heldTechniques as t}
<div class="tech-chip">
{t.text}
{#if t.creator_id === state.player.id}
<span class="text-warning"> yours, swap it away!</span>
{:else}
<span class="text-success"> from {getPlayerName(t.creator_id)}</span>
{/if}
</div>
{/each}
</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>
{#if state.player.swap_offer_to_id}
<div class="delegation-box glass-panel mt-4">
<p>
You offered <strong>"{state.player.swap_offer_technique}"</strong> to
{getPlayerName(state.player.swap_offer_to_id)}. Waiting for their answer...
</p>
<button class="btn btn-secondary mt-4" disabled={swapBusy} on:click={cancelOffer}>Withdraw Offer</button>
</div>
{:else if !state.player.is_ready}
<div class="delegation-box glass-panel mt-4">
<h4>Offer a swap</h4>
<div class="input-row">
<select class="input-field" bind:value={offerTech}>
<option value="" disabled>Pick a technique to give...</option>
{#each heldTechniques as t}
<option value={t.text}>{t.text}</option>
{/each}
</select>
<select class="input-field" bind:value={offerTargetId}>
<option value="" disabled>Pick a crewmate...</option>
{#each swapTargets as p}
<option value={p.id} disabled={offerTechCreator === p.id}>
{displayName(p)}{offerTechCreator === p.id ? ' (wrote it!)' : ''}
</option>
{/each}
</select>
<button class="btn btn-primary" disabled={swapBusy || offerInvalid} on:click={offerSwap}>Offer Swap</button>
</div>
</div>
{:else}
<div class="text-center italic text-muted">
Waiting for all players to submit techniques so they can be shuffled...
</div>
{/if}
{#each incomingOffers as offerer (offerer.id)}
<div class="delegation-box glass-panel mt-4">
<h4>📨 {displayName(offerer)} offered you a mystery technique!</h4>
<p class="text-sm text-muted">Choose one of your techniques to trade back (not one they wrote):</p>
<div class="input-row">
<select class="input-field" bind:value={acceptChoice[offerer.id]}>
<option value="" disabled selected>Pick a technique to give back...</option>
{#each heldTechniques as t}
<option value={t.text} disabled={t.creator_id === offerer.id}>
{t.text}{t.creator_id === offerer.id ? ' (they wrote it!)' : ''}
</option>
{/each}
</select>
<button class="btn btn-primary" disabled={swapBusy || !acceptChoice[offerer.id]} on:click={() => respondOffer(offerer.id, true)}>Accept</button>
<button class="btn btn-secondary" disabled={swapBusy} on:click={() => respondOffer(offerer.id, false)}>Decline</button>
</div>
</div>
{/each}
<div class="text-center mt-6">
{#if state.player.is_ready}
<p class="waiting-box flex justify-center">
<span class="spinner-small"></span>
Done swapping! Waiting for the rest of the crew...
</p>
<button class="btn btn-secondary mt-4" disabled={swapBusy} on:click={() => setSwapReady(false)}>Wait, I want to keep swapping</button>
{:else if ownHeldCount > 0}
<p class="text-muted italic">Swap away {ownHeldCount} more of your own technique{ownHeldCount === 1 ? '' : 's'} before you can ready up.</p>
{:else}
<button class="btn btn-primary btn-large" disabled={swapBusy || !!state.player.swap_offer_to_id} on:click={() => setSwapReady(true)}>I'm done swapping</button>
{/if}
</div>
{:else if !techniquesComplete}
<div class="max-w-4xl mx-auto space-y-6">
<p>Drag and drop your assigned techniques onto your Face Cards:</p>
{#if faceError}
<div class="alert alert-danger">{faceError}</div>
{/if}
<FaceCardAssigner techniques={swappedTechniques} bind:jack={jackTech} bind:queen={queenTech} bind:king={kingTech} />
<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="assigned-techs text-center mt-4">
<div class="tech-assignment-pill"><span class="card-rank">J</span> {state.player.tech_jack}</div>
@@ -325,10 +450,21 @@
<li class="status-row {p.tech_jack ? 'is-ready' : ''}">
<span>{displayName(p)}</span>
<span class="text-sm">
{#if p.tech_jack}
{#if p.needs_reroll}
<span class="text-muted">Spectating</span>
{:else if phase === 'swap_techniques'}
{#if p.is_ready}
<span class="text-success">Done Swapping</span>
{:else}
{@const ownLeft = JSON.parse(p.held_techniques || '[]').filter(t => t.creator_id === p.id).length}
<span class="text-warning">Swapping ({ownLeft} of their own left)</span>
{/if}
{:else if p.tech_jack}
<span class="text-success">Ready</span>
{:else if p.created_techniques && JSON.parse(p.created_techniques).length === 3}
{:else if phase === 'assign_techniques'}
<span class="text-warning">Assigning Face Cards</span>
{:else if p.created_techniques && JSON.parse(p.created_techniques).length === 3}
<span class="text-warning">Techniques Submitted</span>
{:else}
<span class="text-muted">Writing Sheet</span>
{/if}