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:
@@ -7,8 +7,8 @@ import { isEventDomesticTraitKey } from '@sammo-ts/logic';
|
||||
import { resolveWorkspaceRoot } from '../paths.js';
|
||||
|
||||
const DEFAULT_GENERAL_POOL_ROOT = path.resolve(resolveWorkspaceRoot(), 'resources', 'general-pool');
|
||||
const SUPPORTED_POOL = 'SPoolUnderU30';
|
||||
const EXPECTED_COLUMNS = [
|
||||
const SUPPORTED_POOLS = new Set(['SPoolUnderU30', 'SPoolUnderU100']);
|
||||
const BASE_COLUMNS = [
|
||||
'generalName',
|
||||
'leadership',
|
||||
'strength',
|
||||
@@ -18,6 +18,13 @@ const EXPECTED_COLUMNS = [
|
||||
'imgsvr',
|
||||
'picture',
|
||||
] as const;
|
||||
const CENTENNIAL_COLUMNS = [
|
||||
...BASE_COLUMNS,
|
||||
'sourcePhase',
|
||||
'sourceServerId',
|
||||
'sourceGeneralNo',
|
||||
'selectionReasons',
|
||||
] as const;
|
||||
|
||||
export interface GeneralPoolSeedEntry {
|
||||
uniqueName: string;
|
||||
@@ -31,38 +38,58 @@ export interface GeneralPoolLoaderOptions {
|
||||
const readPoolResource = async (filePath: string): Promise<unknown> =>
|
||||
JSON.parse(await fs.readFile(filePath, 'utf8')) as unknown;
|
||||
|
||||
const normalizePoolRow = (row: unknown, index: number): GeneralPoolSeedEntry => {
|
||||
if (!Array.isArray(row) || row.length !== EXPECTED_COLUMNS.length) {
|
||||
throw new Error(`General pool row ${index} does not match the expected ${EXPECTED_COLUMNS.length} columns.`);
|
||||
const normalizePoolRow = (
|
||||
poolName: string,
|
||||
columns: readonly string[],
|
||||
row: unknown,
|
||||
index: number
|
||||
): GeneralPoolSeedEntry => {
|
||||
if (!Array.isArray(row) || row.length !== columns.length) {
|
||||
throw new Error(`General pool row ${index} does not match the expected ${columns.length} columns.`);
|
||||
}
|
||||
const info = Object.fromEntries(EXPECTED_COLUMNS.map((column, columnIndex) => [column, row[columnIndex]]));
|
||||
const uniqueName = info.generalName;
|
||||
if (typeof uniqueName !== 'string' || uniqueName.length === 0) {
|
||||
const info = Object.fromEntries(columns.map((column, columnIndex) => [column, row[columnIndex]]));
|
||||
const generalName = info.generalName;
|
||||
if (typeof generalName !== 'string' || generalName.length === 0) {
|
||||
throw new Error(`General pool row ${index} has no generalName.`);
|
||||
}
|
||||
const uniqueName = poolName === 'SPoolUnderU100' ? `A100${String(index + 1).padStart(4, '0')}` : generalName;
|
||||
if (uniqueName.length > 20) {
|
||||
throw new Error(`General pool row ${index} has a generalName longer than the select_pool key.`);
|
||||
}
|
||||
const isCentennial = poolName === 'SPoolUnderU100';
|
||||
const specialDomesticIsValid =
|
||||
(isCentennial && info.specialDomestic === null) ||
|
||||
(typeof info.specialDomestic === 'string' && isEventDomesticTraitKey(info.specialDomestic));
|
||||
if (
|
||||
!Number.isInteger(info.leadership) ||
|
||||
!Number.isInteger(info.strength) ||
|
||||
!Number.isInteger(info.intel) ||
|
||||
typeof info.specialDomestic !== 'string' ||
|
||||
!isEventDomesticTraitKey(info.specialDomestic) ||
|
||||
!specialDomesticIsValid ||
|
||||
!Array.isArray(info.dex) ||
|
||||
info.dex.length !== 5 ||
|
||||
info.dex.some((value) => typeof value !== 'number' || !Number.isInteger(value) || value < 0) ||
|
||||
info.dex.reduce((sum, value) => sum + Number(value), 0) <= 0 ||
|
||||
(!isCentennial && info.dex.reduce((sum, value) => sum + Number(value), 0) <= 0) ||
|
||||
(info.imgsvr !== 0 && info.imgsvr !== 1) ||
|
||||
typeof info.picture !== 'string'
|
||||
) {
|
||||
throw new Error(`General pool row ${index} contains invalid candidate data.`);
|
||||
}
|
||||
if (
|
||||
isCentennial &&
|
||||
(!Number.isInteger(info.sourcePhase) ||
|
||||
typeof info.sourceServerId !== 'string' ||
|
||||
!Number.isInteger(info.sourceGeneralNo) ||
|
||||
!Array.isArray(info.selectionReasons) ||
|
||||
info.selectionReasons.some((reason) => typeof reason !== 'string'))
|
||||
) {
|
||||
throw new Error(`General pool row ${index} contains invalid source metadata.`);
|
||||
}
|
||||
return {
|
||||
uniqueName,
|
||||
info: {
|
||||
...info,
|
||||
uniqueName,
|
||||
...(isCentennial ? { event100Growth: true } : {}),
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -71,7 +98,7 @@ export const loadGeneralPoolEntries = async (
|
||||
poolName: string,
|
||||
options?: GeneralPoolLoaderOptions
|
||||
): Promise<GeneralPoolSeedEntry[]> => {
|
||||
if (poolName !== SUPPORTED_POOL) {
|
||||
if (!SUPPORTED_POOLS.has(poolName)) {
|
||||
throw new Error(`Unsupported general pool: ${poolName}.`);
|
||||
}
|
||||
const root = path.resolve(options?.generalPoolRoot ?? DEFAULT_GENERAL_POOL_ROOT);
|
||||
@@ -79,13 +106,14 @@ export const loadGeneralPoolEntries = async (
|
||||
if (!isRecord(raw) || !Array.isArray(raw.columns) || !Array.isArray(raw.data)) {
|
||||
throw new Error(`General pool ${poolName} is not a valid resource.`);
|
||||
}
|
||||
const expectedColumns = poolName === 'SPoolUnderU100' ? CENTENNIAL_COLUMNS : BASE_COLUMNS;
|
||||
if (
|
||||
raw.columns.length !== EXPECTED_COLUMNS.length ||
|
||||
raw.columns.some((column, index) => column !== EXPECTED_COLUMNS[index])
|
||||
raw.columns.length !== expectedColumns.length ||
|
||||
raw.columns.some((column, index) => column !== expectedColumns[index])
|
||||
) {
|
||||
throw new Error(`General pool ${poolName} has an unexpected column contract.`);
|
||||
}
|
||||
const entries = raw.data.map(normalizePoolRow);
|
||||
const entries = raw.data.map((row, index) => normalizePoolRow(poolName, expectedColumns, row, index));
|
||||
if (new Set(entries.map((entry) => entry.uniqueName)).size !== entries.length) {
|
||||
throw new Error(`General pool ${poolName} contains duplicate unique names.`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user