feat(ui): tile-crossword loading splash for cold lobby open
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 57s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m17s
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 57s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m17s
On a cold app open the lobby's game list arrives over the network; on a slow link the empty "no games yet" line flashed before the games loaded. Add a full-screen tile splash that lays a Scrabble crossword of ЭРУДИТ / ЗАГРУЗКА / ОЖИДАНИЕ (Эрудит point values, hardcoded since the alphabet table is not cached at boot) until the lobby's first load settles, then removes itself to reveal the populated list. - lib/splash.ts: pure layout + reveal schedule (unit-tested). - components/Splash.svelte: App-level overlay; per-tile drop-in; loops ЗАГРУЗКА → ОЖИДАНИЕ until ready, dismisses on a word boundary. Static ЭРУДИТ under reduced motion / the mock build. - app state: lobbyReady (set by Lobby on first settle) + splashDone, reset on logout. - App.svelte: overlay while routeIsLobby && !splashDone; the plain text splash now only covers non-lobby deep-links during bootstrap. - docs: UI_DESIGN + FUNCTIONAL (+ _ru).
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
// Pure model for the cold-start loading splash: a small Scrabble crossword that lays the
|
||||
// words ЭРУДИТ / ЗАГРУЗКА / ОЖИДАНИЕ out of tiles, letter by letter, until the lobby is
|
||||
// ready. Kept free of any DOM or reactive state so it unit-tests in the node environment;
|
||||
// components/Splash.svelte wraps it with the reactive reveal set and the timer driver.
|
||||
//
|
||||
// The tile point values are hardcoded from the Эрудит ruleset (scrabble-solver
|
||||
// rules.Erudit). They cannot come from alphabet.valueForLetter() here: the splash renders on
|
||||
// a cold start, before the server has sent the variant's alphabet table that lookup reads.
|
||||
|
||||
/**
|
||||
* ERUDIT_VALUES is the per-letter point value (вес) for every letter the splash words use,
|
||||
* taken from the Эрудит ruleset. Letters are upper-cased, matching the rest of the UI.
|
||||
*/
|
||||
export const ERUDIT_VALUES: Record<string, number> = {
|
||||
А: 1,
|
||||
Г: 3,
|
||||
Д: 2,
|
||||
Е: 1,
|
||||
Ж: 5,
|
||||
З: 5,
|
||||
И: 1,
|
||||
К: 2,
|
||||
Н: 1,
|
||||
О: 1,
|
||||
Р: 2,
|
||||
Т: 2,
|
||||
У: 3,
|
||||
Э: 10,
|
||||
};
|
||||
|
||||
/** SplashWord names the three crossword words, used to group tiles for the reveal sequence. */
|
||||
export type SplashWord = 'erudit' | 'zagruzka' | 'ozhidanie';
|
||||
|
||||
/** SplashTile is one placed tile on the 6×8 splash grid. */
|
||||
export interface SplashTile {
|
||||
/** row is the grid row, 0..7 (ЭРУДИТ sits on row 3). */
|
||||
row: number;
|
||||
/** col is the grid column, 0..5. */
|
||||
col: number;
|
||||
/** letter is the upper-cased Cyrillic glyph shown on the tile. */
|
||||
letter: string;
|
||||
/** value is the tile's Эрудит point value. */
|
||||
value: number;
|
||||
/** word groups the tile by its crossword word. */
|
||||
word: SplashWord;
|
||||
/** key is the stable `"row-col"` identifier used by the renderer's reveal set. */
|
||||
key: string;
|
||||
}
|
||||
|
||||
/** COLS and ROWS are the splash grid dimensions (6 wide, 8 tall). */
|
||||
export const COLS = 6;
|
||||
export const ROWS = 8;
|
||||
|
||||
function tile(row: number, col: number, letter: string, word: SplashWord): SplashTile {
|
||||
return { row, col, letter, value: ERUDIT_VALUES[letter] ?? 0, word, key: `${row}-${col}` };
|
||||
}
|
||||
|
||||
// ЭРУДИТ — horizontal on row 3, columns 0..5 (laid left → right).
|
||||
const ERUDIT_TILES: SplashTile[] = [...'ЭРУДИТ'].map((l, i) => tile(3, i, l, 'erudit'));
|
||||
|
||||
// ЗАГРУЗКА — vertical in column 1, rows 0..7 (laid top → bottom). Its row-3 letter (Р) is the
|
||||
// tile already placed by ЭРУДИТ, so it is dropped here ("через имеющиеся буквы").
|
||||
const ZAGRUZKA_TILES: SplashTile[] = [...'ЗАГРУЗКА']
|
||||
.map((l, r) => tile(r, 1, l, 'zagruzka'))
|
||||
.filter((t) => t.row !== 3);
|
||||
|
||||
// ОЖИДАНИЕ — vertical in column 3, rows 0..7. Its row-3 letter (Д) is shared with ЭРУДИТ and
|
||||
// likewise dropped.
|
||||
const OZHIDANIE_TILES: SplashTile[] = [...'ОЖИДАНИЕ']
|
||||
.map((l, r) => tile(r, 3, l, 'ozhidanie'))
|
||||
.filter((t) => t.row !== 3);
|
||||
|
||||
/** SPLASH_TILES is every unique tile on the grid (ЭРУДИТ owns the two shared crossings). */
|
||||
export const SPLASH_TILES: SplashTile[] = [...ERUDIT_TILES, ...ZAGRUZKA_TILES, ...OZHIDANIE_TILES];
|
||||
|
||||
/** ERUDIT_KEYS is the set of ЭРУДИТ tile keys — the tiles kept on screen across a clear. */
|
||||
export const ERUDIT_KEYS: ReadonlySet<string> = new Set(ERUDIT_TILES.map((t) => t.key));
|
||||
|
||||
/** WORD_MS is the time to lay one whole word; PAUSE_MS the gap between words. */
|
||||
export const WORD_MS = 1000;
|
||||
export const PAUSE_MS = 250;
|
||||
|
||||
/** SplashStep is one scheduled action: reveal a tile, check whether the lobby is ready (and
|
||||
* if so dismiss), or clear every non-ЭРУДИТ tile before the cycle repeats. atMs is relative
|
||||
* to the start of its segment (prefix or cycle). */
|
||||
export interface SplashStep {
|
||||
atMs: number;
|
||||
kind: 'reveal' | 'check' | 'clear';
|
||||
/** key is the tile to reveal, set only on a 'reveal' step. */
|
||||
key?: string;
|
||||
}
|
||||
|
||||
/** SplashSchedule is the timed plan the renderer drives: the one-time prefix (ЭРУДИТ) and the
|
||||
* repeating cycle (ЗАГРУЗКА → ОЖИДАНИЕ → clear). The driver plays the prefix once, then loops
|
||||
* the cycle every cycleMs until the splash is dismissed. */
|
||||
export interface SplashSchedule {
|
||||
prefix: SplashStep[];
|
||||
cycle: SplashStep[];
|
||||
prefixMs: number;
|
||||
cycleMs: number;
|
||||
}
|
||||
|
||||
function revealSteps(tiles: SplashTile[], base: number, perTile: number): SplashStep[] {
|
||||
return tiles.map((t, i) => ({ atMs: Math.round(base + i * perTile), kind: 'reveal', key: t.key }));
|
||||
}
|
||||
|
||||
/**
|
||||
* splashSchedule builds the reveal plan. Each word is laid over wordMs (so its per-tile
|
||||
* stagger is wordMs divided by the word's tile count), a readiness check lands at every word
|
||||
* boundary, and pauseMs separates the words; after ОЖИДАНИЕ the cycle clears the two vertical
|
||||
* words and restarts (ЭРУДИТ persists). wordMs and pauseMs are overridable for tuning/tests.
|
||||
*/
|
||||
export function splashSchedule(opts: { wordMs?: number; pauseMs?: number } = {}): SplashSchedule {
|
||||
const wordMs = opts.wordMs ?? WORD_MS;
|
||||
const pauseMs = opts.pauseMs ?? PAUSE_MS;
|
||||
|
||||
// Prefix: ЭРУДИТ left → right, the word completing at wordMs, then a readiness check.
|
||||
const prefix: SplashStep[] = [
|
||||
...revealSteps(ERUDIT_TILES, 0, wordMs / ERUDIT_TILES.length),
|
||||
{ atMs: wordMs, kind: 'check' },
|
||||
];
|
||||
|
||||
// Cycle: pause, ЗАГРУЗКА, check, pause, ОЖИДАНИЕ, check, pause, clear. The leading pause is
|
||||
// the gap after ЭРУДИТ (and, on repeat, after the previous cycle's clear).
|
||||
const sV = wordMs / ZAGRUZKA_TILES.length;
|
||||
const zBase = pauseMs;
|
||||
const oBase = pauseMs + wordMs + pauseMs;
|
||||
const clearAt = oBase + wordMs + pauseMs;
|
||||
const cycle: SplashStep[] = [
|
||||
...revealSteps(ZAGRUZKA_TILES, zBase, sV),
|
||||
{ atMs: zBase + wordMs, kind: 'check' },
|
||||
...revealSteps(OZHIDANIE_TILES, oBase, sV),
|
||||
{ atMs: oBase + wordMs, kind: 'check' },
|
||||
{ atMs: clearAt, kind: 'clear' },
|
||||
];
|
||||
|
||||
return { prefix, cycle, prefixMs: wordMs, cycleMs: clearAt };
|
||||
}
|
||||
Reference in New Issue
Block a user