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:
@@ -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}`);
|
||||
|
||||
Reference in New Issue
Block a user