feat: 사용자 군주 자율행동 업무를 개별 설정한다
NPC 군주의 기존 국가 운영은 유지하고 사용자 군주는 외교, 수뇌 임명, 재정 조정, 천도를 기본 비활성화한다. 내 정보 설정과 인증 API, 엔진 gate를 연결하고 자율행동 기간 및 국가턴 master를 함께 검증한다.
This commit is contained in:
@@ -55,6 +55,10 @@ const zGeneralSettings = z.object({
|
|||||||
defence_train: z.number().int().optional(),
|
defence_train: z.number().int().optional(),
|
||||||
use_treatment: z.number().int().optional(),
|
use_treatment: z.number().int().optional(),
|
||||||
use_auto_nation_turn: 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']);
|
const zGeneralLogType = z.enum(['generalHistory', 'battleDetail', 'battleResult', 'generalAction']);
|
||||||
@@ -211,6 +215,12 @@ const resolveUserSettings = (meta: Record<string, unknown>) => {
|
|||||||
defence_train: readNumber(readSetting('defence_train'), 80),
|
defence_train: readNumber(readSetting('defence_train'), 80),
|
||||||
use_treatment: readNumber(readSetting('use_treatment'), 10),
|
use_treatment: readNumber(readSetting('use_treatment'), 10),
|
||||||
use_auto_nation_turn: readNumber(readSetting('use_auto_nation_turn'), 1),
|
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,
|
myset,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -584,6 +584,10 @@ describe('in-game my information ownership', () => {
|
|||||||
defence_train: 80,
|
defence_train: 80,
|
||||||
use_treatment: 21,
|
use_treatment: 21,
|
||||||
use_auto_nation_turn: 1,
|
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,
|
myset: 3,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,12 @@ import { GeneralActionPipeline } from '@sammo-ts/logic/actionModules/general.js'
|
|||||||
import type { ReservedTurnEntry } from '../../reservedTurnStore.js';
|
import type { ReservedTurnEntry } from '../../reservedTurnStore.js';
|
||||||
import type { TurnGeneral, TurnWorldState } from '../../types.js';
|
import type { TurnGeneral, TurnWorldState } from '../../types.js';
|
||||||
import type { AiCommandCandidate, AiReservedTurnProvider, AiWorldView } 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 {
|
import {
|
||||||
asRecord,
|
asRecord,
|
||||||
joinYearMonth,
|
joinYearMonth,
|
||||||
@@ -401,10 +406,10 @@ export class GeneralAI {
|
|||||||
this.categorizeNationCities();
|
this.categorizeNationCities();
|
||||||
this.categorizeNationGeneral();
|
this.categorizeNationGeneral();
|
||||||
|
|
||||||
if (this.general.npcState >= 2 && [3, 6, 9, 12].includes(this.world.currentMonth)) {
|
if ([3, 6, 9, 12].includes(this.world.currentMonth)) {
|
||||||
if (this.general.officerLevel === 12) {
|
if (this.general.officerLevel === 12 && canUseRulerAutomation(this.general, 'promotion')) {
|
||||||
this.chooseNpcPromotion();
|
this.chooseNpcPromotion();
|
||||||
} else {
|
} else if (this.general.npcState >= 2) {
|
||||||
this.chooseNonLordPromotion();
|
this.chooseNonLordPromotion();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -420,7 +425,7 @@ export class GeneralAI {
|
|||||||
if (!this.nationPolicy.can(actionName)) {
|
if (!this.nationPolicy.can(actionName)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (this.general.npcState < 2 && !AVAILABLE_INSTANT_TURN[actionName]) {
|
if (!canUseAutomatedNationAction(this.general, actionName)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const handler = nationActionHandlers[actionName];
|
const handler = nationActionHandlers[actionName];
|
||||||
@@ -1129,7 +1134,7 @@ export class GeneralAI {
|
|||||||
|
|
||||||
for (let chiefLevel = minChiefLevel; chiefLevel <= 12; chiefLevel += 1) {
|
for (let chiefLevel = minChiefLevel; chiefLevel <= 12; chiefLevel += 1) {
|
||||||
const chief = this.chiefGenerals[chiefLevel];
|
const chief = this.chiefGenerals[chiefLevel];
|
||||||
if (!chief) {
|
if (!chief || chief.id === this.general.id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const penalty = asRecord(chief.penalty);
|
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 minBelong = Math.min(readMetaNumber(asRecord(this.general.meta), 'belong', 0) - 1, 3);
|
||||||
const availableUserChiefCount = Object.values(this.userGenerals).filter((candidate) => {
|
const availableUserChiefCount = Object.values(this.userGenerals).filter((candidate) => {
|
||||||
|
if (candidate.id === this.general.id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const penalty = asRecord(candidate.penalty);
|
const penalty = asRecord(candidate.penalty);
|
||||||
const killturn = readRequiredMetaNumber(asRecord(candidate.meta), 'killturn', `generalId=${candidate.id}`);
|
const killturn = readRequiredMetaNumber(asRecord(candidate.meta), 'killturn', `generalId=${candidate.id}`);
|
||||||
return (
|
return (
|
||||||
@@ -1158,17 +1166,19 @@ export class GeneralAI {
|
|||||||
}).length;
|
}).length;
|
||||||
|
|
||||||
if (userChiefCount === 0 && availableUserChiefCount > 0 && (chiefSet & (1 << 11)) === 0) {
|
if (userChiefCount === 0 && availableUserChiefCount > 0 && (chiefSet & (1 << 11)) === 0) {
|
||||||
const userCandidates = Object.values(this.userGenerals).sort((left, right) => {
|
const userCandidates = Object.values(this.userGenerals)
|
||||||
const leftPenalty = asRecord(left.penalty);
|
.filter((candidate) => candidate.id !== this.general.id)
|
||||||
const rightPenalty = asRecord(right.penalty);
|
.sort((left, right) => {
|
||||||
if ((leftPenalty.noChief === true) !== (rightPenalty.noChief === true)) {
|
const leftPenalty = asRecord(left.penalty);
|
||||||
return leftPenalty.noChief === true ? 1 : -1;
|
const rightPenalty = asRecord(right.penalty);
|
||||||
}
|
if ((leftPenalty.noChief === true) !== (rightPenalty.noChief === true)) {
|
||||||
if ((leftPenalty.noAmbassador === true) !== (rightPenalty.noAmbassador === true)) {
|
return leftPenalty.noChief === true ? 1 : -1;
|
||||||
return leftPenalty.noAmbassador === true ? 1 : -1;
|
}
|
||||||
}
|
if ((leftPenalty.noAmbassador === true) !== (rightPenalty.noAmbassador === true)) {
|
||||||
return right.stats.leadership - left.stats.leadership;
|
return leftPenalty.noAmbassador === true ? 1 : -1;
|
||||||
});
|
}
|
||||||
|
return right.stats.leadership - left.stats.leadership;
|
||||||
|
});
|
||||||
for (const candidate of userCandidates) {
|
for (const candidate of userCandidates) {
|
||||||
const penalty = asRecord(candidate.penalty);
|
const penalty = asRecord(candidate.penalty);
|
||||||
const killturn = readRequiredMetaNumber(
|
const killturn = readRequiredMetaNumber(
|
||||||
@@ -1227,6 +1237,9 @@ export class GeneralAI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const nextChief = generals.find((candidate) => {
|
const nextChief = generals.find((candidate) => {
|
||||||
|
if (candidate.id === this.general.id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if ((effectiveOfficerLevel.get(candidate.id) ?? candidate.officerLevel) > 4) {
|
if ((effectiveOfficerLevel.get(candidate.id) ?? candidate.officerLevel) > 4) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1470,4 +1483,15 @@ export const shouldUseAi = (general: TurnGeneral, world: TurnWorldState): boolea
|
|||||||
return current < limit;
|
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 };
|
export type { GeneralAIOptions, GeneralAiDebugState };
|
||||||
|
|||||||
@@ -61,6 +61,43 @@ export const AVAILABLE_INSTANT_TURN: Record<string, boolean> = {
|
|||||||
NPC전방발령: true,
|
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<UserRulerAutomationFeature, string>;
|
||||||
|
|
||||||
|
const USER_RULER_ACTION_FEATURE: Readonly<Record<string, UserRulerAutomationFeature>> = {
|
||||||
|
불가침제의: '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 buildFlags = (entries: Array<[string, boolean]>): PolicyFlags => Object.fromEntries(entries);
|
||||||
|
|
||||||
const applyPriorityOverride = (priority: string[], override: unknown, flags: PolicyFlags): string[] => {
|
const applyPriorityOverride = (priority: string[], override: unknown, flags: PolicyFlags): string[] => {
|
||||||
|
|||||||
@@ -132,6 +132,10 @@ const zSetMySetting = z.object({
|
|||||||
defence_train: z.number().int().optional(),
|
defence_train: z.number().int().optional(),
|
||||||
use_treatment: z.number().int().optional(),
|
use_treatment: z.number().int().optional(),
|
||||||
use_auto_nation_turn: 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(),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ import {
|
|||||||
|
|
||||||
import type { TurnCalendarContext, TurnCalendarHandler, InMemoryTurnWorld } from './inMemoryWorld.js';
|
import type { TurnCalendarContext, TurnCalendarHandler, InMemoryTurnWorld } from './inMemoryWorld.js';
|
||||||
import { readNumber } from './ai/aiUtils.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 } => {
|
const calcNationDevelopedRate = (cities: City[]): { pop: number; all: number } => {
|
||||||
if (cities.length === 0) {
|
if (cities.length === 0) {
|
||||||
@@ -108,7 +109,9 @@ const resolveNpcMonarch = (nation: Nation, world: InMemoryTurnWorld) => {
|
|||||||
: world
|
: world
|
||||||
.listGenerals()
|
.listGenerals()
|
||||||
.find((general) => general.nationId === nation.id && general.officerLevel === 12) ?? null;
|
.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 = {
|
type NpcFinanceOptions = {
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ import {
|
|||||||
import { buildCommandEnv, buildReservedTurnDefinitions } from './reservedTurnCommands.js';
|
import { buildCommandEnv, buildReservedTurnDefinitions } from './reservedTurnCommands.js';
|
||||||
import { buildFrontStatePatches } from './frontStateHandler.js';
|
import { buildFrontStatePatches } from './frontStateHandler.js';
|
||||||
import { buildActionContext } from './reservedTurnActionContext.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 type { AiReservedTurnProvider } from './ai/types.js';
|
||||||
import { withCanonicalArgumentAliases } from './ai/aiUtils.js';
|
import { withCanonicalArgumentAliases } from './ai/aiUtils.js';
|
||||||
import { rankMetaKey } from './rankData.js';
|
import { rankMetaKey } from './rankData.js';
|
||||||
@@ -1672,7 +1672,7 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
let nationAiState: ReturnType<GeneralAI['getDebugState']> | undefined;
|
let nationAiState: ReturnType<GeneralAI['getDebugState']> | undefined;
|
||||||
let nationAiDecisionDurationNs = 0n;
|
let nationAiDecisionDurationNs = 0n;
|
||||||
let nationUsedAi = false;
|
let nationUsedAi = false;
|
||||||
if (worldView && shouldUseAi(currentGeneral, context.world)) {
|
if (worldView && shouldUseNationAi(currentGeneral, context.world)) {
|
||||||
nationUsedAi = true;
|
nationUsedAi = true;
|
||||||
const aiStartedAt = options.onActionProfiled ? process.hrtime.bigint() : 0n;
|
const aiStartedAt = options.onActionProfiled ? process.hrtime.bigint() : 0n;
|
||||||
sharedAi = new GeneralAI({
|
sharedAi = new GeneralAI({
|
||||||
|
|||||||
@@ -1702,7 +1702,17 @@ async function handleSetMySetting(
|
|||||||
nextMeta.use_treatment = Math.max(10, Math.min(100, settings.use_treatment));
|
nextMeta.use_treatment = Math.max(10, Math.min(100, settings.use_treatment));
|
||||||
}
|
}
|
||||||
if (settings.use_auto_nation_turn !== undefined) {
|
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;
|
let nextTrain = general.train;
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import { describe, expect, it } from 'vitest';
|
|||||||
import type { City, General, Nation } from '@sammo-ts/logic';
|
import type { City, General, Nation } from '@sammo-ts/logic';
|
||||||
import { createRefOrderedActionStack } from '@sammo-ts/logic/actionModules/bundle.js';
|
import { createRefOrderedActionStack } from '@sammo-ts/logic/actionModules/bundle.js';
|
||||||
|
|
||||||
import { GeneralAI } from '../src/turn/ai/generalAi.js';
|
import { GeneralAI, shouldUseNationAi } from '../src/turn/ai/generalAi.js';
|
||||||
import type { TurnGeneral } from '../src/turn/types.js';
|
import { canUseAutomatedNationAction, canUseRulerAutomation } from '../src/turn/ai/policies.js';
|
||||||
|
import type { TurnGeneral, TurnWorldState } from '../src/turn/types.js';
|
||||||
import {
|
import {
|
||||||
calculateRecentWarTurn,
|
calculateRecentWarTurn,
|
||||||
resolveLegacyAiStats,
|
resolveLegacyAiStats,
|
||||||
@@ -672,13 +673,13 @@ describe('legacy NPC user-chief promotion parity', () => {
|
|||||||
expect(ai.consumePromotionPatches().generals).toEqual([{ generalId: 2, officerLevel: 11, officerCity: 0 }]);
|
expect(ai.consumePromotionPatches().generals).toEqual([{ generalId: 2, officerLevel: 11, officerCity: 0 }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('runs automatic appointments only on the quarterly NPC nation turn', () => {
|
it('runs automatic appointments for NPC rulers and only opted-in user rulers in quarter months', () => {
|
||||||
const run = (currentMonth: number) => {
|
const run = (currentMonth: number, npcState = 2, enabled = 0) => {
|
||||||
const ruler = makePromotionGeneral({
|
const ruler = makePromotionGeneral({
|
||||||
id: 1,
|
id: 1,
|
||||||
officerLevel: 12,
|
officerLevel: 12,
|
||||||
npcState: 2,
|
npcState,
|
||||||
meta: { killturn: 100, belong: 2 },
|
meta: { killturn: 100, belong: 2, use_auto_nation_promotion: enabled },
|
||||||
});
|
});
|
||||||
const user = makePromotionGeneral({
|
const user = makePromotionGeneral({
|
||||||
id: 2,
|
id: 2,
|
||||||
@@ -710,6 +711,45 @@ describe('legacy NPC user-chief promotion parity', () => {
|
|||||||
|
|
||||||
expect(run(2)).toEqual([]);
|
expect(run(2)).toEqual([]);
|
||||||
expect(run(3)).toEqual([{ generalId: 2, officerLevel: 11, officerCity: 0, permission: 'ambassador' }]);
|
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -468,4 +468,52 @@ describe('core monthly event actions at the real month boundary', () => {
|
|||||||
expect(lowChiefBill).toBeTypeOf('number');
|
expect(lowChiefBill).toBeTypeOf('number');
|
||||||
expect(highChiefBill).toBe(lowChiefBill);
|
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) })
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -215,6 +215,10 @@ describe('my information world commands', () => {
|
|||||||
defence_train: 94,
|
defence_train: 94,
|
||||||
use_treatment: 200,
|
use_treatment: 200,
|
||||||
use_auto_nation_turn: 0,
|
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 });
|
).resolves.toMatchObject({ ok: true });
|
||||||
@@ -227,6 +231,10 @@ describe('my information world commands', () => {
|
|||||||
defence_train: 999,
|
defence_train: 999,
|
||||||
use_treatment: 100,
|
use_treatment: 100,
|
||||||
use_auto_nation_turn: 0,
|
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,
|
myset: 2,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -275,6 +275,10 @@ const myGeneral = (state: FixtureState) => ({
|
|||||||
defence_train: 80,
|
defence_train: 80,
|
||||||
use_treatment: 21,
|
use_treatment: 21,
|
||||||
use_auto_nation_turn: 1,
|
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,
|
myset: state.myset,
|
||||||
},
|
},
|
||||||
penalties: {},
|
penalties: {},
|
||||||
@@ -1336,6 +1340,16 @@ test('내 정보&설정 keeps desktop density and becomes a 390px horizontal-ide
|
|||||||
'△(훈사40)',
|
'△(훈사40)',
|
||||||
'× [훈련 -3,사기 -6]',
|
'× [훈련 -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 desktop = await page.locator('#container').evaluate((element) => {
|
||||||
const rect = element.getBoundingClientRect();
|
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 page.locator('#set_my_setting').click();
|
||||||
await expect.poll(() => state.settingMutations.length).toBe(1);
|
await expect.poll(() => state.settingMutations.length).toBe(1);
|
||||||
expect(state.settingMutations[0]).not.toHaveProperty('generalId');
|
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 [
|
for (const [effectIndex, scenarioEffect] of [
|
||||||
'event_UnlimitedDefenceThresholdChange',
|
'event_UnlimitedDefenceThresholdChange',
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ type SettingForm = {
|
|||||||
defence_train: number;
|
defence_train: number;
|
||||||
use_treatment: number;
|
use_treatment: number;
|
||||||
use_auto_nation_turn: 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<MyGeneralResponse | null>(null);
|
const data = ref<MyGeneralResponse | null>(null);
|
||||||
@@ -63,6 +67,10 @@ const form = reactive<SettingForm>({
|
|||||||
defence_train: 80,
|
defence_train: 80,
|
||||||
use_treatment: 10,
|
use_treatment: 10,
|
||||||
use_auto_nation_turn: 1,
|
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'];
|
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 selectedIcon = computed(() => iconChoices.value.find((icon) => icon.id === selectedIconId.value) ?? null);
|
||||||
const autorunUser = computed(() => asRecord(world.value?.meta.autorun_user));
|
const autorunUser = computed(() => asRecord(world.value?.meta.autorun_user));
|
||||||
const showAutoNationTurn = computed(() => asRecord(autorunUser.value.options).chief !== false);
|
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 showVacation = computed(() => autorunUser.value.limit_minutes === 0);
|
||||||
const actionAvailability = computed(() => {
|
const actionAvailability = computed(() => {
|
||||||
const general = data.value?.general;
|
const general = data.value?.general;
|
||||||
@@ -409,6 +418,51 @@ onMounted(() => {
|
|||||||
∞ 수뇌가 되었을 때 휴식 턴이어도 적당한 턴을 알아서 넣는 것을 허용합니다.
|
∞ 수뇌가 되었을 때 휴식 턴이어도 적당한 턴을 알아서 넣는 것을 허용합니다.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<fieldset
|
||||||
|
v-if="showUserRulerAutomation"
|
||||||
|
class="ruler-automation-settings"
|
||||||
|
:disabled="form.use_auto_nation_turn === 0"
|
||||||
|
>
|
||||||
|
<legend>사용자 군주 자동 업무</legend>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
v-model="form.use_auto_nation_diplomacy"
|
||||||
|
type="checkbox"
|
||||||
|
:true-value="1"
|
||||||
|
:false-value="0"
|
||||||
|
/>
|
||||||
|
자동 외교 (불가침 제의·선전포고)
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
v-model="form.use_auto_nation_promotion"
|
||||||
|
type="checkbox"
|
||||||
|
:true-value="1"
|
||||||
|
:false-value="0"
|
||||||
|
/>
|
||||||
|
자동 수뇌 임명
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
v-model="form.use_auto_nation_finance"
|
||||||
|
type="checkbox"
|
||||||
|
:true-value="1"
|
||||||
|
:false-value="0"
|
||||||
|
/>
|
||||||
|
자동 세율·지급률 조정
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
v-model="form.use_auto_nation_capital"
|
||||||
|
type="checkbox"
|
||||||
|
:true-value="1"
|
||||||
|
:false-value="0"
|
||||||
|
/>
|
||||||
|
자동 천도
|
||||||
|
</label>
|
||||||
|
<div class="hint">∞ 모두 기본값은 꺼짐이며 자율행동 기간에 군주일 때만 적용됩니다.</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
<label class="setting-line">
|
<label class="setting-line">
|
||||||
수비 【
|
수비 【
|
||||||
<select
|
<select
|
||||||
@@ -730,6 +784,21 @@ button:disabled {
|
|||||||
margin: 0 0 13px;
|
margin: 0 0 13px;
|
||||||
color: orange;
|
color: orange;
|
||||||
}
|
}
|
||||||
|
.ruler-automation-settings {
|
||||||
|
margin: 8px 0 13px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 1px solid #666;
|
||||||
|
}
|
||||||
|
.ruler-automation-settings legend {
|
||||||
|
padding: 0 4px;
|
||||||
|
color: skyblue;
|
||||||
|
}
|
||||||
|
.ruler-automation-settings label {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.ruler-automation-settings .hint {
|
||||||
|
margin: 5px 0 0;
|
||||||
|
}
|
||||||
.action-button {
|
.action-button {
|
||||||
--legacy-button-height: 30px;
|
--legacy-button-height: 30px;
|
||||||
--legacy-button-bg: #225500;
|
--legacy-button-bg: #225500;
|
||||||
|
|||||||
@@ -111,6 +111,10 @@ export type TurnDaemonCommand =
|
|||||||
defence_train?: number;
|
defence_train?: number;
|
||||||
use_treatment?: number;
|
use_treatment?: number;
|
||||||
use_auto_nation_turn?: 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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
| { type: 'dropItem'; requestId?: string; generalId: number; itemType: string }
|
| { type: 'dropItem'; requestId?: string; generalId: number; itemType: string }
|
||||||
|
|||||||
Reference in New Issue
Block a user