fix: Ref 게임 로직과 시나리오 풀 호환을 보정

월 경계, 전투 기술 상한, 연감과 베팅·설문·경매 정산 순서를 Ref 계약에 맞춘다.\n\n시나리오 일반 풀을 ENGINE mutation과 logical tick 기반으로 직렬화하고 914·915 catalog 및 조건부 100기 pool 실행 경계를 추가한다.\n\n경매 worker는 세대별 durable event만 만들고 ENGINE이 row lock 후 상태 전이와 정산을 단일 transaction으로 소유한다.
This commit is contained in:
Hide_D committed 2026-08-23 16:26:14 +00:00
1 parent bf6b7be7b0
commit 85591c68ad
114 files changed
+13327 -901

No files matched your search

+30 -14
View File
@@ -45,6 +45,9 @@ const recordHistoryAccess = async (ctx: GameApiContext): Promise<void> => {
const parseTextArray = (value: unknown): string[] =>
Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : [];
const readStoredSnapshotNumber = (value: unknown): number | null =>
typeof value === 'number' && Number.isFinite(value) ? value : null;
const parseYearbookNations = (value: unknown): YearbookNation[] => {
if (!Array.isArray(value)) {
return [];
@@ -114,6 +117,7 @@ const buildNationSnapshot = async (ctx: GameApiContext) => {
gold: true,
rice: true,
tech: true,
meta: true,
},
orderBy: { id: 'asc' },
}),
@@ -197,33 +201,45 @@ const buildNationSnapshot = async (ctx: GameApiContext) => {
generalStatsByNation.set(general.nationId, entry);
}
return nationRows.map<YearbookNation>((nation) => {
const projected = nationRows.map<YearbookNation>((nation) => {
const generalStats = generalStatsByNation.get(nation.id) ?? {
goldRice: 0,
statPower: 0,
expDed: 0,
generalCount: 0,
};
const cityStats = cityStatsByNation.get(nation.id) ?? { popSum: 0, valueSum: 0, maxSum: 0 };
const resource = Math.round(((nation.gold ?? 0) + (nation.rice ?? 0) + generalStats.goldRice) / 100);
const tech = nation.tech ?? 0;
const cityPower =
nation.level > 0 && cityStats.maxSum > 0
? Math.round((cityStats.popSum * cityStats.valueSum) / cityStats.maxSum / 100)
: 0;
const expDed = Math.round(generalStats.expDed / 100);
const power = Math.round((resource + tech + cityPower + generalStats.statPower + expDed) / 10);
const nationMeta = asRecord(nation.meta);
const storedPower = readStoredSnapshotNumber(nationMeta.power);
let power = 1;
if (nation.id !== 0) {
if (storedPower !== null) {
power = storedPower;
} else {
const cityStats = cityStatsByNation.get(nation.id) ?? { popSum: 0, valueSum: 0, maxSum: 0 };
const resource = Math.round(((nation.gold ?? 0) + (nation.rice ?? 0) + generalStats.goldRice) / 100);
const tech = nation.tech ?? 0;
const cityPower =
nation.level > 0 && cityStats.maxSum > 0
? Math.round((cityStats.popSum * cityStats.valueSum) / cityStats.maxSum / 100)
: 0;
const expDed = Math.round(generalStats.expDed / 100);
power = Math.round((resource + tech + cityPower + generalStats.statPower + expDed) / 10);
}
}
const storedGeneralCount = readStoredSnapshotNumber(nationMeta.gennum);
const generalCount = nation.id === 0 ? 1 : (storedGeneralCount ?? generalStats.generalCount);
return {
id: nation.id,
name: nation.name,
color: nation.color,
level: nation.level,
name: nation.id === 0 ? '재야' : nation.name,
color: nation.id === 0 ? '#000000' : nation.color,
level: nation.id === 0 ? 0 : nation.level,
power,
generalCount: generalStats.generalCount,
generalCount,
cities: cityNamesByNation.get(nation.id) ?? [],
};
});
return projected.sort((left, right) => right.power - left.power);
};
const readGlobalActionLogs = async (ctx: GameApiContext, year: number, month: number) => {