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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +391 -624
app.py CHANGED
@@ -1,668 +1,435 @@
1
- from flask import Flask, render_template_string, request, send_file, jsonify
2
- import yt_dlp
3
- import os
4
- import uuid
5
- import threading
6
- import time
7
- import re
8
 
9
- app = Flask(__name__)
 
 
 
 
 
10
 
11
- # Directory for downloads
12
- DOWNLOAD_DIR = "/tmp/downloads"
13
- os.makedirs(DOWNLOAD_DIR, exist_ok=True)
 
 
 
14
 
15
- # HTML Template with Clean Responsive UI
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  HTML_TEMPLATE = """
17
  <!DOCTYPE html>
18
  <html lang="en">
19
  <head>
20
  <meta charset="UTF-8">
21
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
22
- <title>🎬 Media Downloader - MP4 & MP3</title>
23
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
24
  <style>
25
- * {
26
- margin: 0;
27
- padding: 0;
28
- box-sizing: border-box;
29
- }
30
-
31
- body {
32
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
33
- background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%);
34
- min-height: 100vh;
35
- display: flex;
36
- justify-content: center;
37
- align-items: center;
38
- padding: 20px;
39
- }
40
-
41
- .container {
42
- background: rgba(255, 255, 255, 0.05);
43
- backdrop-filter: blur(20px);
44
- border-radius: 24px;
45
- padding: 40px;
46
- max-width: 650px;
47
- width: 100%;
48
- box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
49
- border: 1px solid rgba(255, 255, 255, 0.1);
50
- }
51
-
52
- .header {
53
- text-align: center;
54
- margin-bottom: 35px;
55
- }
56
-
57
- .logo {
58
- font-size: 3.5rem;
59
- margin-bottom: 15px;
60
- animation: bounce 2s infinite;
61
- }
62
-
63
- @keyframes bounce {
64
- 0%, 100% { transform: translateY(0); }
65
- 50% { transform: translateY(-10px); }
66
- }
67
-
68
- h1 {
69
- color: #fff;
70
- font-size: 2rem;
71
- font-weight: 700;
72
- margin-bottom: 8px;
73
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
74
- -webkit-background-clip: text;
75
- -webkit-text-fill-color: transparent;
76
- background-clip: text;
77
- }
78
-
79
- .subtitle {
80
- color: rgba(255, 255, 255, 0.6);
81
- font-size: 0.95rem;
82
- font-weight: 300;
83
- }
84
-
85
- .input-section {
86
- margin-bottom: 25px;
87
- }
88
-
89
- .input-wrapper {
90
- position: relative;
91
- }
92
-
93
- .input-icon {
94
- position: absolute;
95
- left: 18px;
96
- top: 50%;
97
- transform: translateY(-50%);
98
- font-size: 1.2rem;
99
- }
100
-
101
- input[type="text"] {
102
- width: 100%;
103
- padding: 18px 20px 18px 50px;
104
- border: 2px solid rgba(255, 255, 255, 0.1);
105
- border-radius: 16px;
106
- background: rgba(255, 255, 255, 0.08);
107
- color: #fff;
108
- font-size: 1rem;
109
- font-family: inherit;
110
- transition: all 0.3s ease;
111
- }
112
-
113
- input[type="text"]:focus {
114
- outline: none;
115
- border-color: #667eea;
116
- background: rgba(255, 255, 255, 0.12);
117
- box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.2);
118
- }
119
-
120
- input[type="text"]::placeholder {
121
- color: rgba(255, 255, 255, 0.4);
122
- }
123
-
124
- .btn-group {
125
- display: grid;
126
- grid-template-columns: 1fr 1fr;
127
- gap: 15px;
128
- margin-top: 25px;
129
- }
130
-
131
- .btn {
132
- padding: 18px 28px;
133
- border: none;
134
- border-radius: 14px;
135
- font-size: 1rem;
136
- font-weight: 600;
137
- font-family: inherit;
138
- cursor: pointer;
139
- transition: all 0.3s ease;
140
- display: flex;
141
- align-items: center;
142
- justify-content: center;
143
- gap: 10px;
144
- position: relative;
145
- overflow: hidden;
146
- }
147
-
148
- .btn::before {
149
- content: '';
150
- position: absolute;
151
- top: 0;
152
- left: -100%;
153
- width: 100%;
154
- height: 100%;
155
- background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
156
- transition: left 0.5s ease;
157
- }
158
-
159
- .btn:hover::before {
160
- left: 100%;
161
- }
162
-
163
- .btn-mp4 {
164
- background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
165
- color: #fff;
166
- }
167
-
168
- .btn-mp3 {
169
- background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
170
- color: #fff;
171
- }
172
-
173
- .btn:hover {
174
- transform: translateY(-3px);
175
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
176
- }
177
-
178
- .btn:active {
179
- transform: translateY(-1px);
180
- }
181
-
182
- .btn:disabled {
183
- opacity: 0.6;
184
- cursor: not-allowed;
185
- transform: none !important;
186
- }
187
-
188
- .btn-icon {
189
- font-size: 1.3rem;
190
- }
191
-
192
- .status {
193
- margin-top: 25px;
194
- padding: 18px 20px;
195
- border-radius: 14px;
196
- text-align: center;
197
- font-weight: 500;
198
- display: none;
199
- animation: fadeIn 0.3s ease;
200
- }
201
-
202
- @keyframes fadeIn {
203
- from { opacity: 0; transform: translateY(-10px); }
204
- to { opacity: 1; transform: translateY(0); }
205
- }
206
-
207
- .status.loading {
208
- display: flex;
209
- align-items: center;
210
- justify-content: center;
211
- gap: 12px;
212
- background: rgba(255, 193, 7, 0.15);
213
- color: #ffc107;
214
- border: 1px solid rgba(255, 193, 7, 0.3);
215
- }
216
-
217
- .status.success {
218
- display: block;
219
- background: rgba(76, 217, 100, 0.15);
220
- color: #4cd964;
221
- border: 1px solid rgba(76, 217, 100, 0.3);
222
- }
223
-
224
- .status.error {
225
- display: block;
226
- background: rgba(255, 59, 48, 0.15);
227
- color: #ff3b30;
228
- border: 1px solid rgba(255, 59, 48, 0.3);
229
- }
230
-
231
- .loader {
232
- width: 22px;
233
- height: 22px;
234
- border: 3px solid rgba(255, 193, 7, 0.3);
235
- border-radius: 50%;
236
- border-top-color: #ffc107;
237
- animation: spin 1s linear infinite;
238
- }
239
-
240
- @keyframes spin {
241
- to { transform: rotate(360deg); }
242
- }
243
-
244
- .features {
245
- margin-top: 35px;
246
- display: grid;
247
- grid-template-columns: repeat(3, 1fr);
248
- gap: 15px;
249
- }
250
-
251
- .feature {
252
- text-align: center;
253
- padding: 20px 15px;
254
- background: rgba(255, 255, 255, 0.03);
255
- border-radius: 16px;
256
- border: 1px solid rgba(255, 255, 255, 0.05);
257
- transition: all 0.3s ease;
258
- }
259
-
260
- .feature:hover {
261
- background: rgba(255, 255, 255, 0.08);
262
- transform: translateY(-3px);
263
- }
264
-
265
- .feature-icon {
266
- font-size: 2rem;
267
- margin-bottom: 10px;
268
- }
269
-
270
- .feature-title {
271
- color: #fff;
272
- font-weight: 600;
273
- font-size: 0.9rem;
274
- margin-bottom: 5px;
275
- }
276
-
277
- .feature-desc {
278
- color: rgba(255, 255, 255, 0.5);
279
- font-size: 0.75rem;
280
- }
281
-
282
- .info-bar {
283
- margin-top: 30px;
284
- padding: 15px 20px;
285
- background: rgba(102, 126, 234, 0.1);
286
- border-radius: 12px;
287
- border: 1px solid rgba(102, 126, 234, 0.2);
288
- }
289
-
290
- .info-bar p {
291
- color: rgba(255, 255, 255, 0.7);
292
- font-size: 0.85rem;
293
- text-align: center;
294
- }
295
 
296
- .info-bar a {
297
- color: #667eea;
298
- text-decoration: none;
299
- }
 
 
300
 
301
- .supported-sites {
302
- margin-top: 25px;
303
- text-align: center;
304
- }
 
 
 
 
 
 
 
305
 
306
- .supported-sites p {
307
- color: rgba(255, 255, 255, 0.5);
308
- font-size: 0.8rem;
309
- margin-bottom: 10px;
310
- }
311
 
312
- .site-badges {
313
- display: flex;
314
- flex-wrap: wrap;
315
- justify-content: center;
316
- gap: 8px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  }
318
 
319
- .badge {
320
- padding: 6px 12px;
321
- background: rgba(255, 255, 255, 0.08);
322
- border-radius: 20px;
323
- color: rgba(255, 255, 255, 0.7);
324
- font-size: 0.75rem;
325
  }
 
 
326
 
327
- @media (max-width: 600px) {
328
- .container {
329
- padding: 25px;
330
- }
331
-
332
- h1 {
333
- font-size: 1.6rem;
334
- }
335
-
336
- .btn-group {
337
- grid-template-columns: 1fr;
338
- }
339
-
340
- .features {
341
- grid-template-columns: 1fr;
342
- }
343
 
344
- .logo {
345
- font-size: 2.5rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  }
347
- }
348
- </style>
349
- </head>
350
- <body>
351
- <div class="container">
352
- <div class="header">
353
- <div class="logo">🎬</div>
354
- <h1>Media Downloader</h1>
355
- <p class="subtitle">Download videos as MP4 or extract audio as MP3</p>
356
- </div>
357
-
358
- <div class="input-section">
359
- <div class="input-wrapper">
360
- <span class="input-icon">🔗</span>
361
- <input type="text" id="url" placeholder="Paste video URL here..." autocomplete="off">
362
- </div>
363
- </div>
364
-
365
- <div class="btn-group">
366
- <button class="btn btn-mp4" onclick="download('mp4')" id="btn-mp4">
367
- <span class="btn-icon">📹</span>
368
- <span>Download MP4</span>
369
- </button>
370
- <button class="btn btn-mp3" onclick="download('mp3')" id="btn-mp3">
371
- <span class="btn-icon">🎵</span>
372
- <span>Download MP3</span>
373
- </button>
374
- </div>
375
-
376
- <div id="status" class="status"></div>
377
-
378
- <div class="features">
379
- <div class="feature">
380
- <div class="feature-icon">⚡</div>
381
- <div class="feature-title">Fast</div>
382
- <div class="feature-desc">Quick processing</div>
383
- </div>
384
- <div class="feature">
385
- <div class="feature-icon">🎯</div>
386
- <div class="feature-title">Quality</div>
387
- <div class="feature-desc">Best available</div>
388
- </div>
389
- <div class="feature">
390
- <div class="feature-icon">🔒</div>
391
- <div class="feature-title">Secure</div>
392
- <div class="feature-desc">No data stored</div>
393
- </div>
394
- </div>
395
-
396
- <div class="supported-sites">
397
- <p>Supported platforms:</p>
398
- <div class="site-badges">
399
- <span class="badge">YouTube</span>
400
- <span class="badge">Twitter/X</span>
401
- <span class="badge">Facebook</span>
402
- <span class="badge">Instagram</span>
403
- <span class="badge">TikTok</span>
404
- <span class="badge">Vimeo</span>
405
- <span class="badge">+1000 more</span>
406
- </div>
407
- </div>
408
-
409
- <div class="info-bar">
410
- <p>💡 Tip: Files are automatically deleted after download for your privacy</p>
411
- </div>
412
- </div>
413
 
414
- <script>
415
- async function download(format) {
416
- const url = document.getElementById('url').value.trim();
417
- const status = document.getElementById('status');
418
- const btnMp4 = document.getElementById('btn-mp4');
419
- const btnMp3 = document.getElementById('btn-mp3');
420
-
421
- if (!url) {
422
- showStatus('error', '❌ Please enter a valid URL');
423
- shakeInput();
424
- return;
425
- }
426
 
427
- // Basic URL validation
428
- if (!url.startsWith('http://') && !url.startsWith('https://')) {
429
- showStatus('error', '❌ Please enter a valid URL starting with http:// or https://');
430
- shakeInput();
431
- return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
432
  }
433
 
434
- // Disable buttons
435
- btnMp4.disabled = true;
436
- btnMp3.disabled = true;
437
-
438
- // Show loading
439
- showStatus('loading', 'Processing your request...');
440
-
441
- try {
442
- const response = await fetch('/download', {
443
- method: 'POST',
444
- headers: {
445
- 'Content-Type': 'application/json',
446
- },
447
- body: JSON.stringify({ url: url, format: format })
448
- });
449
-
450
- if (!response.ok) {
451
- const error = await response.json();
452
- throw new Error(error.error || 'Download failed');
453
- }
454
-
455
- // Get filename from header
456
- const contentDisposition = response.headers.get('Content-Disposition');
457
- let filename = `download.${format}`;
458
- if (contentDisposition) {
459
- const match = contentDisposition.match(/filename[^;=\\n]*=((['"]).*?\\2|[^;\\n]*)/);
460
- if (match && match[1]) {
461
- filename = match[1].replace(/['"]/g, '');
462
- }
463
- }
464
-
465
- // Download file
466
- const blob = await response.blob();
467
- const downloadUrl = window.URL.createObjectURL(blob);
468
- const a = document.createElement('a');
469
- a.href = downloadUrl;
470
- a.download = filename;
471
- document.body.appendChild(a);
472
- a.click();
473
- window.URL.revokeObjectURL(downloadUrl);
474
- a.remove();
475
-
476
- showStatus('success', '✅ Download started successfully!');
477
- document.getElementById('url').value = '';
478
-
479
- } catch (error) {
480
- showStatus('error', '❌ ' + error.message);
481
- } finally {
482
- btnMp4.disabled = false;
483
- btnMp3.disabled = false;
484
  }
485
- }
486
 
487
- function showStatus(type, message) {
488
- const status = document.getElementById('status');
489
- status.className = 'status ' + type;
490
- if (type === 'loading') {
491
- status.innerHTML = '<div class="loader"></div><span>' + message + '</span>';
492
- } else {
493
- status.textContent = message;
494
- }
495
  }
496
 
497
- function shakeInput() {
498
- const input = document.getElementById('url');
499
- input.style.animation = 'shake 0.5s ease';
500
- setTimeout(() => {
501
- input.style.animation = '';
502
- }, 500);
 
503
  }
504
 
505
- // Add shake animation
506
- const style = document.createElement('style');
507
- style.textContent = `
508
- @keyframes shake {
509
- 0%, 100% { transform: translateX(0); }
510
- 25% { transform: translateX(-10px); }
511
- 75% { transform: translateX(10px); }
512
- }
513
- `;
514
- document.head.appendChild(style);
515
-
516
- // Allow Enter key to trigger MP4 download
517
- document.getElementById('url').addEventListener('keypress', function(e) {
518
- if (e.key === 'Enter') {
519
- download('mp4');
520
- }
521
- });
522
-
523
- // Focus input on load
524
- window.onload = function() {
525
- document.getElementById('url').focus();
526
- };
527
  </script>
528
  </body>
529
  </html>
530
  """
531
 
 
532
 
533
- def clean_filename(title):
534
- """Clean filename to remove invalid characters"""
535
- clean = re.sub(r'[<>:"/\\|?*]', '', title)
536
- clean = re.sub(r'\s+', ' ', clean).strip()
537
- return clean[:100] if len(clean) > 100 else clean
538
-
539
-
540
- def cleanup_old_files():
541
- """Clean up files older than 5 minutes"""
542
- try:
543
- now = time.time()
544
- for filename in os.listdir(DOWNLOAD_DIR):
545
- filepath = os.path.join(DOWNLOAD_DIR, filename)
546
- if os.path.isfile(filepath):
547
- if now - os.path.getmtime(filepath) > 300: # 5 minutes
548
- os.remove(filepath)
549
- except Exception as e:
550
- print(f"Cleanup error: {e}")
551
 
 
552
 
553
  @app.route('/')
554
  def index():
555
- # Cleanup old files on each visit
556
- threading.Thread(target=cleanup_old_files, daemon=True).start()
557
  return render_template_string(HTML_TEMPLATE)
558
 
559
-
560
- @app.route('/download', methods=['POST'])
561
- def download_video():
562
- try:
563
- data = request.get_json()
564
- url = data.get('url', '').strip()
565
- format_type = data.get('format', 'mp4')
566
-
567
- if not url:
568
- return jsonify({'error': 'URL is required'}), 400
569
-
570
- # Validate URL format
571
- if not url.startswith(('http://', 'https://')):
572
- return jsonify({'error': 'Invalid URL format'}), 400
573
-
574
- # Generate unique filename
575
- unique_id = str(uuid.uuid4())[:8]
576
-
577
- # Configure yt-dlp options
578
- if format_type == 'mp3':
579
- ydl_opts = {
580
- 'format': 'bestaudio/best',
581
- 'postprocessors': [{
582
- 'key': 'FFmpegExtractAudio',
583
- 'preferredcodec': 'mp3',
584
- 'preferredquality': '192',
585
- }],
586
- 'outtmpl': os.path.join(DOWNLOAD_DIR, f'{unique_id}.%(ext)s'),
587
- 'quiet': True,
588
- 'no_warnings': True,
589
- 'extract_flat': False,
590
- 'noplaylist': True,
591
- }
592
- expected_ext = 'mp3'
593
- else:
594
- ydl_opts = {
595
- 'format': 'best[ext=mp4]/bestvideo[ext=mp4]+bestaudio[ext=m4a]/best',
596
- 'outtmpl': os.path.join(DOWNLOAD_DIR, f'{unique_id}.%(ext)s'),
597
- 'quiet': True,
598
- 'no_warnings': True,
599
- 'noplaylist': True,
600
- 'merge_output_format': 'mp4',
601
- }
602
- expected_ext = 'mp4'
603
-
604
- # Download the video/audio
605
- with yt_dlp.YoutubeDL(ydl_opts) as ydl:
606
- info = ydl.extract_info(url, download=True)
607
- title = info.get('title', 'download')
608
- clean_title = clean_filename(title)
609
-
610
- # Find the downloaded file
611
- file_path = None
612
- possible_extensions = ['mp3'] if format_type == 'mp3' else ['mp4', 'webm', 'mkv', 'avi', 'mov']
613
 
614
- for ext in possible_extensions:
615
- potential_path = os.path.join(DOWNLOAD_DIR, f'{unique_id}.{ext}')
616
- if os.path.exists(potential_path):
617
- file_path = potential_path
618
- expected_ext = ext
619
- break
620
-
621
- if not file_path or not os.path.exists(file_path):
622
- return jsonify({'error': 'Failed to download file'}), 500
623
-
624
- download_name = f'{clean_title}.{expected_ext}'
625
-
626
- # Send file
627
- response = send_file(
628
- file_path,
629
- as_attachment=True,
630
- download_name=download_name,
631
- mimetype='audio/mpeg' if format_type == 'mp3' else 'video/mp4'
632
- )
633
-
634
- # Schedule file deletion after sending
635
- def delete_file_later():
636
- time.sleep(30)
637
- try:
638
- if os.path.exists(file_path):
639
- os.remove(file_path)
640
- except Exception as e:
641
- print(f"Error deleting file: {e}")
642
-
643
- threading.Thread(target=delete_file_later, daemon=True).start()
644
-
645
- return response
646
-
647
- except yt_dlp.utils.DownloadError as e:
648
- error_msg = str(e)
649
- if 'Private video' in error_msg:
650
- return jsonify({'error': 'This video is private'}), 400
651
- elif 'Video unavailable' in error_msg:
652
- return jsonify({'error': 'Video is unavailable'}), 400
653
- elif 'age' in error_msg.lower():
654
- return jsonify({'error': 'Age-restricted content cannot be downloaded'}), 400
655
- else:
656
- return jsonify({'error': 'Failed to download. Please check the URL'}), 400
657
- except Exception as e:
658
- print(f"Error: {e}")
659
- return jsonify({'error': 'An error occurred. Please try again'}), 500
660
-
661
-
662
- @app.route('/health')
663
- def health():
664
- return jsonify({'status': 'healthy'})
665
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
666
 
667
  if __name__ == '__main__':
668
- app.run(host='0.0.0.0', port=7860, debug=False)
 
1
+ import eventlet
2
+ eventlet.monkey_patch()
 
 
 
 
 
3
 
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)