merge: scenario 2400 long-run parity
This commit is contained in:
@@ -82,6 +82,15 @@ export interface ScenarioSeedResult {
|
||||
|
||||
const asJson = (value: unknown): InputJsonValue => value as InputJsonValue;
|
||||
|
||||
export const calculateInitialTurnTick = (
|
||||
clock: GameClock,
|
||||
baseTick: number,
|
||||
initialTurnOffsetMicros: number
|
||||
): number => {
|
||||
const offsetTicks = Math.floor((initialTurnOffsetMicros * clock.ticksPerSecond) / 1_000_000);
|
||||
return clock.addTicks(baseTick, offsetTicks);
|
||||
};
|
||||
|
||||
const formatDateTime = (date: Date): string => {
|
||||
const pad = (value: number): string => String(value).padStart(2, '0');
|
||||
return [
|
||||
@@ -277,6 +286,9 @@ export const seedScenarioToDatabase = async (options: ScenarioSeedOptions): Prom
|
||||
initYear: startState.currentYear,
|
||||
initMonth: startState.currentMonth,
|
||||
genius: Math.max(0, Math.floor(asNumber(scenarioConst.defaultMaxGenius, 5))),
|
||||
// Ref ResetHelper keeps the active-user expiry horizon in game_env.
|
||||
// User commands refresh to this value unless they are running in AI mode.
|
||||
killturn: install?.npcMode === 1 ? Math.trunc(4800 / turnTermMinutes / 3) : 4800 / turnTermMinutes,
|
||||
// Ref seeds game_env.develcost before the first general turn. The
|
||||
// monthly pre-handler recalculates the same value at each boundary.
|
||||
develcost: (startState.currentYear - (scenario.startYear ?? startState.currentYear) + 10) * 2,
|
||||
@@ -539,15 +551,12 @@ export const seedScenarioToDatabase = async (options: ScenarioSeedOptions): Prom
|
||||
)
|
||||
),
|
||||
turnTick: BigInt(
|
||||
initialClock.dateToTick(
|
||||
new Date(
|
||||
now.getTime() +
|
||||
Math.floor(
|
||||
(typeof general.meta.initialTurnOffsetMicros === 'number'
|
||||
? general.meta.initialTurnOffsetMicros
|
||||
: 0) / 1_000
|
||||
)
|
||||
)
|
||||
calculateInitialTurnTick(
|
||||
initialClock,
|
||||
initialClockTick,
|
||||
typeof general.meta.initialTurnOffsetMicros === 'number'
|
||||
? general.meta.initialTurnOffsetMicros
|
||||
: 0
|
||||
)
|
||||
),
|
||||
age: resolveGeneralAge(startState.currentYear, general.birthYear),
|
||||
|
||||
@@ -27,6 +27,10 @@ export const resolveConstraintEnv = (
|
||||
month: world.currentMonth,
|
||||
startYear,
|
||||
relYear,
|
||||
// Ref asks each concrete command for its full constraints while the AI
|
||||
// is still choosing a command. Cost-bearing commands therefore need
|
||||
// the current yearly game_env.develcost at this boundary as well.
|
||||
develCost: env.develCost,
|
||||
openingPartYear: env.openingPartYear,
|
||||
minAvailableRecruitPop: env.minAvailableRecruitPop,
|
||||
...(Number.isFinite(killturn) ? { killturn } : {}),
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { GeneralActionPipeline } from '@sammo-ts/logic';
|
||||
import { findCrewTypeById, getTechCost } from '@sammo-ts/logic/world/unitSet.js';
|
||||
import { CommandResolver as RecruitmentCommandResolver } from '@sammo-ts/logic/actions/turn/general/che_징병.js';
|
||||
|
||||
import type { GeneralAI } from '../core.js';
|
||||
import { asRecord, readMetaNumber, valueFit } from '../../aiUtils.js';
|
||||
|
||||
export const do금쌀구매 = (ai: GeneralAI) => {
|
||||
const traceEnabled = (process.env.CORE_AI_TRACE_GENERAL_IDS?.split(',') ?? []).includes(String(ai.general.id));
|
||||
const traceEnabled = [
|
||||
...(process.env.CORE_AI_TRACE_GENERAL_IDS?.split(',') ?? []),
|
||||
...(process.env.GUI_PARITY_CORE_TRACE_GENERAL_IDS?.split(',') ?? []),
|
||||
].includes(String(ai.general.id));
|
||||
const trace = (stage: string, values: Record<string, unknown> = {}) => {
|
||||
if (!traceEnabled) {
|
||||
return;
|
||||
}
|
||||
process.stderr.write(
|
||||
`AI_ECONOMY_TRACE ${JSON.stringify({ generalId: ai.general.id, stage, ...values })}\n`
|
||||
);
|
||||
process.stderr.write(`AI_ECONOMY_TRACE ${JSON.stringify({ generalId: ai.general.id, stage, ...values })}\n`);
|
||||
};
|
||||
const city = ai.city;
|
||||
if (!city) {
|
||||
@@ -55,35 +56,26 @@ export const do금쌀구매 = (ai: GeneralAI) => {
|
||||
const tech = readMetaNumber(asRecord(ai.nation?.meta ?? {}), 'tech', 0);
|
||||
const fullLeadership = readMetaNumber(generalMeta, 'fullLeadership', ai.general.stats.leadership);
|
||||
const crewAmount = fullLeadership * 100;
|
||||
const rawGoldCost = crewType ? (crewType.cost * getTechCost(tech) * crewAmount) / 100 : 0;
|
||||
const actionPipeline = new GeneralActionPipeline(ai.commandEnv.generalActionModules ?? []);
|
||||
const goldCost = Math.round(
|
||||
actionPipeline.onCalcDomestic(
|
||||
{
|
||||
general: ai.general,
|
||||
nation: ai.nation ?? undefined,
|
||||
...(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,
|
||||
},
|
||||
},
|
||||
'징병',
|
||||
'cost',
|
||||
rawGoldCost,
|
||||
{ armType: crewType?.armType ?? 0 }
|
||||
) * (ai.generalPolicy.can('모병') ? 2 : 1)
|
||||
);
|
||||
const recruitContext = {
|
||||
general: ai.general,
|
||||
nation: ai.nation ?? undefined,
|
||||
...(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 },
|
||||
};
|
||||
const recruitment = new RecruitmentCommandResolver(ai.commandEnv.generalActionModules ?? [], ai.commandEnv);
|
||||
const goldCost = crewType
|
||||
? recruitment.getCost(recruitContext, crewType.id, crewAmount, crewType).gold *
|
||||
(ai.generalPolicy.can('모병') ? 2 : 1)
|
||||
: 0;
|
||||
const riceCost = crewType ? (crewType.rice * getTechCost(tech) * crewAmount) / 100 : 0;
|
||||
trace('recruit-cost', {
|
||||
crewTypeId: crewType?.id ?? null,
|
||||
@@ -145,7 +137,9 @@ export const do금쌀구매 = (ai: GeneralAI) => {
|
||||
ai.aiConst.maxResourceActionAmount
|
||||
);
|
||||
if (amount >= ai.nationPolicy.minimumResourceActionAmount) {
|
||||
return ai.buildGeneralCandidate('che_군량매매', { buyRice: false, amount }, '금쌀구매');
|
||||
const result = ai.buildGeneralCandidate('che_군량매매', { buyRice: false, amount }, '금쌀구매');
|
||||
trace('sell', { amount, minimumResourceActionAmount: ai.nationPolicy.minimumResourceActionAmount, result });
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,12 +51,7 @@ const getFullLeadership = (ai: GeneralAI, general: TurnGeneral): number => {
|
||||
return Math.max(0, Math.min(general.stats.leadership + officerBonus, maxStat));
|
||||
};
|
||||
|
||||
const getCrewGoldCost = (
|
||||
ai: GeneralAI,
|
||||
general: TurnGeneral,
|
||||
baseMultiplier: number,
|
||||
finalMultiplier = 1
|
||||
): number => {
|
||||
const getCrewGoldCost = (ai: GeneralAI, general: TurnGeneral, baseMultiplier: number, finalMultiplier = 1): number => {
|
||||
const crewType = findCrewTypeById(ai.unitSet, general.crewTypeId ?? ai.commandEnv.defaultCrewTypeId);
|
||||
const tech = readMetaNumber(asRecord(ai.nation?.meta), 'tech', 0);
|
||||
// Ref evaluates costWithTech() first, including its `/ 100`, and then
|
||||
@@ -64,16 +59,15 @@ const getCrewGoldCost = (
|
||||
// Keeping that operation order is observable at exact resource boundaries
|
||||
// (for example 3036 versus 3036.0000000000005).
|
||||
return (
|
||||
((((crewType?.cost ?? 0) * getTechCost(tech) * getFullLeadership(ai, general)) / 100) * 100 *
|
||||
baseMultiplier) *
|
||||
(((crewType?.cost ?? 0) * getTechCost(tech) * getFullLeadership(ai, general)) / 100) *
|
||||
100 *
|
||||
baseMultiplier *
|
||||
finalMultiplier
|
||||
);
|
||||
};
|
||||
|
||||
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 sortByResource = (generals: TurnGeneral[], resource: ResourceName, descending = false) =>
|
||||
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;
|
||||
@@ -88,9 +82,10 @@ export const do유저장긴급포상 = (ai: GeneralAI) => {
|
||||
['gold', ai.nationPolicy.reqHumanWarUrgentGold],
|
||||
['rice', ai.nationPolicy.reqHumanWarUrgentRice],
|
||||
];
|
||||
const userWarGenerals = Object.values(ai.userWarGenerals);
|
||||
|
||||
for (const [resKey, minimum] of resourceMap) {
|
||||
const generals = sortedByResource(ai.userWarGenerals, resKey);
|
||||
const generals = sortByResource(userWarGenerals, resKey);
|
||||
for (const [index, general] of generals.entries()) {
|
||||
if (general[resKey] >= minimum) {
|
||||
break;
|
||||
@@ -112,7 +107,10 @@ export const do유저장긴급포상 = (ai: GeneralAI) => {
|
||||
continue;
|
||||
}
|
||||
amount = clampLegacy(amount, 100, ai.maxResourceActionAmount);
|
||||
candidates.push([{ destGeneralId: general.id, amount, isGold: resKey === 'gold' }, generals.length - index]);
|
||||
candidates.push([
|
||||
{ destGeneralId: general.id, amount, isGold: resKey === 'gold' },
|
||||
generals.length - index,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,12 +137,13 @@ export const do유저장포상 = (ai: GeneralAI) => {
|
||||
ai.nationPolicy.reqHumanDevelRice,
|
||||
],
|
||||
];
|
||||
const userGenerals = Object.values(ai.userGenerals);
|
||||
|
||||
for (const [resKey, nationMinimum, warMinimum, civilMinimum] of resourceMap) {
|
||||
if (nation[resKey] < nationMinimum) {
|
||||
continue;
|
||||
}
|
||||
const generals = sortedByResource(ai.userGenerals, resKey);
|
||||
const generals = sortByResource(userGenerals, resKey);
|
||||
for (const [index, general] of generals.entries()) {
|
||||
if (general[resKey] >= warMinimum) {
|
||||
break;
|
||||
@@ -171,7 +170,10 @@ export const do유저장포상 = (ai: GeneralAI) => {
|
||||
continue;
|
||||
}
|
||||
amount = clampLegacy(amount, 100, ai.maxResourceActionAmount);
|
||||
candidates.push([{ destGeneralId: general.id, amount, isGold: resKey === 'gold' }, generals.length - index]);
|
||||
candidates.push([
|
||||
{ destGeneralId: general.id, amount, isGold: resKey === 'gold' },
|
||||
generals.length - index,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,12 +190,13 @@ export const doNPC긴급포상 = (ai: GeneralAI) => {
|
||||
['gold', ai.nationPolicy.reqNationGold, ai.nationPolicy.reqNpcWarGold / 2],
|
||||
['rice', ai.nationPolicy.reqNationRice, ai.nationPolicy.reqNpcWarRice / 2],
|
||||
];
|
||||
const npcWarGenerals = Object.values(ai.npcWarGenerals);
|
||||
|
||||
for (const [resKey, nationMinimum, minimum] of resourceMap) {
|
||||
if (nation[resKey] < nationMinimum) {
|
||||
continue;
|
||||
}
|
||||
const generals = sortedByResource(ai.npcWarGenerals, resKey);
|
||||
const generals = sortByResource(npcWarGenerals, resKey);
|
||||
for (const [index, general] of generals.entries()) {
|
||||
if (general[resKey] >= minimum) {
|
||||
break;
|
||||
@@ -215,7 +218,10 @@ export const doNPC긴급포상 = (ai: GeneralAI) => {
|
||||
continue;
|
||||
}
|
||||
amount = clampLegacy(amount, 100, ai.maxResourceActionAmount);
|
||||
candidates.push([{ destGeneralId: general.id, amount, isGold: resKey === 'gold' }, generals.length - index]);
|
||||
candidates.push([
|
||||
{ destGeneralId: general.id, amount, isGold: resKey === 'gold' },
|
||||
generals.length - index,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,13 +238,15 @@ export const doNPC포상 = (ai: GeneralAI) => {
|
||||
['gold', ai.nationPolicy.reqNationGold, ai.nationPolicy.reqNpcWarGold, ai.nationPolicy.reqNpcDevelGold],
|
||||
['rice', ai.nationPolicy.reqNationRice, ai.nationPolicy.reqNpcWarRice, ai.nationPolicy.reqNpcDevelRice],
|
||||
];
|
||||
const npcWarGenerals = Object.values(ai.npcWarGenerals);
|
||||
const npcCivilGenerals = Object.values(ai.npcCivilGenerals);
|
||||
|
||||
for (const [resKey, nationMinimum, warMinimum, civilMinimum] of resourceMap) {
|
||||
if (nation[resKey] < nationMinimum) {
|
||||
continue;
|
||||
}
|
||||
const warGenerals = sortedByResource(ai.npcWarGenerals, resKey);
|
||||
const civilGenerals = sortedByResource(ai.npcCivilGenerals, resKey);
|
||||
const warGenerals = sortByResource(npcWarGenerals, resKey);
|
||||
const civilGenerals = sortByResource(npcCivilGenerals, resKey);
|
||||
const weightBase = Math.max(warGenerals.length, civilGenerals.length);
|
||||
for (const [index, general] of warGenerals.entries()) {
|
||||
if (general[resKey] >= warMinimum) {
|
||||
@@ -308,9 +316,11 @@ export const doNPC몰수 = (ai: GeneralAI) => {
|
||||
['gold', ai.nationPolicy.reqNationGold, ai.nationPolicy.reqNpcWarGold, ai.nationPolicy.reqNpcDevelGold],
|
||||
['rice', ai.nationPolicy.reqNationRice, ai.nationPolicy.reqNpcWarRice, ai.nationPolicy.reqNpcDevelRice],
|
||||
];
|
||||
const npcWarGenerals = Object.values(ai.npcWarGenerals);
|
||||
const npcCivilGenerals = Object.values(ai.npcCivilGenerals);
|
||||
|
||||
for (const [resKey, nationMinimum, warMinimum, civilMinimum] of resourceMap) {
|
||||
for (const general of sortedByResource(ai.npcCivilGenerals, resKey, true)) {
|
||||
for (const general of sortByResource(npcCivilGenerals, resKey, true)) {
|
||||
if (general[resKey] <= civilMinimum * 1.5) {
|
||||
break;
|
||||
}
|
||||
@@ -326,7 +336,7 @@ export const doNPC몰수 = (ai: GeneralAI) => {
|
||||
continue;
|
||||
}
|
||||
const takeSmallAmount = nation[resKey] >= nationMinimum;
|
||||
for (const general of sortedByResource(ai.npcWarGenerals, resKey, true)) {
|
||||
for (const general of sortByResource(npcWarGenerals, resKey, true)) {
|
||||
if (general[resKey] <= warMinimum * (takeSmallAmount ? 2 : 1)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -402,6 +402,7 @@ export class InMemoryTurnWorld {
|
||||
private readonly dirtyTroopIds = new Set<number>();
|
||||
private readonly dirtyDiplomacyKeys = new Set<string>();
|
||||
private readonly createdGeneralIds = new Set<number>();
|
||||
private nextLegacyGeneralScanOrder = 0;
|
||||
private readonly createdNationIds = new Set<number>();
|
||||
private readonly createdTroopIds = new Set<number>();
|
||||
private readonly createdDiplomacyKeys = new Set<string>();
|
||||
@@ -476,7 +477,16 @@ export class InMemoryTurnWorld {
|
||||
const normalized = this.normalizeGeneralClock(
|
||||
normalizeGeneralTurnTime({ ...general }, this.state.lastTurnTime)
|
||||
);
|
||||
const ensured = ensureGeneralKillturn(normalized, worldKillturn);
|
||||
const existingOrder = normalized.meta.legacyScanOrder;
|
||||
const scanOrder =
|
||||
typeof existingOrder === 'number' && Number.isFinite(existingOrder)
|
||||
? existingOrder
|
||||
: this.nextLegacyGeneralScanOrder;
|
||||
this.nextLegacyGeneralScanOrder = Math.max(this.nextLegacyGeneralScanOrder, scanOrder + 1);
|
||||
const ensured = ensureGeneralKillturn(
|
||||
{ ...normalized, meta: { ...normalized.meta, legacyScanOrder: scanOrder } },
|
||||
worldKillturn
|
||||
);
|
||||
this.generals.set(general.id, ensured);
|
||||
}
|
||||
for (const city of snapshot.cities) {
|
||||
@@ -876,7 +886,13 @@ export class InMemoryTurnWorld {
|
||||
const normalized = this.normalizeGeneralClock(
|
||||
normalizeGeneralTurnTime({ ...general }, this.state.lastTurnTime)
|
||||
);
|
||||
const ensured = normalizeGeneralDatabaseIntegers(ensureGeneralKillturn(normalized, worldKillturn));
|
||||
const scanOrder = this.nextLegacyGeneralScanOrder++;
|
||||
const ensured = normalizeGeneralDatabaseIntegers(
|
||||
ensureGeneralKillturn(
|
||||
{ ...normalized, meta: { ...normalized.meta, legacyScanOrder: scanOrder } },
|
||||
worldKillturn
|
||||
)
|
||||
);
|
||||
this.generals.set(general.id, ensured);
|
||||
this.dirtyGeneralIds.add(general.id);
|
||||
this.createdGeneralIds.add(general.id);
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
getOutcome,
|
||||
getRiceIncome,
|
||||
getWallIncome,
|
||||
readLegacyCityTrust,
|
||||
type CityIncomeSource,
|
||||
type Nation,
|
||||
type NationIncomeContext,
|
||||
@@ -44,9 +45,13 @@ const resolveOfficerCity = (meta: Record<string, unknown>): number => {
|
||||
return asNumber(meta.officer_city, 0);
|
||||
};
|
||||
|
||||
export const resolveLegacyIncomeCityTrust = (trust: number): number => readLegacyCityTrust(trust);
|
||||
|
||||
const resolveCityTrust = (meta: Record<string, unknown>): number => {
|
||||
const trust = asNumber(meta.trust, 50);
|
||||
return trust;
|
||||
// Income is calculated in PHP after PDO exposes MariaDB FLOAT using a
|
||||
// six-significant-digit decimal representation.
|
||||
return resolveLegacyIncomeCityTrust(trust);
|
||||
};
|
||||
|
||||
const toIncomeCity = (city: ReturnType<InMemoryTurnWorld['listCities']>[number]): CityIncomeSource => ({
|
||||
|
||||
@@ -157,19 +157,11 @@ const readHiddenSeed = (worldState: WorldStateRow): string | number => {
|
||||
return fail('INTERNAL_SERVER_ERROR', '장수 생성 비밀 seed가 설정되지 않았습니다.');
|
||||
};
|
||||
|
||||
const formatLegacySeedTime = (value: Date): string => {
|
||||
const pad = (part: number): string => String(part).padStart(2, '0');
|
||||
const koreaTime = new Date(value.getTime() + LEGACY_TIMEZONE_OFFSET_MS);
|
||||
return `${koreaTime.getUTCFullYear()}-${pad(koreaTime.getUTCMonth() + 1)}-${pad(
|
||||
koreaTime.getUTCDate()
|
||||
)} ${pad(koreaTime.getUTCHours())}:${pad(koreaTime.getUTCMinutes())}:${pad(koreaTime.getUTCSeconds())}`;
|
||||
};
|
||||
|
||||
export const buildJoinCreateGeneralSeed = (
|
||||
hiddenSeed: string | number,
|
||||
ownerIdentity: string | number,
|
||||
acceptedAt: Date
|
||||
): string => simpleSerialize(hiddenSeed, 'MakeGeneral', ownerIdentity, formatLegacySeedTime(acceptedAt));
|
||||
acceptedTick: number
|
||||
): string => simpleSerialize(hiddenSeed, 'MakeGeneral', ownerIdentity, acceptedTick);
|
||||
|
||||
const lockJoinMutation = async (db: DatabaseClient, userId: string): Promise<void> => {
|
||||
await db.$executeRaw(GamePrisma.sql`SELECT pg_advisory_xact_lock(hashtextextended(${`join-create:${userId}`}, 0))`);
|
||||
@@ -611,7 +603,9 @@ export const createGeneralFromJoin = async (options: {
|
||||
|
||||
const hiddenSeed = readHiddenSeed(worldState);
|
||||
const rng = new RandUtil(
|
||||
new LiteHashDRBG(buildJoinCreateGeneralSeed(hiddenSeed, input.seedOwnerIdentity, acceptedAt))
|
||||
new LiteHashDRBG(
|
||||
buildJoinCreateGeneralSeed(hiddenSeed, input.seedOwnerIdentity, world.dateToGameTick(acceptedAt))
|
||||
)
|
||||
);
|
||||
const worldMeta = asRecord(worldState.meta);
|
||||
const currentGenius = Math.max(
|
||||
|
||||
@@ -55,12 +55,8 @@ const readRuntimeNumber = (world: InMemoryTurnWorld, key: string, fallback: numb
|
||||
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
||||
};
|
||||
|
||||
const buildSpecialityAge = (
|
||||
retirementYear: number,
|
||||
age: number,
|
||||
relativeYear: number,
|
||||
divisor: number
|
||||
): number => Math.max(Math.round((retirementYear - age) / divisor - relativeYear / 2), 3) + age;
|
||||
const buildSpecialityAge = (retirementYear: number, age: number, relativeYear: number, divisor: number): number =>
|
||||
Math.max(Math.round((retirementYear - age) / divisor - relativeYear / 2), 3) + age;
|
||||
|
||||
const resolveSpecialityAge = (
|
||||
general: TurnGeneral,
|
||||
@@ -124,9 +120,7 @@ const resolveTrait = (modules: readonly TraitModule[], key: string, label: strin
|
||||
export const createAssignGeneralSpecialityHandler = (options: {
|
||||
getWorld: () => InMemoryTurnWorld | null;
|
||||
}): MonthlyEventActionHandler => {
|
||||
let modulePromise:
|
||||
| Promise<{ domesticModules: TraitModule[]; warModules: TraitModule[] }>
|
||||
| undefined;
|
||||
let modulePromise: Promise<{ domesticModules: TraitModule[]; warModules: TraitModule[] }> | undefined;
|
||||
const loadModules = () => {
|
||||
modulePromise ??= Promise.all([
|
||||
loadDomesticTraitModules([...LEGACY_DOMESTIC_SELECTION_KEYS]),
|
||||
@@ -143,7 +137,12 @@ export const createAssignGeneralSpecialityHandler = (options: {
|
||||
const { domesticModules, warModules } = await loadModules();
|
||||
const rng = new RandUtil(
|
||||
new LiteHashDRBG(
|
||||
simpleSerialize(resolveHiddenSeed(world), 'assignGeneralSpeciality', environment.year, environment.month)
|
||||
simpleSerialize(
|
||||
resolveHiddenSeed(world),
|
||||
'assignGeneralSpeciality',
|
||||
environment.year,
|
||||
environment.month
|
||||
)
|
||||
)
|
||||
);
|
||||
const defaultDomestic = normalizeCode(world.getScenarioConfig().const.defaultSpecialDomestic);
|
||||
@@ -152,7 +151,11 @@ export const createAssignGeneralSpecialityHandler = (options: {
|
||||
const scenarioStat = world.getScenarioConfig().stat;
|
||||
// ref SQL에 ORDER BY가 없으므로 loader가 보존한 DB scan 순서를 두
|
||||
// domestic/war pass에서 그대로 재사용한다.
|
||||
const generals = world.listGenerals();
|
||||
const generals = world.listGenerals().sort((left, right) => {
|
||||
const leftOrder = readFiniteNumber(left.meta, ['legacyScanOrder']) ?? left.id;
|
||||
const rightOrder = readFiniteNumber(right.meta, ['legacyScanOrder']) ?? right.id;
|
||||
return leftOrder - rightOrder;
|
||||
});
|
||||
|
||||
for (const general of generals) {
|
||||
if (
|
||||
|
||||
@@ -116,7 +116,7 @@ const LEGACY_STAT_CHANGE_GENERAL_ACTIONS = new Set([
|
||||
'che_전투태세',
|
||||
]);
|
||||
|
||||
const applyLegacyGeneralProgression = (
|
||||
export const applyLegacyGeneralProgression = (
|
||||
general: TurnGeneral,
|
||||
previousGeneral: TurnGeneral,
|
||||
actionKey: string,
|
||||
@@ -140,7 +140,13 @@ const applyLegacyGeneralProgression = (
|
||||
// 등급을 강제 재계산한다. 반대로 은퇴의 rebirth()와 선양의
|
||||
// multiplyVar('experience')는 수치를 줄이면서도 기존 등급을 그대로 둔다.
|
||||
const forceRefreshLevel = actionKey === 'che_하야';
|
||||
const preserveLevel = actionKey === 'che_은퇴' || actionKey === 'che_선양';
|
||||
// Battle units update levels before finishBattle() rounds the legacy INT
|
||||
// columns. che_출병 must retain that pre-round result just like Ref.
|
||||
const preserveLevel =
|
||||
actionKey === 'che_은퇴' ||
|
||||
actionKey === 'che_선양' ||
|
||||
actionKey === 'che_출병' ||
|
||||
actionKey === 'che_물자조달';
|
||||
if (!preserveLevel && (forceRefreshLevel || general.experience !== previousGeneral.experience)) {
|
||||
const previousExpLevel = readMetaNumber(previousGeneral.meta, 'explevel', 0);
|
||||
const actionResolvedExpLevel = readMetaNumber(general.meta, 'explevel', previousExpLevel);
|
||||
|
||||
Reference in New Issue
Block a user