yt / server.js
OrbitMC's picture
Update server.js
0042077 verified
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const path = require('path');
const zlib = require('zlib');
const app = express();
const server = http.createServer(app);
// Professional Socket.IO configuration
const io = new Server(server, {
cors: {
origin: "*",
methods: ["GET", "POST"]
},
// Optimized for slow connections
pingTimeout: 120000,
pingInterval: 10000,
upgradeTimeout: 30000,
transports: ['websocket', 'polling'],
allowUpgrades: true,
perMessageDeflate: {
threshold: 256,
zlibDeflateOptions: { level: 6 },
zlibInflateOptions: { chunkSize: 10 * 1024 }
},
httpCompression: true,
maxHttpBufferSize: 1e6
});
app.use(express.static(path.join(__dirname, 'public')));
// Game constants
const TILE_SIZE = 10;
const GRID_WIDTH = 13;
const BASE_TICK_RATE = 15; // Lower for bandwidth
const HOP_DURATION = 120;
// Game state
const players = new Map();
const gameState = {
lanes: new Map(),
furthestLaneIndex: 14,
version: 0 // State version for delta sync
};
// Connection stats
const connectionStats = new Map();
// ============= LANE GENERATION =============
function generateLaneData(index, prevLane) {
let type = 'grass';
let runLength = 1;
if (index <= 4) {
type = 'grass';
} else if (prevLane) {
const r = Math.random();
if (prevLane.type === 'grass') {
if (r < 0.6) type = 'road';
else if (r < 0.9) type = 'water';
else type = 'grass';
} else if (prevLane.type === 'road') {
if (prevLane.runLength < 4 && r < 0.7) {
type = 'road';
runLength = prevLane.runLength + 1;
} else {
type = 'grass';
}
} else if (prevLane.type === 'water') {
if (prevLane.runLength < 2 && r < 0.6) {
type = 'water';
runLength = prevLane.runLength + 1;
} else {
type = 'grass';
}
}
}
const staticObstacles = [];
if (type === 'grass') {
const treeCount = Math.floor(Math.random() * 3);
for (let i = 0; i < treeCount; i++) {
let gx = Math.floor(Math.random() * GRID_WIDTH) - Math.floor(GRID_WIDTH / 2);
if (index < 5 && gx === 0) continue;
if (!staticObstacles.includes(gx)) {
staticObstacles.push(gx);
}
}
}
const lane = {
i: index, // Short keys for bandwidth
t: type.charAt(0), // 'g', 'r', 'w'
rl: runLength,
so: staticObstacles,
sp: Math.round((Math.random() * 0.04 + 0.02) * TILE_SIZE * 100) / 100,
d: Math.random() > 0.5 ? 1 : -1,
obs: [],
timer: 0,
interval: Math.random() * 100 + 150,
obsId: 0
};
// Pre-populate obstacles
if (type === 'road' || type === 'water') {
const count = Math.ceil(Math.random() * 2);
for (let i = 0; i < count; i++) {
let randX = (Math.random() * 120) - 60;
if (Math.abs(randX) < 15) randX += (randX > 0 ? 20 : -20);
const obsWidth = type === 'water'
? (Math.random() > 0.5 ? 40 : 60)
: 12;
lane.obs.push({
id: lane.obsId++,
x: Math.round(randX * 10) / 10,
l: type === 'water' ? 1 : 0, // isLog
w: obsWidth
});
}
}
return lane;
}
function initializeLanes() {
gameState.lanes.clear();
for (let i = -4; i < 15; i++) {
const prevLane = gameState.lanes.get(i - 1);
gameState.lanes.set(i, generateLaneData(i, prevLane));
}
gameState.furthestLaneIndex = 14;
}
initializeLanes();
// ============= COLLISION DETECTION =============
function checkCollisionAtPosition(gridX, gridZ, worldX = null) {
const lane = gameState.lanes.get(gridZ);
if (!lane) return { hit: false };
const px = worldX !== null ? worldX : gridX * TILE_SIZE;
if (Math.abs(px) > (GRID_WIDTH * TILE_SIZE / 2 + 10)) {
return { hit: true, type: 'bounds' };
}
if (lane.t === 'g') {
if (lane.so.includes(gridX)) {
return { hit: true, type: 'tree', blocked: true };
}
return { hit: false };
}
if (lane.t === 'r') {
for (const obs of lane.obs) {
const left = obs.x - obs.w / 2;
const right = obs.x + obs.w / 2;
if (px + 3 > left && px - 3 < right) {
return { hit: true, type: 'car' };
}
}
return { hit: false };
}
if (lane.t === 'w') {
for (const obs of lane.obs) {
if (Math.abs(px - obs.x) < (obs.w / 2) + 4) {
return { hit: false, onLog: true, logId: obs.id };
}
}
return { hit: true, type: 'water' };
}
return { hit: false };
}
function validateMove(player, dx, dz) {
if (!player.alive) return { valid: false, reason: 'dead' };
if (dz < 0) return { valid: false, reason: 'backward' };
const targetX = player.gx + dx;
const targetZ = player.gz + dz;
if (Math.abs(targetX) > Math.floor(GRID_WIDTH / 2)) {
return { valid: false, reason: 'bounds' };
}
const lane = gameState.lanes.get(targetZ);
if (lane && lane.t === 'g' && lane.so.includes(targetX)) {
return { valid: false, reason: 'tree' };
}
return { valid: true, tx: targetX, tz: targetZ };
}
// ============= LEADERBOARD =============
function getLeaderboard() {
return Array.from(players.values())
.filter(p => p.alive || p.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, 3)
.map((p, i) => ({
r: i + 1,
n: p.name,
s: p.score
}));
}
// Compact player data for network
function compactPlayer(p) {
return {
id: p.id,
n: p.name,
gx: p.gx,
gz: p.gz,
wx: Math.round(p.wx * 10) / 10,
wz: Math.round(p.wz * 10) / 10,
r: Math.round(p.rot * 100) / 100,
s: p.score,
a: p.alive ? 1 : 0,
h: p.hopping ? 1 : 0,
ht: p.hopTime
};
}
// Compact lane data
function compactLane(lane) {
return {
i: lane.i,
t: lane.t,
rl: lane.rl,
so: lane.so,
sp: lane.sp,
d: lane.d,
obs: lane.obs.map(o => ({
id: o.id,
x: Math.round(o.x * 10) / 10,
l: o.l,
w: o.w
}))
};
}
// ============= SOCKET HANDLING =============
io.on('connection', (socket) => {
console.log(`[${new Date().toISOString()}] Connection: ${socket.id}`);
// Initialize connection stats
connectionStats.set(socket.id, {
connected: Date.now(),
lastPing: Date.now(),
ping: 0,
quality: 'good'
});
// Send immediate acknowledgment
socket.emit('ack', {
id: socket.id,
t: Date.now(),
v: gameState.version
});
socket.on('join', (data) => {
try {
const name = (data.name || 'Anon').substring(0, 12).replace(/[<>\"\'&]/g, '');
const player = {
id: socket.id,
name: name,
gx: 0, gz: 0,
wx: 0, wz: 0,
score: 0,
alive: true,
hopping: false,
hopTime: 0,
hopStart: { x: 0, z: 0 },
hopTarget: { x: 0, z: 0 },
rot: 0,
logId: null,
lastMove: 0,
moveSeq: 0
};
players.set(socket.id, player);
// Send minimal initial state
const lanesArr = [];
gameState.lanes.forEach((lane) => {
lanesArr.push(compactLane(lane));
});
const otherPlayers = [];
players.forEach((p, id) => {
if (id !== socket.id) {
otherPlayers.push(compactPlayer(p));
}
});
socket.emit('init', {
p: compactPlayer(player),
ps: otherPlayers,
ls: lanesArr,
t: Date.now(),
lb: getLeaderboard()
});
// Notify others
socket.broadcast.emit('pj', compactPlayer(player));
console.log(`[${new Date().toISOString()}] Joined: ${name} (${socket.id})`);
} catch (err) {
console.error('Join error:', err);
socket.emit('err', { m: 'Join failed' });
}
});
socket.on('m', (data) => { // Move
const player = players.get(socket.id);
if (!player) return;
const now = Date.now();
// Rate limit: 70ms minimum
if (now - player.lastMove < 70) {
socket.emit('mr', { s: data.s, r: 'fast' });
return;
}
if (player.hopping) {
socket.emit('mr', { s: data.s, r: 'hop' });
return;
}
if (!player.alive) {
socket.emit('mr', { s: data.s, r: 'dead' });
return;
}
const { dx, dz, s } = data;
const v = validateMove(player, dx, dz);
if (!v.valid) {
socket.emit('mr', { s, r: v.reason });
return;
}
// Execute move
player.lastMove = now;
player.hopping = true;
player.hopTime = now;
player.hopStart = { x: player.wx, z: player.wz };
player.hopTarget = { x: v.tx * TILE_SIZE, z: v.tz * TILE_SIZE };
player.gx = v.tx;
player.gz = v.tz;
player.wx = v.tx * TILE_SIZE;
player.wz = v.tz * TILE_SIZE;
player.logId = null;
player.moveSeq = s;
// Rotation
if (dx === 1) player.rot = -Math.PI / 2;
else if (dx === -1) player.rot = Math.PI / 2;
else if (dz === 1) player.rot = 0;
else if (dz === -1) player.rot = Math.PI;
// Score
if (v.tz > player.score) {
player.score = v.tz;
io.emit('lb', getLeaderboard());
}
// Generate new lanes
const spawnZ = player.score + 12;
while (spawnZ > gameState.furthestLaneIndex) {
gameState.furthestLaneIndex++;
const prevLane = gameState.lanes.get(gameState.furthestLaneIndex - 1);
const newLane = generateLaneData(gameState.furthestLaneIndex, prevLane);
gameState.lanes.set(gameState.furthestLaneIndex, newLane);
io.emit('nl', compactLane(newLane));
}
// Confirm
socket.emit('mc', {
s,
gx: player.gx,
gz: player.gz,
sc: player.score,
t: now
});
// Broadcast
socket.broadcast.emit('pm', {
id: player.id,
gx: player.gx,
gz: player.gz,
wx: player.wx,
wz: player.wz,
r: player.rot,
t: now,
hs: player.hopStart,
ht: player.hopTarget
});
});
socket.on('p', (ct) => { // Ping
const stats = connectionStats.get(socket.id);
if (stats) {
stats.lastPing = Date.now();
}
socket.emit('po', { c: ct, s: Date.now() });
});
socket.on('rs', () => { // Respawn
const player = players.get(socket.id);
if (!player) return;
player.gx = 0;
player.gz = 0;
player.wx = 0;
player.wz = 0;
player.score = 0;
player.alive = true;
player.hopping = false;
player.logId = null;
player.rot = 0;
socket.emit('rsd', compactPlayer(player));
socket.broadcast.emit('prs', { id: player.id });
io.emit('lb', getLeaderboard());
});
socket.on('hb', () => { // Heartbeat
socket.emit('hba');
});
socket.on('disconnect', (reason) => {
console.log(`[${new Date().toISOString()}] Disconnect: ${socket.id} (${reason})`);
players.delete(socket.id);
connectionStats.delete(socket.id);
io.emit('pl', socket.id);
io.emit('lb', getLeaderboard());
});
socket.on('error', (err) => {
console.error(`Socket error ${socket.id}:`, err);
});
});
// ============= GAME LOOP =============
let lastTick = Date.now();
function gameTick() {
const now = Date.now();
const dt = now - lastTick;
lastTick = now;
// Update obstacles
gameState.lanes.forEach((lane) => {
if (lane.t === 'r' || lane.t === 'w') {
lane.timer += dt;
if (lane.timer > lane.interval) {
const startX = -lane.d * (GRID_WIDTH * TILE_SIZE / 2 + 60);
const obsWidth = lane.t === 'w' ? (Math.random() > 0.5 ? 40 : 60) : 12;
lane.obs.push({
id: lane.obsId++,
x: startX,
l: lane.t === 'w' ? 1 : 0,
w: obsWidth
});
lane.timer = 0;
lane.interval = Math.random() * 100 + 200;
}
// Move obstacles
for (let i = lane.obs.length - 1; i >= 0; i--) {
const obs = lane.obs[i];
obs.x += lane.sp * lane.d * (dt / 16.67);
if (Math.abs(obs.x) > 400) {
lane.obs.splice(i, 1);
}
}
}
});
// Update players
players.forEach((player) => {
if (!player.alive) return;
// Complete hop
if (player.hopping && now - player.hopTime >= HOP_DURATION) {
player.hopping = false;
const col = checkCollisionAtPosition(player.gx, player.gz, player.wx);
if (col.hit) {
player.alive = false;
io.emit('pd', { id: player.id, type: col.type });
io.emit('lb', getLeaderboard());
} else if (col.onLog) {
player.logId = col.logId;
}
}
// Check collision during hop
if (player.hopping) {
const progress = (now - player.hopTime) / HOP_DURATION;
const cx = player.hopStart.x + (player.hopTarget.x - player.hopStart.x) * progress;
const cz = player.hopStart.z + (player.hopTarget.z - player.hopStart.z) * progress;
const cgz = Math.round(cz / TILE_SIZE);
const lane = gameState.lanes.get(cgz);
if (lane && lane.t === 'r') {
for (const obs of lane.obs) {
if (Math.abs(cx - obs.x) < (obs.w / 2 + 3)) {
player.alive = false;
player.hopping = false;
io.emit('pd', { id: player.id, type: 'car' });
io.emit('lb', getLeaderboard());
break;
}
}
}
}
// Log riding
if (!player.hopping && player.logId !== null) {
const lane = gameState.lanes.get(player.gz);
if (lane) {
const log = lane.obs.find(o => o.id === player.logId);
if (log) {
player.wx = log.x;
player.gx = Math.round(player.wx / TILE_SIZE);
if (Math.abs(player.wx) > (GRID_WIDTH * TILE_SIZE / 2 + 10)) {
player.alive = false;
io.emit('pd', { id: player.id, type: 'bounds' });
io.emit('lb', getLeaderboard());
}
} else {
player.logId = null;
if (lane.t === 'w') {
player.alive = false;
io.emit('pd', { id: player.id, type: 'water' });
io.emit('lb', getLeaderboard());
}
}
}
}
});
// Broadcast state (minimal)
const playersData = [];
players.forEach(p => playersData.push(compactPlayer(p)));
const obsData = {};
gameState.lanes.forEach((lane, key) => {
if (lane.t !== 'g' && lane.obs.length > 0) {
obsData[key] = lane.obs.map(o => ({
id: o.id,
x: Math.round(o.x * 10) / 10
}));
}
});
io.emit('gs', {
ps: playersData,
obs: obsData,
t: now
});
}
setInterval(gameTick, 1000 / BASE_TICK_RATE);
// Cleanup old lanes
setInterval(() => {
let minZ = Infinity;
players.forEach(p => {
if (p.gz < minZ) minZ = p.gz;
});
if (minZ === Infinity) minZ = 0;
gameState.lanes.forEach((lane, key) => {
if (key < minZ - 15) {
gameState.lanes.delete(key);
}
});
}, 10000);
// Health check endpoint
app.get('/health', (req, res) => {
res.json({
status: 'ok',
players: players.size,
uptime: process.uptime()
});
});
const PORT = process.env.PORT || 7860;
server.listen(PORT, '0.0.0.0', () => {
console.log(`Server running on port ${PORT}`);
});