fix(ai): align NPC final decisions with legacy

This commit is contained in:
2026-07-25 11:23:53 +00:00
parent a83f7a44e3
commit 61159209bf
19 changed files with 1233 additions and 287 deletions
+43 -16
View File
@@ -36,7 +36,7 @@ import { WorldStateView } from './worldStateView.js';
import type { GeneralAIOptions, GeneralAiDebugState } from './types.js';
const ACTION_REST = '휴식';
const lastAttackableByNation = new Map<number, number>();
const lastAttackableByWorld = new WeakMap<object, Map<number, number>>();
const t무장 = 1;
const t지장 = 2;
@@ -129,11 +129,12 @@ export class GeneralAI {
private readonly reservedTurnProvider: AiReservedTurnProvider;
constructor(options: GeneralAIOptions) {
this.general = options.general;
this.general = { ...options.general, meta: { ...options.general.meta } };
this.city = options.city;
this.nation =
const nation =
options.nation ??
(options.general.nationId > 0 ? options.worldRef?.getNationById(options.general.nationId) ?? null : null);
(options.general.nationId > 0 ? (options.worldRef?.getNationById(options.general.nationId) ?? null) : null);
this.nation = nation ? { ...nation, meta: { ...nation.meta } } : nation;
this.world = options.world;
this.worldRef = options.worldRef;
this.map = options.map;
@@ -255,25 +256,39 @@ export class GeneralAI {
return null;
}
const npcMessage = asRecord(this.general.meta).npcmsg;
if (npcMessage && this.rng.nextBool((this.aiConst.npcMessageFreqByDay * this.turnTermMinutes) / (60 * 24))) {
// 메시지 영속화는 turn handler가 담당한다. 여기서는 레거시와 같은 RNG 소비를 보존한다.
}
if (this.general.npcState >= 2) {
this.general.meta = { ...this.general.meta, defence_train: 80 };
}
if (this.general.officerLevel === 12 && this.generalPolicy.can('선양')) {
const abdication = generalActionHandlers['선양']?.(this);
if (abdication) {
return abdication;
}
}
if (this.general.npcState === 5) {
if (this.general.nationId === 0) {
this.general.meta = { ...this.general.meta, killturn: 1 };
return { action: reservedTurn.action, args: reservedTurn.args, reason: '사망' };
}
const result = generalActionHandlers['집합']?.(this);
return result ?? this.buildGeneralCandidate(ACTION_REST, {}, 'npc_troop');
}
if (reservedTurn.action !== ACTION_REST) {
const reservedCandidate = this.buildGeneralCandidate(reservedTurn.action, reservedTurn.args, 'reserved');
if (reservedCandidate) {
return reservedCandidate;
}
return { action: reservedTurn.action, args: reservedTurn.args, reason: 'do예약턴' };
}
if (
readMetaNumber(asRecord(this.general.meta), 'injury', this.general.injury) > this.nationPolicy.cureThreshold
) {
const heal = this.buildGeneralCandidate('che_요양', {}, 'heal');
if (heal) {
return heal;
}
return { action: 'che_요양', args: {}, reason: 'do요양' };
}
if ([2, 3].includes(this.general.npcState) && this.general.nationId === 0) {
@@ -293,7 +308,7 @@ export class GeneralAI {
}
if (this.general.npcState < 2 && this.general.nationId === 0 && !this.generalPolicy.can('국가선택')) {
return this.buildGeneralCandidate(ACTION_REST, {}, 'neutral_user');
return { action: reservedTurn.action, args: reservedTurn.args, reason: '재야유저' };
}
if (this.general.npcState >= 2 && this.general.officerLevel === 12 && !this.nation?.capitalCityId) {
@@ -312,6 +327,12 @@ export class GeneralAI {
if (move) {
return move;
}
if (relYearMonth > 1) {
const disband = generalActionHandlers['해산']?.(this);
if (disband) {
return disband;
}
}
}
for (const actionName of this.generalPolicy.priority) {
@@ -757,11 +778,17 @@ export class GeneralAI {
const declareTerms = warTargets.filter((entry) => entry.state === 1).map((entry) => entry.term);
const minWarTerm = declareTerms.length > 0 ? Math.min(...declareTerms) : null;
let lastAttackable = lastAttackableByNation.get(nationId) ??
readMetaNumber(asRecord(this.nation.meta), 'last_attackable', 0);
let worldLastAttackable = lastAttackableByWorld.get(this.world.meta);
if (!worldLastAttackable) {
worldLastAttackable = new Map();
lastAttackableByWorld.set(this.world.meta, worldLastAttackable);
}
let lastAttackable =
worldLastAttackable.get(nationId) ?? readMetaNumber(asRecord(this.nation.meta), 'last_attackable', 0);
const markAttackable = () => {
lastAttackable = yearMonth;
lastAttackableByNation.set(nationId, yearMonth);
worldLastAttackable.set(nationId, yearMonth);
this.nation!.meta = { ...this.nation!.meta, last_attackable: yearMonth };
};
if (minWarTerm === null) {
@@ -1,7 +1,20 @@
import type { GeneralAI } from '../core.js';
import { valueFit } from '../../aiUtils.js';
import { asRecord, readMetaNumber, valueFit } from '../../aiUtils.js';
import { pickWeightedCandidate, resolveCityTrust, t무장, t지장, t통솔장 } from './helpers.js';
const isTechLimited = (ai: GeneralAI, tech: number): boolean => {
const relativeYear = Math.max(0, ai.world.currentYear - ai.startYear);
const levelIncreaseYears = ai.commandEnv.techLevelIncYear ?? 5;
const initialAllowedLevel = ai.commandEnv.initialAllowedTechLevel ?? 1;
const relativeMaxLevel = valueFit(
Math.floor(relativeYear / levelIncreaseYears) + initialAllowedLevel,
1,
ai.commandEnv.maxTechLevel
);
const currentLevel = valueFit(Math.floor(tech / 1000), 0, ai.commandEnv.maxTechLevel);
return currentLevel >= relativeMaxLevel;
};
export const do일반내정 = (ai: GeneralAI) => {
const city = ai.city;
const nation = ai.nation;
@@ -14,6 +27,7 @@ export const do일반내정 = (ai: GeneralAI) => {
}
const develRate = ai.calcCityDevelRate(city);
const tech = readMetaNumber(asRecord(nation.meta), 'tech', 0);
const isSpringSummer = ai.world.currentMonth <= 6;
const cmdList: Array<[ReturnType<GeneralAI['buildGeneralCandidate']>, number]> = [];
@@ -64,7 +78,13 @@ export const do일반내정 = (ai: GeneralAI) => {
}
if (ai.genType & t지장) {
cmdList.push([ai.buildGeneralCandidate('che_기술연구', {}, '일반내정'), ai.general.stats.intelligence]);
if (!isTechLimited(ai, tech)) {
const nextTech = (tech % 1000) + 1;
const weight = !isTechLimited(ai, tech + 1000)
? ai.general.stats.intelligence / (nextTech / 2000)
: ai.general.stats.intelligence;
cmdList.push([ai.buildGeneralCandidate('che_기술연구', {}, '일반내정'), weight]);
}
if (develRate.agri[0] < 1) {
cmdList.push([
ai.buildGeneralCandidate('che_농지개간', {}, '일반내정'),
@@ -119,6 +139,7 @@ export const do전쟁내정 = (ai: GeneralAI) => {
return null;
}
const develRate = ai.calcCityDevelRate(city);
const tech = readMetaNumber(asRecord(nation.meta), 'tech', 0);
const isSpringSummer = ai.world.currentMonth <= 6;
const cmdList: Array<[ReturnType<GeneralAI['buildGeneralCandidate']>, number]> = [];
@@ -130,10 +151,9 @@ export const do전쟁내정 = (ai: GeneralAI) => {
]);
}
if (develRate.pop[0] < 0.8) {
const weight =
city.frontState > 0
? ai.general.stats.leadership / valueFit(develRate.pop[0], 0.001)
: ai.general.stats.leadership / valueFit(develRate.pop[0], 0.001) / 2;
const weight = [1, 3].includes(city.frontState)
? ai.general.stats.leadership / valueFit(develRate.pop[0], 0.001)
: ai.general.stats.leadership / valueFit(develRate.pop[0], 0.001) / 2;
cmdList.push([ai.buildGeneralCandidate('che_정착장려', {}, '전쟁내정'), weight]);
}
}
@@ -160,27 +180,31 @@ export const do전쟁내정 = (ai: GeneralAI) => {
}
if (ai.genType & t지장) {
cmdList.push([ai.buildGeneralCandidate('che_기술연구', {}, '전쟁내정'), ai.general.stats.intelligence]);
if (!isTechLimited(ai, tech)) {
const nextTech = (tech % 1000) + 1;
const weight = !isTechLimited(ai, tech + 1000)
? ai.general.stats.intelligence / (nextTech / 3000)
: ai.general.stats.intelligence;
cmdList.push([ai.buildGeneralCandidate('che_기술연구', {}, '전쟁내정'), weight]);
}
if (develRate.agri[0] < 0.5) {
const weight =
city.frontState > 0
? ((isSpringSummer ? 1.2 : 0.8) * ai.general.stats.intelligence) /
4 /
valueFit(develRate.agri[0], 0.001, 1)
: ((isSpringSummer ? 1.2 : 0.8) * ai.general.stats.intelligence) /
2 /
valueFit(develRate.agri[0], 0.001, 1);
const weight = [1, 3].includes(city.frontState)
? ((isSpringSummer ? 1.2 : 0.8) * ai.general.stats.intelligence) /
4 /
valueFit(develRate.agri[0], 0.001, 1)
: ((isSpringSummer ? 1.2 : 0.8) * ai.general.stats.intelligence) /
2 /
valueFit(develRate.agri[0], 0.001, 1);
cmdList.push([ai.buildGeneralCandidate('che_농지개간', {}, '전쟁내정'), weight]);
}
if (develRate.comm[0] < 0.5) {
const weight =
city.frontState > 0
? ((isSpringSummer ? 0.8 : 1.2) * ai.general.stats.intelligence) /
4 /
valueFit(develRate.comm[0], 0.001, 1)
: ((isSpringSummer ? 0.8 : 1.2) * ai.general.stats.intelligence) /
2 /
valueFit(develRate.comm[0], 0.001, 1);
const weight = [1, 3].includes(city.frontState)
? ((isSpringSummer ? 0.8 : 1.2) * ai.general.stats.intelligence) /
4 /
valueFit(develRate.comm[0], 0.001, 1)
: ((isSpringSummer ? 0.8 : 1.2) * ai.general.stats.intelligence) /
2 /
valueFit(develRate.comm[0], 0.001, 1);
cmdList.push([ai.buildGeneralCandidate('che_상업투자', {}, '전쟁내정'), weight]);
}
}
@@ -5,7 +5,7 @@ import { do징병 } from './recruitActions.js';
import { do전투준비, do소집해제, do출병 } from './warActions.js';
import { do후방워프, do전방워프, do내정워프, do귀환, do집합 } from './warpActions.js';
import { doNPC헌납, doNPC사망대비 } from './npcActions.js';
import { do국가선택, do중립, do거병, do건국, do방랑군이동 } from './politicsActions.js';
import { do국가선택, do중립, do거병, do건국, do해산, do선양, do방랑군이동 } from './politicsActions.js';
export {
do일반내정,
@@ -27,6 +27,8 @@ export {
do중립,
do거병,
do건국,
do해산,
do선양,
do방랑군이동,
};
@@ -53,5 +55,7 @@ export const generalActionHandlers: Record<
집합: do집합,
거병: do거병,
건국: do건국,
해산: do해산,
선양: do선양,
방랑군이동: do방랑군이동,
};
@@ -18,6 +18,27 @@ export const do국가선택 = (ai: GeneralAI) => {
}
if (ai.rng.nextBool(0.3)) {
const affinity = ai.general.affinity ?? readMetaNumber(asRecord(ai.general.meta), 'affinity', 0);
if (affinity === 999) {
return null;
}
if (ai.world.currentYear < ai.startYear + 3) {
const nations = ai.worldRef.listNations();
const nationCount = nations.length;
const notFullNationCount = nations.filter((nation) => {
const count = ai.worldRef!.listGenerals().filter((general) => general.nationId === nation.id).length;
return count < ai.commandEnv.initialNationGenLimit;
}).length;
if (nationCount === 0 || notFullNationCount === 0) {
return null;
}
const rejectProbability = Math.pow(1 / (nationCount + 1) / Math.pow(notFullNationCount, 3), 1 / 4);
if (ai.rng.nextBool(rejectProbability)) {
return null;
}
} else if (ai.rng.nextBool()) {
return null;
}
return ai.buildGeneralCandidate('che_랜덤임관', {}, '국가선택');
}
@@ -47,13 +68,15 @@ export const do중립 = (ai: GeneralAI) => {
candidates = ['che_물자조달'];
}
for (const key of candidates) {
const cmd = ai.buildGeneralCandidate(key, {}, '중립');
if (cmd) {
return cmd;
}
const picked = ai.buildGeneralCandidate(ai.rng.choice(candidates), {}, '중립');
if (picked) {
return picked;
}
return ai.buildGeneralCandidate(ACTION_REST, {}, '중립');
const supply = ai.buildGeneralCandidate('che_물자조달', {}, '중립');
if (supply) {
return supply;
}
return ai.buildGeneralCandidate('che_견문', {}, '중립') ?? ai.buildGeneralCandidate(ACTION_REST, {}, '중립');
};
export const do거병 = (ai: GeneralAI) => {
@@ -111,13 +134,18 @@ export const do거병 = (ai: GeneralAI) => {
}
const prop = (ai.rng.nextFloat1() * (ai.aiConst.defaultStatNpcMax + ai.aiConst.chiefStatMin)) / 2;
const ratio = (ai.general.stats.leadership + ai.general.stats.strength + ai.general.stats.intelligence) / 3;
const generalMeta = asRecord(ai.general.meta);
const ratio =
(readMetaNumber(generalMeta, 'fullLeadership', ai.general.stats.leadership) +
readMetaNumber(generalMeta, 'fullStrength', ai.general.stats.strength) +
readMetaNumber(generalMeta, 'fullIntelligence', ai.general.stats.intelligence)) /
3;
if (prop >= ratio) {
return null;
}
const relYear = Math.max(0, ai.world.currentYear - ai.startYear);
const more = valueFit(3 - relYear, 1, 3);
const initYear = readMetaNumber(asRecord(ai.world.meta), 'initYear', ai.startYear);
const more = valueFit(3 - ai.world.currentYear + initYear, 1, 3);
if (!ai.rng.nextBool(0.0075 * more)) {
return null;
}
@@ -132,10 +160,39 @@ export const do건국 = (ai: GeneralAI) => {
ai.aiConst.availableNationTypes.length > 0
? (ai.rng.choice(ai.aiConst.availableNationTypes) as string)
: `${prefix}def`;
const colorType = ai.rng.nextRangeInt(0, 34);
const nationName = ai.general.name;
const colorType = ai.rng.nextRangeInt(0, 32);
const nationName = `${Array.from(ai.general.name).slice(1).join('')}`;
return ai.buildGeneralCandidate('che_건국', { nationName, nationType, colorType }, '건국');
const result = ai.buildGeneralCandidate('che_건국', { nationName, nationType, colorType }, '건국');
if (result) {
const nextMeta = { ...ai.general.meta };
delete nextMeta.movingTargetCityID;
ai.general.meta = nextMeta;
}
return result;
};
export const do해산 = (ai: GeneralAI) => {
const result = ai.buildGeneralCandidate('che_해산', {}, '해산');
if (result) {
const nextMeta = { ...ai.general.meta };
delete nextMeta.movingTargetCityID;
ai.general.meta = nextMeta;
}
return result;
};
export const do선양 = (ai: GeneralAI) => {
if (!ai.worldRef) {
return null;
}
const candidates = ai.worldRef
.listGenerals()
.filter((general) => general.nationId === ai.general.nationId && general.npcState !== 5);
if (candidates.length === 0) {
return null;
}
return ai.buildGeneralCandidate('che_선양', { destGeneralID: ai.rng.choice(candidates).id }, '선양');
};
export const do방랑군이동 = (ai: GeneralAI) => {
@@ -143,37 +200,76 @@ export const do방랑군이동 = (ai: GeneralAI) => {
if (!city || !ai.map || !ai.worldRef) {
return null;
}
const lordCities = ai.worldRef
.listGenerals()
.filter((general) => general.officerLevel === 12 && general.nationId === 0)
.map((general) => general.cityId);
if (lordCities.filter((cityId) => cityId === city.id).length <= 1 && [5, 6].includes(city.level)) {
return null;
}
const occupied = new Set(
ai.worldRef
.listCities()
.filter((c) => c.nationId !== 0)
.map((c) => c.id)
.filter((candidate) => candidate.nationId !== 0)
.map((candidate) => candidate.id)
);
for (const general of ai.worldRef.listGenerals()) {
if (general.officerLevel === 12 && general.nationId === 0) {
occupied.add(general.cityId);
}
for (const cityId of lordCities) {
occupied.add(cityId);
}
const nearby = searchDistance(ai.map, city.id, 4);
const candidates: Array<[number, number]> = [];
for (const [cityIdRaw, dist] of Object.entries(nearby)) {
const cityId = Number(cityIdRaw);
if (!Number.isFinite(cityId) || occupied.has(cityId)) {
continue;
}
const target = ai.worldRef.getCityById(cityId);
if (!target || target.level < 5 || target.level > 6) {
continue;
}
candidates.push([cityId, 1 / Math.pow(2, dist)]);
let movingTargetCityId = readMetaNumber(asRecord(ai.general.meta), 'movingTargetCityID', 0) || null;
if (movingTargetCityId === city.id || (movingTargetCityId !== null && occupied.has(movingTargetCityId))) {
movingTargetCityId = null;
}
if (candidates.length === 0) {
return null;
if (movingTargetCityId === null) {
const nearby = searchDistance(ai.map, city.id, 4);
const candidates: Array<[number, number]> = [];
for (const [cityIdRaw, dist] of Object.entries(nearby)) {
const cityId = Number(cityIdRaw);
if (!Number.isFinite(cityId) || occupied.has(cityId)) {
continue;
}
const target = ai.worldRef.getCityById(cityId);
if (!target || target.level < 5 || target.level > 6) {
continue;
}
candidates.push([cityId, 1 / Math.pow(2, dist)]);
}
if (candidates.length === 0) {
return null;
}
movingTargetCityId = ai.rng.choiceUsingWeightPair(candidates);
ai.general.meta = { ...ai.general.meta, movingTargetCityID: movingTargetCityId };
}
const destCityId = ai.rng.choiceUsingWeightPair(candidates);
if (destCityId === city.id) {
if (movingTargetCityId === city.id) {
return ai.buildGeneralCandidate('che_인재탐색', {}, '방랑군이동');
}
return ai.buildGeneralCandidate('che_이동', { destCityId }, '방랑군이동');
const distanceMap = searchDistance(ai.map, movingTargetCityId, 99);
const targetDistance = distanceMap[city.id];
if (targetDistance === undefined) {
return null;
}
const neighbors = ai.map.cities.find((candidate) => candidate.id === city.id)?.connections ?? [];
const nextCandidates: Array<[number, number]> = [];
for (const nextCityId of neighbors) {
const nextCity = ai.worldRef.getCityById(nextCityId);
if (nextCity && [5, 6].includes(nextCity.level) && !occupied.has(nextCityId)) {
nextCandidates.push([nextCityId, 10]);
}
if (distanceMap[nextCityId] !== undefined && distanceMap[nextCityId] + 1 === targetDistance) {
nextCandidates.push([nextCityId, 1]);
}
}
if (nextCandidates.length === 0) {
return null;
}
return ai.buildGeneralCandidate(
'che_이동',
{ destCityId: ai.rng.choiceUsingWeightPair(nextCandidates) },
'방랑군이동'
);
};
@@ -9,7 +9,7 @@ import type { CrewTypeDefinition, General, WarArmTypes } from '@sammo-ts/logic';
import type { GeneralAI } from '../core.js';
import { asRecord, readMetaNumber, roundTo } from '../../aiUtils.js';
import { t통솔장 } from './helpers.js';
import { t무장, t지장, t통솔장 } from './helpers.js';
export const buildRecruitArmTypeWeights = (general: General, armTypes: WarArmTypes): Array<[number, number]> => {
const meta = asRecord(general.meta);
@@ -45,7 +45,7 @@ export const do징병 = (ai: GeneralAI) => {
if (!city || !nation || !ai.unitSet || !ai.map) {
return null;
}
if ([0, 1].includes(ai.dipState) && ai.general.npcState < 2) {
if ([0, 1].includes(ai.dipState)) {
return null;
}
if (!(ai.genType & t통솔장)) {
@@ -55,9 +55,10 @@ export const do징병 = (ai: GeneralAI) => {
return null;
}
const generalMeta = asRecord(ai.general.meta);
const fullLeadership = readMetaNumber(generalMeta, 'fullLeadership', ai.general.stats.leadership);
if (!ai.generalPolicy.can('한계징병')) {
const remainPop =
city.population - ai.nationPolicy.minNpcRecruitCityPopulation - ai.general.stats.leadership * 100;
const remainPop = city.population - ai.nationPolicy.minNpcRecruitCityPopulation - fullLeadership * 100;
if (remainPop <= 0) {
return null;
}
@@ -71,9 +72,16 @@ export const do징병 = (ai: GeneralAI) => {
}
const tech = readMetaNumber(asRecord(nation.meta), 'tech', 0);
const crewAmountBase = ai.general.stats.leadership * 100;
const crewAmountBase = fullLeadership * 100;
const warConfig = buildWarConfig(ai.scenarioConfig, ai.unitSet);
const forcedArmType = readMetaNumber(asRecord(ai.general.meta), 'armType', 0);
let forcedArmType = readMetaNumber(asRecord(ai.general.meta), 'armType', 0);
if (
(forcedArmType === warConfig.armTypes.wizard && !(ai.genType & t지장)) ||
([warConfig.armTypes.footman, warConfig.armTypes.archer, warConfig.armTypes.cavalry].includes(forcedArmType) &&
!(ai.genType & t무장))
) {
forcedArmType = 0;
}
const armType =
forcedArmType > 0
? forcedArmType
@@ -123,25 +131,31 @@ export const do징병 = (ai: GeneralAI) => {
let crewAmount = crewAmountBase;
const goldCost = (picked.cost * getTechCost(tech) * crewAmount) / 100;
const riceCost = crewAmount / 100;
const killCrew = readMetaNumber(generalMeta, 'rank_killcrew', readMetaNumber(generalMeta, 'killcrew', 0));
const deathCrew = readMetaNumber(generalMeta, 'rank_deathcrew', readMetaNumber(generalMeta, 'deathcrew', 0));
const expectedCrewLoss = Math.floor((crewAmount * killCrew * 1.2) / Math.max(deathCrew, 1));
let riceCost = (picked.rice * getTechCost(tech) * expectedCrewLoss) / 100;
if (ai.general.gold <= 0 || ai.general.rice <= 0) {
const remainingGold = ai.general.gold - fullLeadership * 3;
const remainingRice = ai.general.rice - fullLeadership * 4;
if (remainingGold <= 0 || remainingRice <= 0) {
return null;
}
if (ai.generalPolicy.can('모병') && ai.general.gold >= goldCost * 6) {
if (ai.generalPolicy.can('모병') && remainingGold >= goldCost * 6) {
const hire = ai.buildGeneralCandidate('che_모병', { crewType: crewTypeId, amount: crewAmount }, '징병');
if (hire) {
return hire;
}
}
if (ai.general.gold < goldCost && ai.general.gold * 2 >= goldCost) {
if (remainingGold < goldCost && remainingGold * 2 >= goldCost) {
crewAmount *= 0.5;
riceCost *= 0.5;
crewAmount = roundTo(crewAmount - 49, -2);
}
if (!ai.generalPolicy.can('한계징병') && ai.general.rice * 1.1 <= riceCost) {
if (!ai.generalPolicy.can('한계징병') && remainingRice * 1.1 <= riceCost) {
return null;
}
@@ -3,7 +3,7 @@ import { valueFit } from '../../aiUtils.js';
import { pickWeightedCandidate } from './helpers.js';
export const do전투준비 = (ai: GeneralAI) => {
if ([0, 1].includes(ai.dipState) && ai.general.crew <= 0) {
if ([0, 1].includes(ai.dipState)) {
return null;
}
const cmdList: Array<[ReturnType<GeneralAI['buildGeneralCandidate']>, number]> = [];
@@ -1,4 +1,5 @@
import type { GeneralAI } from '../core.js';
import { asRecord, readRequiredMetaNumber } from '../../aiUtils.js';
import { t통솔장 } from './helpers.js';
export const do후방워프 = (ai: GeneralAI) => {
@@ -9,6 +10,9 @@ export const do후방워프 = (ai: GeneralAI) => {
if ([0, 1].includes(ai.dipState)) {
return null;
}
if (!ai.generalPolicy.can('징병')) {
return null;
}
if (!(ai.genType & t통솔장)) {
return null;
}
@@ -104,6 +108,7 @@ export const do전방워프 = (ai: GeneralAI) => {
}
ai.categorizeNationCities();
ai.categorizeNationGeneral();
const candidateCities: Record<number, number> = {};
for (const frontCity of Object.values(ai.frontCities)) {
if (frontCity.supplyState <= 0) {
@@ -195,4 +200,11 @@ export const do귀환 = (ai: GeneralAI) => {
return ai.buildGeneralCandidate('che_귀환', {}, '귀환');
};
export const do집합 = (ai: GeneralAI) => ai.buildGeneralCandidate('che_집합', {}, '집합');
export const do집합 = (ai: GeneralAI) => {
if (ai.general.npcState === 5) {
const killturn = readRequiredMetaNumber(asRecord(ai.general.meta), 'killturn', `generalId=${ai.general.id}`);
const nextKillturn = ((killturn + ai.rng.nextRangeInt(2, 4)) % 5) + 70;
ai.general.meta = { ...ai.general.meta, killturn: nextKillturn };
}
return ai.buildGeneralCandidate('che_집합', {}, '집합');
};
@@ -14,7 +14,25 @@ export const do천도 = (ai: GeneralAI) => {
return null;
}
const cityIds = nationCities.map((city) => city.id);
const nationCityIds = new Set(nationCities.map((city) => city.id));
const connectedCityIds = new Set<number>([ai.nation.capitalCityId]);
const queue = [ai.nation.capitalCityId];
while (queue.length > 0) {
const cityId = queue.shift()!;
const connections = ai.map.cities.find((city) => city.id === cityId)?.connections ?? [];
for (const nextCityId of connections) {
if (!nationCityIds.has(nextCityId) || connectedCityIds.has(nextCityId)) {
continue;
}
connectedCityIds.add(nextCityId);
queue.push(nextCityId);
}
}
if (connectedCityIds.size <= 1) {
return null;
}
const cityIds = Array.from(connectedCityIds);
const distanceList = searchAllDistanceByCityList(ai.map, cityIds);
const capitalId = ai.nation.capitalCityId;
if (!distanceList[capitalId]) {
@@ -28,7 +46,7 @@ export const do천도 = (ai: GeneralAI) => {
}
const cityScores: Record<number, number> = {};
for (const city of nationCities) {
for (const city of nationCities.filter((candidate) => connectedCityIds.has(candidate.id))) {
const sumDistance = Object.values(distanceList[city.id] ?? {}).reduce((acc, value) => acc + value, 0);
if (sumDistance <= 0) {
continue;
@@ -39,7 +57,7 @@ export const do천도 = (ai: GeneralAI) => {
const sorted = Object.entries(cityScores).sort((a, b) => b[1] - a[1]);
const topLimit = Math.ceil(sorted.length * 0.25);
for (let idx = 0; idx < Math.min(topLimit, sorted.length); idx += 1) {
for (let idx = 0; idx <= Math.min(topLimit, sorted.length - 1); idx += 1) {
if (Number(sorted[idx][0]) === capitalId) {
return null;
}
@@ -62,5 +80,5 @@ export const do천도 = (ai: GeneralAI) => {
}
}
return ai.buildNationCandidate('che_천도', { destCityId: targetCityId }, '천도');
return ai.buildNationCandidate('che_천도', { destCityID: targetCityId }, '천도');
};
@@ -3,6 +3,18 @@ import { asRecord, joinYearMonth, parseYearMonth, readMetaNumber } from '../../a
import { isNeighbor } from '../../distance.js';
import { resolveNationIncome } from './helpers.js';
const isTechLimited = (ai: GeneralAI, tech: number): boolean => {
const relativeYear = Math.max(0, ai.world.currentYear - ai.startYear);
const levelIncreaseYears = ai.commandEnv.techLevelIncYear ?? 5;
const initialAllowedLevel = ai.commandEnv.initialAllowedTechLevel ?? 1;
const relativeMaxLevel = Math.max(
1,
Math.min(Math.floor(relativeYear / levelIncreaseYears) + initialAllowedLevel, ai.commandEnv.maxTechLevel)
);
const techLevel = Math.max(0, Math.min(Math.floor(tech / 1000), ai.commandEnv.maxTechLevel));
return techLevel >= relativeMaxLevel;
};
export const do불가침제의 = (ai: GeneralAI) => {
if (!ai.nation || ai.general.officerLevel < 12) {
return null;
@@ -66,11 +78,16 @@ export const do불가침제의 = (ai: GeneralAI) => {
}
const [targetYear, targetMonth] = parseYearMonth(Math.floor(yearMonth + diplomatMonth));
return ai.buildNationCandidate(
const result = ai.buildNationCandidate(
'che_불가침제의',
{ destNationId, year: targetYear, month: targetMonth },
'불가침제의'
);
if (result) {
const nextTry = { ...respAssistTry, [`n${destNationId}`]: [destNationId, yearMonth] };
asRecord(ai.nation.meta).resp_assist_try = nextTry;
}
return result;
};
export const do선전포고 = (ai: GeneralAI) => {
@@ -92,6 +109,10 @@ export const do선전포고 = (ai: GeneralAI) => {
if (!ai.map || !ai.worldRef) {
return null;
}
const currentTech = readMetaNumber(asRecord(ai.nation.meta), 'tech', 0);
if (!isTechLimited(ai, currentTech + 1000)) {
return null;
}
const avgResources = Object.values({
...ai.npcWarGenerals,
@@ -134,9 +155,26 @@ export const do선전포고 = (ai: GeneralAI) => {
return null;
}
const lowTargetNations = new Set(
ai.worldRef
.listDiplomacy()
.filter((entry) => entry.fromNationId !== currentNationId && (entry.state === 0 || entry.state === 1))
.map((entry) => entry.fromNationId)
);
const weight: Record<number, number> = {};
const warWeight: Record<number, number> = {};
for (const nation of neighbors) {
weight[nation.id] = 1 / Math.sqrt(nation.power + 1);
const target = lowTargetNations.has(nation.id) ? warWeight : weight;
target[nation.id] = 1 / Math.sqrt(nation.power + 1);
}
if (Object.keys(weight).length === 0) {
if (Object.keys(warWeight).length === 0 || lowTargetNations.size === 0) {
return null;
}
if (ai.rng.nextBool(1 / lowTargetNations.size)) {
return null;
}
Object.assign(weight, warWeight);
}
const destNationId = Number(ai.rng.choiceUsingWeight(weight));
@@ -3,7 +3,10 @@ import type { City } from '@sammo-ts/logic';
import type { GeneralAI } from '../core.js';
import { asRecord, readMetaNumber } from '../../aiUtils.js';
export const pickWeightedCandidate = (ai: GeneralAI, list: Array<[ReturnType<GeneralAI['buildNationCandidate']>, number]>) => {
export const pickWeightedCandidate = (
ai: GeneralAI,
list: Array<[ReturnType<GeneralAI['buildNationCandidate']>, number]>
) => {
const items = list.filter(([item]) => Boolean(item)) as Array<
[ReturnType<GeneralAI['buildNationCandidate']>, number]
>;
@@ -59,23 +62,21 @@ export const selectRecruitableCity = (ai: GeneralAI, minPop: number): Record<num
export const buildAssignmentCandidate = (ai: GeneralAI, destGeneralId: number, destCityId: number, reason: string) =>
ai.buildNationCandidate('che_발령', { destGeneralId, destCityId }, reason);
export const buildSeizureCandidate = (ai: GeneralAI, destGeneralId: number, amount: number, isGold: boolean, reason: string) =>
ai.buildNationCandidate('che_몰수', { destGeneralID: destGeneralId, amount, isGold }, reason);
export const buildSeizureCandidate = (
ai: GeneralAI,
destGeneralId: number,
amount: number,
isGold: boolean,
reason: string
) => ai.buildNationCandidate('che_몰수', { destGeneralID: destGeneralId, amount, isGold }, reason);
export const buildAwardCandidate = (ai: GeneralAI, destGeneralId: number, amount: number, isGold: boolean, reason: string) =>
ai.buildNationCandidate('che_포상', { destGeneralId, amount, isGold }, reason);
export const resolveAwardAmount = (ai: GeneralAI, current: number, target: number): number | null => {
const diff = target - current;
if (diff <= 0) {
return null;
}
const amount = Math.min(diff, ai.maxResourceActionAmount);
if (amount < ai.nationPolicy.minimumResourceActionAmount) {
return null;
}
return amount;
};
export const buildAwardCandidate = (
ai: GeneralAI,
destGeneralId: number,
amount: number,
isGold: boolean,
reason: string
) => ai.buildNationCandidate('che_포상', { destGeneralId, amount, isGold }, reason);
export const resolveNationIncome = (ai: GeneralAI): number => {
const cities = Object.values(ai.supplyCities);
@@ -1,6 +1,34 @@
import type { GeneralAI } from '../core.js';
import { asRecord, readRequiredMetaNumber } from '../../aiUtils.js';
import { buildAwardCandidate, buildSeizureCandidate, pickWeightedCandidate, resolveAwardAmount } from './helpers.js';
import { findCrewTypeById, getTechCost } from '@sammo-ts/logic/world/unitSet.js';
import type { TurnGeneral } from '../../../types.js';
import { asRecord, readMetaNumber, readRequiredMetaNumber } from '../../aiUtils.js';
import { buildAwardCandidate, buildSeizureCandidate, pickWeightedCandidate } from './helpers.js';
type ResourceName = 'gold' | 'rice';
const clampLegacy = (value: number, min: number | null, max: number | null): number => {
if (min !== null && max !== null && max < min) {
return min;
}
return Math.max(min ?? -Infinity, Math.min(max ?? Infinity, value));
};
const getFullLeadership = (general: TurnGeneral): number =>
readMetaNumber(asRecord(general.meta), 'fullLeadership', general.stats.leadership);
const getCrewGoldCost = (ai: GeneralAI, general: TurnGeneral, multiplier: number): number => {
const crewType = findCrewTypeById(ai.unitSet, general.crewTypeId ?? ai.commandEnv.defaultCrewTypeId);
const tech = readMetaNumber(asRecord(ai.nation?.meta), 'tech', 0);
return (crewType?.cost ?? 0) * getTechCost(tech) * getFullLeadership(general) * multiplier;
};
const sortedByResource = (generals: Record<number, TurnGeneral>, resource: ResourceName, descending = false) =>
Object.values(generals).sort((lhs, rhs) =>
descending ? rhs[resource] - lhs[resource] : lhs[resource] - rhs[resource]
);
const canUseGeneral = (general: TurnGeneral): boolean =>
readRequiredMetaNumber(asRecord(general.meta), 'killturn', `generalId=${general.id}`) > 5;
export const do유저장긴급포상 = (ai: GeneralAI) => {
const nation = ai.nation;
@@ -8,24 +36,38 @@ export const do유저장긴급포상 = (ai: GeneralAI) => {
return null;
}
const candidates: Array<[ReturnType<GeneralAI['buildNationCandidate']>, number]> = [];
const resourceMap: Array<['gold' | 'rice', number]> = [
const resourceMap: Array<[ResourceName, number]> = [
['gold', ai.nationPolicy.reqHumanWarUrgentGold],
['rice', ai.nationPolicy.reqHumanWarUrgentRice],
];
for (const [resKey, required] of resourceMap) {
if (nation[resKey] < ai.nationPolicy.reqNationGold && resKey === 'gold') {
continue;
}
if (nation[resKey] < ai.nationPolicy.reqNationRice && resKey === 'rice') {
continue;
}
for (const general of Object.values(ai.userWarGenerals)) {
const amount = resolveAwardAmount(ai, general[resKey], required);
if (!amount) {
for (const [resKey, minimum] of resourceMap) {
const generals = sortedByResource(ai.userWarGenerals, resKey);
for (const [index, general] of generals.entries()) {
if (general[resKey] >= minimum) {
break;
}
if (!canUseGeneral(general)) {
continue;
}
candidates.push([buildAwardCandidate(ai, general.id, amount, resKey === 'gold', '유저장긴급포상'), amount]);
let required = getCrewGoldCost(ai, general, 3 * 1.1);
if (ai.world.currentYear > ai.startYear + 3) {
required = Math.max(required, minimum);
}
const enough = required * 1.1;
if (general[resKey] >= required) {
continue;
}
let amount = Math.sqrt((enough - general[resKey]) * nation[resKey]);
amount = clampLegacy(amount, null, enough - general[resKey]);
if (amount < ai.nationPolicy.minimumResourceActionAmount || nation[resKey] < amount / 2) {
continue;
}
amount = clampLegacy(amount, 100, ai.maxResourceActionAmount);
candidates.push([
buildAwardCandidate(ai, general.id, amount, resKey === 'gold', '유저장긴급포상'),
generals.length - index,
]);
}
}
@@ -38,24 +80,56 @@ export const do유저장포상 = (ai: GeneralAI) => {
return null;
}
const candidates: Array<[ReturnType<GeneralAI['buildNationCandidate']>, number]> = [];
const resourceMap: Array<['gold' | 'rice', number]> = [
['gold', ai.nationPolicy.reqHumanWarRecommandGold],
['rice', ai.nationPolicy.reqHumanWarRecommandRice],
const resourceMap: Array<[ResourceName, number, number, number]> = [
[
'gold',
ai.nationPolicy.reqNationGold,
ai.nationPolicy.reqHumanWarRecommandGold,
ai.nationPolicy.reqHumanDevelGold,
],
[
'rice',
ai.nationPolicy.reqNationRice,
ai.nationPolicy.reqHumanWarRecommandRice,
ai.nationPolicy.reqHumanDevelRice,
],
];
for (const [resKey, required] of resourceMap) {
if (nation[resKey] < ai.nationPolicy.reqNationGold && resKey === 'gold') {
for (const [resKey, nationMinimum, warMinimum, civilMinimum] of resourceMap) {
if (nation[resKey] < nationMinimum) {
continue;
}
if (nation[resKey] < ai.nationPolicy.reqNationRice && resKey === 'rice') {
continue;
}
for (const general of Object.values(ai.userWarGenerals)) {
const amount = resolveAwardAmount(ai, general[resKey], required);
if (!amount) {
const generals = sortedByResource(ai.userGenerals, resKey);
for (const [index, general] of generals.entries()) {
if (general[resKey] >= warMinimum) {
break;
}
if (!canUseGeneral(general)) {
continue;
}
candidates.push([buildAwardCandidate(ai, general.id, amount, resKey === 'gold', '유저장포상'), amount]);
let enough: number;
if (ai.userWarGenerals[general.id]) {
let required = getCrewGoldCost(ai, general, 6 * 1.1);
if (ai.world.currentYear > ai.startYear + 3) {
required = Math.max(required, warMinimum);
}
enough = required * 1.2;
} else {
enough = civilMinimum * 1.2;
}
if (general[resKey] >= enough) {
continue;
}
let amount = Math.sqrt((enough - general[resKey]) * nation[resKey]);
amount = clampLegacy(amount, nation[resKey] - nationMinimum, enough - general[resKey]);
if (amount < ai.nationPolicy.minimumResourceActionAmount || nation[resKey] < amount / 2) {
continue;
}
amount = clampLegacy(amount, 100, ai.maxResourceActionAmount);
candidates.push([
buildAwardCandidate(ai, general.id, amount, resKey === 'gold', '유저장포상'),
generals.length - index,
]);
}
}
@@ -68,28 +142,41 @@ export const doNPC긴급포상 = (ai: GeneralAI) => {
return null;
}
const candidates: Array<[ReturnType<GeneralAI['buildNationCandidate']>, number]> = [];
const resourceMap: Array<['gold' | 'rice', number]> = [
['gold', ai.nationPolicy.reqNpcWarGold / 2],
['rice', ai.nationPolicy.reqNpcWarRice / 2],
const resourceMap: Array<[ResourceName, number, number]> = [
['gold', ai.nationPolicy.reqNationGold, ai.nationPolicy.reqNpcWarGold / 2],
['rice', ai.nationPolicy.reqNationRice, ai.nationPolicy.reqNpcWarRice / 2],
];
for (const [resKey, required] of resourceMap) {
if (nation[resKey] < ai.nationPolicy.reqNationGold && resKey === 'gold') {
for (const [resKey, nationMinimum, minimum] of resourceMap) {
if (nation[resKey] < nationMinimum) {
continue;
}
if (nation[resKey] < ai.nationPolicy.reqNationRice && resKey === 'rice') {
continue;
}
for (const general of Object.values(ai.npcWarGenerals)) {
const killturn = readRequiredMetaNumber(asRecord(general.meta), 'killturn', `generalId=${general.id}`);
if (killturn <= 5) {
const generals = sortedByResource(ai.npcWarGenerals, resKey);
for (const [index, general] of generals.entries()) {
if (general[resKey] >= minimum) {
break;
}
if (!canUseGeneral(general)) {
continue;
}
const amount = resolveAwardAmount(ai, general[resKey], required);
if (!amount) {
let required = getCrewGoldCost(ai, general, 1.5);
if (ai.world.currentYear > ai.startYear + 5) {
required = Math.max(required, minimum);
}
const enough = required * 1.2;
if (general[resKey] >= required) {
continue;
}
candidates.push([buildAwardCandidate(ai, general.id, amount, resKey === 'gold', 'NPC긴급포상'), amount]);
let amount = Math.sqrt((enough - general[resKey]) * nation[resKey]);
amount = clampLegacy(amount, nation[resKey] - nationMinimum * 0.9, enough - general[resKey]);
if (amount < ai.nationPolicy.minimumResourceActionAmount || nation[resKey] < amount / 2) {
continue;
}
amount = clampLegacy(amount, 100, ai.maxResourceActionAmount);
candidates.push([
buildAwardCandidate(ai, general.id, amount, resKey === 'gold', 'NPC긴급포상'),
generals.length - index,
]);
}
}
@@ -102,39 +189,60 @@ export const doNPC포상 = (ai: GeneralAI) => {
return null;
}
const candidates: Array<[ReturnType<GeneralAI['buildNationCandidate']>, number]> = [];
const resourceMap: Array<['gold' | 'rice', number, number]> = [
['gold', ai.nationPolicy.reqNpcWarGold, ai.nationPolicy.reqNpcDevelGold],
['rice', ai.nationPolicy.reqNpcWarRice, ai.nationPolicy.reqNpcDevelRice],
const resourceMap: Array<[ResourceName, number, number, number]> = [
['gold', ai.nationPolicy.reqNationGold, ai.nationPolicy.reqNpcWarGold, ai.nationPolicy.reqNpcDevelGold],
['rice', ai.nationPolicy.reqNationRice, ai.nationPolicy.reqNpcWarRice, ai.nationPolicy.reqNpcDevelRice],
];
for (const [resKey, warReq, devReq] of resourceMap) {
if (nation[resKey] < ai.nationPolicy.reqNationGold && resKey === 'gold') {
for (const [resKey, nationMinimum, warMinimum, civilMinimum] of resourceMap) {
if (nation[resKey] < nationMinimum) {
continue;
}
if (nation[resKey] < ai.nationPolicy.reqNationRice && resKey === 'rice') {
continue;
const warGenerals = sortedByResource(ai.npcWarGenerals, resKey);
const civilGenerals = sortedByResource(ai.npcCivilGenerals, resKey);
const weightBase = Math.max(warGenerals.length, civilGenerals.length);
for (const [index, general] of warGenerals.entries()) {
if (general[resKey] >= warMinimum) {
break;
}
if (!canUseGeneral(general)) {
continue;
}
let required = getCrewGoldCost(ai, general, 3 * 1.1);
if (ai.world.currentYear > ai.startYear + 5) {
required = Math.max(required, warMinimum);
}
const enough = required * 1.5;
if (general[resKey] >= required) {
continue;
}
let amount = Math.sqrt((enough - general[resKey]) * nation[resKey]);
amount = clampLegacy(amount, nation[resKey] - nationMinimum, enough - general[resKey]);
if (nation[resKey] < amount / 2) {
continue;
}
amount = clampLegacy(amount, 100, ai.maxResourceActionAmount);
candidates.push([
buildAwardCandidate(ai, general.id, amount, resKey === 'gold', 'NPC포상'),
weightBase - index,
]);
}
for (const general of Object.values(ai.npcWarGenerals)) {
const killturn = readRequiredMetaNumber(asRecord(general.meta), 'killturn', `generalId=${general.id}`);
if (killturn <= 5) {
for (const [index, general] of civilGenerals.entries()) {
if (general[resKey] >= civilMinimum) {
break;
}
if (!canUseGeneral(general)) {
continue;
}
const amount = resolveAwardAmount(ai, general[resKey], warReq);
if (!amount) {
let amount = civilMinimum * 1.5 - general[resKey];
if (amount < ai.nationPolicy.minimumResourceActionAmount) {
continue;
}
candidates.push([buildAwardCandidate(ai, general.id, amount, resKey === 'gold', 'NPC포상'), amount]);
}
for (const general of Object.values(ai.npcCivilGenerals)) {
const killturn = readRequiredMetaNumber(asRecord(general.meta), 'killturn', `generalId=${general.id}`);
if (killturn <= 5) {
continue;
}
const amount = resolveAwardAmount(ai, general[resKey], devReq);
if (!amount) {
continue;
}
candidates.push([buildAwardCandidate(ai, general.id, amount, resKey === 'gold', 'NPC포상'), amount]);
amount = clampLegacy(amount, 100, ai.maxResourceActionAmount);
candidates.push([
buildAwardCandidate(ai, general.id, amount, resKey === 'gold', 'NPC포상'),
weightBase - index,
]);
}
}
@@ -147,41 +255,46 @@ export const doNPC몰수 = (ai: GeneralAI) => {
return null;
}
const candidates: Array<[ReturnType<GeneralAI['buildNationCandidate']>, number]> = [];
const resourceMap: Array<['gold' | 'rice', number, number]> = [
['gold', ai.nationPolicy.reqNpcWarGold, ai.nationPolicy.reqNpcDevelGold],
['rice', ai.nationPolicy.reqNpcWarRice, ai.nationPolicy.reqNpcDevelRice],
const resourceMap: Array<[ResourceName, number, number, number]> = [
['gold', ai.nationPolicy.reqNationGold, ai.nationPolicy.reqNpcWarGold, ai.nationPolicy.reqNpcDevelGold],
['rice', ai.nationPolicy.reqNationRice, ai.nationPolicy.reqNpcWarRice, ai.nationPolicy.reqNpcDevelRice],
];
for (const [resKey, warReq, devReq] of resourceMap) {
const nationLimit = resKey === 'gold' ? ai.nationPolicy.reqNationGold : ai.nationPolicy.reqNationRice;
const nationEnough = nation[resKey] >= nationLimit;
for (const general of Object.values(ai.npcCivilGenerals)) {
if (general[resKey] <= devReq * 1.5) {
continue;
for (const [resKey, nationMinimum, warMinimum, civilMinimum] of resourceMap) {
for (const general of sortedByResource(ai.npcCivilGenerals, resKey, true)) {
if (general[resKey] <= civilMinimum * 1.5) {
break;
}
const amount = Math.min(general[resKey] - devReq * 1.2, ai.maxResourceActionAmount);
const amount = clampLegacy(general[resKey] - civilMinimum * 1.2, 100, ai.maxResourceActionAmount);
if (amount < ai.nationPolicy.minimumResourceActionAmount) {
continue;
break;
}
candidates.push([buildSeizureCandidate(ai, general.id, amount, resKey === 'gold', 'NPC몰수'), amount]);
}
if (!nationEnough) {
for (const general of Object.values(ai.npcWarGenerals)) {
const minRes = nation[resKey] < nationLimit * 0.5 ? warReq * 2 : warReq;
if (general[resKey] <= minRes) {
continue;
}
const amount = Math.min(general[resKey] - minRes, ai.maxResourceActionAmount);
if (amount < ai.nationPolicy.minimumResourceActionAmount) {
continue;
}
candidates.push([
buildSeizureCandidate(ai, general.id, amount, resKey === 'gold', 'NPC몰수'),
amount,
]);
const nationDelta = nationMinimum * 1.5 - nation[resKey];
if (nationDelta < 0) {
continue;
}
const takeSmallAmount = nation[resKey] >= nationMinimum;
for (const general of sortedByResource(ai.npcWarGenerals, resKey, true)) {
if (general[resKey] <= warMinimum * (takeSmallAmount ? 2 : 1)) {
break;
}
let amount: number;
if (takeSmallAmount) {
const maxAmount = general[resKey] - warMinimum;
const minAmount = general[resKey] - warMinimum * 2;
amount = clampLegacy(Math.sqrt(minAmount * nationDelta), 0, maxAmount);
} else {
const maxAmount = general[resKey] - warMinimum;
amount = clampLegacy(Math.sqrt(maxAmount * nationDelta), 0, maxAmount);
}
if (amount < 100 || amount < ai.nationPolicy.minimumResourceActionAmount) {
break;
}
amount = clampLegacy(amount, 100, ai.maxResourceActionAmount);
candidates.push([buildSeizureCandidate(ai, general.id, amount, resKey === 'gold', 'NPC몰수'), amount]);
}
}
+2 -2
View File
@@ -280,7 +280,7 @@ export class AutorunNationPolicy {
if (this.reqNpcWarGold === 0 || this.reqNpcWarRice === 0) {
const crewType = findCrewTypeById(unitSet, env.defaultCrewTypeId);
const baseGold = crewType ? crewType.cost * getTechCost(tech) * stat.npcMax : 0;
const baseRice = stat.npcMax;
const baseRice = crewType ? crewType.rice * getTechCost(tech) * stat.npcMax : 0;
if (this.reqNpcWarGold === 0) {
this.reqNpcWarGold = roundTo(baseGold * 4, -2);
}
@@ -292,7 +292,7 @@ export class AutorunNationPolicy {
if (this.reqHumanWarUrgentGold === 0 || this.reqHumanWarUrgentRice === 0) {
const crewType = findCrewTypeById(unitSet, env.defaultCrewTypeId);
const baseGold = crewType ? crewType.cost * getTechCost(tech) * stat.max : 0;
const baseRice = stat.max;
const baseRice = crewType ? crewType.rice * getTechCost(tech) * stat.max : 0;
if (this.reqHumanWarUrgentGold === 0) {
this.reqHumanWarUrgentGold = roundTo(baseGold * 6, -2);
}