Download index.html from Unmid/yt: direct link, hf CLI and curl.
- Browser
- Download file 19.7 kB
-
https://huggingface.co/spaces/Unmid/yt/resolve/main/index.html
- Command line
-
hf download hf://spaces/Unmid/yt/index.html
-
curl -L -o index.html https://huggingface.co/spaces/Unmid/yt/resolve/main/index.html
19.7 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
| <title>Void City 3D - Multiplayer</title> | |
| <style> | |
| * { box-sizing: border-box; user-select: none; -webkit-user-select: none; touch-action: none; } | |
| body, html { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background-color: #1a1a1a; font-family: -apple-system, sans-serif; } | |
| #gameCanvas { display: block; width: 100%; height: 100%; } | |
| #uiLayer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; } | |
| #scoreBoard { position: absolute; top: 20px; right: 20px; text-align: right; color: white; text-shadow: 0 2px 4px rgba(0,0,0,0.8); } | |
| .score-row { margin-bottom: 5px; font-weight: bold; font-size: 16px; display: flex; align-items: center; justify-content: flex-end; } | |
| .lb-avatar { width: 20px; height: 20px; border-radius: 50%; margin-right: 8px; border: 1px solid white; } | |
| #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; } | |
| .discord-btn { background: #5865F2; color: white; padding: 15px 30px; border-radius: 5px; font-size: 20px; font-weight: bold; border: none; cursor: pointer; } | |
| #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; } | |
| .res-entry { font-size: 24px; margin: 10px; display: flex; align-items: center; } | |
| </style> | |
| <script src="https://cdn.socket.io/4.7.4/socket.io.min.js"></script> | |
| </head> | |
| <body> | |
| <canvas id="gameCanvas"></canvas> | |
| <div id="uiLayer"> | |
| <div id="scoreBoard">Connecting...</div> | |
| </div> | |
| <div id="loginScreen"> | |
| <h1 style="color:white; font-size:50px; margin-bottom:30px;">VOID CITY 3D</h1> | |
| <button class="discord-btn" onclick="login()">Login with Discord</button> | |
| </div> | |
| <div id="resultsScreen"> | |
| <h1>TOURNAMENT ENDED</h1> | |
| <div id="resultsList"></div> | |
| </div> | |
| <script> | |
| const COLORS = { ground: '#95a5a6', road: '#2c3e50', marking: '#f1c40f', crosswalk: '#ecf0f1' }; | |
| const GRID_SIZE = 8; | |
| const BLOCK_SIZE = 280; | |
| const ROAD_WIDTH = 90; | |
| const CELL_SIZE = BLOCK_SIZE + ROAD_WIDTH; | |
| // --- AUTH --- | |
| const urlParams = new URLSearchParams(window.location.search); | |
| const token = urlParams.get('token') || sessionStorage.getItem('game_token'); | |
| if (token) { | |
| sessionStorage.setItem('game_token', token); | |
| document.getElementById('loginScreen').style.display = 'none'; | |
| window.history.replaceState({}, document.title, "/"); | |
| } | |
| function login() { window.location.href = '/login'; } | |
| // --- ENGINE --- | |
| const canvas = document.getElementById('gameCanvas'); | |
| const ctx = canvas.getContext('2d', { alpha: false }); | |
| let socket; | |
| let mapConfig = { width: 3000, height: 3000 }; | |
| const camera = { x: 0, y: 0, zoom: 1 }; | |
| let staticEntities = []; | |
| let cars = []; | |
| // OPTIMIZATION: Cache static player data (name, avatar, color) | |
| let playerCache = {}; | |
| let players = []; | |
| let myId = null; | |
| const input = { active: false, startX:0, startY:0, currX:0, currY:0 }; | |
| function resize() { | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| } | |
| window.addEventListener('resize', resize); | |
| resize(); | |
| if (token) { | |
| socket = io({ auth: { token: token } }); | |
| socket.on('connect', () => { myId = token; }); | |
| // New Init Event | |
| socket.on('init_game', (data) => { | |
| mapConfig.width = data.width; | |
| mapConfig.height = data.height; | |
| staticEntities = data.static_entities; | |
| // Populate cache | |
| data.players_meta.forEach(p => playerCache[p.id] = p); | |
| }); | |
| // Player Management | |
| socket.on('player_joined', (meta) => { playerCache[meta.id] = meta; }); | |
| socket.on('player_left', (data) => { delete playerCache[data.id]; }); | |
| // Lean Game Update | |
| socket.on('game_update', (state) => { | |
| // Merge dynamic state with static cache | |
| players = state.players.map(p => { | |
| const meta = playerCache[p.id] || { username: 'Unknown', color: '#fff', avatar: '' }; | |
| return { ...p, ...meta }; | |
| }); | |
| cars = state.cars; | |
| if(state.removed.length > 0) { | |
| staticEntities = staticEntities.filter(e => !state.removed.includes(e.id)); | |
| } | |
| updateUI(); | |
| }); | |
| socket.on('tournament_reset', (data) => { | |
| staticEntities = data.static_entities; | |
| document.getElementById('resultsScreen').style.display = 'none'; | |
| }); | |
| socket.on('tournament_end', (data) => { | |
| const list = document.getElementById('resultsList'); | |
| list.innerHTML = data.results.map((p, i) => ` | |
| <div class="res-entry">#${i+1} <img src="${p.avatar}" style="width:30px;border-radius:50%;margin:0 10px">${p.username}: ${p.score}</div> | |
| `).join(''); | |
| document.getElementById('resultsScreen').style.display = 'flex'; | |
| }); | |
| } | |
| // --- INPUT --- | |
| function sendInput() { | |
| if (!socket || !input.active) { | |
| if(socket) socket.emit('input_data', { angle: 0, force: 0 }); | |
| return; | |
| } | |
| const dx = input.currX - input.startX; | |
| const dy = input.currY - input.startY; | |
| const angle = Math.atan2(dy, dx); | |
| const force = Math.min(Math.hypot(dx, dy) / 50, 1); | |
| socket.emit('input_data', { angle, force }); | |
| } | |
| setInterval(sendInput, 50); | |
| function handleStart(x, y) { input.active = true; input.startX = x; input.startY = y; input.currX = x; input.currY = y; } | |
| function handleMove(x, y) { if(input.active) { input.currX = x; input.currY = y; } } | |
| function handleEnd() { input.active = false; } | |
| canvas.addEventListener('mousedown', e => handleStart(e.clientX, e.clientY)); | |
| window.addEventListener('mousemove', e => handleMove(e.clientX, e.clientY)); | |
| window.addEventListener('mouseup', handleEnd); | |
| canvas.addEventListener('touchstart', e => { e.preventDefault(); handleStart(e.touches[0].clientX, e.touches[0].clientY); }, {passive:false}); | |
| canvas.addEventListener('touchmove', e => { e.preventDefault(); handleMove(e.touches[0].clientX, e.touches[0].clientY); }, {passive:false}); | |
| canvas.addEventListener('touchend', handleEnd); | |
| // --- DRAWING --- | |
| function drawCityFloor() { | |
| ctx.fillStyle = COLORS.ground; | |
| ctx.fillRect(0, 0, mapConfig.width, mapConfig.height); | |
| for(let iy=0; iy<=GRID_SIZE; iy++) { | |
| const y = iy * CELL_SIZE; | |
| ctx.fillStyle = COLORS.road; | |
| ctx.fillRect(0, y - ROAD_WIDTH/2, mapConfig.width, ROAD_WIDTH); | |
| ctx.strokeStyle = COLORS.marking; ctx.lineWidth = 2; ctx.setLineDash([20, 20]); | |
| ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(mapConfig.width, y); ctx.stroke(); ctx.setLineDash([]); | |
| for(let ix=0; ix<=GRID_SIZE; ix++) { | |
| const x = ix * CELL_SIZE; ctx.fillStyle = COLORS.crosswalk; | |
| for(let k=-ROAD_WIDTH/2; k<ROAD_WIDTH/2; k+=10) { | |
| ctx.fillRect(x - ROAD_WIDTH/2 - 10, y + k, 8, 5); ctx.fillRect(x + ROAD_WIDTH/2 + 2, y + k, 8, 5); | |
| } | |
| } | |
| } | |
| for(let ix=0; ix<=GRID_SIZE; ix++) { | |
| const x = ix * CELL_SIZE; | |
| ctx.fillStyle = COLORS.road; ctx.fillRect(x - ROAD_WIDTH/2, 0, ROAD_WIDTH, mapConfig.height); | |
| ctx.strokeStyle = COLORS.marking; ctx.lineWidth = 2; ctx.setLineDash([20, 20]); | |
| ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, mapConfig.height); ctx.stroke(); ctx.setLineDash([]); | |
| for(let iy=0; iy<=GRID_SIZE; iy++) { | |
| const y = iy * CELL_SIZE; ctx.fillStyle = COLORS.crosswalk; | |
| for(let k=-ROAD_WIDTH/2; k<ROAD_WIDTH/2; k+=10) { | |
| ctx.fillRect(x + k, y - ROAD_WIDTH/2 - 10, 5, 8); ctx.fillRect(x + k, y + ROAD_WIDTH/2 + 2, 5, 8); | |
| } | |
| } | |
| } | |
| } | |
| function drawBox(ctx, x, y, w, l, h, rotation, color, rx, ry, isFalling) { | |
| const hw = w/2; const hl = l/2; | |
| const b_tl = {x: -hw, y: -hl}; const b_tr = {x: hw, y: -hl}; | |
| const b_br = {x: hw, y: hl}; const b_bl = {x: -hw, y: hl}; | |
| 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}; | |
| 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}; | |
| const drawFace = (p1, p2, p3, p4, c) => { | |
| ctx.fillStyle = c; ctx.beginPath(); ctx.moveTo(p1.x, p1.y); ctx.lineTo(p2.x, p2.y); | |
| ctx.lineTo(p3.x, p3.y); ctx.lineTo(p4.x, p4.y); ctx.closePath(); ctx.fill(); | |
| }; | |
| drawFace(b_tl, r_tl, r_tr, b_tr, color); drawFace(b_tr, r_tr, r_br, b_br, color); | |
| drawFace(b_br, r_br, r_bl, b_bl, color); drawFace(b_bl, r_bl, r_tl, b_tl, color); | |
| ctx.fillStyle = color; ctx.beginPath(); ctx.moveTo(r_tl.x, r_tl.y); ctx.lineTo(r_tr.x, r_tr.y); | |
| ctx.lineTo(r_br.x, r_br.y); ctx.lineTo(r_bl.x, r_bl.y); ctx.closePath(); ctx.fill(); | |
| ctx.fillStyle = 'rgba(255,255,255,0.1)'; ctx.fill(); ctx.strokeStyle = 'rgba(0,0,0,0.1)'; ctx.lineWidth = 1; ctx.stroke(); | |
| } | |
| function draw3DEntity(ent, falling) { | |
| ctx.save(); ctx.translate(ent.x, ent.y); | |
| const h = ent.h || 10; const TILT_FACTOR = 1.2; const PERSPECTIVE_X = 0.0003; | |
| const rx_screen = (ent.x - camera.x) * (h * PERSPECTIVE_X); | |
| const ry_screen = -h * TILT_FACTOR; | |
| const ang = -(ent.rotation || 0); | |
| const rx = rx_screen * Math.cos(ang) - ry_screen * Math.sin(ang); | |
| const ry = rx_screen * Math.sin(ang) + ry_screen * Math.cos(ang); | |
| if (ent.type === 'car') { | |
| ctx.rotate(ent.rotation || 0); | |
| if (!falling) { | |
| ctx.fillStyle = '#111'; | |
| const wx = ent.w/2 - 8; const wy = ent.l/2; | |
| ctx.fillRect(wx, -wy-2, 6, 4); ctx.fillRect(wx, wy-2, 6, 4); | |
| ctx.fillRect(-wx, -wy-2, 6, 4); ctx.fillRect(-wx, wy-2, 6, 4); | |
| } | |
| drawBox(ctx, 0, 0, ent.w, ent.l, ent.h, 0, ent.color, rx, ry, falling); | |
| const ch = ent.cabinH || 10; | |
| const rx_screen_cab = (ent.x - camera.x) * (ch * PERSPECTIVE_X); | |
| const ry_screen_cab = -ch * TILT_FACTOR; | |
| const rx_cab = rx_screen_cab * Math.cos(ang) - ry_screen_cab * Math.sin(ang); | |
| const ry_cab = rx_screen_cab * Math.sin(ang) + ry_screen_cab * Math.cos(ang); | |
| ctx.translate(rx, ry); | |
| const cw = ent.cabinW; const cl = ent.cabinL; | |
| const hw = cw/2; const hl = cl/2; | |
| const b_tl = {x: -hw, y: -hl}; const b_tr = {x: hw, y: -hl}; | |
| const b_br = {x: hw, y: hl}; const b_bl = {x: -hw, y: hl}; | |
| 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}; | |
| 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}; | |
| const drawPoly = (pts, color) => { | |
| ctx.fillStyle = color; ctx.beginPath(); ctx.moveTo(pts[0].x, pts[0].y); | |
| for(let i=1; i<pts.length; i++) ctx.lineTo(pts[i].x, pts[i].y); ctx.closePath(); ctx.fill(); | |
| }; | |
| drawPoly([b_tl, r_tl, r_tr, b_tr], '#34495e'); drawPoly([b_tr, r_tr, r_br, b_br], '#34495e'); | |
| drawPoly([b_br, r_br, r_bl, b_bl], '#34495e'); drawPoly([b_bl, r_bl, r_tl, b_tl], '#34495e'); | |
| drawPoly([r_tl, r_tr, r_br, r_bl], '#34495e'); ctx.fillStyle = 'rgba(255,255,255,0.1)'; ctx.fill(); | |
| } else if (ent.type === 'building') { | |
| ctx.rotate(ent.rotation || 0); | |
| const w = ent.w; const l = ent.l; const hw = w/2; const hl = l/2; | |
| const b_tl = {x: -hw, y: -hl}; const b_tr = {x: hw, y: -hl}; | |
| const b_br = {x: hw, y: hl}; const b_bl = {x: -hw, y: hl}; | |
| 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}; | |
| 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}; | |
| const drawFace = (p1, p2, p3, p4, color, isSide) => { | |
| ctx.fillStyle = color; ctx.beginPath(); ctx.moveTo(p1.x, p1.y); ctx.lineTo(p2.x, p2.y); | |
| ctx.lineTo(p3.x, p3.y); ctx.lineTo(p4.x, p4.y); ctx.closePath(); ctx.fill(); | |
| }; | |
| const sideCol = ent.sideColor || ent.color; | |
| drawFace(b_tl, r_tl, r_tr, b_tr, sideCol, false); drawFace(b_tr, r_tr, r_br, b_br, sideCol, true); | |
| drawFace(b_br, r_br, r_bl, b_bl, sideCol, true); drawFace(b_bl, r_bl, r_tl, b_tl, sideCol, true); | |
| ctx.fillStyle = ent.color; ctx.beginPath(); ctx.moveTo(r_tl.x, r_tl.y); ctx.lineTo(r_tr.x, r_tr.y); | |
| ctx.lineTo(r_br.x, r_br.y); ctx.lineTo(r_bl.x, r_bl.y); ctx.closePath(); ctx.fill(); | |
| ctx.strokeStyle = 'rgba(255,255,255,0.2)'; ctx.lineWidth = 2; ctx.stroke(); | |
| } else { | |
| const rx = (ent.x - camera.x) * (ent.h * PERSPECTIVE_X); | |
| const ry = -ent.h * TILT_FACTOR; | |
| ctx.fillStyle = 'rgba(0,0,0,0.2)'; ctx.beginPath(); ctx.arc(0,0, ent.r, 0, Math.PI*2); ctx.fill(); | |
| if(ent.type === 'rock') { | |
| ctx.fillStyle = ent.color; ctx.beginPath(); | |
| ctx.moveTo(0, -ent.r + ry); ctx.lineTo(ent.r, 0 + ry); | |
| ctx.lineTo(0, ent.r + ry); ctx.lineTo(-ent.r, 0 + ry); ctx.fill(); | |
| } else if (ent.type === 'flower') { | |
| ctx.strokeStyle = '#27ae60'; ctx.lineWidth = 2; | |
| ctx.beginPath(); ctx.moveTo(0,0); ctx.lineTo(rx, ry); ctx.stroke(); | |
| ctx.fillStyle = ent.color; ctx.beginPath(); ctx.arc(rx, ry, ent.r, 0, Math.PI*2); ctx.fill(); | |
| ctx.fillStyle = 'yellow'; ctx.beginPath(); ctx.arc(rx, ry, ent.r/2, 0, Math.PI*2); ctx.fill(); | |
| } else if (ent.type === 'tree') { | |
| const layers = 4; ctx.strokeStyle = '#5d4037'; ctx.lineWidth = ent.r * 0.4; | |
| ctx.beginPath(); ctx.moveTo(0,0); ctx.lineTo(rx*0.5, ry*0.5); ctx.stroke(); | |
| for(let i=0; i<layers; i++) { | |
| const ratio = i / (layers-1); | |
| const lrx = rx * (0.2 + ratio * 0.8); const lry = ry * (0.2 + ratio * 0.8); | |
| ctx.fillStyle = i % 2 === 0 ? ent.color : '#2ecc71'; | |
| const size = ent.r * (1.2 - ratio * 0.4); | |
| ctx.beginPath(); ctx.arc(lrx, lry, size, 0, Math.PI*2); ctx.fill(); | |
| } | |
| } else { | |
| ctx.lineWidth = ent.r * 2; ctx.strokeStyle = ent.sideColor || ent.color; ctx.lineCap = 'round'; | |
| ctx.beginPath(); ctx.moveTo(0,0); ctx.lineTo(rx, ry); ctx.stroke(); | |
| ctx.fillStyle = ent.color; ctx.beginPath(); ctx.arc(rx, ry, ent.r, 0, Math.PI*2); ctx.fill(); | |
| } | |
| } | |
| ctx.restore(); | |
| } | |
| function render() { | |
| if (!socket) { requestAnimationFrame(render); return; } | |
| ctx.fillStyle = '#111'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| const me = players.find(p => p.id === myId); | |
| if (me) { | |
| camera.x += (me.x - camera.x) * 0.1; | |
| camera.y += (me.y - camera.y) * 0.1; | |
| const targetZoom = Math.max(0.3, Math.min(1.0, 400 / (me.r * 2))); | |
| camera.zoom += (targetZoom - camera.zoom) * 0.05; | |
| } | |
| ctx.save(); | |
| ctx.translate(canvas.width/2, canvas.height/2); | |
| ctx.scale(camera.zoom, camera.zoom); | |
| ctx.translate(-camera.x, -camera.y); | |
| drawCityFloor(); | |
| // Draw Players | |
| players.forEach(p => { | |
| if(!p.alive) return; | |
| ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI*2); | |
| ctx.fillStyle = 'black'; ctx.fill(); | |
| ctx.strokeStyle = p.color; ctx.lineWidth = 4; ctx.stroke(); | |
| ctx.fillStyle = 'white'; ctx.font = `bold ${Math.max(14, p.r/2)}px Arial`; | |
| ctx.textAlign = 'center'; ctx.fillText(p.username, p.x, p.y - p.r - 10); | |
| }); | |
| // Clip holes | |
| ctx.save(); | |
| ctx.beginPath(); | |
| ctx.rect(camera.x - (canvas.width/camera.zoom), camera.y - (canvas.height/camera.zoom), canvas.width*2/camera.zoom, canvas.height*2/camera.zoom); | |
| 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); } }); | |
| ctx.clip(); | |
| // Draw Ents | |
| const viewW = (canvas.width / camera.zoom) / 2 + 200; | |
| const viewH = (canvas.height / camera.zoom) / 2 + 200; | |
| const visibleStatic = staticEntities.filter(e => Math.abs(e.x - camera.x) < viewW && Math.abs(e.y - camera.y) < viewH); | |
| const allEnts = [...visibleStatic, ...cars]; | |
| allEnts.sort((a,b) => (a.y + (a.l||0)/2) - (b.y + (b.l||0)/2)); | |
| allEnts.forEach(ent => draw3DEntity(ent, false)); | |
| ctx.restore(); ctx.restore(); | |
| // Joystick | |
| if (input.active) { | |
| ctx.beginPath(); ctx.arc(input.startX, input.startY, 50, 0, Math.PI*2); | |
| ctx.strokeStyle = 'rgba(255,255,255,0.2)'; ctx.lineWidth = 2; ctx.stroke(); | |
| ctx.beginPath(); ctx.arc(input.currX, input.currY, 20, 0, Math.PI*2); | |
| ctx.fillStyle = 'rgba(255,255,255,0.4)'; ctx.fill(); | |
| } | |
| requestAnimationFrame(render); | |
| } | |
| function updateUI() { | |
| const sb = document.getElementById('scoreBoard'); | |
| const sorted = [...players].sort((a,b) => b.score - a.score).slice(0, 5); | |
| sb.innerHTML = sorted.map(p => ` | |
| <div class="score-row"> | |
| ${p.avatar ? `<img src="${p.avatar}" class="lb-avatar">` : ''} | |
| ${p.username}: ${p.score} | |
| </div> | |
| `).join(''); | |
| } | |
| render(); | |
| </script> | |
| </body> | |
| </html> |