Improve character suggestions and serve pools from the API
This commit is contained in:
@@ -7,6 +7,17 @@
|
||||
|
||||
export let state;
|
||||
|
||||
let suggestionError = '';
|
||||
async function suggest(type, apply) {
|
||||
suggestionError = '';
|
||||
try {
|
||||
apply(await getSuggestion(type));
|
||||
} catch (error) {
|
||||
suggestionError = 'Could not load suggestions. Try again.';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Form states
|
||||
let basicDetails = {
|
||||
avatar_look: state.player.avatar_look || '',
|
||||
@@ -252,6 +263,8 @@
|
||||
|
||||
</script>
|
||||
|
||||
{#if suggestionError}<p role="alert">{suggestionError}</p>{/if}
|
||||
|
||||
<div class="character-creation-view">
|
||||
<div class="view-header text-center">
|
||||
<h2>Character Sheet Creation</h2>
|
||||
@@ -281,28 +294,28 @@
|
||||
<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">
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.avatar_look = getSuggestion('look')}>Suggest</button>{/if}
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => suggest('look', value => basicDetails.avatar_look = value)}>Suggest</button>{/if}
|
||||
</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">
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.avatar_smell = getSuggestion('smell')}>Suggest</button>{/if}
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => suggest('smell', value => basicDetails.avatar_smell = value)}>Suggest</button>{/if}
|
||||
</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">
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.first_words = getSuggestion('first_words')}>Suggest</button>{/if}
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => suggest('first_words', value => basicDetails.first_words = value)}>Suggest</button>{/if}
|
||||
</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">
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.good_at_math = getSuggestion('good_at_math')}>Suggest</button>{/if}
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => suggest('good_at_math', value => basicDetails.good_at_math = value)}>Suggest</button>{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-row mt-4">
|
||||
@@ -356,7 +369,7 @@
|
||||
</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}
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => suggest('like', value => inboxAnswers[`${p.id}:like`] = value)}>Suggest</button>{/if}
|
||||
<button class="btn btn-primary" disabled={!(inboxAnswers[`${p.id}:like`] || '').trim()} on:click={() => submitDelegatedAnswer('like', p.id)}>Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -369,7 +382,7 @@
|
||||
</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}
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => suggest('hate', value => inboxAnswers[`${p.id}:hate`] = value)}>Suggest</button>{/if}
|
||||
<button class="btn btn-primary" disabled={!(inboxAnswers[`${p.id}:hate`] || '').trim()} on:click={() => submitDelegatedAnswer('hate', p.id)}>Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,17 @@
|
||||
|
||||
export let state;
|
||||
|
||||
let suggestionError = '';
|
||||
async function suggest(type, apply) {
|
||||
suggestionError = '';
|
||||
try {
|
||||
apply(await getSuggestion(type));
|
||||
} catch (error) {
|
||||
suggestionError = 'Could not load suggestions. Try again.';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getPlayerName(id) {
|
||||
if (!id) return 'None';
|
||||
const p = state.players.find(x => x.id === id);
|
||||
@@ -137,6 +148,8 @@
|
||||
|
||||
</script>
|
||||
|
||||
{#if suggestionError}<p role="alert">{suggestionError}</p>{/if}
|
||||
|
||||
<div class="recruit-phase-view">
|
||||
<div class="view-header text-center">
|
||||
<h2>🐀 New Recruits</h2>
|
||||
@@ -162,28 +175,28 @@
|
||||
<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">
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.avatar_look = getSuggestion('look')}>Suggest</button>{/if}
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => suggest('look', value => basicDetails.avatar_look = value)}>Suggest</button>{/if}
|
||||
</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">
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.avatar_smell = getSuggestion('smell')}>Suggest</button>{/if}
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => suggest('smell', value => basicDetails.avatar_smell = value)}>Suggest</button>{/if}
|
||||
</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">
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.first_words = getSuggestion('first_words')}>Suggest</button>{/if}
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => suggest('first_words', value => basicDetails.first_words = value)}>Suggest</button>{/if}
|
||||
</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">
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.good_at_math = getSuggestion('good_at_math')}>Suggest</button>{/if}
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => suggest('good_at_math', value => basicDetails.good_at_math = value)}>Suggest</button>{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-row mt-4">
|
||||
@@ -264,7 +277,7 @@
|
||||
</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}
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => suggest('like', value => inboxAnswers[`${task.recruit.id}:like`] = value)}>Suggest</button>{/if}
|
||||
<button class="btn btn-primary" disabled={!(inboxAnswers[`${task.recruit.id}:like`] || '').trim()} on:click={() => submitDelegatedAnswer('like', task.recruit.id)}>Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -277,7 +290,7 @@
|
||||
</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}
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => suggest('hate', value => inboxAnswers[`${task.recruit.id}:hate`] = value)}>Suggest</button>{/if}
|
||||
<button class="btn btn-primary" disabled={!(inboxAnswers[`${task.recruit.id}:hate`] || '').trim()} on:click={() => submitDelegatedAnswer('hate', task.recruit.id)}>Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,10 +6,11 @@
|
||||
// - Add a CHANGELOG entry only when a commit changes something players can
|
||||
// see. Skip refactors, tests, and tooling. Keep wording player-facing.
|
||||
|
||||
export const VERSION = 46;
|
||||
export const VERSION = 47;
|
||||
|
||||
// Newest first. Each entry: { version, date: 'YYYY-MM-DD', changes: [string, ...] }.
|
||||
export const CHANGELOG = [
|
||||
{ version: 47, date: '2026-09-04', changes: ['Rewrote character suggestions with scrappy pirate looks, distinctive smells, first words, and crew quirks that fit Yeld.'] },
|
||||
{ version: 46, date: '2026-09-04', changes: ['Added a printable TL;DR rules page, linked at the top of the full rulebook.'] },
|
||||
{ version: 44, date: '2026-09-04', changes: ['Crew Objectives opens as one connected panel, with the toggle and checklist sharing a border and background.'] },
|
||||
{ version: 43, date: '2026-09-04', changes: ['The crew roster keeps players in the same order when roles or ranks change, including when someone becomes the Deep.'] },
|
||||
|
||||
+19
-1229
File diff suppressed because it is too large
Load Diff
@@ -113,18 +113,18 @@
|
||||
async function skipCharacterCreation() {
|
||||
// Suggested values for everyone's free-text fields; the backend only
|
||||
// applies them to fields players haven't filled in themselves.
|
||||
const fills = {};
|
||||
for (const p of state.players) {
|
||||
fills[p.id] = {
|
||||
avatar_look: getSuggestion('look'),
|
||||
avatar_smell: getSuggestion('smell'),
|
||||
first_words: getSuggestion('first_words'),
|
||||
good_at_math: getSuggestion('good_at_math'),
|
||||
like: getSuggestion('like'),
|
||||
hate: getSuggestion('hate'),
|
||||
};
|
||||
}
|
||||
try {
|
||||
const fills = {};
|
||||
for (const p of state.players) {
|
||||
fills[p.id] = {
|
||||
avatar_look: await getSuggestion('look'),
|
||||
avatar_smell: await getSuggestion('smell'),
|
||||
first_words: await getSuggestion('first_words'),
|
||||
good_at_math: await getSuggestion('good_at_math'),
|
||||
like: await getSuggestion('like'),
|
||||
hate: await getSuggestion('hate'),
|
||||
};
|
||||
}
|
||||
await apiRequest(`/game/${gameId}/player/${playerId}/skip-character-creation`, 'POST', {
|
||||
fills: JSON.stringify(fills)
|
||||
});
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
|
||||
const asModule = source => `data:text/javascript;base64,${Buffer.from(source).toString('base64')}`;
|
||||
const api = asModule(await readFile(new URL('../src/lib/api.js', import.meta.url), 'utf8'));
|
||||
const source = (await readFile(new URL('../src/lib/suggestions.js', import.meta.url), 'utf8'))
|
||||
.replace("'./api'", JSON.stringify(api));
|
||||
|
||||
test('suggestions retry failed requests, share the pool, and respect exclusions', async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let calls = 0;
|
||||
globalThis.fetch = async url => {
|
||||
calls++;
|
||||
if (calls === 1) throw new Error('offline');
|
||||
assert.equal(url, '/api/suggestions');
|
||||
return { ok: true, json: async () => ({ look: ['Bandana', 'Eyepatch'], smell: ['Tar'] }) };
|
||||
};
|
||||
try {
|
||||
const { getSuggestion } = await import(asModule(source));
|
||||
await assert.rejects(getSuggestion('look'), /offline/);
|
||||
assert.deepEqual(await Promise.all([
|
||||
getSuggestion('look', [' bandana ']), getSuggestion('smell')
|
||||
]), ['Eyepatch', 'Tar']);
|
||||
assert.equal(calls, 2);
|
||||
assert.equal(await getSuggestion('unknown'), '');
|
||||
assert.ok(['Bandana', 'Eyepatch'].includes(await getSuggestion('look', ['Bandana', 'Eyepatch'])));
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user