feat: 사용자 군주 자율행동 업무를 개별 설정한다

NPC 군주의 기존 국가 운영은 유지하고 사용자 군주는 외교, 수뇌 임명, 재정 조정, 천도를 기본 비활성화한다.

내 정보 설정과 인증 API, 엔진 gate를 연결하고 자율행동 기간 및 국가턴 master를 함께 검증한다.
This commit is contained in:
2026-08-22 08:28:04 +00:00
parent 957445b0e0
commit 9c588dbd67
14 changed files with 309 additions and 28 deletions
+41 -17
View File
@@ -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 };
+37
View File
@@ -61,6 +61,43 @@ export const AVAILABLE_INSTANT_TURN: Record<string, boolean> = {
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 applyPriorityOverride = (priority: string[], override: unknown, flags: PolicyFlags): string[] => {