OrbitMC commited on
Commit
47cf087
·
verified ·
1 Parent(s): 3267608

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +492 -503
server.js CHANGED
@@ -6,571 +6,560 @@ const path = require('path');
6
  const app = express();
7
  const server = http.createServer(app);
8
  const io = new Server(server, {
9
- cors: { origin: "*" },
10
- pingInterval: 1000,
11
- pingTimeout: 5000,
12
- transports: ['websocket', 'polling']
 
 
13
  });
14
 
15
  app.use(express.static(path.join(__dirname, 'public')));
16
 
17
- // Game Constants
18
- const TICK_RATE = 20;
19
- const MAP_SIZE = 3000;
20
- const MAX_PLAYERS = 50;
21
- const ORB_COUNT = 200;
22
- const POWERUP_COUNT = 30;
23
- const OBSTACLE_COUNT = 50;
24
- const DANGER_ZONE_SHRINK_RATE = 0.5;
25
- const SAFE_ZONE_MIN = 500;
26
-
27
- // Game State
28
- const gameState = {
29
- players: new Map(),
30
- orbs: [],
31
- powerups: [],
32
- obstacles: [],
33
- projectiles: [],
34
- killFeed: [],
35
- safeZoneRadius: MAP_SIZE / 2,
36
- safeZoneCenter: { x: MAP_SIZE / 2, y: MAP_SIZE / 2 },
37
- gameTime: 0,
38
- roundActive: true
39
- };
40
 
41
- // Player Classes
42
- const CLASSES = {
43
- TANK: { health: 150, speed: 0.85, size: 35, damage: 8, ability: 'shield', color: '#4CAF50' },
44
- ASSASSIN: { health: 80, speed: 1.3, size: 25, damage: 15, ability: 'dash', color: '#9C27B0' },
45
- WARRIOR: { health: 100, speed: 1.0, size: 30, damage: 12, ability: 'charge', color: '#F44336' },
46
- MAGE: { health: 70, speed: 1.1, size: 28, damage: 20, ability: 'blast', color: '#2196F3' },
47
- BERSERKER: { health: 120, speed: 0.95, size: 32, damage: 10, ability: 'rage', color: '#FF9800' }
48
  };
49
 
50
- // Powerup Types
51
- const POWERUP_TYPES = ['speed', 'damage', 'health', 'shield', 'size', 'magnet', 'ghost', 'rapid'];
 
 
52
 
53
- // Initialize game objects
54
- function initializeGame() {
55
- gameState.orbs = [];
56
- gameState.powerups = [];
57
- gameState.obstacles = [];
58
-
59
- // Spawn orbs
60
- for (let i = 0; i < ORB_COUNT; i++) {
61
- spawnOrb();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  }
63
-
64
- // Spawn powerups
65
- for (let i = 0; i < POWERUP_COUNT; i++) {
66
- spawnPowerup();
 
 
 
 
 
67
  }
68
-
69
- // Spawn obstacles
70
- for (let i = 0; i < OBSTACLE_COUNT; i++) {
71
- gameState.obstacles.push({
72
- id: `obs_${i}`,
73
- x: Math.random() * MAP_SIZE,
74
- y: Math.random() * MAP_SIZE,
75
- radius: 30 + Math.random() * 50,
76
- type: Math.random() > 0.7 ? 'bounce' : 'solid'
77
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  }
 
 
79
  }
80
 
81
- function spawnOrb() {
82
- const value = Math.random() > 0.9 ? 3 : (Math.random() > 0.7 ? 2 : 1);
83
- gameState.orbs.push({
84
- id: `orb_${Date.now()}_${Math.random()}`,
85
- x: Math.random() * MAP_SIZE,
86
- y: Math.random() * MAP_SIZE,
87
- value: value,
88
- radius: 5 + value * 3
89
- });
90
  }
91
 
92
- function spawnPowerup() {
93
- gameState.powerups.push({
94
- id: `pow_${Date.now()}_${Math.random()}`,
95
- x: Math.random() * MAP_SIZE,
96
- y: Math.random() * MAP_SIZE,
97
- type: POWERUP_TYPES[Math.floor(Math.random() * POWERUP_TYPES.length)],
98
- radius: 15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  });
100
  }
101
 
102
- function createPlayer(id, username, playerClass) {
103
- const classData = CLASSES[playerClass] || CLASSES.WARRIOR;
104
- return {
105
- id,
106
- username: username.substring(0, 15),
107
- class: playerClass,
108
- x: MAP_SIZE / 2 + (Math.random() - 0.5) * 500,
109
- y: MAP_SIZE / 2 + (Math.random() - 0.5) * 500,
110
- vx: 0,
111
- vy: 0,
112
- angle: 0,
113
- health: classData.health,
114
- maxHealth: classData.health,
115
- energy: 100,
116
- maxEnergy: 100,
117
- size: classData.size,
118
- baseSize: classData.size,
119
- speed: classData.speed,
120
- baseSpeed: classData.speed,
121
- damage: classData.damage,
122
- baseDamage: classData.damage,
123
- color: classData.color,
124
- score: 0,
125
- kills: 0,
126
- deaths: 0,
127
- killStreak: 0,
128
- maxKillStreak: 0,
129
- orbsCollected: 0,
130
- lastAttack: 0,
131
- lastAbility: 0,
132
- attackCooldown: 500,
133
- abilityCooldown: 5000,
134
- ability: classData.ability,
135
- effects: {},
136
- isAlive: true,
137
- respawnTime: 0,
138
- invincible: false,
139
- lastUpdate: Date.now(),
140
- inputSequence: 0,
141
- pendingInputs: []
142
- };
143
- }
144
 
145
- // Physics and collision
146
- function distance(a, b) {
147
- return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2);
148
- }
149
 
150
- function circleCollision(a, b, radiusA, radiusB) {
151
- return distance(a, b) < radiusA + radiusB;
152
- }
 
153
 
154
- function normalizeVector(vx, vy) {
155
- const mag = Math.sqrt(vx * vx + vy * vy);
156
- if (mag === 0) return { x: 0, y: 0 };
157
- return { x: vx / mag, y: vy / mag };
158
- }
 
 
159
 
160
- // Game logic
161
- function processPlayerInput(player, input) {
162
- if (!player.isAlive) return;
163
-
164
- const speed = 8 * player.speed;
165
- const { joystickX, joystickY, useAbility, sequence } = input;
166
-
167
- // Apply movement with dead zone
168
- if (Math.abs(joystickX) > 0.1 || Math.abs(joystickY) > 0.1) {
169
- const normalized = normalizeVector(joystickX, joystickY);
170
- player.vx = normalized.x * speed * Math.min(1, Math.sqrt(joystickX ** 2 + joystickY ** 2));
171
- player.vy = normalized.y * speed * Math.min(1, Math.sqrt(joystickX ** 2 + joystickY ** 2));
172
- player.angle = Math.atan2(joystickY, joystickX);
173
- } else {
174
- player.vx *= 0.9;
175
- player.vy *= 0.9;
176
  }
177
-
178
- // Use ability
179
- if (useAbility) {
180
- usePlayerAbility(player);
 
 
 
 
 
 
 
 
181
  }
182
-
183
- player.inputSequence = sequence;
184
  }
185
 
186
- function usePlayerAbility(player) {
187
- const now = Date.now();
188
- if (now - player.lastAbility < player.abilityCooldown) return;
189
- if (player.energy < 30) return;
190
-
191
- player.lastAbility = now;
192
- player.energy -= 30;
193
-
194
- switch (player.ability) {
195
- case 'dash':
196
- player.vx *= 4;
197
- player.vy *= 4;
198
- player.invincible = true;
199
- setTimeout(() => { player.invincible = false; }, 300);
200
- break;
201
- case 'shield':
202
- player.effects.shield = { duration: 3000, startTime: now };
203
- player.invincible = true;
204
- setTimeout(() => { player.invincible = false; }, 3000);
205
- break;
206
- case 'charge':
207
- const chargeSpeed = 15;
208
- player.vx = Math.cos(player.angle) * chargeSpeed;
209
- player.vy = Math.sin(player.angle) * chargeSpeed;
210
- player.effects.charging = { duration: 500, startTime: now, damage: player.damage * 2 };
211
- break;
212
- case 'blast':
213
- // Area damage
214
- gameState.players.forEach((target) => {
215
- if (target.id !== player.id && target.isAlive) {
216
- const dist = distance(player, target);
217
- if (dist < 150) {
218
- const damage = player.damage * 2 * (1 - dist / 150);
219
- dealDamage(target, damage, player);
220
- }
221
- }
222
- });
223
- break;
224
- case 'rage':
225
- player.effects.rage = { duration: 5000, startTime: now };
226
- player.damage = player.baseDamage * 1.5;
227
- player.speed = player.baseSpeed * 1.3;
228
- setTimeout(() => {
229
- player.damage = player.baseDamage;
230
- player.speed = player.baseSpeed;
231
- }, 5000);
232
- break;
233
  }
234
- }
235
 
236
- function dealDamage(target, amount, attacker) {
237
- if (target.invincible || !target.isAlive) return;
238
-
239
- target.health -= amount;
240
-
241
- if (target.health <= 0) {
242
- killPlayer(target, attacker);
243
  }
 
 
244
  }
245
 
246
- function killPlayer(victim, killer) {
247
- victim.isAlive = false;
248
- victim.deaths++;
249
- victim.killStreak = 0;
250
- victim.respawnTime = Date.now() + 3000;
 
251
 
252
- if (killer && killer.id !== victim.id) {
253
- const streakBonus = Math.min(killer.killStreak, 10) * 10;
254
- const sizeBonus = Math.floor(victim.size - victim.baseSize);
255
- const killScore = 100 + streakBonus + sizeBonus;
 
 
 
 
 
 
 
 
 
256
 
257
- killer.kills++;
258
- killer.killStreak++;
259
- killer.maxKillStreak = Math.max(killer.maxKillStreak, killer.killStreak);
260
- killer.score += killScore;
261
- killer.size = Math.min(killer.size + 2, killer.baseSize * 2);
262
- killer.health = Math.min(killer.health + 20, killer.maxHealth);
 
 
 
 
 
 
 
 
 
 
 
 
263
 
264
- // Kill feed
265
- gameState.killFeed.unshift({
266
- killer: killer.username,
267
- victim: victim.username,
268
- time: Date.now()
 
269
  });
270
- if (gameState.killFeed.length > 5) gameState.killFeed.pop();
271
- }
272
-
273
- // Drop orbs
274
- const orbsToDrop = Math.min(Math.floor(victim.score / 50), 10);
275
- for (let i = 0; i < orbsToDrop; i++) {
276
- gameState.orbs.push({
277
- id: `orb_${Date.now()}_${i}`,
278
- x: victim.x + (Math.random() - 0.5) * 50,
279
- y: victim.y + (Math.random() - 0.5) * 50,
280
- value: 2,
281
- radius: 8
282
  });
283
- }
284
- }
285
 
286
- function respawnPlayer(player) {
287
- const classData = CLASSES[player.class];
288
- player.isAlive = true;
289
- player.health = classData.health;
290
- player.energy = 100;
291
- player.size = classData.size;
292
- player.x = gameState.safeZoneCenter.x + (Math.random() - 0.5) * gameState.safeZoneRadius;
293
- player.y = gameState.safeZoneCenter.y + (Math.random() - 0.5) * gameState.safeZoneRadius;
294
- player.vx = 0;
295
- player.vy = 0;
296
- player.invincible = true;
297
- player.effects = {};
298
- setTimeout(() => { player.invincible = false; }, 2000);
299
- }
300
 
301
- function updatePlayer(player, deltaTime) {
302
- if (!player.isAlive) {
303
- if (Date.now() >= player.respawnTime) {
304
- respawnPlayer(player);
 
 
305
  }
306
- return;
307
- }
308
-
309
- // Apply velocity
310
- player.x += player.vx;
311
- player.y += player.vy;
312
-
313
- // Friction
314
- player.vx *= 0.95;
315
- player.vy *= 0.95;
316
-
317
- // Boundary check
318
- player.x = Math.max(player.size, Math.min(MAP_SIZE - player.size, player.x));
319
- player.y = Math.max(player.size, Math.min(MAP_SIZE - player.size, player.y));
320
-
321
- // Energy regeneration
322
- player.energy = Math.min(player.energy + 0.2, player.maxEnergy);
323
-
324
- // Safe zone damage
325
- const distFromCenter = distance(player, gameState.safeZoneCenter);
326
- if (distFromCenter > gameState.safeZoneRadius) {
327
- const overDistance = distFromCenter - gameState.safeZoneRadius;
328
- player.health -= Math.min(overDistance * 0.01, 2);
329
- if (player.health <= 0) {
330
- killPlayer(player, null);
331
  }
332
- }
333
-
334
- // Collision with obstacles
335
- gameState.obstacles.forEach(obs => {
336
- if (circleCollision(player, obs, player.size, obs.radius)) {
337
- const angle = Math.atan2(player.y - obs.y, player.x - obs.x);
338
- const overlap = player.size + obs.radius - distance(player, obs);
339
-
340
- if (obs.type === 'bounce') {
341
- player.vx = Math.cos(angle) * 10;
342
- player.vy = Math.sin(angle) * 10;
343
- } else {
344
- player.x += Math.cos(angle) * overlap;
345
- player.y += Math.sin(angle) * overlap;
346
- }
347
  }
348
- });
349
-
350
- // Auto-attack nearby enemies
351
- const now = Date.now();
352
- if (now - player.lastAttack > player.attackCooldown) {
353
- let closestEnemy = null;
354
- let closestDist = player.size + 50;
355
-
356
- gameState.players.forEach((target) => {
357
- if (target.id !== player.id && target.isAlive) {
358
- const dist = distance(player, target);
359
- if (dist < closestDist) {
360
- closestDist = dist;
361
- closestEnemy = target;
362
- }
363
- }
364
- });
365
-
366
- if (closestEnemy) {
367
- player.lastAttack = now;
368
- dealDamage(closestEnemy, player.damage, player);
369
  }
370
- }
371
-
372
- // Collect orbs
373
- const magnetRange = player.effects.magnet ? 150 : 0;
374
- gameState.orbs = gameState.orbs.filter(orb => {
375
- const dist = distance(player, orb);
376
-
377
- // Magnet effect
378
- if (magnetRange > 0 && dist < magnetRange) {
379
- const angle = Math.atan2(player.y - orb.y, player.x - orb.x);
380
- orb.x += Math.cos(angle) * 5;
381
- orb.y += Math.sin(angle) * 5;
382
  }
383
-
384
- if (circleCollision(player, orb, player.size, orb.radius)) {
385
- player.score += orb.value * 10;
386
- player.orbsCollected++;
387
- player.size = Math.min(player.size + orb.value * 0.5, player.baseSize * 2);
388
- return false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
  }
390
- return true;
391
- });
392
-
393
- // Collect powerups
394
- gameState.powerups = gameState.powerups.filter(pow => {
395
- if (circleCollision(player, pow, player.size, pow.radius)) {
396
- applyPowerup(player, pow.type);
397
- return false;
 
398
  }
399
- return true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
400
  });
401
-
402
- // Update effects
403
- Object.keys(player.effects).forEach(effect => {
404
- if (now - player.effects[effect].startTime > player.effects[effect].duration) {
405
- delete player.effects[effect];
406
- }
407
  });
408
- }
409
 
410
- function applyPowerup(player, type) {
411
- const now = Date.now();
412
- const duration = 10000;
413
-
414
- switch (type) {
415
- case 'speed':
416
- player.speed = player.baseSpeed * 1.5;
417
- player.effects.speed = { duration, startTime: now };
418
- setTimeout(() => { player.speed = player.baseSpeed; }, duration);
419
- break;
420
- case 'damage':
421
- player.damage = player.baseDamage * 1.5;
422
- player.effects.damage = { duration, startTime: now };
423
- setTimeout(() => { player.damage = player.baseDamage; }, duration);
424
- break;
425
- case 'health':
426
- player.health = Math.min(player.health + 50, player.maxHealth);
427
- break;
428
- case 'shield':
429
- player.effects.shield = { duration: 5000, startTime: now };
430
- player.invincible = true;
431
- setTimeout(() => { player.invincible = false; }, 5000);
432
- break;
433
- case 'size':
434
- player.size = Math.min(player.size + 10, player.baseSize * 2);
435
- break;
436
- case 'magnet':
437
- player.effects.magnet = { duration, startTime: now };
438
- break;
439
- case 'ghost':
440
- player.effects.ghost = { duration: 5000, startTime: now };
441
- break;
442
- case 'rapid':
443
- player.attackCooldown = 250;
444
- player.effects.rapid = { duration, startTime: now };
445
- setTimeout(() => { player.attackCooldown = 500; }, duration);
446
- break;
447
- }
448
- }
449
 
450
- function updateGame() {
451
- const now = Date.now();
452
- gameState.gameTime++;
453
-
454
- // Shrink safe zone
455
- if (gameState.safeZoneRadius > SAFE_ZONE_MIN && gameState.gameTime % 100 === 0) {
456
- gameState.safeZoneRadius -= DANGER_ZONE_SHRINK_RATE;
457
- }
458
-
459
  // Update players
460
- gameState.players.forEach(player => {
461
- updatePlayer(player, 1 / TICK_RATE);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
462
  });
463
-
464
- // Respawn orbs
465
- while (gameState.orbs.length < ORB_COUNT) {
466
- spawnOrb();
467
- }
468
-
469
- // Respawn powerups
470
- while (gameState.powerups.length < POWERUP_COUNT) {
471
- spawnPowerup();
472
- }
473
-
474
- // Send game state to all players
475
- const playersArray = Array.from(gameState.players.values()).map(p => ({
476
  id: p.id,
477
  username: p.username,
478
- class: p.class,
479
- x: Math.round(p.x),
480
- y: Math.round(p.y),
481
- vx: Math.round(p.vx * 100) / 100,
482
- vy: Math.round(p.vy * 100) / 100,
483
- angle: Math.round(p.angle * 100) / 100,
484
- health: Math.round(p.health),
485
- maxHealth: p.maxHealth,
486
- energy: Math.round(p.energy),
487
- size: Math.round(p.size),
488
  score: p.score,
489
- kills: p.kills,
490
- killStreak: p.killStreak,
491
- color: p.color,
492
- isAlive: p.isAlive,
493
- invincible: p.invincible,
494
- effects: Object.keys(p.effects),
495
- seq: p.inputSequence
496
  }));
497
-
498
- const leaderboard = playersArray
499
- .filter(p => p.isAlive)
500
- .sort((a, b) => b.score - a.score)
501
- .slice(0, 10)
502
- .map((p, i) => ({ rank: i + 1, username: p.username, score: p.score, kills: p.kills }));
503
-
504
- const gameUpdate = {
505
- players: playersArray,
506
- orbs: gameState.orbs.map(o => ({ id: o.id, x: Math.round(o.x), y: Math.round(o.y), v: o.value })),
507
- powerups: gameState.powerups.map(p => ({ id: p.id, x: Math.round(p.x), y: Math.round(p.y), t: p.type })),
508
- obstacles: gameState.obstacles,
509
- safeZone: {
510
- x: Math.round(gameState.safeZoneCenter.x),
511
- y: Math.round(gameState.safeZoneCenter.y),
512
- r: Math.round(gameState.safeZoneRadius)
513
- },
514
- killFeed: gameState.killFeed,
515
- leaderboard,
516
- time: now,
517
- playerCount: gameState.players.size
518
- };
519
-
520
- io.emit('gameState', gameUpdate);
521
- }
522
 
523
- // Socket.io handling
524
- io.on('connection', (socket) => {
525
- console.log(`Player connected: ${socket.id}`);
526
-
527
- socket.on('join', (data) => {
528
- if (gameState.players.size >= MAX_PLAYERS) {
529
- socket.emit('error', { message: 'Server full!' });
530
- return;
531
- }
532
-
533
- const { username, playerClass } = data;
534
- if (!username || username.trim().length < 1) {
535
- socket.emit('error', { message: 'Invalid username!' });
536
- return;
537
  }
538
-
539
- const player = createPlayer(socket.id, username.trim(), playerClass || 'WARRIOR');
540
- gameState.players.set(socket.id, player);
541
-
542
- socket.emit('joined', {
543
- id: socket.id,
544
- mapSize: MAP_SIZE,
545
- classes: CLASSES
546
- });
547
-
548
- console.log(`${username} joined as ${playerClass}`);
549
  });
550
-
551
- socket.on('input', (input) => {
552
- const player = gameState.players.get(socket.id);
553
- if (player) {
554
- processPlayerInput(player, input);
555
- }
556
  });
557
-
558
- socket.on('ping', (startTime) => {
559
- socket.emit('pong', startTime);
 
 
 
 
 
560
  });
561
-
562
- socket.on('disconnect', () => {
563
- const player = gameState.players.get(socket.id);
564
- if (player) {
565
- console.log(`${player.username} disconnected`);
 
566
  }
567
- gameState.players.delete(socket.id);
568
  });
569
- });
570
-
571
- // Initialize and start
572
- initializeGame();
573
- setInterval(updateGame, 1000 / TICK_RATE);
574
 
575
  const PORT = process.env.PORT || 7860;
576
  server.listen(PORT, '0.0.0.0', () => {
 
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')));
18
 
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;
36
 
37
+ if (index <= 4) {
38
+ type = 'grass';
39
+ } else if (prevLane) {
40
+ const r = Math.random();
41
+
42
+ if (prevLane.type === 'grass') {
43
+ if (r < 0.6) type = 'road';
44
+ else if (r < 0.9) type = 'water';
45
+ else type = 'grass';
46
+ } else if (prevLane.type === 'road') {
47
+ if (prevLane.runLength < 4 && r < 0.7) {
48
+ type = 'road';
49
+ runLength = prevLane.runLength + 1;
50
+ } else {
51
+ type = 'grass';
52
+ }
53
+ } else if (prevLane.type === 'water') {
54
+ if (prevLane.runLength < 2 && r < 0.6) {
55
+ type = 'water';
56
+ runLength = prevLane.runLength + 1;
57
+ } else {
58
+ type = 'grass';
59
+ }
60
+ }
61
  }
62
+
63
+ const staticObstacles = [];
64
+ if (type === 'grass') {
65
+ const treeCount = Math.floor(Math.random() * 3);
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
87
+ if (type === 'road' || type === 'water') {
88
+ const count = Math.ceil(Math.random() * 2);
89
+ for (let i = 0; i < count; i++) {
90
+ let randX = (Math.random() * 120) - 60;
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
  }
106
+
107
+ return lane;
108
  }
109
 
110
+ // Initialize lanes
111
+ function initializeLanes() {
112
+ gameState.lanes.clear();
113
+ for (let i = -4; i < 15; i++) {
114
+ const prevLane = gameState.lanes.get(i - 1);
115
+ gameState.lanes.set(i, generateLaneData(i, prevLane));
116
+ }
117
+ gameState.furthestLaneIndex = 14;
 
118
  }
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', () => {