은퇴·사망·통일의 저장 순서와 명예의 전당 및 명장일람 판정을 Ref 흐름에 맞춘다. 유산 행동을 인증된 daemon transaction으로 통합하고 중복 지급·고유 아이템·로그·오류 경계를 회귀 테스트한다.
84 lines
2.9 KiB
TypeScript
84 lines
2.9 KiB
TypeScript
import type { HallOfFameType } from '@sammo-ts/common';
|
|
import type { GamePrisma, InputJsonValue } from '@sammo-ts/infra';
|
|
|
|
const asJson = (value: unknown): InputJsonValue => value as InputJsonValue;
|
|
|
|
const readInteger = (value: unknown, fallback: number): number => {
|
|
const parsed = typeof value === 'string' ? Number(value) : value;
|
|
return typeof parsed === 'number' && Number.isFinite(parsed) ? Math.floor(parsed) : fallback;
|
|
};
|
|
|
|
/**
|
|
* `gameIdx` is fixed when RESET opens a game and deliberately excludes
|
|
* retained ABANDONED rows. Older fixtures may not carry it, so reconstruct the
|
|
* same sequence from completed games plus the configured first index.
|
|
*/
|
|
export const resolveOfficialGameIndex = async (
|
|
prisma: GamePrisma.TransactionClient,
|
|
worldMeta: Record<string, unknown>
|
|
): Promise<number> => {
|
|
if (worldMeta.gameIdx !== undefined) {
|
|
return readInteger(worldMeta.gameIdx, 0);
|
|
}
|
|
const completedGames = await prisma.gameHistory.count({ where: { status: 'COMPLETED' } });
|
|
return completedGames + readInteger(worldMeta.firstGameIdx, 1);
|
|
};
|
|
|
|
export interface HallOfFameCandidate {
|
|
serverId: string;
|
|
season: number;
|
|
scenario: number;
|
|
generalNo: number;
|
|
type: HallOfFameType;
|
|
value: number;
|
|
owner: string | null;
|
|
aux: unknown;
|
|
}
|
|
|
|
/**
|
|
* Ref insertIgnore treats an owner record belonging to another general as a
|
|
* complete winner: it does not reassign that row even when the new value is
|
|
* higher. Only an existing row for the same general and scenario may replace
|
|
* value+aux, keeping every identity column unchanged. This avoids the former
|
|
* Core state where an old general number was combined with a new general's aux.
|
|
*/
|
|
export const persistHallOfFameCandidate = async (
|
|
prisma: GamePrisma.TransactionClient,
|
|
candidate: HallOfFameCandidate
|
|
): Promise<'CREATED' | 'UPDATED' | 'PRESERVED'> => {
|
|
const matches = await prisma.hallOfFame.findMany({
|
|
where: {
|
|
OR: [
|
|
{ serverId: candidate.serverId, type: candidate.type, generalNo: candidate.generalNo },
|
|
...(candidate.owner
|
|
? [{ serverId: candidate.serverId, type: candidate.type, owner: candidate.owner }]
|
|
: []),
|
|
],
|
|
},
|
|
});
|
|
const sameGeneral = matches.find((entry) => entry.generalNo === candidate.generalNo);
|
|
if (!sameGeneral && matches.length > 0) {
|
|
return 'PRESERVED';
|
|
}
|
|
if (!sameGeneral) {
|
|
await prisma.hallOfFame.create({
|
|
data: {
|
|
...candidate,
|
|
aux: asJson(candidate.aux),
|
|
},
|
|
});
|
|
return 'CREATED';
|
|
}
|
|
if (sameGeneral.scenario !== candidate.scenario || candidate.value <= sameGeneral.value) {
|
|
return 'PRESERVED';
|
|
}
|
|
await prisma.hallOfFame.update({
|
|
where: { id: sameGeneral.id },
|
|
data: {
|
|
value: candidate.value,
|
|
aux: asJson(candidate.aux),
|
|
},
|
|
});
|
|
return 'UPDATED';
|
|
};
|