diff --git a/app/game-api/src/router.ts b/app/game-api/src/router.ts index 750f033..c0b8956 100644 --- a/app/game-api/src/router.ts +++ b/app/game-api/src/router.ts @@ -21,6 +21,7 @@ import { boardRouter } from './router/board/index.js'; import { diplomacyRouter } from './router/diplomacy/index.js'; import { yearbookRouter } from './router/yearbook/index.js'; import { rankingRouter } from './router/ranking/index.js'; +import { dynastyRouter } from './router/dynasty/index.js'; export const appRouter = router({ health: healthRouter, @@ -44,6 +45,7 @@ export const appRouter = router({ diplomacy: diplomacyRouter, yearbook: yearbookRouter, ranking: rankingRouter, + dynasty: dynastyRouter, }); export type AppRouter = typeof appRouter; diff --git a/app/game-api/src/router/dynasty/index.ts b/app/game-api/src/router/dynasty/index.ts new file mode 100644 index 0000000..a6df100 --- /dev/null +++ b/app/game-api/src/router/dynasty/index.ts @@ -0,0 +1,139 @@ +import { TRPCError } from '@trpc/server'; +import { z } from 'zod'; + +import { asRecord } from '@sammo-ts/common'; +import { router, procedure } from '../../trpc.js'; + +const zDynastyDetailInput = z.object({ + emperorId: z.number().int().positive(), +}); + +const parseNumberArray = (value: unknown): number[] => + Array.isArray(value) ? value.filter((item): item is number => typeof item === 'number') : []; + +export const dynastyRouter = router({ + getList: procedure.query(async ({ ctx }) => { + const rows = await ctx.db.emperor.findMany({ + orderBy: { id: 'desc' }, + }); + + return { + entries: rows.map((row) => ({ + id: row.id, + serverId: row.serverId ?? '', + phase: row.phase ?? '', + name: row.name ?? '', + year: row.year ?? 0, + month: row.month ?? 0, + color: row.color ?? '#000000', + type: row.type ?? '', + power: row.power ?? 0, + gennum: row.gennum ?? 0, + citynum: row.citynum ?? 0, + })), + }; + }), + getDetail: procedure.input(zDynastyDetailInput).query(async ({ ctx, input }) => { + const emperor = await ctx.db.emperor.findFirst({ + where: { id: input.emperorId }, + }); + if (!emperor) { + throw new TRPCError({ code: 'NOT_FOUND', message: '왕조 정보를 찾을 수 없습니다.' }); + } + + const aux = asRecord(emperor.aux); + const winnerNationId = typeof aux.winnerNationId === 'number' ? aux.winnerNationId : null; + + const serverId = emperor.serverId ?? ''; + const oldNationRows = await ctx.db.oldNation.findMany({ + where: { serverId }, + orderBy: { date: 'desc' }, + }); + + const nationEntries = oldNationRows + .map((row) => { + const data = asRecord(row.data); + const nationId = row.nation ?? (typeof data.nation === 'number' ? data.nation : 0); + return { + nation: nationId, + isWinner: winnerNationId !== null && nationId === winnerNationId, + name: typeof data.name === 'string' ? data.name : row.nation === 0 ? '재야' : '미상', + color: typeof data.color === 'string' ? data.color : '#000000', + type: typeof data.type === 'string' ? data.type : '', + level: typeof data.level === 'number' ? data.level : null, + tech: typeof data.tech === 'number' ? data.tech : null, + maxPower: typeof data.maxPower === 'number' ? data.maxPower : null, + maxCrew: typeof data.maxCrew === 'number' ? data.maxCrew : null, + maxCities: Array.isArray(data.maxCities) ? data.maxCities : [], + generals: parseNumberArray(data.generals), + history: Array.isArray(data.history) ? data.history : [], + date: row.date.toISOString(), + }; + }) + .filter((entry) => entry.nation !== 0); + + const generalIds = Array.from(new Set(nationEntries.flatMap((entry) => entry.generals))); + const generalRows = generalIds.length + ? await ctx.db.oldGeneral.findMany({ + where: { + serverId, + generalNo: { in: generalIds }, + }, + select: { generalNo: true, name: true, lastYearMonth: true }, + }) + : []; + const generalMap = new Map(); + for (const row of generalRows) { + generalMap.set(row.generalNo, { name: row.name, lastYearMonth: row.lastYearMonth }); + } + + const nations = nationEntries.map((entry) => ({ + ...entry, + generalsFull: entry.generals.map((id) => ({ + generalNo: id, + name: generalMap.get(id)?.name ?? `#${id}`, + lastYearMonth: generalMap.get(id)?.lastYearMonth ?? null, + })), + })); + + return { + emperor: { + id: emperor.id, + serverId, + winnerNationId, + phase: emperor.phase ?? '', + nationCount: emperor.nationCount ?? '', + nationName: emperor.nationName ?? '', + nationHist: emperor.nationHist ?? '', + genCount: emperor.genCount ?? '', + personalHist: emperor.personalHist ?? '', + specialHist: emperor.specialHist ?? '', + name: emperor.name ?? '', + type: emperor.type ?? '', + color: emperor.color ?? '#000000', + year: emperor.year ?? 0, + month: emperor.month ?? 0, + power: emperor.power ?? 0, + gennum: emperor.gennum ?? 0, + citynum: emperor.citynum ?? 0, + pop: emperor.pop ?? '0', + poprate: emperor.poprate ?? '', + gold: emperor.gold ?? 0, + rice: emperor.rice ?? 0, + l12name: emperor.l12name ?? '', + l11name: emperor.l11name ?? '', + l10name: emperor.l10name ?? '', + l9name: emperor.l9name ?? '', + l8name: emperor.l8name ?? '', + l7name: emperor.l7name ?? '', + l6name: emperor.l6name ?? '', + l5name: emperor.l5name ?? '', + tiger: emperor.tiger ?? '', + eagle: emperor.eagle ?? '', + gen: emperor.gen ?? '', + history: Array.isArray(emperor.history) ? emperor.history : [], + }, + nations, + }; + }), +}); diff --git a/app/game-engine/src/turn/databaseHooks.ts b/app/game-engine/src/turn/databaseHooks.ts index bc095fd..4d3efd9 100644 --- a/app/game-engine/src/turn/databaseHooks.ts +++ b/app/game-engine/src/turn/databaseHooks.ts @@ -12,7 +12,7 @@ import { type TurnEngineTroopUpdateInput, type TurnEngineWorldStateUpdateInput, } from '@sammo-ts/infra'; -import { finalizeLogEntry, type LogEntryDraft } from '@sammo-ts/logic'; +import { finalizeLogEntry, LogCategory, LogScope, type LogEntryDraft } from '@sammo-ts/logic'; import { asRecord, type RankDataType } from '@sammo-ts/common'; import type { TurnDaemonHooks } from '../lifecycle/types.js'; @@ -316,6 +316,7 @@ export const createDatabaseTurnHooks = async ( deletedTroops, deletedGenerals, deletedNations, + deletedNationSnapshots, diplomacy, logs, createdGenerals, @@ -335,6 +336,77 @@ export const createDatabaseTurnHooks = async ( data: worldStateUpdate, }); + const meta = asRecord(state.meta); + const serverId = + typeof meta.serverId === 'string' && meta.serverId.trim() ? meta.serverId.trim() : 'default'; + + if (deletedNationSnapshots.length > 0) { + const nationIds = deletedNationSnapshots.map((snapshot) => snapshot.nation.id); + const historyRows = await prisma.logEntry.findMany({ + where: { + nationId: { in: nationIds }, + scope: LogScope.NATION, + category: LogCategory.HISTORY, + }, + orderBy: { id: 'asc' }, + select: { nationId: true, text: true }, + }); + const historyMap = new Map(); + for (const row of historyRows) { + const bucket = historyMap.get(row.nationId ?? 0) ?? []; + bucket.push(row.text); + historyMap.set(row.nationId ?? 0, bucket); + } + await Promise.all( + deletedNationSnapshots.map((snapshot) => + prisma.oldNation.upsert({ + where: { + serverId_nation: { + serverId, + nation: snapshot.nation.id, + }, + }, + update: { + data: { + nation: snapshot.nation.id, + name: snapshot.nation.name, + color: snapshot.nation.color, + type: snapshot.nation.typeCode, + level: snapshot.nation.level, + gold: snapshot.nation.gold, + rice: snapshot.nation.rice, + power: snapshot.nation.power, + capitalCityId: snapshot.nation.capitalCityId, + generals: snapshot.generalIds, + history: historyMap.get(snapshot.nation.id) ?? [], + meta: snapshot.nation.meta ?? {}, + }, + date: snapshot.removedAt, + }, + create: { + serverId, + nation: snapshot.nation.id, + data: { + nation: snapshot.nation.id, + name: snapshot.nation.name, + color: snapshot.nation.color, + type: snapshot.nation.typeCode, + level: snapshot.nation.level, + gold: snapshot.nation.gold, + rice: snapshot.nation.rice, + power: snapshot.nation.power, + capitalCityId: snapshot.nation.capitalCityId, + generals: snapshot.generalIds, + history: historyMap.get(snapshot.nation.id) ?? [], + meta: snapshot.nation.meta ?? {}, + }, + date: snapshot.removedAt, + }, + }) + ) + ); + } + const createdIds = new Set(createdGenerals.map((general) => general.id)); const createdNationIds = new Set(createdNations.map((nation) => nation.id)); const createdTroopIds = new Set(createdTroops.map((troop) => troop.id)); diff --git a/app/game-engine/src/turn/inMemoryWorld.ts b/app/game-engine/src/turn/inMemoryWorld.ts index 6c914ae..2f1d31b 100644 --- a/app/game-engine/src/turn/inMemoryWorld.ts +++ b/app/game-engine/src/turn/inMemoryWorld.ts @@ -224,6 +224,11 @@ export class InMemoryTurnWorld { private readonly deletedTroopIds = new Set(); private readonly deletedGeneralIds = new Set(); private readonly deletedNationIds = new Set(); + private readonly deletedNationSnapshots: Array<{ + nation: Nation; + generalIds: number[]; + removedAt: Date; + }> = []; private readonly logs: LogEntryDraft[] = []; private readonly scenarioConfig: ScenarioConfig; private checkpoint?: TurnCheckpoint; @@ -697,6 +702,7 @@ export class InMemoryTurnWorld { deletedTroops: number[]; deletedGenerals: number[]; deletedNations: number[]; + deletedNationSnapshots: Array<{ nation: Nation; generalIds: number[]; removedAt: Date }>; diplomacy: TurnDiplomacy[]; logs: LogEntryDraft[]; createdGenerals: TurnGeneral[]; @@ -734,6 +740,7 @@ export class InMemoryTurnWorld { const deletedTroops = Array.from(this.deletedTroopIds); const deletedGenerals = Array.from(this.deletedGeneralIds); const deletedNations = Array.from(this.deletedNationIds); + const deletedNationSnapshots = this.deletedNationSnapshots.splice(0, this.deletedNationSnapshots.length); const logs = this.logs.splice(0, this.logs.length); this.dirtyGeneralIds.clear(); @@ -757,6 +764,7 @@ export class InMemoryTurnWorld { deletedTroops, deletedGenerals, deletedNations, + deletedNationSnapshots, diplomacy, logs, createdGenerals, @@ -784,6 +792,17 @@ export class InMemoryTurnWorld { } for (const nationId of collapsedNationIds) { + const nation = this.nations.get(nationId); + if (nation) { + const generalIds = Array.from(this.generals.values()) + .filter((general) => general.nationId === nationId) + .map((general) => general.id); + this.deletedNationSnapshots.push({ + nation: { ...nation }, + generalIds, + removedAt: new Date(this.state.lastTurnTime.getTime()), + }); + } for (const general of this.generals.values()) { if (general.nationId !== nationId) { continue; diff --git a/app/game-engine/src/turn/unificationHandler.ts b/app/game-engine/src/turn/unificationHandler.ts index cc815d4..b0c3626 100644 --- a/app/game-engine/src/turn/unificationHandler.ts +++ b/app/game-engine/src/turn/unificationHandler.ts @@ -380,6 +380,358 @@ export const createUnificationHandler = (options: { }); }; + const settleDynasty = async (winnerNationId: number): Promise => { + const world = options.getWorld(); + if (!world) { + return; + } + await ready; + const prisma = connector.prisma; + const state = world.getState(); + const meta = asRecord(state.meta); + + const serverId = + typeof meta.serverId === 'string' && meta.serverId.trim() + ? meta.serverId.trim() + : options.profileName; + const serverName = + typeof meta.serverName === 'string' && meta.serverName.trim() + ? meta.serverName.trim() + : options.profileName; + + const [serverCount, nationRows, cityRows, generalRows, rankRows, historyRows, oldNationRows] = + await Promise.all([ + prisma.gameHistory.count(), + prisma.nation.findMany({ + select: { + id: true, + name: true, + color: true, + level: true, + typeCode: true, + gold: true, + rice: true, + meta: true, + capitalCityId: true, + }, + }), + prisma.city.findMany({ + select: { nationId: true, population: true, populationMax: true }, + }), + prisma.general.findMany({ + select: { + id: true, + userId: true, + name: true, + nationId: true, + dedication: true, + officerLevel: true, + picture: true, + imageServer: true, + leadership: true, + strength: true, + intel: true, + experience: true, + gold: true, + rice: true, + crew: true, + crewTypeId: true, + train: true, + atmos: true, + age: true, + startAge: true, + npcState: true, + personalCode: true, + specialCode: true, + special2Code: true, + cityId: true, + troopId: true, + turnTime: true, + meta: true, + }, + }), + prisma.rankData.findMany({ + where: { + nationId: winnerNationId, + type: { in: ['killnum', 'firenum'] }, + }, + select: { generalId: true, type: true, value: true }, + }), + prisma.logEntry.findMany({ + where: { + nationId: winnerNationId, + scope: LogScope.NATION, + category: LogCategory.HISTORY, + }, + orderBy: { id: 'asc' }, + select: { text: true }, + }), + prisma.oldNation.findMany({ + where: { serverId }, + select: { data: true }, + }), + ]); + + const winnerNation = nationRows.find((nation) => nation.id === winnerNationId); + if (!winnerNation) { + return; + } + + const powerMap = new Map(); + for (const nation of world.listNations()) { + powerMap.set(nation.id, nation.power); + } + + const winnerGenerals = generalRows.filter((general) => general.nationId === winnerNationId); + const winnerGeneralIds = winnerGenerals.map((general) => general.id); + const noNationGenerals = generalRows.filter((general) => general.nationId === 0); + + const cityCount = cityRows.filter((city) => city.nationId === winnerNationId).length; + const popSum = cityRows + .filter((city) => city.nationId === winnerNationId) + .reduce((sum, city) => sum + city.population, 0); + const popMaxSum = cityRows + .filter((city) => city.nationId === winnerNationId) + .reduce((sum, city) => sum + city.populationMax, 0); + const popText = `${popSum} / ${popMaxSum}`; + const popRate = popMaxSum > 0 ? `${Math.round((popSum / popMaxSum) * 10000) / 100} %` : '0 %'; + + const officerMap = new Map(); + for (const general of winnerGenerals) { + if (general.officerLevel < 5) { + continue; + } + if (!officerMap.has(general.officerLevel)) { + officerMap.set(general.officerLevel, { name: general.name, picture: general.picture ?? null }); + } + } + + const generalNameMap = new Map(); + for (const general of generalRows) { + generalNameMap.set(general.id, general.name); + } + + const buildTopList = (type: 'killnum' | 'firenum', limit: number): string => { + const rows = rankRows + .filter((row) => row.type === type && row.value > 0) + .sort((a, b) => b.value - a.value) + .slice(0, limit); + return rows + .map((row) => `${generalNameMap.get(row.generalId) ?? '무명'}【${row.value.toLocaleString('ko-KR')}】`) + .join(', '); + }; + + const tiger = buildTopList('killnum', 5); + const eagle = buildTopList('firenum', 7); + + const gen = winnerGenerals + .slice() + .sort((a, b) => b.dedication - a.dedication) + .map((general) => general.name) + .join(', '); + + const nationNames: string[] = []; + const nationTypeCounts = new Map(); + for (const row of oldNationRows) { + const data = asRecord(row.data); + const name = typeof data.name === 'string' ? data.name : ''; + const type = typeof data.type === 'string' ? data.type : ''; + if (name) { + nationNames.push(name); + } + if (type) { + nationTypeCounts.set(type, (nationTypeCounts.get(type) ?? 0) + 1); + } + } + if (!nationNames.includes(winnerNation.name)) { + nationNames.push(winnerNation.name); + } + if (winnerNation.typeCode) { + nationTypeCounts.set(winnerNation.typeCode, (nationTypeCounts.get(winnerNation.typeCode) ?? 0) + 1); + } + const nationHist = Array.from(nationTypeCounts.entries()) + .map(([key, count]) => `${key}(${count})`) + .join(', '); + + const phase = `${serverName}${serverCount}기`; + const nationCount = `${Math.max(1, nationRows.length)} / ${Math.max(1, nationRows.length)}`; + const genCount = `${generalRows.length} / ${generalRows.length}`; + + const history = historyRows.map((row) => row.text); + + await prisma.oldNation.upsert({ + where: { + serverId_nation: { + serverId, + nation: winnerNationId, + }, + }, + update: { + data: { + nation: winnerNationId, + name: winnerNation.name, + color: winnerNation.color, + type: winnerNation.typeCode, + level: winnerNation.level, + gold: winnerNation.gold, + rice: winnerNation.rice, + power: powerMap.get(winnerNationId) ?? 0, + capitalCityId: winnerNation.capitalCityId, + generals: winnerGeneralIds, + history, + meta: winnerNation.meta ?? {}, + }, + date: new Date(), + }, + create: { + serverId, + nation: winnerNationId, + data: { + nation: winnerNationId, + name: winnerNation.name, + color: winnerNation.color, + type: winnerNation.typeCode, + level: winnerNation.level, + gold: winnerNation.gold, + rice: winnerNation.rice, + power: powerMap.get(winnerNationId) ?? 0, + capitalCityId: winnerNation.capitalCityId, + generals: winnerGeneralIds, + history, + meta: winnerNation.meta ?? {}, + }, + }, + }); + + await prisma.oldNation.upsert({ + where: { + serverId_nation: { + serverId, + nation: 0, + }, + }, + update: { + data: { + nation: 0, + name: '재야', + color: '#000000', + type: 'neutral', + level: 0, + gold: 0, + rice: 0, + power: 0, + capitalCityId: null, + generals: noNationGenerals.map((general) => general.id), + history: [], + meta: {}, + }, + date: new Date(), + }, + create: { + serverId, + nation: 0, + data: { + nation: 0, + name: '재야', + color: '#000000', + type: 'neutral', + level: 0, + gold: 0, + rice: 0, + power: 0, + capitalCityId: null, + generals: noNationGenerals.map((general) => general.id), + history: [], + meta: {}, + }, + }, + }); + + const oldGeneralTargets = generalRows.filter( + (general) => general.nationId === 0 || general.nationId === winnerNationId + ); + await Promise.all( + oldGeneralTargets.map((general) => + ((snapshot) => + prisma.oldGeneral.upsert({ + where: { + by_no: { + serverId, + generalNo: general.id, + }, + }, + update: { + owner: general.userId ?? null, + name: general.name, + lastYearMonth: state.currentYear * 100 + state.currentMonth, + turnTime: general.turnTime, + data: snapshot, + }, + create: { + serverId, + generalNo: general.id, + owner: general.userId ?? null, + name: general.name, + lastYearMonth: state.currentYear * 100 + state.currentMonth, + turnTime: general.turnTime, + data: snapshot, + }, + }))( { + ...general, + turnTime: general.turnTime.toISOString(), + }) + ) + ); + + await prisma.emperor.create({ + data: { + serverId, + phase, + nationCount, + nationName: nationNames.join(', '), + nationHist, + genCount, + personalHist: '', + specialHist: '', + name: winnerNation.name, + type: winnerNation.typeCode, + color: winnerNation.color, + year: state.currentYear, + month: state.currentMonth, + power: powerMap.get(winnerNationId) ?? 0, + gennum: winnerGenerals.length, + citynum: cityCount, + pop: popText, + poprate: popRate, + gold: winnerNation.gold, + rice: winnerNation.rice, + l12name: officerMap.get(12)?.name ?? '', + l12pic: officerMap.get(12)?.picture ?? '', + l11name: officerMap.get(11)?.name ?? '', + l11pic: officerMap.get(11)?.picture ?? '', + l10name: officerMap.get(10)?.name ?? '', + l10pic: officerMap.get(10)?.picture ?? '', + l9name: officerMap.get(9)?.name ?? '', + l9pic: officerMap.get(9)?.picture ?? '', + l8name: officerMap.get(8)?.name ?? '', + l8pic: officerMap.get(8)?.picture ?? '', + l7name: officerMap.get(7)?.name ?? '', + l7pic: officerMap.get(7)?.picture ?? '', + l6name: officerMap.get(6)?.name ?? '', + l6pic: officerMap.get(6)?.picture ?? '', + l5name: officerMap.get(5)?.name ?? '', + l5pic: officerMap.get(5)?.picture ?? '', + tiger, + eagle, + gen, + history, + aux: { + winnerNationId, + }, + }, + }); + }; + const handler: TurnCalendarHandler = { onMonthChanged: (context) => { const world = options.getWorld(); @@ -407,6 +759,7 @@ export const createUnificationHandler = (options: { world.pushLog(buildUnificationLog(winner.name)); void settleInheritance(winner.id, context.currentYear, context.currentMonth); void settleHallOfFame(winner.id); + void settleDynasty(winner.id); }, }; diff --git a/app/game-frontend/src/router/index.ts b/app/game-frontend/src/router/index.ts index cbf6af9..cce04df 100644 --- a/app/game-frontend/src/router/index.ts +++ b/app/game-frontend/src/router/index.ts @@ -22,6 +22,8 @@ import ScoutMessageView from '../views/ScoutMessageView.vue'; import DiplomacyView from '../views/DiplomacyView.vue'; import BestGeneralView from '../views/BestGeneralView.vue'; import HallOfFameView from '../views/HallOfFameView.vue'; +import DynastyListView from '../views/DynastyListView.vue'; +import DynastyDetailView from '../views/DynastyDetailView.vue'; import { useSessionStore } from '../stores/session'; const routes = [ @@ -178,6 +180,16 @@ const routes = [ name: 'hall-of-fame', component: HallOfFameView, }, + { + path: '/dynasty', + name: 'dynasty-list', + component: DynastyListView, + }, + { + path: '/dynasty/:id', + name: 'dynasty-detail', + component: DynastyDetailView, + }, { path: '/my-page', name: 'my-page', diff --git a/app/game-frontend/src/views/DynastyDetailView.vue b/app/game-frontend/src/views/DynastyDetailView.vue new file mode 100644 index 0000000..6683d09 --- /dev/null +++ b/app/game-frontend/src/views/DynastyDetailView.vue @@ -0,0 +1,262 @@ + + + diff --git a/app/game-frontend/src/views/DynastyListView.vue b/app/game-frontend/src/views/DynastyListView.vue new file mode 100644 index 0000000..f015ab3 --- /dev/null +++ b/app/game-frontend/src/views/DynastyListView.vue @@ -0,0 +1,86 @@ + + + diff --git a/app/game-frontend/src/views/MainView.vue b/app/game-frontend/src/views/MainView.vue index 54449b9..e1eea92 100644 --- a/app/game-frontend/src/views/MainView.vue +++ b/app/game-frontend/src/views/MainView.vue @@ -100,6 +100,7 @@ watch( 감찰부 명장일람 명예의 전당 + 왕조일람 게시판 전투 시뮬레이터 내 정보 diff --git a/packages/infra/prisma/game.prisma b/packages/infra/prisma/game.prisma index c992a0d..adfb0f5 100644 --- a/packages/infra/prisma/game.prisma +++ b/packages/infra/prisma/game.prisma @@ -193,6 +193,81 @@ model GameHistory { @@map("ng_games") } +model OldNation { + id Int @id @default(autoincrement()) + serverId String @map("server_id") + nation Int @default(0) + data Json @default(dbgenerated("'{}'::jsonb")) + date DateTime @default(now()) + + @@unique([serverId, nation]) + @@index([serverId, nation], name: "by_server") + @@map("ng_old_nations") +} + +model OldGeneral { + id Int @id @default(autoincrement()) + serverId String @map("server_id") + generalNo Int @map("general_no") + owner String? @map("owner") + name String + lastYearMonth Int @map("last_yearmonth") + turnTime DateTime @map("turntime") + data Json @default(dbgenerated("'{}'::jsonb")) + + @@unique([serverId, generalNo], name: "by_no") + @@index([serverId, name], name: "by_name") + @@index([owner, serverId], name: "owner") + @@map("ng_old_generals") +} + +model Emperor { + id Int @id @default(autoincrement()) @map("no") + serverId String? @map("server_id") + phase String? @default("") + nationCount String? @map("nation_count") + nationName String? @map("nation_name") + nationHist String? @map("nation_hist") + genCount String? @map("gen_count") + personalHist String? @map("personal_hist") + specialHist String? @map("special_hist") + name String? @default("") + type String? @default("") + color String? @default("") + year Int? @default(0) + month Int? @default(0) + power Int? @default(0) + gennum Int? @default(0) + citynum Int? @default(0) + pop String? @default("0") + poprate String? @default("") + gold Int? @default(0) + rice Int? @default(0) + l12name String? @default("") + l12pic String? @default("") + l11name String? @default("") + l11pic String? @default("") + l10name String? @default("") + l10pic String? @default("") + l9name String? @default("") + l9pic String? @default("") + l8name String? @default("") + l8pic String? @default("") + l7name String? @default("") + l7pic String? @default("") + l6name String? @default("") + l6pic String? @default("") + l5name String? @default("") + l5pic String? @default("") + tiger String? @default("") + eagle String? @default("") + gen String? @default("") + history Json @default(dbgenerated("'{}'::jsonb")) + aux Json @default(dbgenerated("'{}'::jsonb")) + + @@map("emperior") +} + model Troop { troopLeaderId Int @id @map("troop_leader") nationId Int @map("nation") diff --git a/packages/infra/prisma/migrations/20260129001000_add_dynasty_tables/migration.sql b/packages/infra/prisma/migrations/20260129001000_add_dynasty_tables/migration.sql new file mode 100644 index 0000000..320ed8e --- /dev/null +++ b/packages/infra/prisma/migrations/20260129001000_add_dynasty_tables/migration.sql @@ -0,0 +1,70 @@ +CREATE TABLE IF NOT EXISTS "ng_old_nations" ( + "id" SERIAL PRIMARY KEY, + "server_id" VARCHAR(20) NOT NULL DEFAULT '0', + "nation" INT NOT NULL DEFAULT 0, + "data" JSONB NOT NULL DEFAULT '{}'::jsonb, + "date" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX IF NOT EXISTS "ng_old_nations_server_id_nation" ON "ng_old_nations" ("server_id", "nation"); +CREATE INDEX IF NOT EXISTS "ng_old_nations_by_server" ON "ng_old_nations" ("server_id", "nation"); + +CREATE TABLE IF NOT EXISTS "ng_old_generals" ( + "id" SERIAL PRIMARY KEY, + "server_id" VARCHAR(20) NOT NULL, + "general_no" INT NOT NULL, + "owner" TEXT NULL, + "name" VARCHAR(32) NOT NULL, + "last_yearmonth" INT NOT NULL, + "turntime" TIMESTAMP NOT NULL, + "data" JSONB NOT NULL DEFAULT '{}'::jsonb +); + +CREATE UNIQUE INDEX IF NOT EXISTS "ng_old_generals_by_no" ON "ng_old_generals" ("server_id", "general_no"); +CREATE INDEX IF NOT EXISTS "ng_old_generals_by_name" ON "ng_old_generals" ("server_id", "name"); +CREATE INDEX IF NOT EXISTS "ng_old_generals_owner" ON "ng_old_generals" ("owner", "server_id"); + +CREATE TABLE IF NOT EXISTS "emperior" ( + "no" SERIAL PRIMARY KEY, + "server_id" VARCHAR(20) NULL DEFAULT '', + "phase" VARCHAR(255) NULL DEFAULT '', + "nation_count" VARCHAR(64) NULL DEFAULT '', + "nation_name" TEXT NULL DEFAULT '', + "nation_hist" TEXT NULL DEFAULT '', + "gen_count" VARCHAR(64) NULL DEFAULT '', + "personal_hist" TEXT NULL DEFAULT '', + "special_hist" TEXT NULL DEFAULT '', + "name" VARCHAR(64) NULL DEFAULT '', + "type" VARCHAR(64) NULL DEFAULT '', + "color" VARCHAR(7) NULL DEFAULT '', + "year" INT NULL DEFAULT 0, + "month" INT NULL DEFAULT 0, + "power" INT NULL DEFAULT 0, + "gennum" INT NULL DEFAULT 0, + "citynum" INT NULL DEFAULT 0, + "pop" VARCHAR(255) NULL DEFAULT '0', + "poprate" VARCHAR(255) NULL DEFAULT '', + "gold" INT NULL DEFAULT 0, + "rice" INT NULL DEFAULT 0, + "l12name" VARCHAR(64) NULL DEFAULT '', + "l12pic" VARCHAR(32) NULL DEFAULT '', + "l11name" VARCHAR(64) NULL DEFAULT '', + "l11pic" VARCHAR(32) NULL DEFAULT '', + "l10name" VARCHAR(64) NULL DEFAULT '', + "l10pic" VARCHAR(32) NULL DEFAULT '', + "l9name" VARCHAR(64) NULL DEFAULT '', + "l9pic" VARCHAR(32) NULL DEFAULT '', + "l8name" VARCHAR(64) NULL DEFAULT '', + "l8pic" VARCHAR(32) NULL DEFAULT '', + "l7name" VARCHAR(64) NULL DEFAULT '', + "l7pic" VARCHAR(32) NULL DEFAULT '', + "l6name" VARCHAR(64) NULL DEFAULT '', + "l6pic" VARCHAR(32) NULL DEFAULT '', + "l5name" VARCHAR(64) NULL DEFAULT '', + "l5pic" VARCHAR(32) NULL DEFAULT '', + "tiger" VARCHAR(128) NULL DEFAULT '', + "eagle" VARCHAR(128) NULL DEFAULT '', + "gen" TEXT NULL DEFAULT '', + "history" JSONB NOT NULL DEFAULT '{}'::jsonb, + "aux" JSONB NOT NULL DEFAULT '{}'::jsonb +); diff --git a/packages/infra/src/db.ts b/packages/infra/src/db.ts index 9cb9099..3e55252 100644 --- a/packages/infra/src/db.ts +++ b/packages/infra/src/db.ts @@ -14,6 +14,9 @@ export interface DatabaseClient { rankData: GamePrisma.RankDataDelegate; hallOfFame: GamePrisma.HallOfFameDelegate; gameHistory: GamePrisma.GameHistoryDelegate; + oldNation: GamePrisma.OldNationDelegate; + oldGeneral: GamePrisma.OldGeneralDelegate; + emperor: GamePrisma.EmperorDelegate; generalTurn: GamePrisma.GeneralTurnDelegate; nationTurn: GamePrisma.NationTurnDelegate; troop: GamePrisma.TroopDelegate;