feat: add logical game clock

This commit is contained in:
2026-08-04 02:51:27 +00:00
parent 87965a39d6
commit a26031dc3f
51 changed files with 1605 additions and 398 deletions
+29 -23
View File
@@ -9,7 +9,8 @@ import { ItemLoader, isItemKey } from '@sammo-ts/logic';
import { asNumber, asRecord } from '@sammo-ts/common';
import { buildAuctionAlias } from '@sammo-ts/logic';
import { openAuctionWithDaemon } from '../../auction/open.js';
import { resolveAuctionTimerScore } from '../../auction/scheduler.js';
import { loadCurrentGameTime } from '../../services/gameClock.js';
const zBidInput = z.object({
auctionId: z.number().int().positive(),
@@ -99,10 +100,7 @@ const ensureAuctionSeasonActive = async (db: DatabaseClient): Promise<void> => {
}
};
const loadAuction = async (
db: DatabaseClient,
auctionId: number
): Promise<AuctionRow | null> => {
const loadAuction = async (db: DatabaseClient, auctionId: number): Promise<AuctionRow | null> => {
const rows = (await db.$queryRaw(
GamePrisma.sql`
SELECT id,
@@ -190,10 +188,7 @@ export const auctionRouter = router({
const [auctions, worldState, point, recentLogs] = await Promise.all([
ctx.db.auction.findMany({
where: {
OR: [
{ type: { in: ['BUY_RICE', 'SELL_RICE'] }, status: 'OPEN' },
{ type: 'UNIQUE_ITEM' },
],
OR: [{ type: { in: ['BUY_RICE', 'SELL_RICE'] }, status: 'OPEN' }, { type: 'UNIQUE_ITEM' }],
},
orderBy: [{ status: 'asc' }, { id: 'desc' }],
take: 120,
@@ -238,7 +233,7 @@ export const auctionRouter = router({
const hiddenSeed =
typeof worldMeta.hiddenSeed === 'string' || typeof worldMeta.hiddenSeed === 'number'
? worldMeta.hiddenSeed
: worldState?.id ?? 0;
: (worldState?.id ?? 0);
const callerAlias = buildAuctionAlias(general.id, hiddenSeed, configConst);
const mapped = auctions.map((auction) => {
@@ -252,8 +247,8 @@ export const auctionRouter = router({
status: auction.status,
hostGeneralId: isUnique ? null : auction.hostGeneralId,
hostName: isUnique
? auction.hostName ?? buildAuctionAlias(auction.hostGeneralId, hiddenSeed, configConst)
: auction.hostName ?? names.get(auction.hostGeneralId) ?? '상인',
? (auction.hostName ?? buildAuctionAlias(auction.hostGeneralId, hiddenSeed, configConst))
: (auction.hostName ?? names.get(auction.hostGeneralId) ?? '상인'),
isCallerHost: auction.hostGeneralId === general.id,
closeAt: auction.closeAt.toISOString(),
detail,
@@ -262,7 +257,7 @@ export const auctionRouter = router({
amount: highestBid.amount,
bidderName: isUnique
? buildAuctionAlias(highestBid.generalId, hiddenSeed, configConst)
: names.get(highestBid.generalId) ?? '상인',
: (names.get(highestBid.generalId) ?? '상인'),
isCaller: highestBid.generalId === general.id,
eventAt: highestBid.eventAt.toISOString(),
}
@@ -305,14 +300,13 @@ export const auctionRouter = router({
const hiddenSeed =
typeof worldMeta.hiddenSeed === 'string' || typeof worldMeta.hiddenSeed === 'number'
? worldMeta.hiddenSeed
: worldState?.id ?? 0;
: (worldState?.id ?? 0);
return {
auction: {
id: auction.id,
targetCode: auction.targetCode,
status: auction.status,
hostName:
auction.hostName ?? buildAuctionAlias(auction.hostGeneralId, hiddenSeed, configConst),
hostName: auction.hostName ?? buildAuctionAlias(auction.hostGeneralId, hiddenSeed, configConst),
isCallerHost: auction.hostGeneralId === general.id,
closeAt: auction.closeAt.toISOString(),
detail: parseDetail(auction.detail),
@@ -362,7 +356,8 @@ export const auctionRouter = router({
throw new TRPCError({ code: 'BAD_REQUEST', message: '경매가 종료되었습니다.' });
}
const now = new Date();
const gameTime = await loadCurrentGameTime(ctx.db);
const { now } = gameTime;
if (auction.closeAt <= now) {
throw new TRPCError({ code: 'BAD_REQUEST', message: '경매가 종료되었습니다.' });
}
@@ -418,7 +413,9 @@ export const auctionRouter = router({
const timerKeys = buildAuctionTimerKeys(ctx.profile.name);
const nextCloseAt = new Date(result.closeAt);
await ctx.redis.zAdd(timerKeys.timerKey, [{ score: nextCloseAt.getTime(), value: String(auction.id) }]);
await ctx.redis.zAdd(timerKeys.timerKey, [
{ score: resolveAuctionTimerScore(gameTime, nextCloseAt), value: String(auction.id) },
]);
return { ok: true };
}),
@@ -434,7 +431,8 @@ export const auctionRouter = router({
throw new TRPCError({ code: 'BAD_REQUEST', message: '경매가 종료되었습니다.' });
}
const now = new Date();
const gameTime = await loadCurrentGameTime(ctx.db);
const { now } = gameTime;
if (auction.closeAt <= now) {
throw new TRPCError({ code: 'BAD_REQUEST', message: '경매가 종료되었습니다.' });
}
@@ -490,7 +488,9 @@ export const auctionRouter = router({
const timerKeys = buildAuctionTimerKeys(ctx.profile.name);
const nextCloseAt = new Date(result.closeAt);
await ctx.redis.zAdd(timerKeys.timerKey, [{ score: nextCloseAt.getTime(), value: String(auction.id) }]);
await ctx.redis.zAdd(timerKeys.timerKey, [
{ score: resolveAuctionTimerScore(gameTime, nextCloseAt), value: String(auction.id) },
]);
return { ok: true };
}),
@@ -506,7 +506,8 @@ export const auctionRouter = router({
throw new TRPCError({ code: 'BAD_REQUEST', message: '경매가 종료되었습니다.' });
}
const now = new Date();
const gameTime = await loadCurrentGameTime(ctx.db);
const { now } = gameTime;
if (auction.closeAt <= now) {
throw new TRPCError({ code: 'BAD_REQUEST', message: '경매가 종료되었습니다.' });
}
@@ -586,7 +587,10 @@ export const auctionRouter = router({
}
const otherItem = await itemLoader.load(other.targetCode);
if (otherItem.slot === itemModule.slot) {
throw new TRPCError({ code: 'BAD_REQUEST', message: '1순위 입찰자인 경매중에 같은 부위가 있습니다.' });
throw new TRPCError({
code: 'BAD_REQUEST',
message: '1순위 입찰자인 경매중에 같은 부위가 있습니다.',
});
}
}
}
@@ -620,7 +624,9 @@ export const auctionRouter = router({
const timerKeys = buildAuctionTimerKeys(ctx.profile.name);
const nextCloseAt = new Date(result.closeAt);
await ctx.redis.zAdd(timerKeys.timerKey, [{ score: nextCloseAt.getTime(), value: String(auction.id) }]);
await ctx.redis.zAdd(timerKeys.timerKey, [
{ score: resolveAuctionTimerScore(gameTime, nextCloseAt), value: String(auction.id) },
]);
return { ok: true };
}),