yt / index.html
OrbitMC's picture
Upload 4 files
10f2308 verified
<!DOCTYPE html>
<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>