OrbitMC commited on
Commit
4b7180e
·
verified ·
1 Parent(s): cc962ce

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +2168 -365
app.py CHANGED
@@ -5,79 +5,611 @@ from flask import Flask, render_template_string, request
5
  from flask_socketio import SocketIO, emit
6
  import random
7
  import time
 
8
 
9
  # --- SERVER CONFIGURATION ---
10
  WIDTH = 375
11
  HEIGHT = 812
12
  app = Flask(__name__)
13
- app.config['SECRET_KEY'] = 'jungle_jump_secret'
14
  socketio = SocketIO(app, async_mode='eventlet', cors_allowed_origins='*')
15
 
16
  # --- GAME STATE ---
17
- players = {} # {sid: {name, x, y, score, faceRight, vx}}
18
- platforms = [] # Shared map
 
 
19
  highest_y_generated = HEIGHT
 
20
 
21
- # Generate initial world (Start with 100 platforms)
22
- def generate_platform(y_pos):
23
- # REMOVED 'break' type as requested
24
  p_type = 'normal'
25
  vx = 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
- # Small chance for moving platform
28
- if random.random() < 0.2:
29
- vx = 2 if random.random() > 0.5 else -2
30
-
31
  return {
32
- 'id': int(time.time() * 1000) + random.randint(0,1000),
33
- 'x': random.random() * (WIDTH - 60),
34
  'y': y_pos,
35
- 'w': 60,
36
- 'h': 15,
37
  'type': p_type,
38
  'vx': vx,
39
- 'broken': False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  }
41
 
42
  def init_world():
43
- global platforms, highest_y_generated
44
  platforms = []
 
 
 
45
  # Base platform
46
- platforms.append({'id': 0, 'x': WIDTH/2 - 30, 'y': HEIGHT - 150, 'w': 60, 'h': 15, 'type': 'normal', 'vx': 0, 'broken': False})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
- # Generate up
49
- y = HEIGHT - 250
50
- for _ in range(200): # Pre-generate 200 platforms
51
- platforms.append(generate_platform(y))
52
- y -= (80 + random.random() * 40)
53
  highest_y_generated = y
54
 
55
  init_world()
56
 
57
- # --- HTML TEMPLATE (Your Game Code Modified) ---
58
  HTML_TEMPLATE = """
59
  <!DOCTYPE html>
60
  <html lang="en">
61
  <head>
62
  <meta charset="UTF-8">
63
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
64
- <title>Jungle Jump Multiplayer</title>
 
65
  <style>
66
- body { margin: 0; background-color: #222; display: flex; justify-content: center; align-items: center; height: 100vh; overflow: hidden; font-family: 'Comic Sans MS', cursive, sans-serif; touch-action: none; user-select: none; }
67
- #gameContainer { position: relative; width: 100%; height: 100%; max-width: 50vh; aspect-ratio: 9/16; background: #659d33; box-shadow: 0 0 20px rgba(0,0,0,0.5); overflow: hidden; }
68
- canvas { display: block; width: 100%; height: 100%; }
69
 
70
- /* UI Overlays */
71
- #loginScreen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9); z-index: 20; display: flex; flex-direction: column; justify-content: center; align-items: center; color: white; }
72
- #loginScreen input { padding: 15px; font-size: 20px; border-radius: 10px; border: none; text-align: center; margin-bottom: 20px; }
73
- #loginScreen button { padding: 15px 30px; font-size: 20px; background: #d8c222; border: none; border-radius: 10px; cursor: pointer; font-weight: bold; }
74
-
75
- #leaderboard { position: absolute; top: 10px; left: 10px; background: rgba(0,0,0,0.5); padding: 10px; border-radius: 8px; color: white; font-size: 12px; pointer-events: none; z-index: 5; }
76
- #score { position: absolute; top: 20px; right: 20px; font-size: 28px; font-weight: bold; color: #000; text-shadow: 1px 1px 0px rgba(255,255,255,0.5); z-index: 5; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
- #gameOverScreen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); display: none; flex-direction: column; justify-content: center; align-items: center; color: white; z-index: 10; }
79
- #gameOverScreen h1 { font-size: 40px; color: #d8c222; margin-bottom: 10px; }
80
- #restartBtn { padding: 15px 40px; font-size: 24px; background: #d8c222; border: 4px solid #000; border-radius: 15px; cursor: pointer; font-weight: bold; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  </style>
82
  </head>
83
  <body>
@@ -85,358 +617,1590 @@ HTML_TEMPLATE = """
85
  <div id="gameContainer">
86
  <canvas id="gameCanvas"></canvas>
87
 
 
88
  <div id="loginScreen">
89
- <h1>JUNGLE JUMP ONLINE</h1>
90
- <input type="text" id="usernameInput" placeholder="Enter Name" maxlength="10">
91
- <button onclick="startGame()">JOIN GAME</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  </div>
93
-
94
- <div id="leaderboard">Loading...</div>
95
- <div id="score">0</div>
96
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  <div id="gameOverScreen">
98
- <h1>GAME OVER</h1>
99
- <p id="finalScore" style="font-size: 24px; margin-bottom: 30px;">Score: 0</p>
100
- <button id="restartBtn">Try Again</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  </div>
102
  </div>
103
 
104
  <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
105
  <script>
106
- // --- COOKIE HELPERS ---
107
- function setCookie(cname, cvalue, exdays) {
108
- const d = new Date();
109
- d.setTime(d.getTime() + (exdays*24*60*60*1000));
110
- let expires = "expires="+ d.toUTCString();
111
- document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
112
- }
113
- function getCookie(cname) {
114
- let name = cname + "=";
115
- let decodedCookie = decodeURIComponent(document.cookie);
116
- let ca = decodedCookie.split(';');
117
- for(let i = 0; i <ca.length; i++) {
118
- let c = ca[i];
119
- while (c.charAt(0) == ' ') { c = c.substring(1); }
120
- if (c.indexOf(name) == 0) { return c.substring(name.length, c.length); }
121
- }
122
- return "";
123
- }
124
-
125
- // --- SETUP ---
126
- const socket = io();
127
- const canvas = document.getElementById('gameCanvas');
128
- const ctx = canvas.getContext('2d', { alpha: false });
129
- const GAME_W = 375;
130
- const GAME_H = 812;
131
-
132
- // Load Username
133
- const savedName = getCookie("username");
134
- if(savedName) document.getElementById('usernameInput').value = savedName;
135
-
136
- let gameState = 'LOGIN';
137
- let myId = null;
138
- let score = 0;
139
- let cameraY = 0;
140
-
141
- // Entities
142
- let platforms = []; // Synced from server
143
- let otherPlayers = {}; // {id: {x,y,vx...}}
144
-
145
- const player = {
146
- x: GAME_W / 2 - 20,
147
- y: GAME_H - 200,
148
- w: 40, h: 40,
149
- vx: 0, vy: 0,
150
- faceRight: true,
151
- dead: false
152
- };
153
-
154
- const keys = { left: false, right: false };
155
- const GRAVITY = 0.4;
156
- const JUMP_FORCE = -11.5;
157
- const MOVEMENT_SPEED = 6.5;
158
-
159
- // --- SOCKET LISTENERS ---
160
- socket.on('connect', () => { myId = socket.id; });
161
-
162
- socket.on('world_data', (data) => {
163
- // Initial map load or refresh
164
- platforms = data.platforms;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  });
166
-
167
- socket.on('game_update', (data) => {
168
- // Update other players
169
- otherPlayers = data.players;
170
-
171
- // Update dynamic platforms (moving ones)
172
- if(data.platforms) {
173
- // Merge new platforms or update existing positions
174
- data.platforms.forEach(srvPlat => {
175
- const idx = platforms.findIndex(p => p.id === srvPlat.id);
176
- if(idx !== -1) {
177
- platforms[idx].x = srvPlat.x; // Update position of moving plats
178
- } else {
179
- platforms.push(srvPlat); // Add new
180
- }
181
- });
182
- // Cleanup old platforms locally to save memory
183
- platforms = platforms.filter(p => p.y > cameraY - 100);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  }
185
-
186
- // Update Leaderboard
187
- let lbHtml = "<b>TOP JUMPERS</b><br>";
188
- let sorted = Object.values(otherPlayers).sort((a,b) => b.score - a.score).slice(0,5);
189
- sorted.forEach(p => {
190
- lbHtml += `${p.name}: ${p.score}<br>`;
191
- });
192
- document.getElementById('leaderboard').innerHTML = lbHtml;
193
- });
194
-
195
- function startGame() {
196
- const name = document.getElementById('usernameInput').value || "Guest";
197
- setCookie("username", name, 30); // Save for 30 days
198
 
199
- document.getElementById('loginScreen').style.display = 'none';
 
 
 
 
 
 
200
 
201
- socket.emit('join', { name: name });
202
- initGame();
203
- }
204
-
205
- function initGame() {
206
- score = 0;
207
- cameraY = 0;
208
- player.x = GAME_W / 2 - 20;
209
- player.y = GAME_H - 250;
210
- player.vx = 0;
211
- player.vy = 0;
212
- player.dead = false;
213
 
214
- document.getElementById('gameOverScreen').style.display = 'none';
215
- document.getElementById('score').innerText = 0;
216
- gameState = 'PLAYING';
217
 
218
- requestAnimationFrame(gameLoop);
219
  }
220
-
221
- // --- DRAWING (Reused & Adapted) ---
222
- function drawDoodler(x, y, w, h, facingRight, isMe, name) {
 
 
223
  ctx.save();
224
- ctx.translate(x + w/2, y + h/2);
225
 
226
- if (!facingRight) ctx.scale(-1, 1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
 
228
- // Legs
229
- ctx.strokeStyle = '#000';
230
- ctx.lineWidth = 3;
231
- ctx.beginPath();
232
- ctx.moveTo(-10, 15); ctx.lineTo(-12, 22);
233
- ctx.moveTo(10, 15); ctx.lineTo(12, 22);
234
- ctx.stroke();
235
 
236
- // Body color
237
- ctx.fillStyle = isMe ? '#d8c222' : 'rgba(200, 100, 100, 0.8)'; // Others are reddish
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  ctx.beginPath();
239
- ctx.moveTo(-15, -10); ctx.lineTo(-15, 15);
240
- ctx.quadraticCurveTo(0, 20, 15, 15); ctx.lineTo(15, -10);
241
- ctx.quadraticCurveTo(0, -20, -15, -10);
242
- ctx.fill();
 
243
  ctx.stroke();
244
-
245
- // Snout
246
- ctx.beginPath(); ctx.rect(12, -8, 12, 12); ctx.fill(); ctx.stroke();
247
-
248
- // Eye
249
- ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(8, -5, 6, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
250
- ctx.fillStyle = '#000'; ctx.beginPath(); ctx.arc(10, -5, 2, 0, Math.PI * 2); ctx.fill();
251
-
252
- // Backpack
253
- ctx.strokeStyle = '#5c6b1f';
254
- ctx.beginPath(); ctx.moveTo(-14, 5); ctx.lineTo(14, 5); ctx.stroke();
255
-
256
- ctx.restore();
257
-
258
- // Draw Name Tag
259
- if (name) {
260
- ctx.fillStyle = isMe ? '#ffff00' : '#ffffff';
261
- ctx.font = 'bold 12px Arial';
262
- ctx.textAlign = 'center';
263
- ctx.fillText(name, x + w/2, y - 15);
264
  }
265
  }
 
 
 
 
 
 
 
 
266
 
267
- function drawPlatform(p) {
268
- // Only draw if visible
269
- if (p.y > cameraY + GAME_H || p.y < cameraY - 50) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
 
271
- let color = '#76c442'; // Green (Normal)
272
- let detailColor = '#9fe060';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
 
274
- if (p.vx !== 0) { color = '#4287c4'; detailColor = '#60a9e0'; } // Blue (Moving)
275
-
276
- ctx.save();
277
- // Adjust for Camera
278
- const drawY = p.y - cameraY;
279
-
280
- ctx.fillStyle = color;
281
- ctx.strokeStyle = '#000';
282
- ctx.lineWidth = 2;
283
 
 
 
284
  ctx.beginPath();
285
- ctx.roundRect(p.x, drawY, p.w, p.h, 6);
286
  ctx.fill();
287
- ctx.stroke();
288
-
289
- ctx.fillStyle = detailColor;
 
 
 
 
 
 
 
 
 
 
 
290
  ctx.beginPath();
291
- ctx.roundRect(p.x + 3, drawY + 2, p.w - 6, p.h/2 - 2, 3);
 
 
 
 
 
292
  ctx.fill();
293
- ctx.restore();
294
- }
295
-
296
- function drawBackground() {
297
- ctx.fillStyle = '#659d33';
298
- ctx.fillRect(0, 0, GAME_W, GAME_H);
299
 
300
- // Grid Effect
301
- ctx.strokeStyle = 'rgba(0, 30, 0, 0.05)';
302
- ctx.lineWidth = 2;
303
- const gridSize = 40;
304
- const offset = Math.floor(cameraY) % gridSize;
305
  ctx.beginPath();
306
- for(let x = 0; x <= GAME_W; x += gridSize) { ctx.moveTo(x, 0); ctx.lineTo(x, GAME_H); }
307
- for(let y = -offset; y <= GAME_H; y += gridSize) { ctx.moveTo(0, y); ctx.lineTo(GAME_W, y); }
308
- ctx.stroke();
 
 
 
 
 
 
 
 
 
309
  }
 
 
 
310
 
311
- // --- GAME LOOP ---
312
- function update() {
313
- if (gameState !== 'PLAYING') return;
314
-
315
- // Input
316
- if (keys.left) player.vx = -MOVEMENT_SPEED;
317
- else if (keys.right) player.vx = MOVEMENT_SPEED;
318
- else player.vx *= 0.8;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
 
320
- player.x += player.vx;
321
- if (player.x + player.w < 0) player.x = GAME_W;
322
- if (player.x > GAME_W) player.x = -player.w;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
 
324
- if (player.vx > 0.5) player.faceRight = true;
325
- if (player.vx < -0.5) player.faceRight = false;
326
-
327
- player.vy += GRAVITY;
328
- player.y += player.vy;
329
-
330
- // Camera Logic (Player never goes above 45% of screen)
331
- const threshold = cameraY + (GAME_H * 0.45);
332
- if (player.y < threshold) {
333
- let diff = threshold - player.y;
334
- cameraY -= diff; // Camera goes UP (Y decreases)
335
- score += Math.floor(diff/2);
336
- document.getElementById('score').innerText = score;
337
  }
338
-
339
- // Platform Collision
340
- if (player.vy > 0) {
341
- platforms.forEach(p => {
342
- if (player.x + player.w > p.x + 5 && player.x < p.x + p.w - 5 &&
343
- player.y + player.h > p.y && player.y + player.h < p.y + p.h + 10) {
344
- player.vy = JUMP_FORCE;
345
- }
346
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
  }
348
-
349
- // Death
350
- if (player.y > cameraY + GAME_H) {
351
- player.dead = true;
352
- gameState = 'GAMEOVER';
353
- document.getElementById('finalScore').innerText = "Score: " + score;
354
- document.getElementById('gameOverScreen').style.display = 'flex';
355
- socket.emit('player_died');
356
  }
357
-
358
- // Send Update to Server (Rate limited by frame, could be throttled)
359
- socket.emit('update', {
360
- x: player.x,
361
- y: player.y,
362
- vx: player.vx,
363
- faceRight: player.faceRight,
364
- score: score
365
- });
366
  }
367
-
368
- function draw() {
369
- ctx.clearRect(0, 0, canvas.width, canvas.height);
370
- drawBackground();
371
-
372
- // Draw Platforms
373
- platforms.forEach(drawPlatform);
374
-
375
- // Draw Other Players
376
- for (let id in otherPlayers) {
377
- if(id === myId) continue;
378
- const p = otherPlayers[id];
379
- // Only draw if on screen
380
- if (p.y > cameraY - 50 && p.y < cameraY + GAME_H + 50) {
381
- drawDoodler(p.x, p.y - cameraY, 40, 40, p.faceRight, false, p.name);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
  }
383
  }
384
-
385
- // Draw Me
386
- if (gameState !== 'GAMEOVER') {
387
- drawDoodler(player.x, player.y - cameraY, player.w, player.h, player.faceRight, true, "YOU");
 
 
388
  }
389
  }
390
-
391
- function gameLoop() {
392
- update();
393
- draw();
394
- if(gameState !== 'GAMEOVER' || gameState === 'LOGIN') {
395
- requestAnimationFrame(gameLoop);
 
 
 
 
 
 
 
396
  }
397
  }
398
-
399
- // --- INPUT ---
400
- window.addEventListener('keydown', e => {
401
- if (e.code === 'ArrowLeft') keys.left = true;
402
- if (e.code === 'ArrowRight') keys.right = true;
 
 
 
 
 
 
 
 
 
 
 
403
  });
404
- window.addEventListener('keyup', e => {
405
- if (e.code === 'ArrowLeft') keys.left = false;
406
- if (e.code === 'ArrowRight') keys.right = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
408
 
409
- // Touch
410
- canvas.addEventListener('touchstart', e => {
411
- e.preventDefault();
412
- const rect = canvas.getBoundingClientRect();
413
- const t = e.touches[0];
414
- const x = (t.clientX - rect.left) * (GAME_W / rect.width);
415
- if (x < GAME_W / 2) { keys.left = true; keys.right = false; }
416
- else { keys.right = true; keys.left = false; }
417
- }, {passive: false});
418
-
419
- canvas.addEventListener('touchend', e => {
420
- e.preventDefault();
421
- keys.left = false; keys.right = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
422
  });
 
 
 
 
 
 
 
 
 
 
 
423
 
424
- document.getElementById('restartBtn').addEventListener('click', initGame);
 
 
 
 
 
 
 
 
 
 
 
425
 
426
- // Resize
427
- function resize() {
428
- canvas.width = GAME_W;
429
- canvas.height = GAME_H;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
430
  }
431
- window.addEventListener('resize', resize);
432
- resize();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
 
434
  </script>
435
  </body>
436
  </html>
437
  """
438
 
439
- # --- FLASK ROUTES & EVENTS ---
440
 
441
  @app.route('/')
442
  def index():
@@ -445,53 +2209,84 @@ def index():
445
  @socketio.on('join')
446
  def on_join(data):
447
  players[request.sid] = {
448
- 'name': data.get('name', 'Guest'),
449
- 'x': WIDTH/2, 'y': HEIGHT - 200,
450
- 'vx': 0, 'score': 0, 'faceRight': True
 
 
 
 
 
451
  }
452
- # Send current platforms to new user
453
- emit('world_data', {'platforms': platforms})
 
 
 
454
 
455
  @socketio.on('update')
456
  def on_update(data):
457
  if request.sid in players:
458
  p = players[request.sid]
459
- p.update(data)
 
 
 
 
 
460
 
461
- # Infinite Level Generation Logic
462
- # If a player goes high enough, generate more platforms
463
- # Note: Player Y coordinate gets smaller as they go up (0 is top)
 
 
 
 
 
 
 
 
464
  global highest_y_generated
465
- if p['y'] < highest_y_generated + 800: # Buffer
466
- for _ in range(10):
467
- new_plat = generate_platform(highest_y_generated - (80 + random.random()*40))
 
 
468
  platforms.append(new_plat)
 
 
 
 
 
 
 
469
  highest_y_generated = new_plat['y']
470
-
471
- # Keep platforms array size manageable (remove very old ones)
472
- # Find the lowest player
473
- lowest_y = max(pl['y'] for pl in players.values())
474
- # Remove platforms 1000 pixels below the lowest player
475
- # (In a real production app, be more careful here, but this works for jam code)
476
- # global platforms
477
- # platforms = [plat for plat in platforms if plat['y'] < lowest_y + 1000]
478
 
479
  @socketio.on('player_died')
480
  def on_died():
481
  if request.sid in players:
482
- # We don't delete them, just stop updating or reset score?
483
- # For now, client handles reset, we just keep tracking
484
  players[request.sid]['score'] = 0
 
 
 
 
 
 
 
 
 
 
 
 
485
 
486
  @socketio.on('disconnect')
487
  def on_disconnect():
488
  if request.sid in players:
489
  del players[request.sid]
490
 
491
- # Server Loop to broadcast state
492
  def server_loop():
493
  while True:
494
- socketio.sleep(0.05) # 20 ticks per second
495
 
496
  # Update moving platforms
497
  for p in platforms:
@@ -500,24 +2295,32 @@ def server_loop():
500
  if p['x'] < 0 or p['x'] + p['w'] > WIDTH:
501
  p['vx'] *= -1
502
 
503
- # Broadcast state
504
- socketio.emit('game_update', {
505
- 'players': players,
506
- 'platforms': [p for p in platforms if p['vx'] != 0] # Optim: Only send moving plats updates?
507
- # Actually, for simplicity/sync, let's send players and rely on initial map for statics.
508
- # But new plats need to be sent.
509
- # For this simple version, let's just send the whole relevant chunk or just players + moving plats.
510
- })
 
 
 
 
 
 
 
 
511
 
512
- # To ensure new platforms are seen, we periodically send the top chunk
513
- # Or better: The client simply requests/receives updates.
514
- # For simplicity in this specific "One File" constraint:
515
  socketio.emit('game_update', {
516
  'players': players,
517
- 'platforms': platforms[-20:] # Send the newest 20 platforms constantly to ensure sync
518
  })
519
 
520
  socketio.start_background_task(server_loop)
521
 
522
  if __name__ == '__main__':
 
 
523
  socketio.run(app, host='0.0.0.0', port=7860)
 
5
  from flask_socketio import SocketIO, emit
6
  import random
7
  import time
8
+ import math
9
 
10
  # --- SERVER CONFIGURATION ---
11
  WIDTH = 375
12
  HEIGHT = 812
13
  app = Flask(__name__)
14
+ app.config['SECRET_KEY'] = 'jungle_jump_pro_2024'
15
  socketio = SocketIO(app, async_mode='eventlet', cors_allowed_origins='*')
16
 
17
  # --- GAME STATE ---
18
+ players = {}
19
+ platforms = []
20
+ collectibles = []
21
+ enemies = []
22
  highest_y_generated = HEIGHT
23
+ game_events = [] # For broadcasting special events
24
 
25
+ def generate_platform(y_pos, difficulty=1):
 
 
26
  p_type = 'normal'
27
  vx = 0
28
+ special = None
29
+
30
+ rand = random.random()
31
+ if rand < 0.15:
32
+ p_type = 'bouncy'
33
+ elif rand < 0.25:
34
+ p_type = 'ice'
35
+ elif rand < 0.32:
36
+ p_type = 'crumbling'
37
+ elif rand < 0.38:
38
+ p_type = 'cloud'
39
+ elif rand < 0.42:
40
+ p_type = 'golden'
41
+
42
+ if random.random() < 0.25:
43
+ vx = (2 + difficulty * 0.5) if random.random() > 0.5 else -(2 + difficulty * 0.5)
44
+
45
+ # Special items on platform
46
+ if random.random() < 0.08:
47
+ special = 'spring'
48
+ elif random.random() < 0.05:
49
+ special = 'jetpack'
50
+ elif random.random() < 0.04:
51
+ special = 'shield'
52
+ elif random.random() < 0.03:
53
+ special = 'magnet'
54
 
 
 
 
 
55
  return {
56
+ 'id': int(time.time() * 1000) + random.randint(0, 9999),
57
+ 'x': random.random() * (WIDTH - 70),
58
  'y': y_pos,
59
+ 'w': 70 if p_type != 'cloud' else 90,
60
+ 'h': 18,
61
  'type': p_type,
62
  'vx': vx,
63
+ 'special': special,
64
+ 'broken': False,
65
+ 'wobble': 0
66
+ }
67
+
68
+ def generate_collectible(y_pos):
69
+ c_type = 'coin' if random.random() > 0.15 else 'gem'
70
+ return {
71
+ 'id': int(time.time() * 1000) + random.randint(0, 9999),
72
+ 'x': random.random() * (WIDTH - 30),
73
+ 'y': y_pos - 50,
74
+ 'type': c_type,
75
+ 'collected': False,
76
+ 'value': 10 if c_type == 'coin' else 50
77
+ }
78
+
79
+ def generate_enemy(y_pos):
80
+ e_type = random.choice(['slime', 'bat', 'spike'])
81
+ return {
82
+ 'id': int(time.time() * 1000) + random.randint(0, 9999),
83
+ 'x': random.random() * (WIDTH - 40),
84
+ 'y': y_pos - 80,
85
+ 'type': e_type,
86
+ 'vx': 2 if random.random() > 0.5 else -2,
87
+ 'active': True
88
  }
89
 
90
  def init_world():
91
+ global platforms, collectibles, enemies, highest_y_generated
92
  platforms = []
93
+ collectibles = []
94
+ enemies = []
95
+
96
  # Base platform
97
+ platforms.append({
98
+ 'id': 0, 'x': WIDTH/2 - 50, 'y': HEIGHT - 150,
99
+ 'w': 100, 'h': 20, 'type': 'start', 'vx': 0,
100
+ 'special': None, 'broken': False, 'wobble': 0
101
+ })
102
+
103
+ y = HEIGHT - 280
104
+ for i in range(300):
105
+ difficulty = min(i / 50, 5)
106
+ gap = 75 + random.random() * (35 + difficulty * 5)
107
+ platforms.append(generate_platform(y, difficulty))
108
+
109
+ if random.random() < 0.4:
110
+ collectibles.append(generate_collectible(y))
111
+
112
+ if random.random() < 0.08 and i > 10:
113
+ enemies.append(generate_enemy(y))
114
+
115
+ y -= gap
116
 
 
 
 
 
 
117
  highest_y_generated = y
118
 
119
  init_world()
120
 
121
+ # --- ENHANCED HTML TEMPLATE ---
122
  HTML_TEMPLATE = """
123
  <!DOCTYPE html>
124
  <html lang="en">
125
  <head>
126
  <meta charset="UTF-8">
127
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
128
+ <title>🌴 Jungle Jump Pro - Multiplayer</title>
129
+ <link href="https://fonts.googleapis.com/css2?family=Fredoka+One&family=Nunito:wght@400;700;900&display=swap" rel="stylesheet">
130
  <style>
131
+ * { margin: 0; padding: 0; box-sizing: border-box; }
 
 
132
 
133
+ body {
134
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
135
+ display: flex;
136
+ justify-content: center;
137
+ align-items: center;
138
+ min-height: 100vh;
139
+ overflow: hidden;
140
+ font-family: 'Nunito', sans-serif;
141
+ touch-action: none;
142
+ user-select: none;
143
+ }
144
+
145
+ #gameContainer {
146
+ position: relative;
147
+ width: 100%;
148
+ height: 100%;
149
+ max-width: 420px;
150
+ max-height: 100vh;
151
+ aspect-ratio: 9/19;
152
+ border-radius: 20px;
153
+ overflow: hidden;
154
+ box-shadow:
155
+ 0 0 60px rgba(79, 172, 254, 0.3),
156
+ 0 0 100px rgba(0, 242, 254, 0.2),
157
+ inset 0 0 60px rgba(255, 255, 255, 0.05);
158
+ }
159
+
160
+ canvas {
161
+ display: block;
162
+ width: 100%;
163
+ height: 100%;
164
+ border-radius: 20px;
165
+ }
166
+
167
+ /* Login Screen */
168
+ #loginScreen {
169
+ position: absolute;
170
+ top: 0; left: 0;
171
+ width: 100%; height: 100%;
172
+ background: linear-gradient(180deg,
173
+ rgba(15, 52, 96, 0.98) 0%,
174
+ rgba(26, 26, 46, 0.98) 100%);
175
+ z-index: 100;
176
+ display: flex;
177
+ flex-direction: column;
178
+ justify-content: center;
179
+ align-items: center;
180
+ backdrop-filter: blur(10px);
181
+ }
182
+
183
+ .game-logo {
184
+ font-family: 'Fredoka One', cursive;
185
+ font-size: 42px;
186
+ background: linear-gradient(45deg, #00f2fe, #4facfe, #00f2fe);
187
+ background-size: 200% 200%;
188
+ -webkit-background-clip: text;
189
+ -webkit-text-fill-color: transparent;
190
+ animation: gradientShift 3s ease infinite;
191
+ text-shadow: 0 0 30px rgba(79, 172, 254, 0.5);
192
+ margin-bottom: 10px;
193
+ }
194
+
195
+ .game-subtitle {
196
+ color: #4facfe;
197
+ font-size: 16px;
198
+ margin-bottom: 40px;
199
+ opacity: 0.8;
200
+ }
201
+
202
+ @keyframes gradientShift {
203
+ 0%, 100% { background-position: 0% 50%; }
204
+ 50% { background-position: 100% 50%; }
205
+ }
206
+
207
+ .login-input {
208
+ width: 260px;
209
+ padding: 18px 25px;
210
+ font-size: 18px;
211
+ font-family: 'Nunito', sans-serif;
212
+ font-weight: 700;
213
+ border: 3px solid rgba(79, 172, 254, 0.3);
214
+ border-radius: 50px;
215
+ background: rgba(255, 255, 255, 0.1);
216
+ color: white;
217
+ text-align: center;
218
+ outline: none;
219
+ transition: all 0.3s ease;
220
+ margin-bottom: 20px;
221
+ }
222
+
223
+ .login-input:focus {
224
+ border-color: #4facfe;
225
+ box-shadow: 0 0 30px rgba(79, 172, 254, 0.4);
226
+ background: rgba(255, 255, 255, 0.15);
227
+ }
228
+
229
+ .login-input::placeholder {
230
+ color: rgba(255, 255, 255, 0.5);
231
+ }
232
+
233
+ .play-btn {
234
+ padding: 18px 60px;
235
+ font-size: 22px;
236
+ font-family: 'Fredoka One', cursive;
237
+ background: linear-gradient(45deg, #00f2fe, #4facfe);
238
+ border: none;
239
+ border-radius: 50px;
240
+ color: #1a1a2e;
241
+ cursor: pointer;
242
+ transition: all 0.3s ease;
243
+ box-shadow: 0 10px 40px rgba(79, 172, 254, 0.4);
244
+ }
245
+
246
+ .play-btn:hover {
247
+ transform: translateY(-3px) scale(1.05);
248
+ box-shadow: 0 15px 50px rgba(79, 172, 254, 0.6);
249
+ }
250
+
251
+ .play-btn:active {
252
+ transform: translateY(0) scale(0.98);
253
+ }
254
+
255
+ /* Character Selection */
256
+ .character-select {
257
+ display: flex;
258
+ gap: 15px;
259
+ margin: 30px 0;
260
+ }
261
+
262
+ .char-option {
263
+ width: 70px;
264
+ height: 70px;
265
+ border-radius: 50%;
266
+ border: 3px solid rgba(255, 255, 255, 0.2);
267
+ cursor: pointer;
268
+ transition: all 0.3s ease;
269
+ display: flex;
270
+ align-items: center;
271
+ justify-content: center;
272
+ font-size: 32px;
273
+ background: rgba(255, 255, 255, 0.1);
274
+ }
275
+
276
+ .char-option:hover, .char-option.selected {
277
+ border-color: #4facfe;
278
+ transform: scale(1.1);
279
+ box-shadow: 0 0 30px rgba(79, 172, 254, 0.5);
280
+ }
281
+
282
+ /* HUD */
283
+ #hud {
284
+ position: absolute;
285
+ top: 0; left: 0; right: 0;
286
+ padding: 15px 20px;
287
+ display: flex;
288
+ justify-content: space-between;
289
+ align-items: flex-start;
290
+ pointer-events: none;
291
+ z-index: 10;
292
+ }
293
+
294
+ .score-container {
295
+ background: linear-gradient(135deg, rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.5));
296
+ padding: 12px 25px;
297
+ border-radius: 30px;
298
+ border: 2px solid rgba(255, 215, 0, 0.5);
299
+ backdrop-filter: blur(10px);
300
+ }
301
+
302
+ .score-label {
303
+ font-size: 11px;
304
+ color: rgba(255, 215, 0, 0.8);
305
+ text-transform: uppercase;
306
+ letter-spacing: 2px;
307
+ }
308
+
309
+ .score-value {
310
+ font-family: 'Fredoka One', cursive;
311
+ font-size: 32px;
312
+ color: #ffd700;
313
+ text-shadow: 0 0 20px rgba(255, 215, 0, 0.5);
314
+ }
315
 
316
+ .combo-display {
317
+ position: absolute;
318
+ top: 80px;
319
+ right: 20px;
320
+ font-family: 'Fredoka One', cursive;
321
+ font-size: 24px;
322
+ color: #ff6b6b;
323
+ opacity: 0;
324
+ transform: scale(0);
325
+ transition: all 0.3s ease;
326
+ }
327
+
328
+ .combo-display.active {
329
+ opacity: 1;
330
+ transform: scale(1);
331
+ }
332
+
333
+ #leaderboard {
334
+ background: linear-gradient(135deg, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0.6));
335
+ padding: 15px;
336
+ border-radius: 15px;
337
+ border: 2px solid rgba(79, 172, 254, 0.3);
338
+ backdrop-filter: blur(10px);
339
+ min-width: 140px;
340
+ }
341
+
342
+ .lb-title {
343
+ font-size: 12px;
344
+ color: #4facfe;
345
+ text-transform: uppercase;
346
+ letter-spacing: 2px;
347
+ margin-bottom: 10px;
348
+ text-align: center;
349
+ }
350
+
351
+ .lb-entry {
352
+ display: flex;
353
+ justify-content: space-between;
354
+ padding: 5px 0;
355
+ font-size: 13px;
356
+ color: white;
357
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
358
+ }
359
+
360
+ .lb-entry:last-child {
361
+ border-bottom: none;
362
+ }
363
+
364
+ .lb-entry.me {
365
+ color: #ffd700;
366
+ font-weight: 700;
367
+ }
368
+
369
+ /* Power-up Indicators */
370
+ #powerups {
371
+ position: absolute;
372
+ bottom: 20px;
373
+ left: 20px;
374
+ display: flex;
375
+ gap: 10px;
376
+ z-index: 10;
377
+ }
378
+
379
+ .powerup-icon {
380
+ width: 50px;
381
+ height: 50px;
382
+ border-radius: 12px;
383
+ background: rgba(0, 0, 0, 0.7);
384
+ border: 2px solid rgba(255, 255, 255, 0.3);
385
+ display: flex;
386
+ align-items: center;
387
+ justify-content: center;
388
+ font-size: 24px;
389
+ position: relative;
390
+ opacity: 0.3;
391
+ transition: all 0.3s ease;
392
+ }
393
+
394
+ .powerup-icon.active {
395
+ opacity: 1;
396
+ border-color: #4facfe;
397
+ box-shadow: 0 0 20px rgba(79, 172, 254, 0.5);
398
+ animation: powerupPulse 1s ease infinite;
399
+ }
400
+
401
+ .powerup-timer {
402
+ position: absolute;
403
+ bottom: -5px;
404
+ left: 50%;
405
+ transform: translateX(-50%);
406
+ width: 40px;
407
+ height: 4px;
408
+ background: rgba(0, 0, 0, 0.5);
409
+ border-radius: 2px;
410
+ overflow: hidden;
411
+ }
412
+
413
+ .powerup-timer-fill {
414
+ height: 100%;
415
+ background: #4facfe;
416
+ transition: width 0.1s linear;
417
+ }
418
+
419
+ @keyframes powerupPulse {
420
+ 0%, 100% { transform: scale(1); }
421
+ 50% { transform: scale(1.1); }
422
+ }
423
+
424
+ /* Game Over Screen */
425
+ #gameOverScreen {
426
+ position: absolute;
427
+ top: 0; left: 0;
428
+ width: 100%; height: 100%;
429
+ background: linear-gradient(180deg,
430
+ rgba(15, 52, 96, 0.95) 0%,
431
+ rgba(26, 26, 46, 0.98) 100%);
432
+ display: none;
433
+ flex-direction: column;
434
+ justify-content: center;
435
+ align-items: center;
436
+ z-index: 50;
437
+ backdrop-filter: blur(10px);
438
+ }
439
+
440
+ .game-over-title {
441
+ font-family: 'Fredoka One', cursive;
442
+ font-size: 48px;
443
+ color: #ff6b6b;
444
+ margin-bottom: 20px;
445
+ text-shadow: 0 0 30px rgba(255, 107, 107, 0.5);
446
+ }
447
+
448
+ .final-score {
449
+ font-family: 'Fredoka One', cursive;
450
+ font-size: 64px;
451
+ color: #ffd700;
452
+ margin-bottom: 10px;
453
+ }
454
+
455
+ .score-breakdown {
456
+ color: rgba(255, 255, 255, 0.7);
457
+ margin-bottom: 30px;
458
+ text-align: center;
459
+ }
460
+
461
+ .stats-grid {
462
+ display: grid;
463
+ grid-template-columns: repeat(3, 1fr);
464
+ gap: 20px;
465
+ margin-bottom: 40px;
466
+ }
467
+
468
+ .stat-item {
469
+ text-align: center;
470
+ padding: 15px;
471
+ background: rgba(255, 255, 255, 0.1);
472
+ border-radius: 15px;
473
+ }
474
+
475
+ .stat-value {
476
+ font-family: 'Fredoka One', cursive;
477
+ font-size: 28px;
478
+ color: #4facfe;
479
+ }
480
+
481
+ .stat-label {
482
+ font-size: 12px;
483
+ color: rgba(255, 255, 255, 0.6);
484
+ text-transform: uppercase;
485
+ }
486
+
487
+ .restart-btn {
488
+ padding: 18px 60px;
489
+ font-size: 22px;
490
+ font-family: 'Fredoka One', cursive;
491
+ background: linear-gradient(45deg, #ff6b6b, #ffa502);
492
+ border: none;
493
+ border-radius: 50px;
494
+ color: white;
495
+ cursor: pointer;
496
+ transition: all 0.3s ease;
497
+ box-shadow: 0 10px 40px rgba(255, 107, 107, 0.4);
498
+ }
499
+
500
+ .restart-btn:hover {
501
+ transform: translateY(-3px) scale(1.05);
502
+ }
503
+
504
+ /* Floating Messages */
505
+ #floatingMessages {
506
+ position: absolute;
507
+ top: 50%;
508
+ left: 50%;
509
+ transform: translate(-50%, -50%);
510
+ pointer-events: none;
511
+ z-index: 15;
512
+ }
513
+
514
+ .float-msg {
515
+ position: absolute;
516
+ font-family: 'Fredoka One', cursive;
517
+ font-size: 24px;
518
+ color: #ffd700;
519
+ text-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
520
+ animation: floatUp 1s ease forwards;
521
+ white-space: nowrap;
522
+ }
523
+
524
+ @keyframes floatUp {
525
+ 0% { opacity: 1; transform: translateY(0) scale(1); }
526
+ 100% { opacity: 0; transform: translateY(-50px) scale(1.2); }
527
+ }
528
+
529
+ /* Mobile Controls */
530
+ #mobileControls {
531
+ position: absolute;
532
+ bottom: 30px;
533
+ left: 0; right: 0;
534
+ display: none;
535
+ justify-content: space-between;
536
+ padding: 0 30px;
537
+ z-index: 10;
538
+ }
539
+
540
+ .mobile-btn {
541
+ width: 80px;
542
+ height: 80px;
543
+ border-radius: 50%;
544
+ background: rgba(255, 255, 255, 0.15);
545
+ border: 3px solid rgba(255, 255, 255, 0.3);
546
+ display: flex;
547
+ align-items: center;
548
+ justify-content: center;
549
+ font-size: 36px;
550
+ color: white;
551
+ backdrop-filter: blur(5px);
552
+ transition: all 0.2s ease;
553
+ }
554
+
555
+ .mobile-btn:active {
556
+ background: rgba(79, 172, 254, 0.4);
557
+ border-color: #4facfe;
558
+ transform: scale(0.95);
559
+ }
560
+
561
+ @media (max-width: 768px) {
562
+ #mobileControls { display: flex; }
563
+ }
564
+
565
+ /* Event Feed */
566
+ #eventFeed {
567
+ position: absolute;
568
+ top: 150px;
569
+ left: 10px;
570
+ width: 180px;
571
+ pointer-events: none;
572
+ z-index: 10;
573
+ }
574
+
575
+ .event-item {
576
+ padding: 8px 12px;
577
+ margin-bottom: 5px;
578
+ background: rgba(0, 0, 0, 0.6);
579
+ border-radius: 20px;
580
+ font-size: 11px;
581
+ color: white;
582
+ animation: eventSlide 0.3s ease;
583
+ border-left: 3px solid #4facfe;
584
+ }
585
+
586
+ @keyframes eventSlide {
587
+ from { opacity: 0; transform: translateX(-20px); }
588
+ to { opacity: 1; transform: translateX(0); }
589
+ }
590
+
591
+ /* Loading */
592
+ .loading-dots {
593
+ display: flex;
594
+ gap: 8px;
595
+ margin-top: 20px;
596
+ }
597
+
598
+ .loading-dot {
599
+ width: 12px;
600
+ height: 12px;
601
+ border-radius: 50%;
602
+ background: #4facfe;
603
+ animation: loadingBounce 1.4s ease infinite;
604
+ }
605
+
606
+ .loading-dot:nth-child(2) { animation-delay: 0.2s; }
607
+ .loading-dot:nth-child(3) { animation-delay: 0.4s; }
608
+
609
+ @keyframes loadingBounce {
610
+ 0%, 80%, 100% { transform: scale(0.6); opacity: 0.5; }
611
+ 40% { transform: scale(1); opacity: 1; }
612
+ }
613
  </style>
614
  </head>
615
  <body>
 
617
  <div id="gameContainer">
618
  <canvas id="gameCanvas"></canvas>
619
 
620
+ <!-- Login Screen -->
621
  <div id="loginScreen">
622
+ <div class="game-logo">JUNGLE JUMP</div>
623
+ <div class="game-subtitle">✨ Multiplayer Pro Edition ✨</div>
624
+
625
+ <div class="character-select">
626
+ <div class="char-option selected" data-char="dino" onclick="selectChar(this)">🦖</div>
627
+ <div class="char-option" data-char="frog" onclick="selectChar(this)">🐸</div>
628
+ <div class="char-option" data-char="cat" onclick="selectChar(this)">🐱</div>
629
+ <div class="char-option" data-char="robot" onclick="selectChar(this)">🤖</div>
630
+ <div class="char-option" data-char="alien" onclick="selectChar(this)">👽</div>
631
+ </div>
632
+
633
+ <input type="text" id="usernameInput" class="login-input" placeholder="Enter Your Name" maxlength="12">
634
+ <button class="play-btn" onclick="startGame()">🚀 PLAY NOW</button>
635
+
636
+ <div class="loading-dots" id="loadingDots" style="display: none;">
637
+ <div class="loading-dot"></div>
638
+ <div class="loading-dot"></div>
639
+ <div class="loading-dot"></div>
640
+ </div>
641
  </div>
642
+
643
+ <!-- HUD -->
644
+ <div id="hud" style="display: none;">
645
+ <div id="leaderboard">
646
+ <div class="lb-title">🏆 Top Jumpers</div>
647
+ <div id="lbEntries">Connecting...</div>
648
+ </div>
649
+
650
+ <div class="score-container">
651
+ <div class="score-label">Score</div>
652
+ <div class="score-value" id="scoreValue">0</div>
653
+ </div>
654
+ </div>
655
+
656
+ <div class="combo-display" id="comboDisplay">x2 COMBO!</div>
657
+
658
+ <!-- Power-up Indicators -->
659
+ <div id="powerups" style="display: none;">
660
+ <div class="powerup-icon" id="pwrJetpack">🚀
661
+ <div class="powerup-timer"><div class="powerup-timer-fill" id="jetpackTimer"></div></div>
662
+ </div>
663
+ <div class="powerup-icon" id="pwrShield">🛡️
664
+ <div class="powerup-timer"><div class="powerup-timer-fill" id="shieldTimer"></div></div>
665
+ </div>
666
+ <div class="powerup-icon" id="pwrMagnet">🧲
667
+ <div class="powerup-timer"><div class="powerup-timer-fill" id="magnetTimer"></div></div>
668
+ </div>
669
+ </div>
670
+
671
+ <!-- Event Feed -->
672
+ <div id="eventFeed"></div>
673
+
674
+ <!-- Floating Messages -->
675
+ <div id="floatingMessages"></div>
676
+
677
+ <!-- Mobile Controls -->
678
+ <div id="mobileControls">
679
+ <div class="mobile-btn" id="btnLeft">◀</div>
680
+ <div class="mobile-btn" id="btnRight">▶</div>
681
+ </div>
682
+
683
+ <!-- Game Over Screen -->
684
  <div id="gameOverScreen">
685
+ <div class="game-over-title">GAME OVER</div>
686
+ <div class="final-score" id="finalScoreValue">0</div>
687
+ <div class="score-breakdown">
688
+ <div>Height: <span id="heightReached">0</span>m</div>
689
+ <div>Best Combo: x<span id="bestCombo">0</span></div>
690
+ </div>
691
+
692
+ <div class="stats-grid">
693
+ <div class="stat-item">
694
+ <div class="stat-value" id="statCoins">0</div>
695
+ <div class="stat-label">Coins</div>
696
+ </div>
697
+ <div class="stat-item">
698
+ <div class="stat-value" id="statEnemies">0</div>
699
+ <div class="stat-label">Enemies</div>
700
+ </div>
701
+ <div class="stat-item">
702
+ <div class="stat-value" id="statJumps">0</div>
703
+ <div class="stat-label">Jumps</div>
704
+ </div>
705
+ </div>
706
+
707
+ <button class="restart-btn" onclick="restartGame()">🔄 TRY AGAIN</button>
708
  </div>
709
  </div>
710
 
711
  <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
712
  <script>
713
+ // ============================================
714
+ // 🎮 JUNGLE JUMP PRO - COMPLETE GAME ENGINE
715
+ // ============================================
716
+
717
+ // --- UTILITIES ---
718
+ const $ = id => document.getElementById(id);
719
+ const setCookie = (n, v, d) => { const e = new Date(); e.setTime(e.getTime() + d*24*60*60*1000); document.cookie = `${n}=${v};expires=${e.toUTCString()};path=/`; };
720
+ const getCookie = n => { const m = document.cookie.match(new RegExp('(^| )' + n + '=([^;]+)')); return m ? m[2] : ''; };
721
+
722
+ // --- CONFIGURATION ---
723
+ const CONFIG = {
724
+ GAME_W: 375,
725
+ GAME_H: 812,
726
+ GRAVITY: 0.42,
727
+ JUMP_FORCE: -13,
728
+ SUPER_JUMP: -20,
729
+ JETPACK_FORCE: -0.8,
730
+ MOVE_SPEED: 7.5,
731
+ FRICTION: 0.88,
732
+ PARTICLE_LIMIT: 150,
733
+ TRAIL_LENGTH: 12
734
+ };
735
+
736
+ // --- SOCKET CONNECTION ---
737
+ const socket = io();
738
+ let myId = null;
739
+
740
+ // --- GAME STATE ---
741
+ let gameState = 'LOGIN';
742
+ let selectedChar = 'dino';
743
+ let score = 0;
744
+ let coins = 0;
745
+ let combo = 0;
746
+ let maxCombo = 0;
747
+ let enemiesDefeated = 0;
748
+ let jumpCount = 0;
749
+ let cameraY = 0;
750
+ let cameraTargetY = 0;
751
+ let screenShake = 0;
752
+
753
+ // --- ENTITIES ---
754
+ let platforms = [];
755
+ let collectibles = [];
756
+ let enemies = [];
757
+ let otherPlayers = {};
758
+ let particles = [];
759
+ let floatingTexts = [];
760
+ let trailPoints = [];
761
+
762
+ // --- PLAYER ---
763
+ const player = {
764
+ x: CONFIG.GAME_W / 2 - 22,
765
+ y: CONFIG.GAME_H - 200,
766
+ w: 44,
767
+ h: 44,
768
+ vx: 0,
769
+ vy: 0,
770
+ faceRight: true,
771
+ grounded: false,
772
+
773
+ // Power-ups
774
+ hasJetpack: false,
775
+ jetpackFuel: 0,
776
+ hasShield: false,
777
+ shieldTime: 0,
778
+ hasMagnet: false,
779
+ magnetTime: 0,
780
+
781
+ // Animation
782
+ frame: 0,
783
+ squash: 1,
784
+ stretch: 1,
785
+ rotation: 0,
786
+ trail: [],
787
+ glowIntensity: 0
788
+ };
789
+
790
+ const keys = { left: false, right: false };
791
+ let lastTime = 0;
792
+ let deltaTime = 0;
793
+
794
+ // --- CANVAS SETUP ---
795
+ const canvas = $('gameCanvas');
796
+ const ctx = canvas.getContext('2d');
797
+ canvas.width = CONFIG.GAME_W;
798
+ canvas.height = CONFIG.GAME_H;
799
+
800
+ // --- CHARACTER SELECTION ---
801
+ function selectChar(el) {
802
+ document.querySelectorAll('.char-option').forEach(c => c.classList.remove('selected'));
803
+ el.classList.add('selected');
804
+ selectedChar = el.dataset.char;
805
+ }
806
+
807
+ // Load saved data
808
+ const savedName = getCookie('jj_username');
809
+ const savedChar = getCookie('jj_character');
810
+ if (savedName) $('usernameInput').value = savedName;
811
+ if (savedChar) {
812
+ selectedChar = savedChar;
813
+ document.querySelectorAll('.char-option').forEach(c => {
814
+ c.classList.toggle('selected', c.dataset.char === savedChar);
815
  });
816
+ }
817
+
818
+ // ============================================
819
+ // 🎨 PARTICLE SYSTEM
820
+ // ============================================
821
+ class Particle {
822
+ constructor(x, y, type = 'dust') {
823
+ this.x = x;
824
+ this.y = y;
825
+ this.type = type;
826
+ this.life = 1;
827
+ this.decay = 0.02 + Math.random() * 0.03;
828
+
829
+ switch(type) {
830
+ case 'dust':
831
+ this.vx = (Math.random() - 0.5) * 3;
832
+ this.vy = -Math.random() * 2 - 1;
833
+ this.size = 4 + Math.random() * 4;
834
+ this.color = `hsla(${35 + Math.random() * 20}, 60%, 70%, `;
835
+ break;
836
+ case 'star':
837
+ this.vx = (Math.random() - 0.5) * 6;
838
+ this.vy = (Math.random() - 0.5) * 6;
839
+ this.size = 3 + Math.random() * 5;
840
+ this.color = `hsla(${50 + Math.random() * 30}, 100%, 70%, `;
841
+ this.rotation = Math.random() * Math.PI * 2;
842
+ this.rotationSpeed = (Math.random() - 0.5) * 0.3;
843
+ break;
844
+ case 'coin':
845
+ this.vx = (Math.random() - 0.5) * 8;
846
+ this.vy = -Math.random() * 5 - 2;
847
+ this.size = 6 + Math.random() * 4;
848
+ this.color = `hsla(45, 100%, 60%, `;
849
+ break;
850
+ case 'jump':
851
+ this.vx = (Math.random() - 0.5) * 4;
852
+ this.vy = Math.random() * 2;
853
+ this.size = 8 + Math.random() * 6;
854
+ this.color = `hsla(${180 + Math.random() * 40}, 80%, 60%, `;
855
+ this.decay = 0.04;
856
+ break;
857
+ case 'jetpack':
858
+ this.vx = (Math.random() - 0.5) * 2;
859
+ this.vy = Math.random() * 4 + 2;
860
+ this.size = 6 + Math.random() * 8;
861
+ this.color = `hsla(${20 + Math.random() * 30}, 100%, 60%, `;
862
+ this.decay = 0.05;
863
+ break;
864
+ case 'shield':
865
+ this.angle = Math.random() * Math.PI * 2;
866
+ this.radius = 30 + Math.random() * 10;
867
+ this.size = 4;
868
+ this.color = `hsla(200, 100%, 70%, `;
869
+ this.decay = 0.03;
870
+ break;
871
+ case 'death':
872
+ this.vx = (Math.random() - 0.5) * 10;
873
+ this.vy = (Math.random() - 0.5) * 10;
874
+ this.size = 5 + Math.random() * 10;
875
+ this.color = `hsla(0, 80%, 50%, `;
876
+ this.decay = 0.02;
877
+ break;
878
+ case 'enemy':
879
+ this.vx = (Math.random() - 0.5) * 8;
880
+ this.vy = (Math.random() - 0.5) * 8;
881
+ this.size = 8 + Math.random() * 8;
882
+ this.color = `hsla(${280 + Math.random() * 40}, 80%, 50%, `;
883
+ break;
884
  }
885
+ }
886
+
887
+ update() {
888
+ this.x += this.vx;
889
+ this.y += this.vy;
 
 
 
 
 
 
 
 
890
 
891
+ if (this.type === 'shield') {
892
+ this.angle += 0.1;
893
+ this.x = player.x + player.w/2 + Math.cos(this.angle) * this.radius;
894
+ this.y = player.y + player.h/2 + Math.sin(this.angle) * this.radius;
895
+ } else {
896
+ this.vy += 0.1; // Gravity on particles
897
+ }
898
 
899
+ if (this.rotation !== undefined) {
900
+ this.rotation += this.rotationSpeed;
901
+ }
 
 
 
 
 
 
 
 
 
902
 
903
+ this.life -= this.decay;
904
+ this.size *= 0.98;
 
905
 
906
+ return this.life > 0;
907
  }
908
+
909
+ draw(ctx, cameraY) {
910
+ const screenY = this.y - cameraY;
911
+ if (screenY < -50 || screenY > CONFIG.GAME_H + 50) return;
912
+
913
  ctx.save();
914
+ ctx.globalAlpha = this.life;
915
 
916
+ if (this.type === 'star') {
917
+ ctx.translate(this.x, screenY);
918
+ ctx.rotate(this.rotation);
919
+ ctx.fillStyle = this.color + this.life + ')';
920
+ drawStar(ctx, 0, 0, 5, this.size, this.size/2);
921
+ } else {
922
+ ctx.fillStyle = this.color + this.life + ')';
923
+ ctx.beginPath();
924
+ ctx.arc(this.x, screenY, this.size/2, 0, Math.PI * 2);
925
+ ctx.fill();
926
+ }
927
+
928
+ ctx.restore();
929
+ }
930
+ }
931
+
932
+ function drawStar(ctx, cx, cy, spikes, outerR, innerR) {
933
+ let rot = Math.PI / 2 * 3;
934
+ let step = Math.PI / spikes;
935
+ ctx.beginPath();
936
+ ctx.moveTo(cx, cy - outerR);
937
+ for (let i = 0; i < spikes; i++) {
938
+ ctx.lineTo(cx + Math.cos(rot) * outerR, cy + Math.sin(rot) * outerR);
939
+ rot += step;
940
+ ctx.lineTo(cx + Math.cos(rot) * innerR, cy + Math.sin(rot) * innerR);
941
+ rot += step;
942
+ }
943
+ ctx.closePath();
944
+ ctx.fill();
945
+ }
946
+
947
+ function spawnParticles(x, y, type, count = 10) {
948
+ for (let i = 0; i < count; i++) {
949
+ if (particles.length < CONFIG.PARTICLE_LIMIT) {
950
+ particles.push(new Particle(x, y, type));
951
+ }
952
+ }
953
+ }
954
 
955
+ // ============================================
956
+ // 🎨 DRAWING FUNCTIONS
957
+ // ============================================
 
 
 
 
958
 
959
+ function drawBackground() {
960
+ // Gradient sky based on height
961
+ const heightFactor = Math.min(Math.abs(cameraY) / 50000, 1);
962
+ const gradient = ctx.createLinearGradient(0, 0, 0, CONFIG.GAME_H);
963
+
964
+ // Transition from jungle green to sky blue to space purple
965
+ if (heightFactor < 0.3) {
966
+ gradient.addColorStop(0, `hsl(${100 - heightFactor * 100}, ${60 - heightFactor * 30}%, ${35 + heightFactor * 20}%)`);
967
+ gradient.addColorStop(1, `hsl(${120 - heightFactor * 50}, ${50 - heightFactor * 20}%, ${25 + heightFactor * 15}%)`);
968
+ } else if (heightFactor < 0.7) {
969
+ const t = (heightFactor - 0.3) / 0.4;
970
+ gradient.addColorStop(0, `hsl(${200 + t * 40}, ${70 + t * 20}%, ${50 - t * 20}%)`);
971
+ gradient.addColorStop(1, `hsl(${180 + t * 60}, ${60 + t * 20}%, ${40 - t * 15}%)`);
972
+ } else {
973
+ const t = (heightFactor - 0.7) / 0.3;
974
+ gradient.addColorStop(0, `hsl(${260 + t * 20}, ${80}%, ${20 - t * 10}%)`);
975
+ gradient.addColorStop(1, `hsl(${280 + t * 20}, ${70}%, ${15 - t * 10}%)`);
976
+ }
977
+
978
+ ctx.fillStyle = gradient;
979
+ ctx.fillRect(0, 0, CONFIG.GAME_W, CONFIG.GAME_H);
980
+
981
+ // Parallax stars (appear at higher altitudes)
982
+ if (heightFactor > 0.3) {
983
+ const starAlpha = (heightFactor - 0.3) / 0.7;
984
+ ctx.fillStyle = `rgba(255, 255, 255, ${starAlpha * 0.8})`;
985
+ const starOffset = cameraY * 0.1;
986
+ for (let i = 0; i < 50; i++) {
987
+ const sx = (i * 73 + starOffset * 0.3) % CONFIG.GAME_W;
988
+ const sy = (i * 137 + starOffset * 0.2) % CONFIG.GAME_H;
989
+ const size = 1 + (i % 3);
990
+ ctx.beginPath();
991
+ ctx.arc(sx, sy, size, 0, Math.PI * 2);
992
+ ctx.fill();
993
+ }
994
+ }
995
+
996
+ // Parallax clouds/leaves
997
+ ctx.fillStyle = `rgba(255, 255, 255, ${0.1 - heightFactor * 0.08})`;
998
+ const cloudOffset = cameraY * 0.3;
999
+ for (let i = 0; i < 8; i++) {
1000
+ const cx = ((i * 157 + cloudOffset * 0.5) % (CONFIG.GAME_W + 100)) - 50;
1001
+ const cy = ((i * 211 + cloudOffset * 0.3) % (CONFIG.GAME_H + 100)) - 50;
1002
+ drawCloud(ctx, cx, cy, 40 + i * 10);
1003
+ }
1004
+
1005
+ // Subtle grid pattern
1006
+ ctx.strokeStyle = `rgba(255, 255, 255, 0.03)`;
1007
+ ctx.lineWidth = 1;
1008
+ const gridSize = 50;
1009
+ const gridOffset = cameraY % gridSize;
1010
+ ctx.beginPath();
1011
+ for (let x = 0; x <= CONFIG.GAME_W; x += gridSize) {
1012
+ ctx.moveTo(x, 0);
1013
+ ctx.lineTo(x, CONFIG.GAME_H);
1014
+ }
1015
+ for (let y = -gridOffset; y <= CONFIG.GAME_H; y += gridSize) {
1016
+ ctx.moveTo(0, y);
1017
+ ctx.lineTo(CONFIG.GAME_W, y);
1018
+ }
1019
+ ctx.stroke();
1020
+ }
1021
+
1022
+ function drawCloud(ctx, x, y, size) {
1023
+ ctx.beginPath();
1024
+ ctx.arc(x, y, size * 0.5, 0, Math.PI * 2);
1025
+ ctx.arc(x + size * 0.4, y - size * 0.1, size * 0.4, 0, Math.PI * 2);
1026
+ ctx.arc(x + size * 0.8, y, size * 0.35, 0, Math.PI * 2);
1027
+ ctx.fill();
1028
+ }
1029
+
1030
+ function drawPlatform(p) {
1031
+ const screenY = p.y - cameraY;
1032
+ if (screenY < -50 || screenY > CONFIG.GAME_H + 50) return;
1033
+
1034
+ ctx.save();
1035
+
1036
+ // Apply wobble for unstable platforms
1037
+ if (p.wobble) {
1038
+ ctx.translate(p.x + p.w/2, screenY + p.h/2);
1039
+ ctx.rotate(Math.sin(Date.now() / 100) * p.wobble * 0.05);
1040
+ ctx.translate(-(p.x + p.w/2), -(screenY + p.h/2));
1041
+ }
1042
+
1043
+ // Platform colors and styles based on type
1044
+ let mainColor, topColor, shadowColor, glowColor;
1045
+
1046
+ switch(p.type) {
1047
+ case 'start':
1048
+ mainColor = '#4ade80';
1049
+ topColor = '#86efac';
1050
+ shadowColor = '#166534';
1051
+ glowColor = 'rgba(74, 222, 128, 0.3)';
1052
+ break;
1053
+ case 'normal':
1054
+ mainColor = '#22c55e';
1055
+ topColor = '#4ade80';
1056
+ shadowColor = '#15803d';
1057
+ glowColor = 'rgba(34, 197, 94, 0.2)';
1058
+ break;
1059
+ case 'bouncy':
1060
+ mainColor = '#f97316';
1061
+ topColor = '#fb923c';
1062
+ shadowColor = '#c2410c';
1063
+ glowColor = 'rgba(249, 115, 22, 0.3)';
1064
+ // Bouncy animation
1065
+ const bounce = Math.sin(Date.now() / 200) * 2;
1066
+ ctx.translate(0, bounce);
1067
+ break;
1068
+ case 'ice':
1069
+ mainColor = '#38bdf8';
1070
+ topColor = '#7dd3fc';
1071
+ shadowColor = '#0369a1';
1072
+ glowColor = 'rgba(56, 189, 248, 0.4)';
1073
+ break;
1074
+ case 'crumbling':
1075
+ mainColor = '#a16207';
1076
+ topColor = '#ca8a04';
1077
+ shadowColor = '#713f12';
1078
+ glowColor = 'rgba(161, 98, 7, 0.2)';
1079
+ if (p.broken) {
1080
+ ctx.globalAlpha = 0.5;
1081
+ }
1082
+ break;
1083
+ case 'cloud':
1084
+ mainColor = 'rgba(255, 255, 255, 0.8)';
1085
+ topColor = 'rgba(255, 255, 255, 0.95)';
1086
+ shadowColor = 'rgba(200, 200, 200, 0.5)';
1087
+ glowColor = 'rgba(255, 255, 255, 0.3)';
1088
+ break;
1089
+ case 'golden':
1090
+ mainColor = '#fbbf24';
1091
+ topColor = '#fcd34d';
1092
+ shadowColor = '#b45309';
1093
+ glowColor = 'rgba(251, 191, 36, 0.5)';
1094
+ // Golden shimmer
1095
+ ctx.shadowColor = '#fbbf24';
1096
+ ctx.shadowBlur = 15 + Math.sin(Date.now() / 100) * 5;
1097
+ break;
1098
+ default:
1099
+ mainColor = '#22c55e';
1100
+ topColor = '#4ade80';
1101
+ shadowColor = '#15803d';
1102
+ glowColor = 'rgba(34, 197, 94, 0.2)';
1103
+ }
1104
+
1105
+ // Moving platform indicator
1106
+ if (p.vx !== 0) {
1107
+ ctx.shadowColor = '#60a5fa';
1108
+ ctx.shadowBlur = 10;
1109
+ }
1110
+
1111
+ // Glow effect
1112
+ ctx.fillStyle = glowColor;
1113
+ ctx.beginPath();
1114
+ roundRect(ctx, p.x - 5, screenY - 5, p.w + 10, p.h + 10, 12);
1115
+ ctx.fill();
1116
+
1117
+ // Shadow
1118
+ ctx.fillStyle = shadowColor;
1119
+ ctx.beginPath();
1120
+ roundRect(ctx, p.x, screenY + 4, p.w, p.h, 8);
1121
+ ctx.fill();
1122
+
1123
+ // Main body
1124
+ ctx.fillStyle = mainColor;
1125
+ ctx.beginPath();
1126
+ roundRect(ctx, p.x, screenY, p.w, p.h, 8);
1127
+ ctx.fill();
1128
+
1129
+ // Top highlight
1130
+ ctx.fillStyle = topColor;
1131
+ ctx.beginPath();
1132
+ roundRect(ctx, p.x + 3, screenY + 2, p.w - 6, p.h/2 - 1, 5);
1133
+ ctx.fill();
1134
+
1135
+ // Decorative details
1136
+ if (p.type === 'bouncy') {
1137
+ // Spring coils
1138
+ ctx.strokeStyle = '#ea580c';
1139
+ ctx.lineWidth = 2;
1140
  ctx.beginPath();
1141
+ for (let i = 0; i < 3; i++) {
1142
+ const cx = p.x + 15 + i * 20;
1143
+ ctx.moveTo(cx, screenY + p.h);
1144
+ ctx.quadraticCurveTo(cx + 5, screenY + p.h + 8, cx + 10, screenY + p.h);
1145
+ }
1146
  ctx.stroke();
1147
+ }
1148
+
1149
+ if (p.type === 'ice') {
1150
+ // Ice crystals
1151
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
1152
+ for (let i = 0; i < 3; i++) {
1153
+ const ix = p.x + 10 + i * 20;
1154
+ ctx.beginPath();
1155
+ ctx.moveTo(ix, screenY + 4);
1156
+ ctx.lineTo(ix + 4, screenY + 10);
1157
+ ctx.lineTo(ix - 4, screenY + 10);
1158
+ ctx.closePath();
1159
+ ctx.fill();
 
 
 
 
 
 
 
1160
  }
1161
  }
1162
+
1163
+ // Draw special items on platform
1164
+ if (p.special && !p.specialCollected) {
1165
+ drawSpecialItem(p.x + p.w/2, screenY - 25, p.special);
1166
+ }
1167
+
1168
+ ctx.restore();
1169
+ }
1170
 
1171
+ function drawSpecialItem(x, y, type) {
1172
+ ctx.save();
1173
+
1174
+ const bob = Math.sin(Date.now() / 200) * 3;
1175
+ const glow = 10 + Math.sin(Date.now() / 150) * 5;
1176
+
1177
+ ctx.translate(x, y + bob);
1178
+
1179
+ switch(type) {
1180
+ case 'spring':
1181
+ ctx.shadowColor = '#f97316';
1182
+ ctx.shadowBlur = glow;
1183
+ ctx.fillStyle = '#f97316';
1184
+ ctx.beginPath();
1185
+ ctx.ellipse(0, 0, 15, 10, 0, 0, Math.PI * 2);
1186
+ ctx.fill();
1187
+ ctx.fillStyle = '#fbbf24';
1188
+ ctx.beginPath();
1189
+ ctx.ellipse(0, -3, 12, 6, 0, 0, Math.PI * 2);
1190
+ ctx.fill();
1191
+ // Spring icon
1192
+ ctx.strokeStyle = '#fff';
1193
+ ctx.lineWidth = 2;
1194
+ ctx.beginPath();
1195
+ ctx.moveTo(-6, 3);
1196
+ for (let i = 0; i < 4; i++) {
1197
+ ctx.lineTo(-6 + i * 4, i % 2 ? -3 : 3);
1198
+ }
1199
+ ctx.stroke();
1200
+ break;
1201
+
1202
+ case 'jetpack':
1203
+ ctx.shadowColor = '#ef4444';
1204
+ ctx.shadowBlur = glow;
1205
+ ctx.fillStyle = '#64748b';
1206
+ roundRect(ctx, -12, -15, 24, 30, 5);
1207
+ ctx.fill();
1208
+ ctx.fillStyle = '#ef4444';
1209
+ ctx.beginPath();
1210
+ ctx.ellipse(-6, 18, 5, 8, 0, 0, Math.PI * 2);
1211
+ ctx.ellipse(6, 18, 5, 8, 0, 0, Math.PI * 2);
1212
+ ctx.fill();
1213
+ ctx.fillStyle = '#fbbf24';
1214
+ ctx.beginPath();
1215
+ ctx.ellipse(-6, 22, 3, 5 + Math.sin(Date.now()/50) * 2, 0, 0, Math.PI * 2);
1216
+ ctx.ellipse(6, 22, 3, 5 + Math.sin(Date.now()/50 + 1) * 2, 0, 0, Math.PI * 2);
1217
+ ctx.fill();
1218
+ break;
1219
+
1220
+ case 'shield':
1221
+ ctx.shadowColor = '#3b82f6';
1222
+ ctx.shadowBlur = glow;
1223
+ ctx.fillStyle = '#3b82f6';
1224
+ ctx.beginPath();
1225
+ ctx.moveTo(0, -15);
1226
+ ctx.lineTo(15, -5);
1227
+ ctx.lineTo(15, 5);
1228
+ ctx.lineTo(0, 18);
1229
+ ctx.lineTo(-15, 5);
1230
+ ctx.lineTo(-15, -5);
1231
+ ctx.closePath();
1232
+ ctx.fill();
1233
+ ctx.fillStyle = '#60a5fa';
1234
+ ctx.beginPath();
1235
+ ctx.moveTo(0, -10);
1236
+ ctx.lineTo(10, -3);
1237
+ ctx.lineTo(10, 3);
1238
+ ctx.lineTo(0, 12);
1239
+ ctx.lineTo(-10, 3);
1240
+ ctx.lineTo(-10, -3);
1241
+ ctx.closePath();
1242
+ ctx.fill();
1243
+ break;
1244
+
1245
+ case 'magnet':
1246
+ ctx.shadowColor = '#a855f7';
1247
+ ctx.shadowBlur = glow;
1248
+ // U-shape magnet
1249
+ ctx.strokeStyle = '#ef4444';
1250
+ ctx.lineWidth = 6;
1251
+ ctx.lineCap = 'round';
1252
+ ctx.beginPath();
1253
+ ctx.arc(0, 0, 12, Math.PI, 0, false);
1254
+ ctx.stroke();
1255
+ ctx.strokeStyle = '#3b82f6';
1256
+ ctx.beginPath();
1257
+ ctx.moveTo(-12, 0);
1258
+ ctx.lineTo(-12, 10);
1259
+ ctx.moveTo(12, 0);
1260
+ ctx.lineTo(12, 10);
1261
+ ctx.stroke();
1262
+ break;
1263
+ }
1264
+
1265
+ ctx.restore();
1266
+ }
1267
 
1268
+ function drawCollectible(c) {
1269
+ if (c.collected) return;
1270
+
1271
+ const screenY = c.y - cameraY;
1272
+ if (screenY < -50 || screenY > CONFIG.GAME_H + 50) return;
1273
+
1274
+ ctx.save();
1275
+
1276
+ const bob = Math.sin(Date.now() / 200 + c.id) * 4;
1277
+ const rotation = Date.now() / 500;
1278
+ const scale = 0.9 + Math.sin(Date.now() / 300) * 0.1;
1279
+
1280
+ ctx.translate(c.x + 15, screenY + bob);
1281
+ ctx.rotate(c.type === 'coin' ? rotation : 0);
1282
+ ctx.scale(scale, scale);
1283
+
1284
+ if (c.type === 'coin') {
1285
+ // Coin glow
1286
+ ctx.shadowColor = '#fbbf24';
1287
+ ctx.shadowBlur = 15;
1288
 
1289
+ // Coin body
1290
+ ctx.fillStyle = '#fbbf24';
1291
+ ctx.beginPath();
1292
+ ctx.ellipse(0, 0, 12, 12, 0, 0, Math.PI * 2);
1293
+ ctx.fill();
 
 
 
 
1294
 
1295
+ // Inner circle
1296
+ ctx.fillStyle = '#fcd34d';
1297
  ctx.beginPath();
1298
+ ctx.ellipse(0, 0, 8, 8, 0, 0, Math.PI * 2);
1299
  ctx.fill();
1300
+
1301
+ // Star or dollar sign
1302
+ ctx.fillStyle = '#f59e0b';
1303
+ ctx.font = 'bold 12px Arial';
1304
+ ctx.textAlign = 'center';
1305
+ ctx.textBaseline = 'middle';
1306
+ ctx.fillText('$', 0, 1);
1307
+ } else {
1308
+ // Gem glow
1309
+ ctx.shadowColor = '#a855f7';
1310
+ ctx.shadowBlur = 20;
1311
+
1312
+ // Gem shape
1313
+ ctx.fillStyle = '#a855f7';
1314
  ctx.beginPath();
1315
+ ctx.moveTo(0, -15);
1316
+ ctx.lineTo(12, -5);
1317
+ ctx.lineTo(8, 12);
1318
+ ctx.lineTo(-8, 12);
1319
+ ctx.lineTo(-12, -5);
1320
+ ctx.closePath();
1321
  ctx.fill();
 
 
 
 
 
 
1322
 
1323
+ // Highlight
1324
+ ctx.fillStyle = '#c084fc';
 
 
 
1325
  ctx.beginPath();
1326
+ ctx.moveTo(0, -12);
1327
+ ctx.lineTo(8, -4);
1328
+ ctx.lineTo(0, 0);
1329
+ ctx.lineTo(-8, -4);
1330
+ ctx.closePath();
1331
+ ctx.fill();
1332
+
1333
+ // Sparkle
1334
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
1335
+ ctx.beginPath();
1336
+ ctx.arc(-4, -6, 2, 0, Math.PI * 2);
1337
+ ctx.fill();
1338
  }
1339
+
1340
+ ctx.restore();
1341
+ }
1342
 
1343
+ function drawEnemy(e) {
1344
+ if (!e.active) return;
1345
+
1346
+ const screenY = e.y - cameraY;
1347
+ if (screenY < -100 || screenY > CONFIG.GAME_H + 100) return;
1348
+
1349
+ ctx.save();
1350
+ ctx.translate(e.x + 20, screenY + 20);
1351
+
1352
+ const bob = Math.sin(Date.now() / 200) * 3;
1353
+
1354
+ switch(e.type) {
1355
+ case 'slime':
1356
+ // Slime body
1357
+ ctx.fillStyle = '#22c55e';
1358
+ ctx.shadowColor = '#22c55e';
1359
+ ctx.shadowBlur = 10;
1360
+
1361
+ const squish = 1 + Math.sin(Date.now() / 150) * 0.1;
1362
+ ctx.scale(squish, 1/squish);
1363
+
1364
+ ctx.beginPath();
1365
+ ctx.ellipse(0, bob + 5, 22, 18, 0, 0, Math.PI * 2);
1366
+ ctx.fill();
1367
+
1368
+ // Eyes
1369
+ ctx.fillStyle = '#fff';
1370
+ ctx.beginPath();
1371
+ ctx.ellipse(-8, bob - 2, 6, 8, 0, 0, Math.PI * 2);
1372
+ ctx.ellipse(8, bob - 2, 6, 8, 0, 0, Math.PI * 2);
1373
+ ctx.fill();
1374
+
1375
+ ctx.fillStyle = '#000';
1376
+ ctx.beginPath();
1377
+ ctx.arc(-6, bob, 3, 0, Math.PI * 2);
1378
+ ctx.arc(10, bob, 3, 0, Math.PI * 2);
1379
+ ctx.fill();
1380
+ break;
1381
+
1382
+ case 'bat':
1383
+ ctx.translate(0, bob);
1384
+
1385
+ // Wing flap
1386
+ const wingAngle = Math.sin(Date.now() / 50) * 0.5;
1387
+
1388
+ // Body
1389
+ ctx.fillStyle = '#581c87';
1390
+ ctx.shadowColor = '#a855f7';
1391
+ ctx.shadowBlur = 8;
1392
+ ctx.beginPath();
1393
+ ctx.ellipse(0, 0, 12, 10, 0, 0, Math.PI * 2);
1394
+ ctx.fill();
1395
+
1396
+ // Wings
1397
+ ctx.save();
1398
+ ctx.rotate(wingAngle);
1399
+ ctx.beginPath();
1400
+ ctx.ellipse(-20, 0, 15, 8, -0.3, 0, Math.PI * 2);
1401
+ ctx.fill();
1402
+ ctx.restore();
1403
+
1404
+ ctx.save();
1405
+ ctx.rotate(-wingAngle);
1406
+ ctx.beginPath();
1407
+ ctx.ellipse(20, 0, 15, 8, 0.3, 0, Math.PI * 2);
1408
+ ctx.fill();
1409
+ ctx.restore();
1410
+
1411
+ // Eyes
1412
+ ctx.fillStyle = '#ef4444';
1413
+ ctx.beginPath();
1414
+ ctx.arc(-5, -2, 3, 0, Math.PI * 2);
1415
+ ctx.arc(5, -2, 3, 0, Math.PI * 2);
1416
+ ctx.fill();
1417
+ break;
1418
+
1419
+ case 'spike':
1420
+ ctx.shadowColor = '#ef4444';
1421
+ ctx.shadowBlur = 10;
1422
+
1423
+ // Spike ball
1424
+ ctx.fillStyle = '#6b7280';
1425
+ ctx.beginPath();
1426
+ ctx.arc(0, 0, 15, 0, Math.PI * 2);
1427
+ ctx.fill();
1428
+
1429
+ // Spikes
1430
+ ctx.fillStyle = '#374151';
1431
+ for (let i = 0; i < 8; i++) {
1432
+ const angle = (i / 8) * Math.PI * 2 + Date.now() / 1000;
1433
+ ctx.save();
1434
+ ctx.rotate(angle);
1435
+ ctx.beginPath();
1436
+ ctx.moveTo(12, -4);
1437
+ ctx.lineTo(25, 0);
1438
+ ctx.lineTo(12, 4);
1439
+ ctx.closePath();
1440
+ ctx.fill();
1441
+ ctx.restore();
1442
+ }
1443
+
1444
+ // Angry face
1445
+ ctx.fillStyle = '#ef4444';
1446
+ ctx.beginPath();
1447
+ ctx.arc(-5, -3, 3, 0, Math.PI * 2);
1448
+ ctx.arc(5, -3, 3, 0, Math.PI * 2);
1449
+ ctx.fill();
1450
+ break;
1451
+ }
1452
+
1453
+ ctx.restore();
1454
+ }
1455
 
1456
+ function drawPlayer(p, screenY, isMe, name, char) {
1457
+ ctx.save();
1458
+ ctx.translate(p.x + p.w/2, screenY + p.h/2);
1459
+
1460
+ // Apply squash and stretch
1461
+ if (isMe) {
1462
+ ctx.scale(player.squash, player.stretch);
1463
+ }
1464
+
1465
+ // Flip based on direction
1466
+ if (!p.faceRight) ctx.scale(-1, 1);
1467
+
1468
+ // Draw trail for me
1469
+ if (isMe && player.trail.length > 2) {
1470
+ ctx.globalAlpha = 0.3;
1471
+ for (let i = 0; i < player.trail.length - 1; i++) {
1472
+ const t = player.trail[i];
1473
+ const alpha = i / player.trail.length * 0.5;
1474
+ ctx.fillStyle = `rgba(79, 172, 254, ${alpha})`;
1475
+ ctx.beginPath();
1476
+ ctx.arc(t.x - p.x - p.w/2, t.y - cameraY - screenY, 10 - i * 0.5, 0, Math.PI * 2);
1477
+ ctx.fill();
1478
+ }
1479
+ ctx.globalAlpha = 1;
1480
+ }
1481
+
1482
+ // Character glow
1483
+ if (isMe) {
1484
+ ctx.shadowColor = '#4facfe';
1485
+ ctx.shadowBlur = 15 + Math.sin(Date.now() / 200) * 5;
1486
+ } else {
1487
+ ctx.shadowColor = '#ff6b6b';
1488
+ ctx.shadowBlur = 10;
1489
+ }
1490
+
1491
+ // Shield effect
1492
+ if (isMe && player.hasShield) {
1493
+ ctx.strokeStyle = `rgba(59, 130, 246, ${0.5 + Math.sin(Date.now() / 100) * 0.3})`;
1494
+ ctx.lineWidth = 3;
1495
+ ctx.beginPath();
1496
+ ctx.arc(0, 0, 35, 0, Math.PI * 2);
1497
+ ctx.stroke();
1498
 
1499
+ // Shield particles
1500
+ for (let i = 0; i < 6; i++) {
1501
+ const angle = Date.now() / 500 + i * Math.PI / 3;
1502
+ const sx = Math.cos(angle) * 32;
1503
+ const sy = Math.sin(angle) * 32;
1504
+ ctx.fillStyle = 'rgba(96, 165, 250, 0.8)';
1505
+ ctx.beginPath();
1506
+ ctx.arc(sx, sy, 4, 0, Math.PI * 2);
1507
+ ctx.fill();
 
 
 
 
1508
  }
1509
+ }
1510
+
1511
+ // Draw character based on type
1512
+ const charEmoji = {
1513
+ 'dino': '🦖',
1514
+ 'frog': '🐸',
1515
+ 'cat': '🐱',
1516
+ 'robot': '🤖',
1517
+ 'alien': '👽'
1518
+ };
1519
+
1520
+ // Body background
1521
+ ctx.fillStyle = isMe ? '#4facfe' : '#ff6b6b';
1522
+ ctx.beginPath();
1523
+ ctx.arc(0, 0, 20, 0, Math.PI * 2);
1524
+ ctx.fill();
1525
+
1526
+ // Character emoji
1527
+ ctx.font = '28px Arial';
1528
+ ctx.textAlign = 'center';
1529
+ ctx.textBaseline = 'middle';
1530
+ ctx.fillText(charEmoji[char] || '🦖', 0, 2);
1531
+
1532
+ // Jetpack flames
1533
+ if (isMe && player.hasJetpack) {
1534
+ const flameHeight = 15 + Math.random() * 10;
1535
+ const gradient = ctx.createLinearGradient(0, 20, 0, 20 + flameHeight);
1536
+ gradient.addColorStop(0, '#fbbf24');
1537
+ gradient.addColorStop(0.5, '#f97316');
1538
+ gradient.addColorStop(1, 'rgba(239, 68, 68, 0)');
1539
+ ctx.fillStyle = gradient;
1540
+ ctx.beginPath();
1541
+ ctx.moveTo(-8, 20);
1542
+ ctx.quadraticCurveTo(-10, 20 + flameHeight/2, 0, 20 + flameHeight);
1543
+ ctx.quadraticCurveTo(10, 20 + flameHeight/2, 8, 20);
1544
+ ctx.closePath();
1545
+ ctx.fill();
1546
+ }
1547
+
1548
+ ctx.restore();
1549
+
1550
+ // Draw name tag
1551
+ if (name) {
1552
+ ctx.save();
1553
+ ctx.fillStyle = isMe ? '#4facfe' : '#ff6b6b';
1554
+ ctx.font = 'bold 12px Nunito';
1555
+ ctx.textAlign = 'center';
1556
+ ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
1557
+ ctx.shadowBlur = 4;
1558
+
1559
+ // Name background
1560
+ const nameWidth = ctx.measureText(name).width + 16;
1561
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
1562
+ roundRect(ctx, p.x + p.w/2 - nameWidth/2, screenY - 28, nameWidth, 20, 10);
1563
+ ctx.fill();
1564
+
1565
+ ctx.fillStyle = isMe ? '#4facfe' : '#ffffff';
1566
+ ctx.fillText(name, p.x + p.w/2, screenY - 15);
1567
+ ctx.restore();
1568
+ }
1569
+ }
1570
+
1571
+ function roundRect(ctx, x, y, w, h, r) {
1572
+ ctx.beginPath();
1573
+ ctx.moveTo(x + r, y);
1574
+ ctx.lineTo(x + w - r, y);
1575
+ ctx.quadraticCurveTo(x + w, y, x + w, y + r);
1576
+ ctx.lineTo(x + w, y + h - r);
1577
+ ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
1578
+ ctx.lineTo(x + r, y + h);
1579
+ ctx.quadraticCurveTo(x, y + h, x, y + h - r);
1580
+ ctx.lineTo(x, y + r);
1581
+ ctx.quadraticCurveTo(x, y, x + r, y);
1582
+ ctx.closePath();
1583
+ }
1584
+
1585
+ // ============================================
1586
+ // 🎮 GAME LOGIC
1587
+ // ============================================
1588
+
1589
+ function update(dt) {
1590
+ if (gameState !== 'PLAYING') return;
1591
+
1592
+ // Input handling
1593
+ if (keys.left) player.vx -= CONFIG.MOVE_SPEED * 0.15;
1594
+ if (keys.right) player.vx += CONFIG.MOVE_SPEED * 0.15;
1595
+
1596
+ player.vx = Math.max(-CONFIG.MOVE_SPEED, Math.min(CONFIG.MOVE_SPEED, player.vx));
1597
+ player.vx *= CONFIG.FRICTION;
1598
+
1599
+ player.x += player.vx;
1600
+
1601
+ // Screen wrap
1602
+ if (player.x + player.w < 0) player.x = CONFIG.GAME_W;
1603
+ if (player.x > CONFIG.GAME_W) player.x = -player.w;
1604
+
1605
+ // Direction
1606
+ if (Math.abs(player.vx) > 0.5) {
1607
+ player.faceRight = player.vx > 0;
1608
+ }
1609
+
1610
+ // Gravity / Jetpack
1611
+ if (player.hasJetpack && player.jetpackFuel > 0) {
1612
+ player.vy += CONFIG.JETPACK_FORCE;
1613
+ player.vy = Math.max(player.vy, -15);
1614
+ player.jetpackFuel -= dt;
1615
+
1616
+ if (player.jetpackFuel <= 0) {
1617
+ player.hasJetpack = false;
1618
+ addFloatingText(player.x, player.y - cameraY, 'Jetpack Empty!', '#ef4444');
1619
  }
1620
+
1621
+ // Jetpack particles
1622
+ if (Math.random() > 0.3) {
1623
+ spawnParticles(player.x + player.w/2, player.y + player.h, 'jetpack', 2);
 
 
 
 
1624
  }
1625
+
1626
+ $('jetpackTimer').style.width = (player.jetpackFuel / 5) * 100 + '%';
1627
+ } else {
1628
+ player.vy += CONFIG.GRAVITY;
 
 
 
 
 
1629
  }
1630
+
1631
+ player.y += player.vy;
1632
+
1633
+ // Squash and stretch animation
1634
+ if (player.vy < -2) {
1635
+ player.stretch = 1.2;
1636
+ player.squash = 0.85;
1637
+ } else if (player.vy > 2) {
1638
+ player.stretch = 0.85;
1639
+ player.squash = 1.15;
1640
+ } else {
1641
+ player.stretch += (1 - player.stretch) * 0.2;
1642
+ player.squash += (1 - player.squash) * 0.2;
1643
+ }
1644
+
1645
+ // Update trail
1646
+ player.trail.push({ x: player.x + player.w/2, y: player.y + player.h/2 });
1647
+ if (player.trail.length > CONFIG.TRAIL_LENGTH) {
1648
+ player.trail.shift();
1649
+ }
1650
+
1651
+ // Shield timer
1652
+ if (player.hasShield) {
1653
+ player.shieldTime -= dt;
1654
+ if (player.shieldTime <= 0) {
1655
+ player.hasShield = false;
1656
+ addFloatingText(player.x, player.y - cameraY, 'Shield Gone!', '#3b82f6');
1657
+ }
1658
+ $('shieldTimer').style.width = (player.shieldTime / 8) * 100 + '%';
1659
+
1660
+ // Shield particles
1661
+ if (Math.random() > 0.7) {
1662
+ spawnParticles(player.x + player.w/2, player.y + player.h/2, 'shield', 1);
1663
+ }
1664
+ }
1665
+
1666
+ // Magnet timer
1667
+ if (player.hasMagnet) {
1668
+ player.magnetTime -= dt;
1669
+ if (player.magnetTime <= 0) {
1670
+ player.hasMagnet = false;
1671
+ }
1672
+ $('magnetTimer').style.width = (player.magnetTime / 10) * 100 + '%';
1673
+ }
1674
+
1675
+ // Camera follow
1676
+ const threshold = cameraY + CONFIG.GAME_H * 0.4;
1677
+ if (player.y < threshold) {
1678
+ const diff = threshold - player.y;
1679
+ cameraY -= diff;
1680
+ score += Math.floor(diff * 0.5);
1681
+ updateScoreDisplay();
1682
+ }
1683
+
1684
+ // Platform collision
1685
+ if (player.vy > 0) {
1686
+ for (let p of platforms) {
1687
+ if (checkPlatformCollision(p)) {
1688
+ handlePlatformLand(p);
1689
+ break;
1690
  }
1691
  }
1692
+ }
1693
+
1694
+ // Collectible collision
1695
+ for (let c of collectibles) {
1696
+ if (!c.collected && checkCollectibleCollision(c)) {
1697
+ collectItem(c);
1698
  }
1699
  }
1700
+
1701
+ // Magnet effect
1702
+ if (player.hasMagnet) {
1703
+ for (let c of collectibles) {
1704
+ if (!c.collected) {
1705
+ const dx = (player.x + player.w/2) - (c.x + 15);
1706
+ const dy = (player.y + player.h/2) - c.y;
1707
+ const dist = Math.sqrt(dx*dx + dy*dy);
1708
+ if (dist < 150) {
1709
+ c.x += dx * 0.1;
1710
+ c.y += dy * 0.1;
1711
+ }
1712
+ }
1713
  }
1714
  }
1715
+
1716
+ // Enemy collision
1717
+ for (let e of enemies) {
1718
+ if (e.active && checkEnemyCollision(e)) {
1719
+ handleEnemyCollision(e);
1720
+ }
1721
+ }
1722
+
1723
+ // Update particles
1724
+ particles = particles.filter(p => p.update());
1725
+
1726
+ // Update floating texts
1727
+ floatingTexts = floatingTexts.filter(t => {
1728
+ t.life -= dt * 2;
1729
+ t.y -= 1;
1730
+ return t.life > 0;
1731
  });
1732
+
1733
+ // Screen shake decay
1734
+ screenShake *= 0.9;
1735
+
1736
+ // Death check
1737
+ if (player.y > cameraY + CONFIG.GAME_H + 50) {
1738
+ gameOver();
1739
+ }
1740
+
1741
+ // Send update to server
1742
+ socket.emit('update', {
1743
+ x: player.x,
1744
+ y: player.y,
1745
+ vx: player.vx,
1746
+ faceRight: player.faceRight,
1747
+ score: score,
1748
+ char: selectedChar
1749
  });
1750
+
1751
+ // Update power-up UI
1752
+ $('pwrJetpack').classList.toggle('active', player.hasJetpack);
1753
+ $('pwrShield').classList.toggle('active', player.hasShield);
1754
+ $('pwrMagnet').classList.toggle('active', player.hasMagnet);
1755
+ }
1756
+
1757
+ function checkPlatformCollision(p) {
1758
+ return player.x + player.w > p.x + 5 &&
1759
+ player.x < p.x + p.w - 5 &&
1760
+ player.y + player.h > p.y &&
1761
+ player.y + player.h < p.y + p.h + player.vy + 5;
1762
+ }
1763
+
1764
+ function handlePlatformLand(p) {
1765
+ jumpCount++;
1766
+
1767
+ // Handle different platform types
1768
+ switch(p.type) {
1769
+ case 'bouncy':
1770
+ player.vy = CONFIG.SUPER_JUMP;
1771
+ spawnParticles(player.x + player.w/2, player.y + player.h, 'jump', 15);
1772
+ addFloatingText(player.x, player.y - cameraY - 20, 'BOING!', '#f97316');
1773
+ screenShake = 5;
1774
+ break;
1775
+
1776
+ case 'crumbling':
1777
+ if (!p.broken) {
1778
+ player.vy = CONFIG.JUMP_FORCE;
1779
+ p.broken = true;
1780
+ p.wobble = 0.5;
1781
+ spawnParticles(p.x + p.w/2, p.y, 'dust', 20);
1782
+ setTimeout(() => {
1783
+ platforms = platforms.filter(pl => pl.id !== p.id);
1784
+ }, 300);
1785
+ }
1786
+ break;
1787
+
1788
+ case 'ice':
1789
+ player.vy = CONFIG.JUMP_FORCE;
1790
+ player.vx *= 1.5; // Slippery!
1791
+ spawnParticles(player.x + player.w/2, player.y + player.h, 'star', 8);
1792
+ break;
1793
+
1794
+ case 'cloud':
1795
+ // One-way platform, can jump through from below
1796
+ player.vy = CONFIG.JUMP_FORCE * 0.8;
1797
+ spawnParticles(player.x + player.w/2, player.y + player.h, 'dust', 10);
1798
+ break;
1799
+
1800
+ case 'golden':
1801
+ player.vy = CONFIG.JUMP_FORCE;
1802
+ coins += 50;
1803
+ score += 100;
1804
+ updateScoreDisplay();
1805
+ spawnParticles(player.x + player.w/2, player.y + player.h, 'coin', 20);
1806
+ addFloatingText(player.x, player.y - cameraY - 20, '+100 BONUS!', '#fbbf24');
1807
+ screenShake = 3;
1808
+ break;
1809
+
1810
+ default:
1811
+ player.vy = CONFIG.JUMP_FORCE;
1812
+ spawnParticles(player.x + player.w/2, player.y + player.h, 'jump', 8);
1813
+ }
1814
+
1815
+ // Handle special items
1816
+ if (p.special && !p.specialCollected) {
1817
+ collectSpecial(p);
1818
+ }
1819
+
1820
+ // Combo system
1821
+ if (combo > 0) {
1822
+ combo++;
1823
+ maxCombo = Math.max(maxCombo, combo);
1824
+ if (combo > 2) {
1825
+ addFloatingText(player.x, player.y - cameraY - 40, `x${combo} COMBO!`, '#ff6b6b');
1826
+ score += combo * 10;
1827
+ updateScoreDisplay();
1828
+ }
1829
+ } else {
1830
+ combo = 1;
1831
+ }
1832
+
1833
+ // Reset combo after delay
1834
+ clearTimeout(window.comboTimeout);
1835
+ window.comboTimeout = setTimeout(() => { combo = 0; }, 1500);
1836
+
1837
+ updateComboDisplay();
1838
+ }
1839
 
1840
+ function collectSpecial(p) {
1841
+ p.specialCollected = true;
1842
+
1843
+ switch(p.special) {
1844
+ case 'spring':
1845
+ player.vy = CONFIG.SUPER_JUMP * 1.5;
1846
+ spawnParticles(player.x + player.w/2, player.y + player.h, 'jump', 25);
1847
+ addFloatingText(player.x, player.y - cameraY - 30, 'SUPER JUMP!', '#f97316');
1848
+ screenShake = 8;
1849
+ break;
1850
+
1851
+ case 'jetpack':
1852
+ player.hasJetpack = true;
1853
+ player.jetpackFuel = 5; // 5 seconds
1854
+ addFloatingText(player.x, player.y - cameraY - 30, 'JETPACK!', '#ef4444');
1855
+ addEventFeed('🚀 Jetpack activated!');
1856
+ break;
1857
+
1858
+ case 'shield':
1859
+ player.hasShield = true;
1860
+ player.shieldTime = 8; // 8 seconds
1861
+ addFloatingText(player.x, player.y - cameraY - 30, 'SHIELD!', '#3b82f6');
1862
+ addEventFeed('🛡️ Shield activated!');
1863
+ break;
1864
+
1865
+ case 'magnet':
1866
+ player.hasMagnet = true;
1867
+ player.magnetTime = 10; // 10 seconds
1868
+ addFloatingText(player.x, player.y - cameraY - 30, 'MAGNET!', '#a855f7');
1869
+ addEventFeed('🧲 Coin magnet active!');
1870
+ break;
1871
+ }
1872
+ }
1873
+
1874
+ function checkCollectibleCollision(c) {
1875
+ return player.x + player.w > c.x &&
1876
+ player.x < c.x + 30 &&
1877
+ player.y + player.h > c.y - 15 &&
1878
+ player.y < c.y + 15;
1879
+ }
1880
+
1881
+ function collectItem(c) {
1882
+ c.collected = true;
1883
+ const value = c.value * (1 + combo * 0.1);
1884
+ coins += c.type === 'coin' ? 1 : 5;
1885
+ score += Math.floor(value);
1886
+ updateScoreDisplay();
1887
+
1888
+ spawnParticles(c.x + 15, c.y, c.type, 10);
1889
+ addFloatingText(c.x, c.y - cameraY, '+' + Math.floor(value), c.type === 'coin' ? '#fbbf24' : '#a855f7');
1890
+ }
1891
+
1892
+ function checkEnemyCollision(e) {
1893
+ const ex = e.x;
1894
+ const ey = e.y;
1895
+ const ew = 40;
1896
+ const eh = 40;
1897
+
1898
+ return player.x + player.w > ex &&
1899
+ player.x < ex + ew &&
1900
+ player.y + player.h > ey &&
1901
+ player.y < ey + eh;
1902
+ }
1903
+
1904
+ function handleEnemyCollision(e) {
1905
+ // If coming from above and moving down, defeat enemy
1906
+ if (player.vy > 0 && player.y + player.h < e.y + 20) {
1907
+ e.active = false;
1908
+ enemiesDefeated++;
1909
+ player.vy = CONFIG.JUMP_FORCE;
1910
+ score += 50;
1911
+ updateScoreDisplay();
1912
+ spawnParticles(e.x + 20, e.y + 20, 'enemy', 20);
1913
+ addFloatingText(e.x, e.y - cameraY, '+50 ENEMY!', '#a855f7');
1914
+ screenShake = 5;
1915
+ addEventFeed('💀 Enemy defeated!');
1916
+ } else if (!player.hasShield) {
1917
+ // Hit by enemy
1918
+ spawnParticles(player.x + player.w/2, player.y + player.h/2, 'death', 30);
1919
+ screenShake = 15;
1920
+ gameOver();
1921
+ } else {
1922
+ // Shield protects
1923
+ e.active = false;
1924
+ spawnParticles(e.x + 20, e.y + 20, 'shield', 15);
1925
+ addFloatingText(player.x, player.y - cameraY - 20, 'BLOCKED!', '#3b82f6');
1926
+ }
1927
+ }
1928
+
1929
+ function addFloatingText(x, y, text, color) {
1930
+ floatingTexts.push({ x, y, text, color, life: 1 });
1931
+ }
1932
+
1933
+ function addEventFeed(text) {
1934
+ const feed = $('eventFeed');
1935
+ const item = document.createElement('div');
1936
+ item.className = 'event-item';
1937
+ item.textContent = text;
1938
+ feed.insertBefore(item, feed.firstChild);
1939
+
1940
+ // Remove old items
1941
+ while (feed.children.length > 5) {
1942
+ feed.removeChild(feed.lastChild);
1943
+ }
1944
+
1945
+ // Auto-remove after 5 seconds
1946
+ setTimeout(() => item.remove(), 5000);
1947
+ }
1948
+
1949
+ function updateScoreDisplay() {
1950
+ $('scoreValue').textContent = score.toLocaleString();
1951
+ }
1952
+
1953
+ function updateComboDisplay() {
1954
+ const disp = $('comboDisplay');
1955
+ if (combo > 2) {
1956
+ disp.textContent = `x${combo} COMBO!`;
1957
+ disp.classList.add('active');
1958
+ } else {
1959
+ disp.classList.remove('active');
1960
+ }
1961
+ }
1962
+
1963
+ function gameOver() {
1964
+ gameState = 'GAMEOVER';
1965
+
1966
+ $('finalScoreValue').textContent = score.toLocaleString();
1967
+ $('heightReached').textContent = Math.floor(Math.abs(cameraY) / 100);
1968
+ $('bestCombo').textContent = maxCombo;
1969
+ $('statCoins').textContent = coins;
1970
+ $('statEnemies').textContent = enemiesDefeated;
1971
+ $('statJumps').textContent = jumpCount;
1972
+
1973
+ $('gameOverScreen').style.display = 'flex';
1974
+
1975
+ socket.emit('player_died');
1976
+ }
1977
+
1978
+ function draw() {
1979
+ // Apply screen shake
1980
+ ctx.save();
1981
+ if (screenShake > 0.5) {
1982
+ ctx.translate(
1983
+ (Math.random() - 0.5) * screenShake,
1984
+ (Math.random() - 0.5) * screenShake
1985
+ );
1986
+ }
1987
+
1988
+ // Background
1989
+ drawBackground();
1990
+
1991
+ // Platforms
1992
+ platforms.forEach(drawPlatform);
1993
+
1994
+ // Collectibles
1995
+ collectibles.forEach(drawCollectible);
1996
+
1997
+ // Enemies
1998
+ enemies.forEach(drawEnemy);
1999
+
2000
+ // Other players
2001
+ for (let id in otherPlayers) {
2002
+ if (id === myId) continue;
2003
+ const op = otherPlayers[id];
2004
+ const screenY = op.y - cameraY;
2005
+ if (screenY > -100 && screenY < CONFIG.GAME_H + 100) {
2006
+ drawPlayer(op, screenY, false, op.name, op.char || 'dino');
2007
+ }
2008
+ }
2009
+
2010
+ // Me
2011
+ if (gameState === 'PLAYING') {
2012
+ const screenY = player.y - cameraY;
2013
+ drawPlayer(player, screenY, true, 'YOU', selectedChar);
2014
+ }
2015
+
2016
+ // Particles
2017
+ particles.forEach(p => p.draw(ctx, cameraY));
2018
+
2019
+ // Floating texts
2020
+ ctx.font = 'bold 18px Fredoka One';
2021
+ ctx.textAlign = 'center';
2022
+ floatingTexts.forEach(t => {
2023
+ ctx.globalAlpha = t.life;
2024
+ ctx.fillStyle = t.color;
2025
+ ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
2026
+ ctx.shadowBlur = 4;
2027
+ ctx.fillText(t.text, t.x + player.w/2, t.y);
2028
  });
2029
+ ctx.globalAlpha = 1;
2030
+
2031
+ // Height indicator
2032
+ const height = Math.floor(Math.abs(cameraY) / 100);
2033
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
2034
+ ctx.font = '14px Nunito';
2035
+ ctx.textAlign = 'right';
2036
+ ctx.fillText(height + 'm', CONFIG.GAME_W - 15, CONFIG.GAME_H - 15);
2037
+
2038
+ ctx.restore();
2039
+ }
2040
 
2041
+ function gameLoop(timestamp) {
2042
+ deltaTime = (timestamp - lastTime) / 1000;
2043
+ lastTime = timestamp;
2044
+
2045
+ // Cap delta time
2046
+ if (deltaTime > 0.1) deltaTime = 0.016;
2047
+
2048
+ update(deltaTime);
2049
+ draw();
2050
+
2051
+ requestAnimationFrame(gameLoop);
2052
+ }
2053
 
2054
+ // ============================================
2055
+ // 🔌 SOCKET HANDLERS
2056
+ // ============================================
2057
+
2058
+ socket.on('connect', () => { myId = socket.id; });
2059
+
2060
+ socket.on('world_data', (data) => {
2061
+ platforms = data.platforms || [];
2062
+ collectibles = data.collectibles || [];
2063
+ enemies = data.enemies || [];
2064
+ });
2065
+
2066
+ socket.on('game_update', (data) => {
2067
+ otherPlayers = data.players || {};
2068
+
2069
+ // Update platforms (especially moving ones)
2070
+ if (data.platforms) {
2071
+ data.platforms.forEach(sp => {
2072
+ const idx = platforms.findIndex(p => p.id === sp.id);
2073
+ if (idx !== -1) {
2074
+ platforms[idx].x = sp.x;
2075
+ } else if (!platforms.find(p => p.id === sp.id)) {
2076
+ platforms.push(sp);
2077
+ }
2078
+ });
2079
  }
2080
+
2081
+ // Clean up old platforms
2082
+ platforms = platforms.filter(p => p.y > cameraY - 200);
2083
+ collectibles = collectibles.filter(c => c.y > cameraY - 200);
2084
+ enemies = enemies.filter(e => e.y > cameraY - 200);
2085
+
2086
+ // Update leaderboard
2087
+ let lbHtml = '';
2088
+ const sorted = Object.values(otherPlayers)
2089
+ .sort((a, b) => b.score - a.score)
2090
+ .slice(0, 5);
2091
+
2092
+ sorted.forEach((p, i) => {
2093
+ const isMe = Object.keys(otherPlayers).find(k => otherPlayers[k] === p) === myId;
2094
+ const medal = i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : ' ';
2095
+ lbHtml += `<div class="lb-entry ${isMe ? 'me' : ''}">${medal} ${p.name}<span>${p.score}</span></div>`;
2096
+ });
2097
+
2098
+ $('lbEntries').innerHTML = lbHtml || 'No players yet';
2099
+ });
2100
+
2101
+ socket.on('player_event', (data) => {
2102
+ if (data.type === 'powerup') {
2103
+ addEventFeed(`${data.name} got ${data.item}!`);
2104
+ } else if (data.type === 'record') {
2105
+ addEventFeed(`🏆 ${data.name} reached ${data.height}m!`);
2106
+ }
2107
+ });
2108
+
2109
+ // ============================================
2110
+ // 🎮 GAME CONTROLS
2111
+ // ============================================
2112
+
2113
+ function startGame() {
2114
+ const name = $('usernameInput').value.trim() || 'Player';
2115
+ setCookie('jj_username', name, 30);
2116
+ setCookie('jj_character', selectedChar, 30);
2117
+
2118
+ $('loadingDots').style.display = 'flex';
2119
+
2120
+ socket.emit('join', { name, char: selectedChar });
2121
+
2122
+ setTimeout(() => {
2123
+ $('loginScreen').style.display = 'none';
2124
+ $('hud').style.display = 'flex';
2125
+ $('powerups').style.display = 'flex';
2126
+ initGame();
2127
+ }, 500);
2128
+ }
2129
+
2130
+ function initGame() {
2131
+ score = 0;
2132
+ coins = 0;
2133
+ combo = 0;
2134
+ maxCombo = 0;
2135
+ enemiesDefeated = 0;
2136
+ jumpCount = 0;
2137
+ cameraY = 0;
2138
+ screenShake = 0;
2139
+
2140
+ player.x = CONFIG.GAME_W / 2 - player.w / 2;
2141
+ player.y = CONFIG.GAME_H - 250;
2142
+ player.vx = 0;
2143
+ player.vy = 0;
2144
+ player.hasJetpack = false;
2145
+ player.hasShield = false;
2146
+ player.hasMagnet = false;
2147
+ player.trail = [];
2148
+
2149
+ particles = [];
2150
+ floatingTexts = [];
2151
+
2152
+ $('gameOverScreen').style.display = 'none';
2153
+ updateScoreDisplay();
2154
+
2155
+ gameState = 'PLAYING';
2156
+ }
2157
+
2158
+ function restartGame() {
2159
+ socket.emit('player_restart');
2160
+ initGame();
2161
+ }
2162
+
2163
+ // Keyboard controls
2164
+ window.addEventListener('keydown', e => {
2165
+ if (e.code === 'ArrowLeft' || e.code === 'KeyA') keys.left = true;
2166
+ if (e.code === 'ArrowRight' || e.code === 'KeyD') keys.right = true;
2167
+ });
2168
+
2169
+ window.addEventListener('keyup', e => {
2170
+ if (e.code === 'ArrowLeft' || e.code === 'KeyA') keys.left = false;
2171
+ if (e.code === 'ArrowRight' || e.code === 'KeyD') keys.right = false;
2172
+ });
2173
+
2174
+ // Mobile controls
2175
+ $('btnLeft').addEventListener('touchstart', e => { e.preventDefault(); keys.left = true; });
2176
+ $('btnLeft').addEventListener('touchend', e => { e.preventDefault(); keys.left = false; });
2177
+ $('btnRight').addEventListener('touchstart', e => { e.preventDefault(); keys.right = true; });
2178
+ $('btnRight').addEventListener('touchend', e => { e.preventDefault(); keys.right = false; });
2179
+
2180
+ // Touch anywhere to control
2181
+ canvas.addEventListener('touchstart', e => {
2182
+ e.preventDefault();
2183
+ const rect = canvas.getBoundingClientRect();
2184
+ const x = (e.touches[0].clientX - rect.left) / rect.width * CONFIG.GAME_W;
2185
+ keys.left = x < CONFIG.GAME_W / 2;
2186
+ keys.right = x >= CONFIG.GAME_W / 2;
2187
+ }, { passive: false });
2188
+
2189
+ canvas.addEventListener('touchend', e => {
2190
+ e.preventDefault();
2191
+ keys.left = false;
2192
+ keys.right = false;
2193
+ });
2194
+
2195
+ // Start game loop
2196
+ requestAnimationFrame(gameLoop);
2197
 
2198
  </script>
2199
  </body>
2200
  </html>
2201
  """
2202
 
2203
+ # --- FLASK ROUTES & SOCKET EVENTS ---
2204
 
2205
  @app.route('/')
2206
  def index():
 
2209
  @socketio.on('join')
2210
  def on_join(data):
2211
  players[request.sid] = {
2212
+ 'name': data.get('name', 'Guest')[:12],
2213
+ 'char': data.get('char', 'dino'),
2214
+ 'x': WIDTH/2,
2215
+ 'y': HEIGHT - 200,
2216
+ 'vx': 0,
2217
+ 'score': 0,
2218
+ 'faceRight': True,
2219
+ 'highestY': HEIGHT
2220
  }
2221
+ emit('world_data', {
2222
+ 'platforms': platforms,
2223
+ 'collectibles': collectibles,
2224
+ 'enemies': enemies
2225
+ })
2226
 
2227
  @socketio.on('update')
2228
  def on_update(data):
2229
  if request.sid in players:
2230
  p = players[request.sid]
2231
+ p['x'] = data.get('x', p['x'])
2232
+ p['y'] = data.get('y', p['y'])
2233
+ p['vx'] = data.get('vx', p['vx'])
2234
+ p['faceRight'] = data.get('faceRight', p['faceRight'])
2235
+ p['score'] = data.get('score', p['score'])
2236
+ p['char'] = data.get('char', p.get('char', 'dino'))
2237
 
2238
+ # Track highest point for achievements
2239
+ if p['y'] < p['highestY'] - 5000:
2240
+ p['highestY'] = p['y']
2241
+ height = int(abs(p['y']) / 100)
2242
+ socketio.emit('player_event', {
2243
+ 'type': 'record',
2244
+ 'name': p['name'],
2245
+ 'height': height
2246
+ })
2247
+
2248
+ # Generate more world as players climb
2249
  global highest_y_generated
2250
+ if p['y'] < highest_y_generated + 1500:
2251
+ difficulty = min(abs(highest_y_generated) / 5000, 5)
2252
+ for _ in range(15):
2253
+ gap = 75 + random.random() * (35 + difficulty * 8)
2254
+ new_plat = generate_platform(highest_y_generated - gap, difficulty)
2255
  platforms.append(new_plat)
2256
+
2257
+ if random.random() < 0.4:
2258
+ collectibles.append(generate_collectible(highest_y_generated - gap))
2259
+
2260
+ if random.random() < 0.06 + difficulty * 0.02:
2261
+ enemies.append(generate_enemy(highest_y_generated - gap))
2262
+
2263
  highest_y_generated = new_plat['y']
 
 
 
 
 
 
 
 
2264
 
2265
  @socketio.on('player_died')
2266
  def on_died():
2267
  if request.sid in players:
 
 
2268
  players[request.sid]['score'] = 0
2269
+ players[request.sid]['highestY'] = HEIGHT
2270
+
2271
+ @socketio.on('player_restart')
2272
+ def on_restart():
2273
+ if request.sid in players:
2274
+ players[request.sid]['score'] = 0
2275
+ players[request.sid]['highestY'] = HEIGHT
2276
+ emit('world_data', {
2277
+ 'platforms': platforms[-100:],
2278
+ 'collectibles': [c for c in collectibles if not c['collected']][-50:],
2279
+ 'enemies': [e for e in enemies if e['active']][-20:]
2280
+ })
2281
 
2282
  @socketio.on('disconnect')
2283
  def on_disconnect():
2284
  if request.sid in players:
2285
  del players[request.sid]
2286
 
 
2287
  def server_loop():
2288
  while True:
2289
+ socketio.sleep(0.04) # 25 ticks per second
2290
 
2291
  # Update moving platforms
2292
  for p in platforms:
 
2295
  if p['x'] < 0 or p['x'] + p['w'] > WIDTH:
2296
  p['vx'] *= -1
2297
 
2298
+ # Update enemies
2299
+ for e in enemies:
2300
+ if e['active']:
2301
+ e['x'] += e['vx']
2302
+ if e['x'] < 0 or e['x'] + 40 > WIDTH:
2303
+ e['vx'] *= -1
2304
+
2305
+ # Find the lowest active player for cleanup
2306
+ if players:
2307
+ lowest_y = min(p['y'] for p in players.values())
2308
+
2309
+ # Clean up old entities
2310
+ global platforms, collectibles, enemies
2311
+ platforms = [p for p in platforms if p['y'] < lowest_y + 1500][-500:]
2312
+ collectibles = [c for c in collectibles if c['y'] < lowest_y + 1500 and not c['collected']][-200:]
2313
+ enemies = [e for e in enemies if e['y'] < lowest_y + 1500 and e['active']][-50:]
2314
 
2315
+ # Broadcast game state
 
 
2316
  socketio.emit('game_update', {
2317
  'players': players,
2318
+ 'platforms': [p for p in platforms if p['vx'] != 0][-30:] + platforms[-40:]
2319
  })
2320
 
2321
  socketio.start_background_task(server_loop)
2322
 
2323
  if __name__ == '__main__':
2324
+ print("🎮 Jungle Jump Pro Server Starting...")
2325
+ print("🌐 Open http://localhost:7860 to play!")
2326
  socketio.run(app, host='0.0.0.0', port=7860)