From 9c588dbd675c61aa5969b5254b1ed3651c84f62e Mon Sep 17 00:00:00 2001 From: hided62 Date: Sat, 22 Aug 2026 08:28:04 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EA=B5=B0?= =?UTF-8?q?=EC=A3=BC=20=EC=9E=90=EC=9C=A8=ED=96=89=EB=8F=99=20=EC=97=85?= =?UTF-8?q?=EB=AC=B4=EB=A5=BC=20=EA=B0=9C=EB=B3=84=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NPC 군주의 기존 국가 운영은 유지하고 사용자 군주는 외교, 수뇌 임명, 재정 조정, 천도를 기본 비활성화한다. 내 정보 설정과 인증 API, 엔진 gate를 연결하고 자율행동 기간 및 국가턴 master를 함께 검증한다. --- app/game-api/src/router/general/index.ts | 10 +++ .../test/inGameMenuPermissions.test.ts | 4 ++ app/game-engine/src/turn/ai/generalAi/core.ts | 58 +++++++++++----- app/game-engine/src/turn/ai/policies.ts | 37 ++++++++++ app/game-engine/src/turn/commandRegistry.ts | 4 ++ app/game-engine/src/turn/npcTaxHandler.ts | 7 +- .../src/turn/reservedTurnHandler.ts | 4 +- .../src/turn/worldCommandHandler.ts | 12 +++- .../generalAiLegacyDecisionParity.test.ts | 52 ++++++++++++-- .../test/monthlyCoreEventAction.test.ts | 48 +++++++++++++ .../test/myInformationCommands.test.ts | 8 +++ app/game-frontend/e2e/inGameMenus.spec.ts | 20 ++++++ app/game-frontend/src/views/MyPageView.vue | 69 +++++++++++++++++++ packages/common/src/turnDaemon/types.ts | 4 ++ 14 files changed, 309 insertions(+), 28 deletions(-) diff --git a/app/game-api/src/router/general/index.ts b/app/game-api/src/router/general/index.ts index a09371d8..2a3fcca4 100644 --- a/app/game-api/src/router/general/index.ts +++ b/app/game-api/src/router/general/index.ts @@ -55,6 +55,10 @@ const zGeneralSettings = z.object({ defence_train: z.number().int().optional(), use_treatment: z.number().int().optional(), use_auto_nation_turn: z.number().int().optional(), + use_auto_nation_diplomacy: z.number().int().min(0).max(1).optional(), + use_auto_nation_promotion: z.number().int().min(0).max(1).optional(), + use_auto_nation_finance: z.number().int().min(0).max(1).optional(), + use_auto_nation_capital: z.number().int().min(0).max(1).optional(), }); const zGeneralLogType = z.enum(['generalHistory', 'battleDetail', 'battleResult', 'generalAction']); @@ -211,6 +215,12 @@ const resolveUserSettings = (meta: Record) => { defence_train: readNumber(readSetting('defence_train'), 80), use_treatment: readNumber(readSetting('use_treatment'), 10), use_auto_nation_turn: readNumber(readSetting('use_auto_nation_turn'), 1), + // Ref가 NPC 군주에게만 수행하던 국가 운영은 사용자 군주에게 opt-in이다. + // 누락된 값은 신규 게임과 기존 장수 모두 안전한 기본값(사용 안함)으로 해석한다. + use_auto_nation_diplomacy: readNumber(readSetting('use_auto_nation_diplomacy'), 0), + use_auto_nation_promotion: readNumber(readSetting('use_auto_nation_promotion'), 0), + use_auto_nation_finance: readNumber(readSetting('use_auto_nation_finance'), 0), + use_auto_nation_capital: readNumber(readSetting('use_auto_nation_capital'), 0), myset, }; }; diff --git a/app/game-api/test/inGameMenuPermissions.test.ts b/app/game-api/test/inGameMenuPermissions.test.ts index 32cad590..17856e5f 100644 --- a/app/game-api/test/inGameMenuPermissions.test.ts +++ b/app/game-api/test/inGameMenuPermissions.test.ts @@ -584,6 +584,10 @@ describe('in-game my information ownership', () => { defence_train: 80, use_treatment: 21, use_auto_nation_turn: 1, + use_auto_nation_diplomacy: 0, + use_auto_nation_promotion: 0, + use_auto_nation_finance: 0, + use_auto_nation_capital: 0, myset: 3, }); diff --git a/app/game-engine/src/turn/ai/generalAi/core.ts b/app/game-engine/src/turn/ai/generalAi/core.ts index 9c30d58c..67182b93 100644 --- a/app/game-engine/src/turn/ai/generalAi/core.ts +++ b/app/game-engine/src/turn/ai/generalAi/core.ts @@ -22,7 +22,12 @@ import { GeneralActionPipeline } from '@sammo-ts/logic/actionModules/general.js' import type { ReservedTurnEntry } from '../../reservedTurnStore.js'; import type { TurnGeneral, TurnWorldState } from '../../types.js'; import type { AiCommandCandidate, AiReservedTurnProvider, AiWorldView } from '../types.js'; -import { AutorunGeneralPolicy, AutorunNationPolicy, AVAILABLE_INSTANT_TURN } from '../policies.js'; +import { + AutorunGeneralPolicy, + AutorunNationPolicy, + canUseAutomatedNationAction, + canUseRulerAutomation, +} from '../policies.js'; import { asRecord, joinYearMonth, @@ -401,10 +406,10 @@ export class GeneralAI { this.categorizeNationCities(); this.categorizeNationGeneral(); - if (this.general.npcState >= 2 && [3, 6, 9, 12].includes(this.world.currentMonth)) { - if (this.general.officerLevel === 12) { + if ([3, 6, 9, 12].includes(this.world.currentMonth)) { + if (this.general.officerLevel === 12 && canUseRulerAutomation(this.general, 'promotion')) { this.chooseNpcPromotion(); - } else { + } else if (this.general.npcState >= 2) { this.chooseNonLordPromotion(); } } @@ -420,7 +425,7 @@ export class GeneralAI { if (!this.nationPolicy.can(actionName)) { continue; } - if (this.general.npcState < 2 && !AVAILABLE_INSTANT_TURN[actionName]) { + if (!canUseAutomatedNationAction(this.general, actionName)) { continue; } const handler = nationActionHandlers[actionName]; @@ -1129,7 +1134,7 @@ export class GeneralAI { for (let chiefLevel = minChiefLevel; chiefLevel <= 12; chiefLevel += 1) { const chief = this.chiefGenerals[chiefLevel]; - if (!chief) { + if (!chief || chief.id === this.general.id) { continue; } const penalty = asRecord(chief.penalty); @@ -1148,6 +1153,9 @@ export class GeneralAI { const minBelong = Math.min(readMetaNumber(asRecord(this.general.meta), 'belong', 0) - 1, 3); const availableUserChiefCount = Object.values(this.userGenerals).filter((candidate) => { + if (candidate.id === this.general.id) { + return false; + } const penalty = asRecord(candidate.penalty); const killturn = readRequiredMetaNumber(asRecord(candidate.meta), 'killturn', `generalId=${candidate.id}`); return ( @@ -1158,17 +1166,19 @@ export class GeneralAI { }).length; if (userChiefCount === 0 && availableUserChiefCount > 0 && (chiefSet & (1 << 11)) === 0) { - const userCandidates = Object.values(this.userGenerals).sort((left, right) => { - const leftPenalty = asRecord(left.penalty); - const rightPenalty = asRecord(right.penalty); - if ((leftPenalty.noChief === true) !== (rightPenalty.noChief === true)) { - return leftPenalty.noChief === true ? 1 : -1; - } - if ((leftPenalty.noAmbassador === true) !== (rightPenalty.noAmbassador === true)) { - return leftPenalty.noAmbassador === true ? 1 : -1; - } - return right.stats.leadership - left.stats.leadership; - }); + const userCandidates = Object.values(this.userGenerals) + .filter((candidate) => candidate.id !== this.general.id) + .sort((left, right) => { + const leftPenalty = asRecord(left.penalty); + const rightPenalty = asRecord(right.penalty); + if ((leftPenalty.noChief === true) !== (rightPenalty.noChief === true)) { + return leftPenalty.noChief === true ? 1 : -1; + } + if ((leftPenalty.noAmbassador === true) !== (rightPenalty.noAmbassador === true)) { + return leftPenalty.noAmbassador === true ? 1 : -1; + } + return right.stats.leadership - left.stats.leadership; + }); for (const candidate of userCandidates) { const penalty = asRecord(candidate.penalty); const killturn = readRequiredMetaNumber( @@ -1227,6 +1237,9 @@ export class GeneralAI { } } const nextChief = generals.find((candidate) => { + if (candidate.id === this.general.id) { + return false; + } if ((effectiveOfficerLevel.get(candidate.id) ?? candidate.officerLevel) > 4) { return false; } @@ -1470,4 +1483,15 @@ export const shouldUseAi = (general: TurnGeneral, world: TurnWorldState): boolea return current < limit; }; +export const shouldUseNationAi = (general: TurnGeneral, world: TurnWorldState): boolean => { + if (!shouldUseAi(general, world)) { + return false; + } + if (general.npcState >= 2) { + return true; + } + // Ref TurnExecutionHelper는 사용자 수뇌의 국가 AI에만 이 개인 설정을 적용한다. + return readMetaNumber(asRecord(general.meta), 'use_auto_nation_turn', 1) !== 0; +}; + export type { GeneralAIOptions, GeneralAiDebugState }; diff --git a/app/game-engine/src/turn/ai/policies.ts b/app/game-engine/src/turn/ai/policies.ts index b1a33a45..7bcd5858 100644 --- a/app/game-engine/src/turn/ai/policies.ts +++ b/app/game-engine/src/turn/ai/policies.ts @@ -61,6 +61,43 @@ export const AVAILABLE_INSTANT_TURN: Record = { NPC전방발령: true, }; +export type UserRulerAutomationFeature = 'diplomacy' | 'promotion' | 'finance' | 'capital'; + +const USER_RULER_AUTOMATION_META_KEY = { + diplomacy: 'use_auto_nation_diplomacy', + promotion: 'use_auto_nation_promotion', + finance: 'use_auto_nation_finance', + capital: 'use_auto_nation_capital', +} as const satisfies Record; + +const USER_RULER_ACTION_FEATURE: Readonly> = { + 불가침제의: 'diplomacy', + 선전포고: 'diplomacy', + 천도: 'capital', +}; + +/** NPC는 기존 계약을 유지하고, 사용자 군주는 명시적으로 고른 업무만 위임한다. */ +export const canUseRulerAutomation = ( + general: TurnGeneral, + feature: UserRulerAutomationFeature +): boolean => { + if (general.npcState >= 2) { + return true; + } + if (general.officerLevel !== 12) { + return false; + } + return readMetaNumber(asRecord(general.meta), USER_RULER_AUTOMATION_META_KEY[feature], 0) === 1; +}; + +export const canUseAutomatedNationAction = (general: TurnGeneral, actionName: string): boolean => { + if (general.npcState >= 2 || AVAILABLE_INSTANT_TURN[actionName]) { + return true; + } + const feature = USER_RULER_ACTION_FEATURE[actionName]; + return feature ? canUseRulerAutomation(general, feature) : false; +}; + const buildFlags = (entries: Array<[string, boolean]>): PolicyFlags => Object.fromEntries(entries); const applyPriorityOverride = (priority: string[], override: unknown, flags: PolicyFlags): string[] => { diff --git a/app/game-engine/src/turn/commandRegistry.ts b/app/game-engine/src/turn/commandRegistry.ts index 51ea335d..cd6bb585 100644 --- a/app/game-engine/src/turn/commandRegistry.ts +++ b/app/game-engine/src/turn/commandRegistry.ts @@ -132,6 +132,10 @@ const zSetMySetting = z.object({ defence_train: z.number().int().optional(), use_treatment: z.number().int().optional(), use_auto_nation_turn: z.number().int().optional(), + use_auto_nation_diplomacy: z.number().int().optional(), + use_auto_nation_promotion: z.number().int().optional(), + use_auto_nation_finance: z.number().int().optional(), + use_auto_nation_capital: z.number().int().optional(), }), }); diff --git a/app/game-engine/src/turn/npcTaxHandler.ts b/app/game-engine/src/turn/npcTaxHandler.ts index 8b67e935..0acb3ca9 100644 --- a/app/game-engine/src/turn/npcTaxHandler.ts +++ b/app/game-engine/src/turn/npcTaxHandler.ts @@ -18,7 +18,8 @@ import { import type { TurnCalendarContext, TurnCalendarHandler, InMemoryTurnWorld } from './inMemoryWorld.js'; import { readNumber } from './ai/aiUtils.js'; -import { AutorunNationPolicy } from './ai/policies.js'; +import { shouldUseNationAi } from './ai/generalAi.js'; +import { AutorunNationPolicy, canUseRulerAutomation } from './ai/policies.js'; const calcNationDevelopedRate = (cities: City[]): { pop: number; all: number } => { if (cities.length === 0) { @@ -108,7 +109,9 @@ const resolveNpcMonarch = (nation: Nation, world: InMemoryTurnWorld) => { : world .listGenerals() .find((general) => general.nationId === nation.id && general.officerLevel === 12) ?? null; - return chief && chief.npcState >= 2 ? chief : null; + return chief && canUseRulerAutomation(chief, 'finance') && shouldUseNationAi(chief, world.getState()) + ? chief + : null; }; type NpcFinanceOptions = { diff --git a/app/game-engine/src/turn/reservedTurnHandler.ts b/app/game-engine/src/turn/reservedTurnHandler.ts index d6691e51..8e3b2945 100644 --- a/app/game-engine/src/turn/reservedTurnHandler.ts +++ b/app/game-engine/src/turn/reservedTurnHandler.ts @@ -58,7 +58,7 @@ import { import { buildCommandEnv, buildReservedTurnDefinitions } from './reservedTurnCommands.js'; import { buildFrontStatePatches } from './frontStateHandler.js'; import { buildActionContext } from './reservedTurnActionContext.js'; -import { GeneralAI, shouldUseAi } from './ai/generalAi.js'; +import { GeneralAI, shouldUseAi, shouldUseNationAi } from './ai/generalAi.js'; import type { AiReservedTurnProvider } from './ai/types.js'; import { withCanonicalArgumentAliases } from './ai/aiUtils.js'; import { rankMetaKey } from './rankData.js'; @@ -1672,7 +1672,7 @@ export const createReservedTurnHandler = async (options: { let nationAiState: ReturnType | undefined; let nationAiDecisionDurationNs = 0n; let nationUsedAi = false; - if (worldView && shouldUseAi(currentGeneral, context.world)) { + if (worldView && shouldUseNationAi(currentGeneral, context.world)) { nationUsedAi = true; const aiStartedAt = options.onActionProfiled ? process.hrtime.bigint() : 0n; sharedAi = new GeneralAI({ diff --git a/app/game-engine/src/turn/worldCommandHandler.ts b/app/game-engine/src/turn/worldCommandHandler.ts index e712f08d..db35d864 100644 --- a/app/game-engine/src/turn/worldCommandHandler.ts +++ b/app/game-engine/src/turn/worldCommandHandler.ts @@ -1702,7 +1702,17 @@ async function handleSetMySetting( nextMeta.use_treatment = Math.max(10, Math.min(100, settings.use_treatment)); } if (settings.use_auto_nation_turn !== undefined) { - nextMeta.use_auto_nation_turn = settings.use_auto_nation_turn; + nextMeta.use_auto_nation_turn = settings.use_auto_nation_turn === 0 ? 0 : 1; + } + for (const key of [ + 'use_auto_nation_diplomacy', + 'use_auto_nation_promotion', + 'use_auto_nation_finance', + 'use_auto_nation_capital', + ] as const) { + if (settings[key] !== undefined) { + nextMeta[key] = settings[key] === 1 ? 1 : 0; + } } let nextTrain = general.train; diff --git a/app/game-engine/test/generalAiLegacyDecisionParity.test.ts b/app/game-engine/test/generalAiLegacyDecisionParity.test.ts index 0a017430..c3b6fd4e 100644 --- a/app/game-engine/test/generalAiLegacyDecisionParity.test.ts +++ b/app/game-engine/test/generalAiLegacyDecisionParity.test.ts @@ -2,8 +2,9 @@ import { describe, expect, it } from 'vitest'; import type { City, General, Nation } from '@sammo-ts/logic'; import { createRefOrderedActionStack } from '@sammo-ts/logic/actionModules/bundle.js'; -import { GeneralAI } from '../src/turn/ai/generalAi.js'; -import type { TurnGeneral } from '../src/turn/types.js'; +import { GeneralAI, shouldUseNationAi } from '../src/turn/ai/generalAi.js'; +import { canUseAutomatedNationAction, canUseRulerAutomation } from '../src/turn/ai/policies.js'; +import type { TurnGeneral, TurnWorldState } from '../src/turn/types.js'; import { calculateRecentWarTurn, resolveLegacyAiStats, @@ -672,13 +673,13 @@ describe('legacy NPC user-chief promotion parity', () => { expect(ai.consumePromotionPatches().generals).toEqual([{ generalId: 2, officerLevel: 11, officerCity: 0 }]); }); - it('runs automatic appointments only on the quarterly NPC nation turn', () => { - const run = (currentMonth: number) => { + it('runs automatic appointments for NPC rulers and only opted-in user rulers in quarter months', () => { + const run = (currentMonth: number, npcState = 2, enabled = 0) => { const ruler = makePromotionGeneral({ id: 1, officerLevel: 12, - npcState: 2, - meta: { killturn: 100, belong: 2 }, + npcState, + meta: { killturn: 100, belong: 2, use_auto_nation_promotion: enabled }, }); const user = makePromotionGeneral({ id: 2, @@ -710,6 +711,45 @@ describe('legacy NPC user-chief promotion parity', () => { expect(run(2)).toEqual([]); expect(run(3)).toEqual([{ generalId: 2, officerLevel: 11, officerCity: 0, permission: 'ambassador' }]); + expect(run(3, 0)).toEqual([]); + expect(run(3, 0, 1)).toEqual([ + { generalId: 2, officerLevel: 11, officerCity: 0, permission: 'ambassador' }, + ]); + }); + + it('keeps user-ruler duties individually disabled until each setting is enabled', () => { + const ruler = makePromotionGeneral({ id: 1, officerLevel: 12, npcState: 0, meta: { killturn: 0 } }); + expect(canUseAutomatedNationAction(ruler, '선전포고')).toBe(false); + expect(canUseAutomatedNationAction(ruler, '천도')).toBe(false); + expect(canUseRulerAutomation(ruler, 'finance')).toBe(false); + + ruler.meta = { + ...ruler.meta, + use_auto_nation_diplomacy: 1, + use_auto_nation_capital: 1, + use_auto_nation_finance: 1, + }; + expect(canUseAutomatedNationAction(ruler, '불가침제의')).toBe(true); + expect(canUseAutomatedNationAction(ruler, '선전포고')).toBe(true); + expect(canUseAutomatedNationAction(ruler, '천도')).toBe(true); + expect(canUseRulerAutomation(ruler, 'finance')).toBe(true); + }); + + it('honors the existing automatic nation-turn master switch for user chiefs only', () => { + const world = { + currentYear: 190, + currentMonth: 3, + meta: {}, + } as TurnWorldState; + const build = (npcState: number, useAutoNationTurn: number) => + makePromotionGeneral({ + npcState, + meta: { killturn: 0, autorun_limit: 19004, use_auto_nation_turn: useAutoNationTurn }, + }); + + expect(shouldUseNationAi(build(0, 0), world)).toBe(false); + expect(shouldUseNationAi(build(0, 1), world)).toBe(true); + expect(shouldUseNationAi(build(2, 0), world)).toBe(true); }); }); diff --git a/app/game-engine/test/monthlyCoreEventAction.test.ts b/app/game-engine/test/monthlyCoreEventAction.test.ts index bb593361..2e65f899 100644 --- a/app/game-engine/test/monthlyCoreEventAction.test.ts +++ b/app/game-engine/test/monthlyCoreEventAction.test.ts @@ -468,4 +468,52 @@ describe('core monthly event actions at the real month boundary', () => { expect(lowChiefBill).toBeTypeOf('number'); expect(highChiefBill).toBe(lowChiefBill); }); + + it('keeps user-ruler finance unchanged by default and applies it only after opt-in', () => { + const config: TurnWorldSnapshot['scenarioConfig'] = { + stat: { total: 300, min: 10, max: 100, npcTotal: 150, npcMax: 50, npcMin: 10, chiefMin: 70 }, + iconPath: '', + map: {}, + const: { baseGold: 0, baseRice: 0 }, + environment: { mapName: map.id, unitSet: 'default' }, + }; + const buildFinanceWorld = (settings: { + finance: number; + autorunLimit?: number; + nationTurn?: number; + }) => { + const chief = buildGeneral(1, 1, 3); + chief.npcState = 0; + chief.officerLevel = 12; + chief.meta = { + ...chief.meta, + use_auto_nation_finance: settings.finance, + ...(settings.autorunLimit === undefined ? {} : { autorun_limit: settings.autorunLimit }), + ...(settings.nationTurn === undefined ? {} : { use_auto_nation_turn: settings.nationTurn }), + }; + const subordinate = buildGeneral(2, 1, 3); + return buildWorld([], () => new Map(), { + currentMonth: 12, + generals: [chief, subordinate], + nations: [buildNation()], + cities: [buildCity()], + }); + }; + const options = { scenarioConfig: config, commandEnv: buildCommandEnv(config) }; + const disabled = buildFinanceWorld({ finance: 0, autorunLimit: 19101 }); + const outsideAutorun = buildFinanceWorld({ finance: 1 }); + const masterDisabled = buildFinanceWorld({ finance: 1, autorunLimit: 19101, nationTurn: 0 }); + const enabled = buildFinanceWorld({ finance: 1, autorunLimit: 19101, nationTurn: 1 }); + + expect(calculateNpcNationFinance(disabled, disabled.getNationById(1)!, 12, options)).toBeNull(); + expect( + calculateNpcNationFinance(outsideAutorun, outsideAutorun.getNationById(1)!, 12, options) + ).toBeNull(); + expect( + calculateNpcNationFinance(masterDisabled, masterDisabled.getNationById(1)!, 12, options) + ).toBeNull(); + expect(calculateNpcNationFinance(enabled, enabled.getNationById(1)!, 12, options)).toEqual( + expect.objectContaining({ rate: expect.any(Number), bill: expect.any(Number) }) + ); + }); }); diff --git a/app/game-engine/test/myInformationCommands.test.ts b/app/game-engine/test/myInformationCommands.test.ts index e7f37371..8bb47870 100644 --- a/app/game-engine/test/myInformationCommands.test.ts +++ b/app/game-engine/test/myInformationCommands.test.ts @@ -215,6 +215,10 @@ describe('my information world commands', () => { defence_train: 94, use_treatment: 200, use_auto_nation_turn: 0, + use_auto_nation_diplomacy: 1, + use_auto_nation_promotion: 1, + use_auto_nation_finance: 1, + use_auto_nation_capital: 1, }, }) ).resolves.toMatchObject({ ok: true }); @@ -227,6 +231,10 @@ describe('my information world commands', () => { defence_train: 999, use_treatment: 100, use_auto_nation_turn: 0, + use_auto_nation_diplomacy: 1, + use_auto_nation_promotion: 1, + use_auto_nation_finance: 1, + use_auto_nation_capital: 1, myset: 2, }, }); diff --git a/app/game-frontend/e2e/inGameMenus.spec.ts b/app/game-frontend/e2e/inGameMenus.spec.ts index 4b7294d0..ea13a384 100644 --- a/app/game-frontend/e2e/inGameMenus.spec.ts +++ b/app/game-frontend/e2e/inGameMenus.spec.ts @@ -275,6 +275,10 @@ const myGeneral = (state: FixtureState) => ({ defence_train: 80, use_treatment: 21, use_auto_nation_turn: 1, + use_auto_nation_diplomacy: 0, + use_auto_nation_promotion: 0, + use_auto_nation_finance: 0, + use_auto_nation_capital: 0, myset: state.myset, }, penalties: {}, @@ -1336,6 +1340,16 @@ test('내 정보&설정 keeps desktop density and becomes a 390px horizontal-ide '△(훈사40)', '× [훈련 -3,사기 -6]', ]); + const rulerAutomation = page.locator('.ruler-automation-settings'); + await expect(rulerAutomation).toBeVisible(); + const diplomacyAutomation = page.getByRole('checkbox', { name: '자동 외교 (불가침 제의·선전포고)' }); + const promotionAutomation = page.getByRole('checkbox', { name: '자동 수뇌 임명' }); + const financeAutomation = page.getByRole('checkbox', { name: '자동 세율·지급률 조정' }); + const capitalAutomation = page.getByRole('checkbox', { name: '자동 천도' }); + for (const checkbox of [diplomacyAutomation, promotionAutomation, financeAutomation, capitalAutomation]) { + await expect(checkbox).not.toBeChecked(); + await checkbox.check(); + } const desktop = await page.locator('#container').evaluate((element) => { const rect = element.getBoundingClientRect(); @@ -1394,6 +1408,12 @@ test('내 정보&설정 keeps desktop density and becomes a 390px horizontal-ide await page.locator('#set_my_setting').click(); await expect.poll(() => state.settingMutations.length).toBe(1); expect(state.settingMutations[0]).not.toHaveProperty('generalId'); + expect(state.settingMutations[0]).toMatchObject({ + use_auto_nation_diplomacy: 1, + use_auto_nation_promotion: 1, + use_auto_nation_finance: 1, + use_auto_nation_capital: 1, + }); for (const [effectIndex, scenarioEffect] of [ 'event_UnlimitedDefenceThresholdChange', diff --git a/app/game-frontend/src/views/MyPageView.vue b/app/game-frontend/src/views/MyPageView.vue index b1f220c6..86d84f53 100644 --- a/app/game-frontend/src/views/MyPageView.vue +++ b/app/game-frontend/src/views/MyPageView.vue @@ -31,6 +31,10 @@ type SettingForm = { defence_train: number; use_treatment: number; use_auto_nation_turn: number; + use_auto_nation_diplomacy: number; + use_auto_nation_promotion: number; + use_auto_nation_finance: number; + use_auto_nation_capital: number; }; const data = ref(null); @@ -63,6 +67,10 @@ const form = reactive({ defence_train: 80, use_treatment: 10, use_auto_nation_turn: 1, + use_auto_nation_diplomacy: 0, + use_auto_nation_promotion: 0, + use_auto_nation_finance: 0, + use_auto_nation_capital: 0, }); const logTypes: LogType[] = ['generalAction', 'battleDetail', 'generalHistory', 'battleResult']; @@ -164,6 +172,7 @@ const iconChoices = computed(() => data.value?.iconChoices ?? []); const selectedIcon = computed(() => iconChoices.value.find((icon) => icon.id === selectedIconId.value) ?? null); const autorunUser = computed(() => asRecord(world.value?.meta.autorun_user)); const showAutoNationTurn = computed(() => asRecord(autorunUser.value.options).chief !== false); +const showUserRulerAutomation = computed(() => showAutoNationTurn.value && (data.value?.general.npcState ?? 2) < 2); const showVacation = computed(() => autorunUser.value.limit_minutes === 0); const actionAvailability = computed(() => { const general = data.value?.general; @@ -409,6 +418,51 @@ onMounted(() => { ∞ 수뇌가 되었을 때 휴식 턴이어도 적당한 턴을 알아서 넣는 것을 허용합니다. +
+ 사용자 군주 자동 업무 + + + + +
∞ 모두 기본값은 꺼짐이며 자율행동 기간에 군주일 때만 적용됩니다.
+
+