feat(ui): тост «Ваш ход» называет предыдущего игрока #62
@@ -210,7 +210,10 @@ function openStream(): void {
|
||||
} else if (e.kind === 'nudge') {
|
||||
showToast(t('chat.nudge'), 'info');
|
||||
} else if (e.kind === 'your_turn') {
|
||||
showToast(t('game.yourTurn'), 'info');
|
||||
// Name the player who moved just before this one (the previous seat in turn order), so the
|
||||
// toast reads the same in games with any number of players. Fall back to the bare label when
|
||||
// no name is present (an older peer, or a gap event without one).
|
||||
showToast(e.opponentName ? t('game.yourTurnBy', { name: e.opponentName }) : t('game.yourTurn'), 'info');
|
||||
} else if (e.kind === 'opponent_moved' || e.kind === 'game_over') {
|
||||
// Keep both caches fresh from any screen, so neither the lobby nor the game flashes a stale
|
||||
// frame on the next navigation. The lobby snapshot is patched from the event's own GameView
|
||||
|
||||
@@ -300,6 +300,39 @@ describe('codec', () => {
|
||||
expect(inv.variant).toBe('scrabble_en');
|
||||
});
|
||||
|
||||
it('decodes a your_turn event, carrying the previous player name for the toast', () => {
|
||||
const b = new Builder(64);
|
||||
const gid = b.createString('g-1');
|
||||
const name = b.createString('Ann');
|
||||
fb.YourTurnEvent.startYourTurnEvent(b);
|
||||
fb.YourTurnEvent.addGameId(b, gid);
|
||||
fb.YourTurnEvent.addOpponentName(b, name);
|
||||
fb.YourTurnEvent.addMoveCount(b, 7);
|
||||
b.finish(fb.YourTurnEvent.endYourTurnEvent(b));
|
||||
expect(decodeEvent('your_turn', b.asUint8Array())).toEqual({
|
||||
kind: 'your_turn',
|
||||
gameId: 'g-1',
|
||||
deadlineUnix: 0,
|
||||
opponentName: 'Ann',
|
||||
moveCount: 7,
|
||||
});
|
||||
});
|
||||
|
||||
it('decodes a your_turn event with no opponent name as an empty string', () => {
|
||||
const b = new Builder(64);
|
||||
const gid = b.createString('g-2');
|
||||
fb.YourTurnEvent.startYourTurnEvent(b);
|
||||
fb.YourTurnEvent.addGameId(b, gid);
|
||||
b.finish(fb.YourTurnEvent.endYourTurnEvent(b));
|
||||
expect(decodeEvent('your_turn', b.asUint8Array())).toEqual({
|
||||
kind: 'your_turn',
|
||||
gameId: 'g-2',
|
||||
deadlineUnix: 0,
|
||||
opponentName: '',
|
||||
moveCount: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('decodes an opponent_joined event (reusing the match_found payload layout)', () => {
|
||||
const b = new Builder(64);
|
||||
const gid = b.createString('g-open');
|
||||
|
||||
+1
-1
@@ -442,7 +442,7 @@ export function decodeEvent(kind: string, payload: Uint8Array): PushEvent | null
|
||||
switch (kind) {
|
||||
case 'your_turn': {
|
||||
const e = fb.YourTurnEvent.getRootAsYourTurnEvent(bb);
|
||||
return { kind: 'your_turn', gameId: s(e.gameId()), deadlineUnix: Number(e.deadlineUnix()), moveCount: e.moveCount() };
|
||||
return { kind: 'your_turn', gameId: s(e.gameId()), deadlineUnix: Number(e.deadlineUnix()), opponentName: s(e.opponentName()), moveCount: e.moveCount() };
|
||||
}
|
||||
case 'opponent_moved': {
|
||||
const e = fb.OpponentMovedEvent.getRootAsOpponentMovedEvent(bb);
|
||||
|
||||
@@ -137,7 +137,7 @@ describe('advanceCached', () => {
|
||||
});
|
||||
|
||||
it('ignores event kinds that do not advance a game board', () => {
|
||||
const yourTurn: PushEvent = { kind: 'your_turn', gameId: 'g1', deadlineUnix: 0, moveCount: 4 };
|
||||
const yourTurn: PushEvent = { kind: 'your_turn', gameId: 'g1', deadlineUnix: 0, opponentName: 'Ann', moveCount: 4 };
|
||||
expect(advanceCached(cache(3, 0), yourTurn)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,11 @@ describe('i18n catalog', () => {
|
||||
expect(translate('ru', 'game.bag', { n: 7 })).toBe('7 в мешке');
|
||||
});
|
||||
|
||||
it('names the previous player in the cross-game your-turn toast', () => {
|
||||
expect(translate('ru', 'game.yourTurnBy', { name: 'Аня' })).toBe('Аня: Ваш ход!');
|
||||
expect(translate('en', 'game.yourTurnBy', { name: 'Ann' })).toBe('Ann: Your turn!');
|
||||
});
|
||||
|
||||
it('localizes move-history action labels', () => {
|
||||
expect(translate('ru', 'move.exchange')).toBe('обмен');
|
||||
expect(translate('ru', 'move.pass')).toBe('пас');
|
||||
|
||||
@@ -61,6 +61,7 @@ export const en = {
|
||||
'game.bagEmpty': 'Bag is empty',
|
||||
'game.hints': 'Hints {n}',
|
||||
'game.yourTurn': 'Your turn',
|
||||
'game.yourTurnBy': '{name}: Your turn!',
|
||||
'game.opponentsTurn': "Opponent's turn",
|
||||
'game.searchingForOpponent': 'Searching for opponent…',
|
||||
'game.waiting': "Waiting for {name}",
|
||||
|
||||
@@ -62,6 +62,7 @@ export const ru: Record<MessageKey, string> = {
|
||||
'game.bagEmpty': 'Мешок пуст',
|
||||
'game.hints': 'Подсказки {n}',
|
||||
'game.yourTurn': 'Ваш ход',
|
||||
'game.yourTurnBy': '{name}: Ваш ход!',
|
||||
'game.opponentsTurn': 'Ход соперника',
|
||||
'game.searchingForOpponent': 'Поиск соперника...',
|
||||
'game.waiting': 'Ожидаем {name}',
|
||||
|
||||
@@ -598,7 +598,7 @@ export class MockGateway implements GatewayClient {
|
||||
g.view.moveCount += 1;
|
||||
g.view.toMove = this.mySeat(g);
|
||||
this.emit({ kind: 'opponent_moved', gameId, move: structuredClone(move), game: structuredClone(g.view), bagLen: g.bagLen });
|
||||
this.emit({ kind: 'your_turn', gameId, deadlineUnix: Math.floor(Date.now() / 1000) + 86400, moveCount: g.view.moveCount });
|
||||
this.emit({ kind: 'your_turn', gameId, deadlineUnix: Math.floor(Date.now() / 1000) + 86400, opponentName: g.view.seats[opp].displayName, moveCount: g.view.moveCount });
|
||||
}, 1600);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -255,7 +255,7 @@ export interface GameList {
|
||||
* so a client falls back to a refetch when a payload is absent (a gap, or an older peer).
|
||||
*/
|
||||
export type PushEvent =
|
||||
| { kind: 'your_turn'; gameId: string; deadlineUnix: number; moveCount: number }
|
||||
| { kind: 'your_turn'; gameId: string; deadlineUnix: number; opponentName: string; moveCount: number }
|
||||
| { kind: 'opponent_moved'; gameId: string; move?: MoveRecord; game?: GameView; bagLen: number }
|
||||
| { kind: 'game_over'; gameId: string; result: string; scoreLine: string; game?: GameView }
|
||||
| { kind: 'chat_message'; message: ChatMessage }
|
||||
|
||||
Reference in New Issue
Block a user