diff --git a/app/game-api/test/commandTable.test.ts b/app/game-api/test/commandTable.test.ts index 9741acc8..456dc6f0 100644 --- a/app/game-api/test/commandTable.test.ts +++ b/app/game-api/test/commandTable.test.ts @@ -119,6 +119,7 @@ describe('buildTurnCommandTable', () => { 'che_단련', 'che_숙련전환', 'che_견문', + 'che_은퇴', 'che_장비매매', 'che_군량매매', 'che_내정특기초기화', @@ -135,9 +136,67 @@ describe('buildTurnCommandTable', () => { 'che_주민선정', ], 군사: ['che_징병', 'che_모병', 'che_훈련', 'che_사기진작', 'che_출병', 'che_집합', 'che_소집해제'], - 인사: ['che_이동', 'che_인재탐색', 'che_귀환', 'che_임관', 'che_랜덤임관'], + 인사: ['che_이동', 'che_강행', 'che_인재탐색', 'che_귀환', 'che_임관', 'che_랜덤임관'], 계략: ['che_화계'], - 국가: ['che_증여', 'che_헌납', 'che_물자조달', 'che_거병', 'che_건국', 'che_선양', 'che_해산'], + 국가: [ + 'che_증여', + 'che_헌납', + 'che_물자조달', + 'che_하야', + 'che_거병', + 'che_건국', + 'che_선양', + 'che_해산', + ], + }); + }); + + it('projects the Ref availability boundaries for force move, retirement, and resignation', async () => { + const buildTable = (general: GeneralRow, nation: NationRow | null = buildNation()) => + buildTurnCommandTable({ + worldState: buildWorldState(), + general, + city: buildCity(), + nation, + nationGenerals: null, + }); + const findCommand = (table: Awaited>, key: string) => + table.general.flatMap((group) => group.values).find((command) => command.key === key); + + const ordinary = await buildTable(buildGeneral()); + expect(findCommand(ordinary, 'che_강행')).toMatchObject({ + name: '강행', + reqArg: true, + possible: true, + inputFields: [{ key: 'destCityId', optionSource: 'cities' }], + }); + expect(findCommand(ordinary, 'che_은퇴')).toMatchObject({ + name: '은퇴', + possible: false, + status: 'blocked', + reason: '나이가 60세 이상이어야 합니다.', + }); + expect(findCommand(ordinary, 'che_하야')).toMatchObject({ + name: '하야', + possible: true, + status: 'available', + }); + + const oldEnough = await buildTable({ ...buildGeneral(), age: 60 } as GeneralRow); + expect(findCommand(oldEnough, 'che_은퇴')).toMatchObject({ possible: true, status: 'available' }); + + const ruler = await buildTable({ ...buildGeneral(), officerLevel: 12 } as GeneralRow); + expect(findCommand(ruler, 'che_하야')).toMatchObject({ + possible: false, + status: 'blocked', + reason: expect.stringContaining('군주'), + }); + + const neutral = await buildTable({ ...buildGeneral(), nationId: 0, officerLevel: 0 } as GeneralRow, null); + expect(findCommand(neutral, 'che_하야')).toMatchObject({ + possible: false, + status: 'blocked', + reason: '재야입니다.', }); }); diff --git a/app/game-engine/test/defaultCommandProfileAiCoverage.test.ts b/app/game-engine/test/defaultCommandProfileAiCoverage.test.ts index c8c8cc06..6482c865 100644 --- a/app/game-engine/test/defaultCommandProfileAiCoverage.test.ts +++ b/app/game-engine/test/defaultCommandProfileAiCoverage.test.ts @@ -18,13 +18,16 @@ const GENERAL_AI_ACTIONS = [ const NATION_AI_ACTIONS = ['che_몰수', 'che_발령', 'che_선전포고', 'che_천도', 'che_포상'] as const; const GENERAL_REF_EDITOR_ACTIONS = [ + 'che_은퇴', 'che_임관', 'che_랜덤임관', + 'che_강행', 'che_징병', 'che_출병', 'che_농지개간', 'che_화계', 'che_증여', + 'che_하야', 'che_장비매매', ] as const; const NATION_REF_EDITOR_ACTIONS = ['che_포상', 'che_발령', 'che_증축', 'che_필사즉생'] as const; diff --git a/app/game-frontend/e2e/commandArguments.spec.ts b/app/game-frontend/e2e/commandArguments.spec.ts index 2991c6ac..9728e31d 100644 --- a/app/game-frontend/e2e/commandArguments.spec.ts +++ b/app/game-frontend/e2e/commandArguments.spec.ts @@ -294,7 +294,7 @@ const chiefCenter = { })), }; -const install = async (page: Page, rejectGeneral = false) => { +const install = async (page: Page, rejectGeneral = false, commandTableResponse: unknown = commandTable) => { const requests: unknown[] = []; const generalTurns = turns(30); const nationTurns = turns(12); @@ -344,7 +344,7 @@ const install = async (page: Page, rejectGeneral = false) => { ? { kind: 'snapshot', revision: 'BBBBBBBBBBBBBBBBBBBBBB', - data: commandTable, + data: commandTableResponse, } : { kind: 'unchanged', revision: 'BBBBBBBBBBBBBBBBBBBBBB' }, boardAccess: initial @@ -400,7 +400,7 @@ const install = async (page: Page, rejectGeneral = false) => { myCity: 1, myNation: 1, }); - if (name === 'turns.getCommandTable') return response(commandTable); + if (name === 'turns.getCommandTable') return response(commandTableResponse); if (name === 'nation.getChiefCenter') return response(chiefCenter); if (name === 'turns.reserved.getGeneral') return response({ turns: generalTurns, revision: generalRevision }); @@ -461,6 +461,117 @@ const install = async (page: Page, rejectGeneral = false) => { return requests; }; +test('reserves force move, retirement, and resignation from the user command picker', async ({ page }) => { + const specialCommandTable = { + general: [ + { + category: '개인', + values: [ + { + key: 'che_은퇴', + name: '은퇴', + reqArg: false, + possible: false, + status: 'blocked', + reason: '나이가 60세 이상이어야 합니다.', + inputFields: [], + }, + ], + }, + { + category: '인사', + values: [ + { + key: 'che_강행', + name: '강행', + reqArg: true, + possible: true, + status: 'available', + inputFields: [ + { + key: 'destCityId', + label: '대상 도시', + kind: 'select', + required: true, + optionSource: 'cities', + }, + ], + }, + ], + }, + { + category: '국가', + values: [ + { + key: 'che_하야', + name: '하야', + reqArg: false, + possible: true, + status: 'available', + inputFields: [], + }, + ], + }, + ], + nation: [], + inputOptions, + }; + const requests = await install(page, false, specialCommandTable); + await page.setViewportSize({ width: 1200, height: 900 }); + await page.goto('/'); + + const editor = page.locator('[data-command-scope="general"]'); + + await editor.getByRole('button', { name: '1턴 명령 입력', exact: true }).click(); + let picker = page.getByTestId('command-picker'); + const retirement = picker.getByRole('button', { name: '은퇴', exact: true }); + await expect(retirement).toHaveClass(/blocked/); + await expect(retirement).toHaveAttribute('title', '나이가 60세 이상이어야 합니다.'); + await retirement.hover(); + await retirement.focus(); + await expect(retirement).toBeFocused(); + await picker.screenshot({ path: test.info().outputPath('special-user-commands-desktop-1200.png') }); + await retirement.click(); + await expect(editor.locator('.action-column > div').nth(0)).toHaveText('은퇴'); + + await editor.getByRole('button', { name: '2턴 명령 입력', exact: true }).click(); + picker = page.getByTestId('command-picker'); + await picker.getByRole('button', { name: '국가', exact: true }).click(); + await picker.getByRole('button', { name: '하야', exact: true }).click(); + await expect(editor.locator('.action-column > div').nth(1)).toHaveText('하야'); + + await editor.getByRole('button', { name: '3턴 명령 입력', exact: true }).click(); + picker = page.getByTestId('command-picker'); + await picker.getByRole('button', { name: '인사', exact: true }).click(); + await picker.getByRole('button', { name: '강행', exact: true }).click(); + const forceMoveForm = picker.getByTestId('command-argument-form'); + await expect(forceMoveForm.getByTestId('command-argument-guidance')).toContainText('선택한 도시로 강행합니다.'); + await forceMoveForm.locator('select').selectOption('2'); + await picker.getByRole('button', { name: '입력', exact: true }).click(); + await expect(editor.locator('.action-column > div').nth(2)).toHaveText('강행'); + + const serialized = JSON.stringify(requests); + expect(serialized).toContain('"action":"che_은퇴","args":{}'); + expect(serialized).toContain('"action":"che_하야","args":{}'); + expect(serialized).toContain('"action":"che_강행","args":{"destCityId":2}'); + + await page.setViewportSize({ width: 500, height: 900 }); + await editor.getByRole('button', { name: '4턴 명령 입력', exact: true }).click(); + picker = page.getByTestId('command-picker'); + await expect(picker.locator('.category-btn')).toHaveText(['개인', '인사', '국가']); + const mobileGeometry = await picker.evaluate((element) => ({ + width: element.getBoundingClientRect().width, + horizontalOverflow: element.scrollWidth - element.clientWidth, + categoryColumns: getComputedStyle(element.querySelector('.category-list')!).gridTemplateColumns, + })); + expect(mobileGeometry.width).toBeLessThanOrEqual(500); + expect(mobileGeometry.horizontalOverflow).toBeLessThanOrEqual(0); + expect(mobileGeometry.categoryColumns.split(' ')).toHaveLength(3); + await picker.getByRole('button', { name: '개인', exact: true }).click(); + await expect(picker.getByRole('button', { name: '은퇴', exact: true })).toBeVisible(); + await picker.screenshot({ path: test.info().outputPath('special-user-commands-mobile-500.png') }); +}); + test('enters general and nation command arguments and sends exact values', async ({ page }) => { const requests = await install(page); await page.setViewportSize({ width: 1200, height: 900 }); diff --git a/resources/turn-commands/default.json b/resources/turn-commands/default.json index 74cc8411..86ee083d 100644 --- a/resources/turn-commands/default.json +++ b/resources/turn-commands/default.json @@ -11,6 +11,7 @@ "che_사기진작", "che_요양", "che_견문", + "che_은퇴", "che_내정특기초기화", "che_전투특기초기화", "che_장비매매", @@ -34,6 +35,8 @@ "che_증여", "che_헌납", "che_이동", + "che_강행", + "che_하야", "che_선양", "che_해산", "휴식"