OrbitMC commited on
Commit
cc962ce
·
verified ·
1 Parent(s): 69ee610

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +448 -360
app.py CHANGED
@@ -4,432 +4,520 @@ eventlet.monkey_patch()
4
  from flask import Flask, render_template_string, request
5
  from flask_socketio import SocketIO, emit
6
  import random
7
- import math
8
  import time
9
- from threading import Lock
10
 
11
- # --- CONFIGURATION ---
12
- WIDTH, HEIGHT = 2000, 2000 # Virtual world size
13
- BaseSpeed = 5
14
- Friction = 0.96 # Slippery space physics
15
- MaxSpeed = 15
16
- ArenaShrinkRate = 0.5 # Pixels per tick
17
-
18
- # --- APP SETUP ---
19
  app = Flask(__name__)
20
- app.config['SECRET_KEY'] = 'secret!'
21
  socketio = SocketIO(app, async_mode='eventlet', cors_allowed_origins='*')
22
 
23
  # --- GAME STATE ---
24
- thread = None
25
- thread_lock = Lock()
26
- game_active = True
27
-
28
- # Data structures
29
- players = {} # {sid: {x, y, r, color, name, vx, vy, score, dead}}
30
- food = [] # [{x, y, r, color}]
31
- arena_radius = 1000
32
- start_time = time.time()
33
-
34
- # --- HTML/JS CLIENT ---
35
- # We embed the HTML here to keep it 1 file.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  HTML_TEMPLATE = """
37
  <!DOCTYPE html>
38
  <html lang="en">
39
  <head>
40
  <meta charset="UTF-8">
41
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
42
- <title>Orbit Bumpers</title>
43
  <style>
44
- body { margin: 0; overflow: hidden; background: #0b0b14; font-family: 'Segoe UI', Tahoma, sans-serif; color: white; touch-action: none; }
45
- #loginOverlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.85); display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 10; }
46
- input { padding: 15px; font-size: 20px; border-radius: 30px; border: none; outline: none; text-align: center; margin-bottom: 20px; width: 80%; max-width: 300px; }
47
- button { padding: 15px 40px; font-size: 20px; border-radius: 30px; border: none; cursor: pointer; background: #ff0055; color: white; font-weight: bold; transition: transform 0.1s; }
48
- button:active { transform: scale(0.95); }
49
- #uiLayer { position: absolute; top: 10px; left: 10px; pointer-events: none; z-index: 5; }
50
- #leaderboard { position: absolute; top: 10px; right: 10px; background: rgba(0,0,0,0.5); padding: 10px; border-radius: 10px; pointer-events: none; text-align: right; }
51
- #deathScreen { display: none; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; pointer-events: none; }
52
- h1 { margin: 0; text-shadow: 0 0 10px #ff0055; }
 
 
 
 
 
 
53
  </style>
54
  </head>
55
  <body>
56
 
57
- <div id="loginOverlay">
58
- <h1>ORBIT BUMPERS</h1>
59
- <p>Knock them out. Eat stars. Don't fall.</p>
60
- <input type="text" id="usernameInput" placeholder="Enter Nickname" maxlength="10">
61
- <button onclick="startGame()">BOOST!</button>
 
 
62
  </div>
63
 
64
- <div id="uiLayer">
65
- Score: <span id="scoreVal">0</span> | Mass: <span id="massVal">20</span>
66
- </div>
67
- <div id="leaderboard">
68
- <b>TOP ORBITS</b><br>
69
- <div id="lbContent"></div>
70
- </div>
71
- <div id="deathScreen">
72
- <h1 style="font-size: 50px; color: red;">ELIMINATED</h1>
73
- <p>Refresh to rejoin</p>
74
- </div>
75
 
76
- <canvas id="gameCanvas"></canvas>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
- <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
79
- <script>
80
- const socket = io();
81
- const canvas = document.getElementById('gameCanvas');
82
- const ctx = canvas.getContext('2d');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
- let width, height;
85
- let mySocketId = null;
86
- let camX = 0, camY = 0;
87
- let gameState = { players: {}, food: [], arena_r: 1000 };
88
- let particles = [];
89
-
90
- // --- AUDIO SYSTEM (Synth) ---
91
- const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
92
- function playSound(type) {
93
- if (audioCtx.state === 'suspended') audioCtx.resume();
94
- const osc = audioCtx.createOscillator();
95
- const gain = audioCtx.createGain();
96
- osc.connect(gain);
97
- gain.connect(audioCtx.destination);
98
-
99
- const now = audioCtx.currentTime;
100
-
101
- if (type === 'bump') {
102
- osc.type = 'square';
103
- osc.frequency.setValueAtTime(150, now);
104
- osc.frequency.exponentialRampToValueAtTime(40, now + 0.2);
105
- gain.gain.setValueAtTime(0.1, now);
106
- gain.gain.exponentialRampToValueAtTime(0.01, now + 0.2);
107
- osc.start(now);
108
- osc.stop(now + 0.2);
109
- } else if (type === 'eat') {
110
- osc.type = 'sine';
111
- osc.frequency.setValueAtTime(600, now);
112
- osc.frequency.exponentialRampToValueAtTime(1200, now + 0.1);
113
- gain.gain.setValueAtTime(0.05, now);
114
- gain.gain.exponentialRampToValueAtTime(0.01, now + 0.1);
115
- osc.start(now);
116
- osc.stop(now + 0.1);
117
- } else if (type === 'dash') {
118
- osc.type = 'triangle';
119
- osc.frequency.setValueAtTime(200, now);
120
- osc.frequency.linearRampToValueAtTime(100, now + 0.3);
121
- gain.gain.setValueAtTime(0.1, now);
122
- gain.gain.linearRampToValueAtTime(0, now + 0.3);
123
- osc.start(now);
124
- osc.stop(now + 0.3);
125
- }
126
  }
127
 
128
- // --- RESIZE HANDLING ---
129
- function resize() {
130
- width = window.innerWidth;
131
- height = window.innerHeight;
132
- canvas.width = width;
133
- canvas.height = height;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  }
135
- window.addEventListener('resize', resize);
136
- resize();
137
 
138
- // --- CONTROLS ---
139
- let mouseX = width/2, mouseY = height/2;
 
 
 
 
140
 
141
- // PC
142
- window.addEventListener('mousemove', (e) => {
143
- mouseX = e.clientX;
144
- mouseY = e.clientY;
145
- });
146
- window.addEventListener('mousedown', () => {
147
- sendInput(true);
148
- });
149
- window.addEventListener('mouseup', () => {
150
- sendInput(false);
151
- });
152
 
153
- // Mobile
154
- window.addEventListener('touchstart', (e) => {
155
- mouseX = e.touches[0].clientX;
156
- mouseY = e.touches[0].clientY;
157
- sendInput(true);
158
- });
159
- window.addEventListener('touchend', () => {
160
- sendInput(false);
161
- });
162
- window.addEventListener('touchmove', (e) => {
163
- mouseX = e.touches[0].clientX;
164
- mouseY = e.touches[0].clientY;
165
- e.preventDefault();
166
- }, {passive: false});
167
-
168
- function sendInput(boosting) {
169
- if(!mySocketId || !gameState.players[mySocketId]) return;
170
- // Calculate angle relative to center of screen (player is always center)
171
- const dx = mouseX - width/2;
172
- const dy = mouseY - height/2;
173
- const angle = Math.atan2(dy, dx);
174
- socket.emit('input', { angle: angle, boost: boosting });
175
- if(boosting) playSound('dash');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  }
177
 
178
- function startGame() {
179
- const name = document.getElementById('usernameInput').value || "Unknown";
180
- document.getElementById('loginOverlay').style.display = 'none';
181
- socket.emit('join', { name: name });
182
- // Start bg music loop? (Browser policy prevents auto-audio, handled by clicks)
 
 
 
183
  }
184
 
185
- // --- SOCKET EVENTS ---
186
- socket.on('connect', () => { mySocketId = socket.id; });
187
- socket.on('game_update', (data) => {
188
- gameState = data;
189
- // Handle My Player Specifics
190
- if (gameState.players[mySocketId]) {
191
- const me = gameState.players[mySocketId];
192
- camX = me.x;
193
- camY = me.y;
194
- document.getElementById('scoreVal').innerText = me.score;
195
- document.getElementById('massVal').innerText = Math.floor(me.r);
196
- } else if (mySocketId) {
197
- // I died
198
- // document.getElementById('deathScreen').style.display = 'block';
199
- }
200
- updateLeaderboard();
201
- });
202
 
203
- socket.on('sound_event', (data) => {
204
- playSound(data.type);
 
 
 
 
 
205
  });
 
206
 
207
- // --- RENDER LOOP ---
208
- function draw() {
209
- ctx.fillStyle = '#0b0b14'; // Background
210
- ctx.fillRect(0, 0, width, height);
211
-
212
- ctx.save();
213
- // Camera follow logic
214
- ctx.translate(width/2 - camX, height/2 - camY);
215
-
216
- // 1. Draw Arena Boundary
217
- ctx.beginPath();
218
- ctx.arc(0, 0, gameState.arena_r, 0, Math.PI * 2);
219
- ctx.strokeStyle = '#ff0055';
220
- ctx.lineWidth = 20;
221
- ctx.stroke();
222
- // Grid lines inside
223
- ctx.strokeStyle = 'rgba(255,255,255,0.05)';
224
- ctx.lineWidth = 2;
225
- for(let i=-2000; i<2000; i+=100) {
226
- ctx.beginPath(); ctx.moveTo(i, -2000); ctx.lineTo(i, 2000); ctx.stroke();
227
- ctx.beginPath(); ctx.moveTo(-2000, i); ctx.lineTo(2000, i); ctx.stroke();
228
- }
229
 
230
- // 2. Draw Food
231
- gameState.food.forEach(f => {
232
- ctx.beginPath();
233
- ctx.arc(f.x, f.y, f.r, 0, Math.PI*2);
234
- ctx.fillStyle = f.color;
235
- ctx.shadowBlur = 10;
236
- ctx.shadowColor = f.color;
237
- ctx.fill();
238
- ctx.shadowBlur = 0;
239
- });
240
 
241
- // 3. Draw Players
242
- for (let id in gameState.players) {
243
- const p = gameState.players[id];
244
- if(p.dead) continue;
245
-
246
- // Body
247
- ctx.beginPath();
248
- ctx.arc(p.x, p.y, p.r, 0, Math.PI*2);
249
- ctx.fillStyle = p.color;
250
- ctx.fill();
251
-
252
- // Border (white if it's me)
253
- ctx.lineWidth = 3;
254
- ctx.strokeStyle = (id === mySocketId) ? '#ffffff' : 'rgba(0,0,0,0.3)';
255
- ctx.stroke();
256
-
257
- // Name
258
- ctx.fillStyle = 'white';
259
- ctx.font = 'bold 14px Arial';
260
- ctx.textAlign = 'center';
261
- ctx.fillText(p.name, p.x, p.y - p.r - 10);
262
  }
263
-
264
- ctx.restore();
265
- requestAnimationFrame(draw);
266
  }
267
 
268
- function updateLeaderboard() {
269
- const sorted = Object.values(gameState.players).sort((a,b) => b.score - a.score).slice(0, 5);
270
- let html = "";
271
- sorted.forEach(p => {
272
- html += `<div><span style="color:${p.color}">●</span> ${p.name}: ${p.score}</div>`;
273
- });
274
- document.getElementById('lbContent').innerHTML = html;
275
  }
 
276
 
 
 
277
  draw();
278
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
  </body>
280
  </html>
281
  """
282
 
283
- # --- BACKEND LOGIC ---
284
-
285
- def init_food():
286
- # Spawn 50 random food pellets
287
- for _ in range(50):
288
- food.append({
289
- 'x': random.randint(-1000, 1000),
290
- 'y': random.randint(-1000, 1000),
291
- 'r': random.randint(3, 6),
292
- 'color': random.choice(['#0ff', '#ff0', '#f0f', '#0f0'])
293
- })
294
-
295
- init_food()
296
 
297
  @app.route('/')
298
  def index():
299
  return render_template_string(HTML_TEMPLATE)
300
 
301
  @socketio.on('join')
302
- def handle_join(data):
303
- # Spawn player
304
- angle = random.uniform(0, math.pi * 2)
305
- dist = random.uniform(0, 500)
306
  players[request.sid] = {
307
- 'x': math.cos(angle) * dist,
308
- 'y': math.sin(angle) * dist,
309
- 'r': 20,
310
- 'color': f'hsl({random.randint(0,360)}, 70%, 50%)',
311
- 'name': data.get('name', 'Guest')[:10],
312
- 'vx': 0,
313
- 'vy': 0,
314
- 'score': 0,
315
- 'boost': False,
316
- 'angle_input': 0,
317
- 'dead': False
318
  }
319
- emit('game_update', get_state(), broadcast=True)
 
320
 
321
- @socketio.on('input')
322
- def handle_input(data):
323
  if request.sid in players:
324
  p = players[request.sid]
325
- if not p['dead']:
326
- p['angle_input'] = data['angle']
327
- p['boost'] = data['boost']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
 
329
  @socketio.on('disconnect')
330
- def handle_disconnect():
331
  if request.sid in players:
332
  del players[request.sid]
333
 
334
- def get_state():
335
- return {'players': players, 'food': food, 'arena_r': arena_radius}
336
-
337
- def game_loop():
338
- global arena_radius
339
  while True:
340
- socketio.sleep(0.016) # ~60 FPS
341
 
342
- # Shrink Arena Logic (Battle Royale)
343
- if arena_radius > 200:
344
- arena_radius -= 0.1
 
 
 
345
 
346
- # Replenish food
347
- if len(food) < 50:
348
- food.append({
349
- 'x': random.randint(int(-arena_radius), int(arena_radius)),
350
- 'y': random.randint(int(-arena_radius), int(arena_radius)),
351
- 'r': random.randint(3, 6),
352
- 'color': random.choice(['#0ff', '#ff0', '#f0f', '#0f0'])
353
- })
354
-
355
- for sid, p in players.items():
356
- if p['dead']: continue
357
-
358
- # 1. Physics Movement
359
- # Acceleration based on input
360
- accel = 0.5
361
- if p['boost']: accel = 1.5 # Boost speed
362
-
363
- # Simple trigonometry for direction
364
- if p['boost']:
365
- p['vx'] += math.cos(p['angle_input']) * accel
366
- p['vy'] += math.sin(p['angle_input']) * accel
367
-
368
- # Friction (Drag)
369
- p['vx'] *= Friction
370
- p['vy'] *= Friction
371
-
372
- # Apply Position
373
- p['x'] += p['vx']
374
- p['y'] += p['vy']
375
-
376
- # 2. Arena Boundary Check (Death)
377
- dist_from_center = math.sqrt(p['x']**2 + p['y']**2)
378
- if dist_from_center > arena_radius:
379
- p['dead'] = True
380
- socketio.emit('sound_event', {'type': 'bump'}, to=sid) # Play death sound locally
381
- continue
382
-
383
- # 3. Eat Food
384
- # Iterate backwards to remove safely
385
- for i in range(len(food)-1, -1, -1):
386
- f = food[i]
387
- dx = p['x'] - f['x']
388
- dy = p['y'] - f['y']
389
- dist = math.sqrt(dx*dx + dy*dy)
390
- if dist < p['r'] + f['r']:
391
- p['r'] += 0.5 # Grow
392
- p['score'] += 10
393
- if p['r'] > 60: p['r'] = 60 # Max size cap
394
- food.pop(i)
395
- socketio.emit('sound_event', {'type': 'eat'}, to=sid)
396
-
397
- # 4. Player vs Player Collision (Bumping)
398
- for other_sid, other in players.items():
399
- if sid == other_sid or other['dead']: continue
400
-
401
- dx = other['x'] - p['x']
402
- dy = other['y'] - p['y']
403
- dist = math.sqrt(dx*dx + dy*dy)
404
-
405
- min_dist = p['r'] + other['r']
406
-
407
- if dist < min_dist:
408
- # Physics: Elastic Collision response
409
- angle = math.atan2(dy, dx)
410
- force = 15 # Bounce force
411
-
412
- # Push them apart immediately (anti-stick)
413
- overlap = min_dist - dist
414
- p['x'] -= math.cos(angle) * overlap/2
415
- p['y'] -= math.sin(angle) * overlap/2
416
- other['x'] += math.cos(angle) * overlap/2
417
- other['y'] += math.sin(angle) * overlap/2
418
-
419
- # Swap Momentum roughly
420
- p['vx'] -= math.cos(angle) * force
421
- p['vy'] -= math.sin(angle) * force
422
- other['vx'] += math.cos(angle) * force
423
- other['vy'] += math.sin(angle) * force
424
-
425
- socketio.emit('sound_event', {'type': 'bump'})
426
-
427
- socketio.emit('game_update', get_state())
428
-
429
- # Start Game Thread
430
- with thread_lock:
431
- if thread is None:
432
- thread = socketio.start_background_task(game_loop)
433
 
434
  if __name__ == '__main__':
435
  socketio.run(app, host='0.0.0.0', port=7860)
 
4
  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>
84
 
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():
443
  return render_template_string(HTML_TEMPLATE)
444
 
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:
498
+ if p['vx'] != 0:
499
+ p['x'] += p['vx']
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)