+ travel with the total. Shown only on the newest row and on
+ one whose meaning panel is open — every row, in a 9rem list,
+ used to turn three rows of history into three rows of
+ arithmetic. -->
+
+ {#each entry.parts as p, partIndex (partIndex)}
+
+{p.value} {pointKindLabels[p.kind] ?? ''}
+ {/each}
+
{/if}
{#if open}
+
+ {best > 0 ? `${t.bestScore}: ${best}` : difficultyHints[difficulty]}
+
{/each}
@@ -42,9 +47,9 @@
}
legend {
- padding: 0 0 6px;
+ padding: 0 0 var(--space-2);
font-weight: 600;
- font-size: var(--text-5);
+ font-size: var(--text-2);
}
/* auto-fit rather than three fixed columns: at 360px each of three got
@@ -87,6 +92,6 @@
.best {
color: var(--text-muted);
- font-size: var(--text-2);
+ font-size: var(--text-1);
}
diff --git a/web/src/lib/stores/settings.svelte.js b/web/src/lib/stores/settings.svelte.js
index de2fea4..b9bf430 100644
--- a/web/src/lib/stores/settings.svelte.js
+++ b/web/src/lib/stores/settings.svelte.js
@@ -11,6 +11,7 @@
const NICKNAME_KEY = 'noitu.nickname';
const THEME_KEY = 'noitu.theme';
const BEST_KEY = 'noitu.bestScores';
+const DIFFICULTY_KEY = 'noitu.lastDifficulty';
/** Mirrors the server's own cap so the input cannot promise a name it will lose. */
export const MAX_NICKNAME_LENGTH = 20;
@@ -82,6 +83,20 @@ function normalizeTheme(value) {
return value === 'dark' ? 'dark' : 'light';
}
+/**
+ * The bot difficulty last picked, from the landing screen or the quick-match
+ * queue's own nudge. `null` when nothing has been picked yet in this browser
+ * — a caller falls back to its own default rather than this store guessing
+ * one on its behalf.
+ * @returns {number | null}
+ */
+function initialDifficulty() {
+ const raw = read(DIFFICULTY_KEY, '');
+ if (raw === '') return null;
+ const value = Number(raw);
+ return Number.isFinite(value) ? value : null;
+}
+
/** Reads the theme the inline script in app.html already applied, if any. */
function initialTheme() {
const saved = read(THEME_KEY, '');
@@ -99,7 +114,9 @@ export function createSettingsStore() {
/** @type {'light' | 'dark'} */
theme: initialTheme(),
/** @type {Record} */
- bestScores: readBestScores()
+ bestScores: readBestScores(),
+ /** @type {number | null} */
+ lastDifficulty: initialDifficulty()
});
return {
@@ -128,6 +145,17 @@ export function createSettingsStore() {
this.setTheme(state.theme === 'dark' ? 'light' : 'dark');
},
+ /**
+ * Remembers the ladder rung the player last picked, so a quick-match
+ * wait offering the bot as a nudge sends them to the rung they already
+ * chose rather than back to the ladder's default.
+ * @param {number} difficulty - a Difficulty enum value
+ */
+ setLastDifficulty(difficulty) {
+ state.lastDifficulty = difficulty;
+ write(DIFFICULTY_KEY, String(difficulty));
+ },
+
/**
* @param {number|string} difficulty - a Difficulty enum value
* @returns {number}
diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte
index 7e4cd1c..928a3ae 100644
--- a/web/src/routes/+page.svelte
+++ b/web/src/routes/+page.svelte
@@ -3,9 +3,17 @@
import DifficultyPicker from '$lib/components/DifficultyPicker.svelte';
import NicknameInput from '$lib/components/NicknameInput.svelte';
import { Difficulty } from '$lib/proto/noitu/v1/game_pb.js';
- import { t } from '$lib/i18n/vi.js';
+ import { fill, t } from '$lib/i18n/vi.js';
+ import { settings } from '$lib/stores/settings.svelte.js';
- let difficulty = $state(Difficulty.MEDIUM);
+ // Resumes the rung last played rather than always opening on Medium, so
+ // choosing a difficulty is not a step repeated every visit.
+ let difficulty = $state(settings.state.lastDifficulty ?? Difficulty.MEDIUM);
+
+ /** @param {number} value */
+ function selectDifficulty(value) {
+ settings.setLastDifficulty(value);
+ }
function playBot() {
goto(`/play?difficulty=${difficulty}`);
@@ -21,8 +29,14 @@
screen reader has something to land on. -->