|
<!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> |