import { i18n } from '../i18n' import { sdk } from '../sdk' import { deleteGame, exportGame, listGames } from '../serverApi' const { InputSpec, Value } = sdk /** * Pick a game, then keep it or end it. * * The dropdown is built when the form opens rather than being a fixed list — `inputSpec` takes a * function, so the options are whatever is on the server at that moment, labelled well enough to * tell two games apart without opening Games in Progress first. * * Ending is the only way a game finishes other than being played out. Until this existed an * abandoned game stayed active in the index and was faithfully resumed on every boot, forever, * with no route, player action or resignation able to stop it. */ export const manageGame = sdk.Action.withInput( 'manage-game', async () => ({ name: i18n('Manage Game'), description: i18n( 'Save a game for later, or end one that will never finish', ), warning: i18n( 'Ending a game cannot be undone. Its save is returned, so it can still be replayed.', ), allowedStatuses: 'only-running', group: null, visibility: 'enabled', }), async ({ effects }) => { const games = await listGames(effects) const values: Record = {} for (const g of games) { const name = g.gameCode ?? g.gameId values[g.gameId] = g.state === 'lobby' ? `${name} — ${i18n('waiting to start')}, ${g.playerCount} ${i18n('seats')}` : `${name} — ${g.playerCount} ${i18n('players')}, ${i18n('Day')} ${g.day} ${i18n('Stage')} ${g.stage}` + (g.waitingOn ? `, ${i18n('waiting on')} ${g.waitingOn.name}` : '') } // A select needs a default drawn from its own options, and an empty server has none to offer — // so it says so in the dropdown rather than rendering an empty one. if (Object.keys(values).length === 0) values['none'] = i18n('No games on this server') return InputSpec.of({ gameId: Value.select({ name: i18n('Game'), description: i18n('Which game to act on'), default: Object.keys(values)[0]!, values, }), operation: Value.select({ name: i18n('What to do'), description: i18n( 'Export changes nothing. Ending deletes the game from the server.', ), default: 'export', values: { export: i18n('Export — save a copy, leave the game running'), end: i18n('End the game — delete it from the server'), }, }), }) }, // Nothing to pre-fill: which game you mean is the question being asked, and Export is already // the default because it is the choice that cannot lose anything. async () => null, async ({ effects, input }) => { if (input.gameId === 'none') { return { version: '1', title: i18n('Manage Game'), message: i18n('There are no games on this server.'), result: null, } } if (input.operation === 'export') { const save = await exportGame(effects, input.gameId) return { version: '1', title: i18n('Game Saved'), message: i18n( 'This is the whole game — its seed and every move made in it. Save it as a .json file and open it in the replay viewer to watch the game back. The game itself is untouched and still running.', ), result: { type: 'single', name: i18n('Save'), description: null, value: JSON.stringify(save), masked: false, copyable: true, qr: false, }, } } const { gameCode, save } = await deleteGame(effects, input.gameId) return { version: '1', title: i18n('Game Ended'), message: `${gameCode ?? i18n('The game')} — ` + i18n( 'deleted from the server, and anyone still connected to it has been disconnected. Its save is below: keep it if you want to watch the game back, because this is the only copy left.', ), result: { type: 'single', name: i18n('Save'), description: null, value: JSON.stringify(save), masked: false, copyable: true, qr: false, }, } }, )