import { Server, Room, matchMaker } from '@colyseus/core'; import { WebSocketTransport } from '@colyseus/ws-transport'; import { createServer } from 'node:http'; import { rm, readFile, writeFile } from 'node:fs/promises'; const U = String.fromCharCode(95); const chars = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789'; const maxPals = 4; const usedCodes = new Set(); function makeCode() { let s = ''; for (let i = 0; i < 5; i++) { s += chars.charAt(Math.floor(Math.random() * chars.length)); } return usedCodes.has(s) ? makeCode() : s; } class BlastRoom extends Room { maxClients = maxPals; hostId = null; slots = new Map(); onCreate() { this.roomId = makeCode(); usedCodes.add(this.roomId); this.onMessage('snap', (c, d) => { if (c.sessionId === this.hostId) { this.broadcast('snap', d, { except: c }); } }); this.onMessage('in', (c, d) => { this.toHost('in', Object.assign({ slot: this.slots.get(c.sessionId) }, d)); }); this.onMessage('bomb', (c) => { this.toHost('bomb', { slot: this.slots.get(c.sessionId) }); }); } toHost(type, data) { const h = this.clients.find((c) => c.sessionId === this.hostId); if (h) { h.send(type, data); } } onJoin(c) { const taken = new Set(this.slots.values()); let slot = 0; while (taken.has(slot)) { slot++; } this.slots.set(c.sessionId, slot); if (!this.hostId) { this.hostId = c.sessionId; } c.send('welcome', { code: this.roomId, slot: slot, host: c.sessionId === this.hostId }); if (c.sessionId !== this.hostId) { this.toHost('joined', { slot: slot }); } if (this.clients.length >= maxPals) { this.lock(); } } onLeave(c) { const slot = this.slots.get(c.sessionId); this.slots.delete(c.sessionId); if (c.sessionId === this.hostId) { this.broadcast('hostLeft', {}); this.disconnect(); return; } this.toHost('left', { slot: slot }); this.unlock(); } onDispose() { usedCodes.delete(this.roomId); } } class ArenaRoom extends BlastRoom { playing = false; onCreate() { super.onCreate(); this.setMetadata({ playing: false }); this.onMessage('state', (c, d) => { if (c.sessionId !== this.hostId) { return; } this.playing = !!(d && d.playing); this.setMetadata({ playing: this.playing }); if (this.playing || this.clients.length >= maxPals) { this.lock(); } else { this.unlock(); } }); } onLeave(c) { super.onLeave(c); if (this.playing) { this.lock(); } } } const players = {}; readFile('stats.json', 'utf8').then((t) => Object.assign(players, JSON.parse(t))).catch(() => {}); let saveTimer = null; function saveStats() { if (saveTimer) { return; } saveTimer = setTimeout(() => { saveTimer = null; writeFile('stats.json', JSON.stringify(players)).catch(() => {}); }, 5000); } function readBody(req) { return new Promise((resolve) => { let body = ''; req.on('data', (c) => { body += c; if (body.length > 4000) { req.destroy(); } }); req.on('end', () => resolve(body)); req.on('error', () => resolve('')); }); } function num(v) { const n = Number(v); return isFinite(n) && n >= 0 ? n : 0; } function addStats(text) { const o = JSON.parse(text); const id = String(o.id || '').replace(/[^a-z0-9]/g, '').slice(0, 20); if (id.length < 8) { return; } const p = players[id] || { firstSeen: Date.now() }; p.firstSeen = Math.min(p.firstSeen, num(o.firstSeen) || Date.now()); p.lastSeen = Date.now(); p.sessions = Math.max(p.sessions || 0, num(o.sessions)); p.playMinutes = Math.max(p.playMinutes || 0, num(o.playMinutes)); p.completion = Math.min(100, Math.max(p.completion || 0, num(o.completion))); p.arenaGames = Math.max(p.arenaGames || 0, num(o.arenaGames)); players[id] = p; saveStats(); } function day(t) { return new Date(t).toISOString().slice(0, 16).replace('T', ' '); } function statsPage() { const list = Object.entries(players).sort((a, b) => b[1].lastSeen - a[1].lastSeen); let mins = 0; let arena = 0; const rows = list.map(([id, p], i) => { mins += p.playMinutes || 0; arena += p.arenaGames || 0; return '
Players: ' + list.length + ' Total play time: ' + Math.round(mins) + ' minutes Arena battles: ' + arena + ' (times are UTC)
' + '| # | Player | First played | Last played | Sessions | Minutes played | Completed | Arena battles |
|---|