OrbitMC commited on
Commit
a689c1d
·
verified ·
1 Parent(s): 46e2226

Create server.js

Browse files
Files changed (1) hide show
  1. server.js +434 -0
server.js ADDED
@@ -0,0 +1,434 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const http = require('http');
3
+ const { Server } = require('socket.io');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const app = express();
8
+ const server = http.createServer(app);
9
+ const io = new Server(server, {
10
+ cors: { origin: "*" },
11
+ pingInterval: 2000,
12
+ pingTimeout: 5000
13
+ });
14
+
15
+ app.use(express.static('public'));
16
+
17
+ // Game constants
18
+ const TICK_RATE = 60;
19
+ const TICK_INTERVAL = 1000 / TICK_RATE;
20
+ const GAME_WIDTH = 540;
21
+ const GAME_HEIGHT = 960;
22
+ const GRAVITY = 0.6;
23
+ const JUMP_FORCE = -17;
24
+ const SPRING_FORCE = -26;
25
+ const PLAYER_WIDTH = 40;
26
+ const PLAYER_HEIGHT = 50;
27
+ const PLATFORM_WIDTH = 70;
28
+ const PLATFORM_HEIGHT = 15;
29
+ const MOVE_SPEED = 8;
30
+ const MAX_FALL_SPEED = 20;
31
+
32
+ // Leaderboard file path
33
+ const LEADERBOARD_PATH = process.env.NODE_ENV === 'production'
34
+ ? '/app/data/leaderboard.json'
35
+ : './leaderboard.json';
36
+
37
+ // Seeded random number generator for deterministic platform generation
38
+ class SeededRandom {
39
+ constructor(seed) {
40
+ this.seed = seed % 2147483647;
41
+ if (this.seed <= 0) this.seed += 2147483646;
42
+ }
43
+
44
+ next() {
45
+ this.seed = (this.seed * 16807) % 2147483647;
46
+ return (this.seed - 1) / 2147483646;
47
+ }
48
+
49
+ nextRange(min, max) {
50
+ return min + this.next() * (max - min);
51
+ }
52
+
53
+ nextInt(min, max) {
54
+ return Math.floor(this.nextRange(min, max));
55
+ }
56
+ }
57
+
58
+ // Global game seed
59
+ const WORLD_SEED = 12345;
60
+ const CHUNK_SIZE = 500;
61
+
62
+ // Platform types
63
+ const PLATFORM_NORMAL = 0;
64
+ const PLATFORM_MOVING = 1;
65
+ const PLATFORM_BREAKABLE = 2;
66
+ const PLATFORM_SPRING = 3;
67
+
68
+ // Generate platforms for a chunk
69
+ function generateChunk(chunkIndex) {
70
+ const rng = new SeededRandom(WORLD_SEED + chunkIndex * 7919);
71
+ const platforms = [];
72
+ const chunkTop = -chunkIndex * CHUNK_SIZE;
73
+ const chunkBottom = chunkTop + CHUNK_SIZE;
74
+
75
+ // Difficulty scaling
76
+ const difficulty = Math.min(chunkIndex / 20, 1);
77
+ const platformCount = Math.max(4, Math.floor(8 - difficulty * 3));
78
+ const verticalSpacing = CHUNK_SIZE / platformCount;
79
+
80
+ for (let i = 0; i < platformCount; i++) {
81
+ const y = chunkTop + i * verticalSpacing + rng.nextRange(10, verticalSpacing - 30);
82
+ const x = rng.nextRange(20, GAME_WIDTH - PLATFORM_WIDTH - 20);
83
+
84
+ // Determine platform type
85
+ let type = PLATFORM_NORMAL;
86
+ const typeRoll = rng.next();
87
+
88
+ if (chunkIndex > 2) {
89
+ if (typeRoll < 0.15 * difficulty) {
90
+ type = PLATFORM_BREAKABLE;
91
+ } else if (typeRoll < 0.3 * difficulty) {
92
+ type = PLATFORM_MOVING;
93
+ } else if (typeRoll < 0.1 + 0.1 * difficulty) {
94
+ type = PLATFORM_SPRING;
95
+ }
96
+ }
97
+
98
+ platforms.push({
99
+ id: `${chunkIndex}_${i}`,
100
+ x: x,
101
+ y: y,
102
+ width: PLATFORM_WIDTH,
103
+ height: PLATFORM_HEIGHT,
104
+ type: type,
105
+ broken: false,
106
+ moveDir: type === PLATFORM_MOVING ? (rng.next() > 0.5 ? 1 : -1) : 0,
107
+ moveSpeed: type === PLATFORM_MOVING ? rng.nextRange(1.5, 3) : 0,
108
+ originalX: x
109
+ });
110
+ }
111
+
112
+ return platforms;
113
+ }
114
+
115
+ // Game state
116
+ const players = new Map();
117
+ const platformCache = new Map();
118
+ const brokenPlatforms = new Set();
119
+
120
+ // Get platforms for a range
121
+ function getPlatformsInRange(minY, maxY) {
122
+ const minChunk = Math.floor(-maxY / CHUNK_SIZE);
123
+ const maxChunk = Math.floor(-minY / CHUNK_SIZE) + 1;
124
+ const platforms = [];
125
+
126
+ for (let i = Math.max(0, minChunk); i <= maxChunk; i++) {
127
+ if (!platformCache.has(i)) {
128
+ platformCache.set(i, generateChunk(i));
129
+ }
130
+ platforms.push(...platformCache.get(i));
131
+ }
132
+
133
+ return platforms.filter(p => !brokenPlatforms.has(p.id));
134
+ }
135
+
136
+ // Load leaderboard
137
+ function loadLeaderboard() {
138
+ try {
139
+ if (fs.existsSync(LEADERBOARD_PATH)) {
140
+ const data = fs.readFileSync(LEADERBOARD_PATH, 'utf8');
141
+ return JSON.parse(data);
142
+ }
143
+ } catch (e) {
144
+ console.log('Creating new leaderboard');
145
+ }
146
+ return [];
147
+ }
148
+
149
+ // Save leaderboard
150
+ function saveLeaderboard(leaderboard) {
151
+ try {
152
+ const dir = path.dirname(LEADERBOARD_PATH);
153
+ if (!fs.existsSync(dir)) {
154
+ fs.mkdirSync(dir, { recursive: true });
155
+ }
156
+ fs.writeFileSync(LEADERBOARD_PATH, JSON.stringify(leaderboard, null, 2));
157
+ } catch (e) {
158
+ console.error('Failed to save leaderboard:', e);
159
+ }
160
+ }
161
+
162
+ let leaderboard = loadLeaderboard();
163
+
164
+ // Player class
165
+ class Player {
166
+ constructor(id, username) {
167
+ this.id = id;
168
+ this.username = username;
169
+ this.x = GAME_WIDTH / 2 - PLAYER_WIDTH / 2;
170
+ this.y = 0;
171
+ this.vx = 0;
172
+ this.vy = 0;
173
+ this.score = 0;
174
+ this.highestY = 0;
175
+ this.input = { left: false, right: false };
176
+ this.lastProcessedInput = 0;
177
+ this.alive = true;
178
+ this.color = this.generateColor();
179
+ this.facingRight = true;
180
+ }
181
+
182
+ generateColor() {
183
+ const hue = Math.random() * 360;
184
+ return `hsl(${hue}, 70%, 60%)`;
185
+ }
186
+
187
+ update(platforms) {
188
+ if (!this.alive) return;
189
+
190
+ // Apply input
191
+ if (this.input.left) {
192
+ this.vx = -MOVE_SPEED;
193
+ this.facingRight = false;
194
+ } else if (this.input.right) {
195
+ this.vx = MOVE_SPEED;
196
+ this.facingRight = true;
197
+ } else {
198
+ this.vx *= 0.85;
199
+ }
200
+
201
+ // Apply gravity
202
+ this.vy += GRAVITY;
203
+ this.vy = Math.min(this.vy, MAX_FALL_SPEED);
204
+
205
+ // Update position
206
+ this.x += this.vx;
207
+ this.y += this.vy;
208
+
209
+ // Screen wrap
210
+ if (this.x < -PLAYER_WIDTH) {
211
+ this.x = GAME_WIDTH;
212
+ } else if (this.x > GAME_WIDTH) {
213
+ this.x = -PLAYER_WIDTH;
214
+ }
215
+
216
+ // Platform collision (only when falling)
217
+ if (this.vy > 0) {
218
+ for (const platform of platforms) {
219
+ if (platform.broken) continue;
220
+
221
+ if (this.checkPlatformCollision(platform)) {
222
+ if (platform.type === PLATFORM_BREAKABLE) {
223
+ platform.broken = true;
224
+ brokenPlatforms.add(platform.id);
225
+ }
226
+
227
+ this.y = platform.y - PLAYER_HEIGHT;
228
+ this.vy = platform.type === PLATFORM_SPRING ? SPRING_FORCE : JUMP_FORCE;
229
+ break;
230
+ }
231
+ }
232
+ }
233
+
234
+ // Update score
235
+ const newScore = Math.floor(-this.y / 10);
236
+ if (newScore > this.score) {
237
+ this.score = newScore;
238
+ if (this.y < this.highestY) {
239
+ this.highestY = this.y;
240
+ }
241
+ }
242
+
243
+ // Check death (fell below screen)
244
+ const cameraY = this.highestY - GAME_HEIGHT * 0.4;
245
+ if (this.y > cameraY + GAME_HEIGHT + 100) {
246
+ this.alive = false;
247
+ }
248
+ }
249
+
250
+ checkPlatformCollision(platform) {
251
+ const playerBottom = this.y + PLAYER_HEIGHT;
252
+ const playerPrevBottom = playerBottom - this.vy;
253
+
254
+ return this.x + PLAYER_WIDTH > platform.x &&
255
+ this.x < platform.x + platform.width &&
256
+ playerPrevBottom <= platform.y &&
257
+ playerBottom >= platform.y &&
258
+ playerBottom <= platform.y + platform.height + 10;
259
+ }
260
+
261
+ reset() {
262
+ this.x = GAME_WIDTH / 2 - PLAYER_WIDTH / 2;
263
+ this.y = 0;
264
+ this.vx = 0;
265
+ this.vy = 0;
266
+ this.score = 0;
267
+ this.highestY = 0;
268
+ this.alive = true;
269
+ }
270
+
271
+ getState() {
272
+ return {
273
+ id: this.id,
274
+ username: this.username,
275
+ x: this.x,
276
+ y: this.y,
277
+ vx: this.vx,
278
+ vy: this.vy,
279
+ score: this.score,
280
+ alive: this.alive,
281
+ color: this.color,
282
+ facingRight: this.facingRight,
283
+ lastProcessedInput: this.lastProcessedInput
284
+ };
285
+ }
286
+ }
287
+
288
+ // Socket.io connection handling
289
+ io.on('connection', (socket) => {
290
+ console.log(`Player connected: ${socket.id}`);
291
+
292
+ socket.on('join', (data) => {
293
+ const username = (data.username || 'Player').substring(0, 16);
294
+ const player = new Player(socket.id, username);
295
+ players.set(socket.id, player);
296
+
297
+ // Send initial state
298
+ socket.emit('init', {
299
+ playerId: socket.id,
300
+ worldSeed: WORLD_SEED,
301
+ chunkSize: CHUNK_SIZE,
302
+ leaderboard: leaderboard.slice(0, 10)
303
+ });
304
+
305
+ console.log(`${username} joined the game`);
306
+ });
307
+
308
+ socket.on('input', (data) => {
309
+ const player = players.get(socket.id);
310
+ if (player && player.alive) {
311
+ player.input = {
312
+ left: data.left || false,
313
+ right: data.right || false
314
+ };
315
+ player.lastProcessedInput = data.seq || 0;
316
+ }
317
+ });
318
+
319
+ socket.on('restart', () => {
320
+ const player = players.get(socket.id);
321
+ if (player) {
322
+ // Save score to leaderboard
323
+ if (player.score > 0) {
324
+ const entry = {
325
+ username: player.username,
326
+ score: player.score,
327
+ date: Date.now()
328
+ };
329
+
330
+ leaderboard.push(entry);
331
+ leaderboard.sort((a, b) => b.score - a.score);
332
+ leaderboard = leaderboard.slice(0, 100);
333
+ saveLeaderboard(leaderboard);
334
+
335
+ io.emit('leaderboard', leaderboard.slice(0, 10));
336
+ }
337
+
338
+ player.reset();
339
+ }
340
+ });
341
+
342
+ socket.on('disconnect', () => {
343
+ const player = players.get(socket.id);
344
+ if (player) {
345
+ // Save score
346
+ if (player.score > 0) {
347
+ const entry = {
348
+ username: player.username,
349
+ score: player.score,
350
+ date: Date.now()
351
+ };
352
+
353
+ leaderboard.push(entry);
354
+ leaderboard.sort((a, b) => b.score - a.score);
355
+ leaderboard = leaderboard.slice(0, 100);
356
+ saveLeaderboard(leaderboard);
357
+ }
358
+ }
359
+ players.delete(socket.id);
360
+ console.log(`Player disconnected: ${socket.id}`);
361
+ });
362
+ });
363
+
364
+ // Update moving platforms
365
+ function updatePlatforms() {
366
+ for (const [, platforms] of platformCache) {
367
+ for (const platform of platforms) {
368
+ if (platform.type === PLATFORM_MOVING && !platform.broken) {
369
+ platform.x += platform.moveSpeed * platform.moveDir;
370
+
371
+ if (platform.x <= 20 || platform.x >= GAME_WIDTH - PLATFORM_WIDTH - 20) {
372
+ platform.moveDir *= -1;
373
+ }
374
+ }
375
+ }
376
+ }
377
+ }
378
+
379
+ // Game loop
380
+ let lastTick = Date.now();
381
+
382
+ function gameLoop() {
383
+ const now = Date.now();
384
+ const delta = now - lastTick;
385
+
386
+ if (delta >= TICK_INTERVAL) {
387
+ lastTick = now - (delta % TICK_INTERVAL);
388
+
389
+ // Update platforms
390
+ updatePlatforms();
391
+
392
+ // Update all players
393
+ for (const [, player] of players) {
394
+ if (player.alive) {
395
+ const platforms = getPlatformsInRange(
396
+ player.y - GAME_HEIGHT,
397
+ player.y + GAME_HEIGHT
398
+ );
399
+ player.update(platforms);
400
+ }
401
+ }
402
+
403
+ // Broadcast game state
404
+ const gameState = {
405
+ timestamp: now,
406
+ players: Array.from(players.values()).map(p => p.getState()),
407
+ brokenPlatforms: Array.from(brokenPlatforms)
408
+ };
409
+
410
+ io.emit('state', gameState);
411
+ }
412
+
413
+ setImmediate(gameLoop);
414
+ }
415
+
416
+ // Cleanup old platform cache periodically
417
+ setInterval(() => {
418
+ const activeMinChunk = Math.min(...Array.from(players.values()).map(p =>
419
+ Math.floor(-p.y / CHUNK_SIZE) - 2
420
+ ));
421
+
422
+ for (const [chunk] of platformCache) {
423
+ if (chunk < activeMinChunk - 5) {
424
+ platformCache.delete(chunk);
425
+ }
426
+ }
427
+ }, 10000);
428
+
429
+ // Start server
430
+ const PORT = process.env.PORT || 7860;
431
+ server.listen(PORT, '0.0.0.0', () => {
432
+ console.log(`Server running on port ${PORT}`);
433
+ gameLoop();
434
+ });