OrbitMC commited on
Commit
0042077
·
verified ·
1 Parent(s): 7274766

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +375 -334
server.js CHANGED
@@ -2,16 +2,30 @@ const express = require('express');
2
  const http = require('http');
3
  const { Server } = require('socket.io');
4
  const path = require('path');
 
5
 
6
  const app = express();
7
  const server = http.createServer(app);
 
 
8
  const io = new Server(server, {
9
  cors: {
10
  origin: "*",
11
  methods: ["GET", "POST"]
12
  },
13
- pingTimeout: 60000,
14
- pingInterval: 25000
 
 
 
 
 
 
 
 
 
 
 
15
  });
16
 
17
  app.use(express.static(path.join(__dirname, 'public')));
@@ -19,17 +33,21 @@ app.use(express.static(path.join(__dirname, 'public')));
19
  // Game constants
20
  const TILE_SIZE = 10;
21
  const GRID_WIDTH = 13;
22
- const TICK_RATE = 20; // Server updates per second
23
- const INTERPOLATION_BUFFER = 100; // ms
24
 
25
  // Game state
26
  const players = new Map();
27
  const gameState = {
28
  lanes: new Map(),
29
- furthestLaneIndex: 14
 
30
  };
31
 
32
- // Lane types and generation
 
 
 
33
  function generateLaneData(index, prevLane) {
34
  let type = 'grass';
35
  let runLength = 1;
@@ -66,21 +84,23 @@ function generateLaneData(index, prevLane) {
66
  for (let i = 0; i < treeCount; i++) {
67
  let gx = Math.floor(Math.random() * GRID_WIDTH) - Math.floor(GRID_WIDTH / 2);
68
  if (index < 5 && gx === 0) continue;
69
- staticObstacles.push(gx);
 
 
70
  }
71
  }
72
 
73
  const lane = {
74
- index,
75
- type,
76
- runLength,
77
- staticObstacles,
78
- zPos: index * TILE_SIZE,
79
- speed: (Math.random() * 0.04 + 0.02) * TILE_SIZE,
80
- direction: Math.random() > 0.5 ? 1 : -1,
81
- obstacles: [],
82
  timer: 0,
83
- interval: Math.random() * 100 + 150
 
84
  };
85
 
86
  // Pre-populate obstacles
@@ -91,15 +111,14 @@ function generateLaneData(index, prevLane) {
91
  if (Math.abs(randX) < 15) randX += (randX > 0 ? 20 : -20);
92
 
93
  const obsWidth = type === 'water'
94
- ? (Math.random() > 0.5 ? 4 : 6) * TILE_SIZE
95
  : 12;
96
 
97
- lane.obstacles.push({
98
- id: `${index}-${Date.now()}-${i}`,
99
- x: randX,
100
- isLog: type === 'water',
101
- width: obsWidth,
102
- speed: lane.speed * lane.direction
103
  });
104
  }
105
  }
@@ -107,7 +126,6 @@ function generateLaneData(index, prevLane) {
107
  return lane;
108
  }
109
 
110
- // Initialize lanes
111
  function initializeLanes() {
112
  gameState.lanes.clear();
113
  for (let i = -4; i < 15; i++) {
@@ -119,447 +137,470 @@ function initializeLanes() {
119
 
120
  initializeLanes();
121
 
122
- // Update obstacles positions
123
- function updateObstacles() {
124
- gameState.lanes.forEach((lane) => {
125
- if (lane.type === 'road' || lane.type === 'water') {
126
- lane.timer++;
127
-
128
- if (lane.timer > lane.interval) {
129
- const startX = -lane.direction * (GRID_WIDTH * TILE_SIZE / 2 + 60);
130
- const obsWidth = lane.type === 'water'
131
- ? (Math.random() > 0.5 ? 4 : 6) * TILE_SIZE
132
- : 12;
133
-
134
- lane.obstacles.push({
135
- id: `${lane.index}-${Date.now()}`,
136
- x: startX,
137
- isLog: lane.type === 'water',
138
- width: obsWidth,
139
- speed: lane.speed * lane.direction
140
- });
141
-
142
- lane.timer = 0;
143
- lane.interval = Math.random() * 100 + 200;
144
- }
145
-
146
- // Move obstacles
147
- for (let i = lane.obstacles.length - 1; i >= 0; i--) {
148
- const obs = lane.obstacles[i];
149
- obs.x += obs.speed;
150
- if (Math.abs(obs.x) > 400) {
151
- lane.obstacles.splice(i, 1);
152
- }
153
- }
154
- }
155
- });
156
- }
157
-
158
- // Check collision at position
159
  function checkCollisionAtPosition(gridX, gridZ, worldX = null) {
160
  const lane = gameState.lanes.get(gridZ);
161
- if (!lane) return { collision: false };
162
 
163
- const playerX = worldX !== null ? worldX : gridX * TILE_SIZE;
164
 
165
- // Check bounds
166
- if (Math.abs(playerX) > (GRID_WIDTH * TILE_SIZE / 2 + 10)) {
167
- return { collision: true, type: 'bounds' };
168
  }
169
 
170
- // Check static obstacles (trees)
171
- if (lane.type === 'grass') {
172
- if (lane.staticObstacles.includes(gridX)) {
173
- return { collision: true, type: 'tree', blocked: true };
174
  }
175
- return { collision: false };
176
  }
177
 
178
- // Check road (cars)
179
- if (lane.type === 'road') {
180
- for (const obs of lane.obstacles) {
181
- const obsLeft = obs.x - obs.width / 2;
182
- const obsRight = obs.x + obs.width / 2;
183
- const playerLeft = playerX - 3;
184
- const playerRight = playerX + 3;
185
-
186
- if (playerRight > obsLeft && playerLeft < obsRight) {
187
- return { collision: true, type: 'car' };
188
  }
189
  }
190
- return { collision: false };
191
  }
192
 
193
- // Check water (logs)
194
- if (lane.type === 'water') {
195
- for (const obs of lane.obstacles) {
196
- const distX = Math.abs(playerX - obs.x);
197
- const safeDist = (obs.width / 2) + 4;
198
-
199
- if (distX < safeDist) {
200
- return { collision: false, onLog: true, log: obs };
201
  }
202
  }
203
- return { collision: true, type: 'water' };
204
  }
205
 
206
- return { collision: false };
207
  }
208
 
209
- // Validate move
210
  function validateMove(player, dx, dz) {
211
- if (!player.alive) return { valid: false };
212
- if (dz < 0) return { valid: false }; // No backward
213
 
214
- const targetX = player.gridX + dx;
215
- const targetZ = player.gridZ + dz;
216
 
217
  if (Math.abs(targetX) > Math.floor(GRID_WIDTH / 2)) {
218
- return { valid: false };
219
  }
220
 
221
- // Check for tree blocking
222
  const lane = gameState.lanes.get(targetZ);
223
- if (lane && lane.type === 'grass') {
224
- if (lane.staticObstacles.includes(targetX)) {
225
- return { valid: false };
226
- }
227
  }
228
 
229
- return { valid: true, targetX, targetZ };
230
  }
231
 
232
- // Get leaderboard
233
  function getLeaderboard() {
234
- const sorted = Array.from(players.values())
235
  .filter(p => p.alive || p.score > 0)
236
  .sort((a, b) => b.score - a.score)
237
- .slice(0, 3);
238
-
239
- return sorted.map((p, i) => ({
240
- rank: i + 1,
241
- username: p.username,
242
- score: p.score
243
- }));
244
  }
245
 
246
- // Socket handling
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  io.on('connection', (socket) => {
248
- console.log('Player connected:', socket.id);
 
 
 
 
 
 
 
 
249
 
250
- socket.on('join', (username) => {
251
- const sanitizedName = username.substring(0, 15).replace(/[<>]/g, '');
252
-
253
- const player = {
254
- id: socket.id,
255
- username: sanitizedName || 'Anonymous',
256
- gridX: 0,
257
- gridZ: 0,
258
- worldX: 0,
259
- worldZ: 0,
260
- score: 0,
261
- alive: true,
262
- isHopping: false,
263
- hopStartTime: 0,
264
- hopDuration: 120,
265
- hopStartPos: { x: 0, z: 0 },
266
- hopTargetPos: { x: 0, z: 0 },
267
- rotation: 0,
268
- attachedLog: null,
269
- lastMoveTime: 0
270
- };
271
-
272
- players.set(socket.id, player);
273
 
274
- // Send initial state
275
- const lanesData = {};
276
- gameState.lanes.forEach((lane, key) => {
277
- lanesData[key] = lane;
278
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
 
280
- socket.emit('init', {
281
- playerId: socket.id,
282
- player: player,
283
- players: Array.from(players.values()).filter(p => p.id !== socket.id),
284
- lanes: lanesData,
285
- serverTime: Date.now()
286
- });
 
 
 
 
 
287
 
288
- socket.broadcast.emit('playerJoined', player);
289
- io.emit('leaderboard', getLeaderboard());
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
  });
291
 
292
- socket.on('move', (data) => {
293
  const player = players.get(socket.id);
294
  if (!player) return;
295
 
296
  const now = Date.now();
297
 
298
- // Rate limiting - minimum 80ms between moves
299
- if (now - player.lastMoveTime < 80) {
300
- socket.emit('moveRejected', { reason: 'tooFast' });
301
  return;
302
  }
303
 
304
- // Can't move while hopping (server-side enforcement)
305
- if (player.isHopping) {
306
- socket.emit('moveRejected', { reason: 'hopping' });
307
  return;
308
  }
309
 
310
  if (!player.alive) {
311
- socket.emit('moveRejected', { reason: 'dead' });
312
- return;
313
- }
314
-
315
- const { dx, dz, seq } = data;
316
- const validation = validateMove(player, dx, dz);
317
-
318
- if (!validation.valid) {
319
- socket.emit('moveRejected', { seq, reason: 'invalid' });
320
  return;
321
  }
322
 
323
- // Check collision at target
324
- const targetWorldX = validation.targetX * TILE_SIZE;
325
- const collisionCheck = checkCollisionAtPosition(
326
- validation.targetX,
327
- validation.targetZ,
328
- targetWorldX
329
- );
330
 
331
- if (collisionCheck.blocked) {
332
- socket.emit('moveRejected', { seq, reason: 'blocked' });
333
  return;
334
  }
335
 
336
- // Update player state
337
- player.lastMoveTime = now;
338
- player.isHopping = true;
339
- player.hopStartTime = now;
340
- player.hopStartPos = { x: player.worldX, z: player.worldZ };
341
- player.hopTargetPos = { x: targetWorldX, z: validation.targetZ * TILE_SIZE };
342
- player.gridX = validation.targetX;
343
- player.gridZ = validation.targetZ;
344
- player.worldX = targetWorldX;
345
- player.worldZ = validation.targetZ * TILE_SIZE;
346
- player.attachedLog = null;
347
-
348
- // Set rotation
349
- if (dx === 1) player.rotation = -Math.PI / 2;
350
- else if (dx === -1) player.rotation = Math.PI / 2;
351
- else if (dz === 1) player.rotation = 0;
352
- else if (dz === -1) player.rotation = Math.PI;
353
-
354
- // Update score
355
- if (validation.targetZ > player.score) {
356
- player.score = validation.targetZ;
357
- io.emit('leaderboard', getLeaderboard());
 
358
  }
359
 
360
- // Generate new lanes if needed
361
  const spawnZ = player.score + 12;
362
  while (spawnZ > gameState.furthestLaneIndex) {
363
  gameState.furthestLaneIndex++;
364
  const prevLane = gameState.lanes.get(gameState.furthestLaneIndex - 1);
365
  const newLane = generateLaneData(gameState.furthestLaneIndex, prevLane);
366
  gameState.lanes.set(gameState.furthestLaneIndex, newLane);
367
- io.emit('newLane', newLane);
368
  }
369
 
370
- // Confirm move
371
- socket.emit('moveConfirmed', {
372
- seq,
373
- gridX: player.gridX,
374
- gridZ: player.gridZ,
375
- worldX: player.worldX,
376
- worldZ: player.worldZ,
377
- score: player.score,
378
- serverTime: now
379
  });
380
 
381
- // Broadcast to others
382
- socket.broadcast.emit('playerMoved', {
383
  id: player.id,
384
- gridX: player.gridX,
385
- gridZ: player.gridZ,
386
- worldX: player.worldX,
387
- worldZ: player.worldZ,
388
- rotation: player.rotation,
389
- hopStartTime: now,
390
- score: player.score
 
391
  });
392
  });
393
 
394
- socket.on('ping', (clientTime) => {
395
- socket.emit('pong', { clientTime, serverTime: Date.now() });
 
 
 
 
396
  });
397
 
398
- socket.on('respawn', () => {
399
  const player = players.get(socket.id);
400
  if (!player) return;
401
 
402
- player.gridX = 0;
403
- player.gridZ = 0;
404
- player.worldX = 0;
405
- player.worldZ = 0;
406
  player.score = 0;
407
  player.alive = true;
408
- player.isHopping = false;
409
- player.attachedLog = null;
410
- player.rotation = 0;
411
 
412
- socket.emit('respawned', player);
413
- socket.broadcast.emit('playerRespawned', { id: player.id, player });
414
- io.emit('leaderboard', getLeaderboard());
415
  });
416
 
417
- socket.on('disconnect', () => {
418
- console.log('Player disconnected:', socket.id);
 
 
 
 
419
  players.delete(socket.id);
420
- io.emit('playerLeft', socket.id);
421
- io.emit('leaderboard', getLeaderboard());
 
 
 
 
 
422
  });
423
  });
424
 
425
- // Game loop
426
- setInterval(() => {
427
- updateObstacles();
428
 
429
- // Update players
430
  const now = Date.now();
431
- players.forEach((player) => {
432
- if (!player.alive) return;
433
 
434
- // Complete hop if time elapsed
435
- if (player.isHopping) {
436
- if (now - player.hopStartTime >= player.hopDuration) {
437
- player.isHopping = false;
 
 
 
 
438
 
439
- // Check collision at landing
440
- const collision = checkCollisionAtPosition(
441
- player.gridX,
442
- player.gridZ,
443
- player.worldX
444
- );
445
 
446
- if (collision.collision) {
447
- player.alive = false;
448
- io.emit('playerDied', {
449
- id: player.id,
450
- type: collision.type
451
- });
452
- io.emit('leaderboard', getLeaderboard());
453
- } else if (collision.onLog) {
454
- player.attachedLog = collision.log;
 
455
  }
456
  }
457
  }
 
458
 
459
- // Move with log
460
- if (!player.isHopping && player.attachedLog) {
461
- const lane = gameState.lanes.get(player.gridZ);
462
- if (lane) {
463
- const log = lane.obstacles.find(o => o.id === player.attachedLog.id);
464
- if (log) {
465
- player.worldX = log.x;
466
- player.gridX = Math.round(player.worldX / TILE_SIZE);
467
-
468
- // Check if pushed off screen
469
- if (Math.abs(player.worldX) > (GRID_WIDTH * TILE_SIZE / 2 + 10)) {
470
- player.alive = false;
471
- io.emit('playerDied', { id: player.id, type: 'bounds' });
472
- io.emit('leaderboard', getLeaderboard());
473
- }
474
- } else {
475
- player.attachedLog = null;
476
- // Fell off log
477
- const collision = checkCollisionAtPosition(
478
- player.gridX,
479
- player.gridZ,
480
- player.worldX
481
- );
482
- if (collision.collision && collision.type === 'water') {
 
 
 
 
 
 
483
  player.alive = false;
484
- io.emit('playerDied', { id: player.id, type: 'water' });
485
- io.emit('leaderboard', getLeaderboard());
 
 
486
  }
487
  }
488
  }
489
  }
490
 
491
- // Check car collisions during hop
492
- if (player.isHopping) {
493
- const hopProgress = (now - player.hopStartTime) / player.hopDuration;
494
- const currentX = player.hopStartPos.x + (player.hopTargetPos.x - player.hopStartPos.x) * hopProgress;
495
- const currentZ = player.hopStartPos.z + (player.hopTargetPos.z - player.hopStartPos.z) * hopProgress;
496
- const currentGridZ = Math.round(currentZ / TILE_SIZE);
497
-
498
- const lane = gameState.lanes.get(currentGridZ);
499
- if (lane && lane.type === 'road') {
500
- for (const obs of lane.obstacles) {
501
- const obsLeft = obs.x - obs.width / 2 - 2;
502
- const obsRight = obs.x + obs.width / 2 + 2;
503
 
504
- if (currentX > obsLeft && currentX < obsRight) {
505
  player.alive = false;
506
- player.isHopping = false;
507
- io.emit('playerDied', { id: player.id, type: 'car' });
508
- io.emit('leaderboard', getLeaderboard());
509
- break;
 
 
 
 
 
510
  }
511
  }
512
  }
513
  }
514
  });
515
 
516
- // Broadcast game state
517
- const playersData = Array.from(players.values()).map(p => ({
518
- id: p.id,
519
- username: p.username,
520
- gridX: p.gridX,
521
- gridZ: p.gridZ,
522
- worldX: p.worldX,
523
- worldZ: p.worldZ,
524
- rotation: p.rotation,
525
- score: p.score,
526
- alive: p.alive,
527
- isHopping: p.isHopping,
528
- hopStartTime: p.hopStartTime,
529
- hopStartPos: p.hopStartPos,
530
- hopTargetPos: p.hopTargetPos
531
- }));
532
-
533
- const obstaclesData = {};
534
  gameState.lanes.forEach((lane, key) => {
535
- if (lane.type === 'road' || lane.type === 'water') {
536
- obstaclesData[key] = lane.obstacles;
 
 
 
537
  }
538
  });
539
 
540
- io.emit('gameState', {
541
- players: playersData,
542
- obstacles: obstaclesData,
543
- serverTime: Date.now()
544
  });
 
545
 
546
- }, 1000 / TICK_RATE);
547
 
548
- // Cleanup old lanes periodically
549
  setInterval(() => {
550
  let minZ = Infinity;
551
  players.forEach(p => {
552
- if (p.gridZ < minZ) minZ = p.gridZ;
553
  });
554
-
555
  if (minZ === Infinity) minZ = 0;
556
 
557
  gameState.lanes.forEach((lane, key) => {
558
- if (key < minZ - 10) {
559
  gameState.lanes.delete(key);
560
  }
561
  });
562
- }, 5000);
 
 
 
 
 
 
 
 
 
563
 
564
  const PORT = process.env.PORT || 7860;
565
  server.listen(PORT, '0.0.0.0', () => {
 
2
  const http = require('http');
3
  const { Server } = require('socket.io');
4
  const path = require('path');
5
+ const zlib = require('zlib');
6
 
7
  const app = express();
8
  const server = http.createServer(app);
9
+
10
+ // Professional Socket.IO configuration
11
  const io = new Server(server, {
12
  cors: {
13
  origin: "*",
14
  methods: ["GET", "POST"]
15
  },
16
+ // Optimized for slow connections
17
+ pingTimeout: 120000,
18
+ pingInterval: 10000,
19
+ upgradeTimeout: 30000,
20
+ transports: ['websocket', 'polling'],
21
+ allowUpgrades: true,
22
+ perMessageDeflate: {
23
+ threshold: 256,
24
+ zlibDeflateOptions: { level: 6 },
25
+ zlibInflateOptions: { chunkSize: 10 * 1024 }
26
+ },
27
+ httpCompression: true,
28
+ maxHttpBufferSize: 1e6
29
  });
30
 
31
  app.use(express.static(path.join(__dirname, 'public')));
 
33
  // Game constants
34
  const TILE_SIZE = 10;
35
  const GRID_WIDTH = 13;
36
+ const BASE_TICK_RATE = 15; // Lower for bandwidth
37
+ const HOP_DURATION = 120;
38
 
39
  // Game state
40
  const players = new Map();
41
  const gameState = {
42
  lanes: new Map(),
43
+ furthestLaneIndex: 14,
44
+ version: 0 // State version for delta sync
45
  };
46
 
47
+ // Connection stats
48
+ const connectionStats = new Map();
49
+
50
+ // ============= LANE GENERATION =============
51
  function generateLaneData(index, prevLane) {
52
  let type = 'grass';
53
  let runLength = 1;
 
84
  for (let i = 0; i < treeCount; i++) {
85
  let gx = Math.floor(Math.random() * GRID_WIDTH) - Math.floor(GRID_WIDTH / 2);
86
  if (index < 5 && gx === 0) continue;
87
+ if (!staticObstacles.includes(gx)) {
88
+ staticObstacles.push(gx);
89
+ }
90
  }
91
  }
92
 
93
  const lane = {
94
+ i: index, // Short keys for bandwidth
95
+ t: type.charAt(0), // 'g', 'r', 'w'
96
+ rl: runLength,
97
+ so: staticObstacles,
98
+ sp: Math.round((Math.random() * 0.04 + 0.02) * TILE_SIZE * 100) / 100,
99
+ d: Math.random() > 0.5 ? 1 : -1,
100
+ obs: [],
 
101
  timer: 0,
102
+ interval: Math.random() * 100 + 150,
103
+ obsId: 0
104
  };
105
 
106
  // Pre-populate obstacles
 
111
  if (Math.abs(randX) < 15) randX += (randX > 0 ? 20 : -20);
112
 
113
  const obsWidth = type === 'water'
114
+ ? (Math.random() > 0.5 ? 40 : 60)
115
  : 12;
116
 
117
+ lane.obs.push({
118
+ id: lane.obsId++,
119
+ x: Math.round(randX * 10) / 10,
120
+ l: type === 'water' ? 1 : 0, // isLog
121
+ w: obsWidth
 
122
  });
123
  }
124
  }
 
126
  return lane;
127
  }
128
 
 
129
  function initializeLanes() {
130
  gameState.lanes.clear();
131
  for (let i = -4; i < 15; i++) {
 
137
 
138
  initializeLanes();
139
 
140
+ // ============= COLLISION DETECTION =============
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  function checkCollisionAtPosition(gridX, gridZ, worldX = null) {
142
  const lane = gameState.lanes.get(gridZ);
143
+ if (!lane) return { hit: false };
144
 
145
+ const px = worldX !== null ? worldX : gridX * TILE_SIZE;
146
 
147
+ if (Math.abs(px) > (GRID_WIDTH * TILE_SIZE / 2 + 10)) {
148
+ return { hit: true, type: 'bounds' };
 
149
  }
150
 
151
+ if (lane.t === 'g') {
152
+ if (lane.so.includes(gridX)) {
153
+ return { hit: true, type: 'tree', blocked: true };
 
154
  }
155
+ return { hit: false };
156
  }
157
 
158
+ if (lane.t === 'r') {
159
+ for (const obs of lane.obs) {
160
+ const left = obs.x - obs.w / 2;
161
+ const right = obs.x + obs.w / 2;
162
+ if (px + 3 > left && px - 3 < right) {
163
+ return { hit: true, type: 'car' };
 
 
 
 
164
  }
165
  }
166
+ return { hit: false };
167
  }
168
 
169
+ if (lane.t === 'w') {
170
+ for (const obs of lane.obs) {
171
+ if (Math.abs(px - obs.x) < (obs.w / 2) + 4) {
172
+ return { hit: false, onLog: true, logId: obs.id };
 
 
 
 
173
  }
174
  }
175
+ return { hit: true, type: 'water' };
176
  }
177
 
178
+ return { hit: false };
179
  }
180
 
 
181
  function validateMove(player, dx, dz) {
182
+ if (!player.alive) return { valid: false, reason: 'dead' };
183
+ if (dz < 0) return { valid: false, reason: 'backward' };
184
 
185
+ const targetX = player.gx + dx;
186
+ const targetZ = player.gz + dz;
187
 
188
  if (Math.abs(targetX) > Math.floor(GRID_WIDTH / 2)) {
189
+ return { valid: false, reason: 'bounds' };
190
  }
191
 
 
192
  const lane = gameState.lanes.get(targetZ);
193
+ if (lane && lane.t === 'g' && lane.so.includes(targetX)) {
194
+ return { valid: false, reason: 'tree' };
 
 
195
  }
196
 
197
+ return { valid: true, tx: targetX, tz: targetZ };
198
  }
199
 
200
+ // ============= LEADERBOARD =============
201
  function getLeaderboard() {
202
+ return Array.from(players.values())
203
  .filter(p => p.alive || p.score > 0)
204
  .sort((a, b) => b.score - a.score)
205
+ .slice(0, 3)
206
+ .map((p, i) => ({
207
+ r: i + 1,
208
+ n: p.name,
209
+ s: p.score
210
+ }));
 
211
  }
212
 
213
+ // Compact player data for network
214
+ function compactPlayer(p) {
215
+ return {
216
+ id: p.id,
217
+ n: p.name,
218
+ gx: p.gx,
219
+ gz: p.gz,
220
+ wx: Math.round(p.wx * 10) / 10,
221
+ wz: Math.round(p.wz * 10) / 10,
222
+ r: Math.round(p.rot * 100) / 100,
223
+ s: p.score,
224
+ a: p.alive ? 1 : 0,
225
+ h: p.hopping ? 1 : 0,
226
+ ht: p.hopTime
227
+ };
228
+ }
229
+
230
+ // Compact lane data
231
+ function compactLane(lane) {
232
+ return {
233
+ i: lane.i,
234
+ t: lane.t,
235
+ rl: lane.rl,
236
+ so: lane.so,
237
+ sp: lane.sp,
238
+ d: lane.d,
239
+ obs: lane.obs.map(o => ({
240
+ id: o.id,
241
+ x: Math.round(o.x * 10) / 10,
242
+ l: o.l,
243
+ w: o.w
244
+ }))
245
+ };
246
+ }
247
+
248
+ // ============= SOCKET HANDLING =============
249
  io.on('connection', (socket) => {
250
+ console.log(`[${new Date().toISOString()}] Connection: ${socket.id}`);
251
+
252
+ // Initialize connection stats
253
+ connectionStats.set(socket.id, {
254
+ connected: Date.now(),
255
+ lastPing: Date.now(),
256
+ ping: 0,
257
+ quality: 'good'
258
+ });
259
 
260
+ // Send immediate acknowledgment
261
+ socket.emit('ack', {
262
+ id: socket.id,
263
+ t: Date.now(),
264
+ v: gameState.version
265
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
 
267
+ socket.on('join', (data) => {
268
+ try {
269
+ const name = (data.name || 'Anon').substring(0, 12).replace(/[<>\"\'&]/g, '');
270
+
271
+ const player = {
272
+ id: socket.id,
273
+ name: name,
274
+ gx: 0, gz: 0,
275
+ wx: 0, wz: 0,
276
+ score: 0,
277
+ alive: true,
278
+ hopping: false,
279
+ hopTime: 0,
280
+ hopStart: { x: 0, z: 0 },
281
+ hopTarget: { x: 0, z: 0 },
282
+ rot: 0,
283
+ logId: null,
284
+ lastMove: 0,
285
+ moveSeq: 0
286
+ };
287
+
288
+ players.set(socket.id, player);
289
 
290
+ // Send minimal initial state
291
+ const lanesArr = [];
292
+ gameState.lanes.forEach((lane) => {
293
+ lanesArr.push(compactLane(lane));
294
+ });
295
+
296
+ const otherPlayers = [];
297
+ players.forEach((p, id) => {
298
+ if (id !== socket.id) {
299
+ otherPlayers.push(compactPlayer(p));
300
+ }
301
+ });
302
 
303
+ socket.emit('init', {
304
+ p: compactPlayer(player),
305
+ ps: otherPlayers,
306
+ ls: lanesArr,
307
+ t: Date.now(),
308
+ lb: getLeaderboard()
309
+ });
310
+
311
+ // Notify others
312
+ socket.broadcast.emit('pj', compactPlayer(player));
313
+
314
+ console.log(`[${new Date().toISOString()}] Joined: ${name} (${socket.id})`);
315
+ } catch (err) {
316
+ console.error('Join error:', err);
317
+ socket.emit('err', { m: 'Join failed' });
318
+ }
319
  });
320
 
321
+ socket.on('m', (data) => { // Move
322
  const player = players.get(socket.id);
323
  if (!player) return;
324
 
325
  const now = Date.now();
326
 
327
+ // Rate limit: 70ms minimum
328
+ if (now - player.lastMove < 70) {
329
+ socket.emit('mr', { s: data.s, r: 'fast' });
330
  return;
331
  }
332
 
333
+ if (player.hopping) {
334
+ socket.emit('mr', { s: data.s, r: 'hop' });
 
335
  return;
336
  }
337
 
338
  if (!player.alive) {
339
+ socket.emit('mr', { s: data.s, r: 'dead' });
 
 
 
 
 
 
 
 
340
  return;
341
  }
342
 
343
+ const { dx, dz, s } = data;
344
+ const v = validateMove(player, dx, dz);
 
 
 
 
 
345
 
346
+ if (!v.valid) {
347
+ socket.emit('mr', { s, r: v.reason });
348
  return;
349
  }
350
 
351
+ // Execute move
352
+ player.lastMove = now;
353
+ player.hopping = true;
354
+ player.hopTime = now;
355
+ player.hopStart = { x: player.wx, z: player.wz };
356
+ player.hopTarget = { x: v.tx * TILE_SIZE, z: v.tz * TILE_SIZE };
357
+ player.gx = v.tx;
358
+ player.gz = v.tz;
359
+ player.wx = v.tx * TILE_SIZE;
360
+ player.wz = v.tz * TILE_SIZE;
361
+ player.logId = null;
362
+ player.moveSeq = s;
363
+
364
+ // Rotation
365
+ if (dx === 1) player.rot = -Math.PI / 2;
366
+ else if (dx === -1) player.rot = Math.PI / 2;
367
+ else if (dz === 1) player.rot = 0;
368
+ else if (dz === -1) player.rot = Math.PI;
369
+
370
+ // Score
371
+ if (v.tz > player.score) {
372
+ player.score = v.tz;
373
+ io.emit('lb', getLeaderboard());
374
  }
375
 
376
+ // Generate new lanes
377
  const spawnZ = player.score + 12;
378
  while (spawnZ > gameState.furthestLaneIndex) {
379
  gameState.furthestLaneIndex++;
380
  const prevLane = gameState.lanes.get(gameState.furthestLaneIndex - 1);
381
  const newLane = generateLaneData(gameState.furthestLaneIndex, prevLane);
382
  gameState.lanes.set(gameState.furthestLaneIndex, newLane);
383
+ io.emit('nl', compactLane(newLane));
384
  }
385
 
386
+ // Confirm
387
+ socket.emit('mc', {
388
+ s,
389
+ gx: player.gx,
390
+ gz: player.gz,
391
+ sc: player.score,
392
+ t: now
 
 
393
  });
394
 
395
+ // Broadcast
396
+ socket.broadcast.emit('pm', {
397
  id: player.id,
398
+ gx: player.gx,
399
+ gz: player.gz,
400
+ wx: player.wx,
401
+ wz: player.wz,
402
+ r: player.rot,
403
+ t: now,
404
+ hs: player.hopStart,
405
+ ht: player.hopTarget
406
  });
407
  });
408
 
409
+ socket.on('p', (ct) => { // Ping
410
+ const stats = connectionStats.get(socket.id);
411
+ if (stats) {
412
+ stats.lastPing = Date.now();
413
+ }
414
+ socket.emit('po', { c: ct, s: Date.now() });
415
  });
416
 
417
+ socket.on('rs', () => { // Respawn
418
  const player = players.get(socket.id);
419
  if (!player) return;
420
 
421
+ player.gx = 0;
422
+ player.gz = 0;
423
+ player.wx = 0;
424
+ player.wz = 0;
425
  player.score = 0;
426
  player.alive = true;
427
+ player.hopping = false;
428
+ player.logId = null;
429
+ player.rot = 0;
430
 
431
+ socket.emit('rsd', compactPlayer(player));
432
+ socket.broadcast.emit('prs', { id: player.id });
433
+ io.emit('lb', getLeaderboard());
434
  });
435
 
436
+ socket.on('hb', () => { // Heartbeat
437
+ socket.emit('hba');
438
+ });
439
+
440
+ socket.on('disconnect', (reason) => {
441
+ console.log(`[${new Date().toISOString()}] Disconnect: ${socket.id} (${reason})`);
442
  players.delete(socket.id);
443
+ connectionStats.delete(socket.id);
444
+ io.emit('pl', socket.id);
445
+ io.emit('lb', getLeaderboard());
446
+ });
447
+
448
+ socket.on('error', (err) => {
449
+ console.error(`Socket error ${socket.id}:`, err);
450
  });
451
  });
452
 
453
+ // ============= GAME LOOP =============
454
+ let lastTick = Date.now();
 
455
 
456
+ function gameTick() {
457
  const now = Date.now();
458
+ const dt = now - lastTick;
459
+ lastTick = now;
460
 
461
+ // Update obstacles
462
+ gameState.lanes.forEach((lane) => {
463
+ if (lane.t === 'r' || lane.t === 'w') {
464
+ lane.timer += dt;
465
+
466
+ if (lane.timer > lane.interval) {
467
+ const startX = -lane.d * (GRID_WIDTH * TILE_SIZE / 2 + 60);
468
+ const obsWidth = lane.t === 'w' ? (Math.random() > 0.5 ? 40 : 60) : 12;
469
 
470
+ lane.obs.push({
471
+ id: lane.obsId++,
472
+ x: startX,
473
+ l: lane.t === 'w' ? 1 : 0,
474
+ w: obsWidth
475
+ });
476
 
477
+ lane.timer = 0;
478
+ lane.interval = Math.random() * 100 + 200;
479
+ }
480
+
481
+ // Move obstacles
482
+ for (let i = lane.obs.length - 1; i >= 0; i--) {
483
+ const obs = lane.obs[i];
484
+ obs.x += lane.sp * lane.d * (dt / 16.67);
485
+ if (Math.abs(obs.x) > 400) {
486
+ lane.obs.splice(i, 1);
487
  }
488
  }
489
  }
490
+ });
491
 
492
+ // Update players
493
+ players.forEach((player) => {
494
+ if (!player.alive) return;
495
+
496
+ // Complete hop
497
+ if (player.hopping && now - player.hopTime >= HOP_DURATION) {
498
+ player.hopping = false;
499
+
500
+ const col = checkCollisionAtPosition(player.gx, player.gz, player.wx);
501
+
502
+ if (col.hit) {
503
+ player.alive = false;
504
+ io.emit('pd', { id: player.id, type: col.type });
505
+ io.emit('lb', getLeaderboard());
506
+ } else if (col.onLog) {
507
+ player.logId = col.logId;
508
+ }
509
+ }
510
+
511
+ // Check collision during hop
512
+ if (player.hopping) {
513
+ const progress = (now - player.hopTime) / HOP_DURATION;
514
+ const cx = player.hopStart.x + (player.hopTarget.x - player.hopStart.x) * progress;
515
+ const cz = player.hopStart.z + (player.hopTarget.z - player.hopStart.z) * progress;
516
+ const cgz = Math.round(cz / TILE_SIZE);
517
+
518
+ const lane = gameState.lanes.get(cgz);
519
+ if (lane && lane.t === 'r') {
520
+ for (const obs of lane.obs) {
521
+ if (Math.abs(cx - obs.x) < (obs.w / 2 + 3)) {
522
  player.alive = false;
523
+ player.hopping = false;
524
+ io.emit('pd', { id: player.id, type: 'car' });
525
+ io.emit('lb', getLeaderboard());
526
+ break;
527
  }
528
  }
529
  }
530
  }
531
 
532
+ // Log riding
533
+ if (!player.hopping && player.logId !== null) {
534
+ const lane = gameState.lanes.get(player.gz);
535
+ if (lane) {
536
+ const log = lane.obs.find(o => o.id === player.logId);
537
+ if (log) {
538
+ player.wx = log.x;
539
+ player.gx = Math.round(player.wx / TILE_SIZE);
 
 
 
 
540
 
541
+ if (Math.abs(player.wx) > (GRID_WIDTH * TILE_SIZE / 2 + 10)) {
542
  player.alive = false;
543
+ io.emit('pd', { id: player.id, type: 'bounds' });
544
+ io.emit('lb', getLeaderboard());
545
+ }
546
+ } else {
547
+ player.logId = null;
548
+ if (lane.t === 'w') {
549
+ player.alive = false;
550
+ io.emit('pd', { id: player.id, type: 'water' });
551
+ io.emit('lb', getLeaderboard());
552
  }
553
  }
554
  }
555
  }
556
  });
557
 
558
+ // Broadcast state (minimal)
559
+ const playersData = [];
560
+ players.forEach(p => playersData.push(compactPlayer(p)));
561
+
562
+ const obsData = {};
 
 
 
 
 
 
 
 
 
 
 
 
 
563
  gameState.lanes.forEach((lane, key) => {
564
+ if (lane.t !== 'g' && lane.obs.length > 0) {
565
+ obsData[key] = lane.obs.map(o => ({
566
+ id: o.id,
567
+ x: Math.round(o.x * 10) / 10
568
+ }));
569
  }
570
  });
571
 
572
+ io.emit('gs', {
573
+ ps: playersData,
574
+ obs: obsData,
575
+ t: now
576
  });
577
+ }
578
 
579
+ setInterval(gameTick, 1000 / BASE_TICK_RATE);
580
 
581
+ // Cleanup old lanes
582
  setInterval(() => {
583
  let minZ = Infinity;
584
  players.forEach(p => {
585
+ if (p.gz < minZ) minZ = p.gz;
586
  });
 
587
  if (minZ === Infinity) minZ = 0;
588
 
589
  gameState.lanes.forEach((lane, key) => {
590
+ if (key < minZ - 15) {
591
  gameState.lanes.delete(key);
592
  }
593
  });
594
+ }, 10000);
595
+
596
+ // Health check endpoint
597
+ app.get('/health', (req, res) => {
598
+ res.json({
599
+ status: 'ok',
600
+ players: players.size,
601
+ uptime: process.uptime()
602
+ });
603
+ });
604
 
605
  const PORT = process.env.PORT || 7860;
606
  server.listen(PORT, '0.0.0.0', () => {