1055 lines
18 KiB
Markdown
1055 lines
18 KiB
Markdown
|
|
# Blueprint Server — Contrabbandieri MMO Mobile
|
|||
|
|
|
|||
|
|
## 1. Obiettivo
|
|||
|
|
|
|||
|
|
Realizzare un backend funzionante per un gioco mobile multiplayer asincrono, persistente e trasferibile tra server Linux tramite Docker.
|
|||
|
|
|
|||
|
|
Il server deve gestire:
|
|||
|
|
|
|||
|
|
- autenticazione utenti;
|
|||
|
|
- profilo giocatore;
|
|||
|
|
- denaro, reputazione e livello;
|
|||
|
|
- inventario;
|
|||
|
|
- mercato dinamico;
|
|||
|
|
- missioni generate dal server;
|
|||
|
|
- città e prezzi locali;
|
|||
|
|
- eventi globali;
|
|||
|
|
- classifiche;
|
|||
|
|
- notifiche realtime opzionali;
|
|||
|
|
- deploy tramite Docker Compose.
|
|||
|
|
|
|||
|
|
Il client mobile non deve mai essere considerato affidabile. Tutte le regole di gioco vivono sul server.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. Stack consigliato
|
|||
|
|
|
|||
|
|
### Backend
|
|||
|
|
|
|||
|
|
- Node.js 22 LTS
|
|||
|
|
- TypeScript
|
|||
|
|
- Fastify
|
|||
|
|
- Prisma ORM
|
|||
|
|
- PostgreSQL
|
|||
|
|
- Redis
|
|||
|
|
- Socket.IO
|
|||
|
|
- Zod
|
|||
|
|
- JWT
|
|||
|
|
- Docker Compose
|
|||
|
|
|
|||
|
|
### Perché questo stack
|
|||
|
|
|
|||
|
|
Fastify è veloce, semplice e adatto a API REST. Prisma semplifica schema, migrazioni e accesso al database. PostgreSQL è affidabile per dati persistenti. Redis serve per cache, classifiche, cooldown e notifiche realtime. Socket.IO consente eventi live verso client mobile.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. Architettura generale
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Mobile Client
|
|||
|
|
|
|
|||
|
|
| HTTPS REST / WebSocket
|
|||
|
|
v
|
|||
|
|
Nginx Reverse Proxy
|
|||
|
|
|
|
|||
|
|
v
|
|||
|
|
Game API Node.js
|
|||
|
|
| |
|
|||
|
|
| v
|
|||
|
|
| Redis
|
|||
|
|
v
|
|||
|
|
PostgreSQL
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. Struttura repository
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
contrabbandieri-server/
|
|||
|
|
├── docker-compose.yml
|
|||
|
|
├── docker-compose.prod.yml
|
|||
|
|
├── .env.example
|
|||
|
|
├── README.md
|
|||
|
|
├── data/
|
|||
|
|
│ ├── postgres/
|
|||
|
|
│ ├── redis/
|
|||
|
|
│ └── backups/
|
|||
|
|
├── package.json
|
|||
|
|
├── tsconfig.json
|
|||
|
|
├── prisma/
|
|||
|
|
│ ├── schema.prisma
|
|||
|
|
│ └── seed.ts
|
|||
|
|
├── src/
|
|||
|
|
│ ├── app.ts
|
|||
|
|
│ ├── server.ts
|
|||
|
|
│ ├── config/
|
|||
|
|
│ │ └── env.ts
|
|||
|
|
│ ├── db/
|
|||
|
|
│ │ ├── prisma.ts
|
|||
|
|
│ │ └── redis.ts
|
|||
|
|
│ ├── modules/
|
|||
|
|
│ │ ├── auth/
|
|||
|
|
│ │ ├── players/
|
|||
|
|
│ │ ├── market/
|
|||
|
|
│ │ ├── inventory/
|
|||
|
|
│ │ ├── missions/
|
|||
|
|
│ │ ├── cities/
|
|||
|
|
│ │ ├── events/
|
|||
|
|
│ │ └── leaderboard/
|
|||
|
|
│ ├── realtime/
|
|||
|
|
│ │ └── socket.ts
|
|||
|
|
│ ├── jobs/
|
|||
|
|
│ │ ├── scheduler.ts
|
|||
|
|
│ │ ├── updateMarketPrices.job.ts
|
|||
|
|
│ │ ├── generateMissions.job.ts
|
|||
|
|
│ │ └── generateWorldEvent.job.ts
|
|||
|
|
│ ├── shared/
|
|||
|
|
│ │ ├── authGuard.ts
|
|||
|
|
│ │ ├── errors.ts
|
|||
|
|
│ │ ├── result.ts
|
|||
|
|
│ │ └── validators.ts
|
|||
|
|
│ └── types/
|
|||
|
|
└── nginx/
|
|||
|
|
└── nginx.conf
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. Modello di dominio MVP
|
|||
|
|
|
|||
|
|
### Player
|
|||
|
|
|
|||
|
|
Rappresenta il giocatore.
|
|||
|
|
|
|||
|
|
Campi principali:
|
|||
|
|
|
|||
|
|
- id
|
|||
|
|
- userId
|
|||
|
|
- displayName
|
|||
|
|
- money
|
|||
|
|
- reputation
|
|||
|
|
- level
|
|||
|
|
- experience
|
|||
|
|
- currentCityId
|
|||
|
|
- createdAt
|
|||
|
|
- updatedAt
|
|||
|
|
|
|||
|
|
### City
|
|||
|
|
|
|||
|
|
Ogni città ha mercato e missioni locali.
|
|||
|
|
|
|||
|
|
Esempi iniziali:
|
|||
|
|
|
|||
|
|
- Milano
|
|||
|
|
- Napoli
|
|||
|
|
- Palermo
|
|||
|
|
|
|||
|
|
Campi:
|
|||
|
|
|
|||
|
|
- id
|
|||
|
|
- name
|
|||
|
|
- riskLevel
|
|||
|
|
- policePressure
|
|||
|
|
- economyModifier
|
|||
|
|
|
|||
|
|
### Item
|
|||
|
|
|
|||
|
|
Merce commerciabile.
|
|||
|
|
|
|||
|
|
Esempi:
|
|||
|
|
|
|||
|
|
- Gioielli
|
|||
|
|
- Componenti elettronici
|
|||
|
|
- Auto rubate
|
|||
|
|
- Informazioni riservate
|
|||
|
|
- Armi leggere
|
|||
|
|
|
|||
|
|
Campi:
|
|||
|
|
|
|||
|
|
- id
|
|||
|
|
- name
|
|||
|
|
- basePrice
|
|||
|
|
- rarity
|
|||
|
|
- illegalLevel
|
|||
|
|
- weight
|
|||
|
|
|
|||
|
|
### InventoryItem
|
|||
|
|
|
|||
|
|
Quantità posseduta da un player.
|
|||
|
|
|
|||
|
|
Campi:
|
|||
|
|
|
|||
|
|
- playerId
|
|||
|
|
- itemId
|
|||
|
|
- quantity
|
|||
|
|
|
|||
|
|
### MarketPrice
|
|||
|
|
|
|||
|
|
Prezzo locale per item e città.
|
|||
|
|
|
|||
|
|
Campi:
|
|||
|
|
|
|||
|
|
- cityId
|
|||
|
|
- itemId
|
|||
|
|
- buyPrice
|
|||
|
|
- sellPrice
|
|||
|
|
- demand
|
|||
|
|
- supply
|
|||
|
|
- updatedAt
|
|||
|
|
|
|||
|
|
### Mission
|
|||
|
|
|
|||
|
|
Missione generata dal server.
|
|||
|
|
|
|||
|
|
Campi:
|
|||
|
|
|
|||
|
|
- id
|
|||
|
|
- cityId
|
|||
|
|
- title
|
|||
|
|
- type
|
|||
|
|
- difficulty
|
|||
|
|
- durationSeconds
|
|||
|
|
- rewardMoney
|
|||
|
|
- rewardXp
|
|||
|
|
- risk
|
|||
|
|
- requiredItemId
|
|||
|
|
- requiredItemQuantity
|
|||
|
|
- expiresAt
|
|||
|
|
|
|||
|
|
### PlayerMission
|
|||
|
|
|
|||
|
|
Missione iniziata da un player.
|
|||
|
|
|
|||
|
|
Campi:
|
|||
|
|
|
|||
|
|
- id
|
|||
|
|
- playerId
|
|||
|
|
- missionId
|
|||
|
|
- status
|
|||
|
|
- startedAt
|
|||
|
|
- completesAt
|
|||
|
|
- resolvedAt
|
|||
|
|
|
|||
|
|
### WorldEvent
|
|||
|
|
|
|||
|
|
Evento globale o locale che altera economia/rischio.
|
|||
|
|
|
|||
|
|
Campi:
|
|||
|
|
|
|||
|
|
- id
|
|||
|
|
- title
|
|||
|
|
- description
|
|||
|
|
- type
|
|||
|
|
- cityId nullable
|
|||
|
|
- priceModifier
|
|||
|
|
- policeModifier
|
|||
|
|
- startsAt
|
|||
|
|
- endsAt
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 6. Schema Prisma MVP
|
|||
|
|
|
|||
|
|
```prisma
|
|||
|
|
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])
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 7. API REST MVP
|
|||
|
|
|
|||
|
|
Base path:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
/api/v1
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Auth
|
|||
|
|
|
|||
|
|
```http
|
|||
|
|
POST /auth/register
|
|||
|
|
POST /auth/login
|
|||
|
|
GET /auth/me
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### POST /auth/register
|
|||
|
|
|
|||
|
|
Request:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"email": "player@example.com",
|
|||
|
|
"password": "password123",
|
|||
|
|
"displayName": "ShadowFox"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Response:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"accessToken": "jwt",
|
|||
|
|
"player": {
|
|||
|
|
"id": "...",
|
|||
|
|
"displayName": "ShadowFox",
|
|||
|
|
"money": 1000,
|
|||
|
|
"level": 1
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Player
|
|||
|
|
|
|||
|
|
```http
|
|||
|
|
GET /player/me
|
|||
|
|
POST /player/travel
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### POST /player/travel
|
|||
|
|
|
|||
|
|
Request:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"cityId": "..."
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Regola server:
|
|||
|
|
|
|||
|
|
- verifica che la città esista;
|
|||
|
|
- calcola costo viaggio;
|
|||
|
|
- verifica soldi disponibili;
|
|||
|
|
- aggiorna città corrente;
|
|||
|
|
- scala il denaro.
|
|||
|
|
|
|||
|
|
### Cities
|
|||
|
|
|
|||
|
|
```http
|
|||
|
|
GET /cities
|
|||
|
|
GET /cities/:cityId
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Market
|
|||
|
|
|
|||
|
|
```http
|
|||
|
|
GET /market/current
|
|||
|
|
POST /market/buy
|
|||
|
|
POST /market/sell
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### POST /market/buy
|
|||
|
|
|
|||
|
|
Request:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"itemId": "...",
|
|||
|
|
"quantity": 3
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Regole server:
|
|||
|
|
|
|||
|
|
- il player compra nel mercato della città corrente;
|
|||
|
|
- il prezzo lo decide il server;
|
|||
|
|
- il costo è `buyPrice * quantity`;
|
|||
|
|
- se il player non ha soldi, errore;
|
|||
|
|
- aggiorna inventario;
|
|||
|
|
- aggiorna domanda/offerta.
|
|||
|
|
|
|||
|
|
#### POST /market/sell
|
|||
|
|
|
|||
|
|
Request:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"itemId": "...",
|
|||
|
|
"quantity": 2
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Regole server:
|
|||
|
|
|
|||
|
|
- verifica inventario;
|
|||
|
|
- paga `sellPrice * quantity`;
|
|||
|
|
- riduce inventario;
|
|||
|
|
- aggiorna domanda/offerta.
|
|||
|
|
|
|||
|
|
### Inventory
|
|||
|
|
|
|||
|
|
```http
|
|||
|
|
GET /inventory
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Missions
|
|||
|
|
|
|||
|
|
```http
|
|||
|
|
GET /missions/available
|
|||
|
|
POST /missions/:missionId/start
|
|||
|
|
POST /missions/:playerMissionId/claim
|
|||
|
|
GET /missions/active
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Avvio missione
|
|||
|
|
|
|||
|
|
Il server calcola `completesAt = now + durationSeconds`.
|
|||
|
|
|
|||
|
|
#### Claim missione
|
|||
|
|
|
|||
|
|
Regole:
|
|||
|
|
|
|||
|
|
- se `now < completesAt`, errore;
|
|||
|
|
- se già risolta, errore;
|
|||
|
|
- calcola successo in base a rischio, livello e reputazione;
|
|||
|
|
- assegna premio o fallimento;
|
|||
|
|
- aggiorna XP, soldi, reputazione.
|
|||
|
|
|
|||
|
|
### Events
|
|||
|
|
|
|||
|
|
```http
|
|||
|
|
GET /events/current
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Leaderboard
|
|||
|
|
|
|||
|
|
```http
|
|||
|
|
GET /leaderboard/money
|
|||
|
|
GET /leaderboard/reputation
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 8. Realtime Socket.IO
|
|||
|
|
|
|||
|
|
Endpoint:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
/ws
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Eventi server → client:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
market:updated
|
|||
|
|
mission:completed
|
|||
|
|
worldEvent:started
|
|||
|
|
worldEvent:ended
|
|||
|
|
player:notification
|
|||
|
|
leaderboard:updated
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Eventi client → server:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
client:ping
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Nel prototipo, le azioni importanti restano REST. Socket.IO serve solo per notificare.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 9. Regole mercato dinamico
|
|||
|
|
|
|||
|
|
Ogni città ha prezzi indipendenti.
|
|||
|
|
|
|||
|
|
Formula base:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
rawPrice = basePrice * city.economyModifier * demand / max(supply, 0.25) * eventModifier
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Prezzo acquisto:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
buyPrice = round(rawPrice * 1.12)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Prezzo vendita:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
sellPrice = round(rawPrice * 0.88)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Limiti:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
minPrice = basePrice * 0.35
|
|||
|
|
maxPrice = basePrice * 3.50
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Ogni acquisto:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
demand += quantity * 0.02
|
|||
|
|
supply -= quantity * 0.01
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Ogni vendita:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
demand -= quantity * 0.01
|
|||
|
|
supply += quantity * 0.02
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Ogni ciclo scheduler:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
demand = demand * 0.98 + 1.0 * 0.02
|
|||
|
|
supply = supply * 0.98 + 1.0 * 0.02
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Questo fa tornare lentamente il mercato verso l’equilibrio.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 10. Scheduler
|
|||
|
|
|
|||
|
|
Implementare un processo interno nel backend.
|
|||
|
|
|
|||
|
|
### Ogni minuto
|
|||
|
|
|
|||
|
|
- aggiornare prezzi di mercato;
|
|||
|
|
- completare notifiche missioni;
|
|||
|
|
- cancellare missioni scadute;
|
|||
|
|
- generare nuove missioni se sotto soglia.
|
|||
|
|
|
|||
|
|
### Ogni 15 minuti
|
|||
|
|
|
|||
|
|
- generare possibile evento locale;
|
|||
|
|
- aggiornare classifiche Redis.
|
|||
|
|
|
|||
|
|
### Ogni ora
|
|||
|
|
|
|||
|
|
- generare possibile evento globale;
|
|||
|
|
- pulizia dati temporanei.
|
|||
|
|
|
|||
|
|
Librerie consigliate:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
node-cron
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Per versione avanzata:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
BullMQ + Redis
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Per MVP basta `node-cron`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 11. Sicurezza
|
|||
|
|
|
|||
|
|
### Principi
|
|||
|
|
|
|||
|
|
- mai fidarsi del client;
|
|||
|
|
- validare ogni payload con Zod;
|
|||
|
|
- usare JWT con scadenza breve;
|
|||
|
|
- password con Argon2 o bcrypt;
|
|||
|
|
- rate limit su login e register;
|
|||
|
|
- usare HTTPS tramite Nginx;
|
|||
|
|
- non esporre PostgreSQL e Redis su internet;
|
|||
|
|
- loggare azioni economiche importanti.
|
|||
|
|
|
|||
|
|
### Variabili segrete
|
|||
|
|
|
|||
|
|
Nel file `.env`, non committare mai:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
DATABASE_URL
|
|||
|
|
JWT_SECRET
|
|||
|
|
POSTGRES_PASSWORD
|
|||
|
|
REDIS_URL
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 12. File `.env.example`
|
|||
|
|
|
|||
|
|
```env
|
|||
|
|
NODE_ENV=development
|
|||
|
|
PORT=3000
|
|||
|
|
|
|||
|
|
DATABASE_URL=postgresql://gameuser:gamepassword@db:5432/game
|
|||
|
|
REDIS_URL=redis://redis:6379
|
|||
|
|
|
|||
|
|
JWT_SECRET=change_me_in_production
|
|||
|
|
JWT_EXPIRES_IN=7d
|
|||
|
|
|
|||
|
|
POSTGRES_DB=game
|
|||
|
|
POSTGRES_USER=gameuser
|
|||
|
|
POSTGRES_PASSWORD=gamepassword
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 13. Docker Compose sviluppo
|
|||
|
|
|
|||
|
|
### Regola sui dati persistenti
|
|||
|
|
|
|||
|
|
Il server deve salvare tutti i dati persistenti in volumi montati nella stessa directory del progetto, non in volumi Docker anonimi o nominati.
|
|||
|
|
|
|||
|
|
Struttura dati locale:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
contrabbandieri-server/
|
|||
|
|
└── data/
|
|||
|
|
├── postgres/ # database PostgreSQL persistente
|
|||
|
|
├── redis/ # snapshot Redis, se abilitati
|
|||
|
|
└── backups/ # dump manuali o automatici
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Vantaggi:
|
|||
|
|
|
|||
|
|
- puoi copiare l'intera cartella del progetto su un altro server;
|
|||
|
|
- i dati sono visibili e controllabili dal filesystem Linux;
|
|||
|
|
- backup e restore sono più semplici;
|
|||
|
|
- Docker rimane solo il runtime, non il proprietario nascosto dei dati.
|
|||
|
|
|
|||
|
|
Prima del primo avvio:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
mkdir -p data/postgres data/redis data/backups
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Permessi consigliati se PostgreSQL ha problemi di scrittura:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
sudo chown -R 999:999 data/postgres
|
|||
|
|
sudo chown -R 999:999 data/redis
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
services:
|
|||
|
|
api:
|
|||
|
|
build: .
|
|||
|
|
container_name: contrabbandieri-api
|
|||
|
|
restart: unless-stopped
|
|||
|
|
ports:
|
|||
|
|
- "3000:3000"
|
|||
|
|
env_file:
|
|||
|
|
- .env
|
|||
|
|
depends_on:
|
|||
|
|
- db
|
|||
|
|
- redis
|
|||
|
|
volumes:
|
|||
|
|
- .:/app
|
|||
|
|
- /app/node_modules
|
|||
|
|
command: npm run dev
|
|||
|
|
|
|||
|
|
db:
|
|||
|
|
image: postgres:16
|
|||
|
|
container_name: contrabbandieri-db
|
|||
|
|
restart: unless-stopped
|
|||
|
|
environment:
|
|||
|
|
POSTGRES_DB: ${POSTGRES_DB}
|
|||
|
|
POSTGRES_USER: ${POSTGRES_USER}
|
|||
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
|||
|
|
volumes:
|
|||
|
|
- ./data/postgres:/var/lib/postgresql/data
|
|||
|
|
ports:
|
|||
|
|
- "5432:5432"
|
|||
|
|
|
|||
|
|
redis:
|
|||
|
|
image: redis:7
|
|||
|
|
container_name: contrabbandieri-redis
|
|||
|
|
restart: unless-stopped
|
|||
|
|
ports:
|
|||
|
|
- "6379:6379"
|
|||
|
|
volumes:
|
|||
|
|
- ./data/redis:/data
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 13.1 Backup e migrazione server
|
|||
|
|
|
|||
|
|
Poiché i volumi sono bind-mount locali, la migrazione minima consiste in:
|
|||
|
|
|
|||
|
|
1. fermare i container;
|
|||
|
|
2. fare un dump PostgreSQL;
|
|||
|
|
3. copiare repository, `.env` e cartella `data/backups`;
|
|||
|
|
4. riavviare Docker Compose sul nuovo host.
|
|||
|
|
|
|||
|
|
Backup PostgreSQL:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
docker compose exec db pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" > ./data/backups/game_$(date +%Y%m%d_%H%M).sql
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Restore su nuovo server:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
cat ./data/backups/game.sql | docker compose exec -T db psql -U "$POSTGRES_USER" "$POSTGRES_DB"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Spegnimento controllato prima di copiare dati grezzi:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
docker compose down
|
|||
|
|
tar -czf contrabbandieri-data.tar.gz data .env docker-compose.yml
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Nota: il dump SQL è preferibile alla copia a caldo di `data/postgres`, perché evita inconsistenze se il database è in esecuzione.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 14. Dockerfile backend
|
|||
|
|
|
|||
|
|
```dockerfile
|
|||
|
|
FROM node:22-alpine AS base
|
|||
|
|
WORKDIR /app
|
|||
|
|
COPY package*.json ./
|
|||
|
|
RUN npm ci
|
|||
|
|
COPY . .
|
|||
|
|
RUN npx prisma generate
|
|||
|
|
RUN npm run build
|
|||
|
|
|
|||
|
|
FROM node:22-alpine AS production
|
|||
|
|
WORKDIR /app
|
|||
|
|
ENV NODE_ENV=production
|
|||
|
|
COPY package*.json ./
|
|||
|
|
RUN npm ci --omit=dev
|
|||
|
|
COPY --from=base /app/dist ./dist
|
|||
|
|
COPY --from=base /app/prisma ./prisma
|
|||
|
|
COPY --from=base /app/node_modules/.prisma ./node_modules/.prisma
|
|||
|
|
EXPOSE 3000
|
|||
|
|
CMD ["node", "dist/server.js"]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 15. Package.json base
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"name": "contrabbandieri-server",
|
|||
|
|
"version": "0.1.0",
|
|||
|
|
"type": "module",
|
|||
|
|
"scripts": {
|
|||
|
|
"dev": "tsx watch src/server.ts",
|
|||
|
|
"build": "tsc",
|
|||
|
|
"start": "node dist/server.js",
|
|||
|
|
"prisma:generate": "prisma generate",
|
|||
|
|
"prisma:migrate": "prisma migrate dev",
|
|||
|
|
"prisma:deploy": "prisma migrate deploy",
|
|||
|
|
"prisma:seed": "tsx prisma/seed.ts"
|
|||
|
|
},
|
|||
|
|
"dependencies": {
|
|||
|
|
"@fastify/cors": "latest",
|
|||
|
|
"@fastify/jwt": "latest",
|
|||
|
|
"@fastify/rate-limit": "latest",
|
|||
|
|
"@prisma/client": "latest",
|
|||
|
|
"argon2": "latest",
|
|||
|
|
"dotenv": "latest",
|
|||
|
|
"fastify": "latest",
|
|||
|
|
"ioredis": "latest",
|
|||
|
|
"node-cron": "latest",
|
|||
|
|
"socket.io": "latest",
|
|||
|
|
"zod": "latest"
|
|||
|
|
},
|
|||
|
|
"devDependencies": {
|
|||
|
|
"@types/node": "latest",
|
|||
|
|
"prisma": "latest",
|
|||
|
|
"tsx": "latest",
|
|||
|
|
"typescript": "latest"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 16. Deploy su Linux
|
|||
|
|
|
|||
|
|
### Installazione base
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
sudo apt update
|
|||
|
|
sudo apt install -y ca-certificates curl git ufw
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Docker
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
curl -fsSL https://get.docker.com | sh
|
|||
|
|
sudo usermod -aG docker $USER
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Disconnettersi e riconnettersi.
|
|||
|
|
|
|||
|
|
### Avvio
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git clone <repo>
|
|||
|
|
cd contrabbandieri-server
|
|||
|
|
cp .env.example .env
|
|||
|
|
nano .env
|
|||
|
|
docker compose up -d --build
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Migrazioni
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
docker compose exec api npx prisma migrate deploy
|
|||
|
|
docker compose exec api npx prisma db seed
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 17. Backup e migrazione
|
|||
|
|
|
|||
|
|
### Backup database
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
docker exec contrabbandieri-db pg_dump -U gameuser game > backup.sql
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Restore database
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
cat backup.sql | docker exec -i contrabbandieri-db psql -U gameuser game
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### File da portare su altro server
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
.env
|
|||
|
|
backup.sql
|
|||
|
|
docker-compose.yml
|
|||
|
|
nginx/nginx.conf
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Il codice può essere recuperato da Git.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 18. MVP funzionale minimo
|
|||
|
|
|
|||
|
|
Ordine di implementazione:
|
|||
|
|
|
|||
|
|
1. setup Fastify + TypeScript;
|
|||
|
|
2. Docker Compose con PostgreSQL e Redis;
|
|||
|
|
3. Prisma schema;
|
|||
|
|
4. seed città e item;
|
|||
|
|
5. register/login JWT;
|
|||
|
|
6. player profile;
|
|||
|
|
7. inventory;
|
|||
|
|
8. market current;
|
|||
|
|
9. buy/sell;
|
|||
|
|
10. missioni disponibili;
|
|||
|
|
11. start/claim mission;
|
|||
|
|
12. scheduler prezzi;
|
|||
|
|
13. leaderboard;
|
|||
|
|
14. Socket.IO notifiche.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 19. Seed iniziale
|
|||
|
|
|
|||
|
|
Città:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Milano: economia alta, rischio medio
|
|||
|
|
Napoli: economia media, rischio alto
|
|||
|
|
Palermo: economia bassa, rischio alto
|
|||
|
|
Torino: economia media, rischio basso
|
|||
|
|
Bari: economia bassa, rischio medio
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Item:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Gioielli, base 500
|
|||
|
|
Componenti elettronici, base 350
|
|||
|
|
Documenti falsi, base 700
|
|||
|
|
Auto rubate, base 1800
|
|||
|
|
Informazioni riservate, base 1200
|
|||
|
|
Armi leggere, base 1500
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Missioni template:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Consegna discreta
|
|||
|
|
Recupero merce
|
|||
|
|
Scambio al porto
|
|||
|
|
Infiltrazione uffici
|
|||
|
|
Trasporto notturno
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 20. Estensioni successive
|
|||
|
|
|
|||
|
|
Dopo MVP:
|
|||
|
|
|
|||
|
|
- clan/gang;
|
|||
|
|
- territori;
|
|||
|
|
- PvP asincrono;
|
|||
|
|
- aste tra giocatori;
|
|||
|
|
- crafting oggetti;
|
|||
|
|
- stagioni competitive;
|
|||
|
|
- anti-cheat comportamentale;
|
|||
|
|
- pannello admin web;
|
|||
|
|
- log economico completo;
|
|||
|
|
- bilanciamento automatico dell’economia.
|