From 7e9cde35923c269c6134cea071bb697bdb85a781 Mon Sep 17 00:00:00 2001 From: hided62 Date: Wed, 23 Sep 2026 04:35:44 +0000 Subject: [PATCH] =?UTF-8?q?=EB=B9=99=EC=9D=98=20=EA=B5=B0=EC=A3=BC?= =?UTF-8?q?=EC=9D=98=20=EC=99=B8=EA=B5=90=EA=B6=8C=EC=9E=90=20=EC=9E=84?= =?UTF-8?q?=EB=AA=85=20=EA=B6=8C=ED=95=9C=EC=9D=84=20=EB=B3=B5=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test/nationPersonnelRouter.test.ts | 5 ++-- .../src/scenario/scenarioSeeder.ts | 7 ++++++ .../src/turn/worldCommandHandler.ts | 2 +- .../test/nationPersonnelManagement.test.ts | 23 ++++++++++++++++++- app/game-engine/test/scenarioSeeder.test.ts | 5 ++++ 5 files changed, 38 insertions(+), 4 deletions(-) diff --git a/app/game-api/test/nationPersonnelRouter.test.ts b/app/game-api/test/nationPersonnelRouter.test.ts index 611d12a7..3ae5133b 100644 --- a/app/game-api/test/nationPersonnelRouter.test.ts +++ b/app/game-api/test/nationPersonnelRouter.test.ts @@ -249,9 +249,9 @@ describe('nation personnel router', () => { }); it('keeps ambassador and auditor candidate pools mutually exclusive like Ref', async () => { - const me = { ...baseGeneral, officerLevel: 12 }; + const me = { ...baseGeneral, officerLevel: 12, npcState: 1 }; const rows = [ - listRow({ id: 22, name: '군주', officerLevel: 12 }), + listRow({ id: 22, name: '군주', officerLevel: 12, npcState: 1 }), listRow({ id: 30, name: '현 외교권자', meta: { belong: 5, permission: 'ambassador' } }), listRow({ id: 31, name: '현 조언자', meta: { belong: 5, permission: 'auditor' } }), listRow({ id: 32, name: '일반 후보' }), @@ -283,6 +283,7 @@ describe('nation personnel router', () => { }); const result = await appRouter.createCaller(context).nation.getPersonnelInfo(); + expect(result.me.canChangePermissions).toBe(true); expect(result.permissionCandidates.ambassadors.map((candidate) => candidate.id)).toEqual([30, 32]); expect(result.permissionCandidates.auditors.map((candidate) => candidate.id)).toEqual([31, 32]); }); diff --git a/app/game-engine/src/scenario/scenarioSeeder.ts b/app/game-engine/src/scenario/scenarioSeeder.ts index 8601a976..07429fcd 100644 --- a/app/game-engine/src/scenario/scenarioSeeder.ts +++ b/app/game-engine/src/scenario/scenarioSeeder.ts @@ -288,6 +288,12 @@ export const seedScenarioToDatabase = async (options: ScenarioSeedOptions): Prom }, }); seed.cities = applyInitialChangeCityEvents(seed.cities, seed.initialEvents); + const seededRulerIds = new Map(); + for (const general of [...seed.generals].sort((left, right) => left.id - right.id)) { + if (general.officerLevel === 12 && !seededRulerIds.has(general.nationId)) { + seededRulerIds.set(general.nationId, general.id); + } + } const connector = createGamePostgresConnector({ url: options.databaseUrl }); const generalGold = options.defaultGeneralGold ?? DEFAULT_GENERAL_GOLD; @@ -556,6 +562,7 @@ export const seedScenarioToDatabase = async (options: ScenarioSeedOptions): Prom name: nation.name, color: nation.color, capitalCityId: nation.capitalCityId ?? null, + chiefGeneralId: seededRulerIds.get(nation.id) ?? null, gold: nation.gold, rice: nation.rice, tech: nation.tech, diff --git a/app/game-engine/src/turn/worldCommandHandler.ts b/app/game-engine/src/turn/worldCommandHandler.ts index 79097e8f..657f5cdb 100644 --- a/app/game-engine/src/turn/worldCommandHandler.ts +++ b/app/game-engine/src/turn/worldCommandHandler.ts @@ -2144,7 +2144,7 @@ async function handleChangePermission( }; } const nation = world.getNationById(general.nationId); - if (!nation || general.officerLevel !== 12 || nation.chiefGeneralId !== general.id) { + if (!nation || general.officerLevel !== 12) { return { type: 'changePermission', ok: false, generalId: command.generalId, reason: '군주가 아닙니다.' }; } diff --git a/app/game-engine/test/nationPersonnelManagement.test.ts b/app/game-engine/test/nationPersonnelManagement.test.ts index b8dd09cf..0ed219b7 100644 --- a/app/game-engine/test/nationPersonnelManagement.test.ts +++ b/app/game-engine/test/nationPersonnelManagement.test.ts @@ -52,6 +52,7 @@ const buildGeneral = (id: number, overrides: Partial = {}): TurnGen const buildWorld = (options: { generals?: TurnGeneral[]; nationMeta?: Record; + nationChiefGeneralId?: number | null; cityMeta?: Record; currentYear?: number; scenarioConst?: Record; @@ -106,7 +107,7 @@ const buildWorld = (options: { name: '위', color: '#777777', capitalCityId: 1, - chiefGeneralId: 1, + chiefGeneralId: options.nationChiefGeneralId === undefined ? 1 : options.nationChiefGeneralId, gold: 10_000, rice: 20_000, power: 0, @@ -321,6 +322,26 @@ describe('nation personnel world commands', () => { expect(fixture.world.getGeneralById(3)?.meta.permission).toBe('normal'); }); + it('allows a possessed ruler to appoint an ambassador when a seeded nation has no chief pointer', async () => { + const fixture = buildWorld({ + nationChiefGeneralId: null, + generals: [ + buildGeneral(1, { officerLevel: 12, npcState: 1 }), + buildGeneral(2, { npcState: 2 }), + ], + }); + await expect( + fixture.handler.handle({ + type: 'changePermission', + userId: 'user-1', + generalId: 1, + isAmbassador: true, + targetGeneralIds: [2], + }) + ).resolves.toMatchObject({ ok: true }); + expect(fixture.world.getGeneralById(2)?.meta.permission).toBe('ambassador'); + }); + it('kicks for an unlocked head officer with resource, troop, permission, and log side effects', async () => { const target = buildGeneral(3, { troopId: 3, diff --git a/app/game-engine/test/scenarioSeeder.test.ts b/app/game-engine/test/scenarioSeeder.test.ts index 7c7963fe..1b83dd85 100644 --- a/app/game-engine/test/scenarioSeeder.test.ts +++ b/app/game-engine/test/scenarioSeeder.test.ts @@ -591,6 +591,11 @@ describeDb('scenario database seed', () => { ]); expect(nationCount).toBe(seed.nations.length); + const seededRuler = seed.generals.find((general) => general.officerLevel === 12 && general.nationId > 0); + expect(seededRuler).toBeDefined(); + expect( + (await connector.prisma.nation.findUnique({ where: { id: seededRuler!.nationId } }))?.chiefGeneralId + ).toBe(seededRuler!.id); expect(cityCount).toBe(seed.cities.length); expect(generalCount).toBe(seed.generals.length); expect(diplomacyCount).toBe(seed.nations.length * Math.max(0, seed.nations.length - 1));