diff --git a/server/prisma/migrations/20260609211013_init/migration.sql b/server/prisma/migrations/20260609211013_init/migration.sql new file mode 100644 index 0000000..b3c4116 --- /dev/null +++ b/server/prisma/migrations/20260609211013_init/migration.sql @@ -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; diff --git a/server/prisma/migrations/migration_lock.toml b/server/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/server/prisma/migrations/migration_lock.toml @@ -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" diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma new file mode 100644 index 0000000..48b7afb --- /dev/null +++ b/server/prisma/schema.prisma @@ -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]) +} diff --git a/server/prisma/seed.ts b/server/prisma/seed.ts new file mode 100644 index 0000000..4b90ec9 --- /dev/null +++ b/server/prisma/seed.ts @@ -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 { + 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());