OrbitMC commited on
Commit
25da2a2
·
verified ·
1 Parent(s): ca6391a

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +536 -160
server.js CHANGED
@@ -1,202 +1,578 @@
1
  const express = require('express');
2
  const http = require('http');
3
  const { Server } = require('socket.io');
 
4
 
5
  const app = express();
6
  const server = http.createServer(app);
7
  const io = new Server(server, {
8
  cors: { origin: "*" },
9
- pingTimeout: 60000,
 
 
10
  });
11
 
12
- app.use(express.static('public'));
13
-
14
- // --- CONFIGURATION ---
15
- const PLAYER_SIZE = 20;
16
- // Growth rate (pixels per ms).
17
- // Must match Client exactly for prediction to work.
18
- // 3px per frame (at 60fps) ~= 0.18px per ms
19
- const GROWTH_SPEED = 0.18;
20
-
21
- // --- ACCESS CODES ---
22
- // In HuggingFace, add a secret named 'GAME_CODES'
23
- // For now, we generate them in memory if not found.
24
- let VALID_CODES = new Set();
25
- const FIXED_CODE = "676900";
26
-
27
- function loadCodes() {
28
- if (process.env.GAME_CODES) {
29
- // Expecting comma separated codes
30
- const codes = process.env.GAME_CODES.split(',');
31
- codes.forEach(c => VALID_CODES.add(c.trim()));
32
- } else {
33
- // Generate 29 random codes + fixed code
34
- console.log("--- GENERATING NEW ACCESS CODES (Save these to Secrets) ---");
35
- VALID_CODES.add(FIXED_CODE);
36
- console.log(`1. ${FIXED_CODE} (Master)`);
37
- for(let i=1; i<30; i++) {
38
- const code = Math.floor(100000 + Math.random() * 900000).toString();
39
- VALID_CODES.add(code);
40
- console.log(`${i+1}. ${code}`);
41
- }
42
- console.log("-----------------------------------------------------------");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  }
44
  }
45
- loadCodes();
46
 
47
- // --- GAME STATE ---
48
- let platforms = [];
49
- let players = {};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
- function generatePlatform(prevX, prevW) {
52
- const gap = 80 + Math.random() * 120;
53
- const width = 40 + Math.random() * 60;
54
  return {
55
- x: prevX + prevW + gap,
56
- w: width,
57
- id: Math.random().toString(36).substr(2, 9)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  };
59
  }
60
 
61
- // Init World
62
- platforms.push({ x: 50, w: 100, id: 'start' });
63
- for (let i = 0; i < 10; i++) {
64
- const last = platforms[platforms.length - 1];
65
- platforms.push(generatePlatform(last.x, last.w));
66
  }
67
 
68
- io.on('connection', (socket) => {
69
- let isAuthenticated = false;
 
70
 
71
- // 1. Login with Code
72
- socket.on('login', (data) => {
73
- const { name, code } = data;
74
-
75
- if (!VALID_CODES.has(code)) {
76
- socket.emit('auth_error', 'Invalid Access Code');
77
- return;
78
- }
79
 
80
- isAuthenticated = true;
81
- const cleanName = (name || "Guest").substring(0, 12);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
- // Init Player
84
- players[socket.id] = {
85
- id: socket.id,
86
- name: cleanName,
87
- score: 0,
88
- x: platforms[0].x + platforms[0].w - PLAYER_SIZE - 5,
89
- currentPlatIndex: 0,
90
- growStartTime: 0,
91
- isGrowing: false
92
- };
93
-
94
- // Send Success & World State
95
- socket.emit('init', {
96
- id: socket.id,
97
- platforms: platforms,
98
- players: players
99
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
 
101
- // Broadcast to others
102
- socket.broadcast.emit('player_join', players[socket.id]);
103
- });
 
 
 
 
 
 
104
 
105
- // 2. Start Growing (Timestamp Authoritative)
106
- socket.on('start_grow', () => {
107
- if (!isAuthenticated || !players[socket.id]) return;
108
- const p = players[socket.id];
 
 
 
 
 
 
109
 
110
- p.isGrowing = true;
111
- p.growStartTime = Date.now();
 
 
 
 
112
 
113
- // Relay to others for visual interpolation
114
- socket.broadcast.emit('player_action', { id: socket.id, type: 'grow_start' });
115
- });
116
-
117
- // 3. Stop Growing (The Physics Check)
118
- socket.on('stop_grow', () => {
119
- if (!isAuthenticated || !players[socket.id]) return;
120
- const p = players[socket.id];
121
- if (!p.isGrowing) return;
122
-
123
- const duration = Date.now() - p.growStartTime;
124
- p.isGrowing = false;
125
-
126
- // Calculate Authoritative Height
127
- // Cap max height to prevent exploit
128
- let stickHeight = Math.min(duration * GROWTH_SPEED, 2000);
129
-
130
- // Notify others immediately of the result so they see the animation
131
- socket.broadcast.emit('player_action', { id: socket.id, type: 'grow_stop', height: stickHeight });
 
 
132
 
133
- // --- SERVER PHYSICS CHECK ---
134
- const currentPlat = platforms[p.currentPlatIndex];
135
- const nextPlat = platforms[p.currentPlatIndex + 1];
 
 
 
 
 
 
 
 
 
 
 
136
 
137
- // Generate map if running out
138
- if (!nextPlat) {
139
- const last = platforms[platforms.length - 1];
140
- const newPlat = generatePlatform(last.x, last.w);
141
- platforms.push(newPlat);
142
- io.emit('new_platform', newPlat);
143
  }
144
-
145
- const stickEnd = currentPlat.x + currentPlat.w + stickHeight;
146
- const safeStart = nextPlat.x;
147
- const safeEnd = nextPlat.x + nextPlat.w;
148
- const perfectCenter = nextPlat.x + (nextPlat.w / 2);
149
-
150
- let success = false;
151
- let perfect = false;
152
-
153
- if (stickEnd >= safeStart && stickEnd <= safeEnd) {
154
- success = true;
155
- // Check for perfect center hit (optional bonus logic)
156
- if (Math.abs(stickEnd - perfectCenter) < 5) perfect = true;
 
 
 
 
 
 
 
 
 
 
 
 
157
  }
158
-
159
- // Wait for animation duration (simulated) then update state
160
- setTimeout(() => {
161
- if (success) {
162
- p.score += perfect ? 2 : 1;
163
- p.currentPlatIndex++;
164
- p.x = nextPlat.x + nextPlat.w - PLAYER_SIZE - 5; // Target X
165
-
166
- // Infinite Gen
167
- if (platforms.length < p.currentPlatIndex + 5) {
168
- const last = platforms[platforms.length - 1];
169
- const newPlat = generatePlatform(last.x, last.w);
170
- platforms.push(newPlat);
171
- io.emit('new_platform', newPlat);
172
- }
173
  } else {
174
- p.score = 0;
175
- p.currentPlatIndex = 0;
176
- p.x = platforms[0].x + platforms[0].w - PLAYER_SIZE - 5; // Reset X
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
 
179
- // RECONCILIATION PACKET
180
- // We tell everyone the result.
181
- // The local client will compare this. If they predicted correctly, nothing happens visually.
182
- // If they cheated, this packet will snap them back to reality.
183
- io.emit('player_result', {
184
- id: socket.id,
185
- success: success,
186
- score: p.score,
187
- x: p.x
188
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
 
190
- }, 1000); // 1s represents the fall/walk animation time
 
 
 
 
 
 
 
 
 
 
 
191
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  socket.on('disconnect', () => {
194
- if (players[socket.id]) {
195
- delete players[socket.id];
196
- io.emit('player_leave', socket.id);
197
  }
 
198
  });
199
  });
200
 
201
- const PORT = process.env.PORT || 3000;
202
- server.listen(PORT, () => console.log(`Server running on port ${PORT}`));
 
 
 
 
 
 
 
1
  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: { 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', () => {
577
+ console.log(`Server running on port ${PORT}`);
578
+ });