feat: implement auction bidding and resource adjustment commands
- Added `adjustGeneralResources` command to handle resource adjustments for generals. - Introduced `patchGeneral` command for updating general attributes. - Created `auctionBid` command to facilitate bidding in auctions with validation. - Refactored database interactions to use transactions for auction bids and resource adjustments. - Enhanced error handling for auction and resource operations. - Updated command handling in the turn daemon to support new commands. - Introduced `AuctionBidder` interface and implementation for managing auction bids. - Improved overall structure and readability of auction-related code.
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { authedProcedure, router } from '../../trpc.js';
|
||||
@@ -8,8 +7,6 @@ import { buildAuctionTimerKeys } from '../../auction/keys.js';
|
||||
import { GamePrisma } from '@sammo-ts/infra';
|
||||
import { ItemLoader, isItemKey } from '@sammo-ts/logic';
|
||||
|
||||
const COEFF_EXTENSION_MINUTES_PER_BID = 1 / 6;
|
||||
const MIN_EXTENSION_MINUTES_PER_BID = 1;
|
||||
|
||||
const zBidInput = z.object({
|
||||
auctionId: z.number().int().positive(),
|
||||
@@ -56,15 +53,6 @@ const parseDetail = (detail: unknown): AuctionDetail => {
|
||||
return detail as AuctionDetail;
|
||||
};
|
||||
|
||||
const toTurnMinutes = (tickSeconds: number): number => Math.max(1, Math.round(tickSeconds / 60));
|
||||
|
||||
const resolveTurnMinutes = async (db: DatabaseClient) => {
|
||||
const rows = (await db.$queryRaw(
|
||||
GamePrisma.sql`SELECT tick_seconds as "tickSeconds" FROM world_state ORDER BY id LIMIT 1`
|
||||
)) as Array<{ tickSeconds: number }>;
|
||||
return toTurnMinutes(rows[0]?.tickSeconds ?? 60);
|
||||
};
|
||||
|
||||
const requireAuth = (ctx: GameApiContext): NonNullable<GameApiContext['auth']> => {
|
||||
if (!ctx.auth) {
|
||||
throw new TRPCError({
|
||||
@@ -159,42 +147,6 @@ const loadMyPrevBid = async (
|
||||
return rows[0] ?? null;
|
||||
};
|
||||
|
||||
const cancelAuction = async (
|
||||
db: DatabaseClient,
|
||||
auctionId: number,
|
||||
reason: string
|
||||
) => {
|
||||
const now = new Date();
|
||||
await db.$executeRaw(
|
||||
GamePrisma.sql`
|
||||
UPDATE auction
|
||||
SET status = 'CANCELED',
|
||||
finished_at = ${now},
|
||||
updated_at = ${now},
|
||||
detail = jsonb_set(detail, '{cancelReason}', to_jsonb(${reason}), true)
|
||||
WHERE id = ${auctionId}
|
||||
`
|
||||
);
|
||||
};
|
||||
|
||||
const extendCloseDate = (options: {
|
||||
now: Date;
|
||||
closeAt: Date;
|
||||
turnMinutes: number;
|
||||
availableLatestBidCloseDate?: Date | null;
|
||||
}) => {
|
||||
const { now, closeAt, turnMinutes, availableLatestBidCloseDate } = options;
|
||||
const extendMinutes = Math.max(MIN_EXTENSION_MINUTES_PER_BID, turnMinutes * COEFF_EXTENSION_MINUTES_PER_BID);
|
||||
const extended = new Date(now.getTime() + extendMinutes * 60 * 1000);
|
||||
if (extended.getTime() <= closeAt.getTime()) {
|
||||
return closeAt;
|
||||
}
|
||||
if (availableLatestBidCloseDate && extended.getTime() > availableLatestBidCloseDate.getTime()) {
|
||||
return availableLatestBidCloseDate;
|
||||
}
|
||||
return extended;
|
||||
};
|
||||
|
||||
const shouldUsePrevBid = (highestBid: AuctionBidRow | null, myPrevBid: AuctionBidRow | null): AuctionBidRow | null => {
|
||||
if (!myPrevBid) {
|
||||
return null;
|
||||
@@ -208,13 +160,6 @@ const shouldUsePrevBid = (highestBid: AuctionBidRow | null, myPrevBid: AuctionBi
|
||||
return myPrevBid;
|
||||
};
|
||||
|
||||
const runInTransaction = async <T>(db: DatabaseClient, fn: (tx: DatabaseClient) => Promise<T>): Promise<T> => {
|
||||
if (db.$transaction) {
|
||||
return db.$transaction(fn);
|
||||
}
|
||||
return fn(db);
|
||||
};
|
||||
|
||||
export const auctionRouter = router({
|
||||
bidBuyRice: authedProcedure.input(zBidInput).mutation(async ({ ctx, input }) => {
|
||||
const auth = requireAuth(ctx);
|
||||
@@ -260,71 +205,23 @@ export const auctionRouter = router({
|
||||
throw new TRPCError({ code: 'BAD_REQUEST', message: '금이 부족합니다.' });
|
||||
}
|
||||
|
||||
const eventId = randomUUID();
|
||||
const eventAt = now;
|
||||
const turnMinutes = await resolveTurnMinutes(ctx.db);
|
||||
const availableLatestBidCloseDate = detail.availableLatestBidCloseDate
|
||||
? new Date(detail.availableLatestBidCloseDate)
|
||||
: null;
|
||||
const nextCloseAt = extendCloseDate({
|
||||
now,
|
||||
closeAt: auction.closeAt,
|
||||
turnMinutes,
|
||||
availableLatestBidCloseDate,
|
||||
});
|
||||
|
||||
await runInTransaction(ctx.db, async (tx) => {
|
||||
await tx.$executeRaw(
|
||||
GamePrisma.sql`
|
||||
INSERT INTO auction_bid (auction_id, general_id, amount, event_id, event_at, meta)
|
||||
VALUES (${auction.id}, ${general.id}, ${input.amount}, ${eventId}, ${eventAt}, ${JSON.stringify({ tryExtendCloseDate: true })}::jsonb)
|
||||
`
|
||||
);
|
||||
|
||||
await tx.general.update({
|
||||
where: { id: general.id },
|
||||
data: { gold: general.gold - morePoint },
|
||||
});
|
||||
|
||||
if (highestBid && highestBid.generalId !== general.id && !myPrevBid) {
|
||||
const prev = await tx.general.findUnique({ where: { id: highestBid.generalId } });
|
||||
if (!prev) {
|
||||
await cancelAuction(tx, auction.id, '중복 입찰 등 문제가 발생하여 취소');
|
||||
throw new TRPCError({ code: 'CONFLICT', message: '경매가 취소되었습니다.' });
|
||||
}
|
||||
await tx.general.update({
|
||||
where: { id: prev.id },
|
||||
data: { gold: prev.gold + highestBid.amount },
|
||||
});
|
||||
}
|
||||
|
||||
const updated = await tx.$executeRaw(
|
||||
GamePrisma.sql`
|
||||
UPDATE auction
|
||||
SET close_at = ${nextCloseAt},
|
||||
latest_event_id = ${eventId},
|
||||
latest_event_at = ${eventAt},
|
||||
updated_at = ${eventAt}
|
||||
WHERE id = ${auction.id}
|
||||
AND status = 'OPEN'
|
||||
AND (
|
||||
latest_event_at < ${eventAt}
|
||||
OR (latest_event_at = ${eventAt} AND latest_event_id < ${eventId})
|
||||
)
|
||||
`
|
||||
);
|
||||
|
||||
if (updated === 0) {
|
||||
await cancelAuction(tx, auction.id, '중복 입찰 등 문제가 발생하여 취소');
|
||||
await tx.general.update({
|
||||
where: { id: general.id },
|
||||
data: { gold: general.gold },
|
||||
});
|
||||
throw new TRPCError({ code: 'CONFLICT', message: '경매가 취소되었습니다.' });
|
||||
}
|
||||
const result = await ctx.turnDaemon.requestCommand({
|
||||
type: 'auctionBid',
|
||||
auctionId: auction.id,
|
||||
generalId: general.id,
|
||||
amount: input.amount,
|
||||
tryExtendCloseDate: true,
|
||||
});
|
||||
if (!result || result.type !== 'auctionBid') {
|
||||
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Unexpected response' });
|
||||
}
|
||||
if (!result.ok) {
|
||||
const code = result.reason.includes('취소') ? 'CONFLICT' : 'BAD_REQUEST';
|
||||
throw new TRPCError({ code, message: result.reason });
|
||||
}
|
||||
|
||||
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) }]);
|
||||
|
||||
return { ok: true };
|
||||
@@ -373,71 +270,23 @@ export const auctionRouter = router({
|
||||
throw new TRPCError({ code: 'BAD_REQUEST', message: '쌀이 부족합니다.' });
|
||||
}
|
||||
|
||||
const eventId = randomUUID();
|
||||
const eventAt = now;
|
||||
const turnMinutes = await resolveTurnMinutes(ctx.db);
|
||||
const availableLatestBidCloseDate = detail.availableLatestBidCloseDate
|
||||
? new Date(detail.availableLatestBidCloseDate)
|
||||
: null;
|
||||
const nextCloseAt = extendCloseDate({
|
||||
now,
|
||||
closeAt: auction.closeAt,
|
||||
turnMinutes,
|
||||
availableLatestBidCloseDate,
|
||||
});
|
||||
|
||||
await runInTransaction(ctx.db, async (tx) => {
|
||||
await tx.$executeRaw(
|
||||
GamePrisma.sql`
|
||||
INSERT INTO auction_bid (auction_id, general_id, amount, event_id, event_at, meta)
|
||||
VALUES (${auction.id}, ${general.id}, ${input.amount}, ${eventId}, ${eventAt}, ${JSON.stringify({ tryExtendCloseDate: true })}::jsonb)
|
||||
`
|
||||
);
|
||||
|
||||
await tx.general.update({
|
||||
where: { id: general.id },
|
||||
data: { rice: general.rice - morePoint },
|
||||
});
|
||||
|
||||
if (highestBid && highestBid.generalId !== general.id && !myPrevBid) {
|
||||
const prev = await tx.general.findUnique({ where: { id: highestBid.generalId } });
|
||||
if (!prev) {
|
||||
await cancelAuction(tx, auction.id, '중복 입찰 등 문제가 발생하여 취소');
|
||||
throw new TRPCError({ code: 'CONFLICT', message: '경매가 취소되었습니다.' });
|
||||
}
|
||||
await tx.general.update({
|
||||
where: { id: prev.id },
|
||||
data: { rice: prev.rice + highestBid.amount },
|
||||
});
|
||||
}
|
||||
|
||||
const updated = await tx.$executeRaw(
|
||||
GamePrisma.sql`
|
||||
UPDATE auction
|
||||
SET close_at = ${nextCloseAt},
|
||||
latest_event_id = ${eventId},
|
||||
latest_event_at = ${eventAt},
|
||||
updated_at = ${eventAt}
|
||||
WHERE id = ${auction.id}
|
||||
AND status = 'OPEN'
|
||||
AND (
|
||||
latest_event_at < ${eventAt}
|
||||
OR (latest_event_at = ${eventAt} AND latest_event_id < ${eventId})
|
||||
)
|
||||
`
|
||||
);
|
||||
|
||||
if (updated === 0) {
|
||||
await cancelAuction(tx, auction.id, '중복 입찰 등 문제가 발생하여 취소');
|
||||
await tx.general.update({
|
||||
where: { id: general.id },
|
||||
data: { rice: general.rice },
|
||||
});
|
||||
throw new TRPCError({ code: 'CONFLICT', message: '경매가 취소되었습니다.' });
|
||||
}
|
||||
const result = await ctx.turnDaemon.requestCommand({
|
||||
type: 'auctionBid',
|
||||
auctionId: auction.id,
|
||||
generalId: general.id,
|
||||
amount: input.amount,
|
||||
tryExtendCloseDate: true,
|
||||
});
|
||||
if (!result || result.type !== 'auctionBid') {
|
||||
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Unexpected response' });
|
||||
}
|
||||
if (!result.ok) {
|
||||
const code = result.reason.includes('취소') ? 'CONFLICT' : 'BAD_REQUEST';
|
||||
throw new TRPCError({ code, message: result.reason });
|
||||
}
|
||||
|
||||
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) }]);
|
||||
|
||||
return { ok: true };
|
||||
@@ -550,84 +399,23 @@ export const auctionRouter = router({
|
||||
throw new TRPCError({ code: 'BAD_REQUEST', message: '유산포인트가 부족합니다.' });
|
||||
}
|
||||
|
||||
const eventId = randomUUID();
|
||||
const eventAt = now;
|
||||
const turnMinutes = await resolveTurnMinutes(ctx.db);
|
||||
const availableLatestBidCloseDate = detail.availableLatestBidCloseDate
|
||||
? new Date(detail.availableLatestBidCloseDate)
|
||||
: null;
|
||||
const nextCloseAt = extendCloseDate({
|
||||
now,
|
||||
closeAt: auction.closeAt,
|
||||
turnMinutes,
|
||||
availableLatestBidCloseDate,
|
||||
});
|
||||
|
||||
await runInTransaction(ctx.db, async (tx) => {
|
||||
await tx.$executeRaw(
|
||||
GamePrisma.sql`
|
||||
INSERT INTO auction_bid (auction_id, general_id, amount, event_id, event_at, meta)
|
||||
VALUES (
|
||||
${auction.id},
|
||||
${general.id},
|
||||
${input.amount},
|
||||
${eventId},
|
||||
${eventAt},
|
||||
${JSON.stringify({ tryExtendCloseDate: input.tryExtendCloseDate ?? true })}::jsonb
|
||||
)
|
||||
`
|
||||
);
|
||||
|
||||
const prevValue = inheritPoint?.value ?? 0;
|
||||
await tx.inheritancePoint.upsert({
|
||||
where: { userId_key: { userId: auth.user.id, key: 'previous' } },
|
||||
update: { value: prevValue - morePoint },
|
||||
create: { userId: auth.user.id, key: 'previous', value: prevValue - morePoint },
|
||||
});
|
||||
|
||||
if (highestBid && highestBid.generalId !== general.id && !myPrevBid) {
|
||||
const prev = await tx.general.findUnique({ where: { id: highestBid.generalId } });
|
||||
if (!prev) {
|
||||
await cancelAuction(tx, auction.id, '중복 입찰 등 문제가 발생하여 취소');
|
||||
throw new TRPCError({ code: 'CONFLICT', message: '경매가 취소되었습니다.' });
|
||||
}
|
||||
const prevUserId = prev.userId ?? '';
|
||||
if (prevUserId) {
|
||||
const prevPoint = await tx.inheritancePoint.findUnique({
|
||||
where: { userId_key: { userId: prevUserId, key: 'previous' } },
|
||||
});
|
||||
const nextValue = (prevPoint?.value ?? 0) + highestBid.amount;
|
||||
await tx.inheritancePoint.upsert({
|
||||
where: { userId_key: { userId: prevUserId, key: 'previous' } },
|
||||
update: { value: nextValue },
|
||||
create: { userId: prevUserId, key: 'previous', value: nextValue },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const updated = await tx.$executeRaw(
|
||||
GamePrisma.sql`
|
||||
UPDATE auction
|
||||
SET close_at = ${nextCloseAt},
|
||||
latest_event_id = ${eventId},
|
||||
latest_event_at = ${eventAt},
|
||||
updated_at = ${eventAt}
|
||||
WHERE id = ${auction.id}
|
||||
AND status = 'OPEN'
|
||||
AND (
|
||||
latest_event_at < ${eventAt}
|
||||
OR (latest_event_at = ${eventAt} AND latest_event_id < ${eventId})
|
||||
)
|
||||
`
|
||||
);
|
||||
|
||||
if (updated === 0) {
|
||||
await cancelAuction(tx, auction.id, '중복 입찰 등 문제가 발생하여 취소');
|
||||
throw new TRPCError({ code: 'CONFLICT', message: '경매가 취소되었습니다.' });
|
||||
}
|
||||
const result = await ctx.turnDaemon.requestCommand({
|
||||
type: 'auctionBid',
|
||||
auctionId: auction.id,
|
||||
generalId: general.id,
|
||||
amount: input.amount,
|
||||
tryExtendCloseDate: input.tryExtendCloseDate ?? true,
|
||||
});
|
||||
if (!result || result.type !== 'auctionBid') {
|
||||
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Unexpected response' });
|
||||
}
|
||||
if (!result.ok) {
|
||||
const code = result.reason.includes('취소') ? 'CONFLICT' : 'BAD_REQUEST';
|
||||
throw new TRPCError({ code, message: result.reason });
|
||||
}
|
||||
|
||||
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) }]);
|
||||
|
||||
return { ok: true };
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
sumInheritanceItems,
|
||||
writeUserStateMeta,
|
||||
} from '../../services/inheritance.js';
|
||||
import type { WorldStateRow } from '../../context.js';
|
||||
import type { GameApiContext, WorldStateRow } from '../../context.js';
|
||||
|
||||
const BUFF_KEYS: InheritBuffType[] = [
|
||||
'warAvoidRatio',
|
||||
@@ -74,6 +74,33 @@ const resolveWorld = async (ctx: { db: { worldState: { findFirst: () => Promise<
|
||||
};
|
||||
};
|
||||
|
||||
const patchGeneral = async (
|
||||
ctx: Pick<GameApiContext, 'turnDaemon'>,
|
||||
generalId: number,
|
||||
patch: {
|
||||
meta?: Record<string, unknown>;
|
||||
turnTime?: string;
|
||||
stats?: {
|
||||
leadership?: number;
|
||||
strength?: number;
|
||||
intelligence?: number;
|
||||
};
|
||||
specialWar?: string;
|
||||
}
|
||||
): Promise<void> => {
|
||||
const result = await ctx.turnDaemon.requestCommand({
|
||||
type: 'patchGeneral',
|
||||
generalId,
|
||||
patch,
|
||||
});
|
||||
if (!result || result.type !== 'patchGeneral') {
|
||||
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Unexpected response' });
|
||||
}
|
||||
if (!result.ok) {
|
||||
throw new TRPCError({ code: 'BAD_REQUEST', message: result.reason });
|
||||
}
|
||||
};
|
||||
|
||||
const buildTurnTimeZoneList = (tickMinutes: number): string[] => {
|
||||
const zones: string[] = [];
|
||||
for (let i = 0; i < 60; i += 1) {
|
||||
@@ -306,13 +333,10 @@ export const inheritRouter = router({
|
||||
const buffText = BUFF_LABELS[input.type];
|
||||
const moreText = prevLevel > 0 ? '추가' : '';
|
||||
buff[input.type] = input.level;
|
||||
await ctx.db.general.update({
|
||||
where: { id: general.id },
|
||||
data: {
|
||||
meta: {
|
||||
...asRecord(general.meta),
|
||||
inheritBuff: serializeBuffRecord(buff),
|
||||
},
|
||||
await patchGeneral(ctx, general.id, {
|
||||
meta: {
|
||||
...asRecord(general.meta),
|
||||
inheritBuff: serializeBuffRecord(buff),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -385,13 +409,10 @@ export const inheritRouter = router({
|
||||
const [warModule] = await loadWarTraitModules([input.specialKey], new WarTraitLoader());
|
||||
const warName = warModule?.name ?? input.specialKey;
|
||||
|
||||
await ctx.db.general.update({
|
||||
where: { id: general.id },
|
||||
data: {
|
||||
meta: {
|
||||
...meta,
|
||||
inheritSpecificSpecialWar: input.specialKey,
|
||||
},
|
||||
await patchGeneral(ctx, general.id, {
|
||||
meta: {
|
||||
...meta,
|
||||
inheritSpecificSpecialWar: input.specialKey,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -441,15 +462,12 @@ export const inheritRouter = router({
|
||||
const prevList = parseJson<string[]>(typeof meta.prev_types_special2 === 'string' ? meta.prev_types_special2 : null) ?? [];
|
||||
prevList.push(general.special2Code);
|
||||
|
||||
await ctx.db.general.update({
|
||||
where: { id: general.id },
|
||||
data: {
|
||||
special2Code: 'None',
|
||||
meta: {
|
||||
...meta,
|
||||
inheritResetSpecialWar: nextLevel,
|
||||
prev_types_special2: JSON.stringify(prevList),
|
||||
},
|
||||
await patchGeneral(ctx, general.id, {
|
||||
specialWar: 'None',
|
||||
meta: {
|
||||
...meta,
|
||||
inheritResetSpecialWar: nextLevel,
|
||||
prev_types_special2: JSON.stringify(prevList),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -496,14 +514,11 @@ export const inheritRouter = router({
|
||||
nextTurnTime = new Date(nextTurnTime.getTime() + tickMinutes * 60000);
|
||||
}
|
||||
|
||||
await ctx.db.general.update({
|
||||
where: { id: general.id },
|
||||
data: {
|
||||
turnTime: nextTurnTime,
|
||||
meta: {
|
||||
...asRecord(general.meta),
|
||||
inheritResetTurnTime: nextLevel,
|
||||
},
|
||||
await patchGeneral(ctx, general.id, {
|
||||
turnTime: nextTurnTime.toISOString(),
|
||||
meta: {
|
||||
...asRecord(general.meta),
|
||||
inheritResetTurnTime: nextLevel,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -620,12 +635,11 @@ export const inheritRouter = router({
|
||||
intel: input.intel + finalBonus[2],
|
||||
};
|
||||
|
||||
await ctx.db.general.update({
|
||||
where: { id: general.id },
|
||||
data: {
|
||||
await patchGeneral(ctx, general.id, {
|
||||
stats: {
|
||||
leadership: nextStats.leadership,
|
||||
strength: nextStats.strength,
|
||||
intel: nextStats.intel,
|
||||
intelligence: nextStats.intel,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -697,13 +711,10 @@ export const inheritRouter = router({
|
||||
throw new TRPCError({ code: 'BAD_REQUEST', message: '이미 구입 명령을 내렸습니다. 다음 턴까지 기다려주세요.' });
|
||||
}
|
||||
|
||||
await ctx.db.general.update({
|
||||
where: { id: general.id },
|
||||
data: {
|
||||
meta: {
|
||||
...meta,
|
||||
inheritRandomUnique: 1,
|
||||
},
|
||||
await patchGeneral(ctx, general.id, {
|
||||
meta: {
|
||||
...meta,
|
||||
inheritRandomUnique: 1,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -755,17 +766,14 @@ export const inheritRouter = router({
|
||||
throw new TRPCError({ code: 'BAD_REQUEST', message: '이미 유니크 경매 신청이 있습니다.' });
|
||||
}
|
||||
|
||||
await ctx.db.general.update({
|
||||
where: { id: general.id },
|
||||
data: {
|
||||
meta: {
|
||||
...meta,
|
||||
inheritSpecificUnique: JSON.stringify({
|
||||
itemId: input.itemId,
|
||||
amount: input.amount,
|
||||
requestedAt: new Date().toISOString(),
|
||||
}),
|
||||
},
|
||||
await patchGeneral(ctx, general.id, {
|
||||
meta: {
|
||||
...meta,
|
||||
inheritSpecificUnique: JSON.stringify({
|
||||
itemId: input.itemId,
|
||||
amount: input.amount,
|
||||
requestedAt: new Date().toISOString(),
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -265,11 +265,17 @@ export const tournamentRouter = router({
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: '장수 정보를 찾을 수 없습니다.' });
|
||||
}
|
||||
|
||||
const nextMeta = { ...asRecord(general.meta), tnmt: 1 };
|
||||
await ctx.db.general.update({
|
||||
where: { id: general.id },
|
||||
data: { meta: nextMeta },
|
||||
const result = await ctx.turnDaemon.requestCommand({
|
||||
type: 'setMySetting',
|
||||
generalId: general.id,
|
||||
settings: { tnmt: 1 },
|
||||
});
|
||||
if (!result || result.type !== 'setMySetting') {
|
||||
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Unexpected response' });
|
||||
}
|
||||
if (!result.ok) {
|
||||
throw new TRPCError({ code: 'BAD_REQUEST', message: result.reason ?? '요청에 실패했습니다.' });
|
||||
}
|
||||
|
||||
const already = participants.find((entry) => entry.id === general.id);
|
||||
if (already) {
|
||||
@@ -403,10 +409,17 @@ export const tournamentRouter = router({
|
||||
throw new TRPCError({ code: 'BAD_REQUEST', message: '소지금이 부족합니다.' });
|
||||
}
|
||||
|
||||
await ctx.db.general.update({
|
||||
where: { id: general.id },
|
||||
data: { gold: general.gold - input.amount },
|
||||
const adjustResult = await ctx.turnDaemon.requestCommand({
|
||||
type: 'adjustGeneralResources',
|
||||
reason: 'tournamentBet',
|
||||
adjustments: [{ generalId: general.id, goldDelta: -input.amount }],
|
||||
});
|
||||
if (!adjustResult || adjustResult.type !== 'adjustGeneralResources') {
|
||||
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Unexpected response' });
|
||||
}
|
||||
if (!adjustResult.ok) {
|
||||
throw new TRPCError({ code: 'BAD_REQUEST', message: adjustResult.reason });
|
||||
}
|
||||
|
||||
await store.appendBettingEntry({
|
||||
generalId: general.id,
|
||||
|
||||
@@ -681,8 +681,9 @@ const seedNpcBets = async (options: {
|
||||
store: TournamentStore;
|
||||
state: TournamentState;
|
||||
baseSeed: string;
|
||||
daemonTransport: TurnDaemonTransport;
|
||||
}): Promise<void> => {
|
||||
const { prisma, store, state, baseSeed } = options;
|
||||
const { prisma, store, state, baseSeed, daemonTransport } = options;
|
||||
const existing = await store.getBettingEntries();
|
||||
if (existing.length > 0) {
|
||||
return;
|
||||
@@ -737,14 +738,14 @@ const seedNpcBets = async (options: {
|
||||
entries.push({ generalId: npc.id as number, targetId, amount: betGold });
|
||||
}
|
||||
|
||||
await prisma.$transaction(
|
||||
npcBetList.map((npc) =>
|
||||
prisma.general.update({
|
||||
where: { id: npc.id as number },
|
||||
data: { gold: (npc.gold as number) - betGold },
|
||||
})
|
||||
)
|
||||
);
|
||||
await daemonTransport.sendCommand({
|
||||
type: 'adjustGeneralResources',
|
||||
reason: 'tournamentNpcBet',
|
||||
adjustments: npcBetList.map((npc) => ({
|
||||
generalId: npc.id as number,
|
||||
goldDelta: -betGold,
|
||||
})),
|
||||
});
|
||||
await store.setBettingEntries(entries);
|
||||
};
|
||||
|
||||
@@ -752,7 +753,8 @@ export const applyPreBattleStage = async (
|
||||
store: TournamentStore,
|
||||
prisma: TournamentPrismaClient,
|
||||
state: TournamentState,
|
||||
baseSeed: string
|
||||
baseSeed: string,
|
||||
daemonTransport: TurnDaemonTransport
|
||||
): Promise<TournamentState> => {
|
||||
const participants = await store.getParticipants();
|
||||
|
||||
@@ -976,7 +978,7 @@ export const applyPreBattleStage = async (
|
||||
nextAt: resolveNextAt(state),
|
||||
};
|
||||
await store.setState(nextState);
|
||||
await seedNpcBets({ prisma, store, state: nextState, baseSeed });
|
||||
await seedNpcBets({ prisma, store, state: nextState, baseSeed, daemonTransport });
|
||||
return nextState;
|
||||
}
|
||||
|
||||
@@ -1115,7 +1117,13 @@ export const runTournamentWorker = async (): Promise<void> => {
|
||||
if (isBattleStage(state.stage)) {
|
||||
nextState = await applyBattle(store, state, String(baseSeed));
|
||||
} else if (isPreBattleStage(state.stage)) {
|
||||
nextState = await applyPreBattleStage(store, postgres.prisma, state, String(baseSeed));
|
||||
nextState = await applyPreBattleStage(
|
||||
store,
|
||||
postgres.prisma,
|
||||
state,
|
||||
String(baseSeed),
|
||||
daemonTransport
|
||||
);
|
||||
}
|
||||
|
||||
await settleTournamentOutcome({
|
||||
|
||||
Reference in New Issue
Block a user