yt / main.py
OrbitMC's picture
Update main.py
a34d80f verified
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)