feat: stats + chat + frontend pages (Stats, Chat, NavBar)

Backend:
- StatsManager.js: JSON persistence, leaderboard, rate-limit 1/5s
- ChatManager.js: 200-msg buffer, JSON persistence
- index.js: routes GET/POST /stats, /chat/history, /chat/send (Zod validation)
- ArenaRoom.js: chat handler broadcasts to room + persists via ChatManager

Unity:
- StatsTracker.cs: distance, maxSpeed, jumps, bumps, checkpoints, raceTime tracking
- ChatUI.cs: F3 toggle, bottom-right panel, polling 3s, unread badge
- NetworkManager.cs: SendChatMessage() + OnMessage<ChatUI.ChatMessage>(chat)
- CheckpointSystem.cs: RegisterCheckpoint/Finish hooks
- PlayerController.cs: RegisterJump/Bump hooks, physics rebalance, billboard fix
- GameHUD.cs: LocalRaceTimer, SetTotalRounds, OnRoundStart signature fix
- GameManager.cs: spectator cam reconnect fix

Frontend:
- NavBar.jsx: fixed top nav, Accueil/Stats/Chat/Jouer
- App.jsx: page state (home/play/stats/chat) + NavBar
- StatsPage.jsx: 6-tab leaderboard, auto-refresh 30s
- ChatPage.jsx: polling 3s, localStorage name, Enter to send

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-17 18:33:06 +02:00
parent 526d30c569
commit 5c98f1638a
15 changed files with 1144 additions and 17 deletions

View File

@@ -2,28 +2,98 @@ const cors = require('cors');
const { Server } = require('@colyseus/core');
const { WebSocketTransport } = require('@colyseus/ws-transport');
const { ArenaRoom } = require('./rooms/ArenaRoom');
const Stats = require('./stats/StatsManager');
const Chat = require('./chat/ChatManager');
const { z } = require('zod');
const PORT = process.env.PORT || 2567;
// Colyseus 0.17 express callback receives the transport's internal Express app
const statsUpdateSchema = z.object({
name: z.string().min(1).max(32),
stats: z.object({
totalDistance: z.number().optional(),
totalJumps: z.number().optional(),
maxSpeed: z.number().optional(),
bestRaceTime: z.number().optional(),
racesPlayed: z.number().optional(),
qualifications: z.number().optional(),
eliminations: z.number().optional(),
checkpointsTotal: z.number().optional(),
bumpsGiven: z.number().optional(),
totalPlaytime: z.number().optional(),
}),
});
const chatSendSchema = z.object({
name: z.string().min(1).max(32),
text: z.string().min(1).max(200),
});
let _gameServer;
const gameServer = new Server({
transport: new WebSocketTransport(),
express: (app) => {
app.use(cors());
app.use(require('express').json());
app.get('/health', (_req, res) => {
res.json({ service: 'game', status: 'ok', timestamp: new Date().toISOString() });
});
app.get('/', (_req, res) => {
res.send('🎮 Game server running');
app.get('/', (_req, res) => res.send('🎮 Game server running'));
// ── Stats ────────────────────────────────────────────────────────────
app.get('/stats', (_req, res) => {
res.json(Stats.getAll());
});
app.get('/stats/leaderboard/:key', (req, res) => {
const board = Stats.getLeaderboard(req.params.key);
if (!board) return res.status(400).json({ error: 'invalid key' });
res.json(board);
});
app.post('/stats/update', (req, res) => {
const parsed = statsUpdateSchema.safeParse(req.body);
if (!parsed.success) return res.status(400).json({ error: parsed.error.issues });
const ok = Stats.update(parsed.data.name, parsed.data.stats);
res.json({ ok });
});
// ── Chat ─────────────────────────────────────────────────────────────
app.get('/chat/history', (req, res) => {
res.json(Chat.getHistory(req.query.since));
});
app.post('/chat/send', (req, res) => {
const parsed = chatSendSchema.safeParse(req.body);
if (!parsed.success) return res.status(400).json({ error: parsed.error.issues });
const msg = Chat.push(parsed.data.name, parsed.data.text);
if (!msg) return res.status(429).json({ error: 'empty or invalid message' });
// Broadcast to all active Colyseus rooms
if (_gameServer) {
try {
const rooms = _gameServer.matchMaker?.rooms;
if (rooms) {
for (const room of rooms.values()) {
room.broadcast('chat', msg);
}
}
} catch (_) {}
}
res.json(msg);
});
},
});
// Define rooms
gameServer.define('arena', ArenaRoom);
console.log('✅ ArenaRoom registered');
_gameServer = gameServer;
gameServer.define('arena', ArenaRoom).then(() => {
console.log('✅ ArenaRoom registered');
});
gameServer.listen(PORT).then(() => {
console.log(`🎮 Game server running on ws://localhost:${PORT}`);