server: database schema, migrations and seed

Define Prisma schema (Player, City, Good, InventoryItem, Transaction,
Mission, WorldEvent), initial migration SQL, and seed script to
populate cities and goods with base game data.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 23:41:27 +02:00
parent 34708340f1
commit 8a91a298a7
4 changed files with 419 additions and 0 deletions
@@ -0,0 +1,178 @@
-- CreateEnum
CREATE TYPE "MissionStatus" AS ENUM ('STARTED', 'COMPLETED', 'FAILED');
-- CreateEnum
CREATE TYPE "MissionType" AS ENUM ('DELIVERY', 'THEFT', 'SMUGGLING', 'INTEL');
-- CreateEnum
CREATE TYPE "EventType" AS ENUM ('MARKET_BOOM', 'POLICE_RAID', 'SHORTAGE', 'BLACKOUT');
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"passwordHash" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Player" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"displayName" TEXT NOT NULL,
"money" INTEGER NOT NULL DEFAULT 1000,
"reputation" INTEGER NOT NULL DEFAULT 0,
"level" INTEGER NOT NULL DEFAULT 1,
"experience" INTEGER NOT NULL DEFAULT 0,
"currentCityId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Player_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "City" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"riskLevel" INTEGER NOT NULL,
"policePressure" DOUBLE PRECISION NOT NULL DEFAULT 1.0,
"economyModifier" DOUBLE PRECISION NOT NULL DEFAULT 1.0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "City_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Item" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"basePrice" INTEGER NOT NULL,
"rarity" INTEGER NOT NULL,
"illegalLevel" INTEGER NOT NULL,
"weight" INTEGER NOT NULL DEFAULT 1,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Item_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "InventoryItem" (
"playerId" TEXT NOT NULL,
"itemId" TEXT NOT NULL,
"quantity" INTEGER NOT NULL,
CONSTRAINT "InventoryItem_pkey" PRIMARY KEY ("playerId","itemId")
);
-- CreateTable
CREATE TABLE "MarketPrice" (
"cityId" TEXT NOT NULL,
"itemId" TEXT NOT NULL,
"buyPrice" INTEGER NOT NULL,
"sellPrice" INTEGER NOT NULL,
"demand" DOUBLE PRECISION NOT NULL DEFAULT 1.0,
"supply" DOUBLE PRECISION NOT NULL DEFAULT 1.0,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "MarketPrice_pkey" PRIMARY KEY ("cityId","itemId")
);
-- CreateTable
CREATE TABLE "Mission" (
"id" TEXT NOT NULL,
"cityId" TEXT NOT NULL,
"title" TEXT NOT NULL,
"type" "MissionType" NOT NULL,
"difficulty" INTEGER NOT NULL,
"durationSeconds" INTEGER NOT NULL,
"rewardMoney" INTEGER NOT NULL,
"rewardXp" INTEGER NOT NULL,
"risk" DOUBLE PRECISION NOT NULL,
"requiredItemId" TEXT,
"requiredItemQuantity" INTEGER,
"expiresAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Mission_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PlayerMission" (
"id" TEXT NOT NULL,
"playerId" TEXT NOT NULL,
"missionId" TEXT NOT NULL,
"status" "MissionStatus" NOT NULL DEFAULT 'STARTED',
"startedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"completesAt" TIMESTAMP(3) NOT NULL,
"resolvedAt" TIMESTAMP(3),
CONSTRAINT "PlayerMission_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "WorldEvent" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT NOT NULL,
"type" "EventType" NOT NULL,
"cityId" TEXT,
"priceModifier" DOUBLE PRECISION NOT NULL DEFAULT 1.0,
"policeModifier" DOUBLE PRECISION NOT NULL DEFAULT 1.0,
"startsAt" TIMESTAMP(3) NOT NULL,
"endsAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "WorldEvent_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "Player_userId_key" ON "Player"("userId");
-- CreateIndex
CREATE UNIQUE INDEX "Player_displayName_key" ON "Player"("displayName");
-- CreateIndex
CREATE UNIQUE INDEX "City_name_key" ON "City"("name");
-- CreateIndex
CREATE UNIQUE INDEX "Item_name_key" ON "Item"("name");
-- AddForeignKey
ALTER TABLE "Player" ADD CONSTRAINT "Player_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Player" ADD CONSTRAINT "Player_currentCityId_fkey" FOREIGN KEY ("currentCityId") REFERENCES "City"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "InventoryItem" ADD CONSTRAINT "InventoryItem_playerId_fkey" FOREIGN KEY ("playerId") REFERENCES "Player"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "InventoryItem" ADD CONSTRAINT "InventoryItem_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "Item"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "MarketPrice" ADD CONSTRAINT "MarketPrice_cityId_fkey" FOREIGN KEY ("cityId") REFERENCES "City"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "MarketPrice" ADD CONSTRAINT "MarketPrice_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "Item"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Mission" ADD CONSTRAINT "Mission_cityId_fkey" FOREIGN KEY ("cityId") REFERENCES "City"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Mission" ADD CONSTRAINT "Mission_requiredItemId_fkey" FOREIGN KEY ("requiredItemId") REFERENCES "Item"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PlayerMission" ADD CONSTRAINT "PlayerMission_playerId_fkey" FOREIGN KEY ("playerId") REFERENCES "Player"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PlayerMission" ADD CONSTRAINT "PlayerMission_missionId_fkey" FOREIGN KEY ("missionId") REFERENCES "Mission"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "WorldEvent" ADD CONSTRAINT "WorldEvent_cityId_fkey" FOREIGN KEY ("cityId") REFERENCES "City"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"
+157
View File
@@ -0,0 +1,157 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum MissionStatus {
STARTED
COMPLETED
FAILED
}
enum MissionType {
DELIVERY
THEFT
SMUGGLING
INTEL
}
enum EventType {
MARKET_BOOM
POLICE_RAID
SHORTAGE
BLACKOUT
}
model User {
id String @id @default(uuid())
email String @unique
passwordHash String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
player Player?
}
model Player {
id String @id @default(uuid())
userId String @unique
displayName String @unique
money Int @default(1000)
reputation Int @default(0)
level Int @default(1)
experience Int @default(0)
currentCityId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
currentCity City @relation(fields: [currentCityId], references: [id])
inventory InventoryItem[]
missions PlayerMission[]
}
model City {
id String @id @default(uuid())
name String @unique
riskLevel Int
policePressure Float @default(1.0)
economyModifier Float @default(1.0)
createdAt DateTime @default(now())
players Player[]
prices MarketPrice[]
missions Mission[]
events WorldEvent[]
}
model Item {
id String @id @default(uuid())
name String @unique
basePrice Int
rarity Int
illegalLevel Int
weight Int @default(1)
createdAt DateTime @default(now())
inventory InventoryItem[]
prices MarketPrice[]
missions Mission[]
}
model InventoryItem {
playerId String
itemId String
quantity Int
player Player @relation(fields: [playerId], references: [id])
item Item @relation(fields: [itemId], references: [id])
@@id([playerId, itemId])
}
model MarketPrice {
cityId String
itemId String
buyPrice Int
sellPrice Int
demand Float @default(1.0)
supply Float @default(1.0)
updatedAt DateTime @updatedAt
city City @relation(fields: [cityId], references: [id])
item Item @relation(fields: [itemId], references: [id])
@@id([cityId, itemId])
}
model Mission {
id String @id @default(uuid())
cityId String
title String
type MissionType
difficulty Int
durationSeconds Int
rewardMoney Int
rewardXp Int
risk Float
requiredItemId String?
requiredItemQuantity Int?
expiresAt DateTime
createdAt DateTime @default(now())
city City @relation(fields: [cityId], references: [id])
requiredItem Item? @relation(fields: [requiredItemId], references: [id])
playerMissions PlayerMission[]
}
model PlayerMission {
id String @id @default(uuid())
playerId String
missionId String
status MissionStatus @default(STARTED)
startedAt DateTime @default(now())
completesAt DateTime
resolvedAt DateTime?
player Player @relation(fields: [playerId], references: [id])
mission Mission @relation(fields: [missionId], references: [id])
}
model WorldEvent {
id String @id @default(uuid())
title String
description String
type EventType
cityId String?
priceModifier Float @default(1.0)
policeModifier Float @default(1.0)
startsAt DateTime
endsAt DateTime
createdAt DateTime @default(now())
city City? @relation(fields: [cityId], references: [id])
}
+81
View File
@@ -0,0 +1,81 @@
import { PrismaClient } from '@prisma/client';
import { computePrices } from '../src/modules/market/pricing.js';
import { randomFloat } from '../src/shared/validators.js';
const prisma = new PrismaClient();
/** Città iniziali (blueprint §19): economia e rischio differenziati. */
const CITIES = [
{ name: 'Milano', riskLevel: 3, policePressure: 1.1, economyModifier: 1.25 },
{ name: 'Napoli', riskLevel: 4, policePressure: 1.3, economyModifier: 1.0 },
{ name: 'Palermo', riskLevel: 4, policePressure: 1.25, economyModifier: 0.85 },
{ name: 'Torino', riskLevel: 2, policePressure: 0.9, economyModifier: 1.0 },
{ name: 'Bari', riskLevel: 3, policePressure: 1.0, economyModifier: 0.85 },
];
/** Item commerciabili iniziali (blueprint §19). */
const ITEMS = [
{ name: 'Gioielli', basePrice: 500, rarity: 2, illegalLevel: 2, weight: 1 },
{ name: 'Componenti elettronici', basePrice: 350, rarity: 1, illegalLevel: 1, weight: 2 },
{ name: 'Documenti falsi', basePrice: 700, rarity: 3, illegalLevel: 4, weight: 1 },
{ name: 'Auto rubate', basePrice: 1800, rarity: 4, illegalLevel: 5, weight: 10 },
{ name: 'Informazioni riservate', basePrice: 1200, rarity: 4, illegalLevel: 3, weight: 1 },
{ name: 'Armi leggere', basePrice: 1500, rarity: 3, illegalLevel: 5, weight: 4 },
];
async function main(): Promise<void> {
for (const city of CITIES) {
await prisma.city.upsert({
where: { name: city.name },
create: city,
update: {
riskLevel: city.riskLevel,
policePressure: city.policePressure,
economyModifier: city.economyModifier,
},
});
}
console.log(`Città: ${CITIES.length}`);
for (const item of ITEMS) {
await prisma.item.upsert({
where: { name: item.name },
create: item,
update: {
basePrice: item.basePrice,
rarity: item.rarity,
illegalLevel: item.illegalLevel,
weight: item.weight,
},
});
}
console.log(`Item: ${ITEMS.length}`);
// Listino iniziale per ogni coppia città/item, con domanda e offerta
// leggermente casuali. I prezzi già esistenti non vengono toccati.
const cities = await prisma.city.findMany();
const items = await prisma.item.findMany();
let createdPrices = 0;
for (const city of cities) {
for (const item of items) {
const demand = Math.round(randomFloat(0.8, 1.2) * 100) / 100;
const supply = Math.round(randomFloat(0.8, 1.2) * 100) / 100;
const prices = computePrices(item.basePrice, city.economyModifier, demand, supply, 1);
const result = await prisma.marketPrice.upsert({
where: { cityId_itemId: { cityId: city.id, itemId: item.id } },
create: { cityId: city.id, itemId: item.id, demand, supply, ...prices },
update: {},
});
if (result) createdPrices += 1;
}
}
console.log(`Prezzi di mercato: ${createdPrices}`);
console.log('Seed completato.');
}
main()
.catch((err) => {
console.error(err);
process.exit(1);
})
.finally(() => prisma.$disconnect());