feat: 연감 기능 추가 및 데이터베이스 모델 생성
This commit is contained in:
@@ -19,6 +19,7 @@ import { auctionRouter } from './router/auction/index.js';
|
||||
import { tournamentRouter } from './router/tournament/index.js';
|
||||
import { boardRouter } from './router/board/index.js';
|
||||
import { diplomacyRouter } from './router/diplomacy/index.js';
|
||||
import { yearbookRouter } from './router/yearbook/index.js';
|
||||
|
||||
export const appRouter = router({
|
||||
health: healthRouter,
|
||||
@@ -40,6 +41,7 @@ export const appRouter = router({
|
||||
tournament: tournamentRouter,
|
||||
board: boardRouter,
|
||||
diplomacy: diplomacyRouter,
|
||||
yearbook: yearbookRouter,
|
||||
});
|
||||
|
||||
export type AppRouter = typeof appRouter;
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
import { createHash } from 'crypto';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { asRecord, isRecord } from '@sammo-ts/common';
|
||||
import { LogCategory, LogScope } from '@sammo-ts/infra';
|
||||
|
||||
import type { GameApiContext } from '../../context.js';
|
||||
import { loadPublicMap, type BaseMapResult } from '../../maps/worldMap.js';
|
||||
import { procedure, router } from '../../trpc.js';
|
||||
|
||||
type YearbookNation = {
|
||||
id: number;
|
||||
name: string;
|
||||
color: string;
|
||||
level: number;
|
||||
power: number;
|
||||
cities: string[];
|
||||
};
|
||||
|
||||
const joinYearMonth = (year: number, month: number): number => year * 12 + month - 1;
|
||||
|
||||
const computeHash = (payload: unknown): string =>
|
||||
createHash('sha256').update(JSON.stringify(payload)).digest('hex');
|
||||
|
||||
const parseYearbookNations = (value: unknown): YearbookNation[] => {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
const output: YearbookNation[] = [];
|
||||
for (const item of value) {
|
||||
if (!isRecord(item)) {
|
||||
continue;
|
||||
}
|
||||
const id = typeof item.id === 'number' ? item.id : null;
|
||||
const name = typeof item.name === 'string' ? item.name : null;
|
||||
const color = typeof item.color === 'string' ? item.color : null;
|
||||
const level = typeof item.level === 'number' ? item.level : null;
|
||||
const power = typeof item.power === 'number' ? item.power : null;
|
||||
const cities = Array.isArray(item.cities)
|
||||
? item.cities.filter((city): city is string => typeof city === 'string')
|
||||
: null;
|
||||
if (id === null || name === null || color === null || level === null || power === null || !cities) {
|
||||
continue;
|
||||
}
|
||||
output.push({ id, name, color, level, power, cities });
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
const buildNationSnapshot = async (ctx: GameApiContext) => {
|
||||
const [nationRows, cityRows, generalRows] = await Promise.all([
|
||||
ctx.db.nation.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
color: true,
|
||||
level: true,
|
||||
gold: true,
|
||||
rice: true,
|
||||
tech: true,
|
||||
},
|
||||
orderBy: { id: 'asc' },
|
||||
}),
|
||||
ctx.db.city.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
nationId: true,
|
||||
population: true,
|
||||
agriculture: true,
|
||||
commerce: true,
|
||||
security: true,
|
||||
defence: true,
|
||||
wall: true,
|
||||
populationMax: true,
|
||||
agricultureMax: true,
|
||||
commerceMax: true,
|
||||
securityMax: true,
|
||||
defenceMax: true,
|
||||
wallMax: true,
|
||||
},
|
||||
}),
|
||||
ctx.db.general.findMany({
|
||||
select: {
|
||||
nationId: true,
|
||||
npcState: true,
|
||||
leadership: true,
|
||||
strength: true,
|
||||
intel: true,
|
||||
experience: true,
|
||||
dedication: true,
|
||||
gold: true,
|
||||
rice: true,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const cityStatsByNation = new Map<number, { popSum: number; valueSum: number; maxSum: number }>();
|
||||
const cityNamesByNation = new Map<number, string[]>();
|
||||
|
||||
for (const city of cityRows) {
|
||||
const entry = cityStatsByNation.get(city.nationId) ?? { popSum: 0, valueSum: 0, maxSum: 0 };
|
||||
const valueSum =
|
||||
city.population + city.agriculture + city.commerce + city.security + city.wall + city.defence;
|
||||
const maxSum =
|
||||
city.populationMax +
|
||||
city.agricultureMax +
|
||||
city.commerceMax +
|
||||
city.securityMax +
|
||||
city.wallMax +
|
||||
city.defenceMax;
|
||||
entry.popSum += city.population;
|
||||
entry.valueSum += valueSum;
|
||||
entry.maxSum += maxSum;
|
||||
cityStatsByNation.set(city.nationId, entry);
|
||||
|
||||
const cityNames = cityNamesByNation.get(city.nationId) ?? [];
|
||||
cityNames.push(city.name);
|
||||
cityNamesByNation.set(city.nationId, cityNames);
|
||||
}
|
||||
|
||||
const generalStatsByNation = new Map<number, { goldRice: number; statPower: number; expDed: number }>();
|
||||
for (const general of generalRows) {
|
||||
const entry = generalStatsByNation.get(general.nationId) ?? { goldRice: 0, statPower: 0, expDed: 0 };
|
||||
entry.goldRice += general.gold + general.rice;
|
||||
const leadership = general.leadership;
|
||||
const strength = general.strength;
|
||||
const intel = general.intel;
|
||||
const npcMultiplier = general.npcState < 2 ? 1.2 : 1;
|
||||
const leaderCore = leadership >= 40 ? leadership : 0;
|
||||
entry.statPower += npcMultiplier * leaderCore * 2 + (Math.sqrt(intel * strength) * 2 + leadership / 2) / 2;
|
||||
entry.expDed += general.experience + general.dedication;
|
||||
generalStatsByNation.set(general.nationId, entry);
|
||||
}
|
||||
|
||||
return nationRows.map<YearbookNation>((nation) => {
|
||||
const generalStats = generalStatsByNation.get(nation.id) ?? { goldRice: 0, statPower: 0, expDed: 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);
|
||||
|
||||
return {
|
||||
id: nation.id,
|
||||
name: nation.name,
|
||||
color: nation.color,
|
||||
level: nation.level,
|
||||
power,
|
||||
cities: cityNamesByNation.get(nation.id) ?? [],
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const buildLogs = async (ctx: GameApiContext, year: number, month: number) => {
|
||||
const [historyLogs, actionLogs] = await Promise.all([
|
||||
ctx.db.logEntry.findMany({
|
||||
where: {
|
||||
scope: LogScope.SYSTEM,
|
||||
category: LogCategory.HISTORY,
|
||||
year,
|
||||
month,
|
||||
},
|
||||
orderBy: { id: 'desc' },
|
||||
}),
|
||||
ctx.db.logEntry.findMany({
|
||||
where: {
|
||||
scope: LogScope.SYSTEM,
|
||||
category: LogCategory.ACTION,
|
||||
year,
|
||||
month,
|
||||
},
|
||||
orderBy: { id: 'desc' },
|
||||
}),
|
||||
]);
|
||||
|
||||
const globalHistory = historyLogs.map((entry) => entry.text);
|
||||
const globalAction = actionLogs.map((entry) => entry.text);
|
||||
|
||||
return {
|
||||
globalHistory: globalHistory.length ? globalHistory : [`<C>●</>${month}월: 기록 없음`],
|
||||
globalAction: globalAction.length ? globalAction : [`<C>●</>${month}월: 기록 없음`],
|
||||
};
|
||||
};
|
||||
|
||||
export const yearbookRouter = router({
|
||||
getRange: procedure.query(async ({ ctx }) => {
|
||||
const worldState = await ctx.db.worldState.findFirst();
|
||||
if (!worldState) {
|
||||
throw new TRPCError({ code: 'PRECONDITION_FAILED', message: 'World state is not initialized.' });
|
||||
}
|
||||
|
||||
const firstRow = await ctx.db.yearbookHistory.findFirst({
|
||||
where: { profileName: ctx.profile.name },
|
||||
select: { year: true, month: true },
|
||||
orderBy: [{ year: 'asc' }, { month: 'asc' }],
|
||||
});
|
||||
|
||||
const lastRow = await ctx.db.yearbookHistory.findFirst({
|
||||
where: { profileName: ctx.profile.name },
|
||||
select: { year: true, month: true },
|
||||
orderBy: [{ year: 'desc' }, { month: 'desc' }],
|
||||
});
|
||||
|
||||
const currentYearMonth = joinYearMonth(worldState.currentYear, worldState.currentMonth);
|
||||
const fallbackYearMonth = currentYearMonth - 1;
|
||||
const firstYearMonth = firstRow ? joinYearMonth(firstRow.year, firstRow.month) : fallbackYearMonth;
|
||||
const lastYearMonth = lastRow ? joinYearMonth(lastRow.year, lastRow.month) : fallbackYearMonth;
|
||||
|
||||
return {
|
||||
firstYearMonth,
|
||||
lastYearMonth,
|
||||
currentYearMonth,
|
||||
};
|
||||
}),
|
||||
getHistory: procedure
|
||||
.input(
|
||||
z.object({
|
||||
year: z.number().int(),
|
||||
month: z.number().int().min(1).max(12),
|
||||
hash: z.string().optional(),
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const worldState = await ctx.db.worldState.findFirst();
|
||||
if (!worldState) {
|
||||
throw new TRPCError({ code: 'PRECONDITION_FAILED', message: 'World state is not initialized.' });
|
||||
}
|
||||
|
||||
const isCurrent =
|
||||
worldState.currentYear === input.year && worldState.currentMonth === input.month;
|
||||
|
||||
const { globalHistory, globalAction } = await buildLogs(ctx, input.year, input.month);
|
||||
|
||||
if (isCurrent) {
|
||||
const map = await loadPublicMap(ctx, false);
|
||||
if (!map) {
|
||||
throw new TRPCError({ code: 'PRECONDITION_FAILED', message: 'World map is not available.' });
|
||||
}
|
||||
const nations = await buildNationSnapshot(ctx);
|
||||
const data = {
|
||||
year: input.year,
|
||||
month: input.month,
|
||||
map,
|
||||
nations,
|
||||
globalHistory,
|
||||
globalAction,
|
||||
};
|
||||
const hash = computeHash(data);
|
||||
if (input.hash && input.hash === hash) {
|
||||
return { notModified: true, hash };
|
||||
}
|
||||
return { notModified: false, hash, data };
|
||||
}
|
||||
|
||||
const row = await ctx.db.yearbookHistory.findFirst({
|
||||
where: {
|
||||
profileName: ctx.profile.name,
|
||||
year: input.year,
|
||||
month: input.month,
|
||||
},
|
||||
});
|
||||
if (!row) {
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: '연감 데이터를 찾을 수 없습니다.' });
|
||||
}
|
||||
|
||||
const map = asRecord(row.map) as BaseMapResult;
|
||||
const nations = parseYearbookNations(row.nations);
|
||||
const data = {
|
||||
year: input.year,
|
||||
month: input.month,
|
||||
map,
|
||||
nations,
|
||||
globalHistory,
|
||||
globalAction,
|
||||
};
|
||||
const hash = computeHash({ map, nations, globalHistory, globalAction });
|
||||
if (input.hash && input.hash === hash) {
|
||||
return { notModified: true, hash };
|
||||
}
|
||||
return { notModified: false, hash, data };
|
||||
}),
|
||||
});
|
||||
@@ -31,6 +31,7 @@ import { createAuctionFinalizer } from '../auction/finalizer.js';
|
||||
import { createAuctionBidder } from '../auction/bidder.js';
|
||||
import { createTournamentRewardFinalizer } from '../tournament/finalizer.js';
|
||||
import { createTournamentAutoStartHandler } from './tournamentAutoStart.js';
|
||||
import { createYearbookHandler } from './yearbookHandler.js';
|
||||
|
||||
export interface TurnDaemonRuntimeOptions {
|
||||
profile: string;
|
||||
@@ -133,11 +134,17 @@ export const createTurnDaemonRuntime = async (options: TurnDaemonRuntimeOptions)
|
||||
getWorldConfig: () => snapshot.worldConfig ?? null,
|
||||
getTickSeconds: () => worldRef?.getState().tickSeconds ?? null,
|
||||
});
|
||||
const yearbookHandler = createYearbookHandler({
|
||||
databaseUrl: options.databaseUrl,
|
||||
profileName: options.profileName ?? options.profile,
|
||||
getWorld: () => worldRef,
|
||||
});
|
||||
const calendarHandler = composeCalendarHandlers(
|
||||
options.calendarHandler ?? unification?.handler,
|
||||
incomeHandler,
|
||||
frontStateHandler,
|
||||
tournamentAutoStartHandler
|
||||
tournamentAutoStartHandler,
|
||||
yearbookHandler.handler
|
||||
);
|
||||
const worldOptions: InMemoryTurnWorldOptions = {
|
||||
schedule,
|
||||
@@ -319,6 +326,7 @@ export const createTurnDaemonRuntime = async (options: TurnDaemonRuntimeOptions)
|
||||
if (unification) {
|
||||
await unification.close();
|
||||
}
|
||||
await yearbookHandler.close();
|
||||
if (redisConnector) {
|
||||
await redisConnector.disconnect();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
import { createHash } from 'crypto';
|
||||
import { asNumber, asRecord } from '@sammo-ts/common';
|
||||
import { createGamePostgresConnector } from '@sammo-ts/infra';
|
||||
|
||||
import type { TurnCalendarHandler } from './inMemoryWorld.js';
|
||||
import type { InMemoryTurnWorld } from './inMemoryWorld.js';
|
||||
|
||||
const MAP_VERSION = 0 as const;
|
||||
|
||||
type MapCityCompact = [number, number, number, number, number, number];
|
||||
type MapNationCompact = [number, string, string, number];
|
||||
|
||||
type YearbookMap = {
|
||||
result: true;
|
||||
version: number;
|
||||
startYear: number;
|
||||
year: number;
|
||||
month: number;
|
||||
cityList: MapCityCompact[];
|
||||
nationList: MapNationCompact[];
|
||||
};
|
||||
|
||||
type YearbookNation = {
|
||||
id: number;
|
||||
name: string;
|
||||
color: string;
|
||||
level: number;
|
||||
power: number;
|
||||
cities: string[];
|
||||
};
|
||||
|
||||
const readState = (meta: Record<string, unknown>): number => {
|
||||
const raw = meta.state;
|
||||
if (typeof raw === 'number' && Number.isFinite(raw)) {
|
||||
return Math.floor(raw);
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
const resolveStartYear = (meta: Record<string, unknown>): number => {
|
||||
const scenarioMeta = asRecord(meta.scenarioMeta);
|
||||
const startYear = scenarioMeta.startYear;
|
||||
if (typeof startYear === 'number' && Number.isFinite(startYear)) {
|
||||
return startYear;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
const buildMapSnapshot = (world: InMemoryTurnWorld, year: number, month: number): YearbookMap => {
|
||||
const state = world.getState();
|
||||
const cityList: MapCityCompact[] = world.listCities().map((city) => {
|
||||
const meta = asRecord(city.meta);
|
||||
const stateValue = city.state ?? readState(meta);
|
||||
const region = asNumber(meta.region, 0);
|
||||
const supplyFlag = city.supplyState > 0 ? 1 : 0;
|
||||
return [city.id, city.level, stateValue, city.nationId, region, supplyFlag];
|
||||
});
|
||||
|
||||
const nationList: MapNationCompact[] = world.listNations().map((nation) => [
|
||||
nation.id,
|
||||
nation.name,
|
||||
nation.color,
|
||||
nation.capitalCityId ?? 0,
|
||||
]);
|
||||
|
||||
return {
|
||||
result: true,
|
||||
version: MAP_VERSION,
|
||||
startYear: resolveStartYear(asRecord(state.meta)),
|
||||
year,
|
||||
month,
|
||||
cityList,
|
||||
nationList,
|
||||
};
|
||||
};
|
||||
|
||||
const buildNationSnapshot = (world: InMemoryTurnWorld): YearbookNation[] => {
|
||||
const cities = world.listCities();
|
||||
const generals = world.listGenerals();
|
||||
const nations = world.listNations();
|
||||
|
||||
const cityStatsByNation = new Map<number, { popSum: number; valueSum: number; maxSum: number }>();
|
||||
const cityNamesByNation = new Map<number, string[]>();
|
||||
|
||||
for (const city of cities) {
|
||||
const entry = cityStatsByNation.get(city.nationId) ?? { popSum: 0, valueSum: 0, maxSum: 0 };
|
||||
const valueSum =
|
||||
city.population + city.agriculture + city.commerce + city.security + city.wall + city.defence;
|
||||
const maxSum =
|
||||
city.populationMax +
|
||||
city.agricultureMax +
|
||||
city.commerceMax +
|
||||
city.securityMax +
|
||||
city.wallMax +
|
||||
city.defenceMax;
|
||||
entry.popSum += city.population;
|
||||
entry.valueSum += valueSum;
|
||||
entry.maxSum += maxSum;
|
||||
cityStatsByNation.set(city.nationId, entry);
|
||||
|
||||
const cityNames = cityNamesByNation.get(city.nationId) ?? [];
|
||||
cityNames.push(city.name);
|
||||
cityNamesByNation.set(city.nationId, cityNames);
|
||||
}
|
||||
|
||||
const generalStatsByNation = new Map<number, { goldRice: number; statPower: number; expDed: number }>();
|
||||
for (const general of generals) {
|
||||
const entry = generalStatsByNation.get(general.nationId) ?? { goldRice: 0, statPower: 0, expDed: 0 };
|
||||
entry.goldRice += general.gold + general.rice;
|
||||
const leadership = general.stats.leadership;
|
||||
const strength = general.stats.strength;
|
||||
const intel = general.stats.intelligence;
|
||||
const npcMultiplier = general.npcState < 2 ? 1.2 : 1;
|
||||
const leaderCore = leadership >= 40 ? leadership : 0;
|
||||
entry.statPower += npcMultiplier * leaderCore * 2 + (Math.sqrt(intel * strength) * 2 + leadership / 2) / 2;
|
||||
entry.expDed += general.experience + general.dedication;
|
||||
generalStatsByNation.set(general.nationId, entry);
|
||||
}
|
||||
|
||||
return nations.map((nation) => {
|
||||
const generalStats = generalStatsByNation.get(nation.id) ?? { goldRice: 0, statPower: 0, expDed: 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 = asNumber(asRecord(nation.meta).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);
|
||||
|
||||
return {
|
||||
id: nation.id,
|
||||
name: nation.name,
|
||||
color: nation.color,
|
||||
level: nation.level,
|
||||
power,
|
||||
cities: cityNamesByNation.get(nation.id) ?? [],
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const buildHash = (map: YearbookMap, nations: YearbookNation[]): string =>
|
||||
createHash('sha256').update(JSON.stringify({ map, nations })).digest('hex');
|
||||
|
||||
export const createYearbookHandler = (options: {
|
||||
databaseUrl: string;
|
||||
profileName: string;
|
||||
getWorld: () => InMemoryTurnWorld | null;
|
||||
}): { handler: TurnCalendarHandler; close: () => Promise<void> } => {
|
||||
const connector = createGamePostgresConnector({ url: options.databaseUrl });
|
||||
const ready = connector.connect();
|
||||
|
||||
const handler: TurnCalendarHandler = {
|
||||
onMonthChanged: (context) => {
|
||||
const world = options.getWorld();
|
||||
if (!world) {
|
||||
return;
|
||||
}
|
||||
void (async () => {
|
||||
await ready;
|
||||
const map = buildMapSnapshot(world, context.previousYear, context.previousMonth);
|
||||
const nations = buildNationSnapshot(world);
|
||||
const hash = buildHash(map, nations);
|
||||
|
||||
await connector.prisma.yearbookHistory.upsert({
|
||||
where: {
|
||||
profileName_year_month: {
|
||||
profileName: options.profileName,
|
||||
year: context.previousYear,
|
||||
month: context.previousMonth,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
map,
|
||||
nations,
|
||||
hash,
|
||||
},
|
||||
create: {
|
||||
profileName: options.profileName,
|
||||
year: context.previousYear,
|
||||
month: context.previousMonth,
|
||||
map,
|
||||
nations,
|
||||
hash,
|
||||
},
|
||||
});
|
||||
})();
|
||||
},
|
||||
};
|
||||
|
||||
const close = async () => {
|
||||
await connector.disconnect();
|
||||
};
|
||||
|
||||
return { handler, close };
|
||||
};
|
||||
@@ -213,6 +213,21 @@ model DiplomacyLetter {
|
||||
@@map("diplomacy_letter")
|
||||
}
|
||||
|
||||
model YearbookHistory {
|
||||
id Int @id @default(autoincrement())
|
||||
profileName String @map("profile_name")
|
||||
year Int
|
||||
month Int
|
||||
map Json
|
||||
nations Json
|
||||
hash String @default("")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
@@unique([profileName, year, month])
|
||||
@@index([profileName, year, month])
|
||||
@@map("yearbook_history")
|
||||
}
|
||||
|
||||
model Event {
|
||||
id Int @id @default(autoincrement())
|
||||
targetCode String @map("target_code")
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
CREATE TABLE "yearbook_history" (
|
||||
"id" SERIAL PRIMARY KEY,
|
||||
"profile_name" TEXT NOT NULL,
|
||||
"year" INTEGER NOT NULL,
|
||||
"month" INTEGER NOT NULL,
|
||||
"map" JSONB NOT NULL,
|
||||
"nations" JSONB NOT NULL,
|
||||
"hash" TEXT NOT NULL DEFAULT '',
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "yearbook_history_profile_year_month_key" ON "yearbook_history"("profile_name", "year", "month");
|
||||
CREATE INDEX "yearbook_history_profile_year_month_idx" ON "yearbook_history"("profile_name", "year", "month");
|
||||
@@ -10,6 +10,7 @@ export interface DatabaseClient {
|
||||
nation: GamePrisma.NationDelegate;
|
||||
diplomacy: GamePrisma.DiplomacyDelegate;
|
||||
diplomacyLetter: GamePrisma.DiplomacyLetterDelegate;
|
||||
yearbookHistory: GamePrisma.YearbookHistoryDelegate;
|
||||
generalTurn: GamePrisma.GeneralTurnDelegate;
|
||||
nationTurn: GamePrisma.NationTurnDelegate;
|
||||
troop: GamePrisma.TroopDelegate;
|
||||
|
||||
Reference in New Issue
Block a user