import os import json import asyncio import math import uuid import random import time from collections import defaultdict from aiohttp import web, ClientSession import socketio # --- CONFIGURATION --- PORT = int(os.getenv('SERVER_PORT', 8080)) ADMIN_PATH = os.getenv('ADMIN_PATH', '/secret-admin-panel') # !!! REPLACE WITH YOUR DISCORD APP DETAILS !!! DISCORD_CLIENT_ID = "1450064181972832409" DISCORD_CLIENT_SECRET = "F1Tm1JL5sDmbOn9RGT-qYKyc7Q0_9IsW" DISCORD_REDIRECT_URI = "https://orbitmc-yt.hf.space/callback" GAME_TICK_RATE = 30 GRID_SIZE = 8 BLOCK_SIZE = 280 ROAD_WIDTH = 90 CELL_SIZE = BLOCK_SIZE + ROAD_WIDTH MAP_WIDTH = GRID_SIZE * CELL_SIZE MAP_HEIGHT = GRID_SIZE * CELL_SIZE # --- ASSET DEFINITIONS --- ENTITY_TYPES = { 'ROCK': {'r': 8, 'h': 8, 'color': '#7f8c8d', 'score': 1, 'type': 'rock'}, 'FLOWER': {'r': 6, 'h': 4, 'color': '#e84393', 'score': 1, 'type': 'flower'}, 'FENCE': {'w': 20, 'l': 4, 'h': 10, 'color': '#8e44ad', 'score': 1, 'type': 'building'}, 'HYDRANT': {'r': 6, 'h': 12, 'color': '#e74c3c', 'sideColor': '#c0392b', 'score': 1, 'type': 'cylinder'}, 'POST': {'r': 4, 'h': 40, 'color': '#bdc3c7', 'sideColor': '#7f8c8d', 'score': 2, 'type': 'cylinder'}, 'BUSH': {'r': 12, 'h': 10, 'color': '#2ecc71', 'score': 3, 'type': 'bush'}, 'TREE': {'r': 18, 'h': 70, 'color': '#27ae60', 'score': 5, 'type': 'tree'}, 'CAR': {'w': 48, 'l': 24, 'h': 12, 'cabinW': 28, 'cabinL': 20, 'cabinH': 10, 'color': '#e74c3c', 'score': 10, 'type': 'car', 'mobile': True}, 'HOUSE_S': {'w': 70, 'l': 70, 'h': 60, 'color': '#e67e22', 'sideColor': '#d35400', 'score': 50, 'type': 'building', 'windows': True}, 'HOUSE_M': {'w': 100, 'l': 90, 'h': 90, 'color': '#3498db', 'sideColor': '#2980b9', 'score': 100, 'type': 'building', 'windows': True}, 'OFFICE': {'w': 110, 'l': 110, 'h': 160, 'color': '#9b59b6', 'sideColor': '#8e44ad', 'score': 200, 'type': 'building', 'windows': True}, 'TOWER': {'w': 130, 'l': 130, 'h': 280, 'color': '#34495e', 'sideColor': '#2c3e50', 'score': 300, 'type': 'building', 'windows': True} } sio = socketio.AsyncServer(async_mode='aiohttp', cors_allowed_origins='*') app = web.Application() sio.attach(app) class GameState: def __init__(self): self.players = {} self.sockets = {} self.static_entities = [] # Spatial Hash Grid for Static Entities: key=(gridX, gridY), val=[entities] self.static_grid = defaultdict(list) self.dynamic_entities = [] self.removed_entities = [] self.active = True self.user_data_file = 'user_data.json' self.persistent_data = self.load_data() self.generate_map() def load_data(self): if os.path.exists(self.user_data_file): try: with open(self.user_data_file, 'r') as f: return json.load(f) except: return {} return {} def save_data(self): with open(self.user_data_file, 'w') as f: json.dump(self.persistent_data, f, indent=2) def generate_map(self): self.static_entities = [] self.static_grid.clear() self.dynamic_entities = [] # Grid Generation for ix in range(GRID_SIZE + 1): for iy in range(GRID_SIZE + 1): x = ix * CELL_SIZE y = iy * CELL_SIZE if ix < GRID_SIZE and iy < GRID_SIZE: self.generate_block(x + ROAD_WIDTH/2, y + ROAD_WIDTH/2) # Car Generation lane_offset = ROAD_WIDTH / 4 for _ in range(60): self.spawn_car(lane_offset) def add_entity(self, x, y, template, **kwargs): ent = template.copy() ent.update(kwargs) ent['x'] = x ent['y'] = y ent['id'] = str(uuid.uuid4()) ent['startX'] = x ent['startY'] = y ent['varSeed'] = random.random() ent['rotation'] = kwargs.get('rotation', 0) if ent['type'] in ['building', 'car']: ent['radius'] = math.hypot(ent['w']/2, ent.get('l', ent['w'])/2) else: ent['radius'] = ent['r'] if ent['type'] == 'flower': ent['color'] = random.choice(['#e84393', '#fd79a8', '#00b894', '#fab1a0']) # Add to main list AND spatial grid self.static_entities.append(ent) gx = int(x // CELL_SIZE) gy = int(y // CELL_SIZE) self.static_grid[(gx, gy)].append(ent) def get_nearby_static(self, x, y): # Return entities in the 3x3 grid cells around the point gx = int(x // CELL_SIZE) gy = int(y // CELL_SIZE) nearby = [] for ix in range(gx - 1, gx + 2): for iy in range(gy - 1, gy + 2): nearby.extend(self.static_grid[(ix, iy)]) return nearby def generate_block(self, bx, by): cx = bx + BLOCK_SIZE/2 cy = by + BLOCK_SIZE/2 b_type = random.random() if b_type < 0.2: # Park inset = 10 sz = BLOCK_SIZE - inset*2 for k in range(0, int(sz), 25): self.add_entity(bx + inset + k, by + inset, ENTITY_TYPES['FENCE']) self.add_entity(bx + inset + k, by + BLOCK_SIZE - inset, ENTITY_TYPES['FENCE']) for k in range(0, int(sz), 25): self.add_entity(bx + inset, by + inset + k, ENTITY_TYPES['FENCE'], w=5, l=20) self.add_entity(bx + BLOCK_SIZE - inset, by + inset + k, ENTITY_TYPES['FENCE'], w=5, l=20) for _ in range(10): ox = random.random() * (sz-40) + 20 oy = random.random() * (sz-40) + 20 tmpl = ENTITY_TYPES['TREE'] if random.random() > 0.6 else ENTITY_TYPES['FLOWER'] self.add_entity(bx+inset+ox, by+inset+oy, tmpl) self.add_entity(cx, cy, ENTITY_TYPES['HYDRANT']) elif b_type < 0.6: # Residential q = BLOCK_SIZE/4 self.add_entity(cx - q, cy - q, ENTITY_TYPES['HOUSE_S']) self.add_entity(cx + q, cy - q, ENTITY_TYPES['HOUSE_M']) self.add_entity(cx - q, cy + q, ENTITY_TYPES['HOUSE_M']) self.add_entity(cx + q, cy + q, ENTITY_TYPES['HOUSE_S']) self.add_entity(cx, cy, ENTITY_TYPES['TREE']) self.add_entity(bx + 10, cy, ENTITY_TYPES['BUSH']) self.add_entity(bx + BLOCK_SIZE - 10, cy, ENTITY_TYPES['BUSH']) else: # Downtown if random.random() > 0.5: self.add_entity(cx, cy, ENTITY_TYPES['TOWER']) else: self.add_entity(cx - 50, cy, ENTITY_TYPES['OFFICE']) self.add_entity(cx + 50, cy, ENTITY_TYPES['OFFICE']) self.add_entity(bx + 20, by + 20, ENTITY_TYPES['POST']) self.add_entity(bx + BLOCK_SIZE - 20, by + 20, ENTITY_TYPES['POST']) self.add_entity(bx + 20, by + BLOCK_SIZE - 20, ENTITY_TYPES['POST']) self.add_entity(bx + BLOCK_SIZE - 20, by + BLOCK_SIZE - 20, ENTITY_TYPES['POST']) def spawn_car(self, offset): orient = 0 if random.random() > 0.5 else 1 if orient == 0: iy = random.randint(0, GRID_SIZE) y = iy * CELL_SIZE x = random.random() * MAP_WIDTH dir = 1 if random.random() > 0.5 else -1 y += dir * offset rot = 0 if dir == 1 else math.pi else: ix = random.randint(0, GRID_SIZE) x = ix * CELL_SIZE y = random.random() * MAP_HEIGHT dir = 1 if random.random() > 0.5 else -1 x += dir * offset rot = math.pi/2 if dir == 1 else -math.pi/2 car = ENTITY_TYPES['CAR'].copy() car.update({ 'x': x, 'y': y, 'rotation': rot, 'id': str(uuid.uuid4()), 'radius': math.hypot(24, 12), 'type': 'car', 'mobile': True, 'color': random.choice(['#e74c3c', '#3498db', '#f1c40f', '#ecf0f1', '#2c3e50']) }) self.dynamic_entities.append(car) def add_player(self, session_id, user_info): saved = self.persistent_data.get(session_id, {}) self.players[session_id] = { 'id': session_id, 'discord_id': user_info['id'], 'username': user_info['username'], 'avatar': f"https://cdn.discordapp.com/avatars/{user_info['id']}/{user_info['avatar']}.png" if user_info['avatar'] else "", 'x': random.random() * MAP_WIDTH, 'y': random.random() * MAP_HEIGHT, 'r': saved.get('r', 35), 'target_r': saved.get('r', 35), 'score': saved.get('score', 0), 'color': '#3498db', 'input_angle': 0, 'input_force': 0, 'vx': 0, 'vy': 0, 'alive': True, 'respawn_timer': 0 } if session_id not in self.persistent_data: self.persistent_data[session_id] = {'r': 35, 'score': 0, 'username': user_info['username']} self.save_data() def update(self, dt): if not self.active: return self.removed_entities = [] sorted_players = sorted(self.players.values(), key=lambda p: p['r'], reverse=True) for p in sorted_players: if not p['alive']: p['respawn_timer'] -= dt if p['respawn_timer'] <= 0: p['alive'] = True p['r'] = 35; p['target_r'] = 35; p['score'] = 0 p['x'] = random.random() * MAP_WIDTH p['y'] = random.random() * MAP_HEIGHT continue speed = 180 * (1 - (p['r'] / 800)) p['vx'] = math.cos(p['input_angle']) * p['input_force'] * speed p['vy'] = math.sin(p['input_angle']) * p['input_force'] * speed p['x'] = max(p['r'], min(MAP_WIDTH - p['r'], p['x'] + p['vx'] * dt)) p['y'] = max(p['r'], min(MAP_HEIGHT - p['r'], p['y'] + p['vy'] * dt)) if p['r'] < p['target_r']: p['r'] += (p['target_r'] - p['r']) * dt # Eat Static (OPTIMIZED: Use Spatial Hash) nearby_static = self.get_nearby_static(p['x'], p['y']) for ent in nearby_static: # Basic check before expensive sqrt if abs(p['x'] - ent['x']) > p['r'] or abs(p['y'] - ent['y']) > p['r']: continue dist = math.hypot(p['x'] - ent['x'], p['y'] - ent['y']) if dist < p['r'] and p['r'] > ent['radius']: self.static_entities.remove(ent) # Also remove from grid for consistency (slow, but eats are rare compared to frames) gx, gy = int(ent['x']//CELL_SIZE), int(ent['y']//CELL_SIZE) if ent in self.static_grid[(gx, gy)]: self.static_grid[(gx, gy)].remove(ent) self.removed_entities.append(ent['id']) p['score'] += ent['score'] p['target_r'] += math.sqrt(ent['score']) * 0.3 # Eat Cars for car in self.dynamic_entities: dist = math.hypot(p['x'] - car['x'], p['y'] - car['y']) if dist < p['r'] and p['r'] > car['radius']: p['score'] += car['score'] p['target_r'] += math.sqrt(car['score']) * 0.3 car['x'] = random.randint(0, GRID_SIZE) * CELL_SIZE car['y'] = random.random() * MAP_HEIGHT self.removed_entities.append(car['id']) # Eat Players for other in sorted_players: if p == other or not other['alive']: continue dist = math.hypot(p['x'] - other['x'], p['y'] - other['y']) if p['r'] > other['r'] * 1.1 and dist < p['r'] - other['r']*0.4: other['alive'] = False other['respawn_timer'] = 5 p['score'] += 50 + other['score'] * 0.5 p['target_r'] += 5 self.persistent_data[p['id']]['score'] = p['score'] self.persistent_data[p['id']]['r'] = p['target_r'] self.persistent_data[other['id']]['score'] = 0 self.persistent_data[other['id']]['r'] = 35 self.save_data() # Update Cars for car in self.dynamic_entities: speed = 150 car['x'] += math.cos(car['rotation']) * speed * dt car['y'] += math.sin(car['rotation']) * speed * dt if car['x'] > MAP_WIDTH: car['x'] = 0 if car['x'] < 0: car['x'] = MAP_WIDTH if car['y'] > MAP_HEIGHT: car['y'] = 0 if car['y'] < 0: car['y'] = MAP_HEIGHT game = GameState() # --- WEB HANDLERS --- async def handle_index(request): return web.FileResponse('./index.html') async def handle_admin(request): return web.FileResponse('./admin.html') async def handle_login(request): return web.HTTPFound(f"https://discord.com/api/oauth2/authorize?client_id={DISCORD_CLIENT_ID}&redirect_uri={DISCORD_REDIRECT_URI}&response_type=code&scope=identify") async def handle_callback(request): code = request.query.get('code') if not code: return web.Response(text="No code") async with ClientSession() as s: data = {'client_id': DISCORD_CLIENT_ID, 'client_secret': DISCORD_CLIENT_SECRET, 'grant_type': 'authorization_code', 'code': code, 'redirect_uri': DISCORD_REDIRECT_URI} async with s.post('https://discord.com/api/oauth2/token', data=data) as r: token_data = await r.json() if 'access_token' not in token_data: return web.Response(text="Auth failed") headers = {'Authorization': f"Bearer {token_data['access_token']}"} async with s.get('https://discord.com/api/users/@me', headers=headers) as r: user_data = await r.json() sid = str(uuid.uuid4()) game.add_player(sid, user_data) # Broadcast new player metadata to everyone p = game.players[sid] meta = {'id': p['id'], 'username': p['username'], 'avatar': p['avatar'], 'color': p['color']} await sio.emit('player_joined', meta) return web.HTTPFound(f"/?token={sid}") app.router.add_get('/', handle_index) app.router.add_get('/login', handle_login) app.router.add_get('/callback', handle_callback) app.router.add_get(ADMIN_PATH, handle_admin) @sio.event async def connect(sid, environ, auth): token = auth.get('token') if auth else None if not token or token not in game.players: return False game.sockets[sid] = token # Send Static Info (Map + All Player Metadata) all_players_meta = [ {'id': p['id'], 'username': p['username'], 'avatar': p['avatar'], 'color': p['color']} for p in game.players.values() ] await sio.emit('init_game', { 'width': MAP_WIDTH, 'height': MAP_HEIGHT, 'static_entities': game.static_entities, 'players_meta': all_players_meta }, to=sid) @sio.event async def disconnect(sid): if sid in game.sockets: pid = game.sockets[sid] del game.sockets[sid] await sio.emit('player_left', {'id': pid}) @sio.event async def input_data(sid, data): if sid in game.sockets: p = game.players[game.sockets[sid]] p['input_angle'] = data.get('angle', 0) p['input_force'] = min(max(data.get('force', 0), 0), 1) @sio.event async def admin_cmd(sid, data): cmd = data.get('cmd') if cmd == 'start_tournament': game.active = True game.generate_map() for p in game.players.values(): p['r'] = 35; p['target_r'] = 35; p['score'] = 0; p['alive'] = True p['x'] = random.random() * MAP_WIDTH; p['y'] = random.random() * MAP_HEIGHT await sio.emit('tournament_reset', {'static_entities': game.static_entities}) elif cmd == 'stop_tournament': game.active = False res = [{'username': p['username'], 'score': int(p['score']), 'avatar': p['avatar']} for p in game.players.values()] await sio.emit('tournament_end', {'results': sorted(res, key=lambda x: x['score'], reverse=True)}) async def game_loop(): while True: start = time.time() if game.active: game.update(1/GAME_TICK_RATE) # OPTIMIZED PACKET: Strip static data (avatar, username, color, car dimensions) # Only send dynamic data (id, x, y, r, score, alive) state = { 'players': [ {'id': p['id'], 'x': int(p['x']), 'y': int(p['y']), 'r': int(p['r']), 'alive': p['alive'], 'score': int(p['score'])} for p in game.players.values() if p['alive'] ], 'cars': [ {'id': c['id'], 'x': int(c['x']), 'y': int(c['y']), 'rotation': round(c['rotation'], 2), 'color': c['color'], 'w': c['w'], 'l': c['l'], 'h': c['h'], # Keep car static for now as they respawn often 'cabinW': c.get('cabinW'), 'cabinL': c.get('cabinL'), 'type': 'car'} for c in game.dynamic_entities ], 'removed': game.removed_entities } await sio.emit('game_update', state) await asyncio.sleep(max(0, (1/GAME_TICK_RATE) - (time.time() - start))) async def init_app(): asyncio.create_task(game_loop()) return app if __name__ == '__main__': web.run_app(init_app(), port=PORT)