feat(offline): hotseat in-game seat lock + host menu
Game.svelte drives a hotseat game: - the seat-to-move's rack is replaced by an Unlock button when the seat is PIN-locked (board stays visible); canMove gates the move controls on the unlock; PinPad(verify) -> source.unlockSeat reveals it. - the hint slot becomes a host button (hints off in hotseat), always enabled while the game runs (acts on a locked seat too). Master-PIN -> action sheet: skip current / exclude a player / end the game, each with a confirm + a fading check; terminate deletes and returns to the lobby. - per-turn advance re-fetches the next seat's (locked) state; self-resign and chat are hidden (hotseat resign is a host action, no comms). - gamesource: expose unlockSeat / verifyHostPin / hostAction on the local proxy. i18n hotseat.* (en + ru).
This commit is contained in:
@@ -41,6 +41,11 @@ export const localSource = {
|
||||
create: (opts) => load().then((s) => s.create(opts)),
|
||||
list: () => load().then((s) => s.list()),
|
||||
delete: (id) => load().then((s) => s.delete(id)),
|
||||
// Offline hotseat controls (no network equivalent): reveal a PIN-locked seat, and the host
|
||||
// (referee) overrides gated by the master PIN.
|
||||
unlockSeat: (id, pin) => load().then((s) => s.unlockSeat(id, pin)),
|
||||
verifyHostPin: (id, pin) => load().then((s) => s.verifyHostPin(id, pin)),
|
||||
hostAction: (id, pin, action, targetSeat) => load().then((s) => s.hostAction(id, pin, action, targetSeat)),
|
||||
// events must return the unsubscribe synchronously (the screen subscribes in onMount), so it wires
|
||||
// the real subscription once the engine loads and, until then, cancels a pending subscribe.
|
||||
events: (id, onEvent) => {
|
||||
@@ -54,7 +59,8 @@ export const localSource = {
|
||||
unsub();
|
||||
};
|
||||
},
|
||||
} satisfies GameLoopSource & Pick<LocalSource, 'events' | 'create' | 'list' | 'delete'>;
|
||||
} satisfies GameLoopSource &
|
||||
Pick<LocalSource, 'events' | 'create' | 'list' | 'delete' | 'unlockSeat' | 'verifyHostPin' | 'hostAction'>;
|
||||
|
||||
/**
|
||||
* gameSource returns the source that runs the game with the given id: the local engine for a local
|
||||
|
||||
@@ -408,6 +408,16 @@ export const en = {
|
||||
'hotseat.hostPlaysTitle': 'Are you playing too?',
|
||||
'hotseat.hostPlaysYes': 'Yes, I play',
|
||||
'hotseat.hostPlaysNo': 'No',
|
||||
'hotseat.unlock': 'Unlock',
|
||||
'hotseat.unlockTitle': 'Unlock: {name}',
|
||||
'hotseat.turnOf': 'Turn: {name}',
|
||||
'hotseat.host': 'Host',
|
||||
'hotseat.skip': 'Skip the current turn',
|
||||
'hotseat.exclude': 'Exclude a player:',
|
||||
'hotseat.terminate': 'End the game',
|
||||
'hotseat.askSkip': "Skip {name}'s turn?",
|
||||
'hotseat.askExclude': 'Remove {name} from the game?',
|
||||
'hotseat.askTerminate': 'End the game with no result?',
|
||||
} as const;
|
||||
|
||||
export type MessageKey = keyof typeof en;
|
||||
|
||||
@@ -408,4 +408,14 @@ export const ru: Record<MessageKey, string> = {
|
||||
'hotseat.hostPlaysTitle': 'Принимаете участие в игре?',
|
||||
'hotseat.hostPlaysYes': 'Да, играю',
|
||||
'hotseat.hostPlaysNo': 'Нет',
|
||||
'hotseat.unlock': 'Разблокировать',
|
||||
'hotseat.unlockTitle': 'Разблокировать: {name}',
|
||||
'hotseat.turnOf': 'Ход: {name}',
|
||||
'hotseat.host': 'Ведущий',
|
||||
'hotseat.skip': 'Пропустить ход',
|
||||
'hotseat.exclude': 'Исключить игрока:',
|
||||
'hotseat.terminate': 'Отменить партию',
|
||||
'hotseat.askSkip': 'Пропустить ход игрока {name}?',
|
||||
'hotseat.askExclude': 'Удалить {name} из игры?',
|
||||
'hotseat.askTerminate': 'Отменить партию без результата?',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user