merge: 추방 보호 대상 차단을 main에 반영한다
This commit is contained in:
@@ -1997,7 +1997,7 @@ async function handleKick(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const target = world.getGeneralById(command.destGeneralId);
|
const target = world.getGeneralById(command.destGeneralId);
|
||||||
if (!target || target.id === general.id || target.nationId !== general.nationId) {
|
if (!target || target.nationId !== general.nationId) {
|
||||||
return {
|
return {
|
||||||
type: 'kick',
|
type: 'kick',
|
||||||
ok: false,
|
ok: false,
|
||||||
@@ -2005,7 +2005,18 @@ async function handleKick(
|
|||||||
reason: '대상을 찾을 수 없거나 같은 국가가 아닙니다.',
|
reason: '대상을 찾을 수 없거나 같은 국가가 아닙니다.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (resolveMaxSecretPermission(target) === 4 && resolvePermissionKind(target) === 'ambassador') {
|
if (target.id === general.id) {
|
||||||
|
return { type: 'kick', ok: false, generalId: command.generalId, reason: '본인은 추방할 수 없습니다.' };
|
||||||
|
}
|
||||||
|
// Ref 화면은 군주와 본인을 후보에서 제외하지만 서버는 조작 요청을 막지 못했다.
|
||||||
|
// 국가 소유권을 깨뜨리는 대상은 UI와 무관하게 durable command 경계에서 거부한다.
|
||||||
|
if (target.id === nation.chiefGeneralId || target.officerLevel === 12) {
|
||||||
|
return { type: 'kick', ok: false, generalId: command.generalId, reason: '군주는 추방할 수 없습니다.' };
|
||||||
|
}
|
||||||
|
if (target.officerLevel >= 5) {
|
||||||
|
return { type: 'kick', ok: false, generalId: command.generalId, reason: '수뇌는 추방할 수 없습니다.' };
|
||||||
|
}
|
||||||
|
if (resolvePermissionKind(target) === 'ambassador') {
|
||||||
return {
|
return {
|
||||||
type: 'kick',
|
type: 'kick',
|
||||||
ok: false,
|
ok: false,
|
||||||
|
|||||||
@@ -312,6 +312,39 @@ describe('nation personnel world commands', () => {
|
|||||||
expect(fixture.world.peekDirtyState().logs).toHaveLength(2);
|
expect(fixture.world.peekDirtyState().logs).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('rejects self, ruler, head officer, and ambassador targets without partial mutation', async () => {
|
||||||
|
const cases = [
|
||||||
|
{ label: 'self', targetId: 2, reason: '본인은 추방할 수 없습니다.' },
|
||||||
|
{ label: 'ruler', targetId: 1, reason: '군주는 추방할 수 없습니다.' },
|
||||||
|
{ label: 'head officer', targetId: 3, reason: '수뇌는 추방할 수 없습니다.' },
|
||||||
|
{ label: 'ambassador', targetId: 4, reason: '외교권자는 추방할 수 없습니다.' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
for (const testCase of cases) {
|
||||||
|
const fixture = buildWorld({
|
||||||
|
generals: [
|
||||||
|
buildGeneral(1, { officerLevel: 12 }),
|
||||||
|
buildGeneral(2, { officerLevel: 5 }),
|
||||||
|
buildGeneral(3, { officerLevel: 7 }),
|
||||||
|
buildGeneral(4, {
|
||||||
|
meta: { killturn: 12, belong: 5, permission: 'ambassador' },
|
||||||
|
penalty: { noAmbassador: true },
|
||||||
|
}),
|
||||||
|
buildGeneral(5),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const originalTarget = fixture.world.getGeneralById(testCase.targetId);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
fixture.handler.handle({ type: 'kick', generalId: 2, destGeneralId: testCase.targetId })
|
||||||
|
).resolves.toMatchObject({ ok: false, reason: testCase.reason });
|
||||||
|
expect(fixture.world.getGeneralById(testCase.targetId), testCase.label).toEqual(originalTarget);
|
||||||
|
expect(fixture.world.getGeneralById(2)?.meta.killturn, testCase.label).toBe(12);
|
||||||
|
expect(fixture.world.peekDirtyState().logs, testCase.label).toEqual([]);
|
||||||
|
expect(fixture.world.peekDirtyState().nations, testCase.label).toEqual([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('preserves the legacy kick year boundaries and deterministic NPC public message', async () => {
|
it('preserves the legacy kick year boundaries and deterministic NPC public message', async () => {
|
||||||
const early = buildWorld({
|
const early = buildWorld({
|
||||||
currentYear: 181,
|
currentYear: 181,
|
||||||
|
|||||||
@@ -412,6 +412,13 @@ test('personnel reflows row-level appointments at 500px and 390px without gradie
|
|||||||
expect(rowGeometry.gradientCount).toBe(0);
|
expect(rowGeometry.gradientCount).toBe(0);
|
||||||
await expect(page.getByRole('combobox', { name: '외교권자' })).toHaveCount(0);
|
await expect(page.getByRole('combobox', { name: '외교권자' })).toHaveCount(0);
|
||||||
await expect(page.getByRole('combobox', { name: '추방 대상 장수' })).toBeVisible();
|
await expect(page.getByRole('combobox', { name: '추방 대상 장수' })).toBeVisible();
|
||||||
|
await expect(page.getByRole('combobox', { name: '추방 대상 장수' }).locator('option')).toHaveText([
|
||||||
|
'장수 선택',
|
||||||
|
'하후돈 (70/70/70)',
|
||||||
|
'곽가 (70/70/70)',
|
||||||
|
'정욱 (70/70/70)',
|
||||||
|
'장료 (70/70/70)',
|
||||||
|
]);
|
||||||
|
|
||||||
await page.getByRole('button', { name: '허창 태수 변경하기', exact: true }).click();
|
await page.getByRole('button', { name: '허창 태수 변경하기', exact: true }).click();
|
||||||
const picker = page.getByTestId('personnel-selection-dialog');
|
const picker = page.getByTestId('personnel-selection-dialog');
|
||||||
|
|||||||
@@ -99,7 +99,9 @@ const cityCandidates = (level: OfficerLevel): GeneralEntry[] => {
|
|||||||
return candidates;
|
return candidates;
|
||||||
};
|
};
|
||||||
const kickCandidates = computed(() =>
|
const kickCandidates = computed(() =>
|
||||||
(data.value?.generals ?? []).filter((general) => general.id !== data.value?.me.id)
|
(data.value?.generals ?? []).filter(
|
||||||
|
(general) => general.id !== data.value?.me.id && general.officerLevel < 5 && general.permission !== 'ambassador'
|
||||||
|
)
|
||||||
);
|
);
|
||||||
const awardText = (entries: PersonnelResponse['awards']['tigers']): string =>
|
const awardText = (entries: PersonnelResponse['awards']['tigers']): string =>
|
||||||
entries.map((entry) => `${entry.name}【${entry.value.toLocaleString('ko-KR')}】`).join(', ');
|
entries.map((entry) => `${entry.name}【${entry.value.toLocaleString('ko-KR')}】`).join(', ');
|
||||||
|
|||||||
Reference in New Issue
Block a user