feat(ui): offline-first foundations — bundled dicts + local-guest identity
The additive groundwork for the native offline-first experience (ANDROID_PLAN.md §D). Inert until the native cold-boot lands: on the web these paths never fire, so web / VK / Telegram behaviour is unchanged. - ui/scripts/bundle-dicts.mjs copies the scrabble-dictionary release DAWGs into dist/dict/<variant>@<version>.dawg for the native pipeline (run after `pnpm build`, before `cap sync`). - The dict loader gains a bundled tier between the IndexedDB and network tiers, attempted only on a native channel (a packaged app serves ./dict/<key>.dawg from its assets). Native-gated rather than the plan's "404 on the web" because the relative path would otherwise hit the gateway's own session-gated /dict/ route. - __DICT_VERSION__ Vite define (from VITE_DICT_VERSION, default "dev") + its ambient declaration; the offline vs_ai / hotseat creates in NewGame fall back to it when a device-local guest has no profile-advertised version. - New lib/localguest.ts: a persisted device-local guest id (no DB row); the offline vs_ai human seat uses it (and the localized common.guest name) when there is no server session. svelte-check clean, vitest green, native + web builds clean. The cold-boot rewrite, reconciliation, the Profile soft-sign-in gating and the offline-first e2e follow.
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
import { offlineMode } from '../lib/offline.svelte';
|
||||
import { localSource } from '../lib/gamesource';
|
||||
import { newLocalGameId, randomSeed } from '../lib/localgame/id';
|
||||
import { localGuestId } from '../lib/localguest';
|
||||
import { navigate } from '../lib/router.svelte';
|
||||
import { t, type MessageKey } from '../lib/i18n/index.svelte';
|
||||
import { validDisplayName } from '../lib/profileValidation';
|
||||
@@ -78,10 +79,11 @@
|
||||
starting = true;
|
||||
try {
|
||||
// Offline mode: create a device-local vs_ai game instead of enqueuing on the backend. The
|
||||
// dictionary version comes from the profile (advertised for exactly this), and the bag is
|
||||
// seeded locally; the same game screen then drives it through the local source.
|
||||
// dictionary version comes from the profile (advertised for exactly this) or, for a device-local
|
||||
// guest with no profile yet, the bundled __DICT_VERSION__; the bag is seeded locally and the same
|
||||
// game screen then drives it through the local source.
|
||||
if (offlineMode.active) {
|
||||
const version = app.profile?.dictVersions?.[v];
|
||||
const version = app.profile?.dictVersions?.[v] ?? __DICT_VERSION__;
|
||||
if (!version) {
|
||||
handleError(new Error('dict_unavailable'));
|
||||
return;
|
||||
@@ -94,9 +96,10 @@
|
||||
seed: randomSeed(),
|
||||
multipleWords: multipleWordsForRequest(v, multipleWords),
|
||||
seats: [
|
||||
// The human seat carries the real account id so the game screen and the lobby identify
|
||||
// "you" and the correct turn exactly as for an online game (the robot uses a synthetic id).
|
||||
{ kind: 'human', name: app.profile?.displayName ?? 'You', accountId: app.session?.userId },
|
||||
// The human seat carries the account id — the server session's, or a device-local guest id
|
||||
// when there is no session yet — so the game screen and the lobby identify "you" and the
|
||||
// correct turn exactly as for an online game (the robot uses a synthetic id).
|
||||
{ kind: 'human', name: app.profile?.displayName ?? t('common.guest'), accountId: app.session?.userId ?? localGuestId() },
|
||||
{ kind: 'robot', name: 'Robot' },
|
||||
],
|
||||
});
|
||||
@@ -274,7 +277,7 @@
|
||||
if (starting || !hostPin || !inviteVariant || !rosterReady(rows)) return;
|
||||
starting = true;
|
||||
try {
|
||||
const version = app.profile?.dictVersions?.[inviteVariant];
|
||||
const version = app.profile?.dictVersions?.[inviteVariant] ?? __DICT_VERSION__;
|
||||
if (!version) {
|
||||
handleError(new Error('dict_unavailable'));
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user