| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>2D Multiplayer Race Game</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| <style> |
| |
| .game-container { |
| position: relative; |
| width: 100%; |
| height: 400px; |
| background-color: #2c3e50; |
| overflow: hidden; |
| } |
| |
| .track { |
| position: absolute; |
| width: 100%; |
| height: 100%; |
| background-color: #34495e; |
| } |
| |
| .lane { |
| position: absolute; |
| width: 100%; |
| height: 80px; |
| border-top: 2px dashed rgba(255, 255, 255, 0.2); |
| border-bottom: 2px dashed rgba(255, 255, 255, 0.2); |
| } |
| |
| .player-car { |
| position: absolute; |
| width: 60px; |
| height: 30px; |
| bottom: 20px; |
| left: 50px; |
| z-index: 10; |
| transition: left 0.1s; |
| } |
| |
| .obstacle { |
| position: absolute; |
| width: 50px; |
| height: 50px; |
| z-index: 5; |
| } |
| |
| .finish-line { |
| position: absolute; |
| width: 10px; |
| height: 100%; |
| right: 50px; |
| background: repeating-linear-gradient( |
| to bottom, |
| white, |
| white 20px, |
| black 20px, |
| black 40px |
| ); |
| z-index: 1; |
| } |
| |
| .car-icon { |
| font-size: 30px; |
| } |
| |
| .chat-message { |
| animation: fadeIn 0.3s; |
| } |
| |
| @keyframes fadeIn { |
| from { opacity: 0; transform: translateY(5px); } |
| to { opacity: 1; transform: translateY(0); } |
| } |
| |
| @keyframes crash { |
| 0% { transform: rotate(0deg); } |
| 25% { transform: rotate(10deg); } |
| 50% { transform: rotate(-10deg); } |
| 75% { transform: rotate(10deg); } |
| 100% { transform: rotate(0deg); } |
| } |
| |
| .crash-animation { |
| animation: crash 0.5s ease-in-out; |
| } |
| </style> |
| </head> |
| <body class="bg-gray-900 text-white"> |
| <div class="container mx-auto px-4 py-8 max-w-6xl"> |
| <h1 class="text-4xl font-bold text-center mb-6 text-yellow-400"> |
| <i class="fas fa-flag-checkered mr-2"></i>Multiplayer Race |
| </h1> |
| |
| <div class="flex flex-col lg:flex-row gap-6"> |
| |
| <div class="flex-1"> |
| <div class="bg-gray-800 rounded-lg shadow-lg overflow-hidden"> |
| |
| <div class="bg-gray-700 px-4 py-3 flex justify-between items-center"> |
| <h2 class="text-xl font-bold"> |
| <i class="fas fa-trophy mr-2 text-yellow-400"></i>Race Scoreboard |
| </h2> |
| <div class="flex items-center space-x-4"> |
| <div class="flex items-center"> |
| <i class="fas fa-users mr-1 text-blue-400"></i> |
| <span id="player-count">0</span> |
| </div> |
| <div class="flex items-center"> |
| <i class="fas fa-clock mr-1 text-green-400"></i> |
| <span id="game-time">0:00</span> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="px-4 py-3 bg-gray-800 max-h-40 overflow-y-auto" id="scoreboard"> |
| <div class="flex justify-between py-1 px-2 rounded bg-gray-700 mb-2"> |
| <span class="font-bold">Player</span> |
| <span class="font-bold">Position</span> |
| <span class="font-bold">Speed</span> |
| </div> |
| <div class="text-center py-3 text-gray-500" id="empty-scoreboard"> |
| Waiting for players to join... |
| </div> |
| </div> |
| |
| |
| <div class="game-container relative" id="game-container"> |
| <div class="track"> |
| <div class="lane" style="top: 60px;"></div> |
| <div class="lane" style="top: 160px;"></div> |
| <div class="lane" style="top: 260px;"></div> |
| <div class="finish-line"></div> |
| |
| |
| </div> |
| </div> |
| |
| |
| <div class="bg-gray-700 px-4 py-3 flex items-center justify-between"> |
| <div class="flex items-center space-x-2"> |
| <button id="start-game" class="bg-green-600 hover:bg-green-700 px-4 py-2 rounded-lg font-medium flex items-center"> |
| <i class="fas fa-play mr-2"></i> Start Game |
| </button> |
| <button id="reset-game" class="bg-yellow-600 hover:bg-yellow-700 px-4 py-2 rounded-lg font-medium flex items-center"> |
| <i class="fas fa-sync-alt mr-2"></i> Reset |
| </button> |
| </div> |
| |
| <div class="flex items-center space-x-4"> |
| <span id="player-speed">Speed: 0</span> |
| <span id="player-position">Position: -</span> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="w-full lg:w-80 flex flex-col"> |
| <div class="bg-gray-800 rounded-lg shadow-lg overflow-hidden flex-1 flex flex-col"> |
| <div class="bg-gray-700 px-4 py-3"> |
| <h2 class="text-xl font-bold flex items-center"> |
| <i class="fas fa-comments mr-2 text-blue-400"></i>Race Chat |
| </h2> |
| </div> |
| |
| <div class="flex-1 p-4 overflow-y-auto" id="chat-messages"> |
| <div class="text-gray-400 text-center py-8">Send a message to start chatting!</div> |
| </div> |
| |
| <div class="p-4 bg-gray-700"> |
| <div class="flex space-x-2"> |
| <input type="text" id="chat-input" placeholder="Type your message..." |
| class="flex-1 bg-gray-600 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"> |
| <button id="send-message" class="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-lg font-medium"> |
| <i class="fas fa-paper-plane"></i> |
| </button> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="bg-gray-800 rounded-lg shadow-lg overflow-hidden mt-4"> |
| <div class="bg-gray-700 px-4 py-3"> |
| <h2 class="text-xl font-bold flex items-center"> |
| <i class="fas fa-users mr-2 text-purple-400"></i>Online Racers |
| </h2> |
| </div> |
| |
| <div class="p-4" id="player-list"> |
| <div class="text-gray-400 text-center py-4">No other players connected</div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| |
| const gameState = { |
| gameRunning: false, |
| gameTime: 0, |
| playerId: 'player-' + Math.random().toString(36).substr(2, 9), |
| playerName: 'Racer ' + Math.floor(Math.random() * 1000), |
| players: {}, |
| obstacles: [], |
| chatMessages: [], |
| keysPressed: {} |
| }; |
| |
| |
| const gameContainer = document.getElementById('game-container'); |
| const scoreboard = document.getElementById('scoreboard'); |
| const emptyScoreboard = document.getElementById('empty-scoreboard'); |
| const playerCount = document.getElementById('player-count'); |
| const gameTime = document.getElementById('game-time'); |
| const playerSpeed = document.getElementById('player-speed'); |
| const playerPosition = document.getElementById('player-position'); |
| const chatMessages = document.getElementById('chat-messages'); |
| const chatInput = document.getElementById('chat-input'); |
| const sendMessage = document.getElementById('send-message'); |
| const playerList = document.getElementById('player-list'); |
| const startGame = document.getElementById('start-game'); |
| const resetGame = document.getElementById('reset-game'); |
| |
| |
| const carColors = [ |
| 'text-red-500', 'text-blue-500', 'text-green-500', |
| 'text-yellow-500', 'text-purple-500', 'text-pink-500' |
| ]; |
| |
| |
| function initGame() { |
| |
| createPlayer(); |
| |
| |
| window.addEventListener('keydown', handleKeyDown); |
| window.addEventListener('keyup', handleKeyUp); |
| |
| |
| startGame.addEventListener('click', startGameHandler); |
| resetGame.addEventListener('click', resetGameHandler); |
| sendMessage.addEventListener('click', sendChatMessage); |
| chatInput.addEventListener('keypress', (e) => { |
| if (e.key === 'Enter') sendChatMessage(); |
| }); |
| |
| |
| setTimeout(() => { |
| addDemoPlayer('Racer-1', 160); |
| addDemoPlayer('Racer-2', 260); |
| }, 500); |
| |
| |
| gameLoop(); |
| } |
| |
| |
| function createPlayer() { |
| const playerCar = document.createElement('div'); |
| playerCar.id = gameState.playerId; |
| playerCar.className = `player-car ${carColors[0]} car-icon`; |
| playerCar.innerHTML = '<i class="fas fa-car"></i>'; |
| gameContainer.querySelector('.track').appendChild(playerCar); |
| |
| gameState.players[gameState.playerId] = { |
| id: gameState.playerId, |
| name: gameState.playerName, |
| x: 50, |
| y: 60, |
| speed: 0, |
| maxSpeed: 10, |
| acceleration: 0.2, |
| deceleration: 0.1, |
| position: 0, |
| finished: false, |
| finishTime: 0, |
| element: playerCar |
| }; |
| |
| updatePlayerList(); |
| updateScoreboard(); |
| } |
| |
| |
| function addDemoPlayer(name, yPos) { |
| const playerId = 'demo-' + Math.random().toString(36).substr(2, 6); |
| const carColor = carColors[Object.keys(gameState.players).length % carColors.length]; |
| |
| const playerCar = document.createElement('div'); |
| playerCar.className = `player-car ${carColor} car-icon`; |
| playerCar.innerHTML = '<i class="fas fa-car"></i>'; |
| playerCar.style.top = `${yPos}px`; |
| playerCar.style.left = '50px'; |
| gameContainer.querySelector('.track').appendChild(playerCar); |
| |
| gameState.players[playerId] = { |
| id: playerId, |
| name: name, |
| x: 50, |
| y: yPos, |
| speed: 0, |
| maxSpeed: 8 + Math.random() * 4, |
| acceleration: 0.1 + Math.random() * 0.1, |
| deceleration: 0.05 + Math.random() * 0.05, |
| position: 0, |
| finished: false, |
| finishTime: 0, |
| element: playerCar, |
| isDemo: true |
| }; |
| |
| const demoPlayer = gameState.players[playerId]; |
| |
| |
| setInterval(() => { |
| if (gameState.gameRunning && !demoPlayer.finished) { |
| if (Math.random() > 0.3) { |
| demoPlayer.speed = Math.min(demoPlayer.speed + demoPlayer.acceleration, demoPlayer.maxSpeed); |
| } else if (Math.random() > 0.7) { |
| demoPlayer.speed = Math.max(demoPlayer.speed - demoPlayer.deceleration, 0); |
| } |
| |
| |
| if (Math.random() > 0.8) { |
| demoPlayer.speed = Math.max(0, demoPlayer.speed + (Math.random() - 0.5) * 2); |
| } |
| } |
| }, 200); |
| |
| updatePlayerList(); |
| updateScoreboard(); |
| } |
| |
| |
| function handleKeyDown(e) { |
| gameState.keysPressed[e.key] = true; |
| } |
| |
| function handleKeyUp(e) { |
| gameState.keysPressed[e.key] = false; |
| } |
| |
| |
| function startGameHandler() { |
| gameState.gameRunning = true; |
| gameState.gameTime = 0; |
| startGame.disabled = true; |
| startGame.classList.remove('bg-green-600', 'hover:bg-green-700'); |
| startGame.classList.add('bg-gray-600', 'cursor-not-allowed'); |
| |
| |
| for (const playerId in gameState.players) { |
| const player = gameState.players[playerId]; |
| player.x = 50; |
| player.speed = 0; |
| player.finished = false; |
| player.finishTime = 0; |
| player.position = 0; |
| } |
| |
| addChatMessage('System', 'The race has started! Good luck everyone!', 'text-yellow-400'); |
| } |
| |
| |
| function resetGameHandler() { |
| gameState.gameRunning = false; |
| gameState.gameTime = 0; |
| startGame.disabled = false; |
| startGame.classList.add('bg-green-600', 'hover:bg-green-700'); |
| startGame.classList.remove('bg-gray-600', 'cursor-not-allowed'); |
| |
| |
| for (const playerId in gameState.players) { |
| const player = gameState.players[playerId]; |
| player.x = 50; |
| player.speed = 0; |
| player.finished = false; |
| player.finishTime = 0; |
| player.position = 0; |
| player.element.style.left = `${player.x}px`; |
| } |
| |
| addChatMessage('System', 'The race has been reset.', 'text-yellow-400'); |
| updateScoreboard(); |
| } |
| |
| |
| function gameLoop() { |
| if (gameState.gameRunning) { |
| gameState.gameTime += 0.1; |
| updateGameTimeDisplay(); |
| |
| |
| for (const playerId in gameState.players) { |
| const player = gameState.players[playerId]; |
| |
| |
| if (playerId === gameState.playerId) { |
| if (gameState.keysPressed['ArrowRight'] || gameState.keysPressed['d']) { |
| player.speed = Math.min(player.speed + player.acceleration, player.maxSpeed); |
| } |
| |
| if (gameState.keysPressed['ArrowLeft'] || gameState.keysPressed['a']) { |
| player.speed = Math.max(player.speed - player.deceleration, 0); |
| } |
| |
| |
| if (!gameState.keysPressed['ArrowRight'] && !gameState.keysPressed['ArrowLeft'] && |
| !gameState.keysPressed['d'] && !gameState.keysPressed['a']) { |
| player.speed = Math.max(player.speed - player.deceleration, 0); |
| } |
| } |
| |
| |
| if (!player.finished) { |
| player.x += player.speed; |
| |
| |
| if (player.x >= gameContainer.clientWidth - 100) { |
| player.finished = true; |
| player.finishTime = gameState.gameTime; |
| player.position = Object.values(gameState.players).filter(p => p.finished).length; |
| |
| if (playerId === gameState.playerId) { |
| addChatMessage('System', `You finished in ${ordinalSuffix(player.position)} place!`, 'text-green-400'); |
| } else { |
| addChatMessage('System', `${player.name} finished in ${ordinalSuffix(player.position)} place!`, 'text-blue-400'); |
| } |
| } |
| } |
| |
| |
| player.element.style.left = `${player.x}px`; |
| |
| |
| if (playerId === gameState.playerId) { |
| playerSpeed.textContent = `Speed: ${Math.round(player.speed * 10)}`; |
| if (player.finished) { |
| playerPosition.textContent = `Position: ${ordinalSuffix(player.position)}`; |
| } else { |
| playerPosition.textContent = `Position: Racing`; |
| } |
| } |
| } |
| |
| |
| calculatePositions(); |
| } |
| |
| |
| updateScoreboard(); |
| |
| requestAnimationFrame(gameLoop); |
| } |
| |
| |
| function calculatePositions() { |
| const players = Object.values(gameState.players); |
| |
| |
| const racingPlayers = players |
| .filter(p => !p.finished) |
| .sort((a, b) => b.x - a.x); |
| |
| |
| let currentPosition = Object.values(gameState.players).filter(p => p.finished).length + 1; |
| |
| for (const player of racingPlayers) { |
| player.position = currentPosition++; |
| } |
| } |
| |
| |
| function updateGameTimeDisplay() { |
| const minutes = Math.floor(gameState.gameTime / 60); |
| const seconds = Math.floor(gameState.gameTime % 60); |
| const paddedSeconds = seconds.toString().padStart(2, '0'); |
| gameTime.textContent = `${minutes}:${paddedSeconds}`; |
| } |
| |
| |
| function updateScoreboard() { |
| const players = Object.values(gameState.players); |
| |
| if (players.length === 0) { |
| emptyScoreboard.style.display = 'block'; |
| return; |
| } |
| |
| emptyScoreboard.style.display = 'none'; |
| |
| |
| const sortedPlayers = players.sort((a, b) => { |
| if (a.finished && b.finished) return a.finishTime - b.finishTime; |
| if (a.finished) return -1; |
| if (b.finished) return 1; |
| return b.x - a.x; |
| }); |
| |
| |
| const header = scoreboard.firstElementChild; |
| while (scoreboard.children.length > 1) { |
| scoreboard.removeChild(scoreboard.lastChild); |
| } |
| |
| |
| sortedPlayers.forEach((player, index) => { |
| const playerRow = document.createElement('div'); |
| playerRow.className = `flex justify-between py-2 px-2 rounded ${player.id === gameState.playerId ? 'bg-gray-700' : 'bg-gray-800'} mb-1 items-center`; |
| |
| const playerName = document.createElement('div'); |
| playerName.className = 'flex items-center'; |
| playerName.innerHTML = `<span class="w-5 text-gray-400">${index + 1}.</span> ${player.name} ${player.id === gameState.playerId ? '<span class="text-xs bg-blue-600 px-1.5 py-0.5 rounded ml-2">You</span>' : ''}`; |
| |
| const playerPosition = document.createElement('div'); |
| if (player.finished) { |
| playerPosition.textContent = `${ordinalSuffix(player.position)} (${formatTime(player.finishTime)})`; |
| } else { |
| playerPosition.textContent = 'Racing'; |
| } |
| |
| const playerSpeed = document.createElement('div'); |
| playerSpeed.textContent = `${Math.round(player.speed * 10)}`; |
| |
| playerRow.appendChild(playerName); |
| playerRow.appendChild(playerPosition); |
| playerRow.appendChild(playerSpeed); |
| |
| scoreboard.appendChild(playerRow); |
| }); |
| |
| |
| playerCount.textContent = players.length; |
| } |
| |
| |
| function sendChatMessage() { |
| const message = chatInput.value.trim(); |
| if (message) { |
| addChatMessage(gameState.playerName, message); |
| chatInput.value = ''; |
| |
| |
| if (gameState.gameRunning) { |
| setTimeout(() => { |
| const demoPlayers = Object.values(gameState.players).filter(p => p.isDemo); |
| if (demoPlayers.length > 0 && Math.random() > 0.5) { |
| const demoPlayer = demoPlayers[Math.floor(Math.random() * demoPlayers.length)]; |
| const responses = [ |
| "Good luck everyone!", |
| "I'm in the lead!", |
| "Watch out for that turn!", |
| "Almost at the finish line!", |
| "Nice move!", |
| "This is intense!", |
| "I'm catching up!", |
| "Too fast for me!" |
| ]; |
| addChatMessage(demoPlayer.name, responses[Math.floor(Math.random() * responses.length)]); |
| } |
| }, 1000 + Math.random() * 3000); |
| } |
| } |
| } |
| |
| function addChatMessage(name, message, specialClass = '') { |
| const messageDiv = document.createElement('div'); |
| messageDiv.className = `chat-message mb-2 ${specialClass}`; |
| |
| const nameSpan = document.createElement('span'); |
| nameSpan.className = 'font-bold mr-2'; |
| nameSpan.textContent = name + ':'; |
| if (name === 'System') { |
| nameSpan.className += ' text-yellow-400'; |
| } |
| |
| const messageSpan = document.createElement('span'); |
| messageSpan.className = 'text-gray-300'; |
| messageSpan.textContent = message; |
| |
| messageDiv.appendChild(nameSpan); |
| messageDiv.appendChild(messageSpan); |
| |
| chatMessages.appendChild(messageDiv); |
| |
| |
| if (chatMessages.firstChild && chatMessages.firstChild.classList.contains('text-center')) { |
| chatMessages.removeChild(chatMessages.firstChild); |
| } |
| |
| |
| chatMessages.scrollTop = chatMessages.scrollHeight; |
| } |
| |
| |
| function updatePlayerList() { |
| playerList.innerHTML = ''; |
| |
| const players = Object.values(gameState.players); |
| if (players.length === 0) { |
| playerList.innerHTML = '<div class="text-gray-400 text-center py-4">No other players connected</div>'; |
| return; |
| } |
| |
| players.forEach((player, index) => { |
| const playerItem = document.createElement('div'); |
| playerItem.className = `flex items-center py-2 px-3 rounded mb-1 ${player.id === gameState.playerId ? 'bg-gray-700' : 'bg-gray-800'}`; |
| |
| const carIcon = document.createElement('div'); |
| carIcon.className = `car-icon mr-3 ${player.id === gameState.playerId ? 'text-red-500' : carColors[(index % carColors.length) + 1]}`; |
| carIcon.innerHTML = '<i class="fas fa-car"></i>'; |
| |
| const playerName = document.createElement('div'); |
| playerName.className = 'flex-1'; |
| playerName.textContent = player.name; |
| |
| const playerStatus = document.createElement('div'); |
| playerStatus.className = 'text-xs px-2 py-1 rounded-full'; |
| |
| if (player.finished) { |
| playerStatus.textContent = ordinalSuffix(player.position); |
| playerStatus.className += ' bg-green-800 text-green-300'; |
| } else { |
| playerStatus.textContent = 'Racing'; |
| playerStatus.className += ' bg-blue-800 text-blue-300'; |
| } |
| |
| playerItem.appendChild(carIcon); |
| playerItem.appendChild(playerName); |
| playerItem.appendChild(playerStatus); |
| |
| playerList.appendChild(playerItem); |
| }); |
| } |
| |
| |
| function ordinalSuffix(i) { |
| const j = i % 10, k = i % 100; |
| if (j === 1 && k !== 11) return i + "st"; |
| if (j === 2 && k !== 12) return i + "nd"; |
| if (j === 3 && k !== 13) return i + "rd"; |
| return i + "th"; |
| } |
| |
| function formatTime(seconds) { |
| const minutes = Math.floor(seconds / 60); |
| const secs = Math.floor(seconds % 60); |
| const millis = Math.floor((seconds % 1) * 10); |
| return `${minutes}:${secs.toString().padStart(2, '0')}.${millis}`; |
| } |
| |
| |
| document.addEventListener('DOMContentLoaded', initGame); |
| </script> |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=LULDev/race" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| </html> |