chore: add game server (Colyseus) - flat files
This commit is contained in:
Submodule rolld_backend deleted from 592c9002c5
8
rolld_backend/.gitignore
vendored
Normal file
8
rolld_backend/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
node_modules/
|
||||
.env
|
||||
.env.local
|
||||
*.log
|
||||
dist/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.venv/
|
||||
51
rolld_backend/README.md
Normal file
51
rolld_backend/README.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# ROLL'D Backend
|
||||
|
||||
Monorepo backend pour le jeu ROLL'D — architecture microservices.
|
||||
|
||||
## Services
|
||||
|
||||
| Service | Port | Stack | Description |
|
||||
|---------|------|-------|-------------|
|
||||
| auth | 3001 | Node.js + Express | Authentification & sessions |
|
||||
| game | 2567 | Node.js + Colyseus | Serveur de jeu temps réel |
|
||||
| stats | 8000 | Python + FastAPI | Statistiques & analytics |
|
||||
|
||||
## Infrastructure
|
||||
|
||||
| Service | Port | Description |
|
||||
|---------|------|-------------|
|
||||
| postgres | 5432 | Base de données |
|
||||
| redis | 6379 | Cache / PubSub |
|
||||
|
||||
## Quickstart
|
||||
|
||||
```bash
|
||||
# Lancer tout l'environnement de dev
|
||||
docker compose up --build
|
||||
|
||||
# Un seul service
|
||||
docker compose up auth
|
||||
|
||||
# Dev sans Docker
|
||||
cd auth && npm install && npm run dev
|
||||
cd game && npm install && npm run dev
|
||||
cd stats && pip install -r requirements.txt && uvicorn app.main:app --reload
|
||||
```
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
├── auth/ # Service d'authentification
|
||||
│ ├── Dockerfile
|
||||
│ ├── package.json
|
||||
│ └── src/
|
||||
├── game/ # Serveur Colyseus
|
||||
│ ├── Dockerfile
|
||||
│ ├── package.json
|
||||
│ └── src/
|
||||
├── stats/ # Service de statistiques
|
||||
│ ├── Dockerfile
|
||||
│ ├── requirements.txt
|
||||
│ └── app/
|
||||
└── docker-compose.yml
|
||||
```
|
||||
4
rolld_backend/game/.dockerignore
Normal file
4
rolld_backend/game/.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
.git
|
||||
.gitignore
|
||||
12
rolld_backend/game/Dockerfile
Normal file
12
rolld_backend/game/Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 2567
|
||||
|
||||
CMD ["node", "src/index.js"]
|
||||
1306
rolld_backend/game/package-lock.json
generated
Normal file
1306
rolld_backend/game/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
rolld_backend/game/package.json
Normal file
17
rolld_backend/game/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "rolld-game",
|
||||
"version": "1.0.0",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"start": "node src/index.js",
|
||||
"dev": "node --watch src/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@colyseus/core": "^0.17.39",
|
||||
"@colyseus/schema": "^4.0.15",
|
||||
"@colyseus/ws-transport": "^0.17.9",
|
||||
"cors": "^2.8.6",
|
||||
"express": "^4.22.1",
|
||||
"zod": "^4.3.6"
|
||||
}
|
||||
}
|
||||
30
rolld_backend/game/src/index.js
Normal file
30
rolld_backend/game/src/index.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const cors = require('cors');
|
||||
const { Server } = require('@colyseus/core');
|
||||
const { WebSocketTransport } = require('@colyseus/ws-transport');
|
||||
const { ArenaRoom } = require('./rooms/ArenaRoom');
|
||||
|
||||
const PORT = process.env.PORT || 2567;
|
||||
|
||||
// Colyseus 0.17 – express callback receives the transport's internal Express app
|
||||
const gameServer = new Server({
|
||||
transport: new WebSocketTransport(),
|
||||
express: (app) => {
|
||||
app.use(cors());
|
||||
|
||||
app.get('/health', (_req, res) => {
|
||||
res.json({ service: 'game', status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
app.get('/', (_req, res) => {
|
||||
res.send('🎮 Game server running');
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Define rooms
|
||||
gameServer.define('arena', ArenaRoom);
|
||||
console.log('✅ ArenaRoom registered');
|
||||
|
||||
gameServer.listen(PORT).then(() => {
|
||||
console.log(`🎮 Game server running on ws://localhost:${PORT}`);
|
||||
});
|
||||
120
rolld_backend/game/src/rooms/ArenaRoom.js
Normal file
120
rolld_backend/game/src/rooms/ArenaRoom.js
Normal file
@@ -0,0 +1,120 @@
|
||||
const { Room } = require("@colyseus/core");
|
||||
const { GameState, Player } = require("../schema/GameState");
|
||||
|
||||
class ArenaRoom extends Room {
|
||||
maxClients = 20;
|
||||
|
||||
onCreate(options) {
|
||||
this.setState(new GameState());
|
||||
this.setPatchRate(16); // ~62.5 Hz state broadcast
|
||||
console.log(`[ArenaRoom] Room ${this.roomId} created (patchRate=16ms ~62Hz)`);
|
||||
|
||||
// Handle position updates from clients
|
||||
this.onMessage("position", (client, data) => {
|
||||
const player = this.state.players.get(client.sessionId);
|
||||
if (!player) return;
|
||||
|
||||
player.x = data.x ?? player.x;
|
||||
player.y = data.y ?? player.y;
|
||||
player.z = data.z ?? player.z;
|
||||
player.vx = data.vx ?? player.vx;
|
||||
player.vy = data.vy ?? player.vy;
|
||||
player.vz = data.vz ?? player.vz;
|
||||
player.rx = data.rx ?? player.rx;
|
||||
player.ry = data.ry ?? player.ry;
|
||||
player.rz = data.rz ?? player.rz;
|
||||
player.rw = data.rw ?? player.rw;
|
||||
player.avx = data.avx ?? player.avx;
|
||||
player.avy = data.avy ?? player.avy;
|
||||
player.avz = data.avz ?? player.avz;
|
||||
player.t = Date.now();
|
||||
});
|
||||
|
||||
// Handle chat messages (optional, for future)
|
||||
this.onMessage("chat", (client, data) => {
|
||||
this.broadcast("chat", {
|
||||
sender: client.sessionId,
|
||||
name: this.state.players.get(client.sessionId)?.name || "???",
|
||||
message: data.message,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onJoin(client, options) {
|
||||
console.log(`[ArenaRoom] ${client.sessionId} joined (name: ${options.name || "anonymous"})`);
|
||||
|
||||
const player = new Player();
|
||||
player.name = options.name || "Joueur";
|
||||
player.colorR = options.colorR ?? 1;
|
||||
player.colorG = options.colorG ?? 0.4;
|
||||
player.colorB = options.colorB ?? 0.2;
|
||||
|
||||
// Find a spawn position away from other players
|
||||
const spawnPos = this._findSpawnPosition();
|
||||
player.x = spawnPos.x;
|
||||
player.y = spawnPos.y;
|
||||
player.z = spawnPos.z;
|
||||
player.t = Date.now();
|
||||
|
||||
this.state.players.set(client.sessionId, player);
|
||||
}
|
||||
|
||||
onLeave(client, consented) {
|
||||
console.log(`[ArenaRoom] ${client.sessionId} left (consented: ${consented})`);
|
||||
this.state.players.delete(client.sessionId);
|
||||
}
|
||||
|
||||
onDispose() {
|
||||
console.log(`[ArenaRoom] Room ${this.roomId} disposed`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a spawn position elevated and away from existing players.
|
||||
* Tries up to 10 random positions, picks the one farthest from others.
|
||||
* Falls back to random if no good spot found.
|
||||
*/
|
||||
_findSpawnPosition() {
|
||||
const MIN_DIST = 3.0;
|
||||
const SPAWN_Y = 5; // elevated spawn — ball drops naturally
|
||||
const RANGE = 20;
|
||||
let bestPos = { x: 0, y: SPAWN_Y, z: 0 };
|
||||
let bestMinDist = 0;
|
||||
|
||||
const existingPositions = [];
|
||||
this.state.players.forEach((p) => {
|
||||
existingPositions.push({ x: p.x, z: p.z });
|
||||
});
|
||||
|
||||
// If no existing players, just random
|
||||
if (existingPositions.length === 0) {
|
||||
return {
|
||||
x: (Math.random() - 0.5) * RANGE,
|
||||
y: SPAWN_Y,
|
||||
z: (Math.random() - 0.5) * RANGE,
|
||||
};
|
||||
}
|
||||
|
||||
for (let attempt = 0; attempt < 10; attempt++) {
|
||||
const cx = (Math.random() - 0.5) * RANGE;
|
||||
const cz = (Math.random() - 0.5) * RANGE;
|
||||
let minDist = Infinity;
|
||||
for (const p of existingPositions) {
|
||||
const dx = cx - p.x;
|
||||
const dz = cz - p.z;
|
||||
const d = Math.sqrt(dx * dx + dz * dz);
|
||||
if (d < minDist) minDist = d;
|
||||
}
|
||||
if (minDist >= MIN_DIST) {
|
||||
return { x: cx, y: SPAWN_Y, z: cz };
|
||||
}
|
||||
if (minDist > bestMinDist) {
|
||||
bestMinDist = minDist;
|
||||
bestPos = { x: cx, y: SPAWN_Y, z: cz };
|
||||
}
|
||||
}
|
||||
|
||||
return bestPos;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ArenaRoom };
|
||||
59
rolld_backend/game/src/schema/GameState.js
Normal file
59
rolld_backend/game/src/schema/GameState.js
Normal file
@@ -0,0 +1,59 @@
|
||||
const { Schema, MapSchema, defineTypes } = require("@colyseus/schema");
|
||||
|
||||
class Player extends Schema {
|
||||
constructor() {
|
||||
super();
|
||||
this.x = 0;
|
||||
this.y = 5;
|
||||
this.z = 0;
|
||||
this.vx = 0;
|
||||
this.vy = 0;
|
||||
this.vz = 0;
|
||||
this.rx = 0;
|
||||
this.ry = 0;
|
||||
this.rz = 0;
|
||||
this.rw = 1;
|
||||
this.t = 0;
|
||||
this.name = "";
|
||||
this.colorR = 1;
|
||||
this.colorG = 1;
|
||||
this.colorB = 1;
|
||||
this.avx = 0;
|
||||
this.avy = 0;
|
||||
this.avz = 0;
|
||||
}
|
||||
}
|
||||
|
||||
defineTypes(Player, {
|
||||
x: "float32",
|
||||
y: "float32",
|
||||
z: "float32",
|
||||
vx: "float32",
|
||||
vy: "float32",
|
||||
vz: "float32",
|
||||
rx: "float32",
|
||||
ry: "float32",
|
||||
rz: "float32",
|
||||
rw: "float32",
|
||||
t: "float64",
|
||||
name: "string",
|
||||
colorR: "float32",
|
||||
colorG: "float32",
|
||||
colorB: "float32",
|
||||
avx: "float32",
|
||||
avy: "float32",
|
||||
avz: "float32",
|
||||
});
|
||||
|
||||
class GameState extends Schema {
|
||||
constructor() {
|
||||
super();
|
||||
this.players = new MapSchema();
|
||||
}
|
||||
}
|
||||
|
||||
defineTypes(GameState, {
|
||||
players: { map: Player },
|
||||
});
|
||||
|
||||
module.exports = { GameState, Player };
|
||||
Reference in New Issue
Block a user