Files
gioco-mobile/Blueprint_Client.md
T

1055 lines
18 KiB
Markdown
Raw Normal View History

2026-06-09 22:40:43 +02:00
# 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 lequilibrio.
---
## 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 delleconomia.