OrbitMC commited on
Commit
10f2308
·
verified ·
1 Parent(s): a8dc970

Upload 4 files

Browse files
Files changed (4) hide show
  1. admin.html +31 -0
  2. index.html +370 -0
  3. main.py +405 -0
  4. requirements.txt +2 -0
admin.html ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Void City Admin</title>
5
+ <style>
6
+ body { background: #222; color: white; font-family: sans-serif; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; }
7
+ button { padding: 20px 40px; font-size: 24px; margin: 10px; cursor: pointer; border: none; border-radius: 5px; }
8
+ .start { background: #2ecc71; color: white; }
9
+ .stop { background: #e74c3c; color: white; }
10
+ </style>
11
+ <script src="https://cdn.socket.io/4.7.4/socket.io.min.js"></script>
12
+ </head>
13
+ <body>
14
+ <h1>Admin Control Panel</h1>
15
+ <button class="start" onclick="send('start_tournament')">Start Tournament</button>
16
+ <button class="stop" onclick="send('stop_tournament')">Stop & Show Results</button>
17
+
18
+ <script>
19
+ // Use the same token as the client for simplicity in this MVP
20
+ // In prod, you'd want a separate admin auth mechanism
21
+ const token = sessionStorage.getItem('game_token');
22
+ const socket = io({ auth: { token: token } });
23
+
24
+ function send(cmd) {
25
+ if(confirm("Are you sure?")) {
26
+ socket.emit('admin_cmd', { cmd: cmd });
27
+ }
28
+ }
29
+ </script>
30
+ </body>
31
+ </html>
index.html ADDED
@@ -0,0 +1,370 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
+ <title>Void City 3D - Multiplayer</title>
7
+ <style>
8
+ * { box-sizing: border-box; user-select: none; -webkit-user-select: none; touch-action: none; }
9
+ body, html { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background-color: #1a1a1a; font-family: -apple-system, sans-serif; }
10
+ #gameCanvas { display: block; width: 100%; height: 100%; }
11
+ #uiLayer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
12
+
13
+ #scoreBoard { position: absolute; top: 20px; right: 20px; text-align: right; color: white; text-shadow: 0 2px 4px rgba(0,0,0,0.8); }
14
+ .score-row { margin-bottom: 5px; font-weight: bold; font-size: 16px; display: flex; align-items: center; justify-content: flex-end; }
15
+ .lb-avatar { width: 20px; height: 20px; border-radius: 50%; margin-right: 8px; border: 1px solid white; }
16
+
17
+ #loginScreen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 100; pointer-events: auto; }
18
+ .discord-btn { background: #5865F2; color: white; padding: 15px 30px; border-radius: 5px; font-size: 20px; font-weight: bold; border: none; cursor: pointer; }
19
+
20
+ #resultsScreen { display: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.95); z-index: 200; flex-direction: column; align-items: center; justify-content: center; color: white; pointer-events: auto; }
21
+ .res-entry { font-size: 24px; margin: 10px; display: flex; align-items: center; }
22
+ </style>
23
+ <script src="https://cdn.socket.io/4.7.4/socket.io.min.js"></script>
24
+ </head>
25
+ <body>
26
+
27
+ <canvas id="gameCanvas"></canvas>
28
+
29
+ <div id="uiLayer">
30
+ <div id="scoreBoard">Connecting...</div>
31
+ </div>
32
+
33
+ <div id="loginScreen">
34
+ <h1 style="color:white; font-size:50px; margin-bottom:30px;">VOID CITY 3D</h1>
35
+ <button class="discord-btn" onclick="login()">Login with Discord</button>
36
+ </div>
37
+
38
+ <div id="resultsScreen">
39
+ <h1>TOURNAMENT ENDED</h1>
40
+ <div id="resultsList"></div>
41
+ </div>
42
+
43
+ <script>
44
+ const COLORS = { ground: '#95a5a6', road: '#2c3e50', marking: '#f1c40f', crosswalk: '#ecf0f1' };
45
+ const GRID_SIZE = 8;
46
+ const BLOCK_SIZE = 280;
47
+ const ROAD_WIDTH = 90;
48
+ const CELL_SIZE = BLOCK_SIZE + ROAD_WIDTH;
49
+
50
+ // --- AUTH ---
51
+ const urlParams = new URLSearchParams(window.location.search);
52
+ const token = urlParams.get('token') || sessionStorage.getItem('game_token');
53
+ if (token) {
54
+ sessionStorage.setItem('game_token', token);
55
+ document.getElementById('loginScreen').style.display = 'none';
56
+ window.history.replaceState({}, document.title, "/");
57
+ }
58
+ function login() { window.location.href = '/login'; }
59
+
60
+ // --- ENGINE ---
61
+ const canvas = document.getElementById('gameCanvas');
62
+ const ctx = canvas.getContext('2d', { alpha: false });
63
+ let socket;
64
+ let mapConfig = { width: 3000, height: 3000 };
65
+
66
+ const camera = { x: 0, y: 0, zoom: 1 };
67
+ let staticEntities = [];
68
+ let cars = [];
69
+
70
+ // OPTIMIZATION: Cache static player data (name, avatar, color)
71
+ let playerCache = {};
72
+ let players = [];
73
+ let myId = null;
74
+
75
+ const input = { active: false, startX:0, startY:0, currX:0, currY:0 };
76
+
77
+ function resize() {
78
+ canvas.width = window.innerWidth;
79
+ canvas.height = window.innerHeight;
80
+ }
81
+ window.addEventListener('resize', resize);
82
+ resize();
83
+
84
+ if (token) {
85
+ socket = io({ auth: { token: token } });
86
+ socket.on('connect', () => { myId = token; });
87
+
88
+ // New Init Event
89
+ socket.on('init_game', (data) => {
90
+ mapConfig.width = data.width;
91
+ mapConfig.height = data.height;
92
+ staticEntities = data.static_entities;
93
+ // Populate cache
94
+ data.players_meta.forEach(p => playerCache[p.id] = p);
95
+ });
96
+
97
+ // Player Management
98
+ socket.on('player_joined', (meta) => { playerCache[meta.id] = meta; });
99
+ socket.on('player_left', (data) => { delete playerCache[data.id]; });
100
+
101
+ // Lean Game Update
102
+ socket.on('game_update', (state) => {
103
+ // Merge dynamic state with static cache
104
+ players = state.players.map(p => {
105
+ const meta = playerCache[p.id] || { username: 'Unknown', color: '#fff', avatar: '' };
106
+ return { ...p, ...meta };
107
+ });
108
+
109
+ cars = state.cars;
110
+
111
+ if(state.removed.length > 0) {
112
+ staticEntities = staticEntities.filter(e => !state.removed.includes(e.id));
113
+ }
114
+ updateUI();
115
+ });
116
+
117
+ socket.on('tournament_reset', (data) => {
118
+ staticEntities = data.static_entities;
119
+ document.getElementById('resultsScreen').style.display = 'none';
120
+ });
121
+ socket.on('tournament_end', (data) => {
122
+ const list = document.getElementById('resultsList');
123
+ list.innerHTML = data.results.map((p, i) => `
124
+ <div class="res-entry">#${i+1} <img src="${p.avatar}" style="width:30px;border-radius:50%;margin:0 10px">${p.username}: ${p.score}</div>
125
+ `).join('');
126
+ document.getElementById('resultsScreen').style.display = 'flex';
127
+ });
128
+ }
129
+
130
+ // --- INPUT ---
131
+ function sendInput() {
132
+ if (!socket || !input.active) {
133
+ if(socket) socket.emit('input_data', { angle: 0, force: 0 });
134
+ return;
135
+ }
136
+ const dx = input.currX - input.startX;
137
+ const dy = input.currY - input.startY;
138
+ const angle = Math.atan2(dy, dx);
139
+ const force = Math.min(Math.hypot(dx, dy) / 50, 1);
140
+ socket.emit('input_data', { angle, force });
141
+ }
142
+ setInterval(sendInput, 50);
143
+
144
+ function handleStart(x, y) { input.active = true; input.startX = x; input.startY = y; input.currX = x; input.currY = y; }
145
+ function handleMove(x, y) { if(input.active) { input.currX = x; input.currY = y; } }
146
+ function handleEnd() { input.active = false; }
147
+
148
+ canvas.addEventListener('mousedown', e => handleStart(e.clientX, e.clientY));
149
+ window.addEventListener('mousemove', e => handleMove(e.clientX, e.clientY));
150
+ window.addEventListener('mouseup', handleEnd);
151
+ canvas.addEventListener('touchstart', e => { e.preventDefault(); handleStart(e.touches[0].clientX, e.touches[0].clientY); }, {passive:false});
152
+ canvas.addEventListener('touchmove', e => { e.preventDefault(); handleMove(e.touches[0].clientX, e.touches[0].clientY); }, {passive:false});
153
+ canvas.addEventListener('touchend', handleEnd);
154
+
155
+ // --- DRAWING ---
156
+ function drawCityFloor() {
157
+ ctx.fillStyle = COLORS.ground;
158
+ ctx.fillRect(0, 0, mapConfig.width, mapConfig.height);
159
+
160
+ for(let iy=0; iy<=GRID_SIZE; iy++) {
161
+ const y = iy * CELL_SIZE;
162
+ ctx.fillStyle = COLORS.road;
163
+ ctx.fillRect(0, y - ROAD_WIDTH/2, mapConfig.width, ROAD_WIDTH);
164
+ ctx.strokeStyle = COLORS.marking; ctx.lineWidth = 2; ctx.setLineDash([20, 20]);
165
+ ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(mapConfig.width, y); ctx.stroke(); ctx.setLineDash([]);
166
+ for(let ix=0; ix<=GRID_SIZE; ix++) {
167
+ const x = ix * CELL_SIZE; ctx.fillStyle = COLORS.crosswalk;
168
+ for(let k=-ROAD_WIDTH/2; k<ROAD_WIDTH/2; k+=10) {
169
+ ctx.fillRect(x - ROAD_WIDTH/2 - 10, y + k, 8, 5); ctx.fillRect(x + ROAD_WIDTH/2 + 2, y + k, 8, 5);
170
+ }
171
+ }
172
+ }
173
+ for(let ix=0; ix<=GRID_SIZE; ix++) {
174
+ const x = ix * CELL_SIZE;
175
+ ctx.fillStyle = COLORS.road; ctx.fillRect(x - ROAD_WIDTH/2, 0, ROAD_WIDTH, mapConfig.height);
176
+ ctx.strokeStyle = COLORS.marking; ctx.lineWidth = 2; ctx.setLineDash([20, 20]);
177
+ ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, mapConfig.height); ctx.stroke(); ctx.setLineDash([]);
178
+ for(let iy=0; iy<=GRID_SIZE; iy++) {
179
+ const y = iy * CELL_SIZE; ctx.fillStyle = COLORS.crosswalk;
180
+ for(let k=-ROAD_WIDTH/2; k<ROAD_WIDTH/2; k+=10) {
181
+ ctx.fillRect(x + k, y - ROAD_WIDTH/2 - 10, 5, 8); ctx.fillRect(x + k, y + ROAD_WIDTH/2 + 2, 5, 8);
182
+ }
183
+ }
184
+ }
185
+ }
186
+
187
+ function drawBox(ctx, x, y, w, l, h, rotation, color, rx, ry, isFalling) {
188
+ const hw = w/2; const hl = l/2;
189
+ const b_tl = {x: -hw, y: -hl}; const b_tr = {x: hw, y: -hl};
190
+ const b_br = {x: hw, y: hl}; const b_bl = {x: -hw, y: hl};
191
+ const r_tl = {x: b_tl.x + rx, y: b_tl.y + ry}; const r_tr = {x: b_tr.x + rx, y: b_tr.y + ry};
192
+ const r_br = {x: b_br.x + rx, y: b_br.y + ry}; const r_bl = {x: b_bl.x + rx, y: b_bl.y + ry};
193
+
194
+ const drawFace = (p1, p2, p3, p4, c) => {
195
+ ctx.fillStyle = c; ctx.beginPath(); ctx.moveTo(p1.x, p1.y); ctx.lineTo(p2.x, p2.y);
196
+ ctx.lineTo(p3.x, p3.y); ctx.lineTo(p4.x, p4.y); ctx.closePath(); ctx.fill();
197
+ };
198
+ drawFace(b_tl, r_tl, r_tr, b_tr, color); drawFace(b_tr, r_tr, r_br, b_br, color);
199
+ drawFace(b_br, r_br, r_bl, b_bl, color); drawFace(b_bl, r_bl, r_tl, b_tl, color);
200
+
201
+ ctx.fillStyle = color; ctx.beginPath(); ctx.moveTo(r_tl.x, r_tl.y); ctx.lineTo(r_tr.x, r_tr.y);
202
+ ctx.lineTo(r_br.x, r_br.y); ctx.lineTo(r_bl.x, r_bl.y); ctx.closePath(); ctx.fill();
203
+ ctx.fillStyle = 'rgba(255,255,255,0.1)'; ctx.fill(); ctx.strokeStyle = 'rgba(0,0,0,0.1)'; ctx.lineWidth = 1; ctx.stroke();
204
+ }
205
+
206
+ function draw3DEntity(ent, falling) {
207
+ ctx.save(); ctx.translate(ent.x, ent.y);
208
+ const h = ent.h || 10; const TILT_FACTOR = 1.2; const PERSPECTIVE_X = 0.0003;
209
+ const rx_screen = (ent.x - camera.x) * (h * PERSPECTIVE_X);
210
+ const ry_screen = -h * TILT_FACTOR;
211
+ const ang = -(ent.rotation || 0);
212
+ const rx = rx_screen * Math.cos(ang) - ry_screen * Math.sin(ang);
213
+ const ry = rx_screen * Math.sin(ang) + ry_screen * Math.cos(ang);
214
+
215
+ if (ent.type === 'car') {
216
+ ctx.rotate(ent.rotation || 0);
217
+ if (!falling) {
218
+ ctx.fillStyle = '#111';
219
+ const wx = ent.w/2 - 8; const wy = ent.l/2;
220
+ ctx.fillRect(wx, -wy-2, 6, 4); ctx.fillRect(wx, wy-2, 6, 4);
221
+ ctx.fillRect(-wx, -wy-2, 6, 4); ctx.fillRect(-wx, wy-2, 6, 4);
222
+ }
223
+ drawBox(ctx, 0, 0, ent.w, ent.l, ent.h, 0, ent.color, rx, ry, falling);
224
+
225
+ const ch = ent.cabinH || 10;
226
+ const rx_screen_cab = (ent.x - camera.x) * (ch * PERSPECTIVE_X);
227
+ const ry_screen_cab = -ch * TILT_FACTOR;
228
+ const rx_cab = rx_screen_cab * Math.cos(ang) - ry_screen_cab * Math.sin(ang);
229
+ const ry_cab = rx_screen_cab * Math.sin(ang) + ry_screen_cab * Math.cos(ang);
230
+
231
+ ctx.translate(rx, ry);
232
+ const cw = ent.cabinW; const cl = ent.cabinL;
233
+ const hw = cw/2; const hl = cl/2;
234
+ const b_tl = {x: -hw, y: -hl}; const b_tr = {x: hw, y: -hl};
235
+ const b_br = {x: hw, y: hl}; const b_bl = {x: -hw, y: hl};
236
+ const r_tl = {x: b_tl.x + rx_cab, y: b_tl.y + ry_cab}; const r_tr = {x: b_tr.x + rx_cab, y: b_tr.y + ry_cab};
237
+ const r_br = {x: b_br.x + rx_cab, y: b_br.y + ry_cab}; const r_bl = {x: b_bl.x + rx_cab, y: b_bl.y + ry_cab};
238
+
239
+ const drawPoly = (pts, color) => {
240
+ ctx.fillStyle = color; ctx.beginPath(); ctx.moveTo(pts[0].x, pts[0].y);
241
+ for(let i=1; i<pts.length; i++) ctx.lineTo(pts[i].x, pts[i].y); ctx.closePath(); ctx.fill();
242
+ };
243
+ drawPoly([b_tl, r_tl, r_tr, b_tr], '#34495e'); drawPoly([b_tr, r_tr, r_br, b_br], '#34495e');
244
+ drawPoly([b_br, r_br, r_bl, b_bl], '#34495e'); drawPoly([b_bl, r_bl, r_tl, b_tl], '#34495e');
245
+ drawPoly([r_tl, r_tr, r_br, r_bl], '#34495e'); ctx.fillStyle = 'rgba(255,255,255,0.1)'; ctx.fill();
246
+
247
+ } else if (ent.type === 'building') {
248
+ ctx.rotate(ent.rotation || 0);
249
+ const w = ent.w; const l = ent.l; const hw = w/2; const hl = l/2;
250
+ const b_tl = {x: -hw, y: -hl}; const b_tr = {x: hw, y: -hl};
251
+ const b_br = {x: hw, y: hl}; const b_bl = {x: -hw, y: hl};
252
+ const r_tl = {x: b_tl.x + rx, y: b_tl.y + ry}; const r_tr = {x: b_tr.x + rx, y: b_tr.y + ry};
253
+ const r_br = {x: b_br.x + rx, y: b_br.y + ry}; const r_bl = {x: b_bl.x + rx, y: b_bl.y + ry};
254
+
255
+ const drawFace = (p1, p2, p3, p4, color, isSide) => {
256
+ ctx.fillStyle = color; ctx.beginPath(); ctx.moveTo(p1.x, p1.y); ctx.lineTo(p2.x, p2.y);
257
+ ctx.lineTo(p3.x, p3.y); ctx.lineTo(p4.x, p4.y); ctx.closePath(); ctx.fill();
258
+ };
259
+ const sideCol = ent.sideColor || ent.color;
260
+ drawFace(b_tl, r_tl, r_tr, b_tr, sideCol, false); drawFace(b_tr, r_tr, r_br, b_br, sideCol, true);
261
+ drawFace(b_br, r_br, r_bl, b_bl, sideCol, true); drawFace(b_bl, r_bl, r_tl, b_tl, sideCol, true);
262
+ ctx.fillStyle = ent.color; ctx.beginPath(); ctx.moveTo(r_tl.x, r_tl.y); ctx.lineTo(r_tr.x, r_tr.y);
263
+ ctx.lineTo(r_br.x, r_br.y); ctx.lineTo(r_bl.x, r_bl.y); ctx.closePath(); ctx.fill();
264
+ ctx.strokeStyle = 'rgba(255,255,255,0.2)'; ctx.lineWidth = 2; ctx.stroke();
265
+
266
+ } else {
267
+ const rx = (ent.x - camera.x) * (ent.h * PERSPECTIVE_X);
268
+ const ry = -ent.h * TILT_FACTOR;
269
+ ctx.fillStyle = 'rgba(0,0,0,0.2)'; ctx.beginPath(); ctx.arc(0,0, ent.r, 0, Math.PI*2); ctx.fill();
270
+
271
+ if(ent.type === 'rock') {
272
+ ctx.fillStyle = ent.color; ctx.beginPath();
273
+ ctx.moveTo(0, -ent.r + ry); ctx.lineTo(ent.r, 0 + ry);
274
+ ctx.lineTo(0, ent.r + ry); ctx.lineTo(-ent.r, 0 + ry); ctx.fill();
275
+ } else if (ent.type === 'flower') {
276
+ ctx.strokeStyle = '#27ae60'; ctx.lineWidth = 2;
277
+ ctx.beginPath(); ctx.moveTo(0,0); ctx.lineTo(rx, ry); ctx.stroke();
278
+ ctx.fillStyle = ent.color; ctx.beginPath(); ctx.arc(rx, ry, ent.r, 0, Math.PI*2); ctx.fill();
279
+ ctx.fillStyle = 'yellow'; ctx.beginPath(); ctx.arc(rx, ry, ent.r/2, 0, Math.PI*2); ctx.fill();
280
+ } else if (ent.type === 'tree') {
281
+ const layers = 4; ctx.strokeStyle = '#5d4037'; ctx.lineWidth = ent.r * 0.4;
282
+ ctx.beginPath(); ctx.moveTo(0,0); ctx.lineTo(rx*0.5, ry*0.5); ctx.stroke();
283
+ for(let i=0; i<layers; i++) {
284
+ const ratio = i / (layers-1);
285
+ const lrx = rx * (0.2 + ratio * 0.8); const lry = ry * (0.2 + ratio * 0.8);
286
+ ctx.fillStyle = i % 2 === 0 ? ent.color : '#2ecc71';
287
+ const size = ent.r * (1.2 - ratio * 0.4);
288
+ ctx.beginPath(); ctx.arc(lrx, lry, size, 0, Math.PI*2); ctx.fill();
289
+ }
290
+ } else {
291
+ ctx.lineWidth = ent.r * 2; ctx.strokeStyle = ent.sideColor || ent.color; ctx.lineCap = 'round';
292
+ ctx.beginPath(); ctx.moveTo(0,0); ctx.lineTo(rx, ry); ctx.stroke();
293
+ ctx.fillStyle = ent.color; ctx.beginPath(); ctx.arc(rx, ry, ent.r, 0, Math.PI*2); ctx.fill();
294
+ }
295
+ }
296
+ ctx.restore();
297
+ }
298
+
299
+ function render() {
300
+ if (!socket) { requestAnimationFrame(render); return; }
301
+
302
+ ctx.fillStyle = '#111';
303
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
304
+
305
+ const me = players.find(p => p.id === myId);
306
+ if (me) {
307
+ camera.x += (me.x - camera.x) * 0.1;
308
+ camera.y += (me.y - camera.y) * 0.1;
309
+ const targetZoom = Math.max(0.3, Math.min(1.0, 400 / (me.r * 2)));
310
+ camera.zoom += (targetZoom - camera.zoom) * 0.05;
311
+ }
312
+
313
+ ctx.save();
314
+ ctx.translate(canvas.width/2, canvas.height/2);
315
+ ctx.scale(camera.zoom, camera.zoom);
316
+ ctx.translate(-camera.x, -camera.y);
317
+
318
+ drawCityFloor();
319
+
320
+ // Draw Players
321
+ players.forEach(p => {
322
+ if(!p.alive) return;
323
+ ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI*2);
324
+ ctx.fillStyle = 'black'; ctx.fill();
325
+ ctx.strokeStyle = p.color; ctx.lineWidth = 4; ctx.stroke();
326
+ ctx.fillStyle = 'white'; ctx.font = `bold ${Math.max(14, p.r/2)}px Arial`;
327
+ ctx.textAlign = 'center'; ctx.fillText(p.username, p.x, p.y - p.r - 10);
328
+ });
329
+
330
+ // Clip holes
331
+ ctx.save();
332
+ ctx.beginPath();
333
+ ctx.rect(camera.x - (canvas.width/camera.zoom), camera.y - (canvas.height/camera.zoom), canvas.width*2/camera.zoom, canvas.height*2/camera.zoom);
334
+ players.forEach(p => { if(p.alive) { ctx.moveTo(p.x, p.y); ctx.arc(p.x, p.y, p.r, 0, Math.PI*2, true); } });
335
+ ctx.clip();
336
+
337
+ // Draw Ents
338
+ const viewW = (canvas.width / camera.zoom) / 2 + 200;
339
+ const viewH = (canvas.height / camera.zoom) / 2 + 200;
340
+ const visibleStatic = staticEntities.filter(e => Math.abs(e.x - camera.x) < viewW && Math.abs(e.y - camera.y) < viewH);
341
+ const allEnts = [...visibleStatic, ...cars];
342
+ allEnts.sort((a,b) => (a.y + (a.l||0)/2) - (b.y + (b.l||0)/2));
343
+ allEnts.forEach(ent => draw3DEntity(ent, false));
344
+
345
+ ctx.restore(); ctx.restore();
346
+
347
+ // Joystick
348
+ if (input.active) {
349
+ ctx.beginPath(); ctx.arc(input.startX, input.startY, 50, 0, Math.PI*2);
350
+ ctx.strokeStyle = 'rgba(255,255,255,0.2)'; ctx.lineWidth = 2; ctx.stroke();
351
+ ctx.beginPath(); ctx.arc(input.currX, input.currY, 20, 0, Math.PI*2);
352
+ ctx.fillStyle = 'rgba(255,255,255,0.4)'; ctx.fill();
353
+ }
354
+ requestAnimationFrame(render);
355
+ }
356
+
357
+ function updateUI() {
358
+ const sb = document.getElementById('scoreBoard');
359
+ const sorted = [...players].sort((a,b) => b.score - a.score).slice(0, 5);
360
+ sb.innerHTML = sorted.map(p => `
361
+ <div class="score-row">
362
+ ${p.avatar ? `<img src="${p.avatar}" class="lb-avatar">` : ''}
363
+ ${p.username}: ${p.score}
364
+ </div>
365
+ `).join('');
366
+ }
367
+ render();
368
+ </script>
369
+ </body>
370
+ </html>
main.py ADDED
@@ -0,0 +1,405 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import asyncio
4
+ import math
5
+ import uuid
6
+ import random
7
+ import time
8
+ from collections import defaultdict
9
+ from aiohttp import web, ClientSession
10
+ import socketio
11
+
12
+ # --- CONFIGURATION ---
13
+ PORT = int(os.getenv('SERVER_PORT', 8080))
14
+ ADMIN_PATH = os.getenv('ADMIN_PATH', '/secret-admin-panel')
15
+
16
+ # !!! REPLACE WITH YOUR DISCORD APP DETAILS !!!
17
+ DISCORD_CLIENT_ID = "1457974183383535649"
18
+ DISCORD_CLIENT_SECRET = "_RoLp-VIb1nC7IOlZd_zvPWHnG4cNMbB"
19
+ DISCORD_REDIRECT_URI = "http://212.227.65.132:15080/callback"
20
+
21
+ GAME_TICK_RATE = 30
22
+ GRID_SIZE = 8
23
+ BLOCK_SIZE = 280
24
+ ROAD_WIDTH = 90
25
+ CELL_SIZE = BLOCK_SIZE + ROAD_WIDTH
26
+ MAP_WIDTH = GRID_SIZE * CELL_SIZE
27
+ MAP_HEIGHT = GRID_SIZE * CELL_SIZE
28
+
29
+ # --- ASSET DEFINITIONS ---
30
+ ENTITY_TYPES = {
31
+ 'ROCK': {'r': 8, 'h': 8, 'color': '#7f8c8d', 'score': 1, 'type': 'rock'},
32
+ 'FLOWER': {'r': 6, 'h': 4, 'color': '#e84393', 'score': 1, 'type': 'flower'},
33
+ 'FENCE': {'w': 20, 'l': 4, 'h': 10, 'color': '#8e44ad', 'score': 1, 'type': 'building'},
34
+ 'HYDRANT': {'r': 6, 'h': 12, 'color': '#e74c3c', 'sideColor': '#c0392b', 'score': 1, 'type': 'cylinder'},
35
+ 'POST': {'r': 4, 'h': 40, 'color': '#bdc3c7', 'sideColor': '#7f8c8d', 'score': 2, 'type': 'cylinder'},
36
+ 'BUSH': {'r': 12, 'h': 10, 'color': '#2ecc71', 'score': 3, 'type': 'bush'},
37
+ 'TREE': {'r': 18, 'h': 70, 'color': '#27ae60', 'score': 5, 'type': 'tree'},
38
+ 'CAR': {'w': 48, 'l': 24, 'h': 12, 'cabinW': 28, 'cabinL': 20, 'cabinH': 10, 'color': '#e74c3c', 'score': 10, 'type': 'car', 'mobile': True},
39
+ 'HOUSE_S': {'w': 70, 'l': 70, 'h': 60, 'color': '#e67e22', 'sideColor': '#d35400', 'score': 50, 'type': 'building', 'windows': True},
40
+ 'HOUSE_M': {'w': 100, 'l': 90, 'h': 90, 'color': '#3498db', 'sideColor': '#2980b9', 'score': 100, 'type': 'building', 'windows': True},
41
+ 'OFFICE': {'w': 110, 'l': 110, 'h': 160, 'color': '#9b59b6', 'sideColor': '#8e44ad', 'score': 200, 'type': 'building', 'windows': True},
42
+ 'TOWER': {'w': 130, 'l': 130, 'h': 280, 'color': '#34495e', 'sideColor': '#2c3e50', 'score': 300, 'type': 'building', 'windows': True}
43
+ }
44
+
45
+ sio = socketio.AsyncServer(async_mode='aiohttp', cors_allowed_origins='*')
46
+ app = web.Application()
47
+ sio.attach(app)
48
+
49
+ class GameState:
50
+ def __init__(self):
51
+ self.players = {}
52
+ self.sockets = {}
53
+ self.static_entities = []
54
+ # Spatial Hash Grid for Static Entities: key=(gridX, gridY), val=[entities]
55
+ self.static_grid = defaultdict(list)
56
+ self.dynamic_entities = []
57
+ self.removed_entities = []
58
+ self.active = True
59
+ self.user_data_file = 'user_data.json'
60
+ self.persistent_data = self.load_data()
61
+ self.generate_map()
62
+
63
+ def load_data(self):
64
+ if os.path.exists(self.user_data_file):
65
+ try:
66
+ with open(self.user_data_file, 'r') as f: return json.load(f)
67
+ except: return {}
68
+ return {}
69
+
70
+ def save_data(self):
71
+ with open(self.user_data_file, 'w') as f:
72
+ json.dump(self.persistent_data, f, indent=2)
73
+
74
+ def generate_map(self):
75
+ self.static_entities = []
76
+ self.static_grid.clear()
77
+ self.dynamic_entities = []
78
+
79
+ # Grid Generation
80
+ for ix in range(GRID_SIZE + 1):
81
+ for iy in range(GRID_SIZE + 1):
82
+ x = ix * CELL_SIZE
83
+ y = iy * CELL_SIZE
84
+ if ix < GRID_SIZE and iy < GRID_SIZE:
85
+ self.generate_block(x + ROAD_WIDTH/2, y + ROAD_WIDTH/2)
86
+
87
+ # Car Generation
88
+ lane_offset = ROAD_WIDTH / 4
89
+ for _ in range(60):
90
+ self.spawn_car(lane_offset)
91
+
92
+ def add_entity(self, x, y, template, **kwargs):
93
+ ent = template.copy()
94
+ ent.update(kwargs)
95
+ ent['x'] = x
96
+ ent['y'] = y
97
+ ent['id'] = str(uuid.uuid4())
98
+ ent['startX'] = x
99
+ ent['startY'] = y
100
+ ent['varSeed'] = random.random()
101
+ ent['rotation'] = kwargs.get('rotation', 0)
102
+
103
+ if ent['type'] in ['building', 'car']:
104
+ ent['radius'] = math.hypot(ent['w']/2, ent.get('l', ent['w'])/2)
105
+ else:
106
+ ent['radius'] = ent['r']
107
+
108
+ if ent['type'] == 'flower':
109
+ ent['color'] = random.choice(['#e84393', '#fd79a8', '#00b894', '#fab1a0'])
110
+
111
+ # Add to main list AND spatial grid
112
+ self.static_entities.append(ent)
113
+ gx = int(x // CELL_SIZE)
114
+ gy = int(y // CELL_SIZE)
115
+ self.static_grid[(gx, gy)].append(ent)
116
+
117
+ def get_nearby_static(self, x, y):
118
+ # Return entities in the 3x3 grid cells around the point
119
+ gx = int(x // CELL_SIZE)
120
+ gy = int(y // CELL_SIZE)
121
+ nearby = []
122
+ for ix in range(gx - 1, gx + 2):
123
+ for iy in range(gy - 1, gy + 2):
124
+ nearby.extend(self.static_grid[(ix, iy)])
125
+ return nearby
126
+
127
+ def generate_block(self, bx, by):
128
+ cx = bx + BLOCK_SIZE/2
129
+ cy = by + BLOCK_SIZE/2
130
+ b_type = random.random()
131
+
132
+ if b_type < 0.2: # Park
133
+ inset = 10
134
+ sz = BLOCK_SIZE - inset*2
135
+ for k in range(0, int(sz), 25):
136
+ self.add_entity(bx + inset + k, by + inset, ENTITY_TYPES['FENCE'])
137
+ self.add_entity(bx + inset + k, by + BLOCK_SIZE - inset, ENTITY_TYPES['FENCE'])
138
+ for k in range(0, int(sz), 25):
139
+ self.add_entity(bx + inset, by + inset + k, ENTITY_TYPES['FENCE'], w=5, l=20)
140
+ self.add_entity(bx + BLOCK_SIZE - inset, by + inset + k, ENTITY_TYPES['FENCE'], w=5, l=20)
141
+
142
+ for _ in range(10):
143
+ ox = random.random() * (sz-40) + 20
144
+ oy = random.random() * (sz-40) + 20
145
+ tmpl = ENTITY_TYPES['TREE'] if random.random() > 0.6 else ENTITY_TYPES['FLOWER']
146
+ self.add_entity(bx+inset+ox, by+inset+oy, tmpl)
147
+ self.add_entity(cx, cy, ENTITY_TYPES['HYDRANT'])
148
+
149
+ elif b_type < 0.6: # Residential
150
+ q = BLOCK_SIZE/4
151
+ self.add_entity(cx - q, cy - q, ENTITY_TYPES['HOUSE_S'])
152
+ self.add_entity(cx + q, cy - q, ENTITY_TYPES['HOUSE_M'])
153
+ self.add_entity(cx - q, cy + q, ENTITY_TYPES['HOUSE_M'])
154
+ self.add_entity(cx + q, cy + q, ENTITY_TYPES['HOUSE_S'])
155
+ self.add_entity(cx, cy, ENTITY_TYPES['TREE'])
156
+ self.add_entity(bx + 10, cy, ENTITY_TYPES['BUSH'])
157
+ self.add_entity(bx + BLOCK_SIZE - 10, cy, ENTITY_TYPES['BUSH'])
158
+
159
+ else: # Downtown
160
+ if random.random() > 0.5:
161
+ self.add_entity(cx, cy, ENTITY_TYPES['TOWER'])
162
+ else:
163
+ self.add_entity(cx - 50, cy, ENTITY_TYPES['OFFICE'])
164
+ self.add_entity(cx + 50, cy, ENTITY_TYPES['OFFICE'])
165
+
166
+ self.add_entity(bx + 20, by + 20, ENTITY_TYPES['POST'])
167
+ self.add_entity(bx + BLOCK_SIZE - 20, by + 20, ENTITY_TYPES['POST'])
168
+ self.add_entity(bx + 20, by + BLOCK_SIZE - 20, ENTITY_TYPES['POST'])
169
+ self.add_entity(bx + BLOCK_SIZE - 20, by + BLOCK_SIZE - 20, ENTITY_TYPES['POST'])
170
+
171
+ def spawn_car(self, offset):
172
+ orient = 0 if random.random() > 0.5 else 1
173
+ if orient == 0:
174
+ iy = random.randint(0, GRID_SIZE)
175
+ y = iy * CELL_SIZE
176
+ x = random.random() * MAP_WIDTH
177
+ dir = 1 if random.random() > 0.5 else -1
178
+ y += dir * offset
179
+ rot = 0 if dir == 1 else math.pi
180
+ else:
181
+ ix = random.randint(0, GRID_SIZE)
182
+ x = ix * CELL_SIZE
183
+ y = random.random() * MAP_HEIGHT
184
+ dir = 1 if random.random() > 0.5 else -1
185
+ x += dir * offset
186
+ rot = math.pi/2 if dir == 1 else -math.pi/2
187
+
188
+ car = ENTITY_TYPES['CAR'].copy()
189
+ car.update({
190
+ 'x': x, 'y': y, 'rotation': rot, 'id': str(uuid.uuid4()),
191
+ 'radius': math.hypot(24, 12), 'type': 'car', 'mobile': True,
192
+ 'color': random.choice(['#e74c3c', '#3498db', '#f1c40f', '#ecf0f1', '#2c3e50'])
193
+ })
194
+ self.dynamic_entities.append(car)
195
+
196
+ def add_player(self, session_id, user_info):
197
+ saved = self.persistent_data.get(session_id, {})
198
+ self.players[session_id] = {
199
+ 'id': session_id,
200
+ 'discord_id': user_info['id'],
201
+ 'username': user_info['username'],
202
+ 'avatar': f"https://cdn.discordapp.com/avatars/{user_info['id']}/{user_info['avatar']}.png" if user_info['avatar'] else "",
203
+ 'x': random.random() * MAP_WIDTH,
204
+ 'y': random.random() * MAP_HEIGHT,
205
+ 'r': saved.get('r', 35),
206
+ 'target_r': saved.get('r', 35),
207
+ 'score': saved.get('score', 0),
208
+ 'color': '#3498db',
209
+ 'input_angle': 0, 'input_force': 0, 'vx': 0, 'vy': 0,
210
+ 'alive': True, 'respawn_timer': 0
211
+ }
212
+ if session_id not in self.persistent_data:
213
+ self.persistent_data[session_id] = {'r': 35, 'score': 0, 'username': user_info['username']}
214
+ self.save_data()
215
+
216
+ def update(self, dt):
217
+ if not self.active: return
218
+ self.removed_entities = []
219
+
220
+ sorted_players = sorted(self.players.values(), key=lambda p: p['r'], reverse=True)
221
+
222
+ for p in sorted_players:
223
+ if not p['alive']:
224
+ p['respawn_timer'] -= dt
225
+ if p['respawn_timer'] <= 0:
226
+ p['alive'] = True
227
+ p['r'] = 35; p['target_r'] = 35; p['score'] = 0
228
+ p['x'] = random.random() * MAP_WIDTH
229
+ p['y'] = random.random() * MAP_HEIGHT
230
+ continue
231
+
232
+ speed = 180 * (1 - (p['r'] / 800))
233
+ p['vx'] = math.cos(p['input_angle']) * p['input_force'] * speed
234
+ p['vy'] = math.sin(p['input_angle']) * p['input_force'] * speed
235
+ p['x'] = max(p['r'], min(MAP_WIDTH - p['r'], p['x'] + p['vx'] * dt))
236
+ p['y'] = max(p['r'], min(MAP_HEIGHT - p['r'], p['y'] + p['vy'] * dt))
237
+
238
+ if p['r'] < p['target_r']: p['r'] += (p['target_r'] - p['r']) * dt
239
+
240
+ # Eat Static (OPTIMIZED: Use Spatial Hash)
241
+ nearby_static = self.get_nearby_static(p['x'], p['y'])
242
+ for ent in nearby_static:
243
+ # Basic check before expensive sqrt
244
+ if abs(p['x'] - ent['x']) > p['r'] or abs(p['y'] - ent['y']) > p['r']: continue
245
+
246
+ dist = math.hypot(p['x'] - ent['x'], p['y'] - ent['y'])
247
+ if dist < p['r'] and p['r'] > ent['radius']:
248
+ self.static_entities.remove(ent)
249
+ # Also remove from grid for consistency (slow, but eats are rare compared to frames)
250
+ gx, gy = int(ent['x']//CELL_SIZE), int(ent['y']//CELL_SIZE)
251
+ if ent in self.static_grid[(gx, gy)]:
252
+ self.static_grid[(gx, gy)].remove(ent)
253
+
254
+ self.removed_entities.append(ent['id'])
255
+ p['score'] += ent['score']
256
+ p['target_r'] += math.sqrt(ent['score']) * 0.3
257
+
258
+ # Eat Cars
259
+ for car in self.dynamic_entities:
260
+ dist = math.hypot(p['x'] - car['x'], p['y'] - car['y'])
261
+ if dist < p['r'] and p['r'] > car['radius']:
262
+ p['score'] += car['score']
263
+ p['target_r'] += math.sqrt(car['score']) * 0.3
264
+ car['x'] = random.randint(0, GRID_SIZE) * CELL_SIZE
265
+ car['y'] = random.random() * MAP_HEIGHT
266
+ self.removed_entities.append(car['id'])
267
+
268
+ # Eat Players
269
+ for other in sorted_players:
270
+ if p == other or not other['alive']: continue
271
+ dist = math.hypot(p['x'] - other['x'], p['y'] - other['y'])
272
+ if p['r'] > other['r'] * 1.1 and dist < p['r'] - other['r']*0.4:
273
+ other['alive'] = False
274
+ other['respawn_timer'] = 5
275
+ p['score'] += 50 + other['score'] * 0.5
276
+ p['target_r'] += 5
277
+ self.persistent_data[p['id']]['score'] = p['score']
278
+ self.persistent_data[p['id']]['r'] = p['target_r']
279
+ self.persistent_data[other['id']]['score'] = 0
280
+ self.persistent_data[other['id']]['r'] = 35
281
+ self.save_data()
282
+
283
+ # Update Cars
284
+ for car in self.dynamic_entities:
285
+ speed = 150
286
+ car['x'] += math.cos(car['rotation']) * speed * dt
287
+ car['y'] += math.sin(car['rotation']) * speed * dt
288
+ if car['x'] > MAP_WIDTH: car['x'] = 0
289
+ if car['x'] < 0: car['x'] = MAP_WIDTH
290
+ if car['y'] > MAP_HEIGHT: car['y'] = 0
291
+ if car['y'] < 0: car['y'] = MAP_HEIGHT
292
+
293
+ game = GameState()
294
+
295
+ # --- WEB HANDLERS ---
296
+ async def handle_index(request): return web.FileResponse('./index.html')
297
+ async def handle_admin(request): return web.FileResponse('./admin.html')
298
+ async def handle_login(request):
299
+ 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")
300
+
301
+ async def handle_callback(request):
302
+ code = request.query.get('code')
303
+ if not code: return web.Response(text="No code")
304
+ async with ClientSession() as s:
305
+ data = {'client_id': DISCORD_CLIENT_ID, 'client_secret': DISCORD_CLIENT_SECRET, 'grant_type': 'authorization_code', 'code': code, 'redirect_uri': DISCORD_REDIRECT_URI}
306
+ async with s.post('https://discord.com/api/oauth2/token', data=data) as r:
307
+ token_data = await r.json()
308
+ if 'access_token' not in token_data: return web.Response(text="Auth failed")
309
+ headers = {'Authorization': f"Bearer {token_data['access_token']}"}
310
+ async with s.get('https://discord.com/api/users/@me', headers=headers) as r:
311
+ user_data = await r.json()
312
+ sid = str(uuid.uuid4())
313
+ game.add_player(sid, user_data)
314
+
315
+ # Broadcast new player metadata to everyone
316
+ p = game.players[sid]
317
+ meta = {'id': p['id'], 'username': p['username'], 'avatar': p['avatar'], 'color': p['color']}
318
+ await sio.emit('player_joined', meta)
319
+
320
+ return web.HTTPFound(f"/?token={sid}")
321
+
322
+ app.router.add_get('/', handle_index)
323
+ app.router.add_get('/login', handle_login)
324
+ app.router.add_get('/callback', handle_callback)
325
+ app.router.add_get(ADMIN_PATH, handle_admin)
326
+
327
+ @sio.event
328
+ async def connect(sid, environ, auth):
329
+ token = auth.get('token') if auth else None
330
+ if not token or token not in game.players: return False
331
+ game.sockets[sid] = token
332
+
333
+ # Send Static Info (Map + All Player Metadata)
334
+ all_players_meta = [
335
+ {'id': p['id'], 'username': p['username'], 'avatar': p['avatar'], 'color': p['color']}
336
+ for p in game.players.values()
337
+ ]
338
+
339
+ await sio.emit('init_game', {
340
+ 'width': MAP_WIDTH, 'height': MAP_HEIGHT,
341
+ 'static_entities': game.static_entities,
342
+ 'players_meta': all_players_meta
343
+ }, to=sid)
344
+
345
+ @sio.event
346
+ async def disconnect(sid):
347
+ if sid in game.sockets:
348
+ pid = game.sockets[sid]
349
+ del game.sockets[sid]
350
+ await sio.emit('player_left', {'id': pid})
351
+
352
+ @sio.event
353
+ async def input_data(sid, data):
354
+ if sid in game.sockets:
355
+ p = game.players[game.sockets[sid]]
356
+ p['input_angle'] = data.get('angle', 0)
357
+ p['input_force'] = min(max(data.get('force', 0), 0), 1)
358
+
359
+ @sio.event
360
+ async def admin_cmd(sid, data):
361
+ cmd = data.get('cmd')
362
+ if cmd == 'start_tournament':
363
+ game.active = True
364
+ game.generate_map()
365
+ for p in game.players.values():
366
+ p['r'] = 35; p['target_r'] = 35; p['score'] = 0; p['alive'] = True
367
+ p['x'] = random.random() * MAP_WIDTH; p['y'] = random.random() * MAP_HEIGHT
368
+ await sio.emit('tournament_reset', {'static_entities': game.static_entities})
369
+ elif cmd == 'stop_tournament':
370
+ game.active = False
371
+ res = [{'username': p['username'], 'score': int(p['score']), 'avatar': p['avatar']} for p in game.players.values()]
372
+ await sio.emit('tournament_end', {'results': sorted(res, key=lambda x: x['score'], reverse=True)})
373
+
374
+ async def game_loop():
375
+ while True:
376
+ start = time.time()
377
+ if game.active:
378
+ game.update(1/GAME_TICK_RATE)
379
+
380
+ # OPTIMIZED PACKET: Strip static data (avatar, username, color, car dimensions)
381
+ # Only send dynamic data (id, x, y, r, score, alive)
382
+ state = {
383
+ 'players': [
384
+ {'id': p['id'], 'x': int(p['x']), 'y': int(p['y']), 'r': int(p['r']),
385
+ 'alive': p['alive'], 'score': int(p['score'])}
386
+ for p in game.players.values() if p['alive']
387
+ ],
388
+ 'cars': [
389
+ {'id': c['id'], 'x': int(c['x']), 'y': int(c['y']), 'rotation': round(c['rotation'], 2),
390
+ 'color': c['color'], 'w': c['w'], 'l': c['l'], 'h': c['h'], # Keep car static for now as they respawn often
391
+ 'cabinW': c.get('cabinW'), 'cabinL': c.get('cabinL'), 'type': 'car'}
392
+ for c in game.dynamic_entities
393
+ ],
394
+ 'removed': game.removed_entities
395
+ }
396
+ await sio.emit('game_update', state)
397
+
398
+ await asyncio.sleep(max(0, (1/GAME_TICK_RATE) - (time.time() - start)))
399
+
400
+ async def init_app():
401
+ asyncio.create_task(game_loop())
402
+ return app
403
+
404
+ if __name__ == '__main__':
405
+ web.run_app(init_app(), port=PORT)
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ aiohttp==3.9.3
2
+ python-socketio==5.11.1