fix(parity): preserve legacy monthly turn ordering

This commit is contained in:
2026-08-15 09:40:14 +00:00
parent 47e629eb4b
commit b9e45c6e76
26 changed files with 393 additions and 58 deletions
+16 -1
View File
@@ -858,7 +858,22 @@ export class GeneralAI {
continue;
}
if (candidate.stats.leadership >= this.nationPolicy.minNpcWarLeadership) {
const fullLeadership = this.commandEnv.generalActionModules
? resolveLegacyAiStatsWithModules(
candidate,
this.nation,
this.commandEnv.maxStatLevel ?? this.scenarioConfig.stat.max,
this.commandEnv.generalActionModules,
this.worldRef,
this.world,
this.startYear
).fullLeadership
: resolveLegacyAiStats(
candidate,
this.nation,
this.commandEnv.maxStatLevel ?? this.scenarioConfig.stat.max
).fullLeadership;
if (fullLeadership >= this.nationPolicy.minNpcWarLeadership) {
npcWarGenerals[candidate.id] = candidate;
} else {
npcCivilGenerals[candidate.id] = candidate;
@@ -41,7 +41,11 @@ export const doNPC헌납 = (ai: GeneralAI) => {
genRes >= ai.aiConst.minNationalRice / 2
) {
const amount = genRes < ai.aiConst.minNationalRice ? genRes : genRes / 2;
args.push([{ isGold: false, amount }, amount]);
// Ref passes the literal string "rice" here. che_헌납::argTest()
// rejects that candidate because isGold is not boolean; preserving
// the malformed weighted candidate also preserves the RNG draw and
// lets the priority loop continue when it is selected.
args.push([{ isGold: 'rice', amount }, amount]);
}
if (genRes < reqRes * 1.5) {
continue;
@@ -128,11 +128,14 @@ export const do부대후방발령 = (ai: GeneralAI) => {
return null;
}
// Ref consumes the troop-leader draw before the destination-city draw.
// Both selections share the nation command RNG, so reversing them can
// assign the same two outcomes to different leaders and cities.
const leader = ai.rng.choice(troopCandidates);
const destCityId = Number(ai.rng.choiceUsingWeight(cityCandidates));
if (!Number.isFinite(destCityId)) {
return null;
}
const leader = ai.rng.choice(troopCandidates);
return buildAssignmentCandidate(ai, leader.id, destCityId, '부대후방발령');
};
@@ -162,10 +165,10 @@ export const do부대구출발령 = (ai: GeneralAI) => {
return null;
}
const leader = ai.rng.choice(troopCandidates);
const destCityId = pickRandomCityId(ai, ai.frontCities);
if (destCityId === null) {
return null;
}
const leader = ai.rng.choice(troopCandidates);
return buildAssignmentCandidate(ai, leader.id, destCityId, '부대구출발령');
};
@@ -1,4 +1,5 @@
import type { GeneralAI } from '../core.js';
import { GeneralActionPipeline } from '@sammo-ts/logic/actionModules/general.js';
import { findCrewTypeById, getTechCost } from '@sammo-ts/logic/world/unitSet.js';
import type { TurnGeneral } from '../../../types.js';
import { asRecord, readMetaNumber, readRequiredMetaNumber } from '../../aiUtils.js';
@@ -45,6 +46,35 @@ const clampLegacy = (value: number, min: number | null, max: number | null): num
};
const getFullLeadership = (ai: GeneralAI, general: TurnGeneral): number => {
const modules = ai.commandEnv.generalActionModules;
if (modules && modules.length > 0) {
const pipeline = new GeneralActionPipeline(modules);
const adjusted = pipeline.onCalcStat(
{
general,
nation: ai.nation,
...(ai.worldRef
? {
worldView: {
listGenerals: () => ai.worldRef!.listGenerals(),
listGeneralsByCity: (cityId: number) =>
ai.worldRef!.listGenerals().filter((candidate) => candidate.cityId === cityId),
listNations: () => ai.worldRef!.listNations(),
},
}
: {}),
time: {
year: ai.world.currentYear,
month: ai.world.currentMonth,
startYear: ai.startYear,
},
},
'leadership',
general.stats.leadership
);
const maxStat = ai.commandEnv.maxStatLevel ?? ai.scenarioConfig.stat.max;
return Math.trunc(Math.max(0, Math.min(Number(adjusted), maxStat)));
}
const nationLevel = ai.nation?.level ?? 0;
const officerBonus = general.officerLevel === 12 ? nationLevel * 2 : general.officerLevel >= 5 ? nationLevel : 0;
const maxStat = ai.commandEnv.maxStatLevel ?? ai.scenarioConfig.stat.max;