Uncommit feature in character creation
Players can now unlock and revise any reasonably revisable choice while character creation is ongoing: basic Rat Records (also in recruit creation), submitted techniques (until the swap phase begins), J/Q/K face-card assignments (until everyone is ready), and Like/Hate answers written for crewmates. New endpoints unsubmit-techniques and unassign-face-techniques clear the locked-in state server-side so the phase machine waits for the revision. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,11 +16,13 @@
|
||||
};
|
||||
|
||||
let savingBasic = false;
|
||||
let editingBasic = false;
|
||||
|
||||
async function saveBasicDetails() {
|
||||
savingBasic = true;
|
||||
try {
|
||||
await apiRequest(`/game/${state.game.id}/player/${state.player.id}/save-basic`, 'POST', basicDetails);
|
||||
editingBasic = false;
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
@@ -28,6 +30,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
function reviseBasicDetails() {
|
||||
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 || ''
|
||||
};
|
||||
editingBasic = true;
|
||||
}
|
||||
|
||||
// Like/Hate tasks are assigned automatically when character creation starts.
|
||||
// This is a fallback for games that entered the phase before assignments existed.
|
||||
onMount(async () => {
|
||||
@@ -66,6 +78,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function reviseTechniques() {
|
||||
[tech1 = '', tech2 = '', tech3 = ''] = createdTechniques;
|
||||
techError = '';
|
||||
try {
|
||||
await apiRequest(`/game/${state.game.id}/player/${state.player.id}/unsubmit-techniques`, 'POST');
|
||||
} catch(e) {
|
||||
techError = e.message;
|
||||
}
|
||||
}
|
||||
|
||||
// Technique swapping (swap_techniques phase)
|
||||
let offerTech = '', offerTargetId = '';
|
||||
let acceptChoice = {};
|
||||
@@ -122,6 +144,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function reviseFaceTechniques() {
|
||||
jackTech = state.player.tech_jack;
|
||||
queenTech = state.player.tech_queen;
|
||||
kingTech = state.player.tech_king;
|
||||
faceError = '';
|
||||
try {
|
||||
await apiRequest(`/game/${state.game.id}/player/${state.player.id}/unassign-face-techniques`, 'POST');
|
||||
} catch(e) {
|
||||
faceError = e.message;
|
||||
}
|
||||
}
|
||||
|
||||
// Submit delegated answers — one draft per (player, question) so editing one
|
||||
// task never bleeds into another.
|
||||
let inboxAnswers = {};
|
||||
@@ -171,7 +205,7 @@
|
||||
<div class="card glass-panel section-basic">
|
||||
<h3>I. Basic Rat Records {basicComplete ? '✅' : '❌'}</h3>
|
||||
|
||||
{#if basicComplete}
|
||||
{#if basicComplete && !editingBasic}
|
||||
<!-- Saved View -->
|
||||
<div class="read-only-sheet">
|
||||
<div class="record-line"><strong>Look:</strong> {state.player.avatar_look}</div>
|
||||
@@ -179,6 +213,7 @@
|
||||
<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>
|
||||
<button type="button" class="btn btn-secondary btn-small mt-4" on:click={reviseBasicDetails}>✏️ Revise Records</button>
|
||||
{:else}
|
||||
<!-- Form View -->
|
||||
<form on:submit|preventDefault={saveBasicDetails} class="creation-form">
|
||||
@@ -248,9 +283,12 @@
|
||||
<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}
|
||||
{#if p.other_like_from_player_id === state.player.id}
|
||||
<div class="inbox-item">
|
||||
<label>What do you LIKE about {displayName(p)}?</label>
|
||||
<label>
|
||||
What do you LIKE about {displayName(p)}?
|
||||
{#if p.other_like}<span class="text-success">(submitted: "{p.other_like}" — you can revise it)</span>{/if}
|
||||
</label>
|
||||
<div class="input-row">
|
||||
<input type="text" class="input-field" bind:value={inboxAnswers[`${p.id}:like`]} placeholder="e.g. They always share their cheese"/>
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => inboxAnswers[`${p.id}:like`] = getSuggestion('like')}>Suggest</button>{/if}
|
||||
@@ -258,9 +296,12 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if p.other_hate_from_player_id === state.player.id && !p.other_hate}
|
||||
{#if p.other_hate_from_player_id === state.player.id}
|
||||
<div class="inbox-item">
|
||||
<label>What do you HATE about {displayName(p)}?</label>
|
||||
<label>
|
||||
What do you HATE about {displayName(p)}?
|
||||
{#if p.other_hate}<span class="text-success">(submitted: "{p.other_hate}" — you can revise it)</span>{/if}
|
||||
</label>
|
||||
<div class="input-row">
|
||||
<input type="text" class="input-field" bind:value={inboxAnswers[`${p.id}:hate`]} placeholder="e.g. They snore too loudly"/>
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => inboxAnswers[`${p.id}:hate`] = getSuggestion('hate')}>Suggest</button>{/if}
|
||||
@@ -269,7 +310,7 @@
|
||||
</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}
|
||||
{#if state.players.filter(p => p.other_like_from_player_id === state.player.id || p.other_hate_from_player_id === state.player.id).length === 0}
|
||||
<p class="inbox-empty-message text-muted italic">No pending tasks! Wait for other players to assign you tasks.</p>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -312,6 +353,9 @@
|
||||
</form>
|
||||
{:else if phase === 'character_creation'}
|
||||
<div class="submitted-techniques text-center mt-4">
|
||||
{#if techError}
|
||||
<div class="alert alert-danger">{techError}</div>
|
||||
{/if}
|
||||
<div class="tech-chip">#1: {createdTechniques[0]}</div>
|
||||
<div class="tech-chip">#2: {createdTechniques[1]}</div>
|
||||
<div class="tech-chip">#3: {createdTechniques[2]}</div>
|
||||
@@ -319,6 +363,7 @@
|
||||
<span class="spinner-small"></span>
|
||||
Techniques submitted! Once everyone submits, you'll trade them all away in blind swaps.
|
||||
</p>
|
||||
<button type="button" class="btn btn-secondary btn-small mt-4" on:click={reviseTechniques}>✏️ Revise Techniques</button>
|
||||
</div>
|
||||
{:else if phase === 'swap_techniques'}
|
||||
<p class="section-desc">
|
||||
@@ -429,6 +474,9 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class="assigned-techs text-center mt-4">
|
||||
{#if faceError}
|
||||
<div class="alert alert-danger">{faceError}</div>
|
||||
{/if}
|
||||
<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>
|
||||
@@ -436,6 +484,9 @@
|
||||
<span class="spinner-small"></span>
|
||||
Ready! Waiting for other players to finish J/Q/K assignment...
|
||||
</p>
|
||||
{#if phase === 'assign_techniques'}
|
||||
<button type="button" class="btn btn-secondary btn-small mt-4" on:click={reviseFaceTechniques}>✏️ Revise Assignment</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -32,11 +32,13 @@
|
||||
good_at_math: state.player.good_at_math || ''
|
||||
};
|
||||
let savingBasic = false;
|
||||
let editingBasic = false;
|
||||
|
||||
async function saveBasicDetails() {
|
||||
savingBasic = true;
|
||||
try {
|
||||
await apiRequest(`/game/${state.game.id}/player/${me.id}/save-basic`, 'POST', basicDetails);
|
||||
editingBasic = false;
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
@@ -44,6 +46,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
function reviseBasicDetails() {
|
||||
basicDetails = {
|
||||
avatar_look: me.avatar_look || '',
|
||||
avatar_smell: me.avatar_smell || '',
|
||||
first_words: me.first_words || '',
|
||||
good_at_math: me.good_at_math || ''
|
||||
};
|
||||
editingBasic = true;
|
||||
}
|
||||
|
||||
$: 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;
|
||||
@@ -113,8 +125,8 @@
|
||||
// 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,
|
||||
like: p.other_like_from_player_id === me.id,
|
||||
hate: p.other_hate_from_player_id === me.id,
|
||||
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);
|
||||
|
||||
@@ -141,13 +153,14 @@
|
||||
<!-- MY NEW RECRUIT SHEET -->
|
||||
<div class="card glass-panel section-basic">
|
||||
<h3>I. Your New Rat Records {basicComplete ? '✅' : '❌'}</h3>
|
||||
{#if basicComplete}
|
||||
{#if basicComplete && !editingBasic}
|
||||
<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>
|
||||
<button type="button" class="btn btn-secondary btn-small mt-4" on:click={reviseBasicDetails}>✏️ Revise Records</button>
|
||||
{:else}
|
||||
<form on:submit|preventDefault={saveBasicDetails} class="creation-form">
|
||||
<div class="form-group">
|
||||
@@ -245,7 +258,10 @@
|
||||
{#each myTasks as task (task.recruit.id)}
|
||||
{#if task.like}
|
||||
<div class="inbox-item">
|
||||
<label>What do you LIKE about {displayName(task.recruit)}?</label>
|
||||
<label>
|
||||
What do you LIKE about {displayName(task.recruit)}?
|
||||
{#if task.recruit.other_like}<span class="text-success">(submitted: "{task.recruit.other_like}" — you can revise it)</span>{/if}
|
||||
</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"/>
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => inboxAnswers[`${task.recruit.id}:like`] = getSuggestion('like')}>Suggest</button>{/if}
|
||||
@@ -255,7 +271,10 @@
|
||||
{/if}
|
||||
{#if task.hate}
|
||||
<div class="inbox-item">
|
||||
<label>What do you HATE about {displayName(task.recruit)}?</label>
|
||||
<label>
|
||||
What do you HATE about {displayName(task.recruit)}?
|
||||
{#if task.recruit.other_hate}<span class="text-success">(submitted: "{task.recruit.other_hate}" — you can revise it)</span>{/if}
|
||||
</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"/>
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => inboxAnswers[`${task.recruit.id}:hate`] = getSuggestion('hate')}>Suggest</button>{/if}
|
||||
|
||||
Reference in New Issue
Block a user