whacky-wheels-2d / index.html
LukasBe's picture
Add 2 files
21f18a3 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Whacky Wheels - Emoji Edition</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body {
overflow: hidden;
touch-action: none;
background-color: #1a202c;
}
canvas {
display: block;
margin: 0 auto;
background-color: #2d3748;
}
.emoji-button {
font-size: 2rem;
transition: all 0.2s;
}
.emoji-button:hover {
transform: scale(1.2);
}
.emoji-button:active {
transform: scale(0.9);
}
.track-tile {
width: 40px;
height: 40px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
border: 1px solid rgba(255,255,255,0.1);
}
.track-tile-road { background-color: #4a5568; }
.track-tile-grass { background-color: #48bb78; }
.track-tile-oil { background-color: #805ad5; }
.track-tile-power { background-color: #f6e05e; }
.track-tile-start { background-color: #f56565; }
.track-tile-finish { background-color: #4299e1; }
</style>
</head>
<body class="font-sans text-white">
<div id="game-container" class="relative w-full h-screen">
<!-- Splash Screen -->
<div id="splash-screen" class="absolute inset-0 flex flex-col items-center justify-center bg-gray-900 bg-opacity-90 z-10">
<h1 class="text-6xl font-bold mb-8">๐ŸŽฎ Whacky Wheels</h1>
<p class="text-2xl mb-12">Emoji Edition</p>
<div class="flex space-x-8 mb-12">
<button id="player-frog" class="emoji-button bg-gray-800 p-4 rounded-full hover:bg-green-600 transition">๐Ÿธ</button>
<button id="player-turtle" class="emoji-button bg-gray-800 p-4 rounded-full hover:bg-blue-600 transition">๐Ÿข</button>
<button id="player-cat" class="emoji-button bg-gray-800 p-4 rounded-full hover:bg-yellow-600 transition">๐Ÿฑ</button>
<button id="player-dog" class="emoji-button bg-gray-800 p-4 rounded-full hover:bg-red-600 transition">๐Ÿถ</button>
</div>
<button id="start-game" class="px-8 py-4 bg-green-600 text-2xl font-bold rounded-lg hover:bg-green-700 transition transform hover:scale-105">
๐Ÿ START RACE ๐Ÿ
</button>
<div class="mt-12 text-gray-400">
<p>Controls: โ†‘โ†“โ†โ†’ to move, SPACE to fire, Z to drop mines</p>
<p>Press R to restart race</p>
</div>
</div>
<!-- Countdown Screen -->
<div id="countdown-screen" class="absolute inset-0 flex items-center justify-center bg-black bg-opacity-70 z-20 hidden">
<div id="countdown-text" class="text-9xl font-bold">3</div>
</div>
<!-- Game Canvas -->
<canvas id="game-canvas" class="absolute inset-0 w-full h-full"></canvas>
<!-- HUD -->
<div id="game-hud" class="absolute top-0 left-0 right-0 p-4 flex justify-between items-start z-10 hidden">
<div class="bg-gray-900 bg-opacity-70 p-3 rounded-lg">
<div class="text-xl">Lap: <span id="lap-counter">1</span>/3</div>
<div class="text-xl">Speed: <span id="speed-counter">0</span></div>
</div>
<div class="bg-gray-900 bg-opacity-70 p-3 rounded-lg flex items-center">
<div id="weapon-display" class="text-3xl mr-3">๐Ÿ‰</div>
<div id="weapon-count" class="text-xl">x3</div>
</div>
<div class="bg-gray-900 bg-opacity-70 p-3 rounded-lg">
<div class="text-xl">Position: <span id="position-counter">1st</span></div>
<div class="text-xl">Time: <span id="time-counter">0:00</span></div>
</div>
</div>
<!-- Mobile Controls -->
<div id="mobile-controls" class="absolute bottom-0 left-0 right-0 p-4 grid grid-cols-3 gap-4 hidden">
<div></div>
<button id="mobile-up" class="bg-gray-800 bg-opacity-70 p-4 rounded-full text-3xl">โ†‘</button>
<div></div>
<button id="mobile-left" class="bg-gray-800 bg-opacity-70 p-4 rounded-full text-3xl">โ†</button>
<button id="mobile-fire" class="bg-red-800 bg-opacity-70 p-4 rounded-full text-3xl">๐Ÿ’ฅ</button>
<button id="mobile-right" class="bg-gray-800 bg-opacity-70 p-4 rounded-full text-3xl">โ†’</button>
<button id="mobile-down" class="bg-gray-800 bg-opacity-70 p-4 rounded-full text-3xl">โ†“</button>
<button id="mobile-mine" class="bg-gray-800 bg-opacity-70 p-4 rounded-full text-3xl">๐Ÿฆ”</button>
<div></div>
</div>
<!-- Results Screen -->
<div id="results-screen" class="absolute inset-0 flex flex-col items-center justify-center bg-gray-900 bg-opacity-90 z-30 hidden">
<h2 class="text-5xl font-bold mb-8">Race Complete!</h2>
<div id="result-position" class="text-8xl mb-8">1st ๐Ÿ†</div>
<div class="text-2xl mb-4">Time: <span id="result-time">1:23.45</span></div>
<div class="text-2xl mb-12">Best Lap: <span id="result-best-lap">0:27.89</span></div>
<div class="flex space-x-4">
<button id="play-again" class="px-6 py-3 bg-blue-600 text-xl font-bold rounded-lg hover:bg-blue-700 transition">
Race Again
</button>
<button id="back-to-menu" class="px-6 py-3 bg-gray-600 text-xl font-bold rounded-lg hover:bg-gray-700 transition">
Main Menu
</button>
</div>
</div>
</div>
<script>
// Game Constants
const TILE_SIZE = 40;
const KART_SIZE = 30;
const PROJECTILE_SIZE = 20;
const MINE_SIZE = 25;
const EXPLOSION_SIZE = 50;
const MAX_LAPS = 3;
// Track Types
const TRACK_TYPES = {
'G': { name: 'grass', friction: 0.2, color: '#48bb78' },
'R': { name: 'road', friction: 0.05, color: '#4a5568' },
'O': { name: 'oil', friction: 0.3, color: '#805ad5' },
'P': { name: 'power', friction: 0.05, color: '#f6e05e' },
'S': { name: 'start', friction: 0.05, color: '#f56565' },
'F': { name: 'finish', friction: 0.05, color: '#4299e1' }
};
// Game State
let gameState = {
screen: 'splash',
playerEmoji: '๐Ÿธ',
playerName: 'Frog Racer',
track: [],
karts: [],
projectiles: [],
mines: [],
explosions: [],
lap: 1,
position: 1,
raceTime: 0,
lapTimes: [],
bestLapTime: Infinity,
gameTime: 0,
lastTime: 0,
keys: {},
isMobile: false
};
// DOM Elements
const elements = {
splashScreen: document.getElementById('splash-screen'),
countdownScreen: document.getElementById('countdown-screen'),
countdownText: document.getElementById('countdown-text'),
gameCanvas: document.getElementById('game-canvas'),
gameHud: document.getElementById('game-hud'),
mobileControls: document.getElementById('mobile-controls'),
resultsScreen: document.getElementById('results-screen'),
lapCounter: document.getElementById('lap-counter'),
speedCounter: document.getElementById('speed-counter'),
positionCounter: document.getElementById('position-counter'),
timeCounter: document.getElementById('time-counter'),
weaponDisplay: document.getElementById('weapon-display'),
weaponCount: document.getElementById('weapon-count'),
resultPosition: document.getElementById('result-position'),
resultTime: document.getElementById('result-time'),
resultBestLap: document.getElementById('result-best-lap')
};
// Canvas Setup
const canvas = elements.gameCanvas;
const ctx = canvas.getContext('2d');
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
// Event Listeners
document.getElementById('start-game').addEventListener('click', startGame);
document.getElementById('play-again').addEventListener('click', startGame);
document.getElementById('back-to-menu').addEventListener('click', backToMenu);
// Player selection
document.querySelectorAll('[id^="player-"]').forEach(button => {
button.addEventListener('click', function() {
gameState.playerEmoji = this.textContent;
gameState.playerName = this.id.split('-')[1].charAt(0).toUpperCase() + this.id.split('-')[1].slice(1) + ' Racer';
// Update active button
document.querySelectorAll('[id^="player-"]').forEach(btn => {
btn.classList.remove('ring-4', 'ring-white');
});
this.classList.add('ring-4', 'ring-white');
});
});
// Keyboard Controls
window.addEventListener('keydown', (e) => {
gameState.keys[e.key] = true;
// Restart race
if (e.key === 'r' && gameState.screen === 'racing') {
startGame();
}
});
window.addEventListener('keyup', (e) => {
gameState.keys[e.key] = false;
});
// Mobile Controls
function setupMobileControls() {
gameState.isMobile = true;
elements.mobileControls.classList.remove('hidden');
// Control buttons
const mobileControls = {
up: false,
down: false,
left: false,
right: false,
fire: false,
mine: false
};
// Press events
document.getElementById('mobile-up').addEventListener('touchstart', () => mobileControls.up = true);
document.getElementById('mobile-up').addEventListener('touchend', () => mobileControls.up = false);
document.getElementById('mobile-down').addEventListener('touchstart', () => mobileControls.down = true);
document.getElementById('mobile-down').addEventListener('touchend', () => mobileControls.down = false);
document.getElementById('mobile-left').addEventListener('touchstart', () => mobileControls.left = true);
document.getElementById('mobile-left').addEventListener('touchend', () => mobileControls.left = false);
document.getElementById('mobile-right').addEventListener('touchstart', () => mobileControls.right = true);
document.getElementById('mobile-right').addEventListener('touchend', () => mobileControls.right = false);
document.getElementById('mobile-fire').addEventListener('touchstart', () => mobileControls.fire = true);
document.getElementById('mobile-fire').addEventListener('touchend', () => mobileControls.fire = false);
document.getElementById('mobile-mine').addEventListener('touchstart', () => mobileControls.mine = true);
document.getElementById('mobile-mine').addEventListener('touchend', () => mobileControls.mine = false);
// Map mobile controls to keyboard state
setInterval(() => {
gameState.keys['ArrowUp'] = mobileControls.up;
gameState.keys['ArrowDown'] = mobileControls.down;
gameState.keys['ArrowLeft'] = mobileControls.left;
gameState.keys['ArrowRight'] = mobileControls.right;
gameState.keys[' '] = mobileControls.fire;
gameState.keys['z'] = mobileControls.mine;
}, 16);
}
// Check if mobile
if ('ontouchstart' in window || navigator.maxTouchPoints) {
setupMobileControls();
}
// Game Functions
function generateTrack() {
// Simple procedural track generation
const width = 20;
const height = 15;
const track = Array(height).fill().map(() => Array(width).fill('G'));
// Create a looping road
const roadPath = [
{x: 3, y: 3}, {x: 16, y: 3}, {x: 16, y: 11}, {x: 3, y: 11}, {x: 3, y: 3}
];
// Draw the road
for (let i = 0; i < roadPath.length - 1; i++) {
const from = roadPath[i];
const to = roadPath[i+1];
// Horizontal road
if (from.y === to.y) {
const start = Math.min(from.x, to.x);
const end = Math.max(from.x, to.x);
for (let x = start; x <= end; x++) {
track[from.y][x] = 'R';
}
}
// Vertical road
else if (from.x === to.x) {
const start = Math.min(from.y, to.y);
const end = Math.max(from.y, to.y);
for (let y = start; y <= end; y++) {
track[y][from.x] = 'R';
}
}
}
// Add start and finish
track[3][3] = 'S';
track[3][4] = 'F';
// Add some obstacles and power-ups
track[5][5] = 'O';
track[7][10] = 'P';
track[9][14] = 'O';
track[11][7] = 'P';
return track;
}
function createKart(emoji, x, y, isPlayer = false) {
return {
emoji,
x,
y,
angle: 0,
speed: 0,
maxSpeed: isPlayer ? 5 : 4,
acceleration: isPlayer ? 0.1 : 0.08,
turnSpeed: 0.05,
weapon: '๐Ÿ‰',
weaponCount: 3,
isPlayer,
lap: 1,
checkPoint: 0,
waypoints: [
{x: 4, y: 3}, {x: 15, y: 3},
{x: 15, y: 10}, {x: 4, y: 10},
{x: 4, y: 3}
],
nextWaypoint: 1
};
}
function startGame() {
// Reset game state
gameState.track = generateTrack();
gameState.karts = [];
gameState.projectiles = [];
gameState.mines = [];
gameState.explosions = [];
gameState.lap = 1;
gameState.position = 1;
gameState.raceTime = 0;
gameState.lapTimes = [];
gameState.bestLapTime = Infinity;
gameState.gameTime = 0;
gameState.lastTime = 0;
// Create player kart
const startPos = findStartPosition();
gameState.karts.push(createKart(gameState.playerEmoji, startPos.x, startPos.y, true));
// Create AI karts
gameState.karts.push(createKart('๐Ÿข', startPos.x - 40, startPos.y - 40));
gameState.karts.push(createKart('๐Ÿฑ', startPos.x + 40, startPos.y));
gameState.karts.push(createKart('๐Ÿถ', startPos.x, startPos.y + 40));
// Show countdown
gameState.screen = 'countdown';
elements.splashScreen.classList.add('hidden');
elements.countdownScreen.classList.remove('hidden');
elements.gameHud.classList.add('hidden');
elements.resultsScreen.classList.add('hidden');
let count = 3;
elements.countdownText.textContent = count;
const countdownInterval = setInterval(() => {
count--;
if (count > 0) {
elements.countdownText.textContent = count;
} else if (count === 0) {
elements.countdownText.textContent = '๐Ÿ';
} else {
clearInterval(countdownInterval);
startRace();
}
}, 1000);
}
function startRace() {
gameState.screen = 'racing';
elements.countdownScreen.classList.add('hidden');
elements.gameHud.classList.remove('hidden');
// Start game loop
gameState.lastTime = performance.now();
requestAnimationFrame(gameLoop);
}
function findStartPosition() {
for (let y = 0; y < gameState.track.length; y++) {
for (let x = 0; x < gameState.track[y].length; x++) {
if (gameState.track[y][x] === 'S') {
return {
x: x * TILE_SIZE + TILE_SIZE/2,
y: y * TILE_SIZE + TILE_SIZE/2
};
}
}
}
return {x: 100, y: 100}; // Fallback
}
function backToMenu() {
gameState.screen = 'splash';
elements.splashScreen.classList.remove('hidden');
elements.resultsScreen.classList.add('hidden');
}
function gameLoop(timestamp) {
if (gameState.screen !== 'racing') return;
// Calculate delta time
const deltaTime = (timestamp - gameState.lastTime) / 1000;
gameState.lastTime = timestamp;
gameState.gameTime += deltaTime;
gameState.raceTime += deltaTime;
// Update game state
updateKarts(deltaTime);
updateProjectiles(deltaTime);
updateMines(deltaTime);
updateExplosions(deltaTime);
updateAI(deltaTime);
checkCollisions();
checkLapCompletion();
updatePosition();
// Update HUD
updateHUD();
// Draw everything
drawGame();
// Continue loop
requestAnimationFrame(gameLoop);
}
function updateKarts(deltaTime) {
gameState.karts.forEach(kart => {
// Apply friction based on current tile
const tileX = Math.floor(kart.x / TILE_SIZE);
const tileY = Math.floor(kart.y / TILE_SIZE);
let tileType = 'G';
if (tileY >= 0 && tileY < gameState.track.length &&
tileX >= 0 && tileX < gameState.track[tileY].length) {
tileType = gameState.track[tileY][tileX];
}
const friction = TRACK_TYPES[tileType]?.friction || 0.2;
// Player controls
if (kart.isPlayer) {
// Acceleration
if (gameState.keys['ArrowUp'] || gameState.keys['w']) {
kart.speed += kart.acceleration * deltaTime * 60;
}
// Brake/reverse
else if (gameState.keys['ArrowDown'] || gameState.keys['s']) {
kart.speed -= kart.acceleration * deltaTime * 60;
}
// Natural deceleration
else {
if (kart.speed > 0) {
kart.speed = Math.max(0, kart.speed - friction * deltaTime * 20);
} else if (kart.speed < 0) {
kart.speed = Math.min(0, kart.speed + friction * deltaTime * 20);
}
}
// Turning
if (kart.speed !== 0) {
const turnDirection = kart.speed > 0 ? 1 : -1;
if (gameState.keys['ArrowLeft'] || gameState.keys['a']) {
kart.angle -= kart.turnSpeed * turnDirection * deltaTime * 60;
}
if (gameState.keys['ArrowRight'] || gameState.keys['d']) {
kart.angle += kart.turnSpeed * turnDirection * deltaTime * 60;
}
}
// Fire weapon
if (gameState.keys[' '] && kart.weaponCount > 0) {
fireWeapon(kart);
gameState.keys[' '] = false; // Prevent rapid fire
}
// Drop mine
if (gameState.keys['z'] && kart.weaponCount > 0) {
dropMine(kart);
gameState.keys['z'] = false;
}
}
// Clamp speed
kart.speed = Math.max(-kart.maxSpeed * 0.5, Math.min(kart.speed, kart.maxSpeed));
// Apply movement
kart.x += Math.cos(kart.angle) * kart.speed * deltaTime * 60;
kart.y += Math.sin(kart.angle) * kart.speed * deltaTime * 60;
// Track boundaries
const trackWidth = gameState.track[0].length * TILE_SIZE;
const trackHeight = gameState.track.length * TILE_SIZE;
if (kart.x < 0) kart.x = 0;
if (kart.x > trackWidth) kart.x = trackWidth;
if (kart.y < 0) kart.y = 0;
if (kart.y > trackHeight) kart.y = trackHeight;
// Check if kart is on oil
if (tileType === 'O') {
// Random sliding effect
kart.angle += (Math.random() - 0.5) * 0.2;
}
});
}
function fireWeapon(kart) {
if (kart.weaponCount <= 0) return;
const projectile = {
emoji: kart.weapon,
x: kart.x + Math.cos(kart.angle) * KART_SIZE,
y: kart.y + Math.sin(kart.angle) * KART_SIZE,
angle: kart.angle,
speed: 8,
owner: kart.isPlayer ? 'player' : 'ai',
lifetime: 100
};
gameState.projectiles.push(projectile);
kart.weaponCount--;
// Play sound (optional)
if (typeof AudioContext !== 'undefined') {
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
oscillator.type = 'square';
oscillator.frequency.setValueAtTime(800, audioCtx.currentTime);
oscillator.frequency.exponentialRampToValueAtTime(100, audioCtx.currentTime + 0.2);
gainNode.gain.setValueAtTime(0.5, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.2);
oscillator.connect(gainNode);
gainNode.connect(audioCtx.destination);
oscillator.start();
oscillator.stop(audioCtx.currentTime + 0.2);
}
}
function dropMine(kart) {
if (kart.weaponCount <= 0) return;
const mine = {
emoji: '๐Ÿฆ”',
x: kart.x,
y: kart.y,
size: MINE_SIZE,
lifetime: 300,
owner: kart.isPlayer ? 'player' : 'ai'
};
gameState.mines.push(mine);
kart.weaponCount--;
}
function updateProjectiles(deltaTime) {
for (let i = gameState.projectiles.length - 1; i >= 0; i--) {
const proj = gameState.projectiles[i];
// Update position
proj.x += Math.cos(proj.angle) * proj.speed * deltaTime * 60;
proj.y += Math.sin(proj.angle) * proj.speed * deltaTime * 60;
// Decrease lifetime
proj.lifetime--;
// Remove if expired or out of bounds
if (proj.lifetime <= 0 ||
proj.x < 0 || proj.x > canvas.width ||
proj.y < 0 || proj.y > canvas.height) {
gameState.projectiles.splice(i, 1);
}
}
}
function updateMines(deltaTime) {
for (let i = gameState.mines.length - 1; i >= 0; i--) {
const mine = gameState.mines[i];
// Decrease lifetime
mine.lifetime--;
// Remove if expired
if (mine.lifetime <= 0) {
gameState.mines.splice(i, 1);
}
}
}
function updateExplosions(deltaTime) {
for (let i = gameState.explosions.length - 1; i >= 0; i--) {
const explosion = gameState.explosions[i];
// Update size and opacity
explosion.size += 2;
explosion.opacity -= 0.02;
// Remove if faded out
if (explosion.opacity <= 0) {
gameState.explosions.splice(i, 1);
}
}
}
function updateAI(deltaTime) {
gameState.karts.forEach(kart => {
if (kart.isPlayer) return;
// Simple AI that follows waypoints
const target = kart.waypoints[kart.nextWaypoint];
const targetX = target.x * TILE_SIZE + TILE_SIZE/2;
const targetY = target.y * TILE_SIZE + TILE_SIZE/2;
// Calculate angle to target
const dx = targetX - kart.x;
const dy = targetY - kart.y;
const targetAngle = Math.atan2(dy, dx);
// Adjust angle gradually
let angleDiff = targetAngle - kart.angle;
// Normalize angle difference
while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI;
// Turn towards target
if (angleDiff > 0.1) {
kart.angle += kart.turnSpeed * deltaTime * 60;
} else if (angleDiff < -0.1) {
kart.angle -= kart.turnSpeed * deltaTime * 60;
}
// Accelerate
kart.speed = Math.min(kart.maxSpeed, kart.speed + kart.acceleration * deltaTime * 60);
// Check if reached waypoint
const distance = Math.sqrt(dx*dx + dy*dy);
if (distance < 30) {
kart.nextWaypoint = (kart.nextWaypoint + 1) % kart.waypoints.length;
}
// Random weapon firing
if (Math.random() < 0.01 && kart.weaponCount > 0) {
if (Math.random() < 0.7) {
fireWeapon(kart);
} else {
dropMine(kart);
}
}
});
}
function checkCollisions() {
// Projectiles vs Karts
for (let i = gameState.projectiles.length - 1; i >= 0; i--) {
const proj = gameState.projectiles[i];
for (let j = 0; j < gameState.karts.length; j++) {
const kart = gameState.karts[j];
// Don't hit owner
if ((proj.owner === 'player' && kart.isPlayer) ||
(proj.owner === 'ai' && !kart.isPlayer)) {
continue;
}
// Check collision
const dx = proj.x - kart.x;
const dy = proj.y - kart.y;
const distance = Math.sqrt(dx*dx + dy*dy);
if (distance < KART_SIZE) {
// Hit effect
kart.speed *= 0.5; // Slow down
// Create explosion
gameState.explosions.push({
emoji: '๐Ÿ’ฅ',
x: proj.x,
y: proj.y,
size: 20,
opacity: 1.0
});
// Remove projectile
gameState.projectiles.splice(i, 1);
break;
}
}
}
// Mines vs Karts
for (let i = gameState.mines.length - 1; i >= 0; i--) {
const mine = gameState.mines[i];
for (let j = 0; j < gameState.karts.length; j++) {
const kart = gameState.karts[j];
// Don't hit owner
if ((mine.owner === 'player' && kart.isPlayer) ||
(mine.owner === 'ai' && !kart.isPlayer)) {
continue;
}
// Check collision
const dx = mine.x - kart.x;
const dy = mine.y - kart.y;
const distance = Math.sqrt(dx*dx + dy*dy);
if (distance < KART_SIZE + mine.size/2) {
// Hit effect
kart.speed *= 0.3; // Big slow down
// Create explosion
gameState.explosions.push({
emoji: '๐Ÿ’ฅ',
x: mine.x,
y: mine.y,
size: 40,
opacity: 1.0
});
// Remove mine
gameState.mines.splice(i, 1);
break;
}
}
}
// Karts vs Karts
for (let i = 0; i < gameState.karts.length; i++) {
for (let j = i + 1; j < gameState.karts.length; j++) {
const kart1 = gameState.karts[i];
const kart2 = gameState.karts[j];
const dx = kart1.x - kart2.x;
const dy = kart1.y - kart2.y;
const distance = Math.sqrt(dx*dx + dy*dy);
if (distance < KART_SIZE * 1.5) {
// Push karts apart
const pushForce = 0.5;
const angle = Math.atan2(dy, dx);
kart1.x += Math.cos(angle) * pushForce;
kart1.y += Math.sin(angle) * pushForce;
kart2.x -= Math.cos(angle) * pushForce;
kart2.y -= Math.sin(angle) * pushForce;
// Slow down both karts
kart1.speed *= 0.8;
kart2.speed *= 0.8;
}
}
}
}
function checkLapCompletion() {
const finishLine = findFinishPosition();
const finishRect = {
x: finishLine.x - TILE_SIZE/2,
y: finishLine.y - TILE_SIZE/2,
width: TILE_SIZE,
height: TILE_SIZE
};
gameState.karts.forEach(kart => {
// Check if kart is crossing finish line
if (kart.x > finishRect.x && kart.x < finishRect.x + finishRect.width &&
kart.y > finishRect.y && kart.y < finishRect.y + finishRect.height) {
// Check direction (prevent multiple lap counts when sitting on line)
const angleToFinish = Math.atan2(finishLine.y - kart.y, finishLine.x - kart.x);
const angleDiff = Math.abs(normalizeAngle(kart.angle - angleToFinish));
if (angleDiff < Math.PI/2) {
kart.lap++;
kart.checkPoint = 0;
if (kart.isPlayer) {
gameState.lapTimes.push(gameState.raceTime);
if (kart.lap <= MAX_LAPS) {
gameState.lap = kart.lap;
// Check if race finished
if (kart.lap > MAX_LAPS) {
finishRace();
}
}
}
}
}
});
}
function findFinishPosition() {
for (let y = 0; y < gameState.track.length; y++) {
for (let x = 0; x < gameState.track[y].length; x++) {
if (gameState.track[y][x] === 'F') {
return {
x: x * TILE_SIZE + TILE_SIZE/2,
y: y * TILE_SIZE + TILE_SIZE/2
};
}
}
}
return {x: 100, y: 100}; // Fallback
}
function normalizeAngle(angle) {
while (angle > Math.PI) angle -= 2 * Math.PI;
while (angle < -Math.PI) angle += 2 * Math.PI;
return angle;
}
function updatePosition() {
// Simple position tracking based on lap progress
if (gameState.lap <= MAX_LAPS) {
const playerKart = gameState.karts.find(k => k.isPlayer);
// Sort karts by lap progress
gameState.karts.sort((a, b) => {
if (a.lap !== b.lap) return b.lap - a.lap;
return b.checkPoint - a.checkPoint;
});
// Find player position
for (let i = 0; i < gameState.karts.length; i++) {
if (gameState.karts[i].isPlayer) {
gameState.position = i + 1;
break;
}
}
}
}
function updateHUD() {
const playerKart = gameState.karts.find(k => k.isPlayer);
// Update lap counter
elements.lapCounter.textContent = Math.min(gameState.lap, MAX_LAPS);
// Update speed
elements.speedCounter.textContent = Math.abs(Math.round(playerKart.speed * 10));
// Update position
const positions = ['1st', '2nd', '3rd', '4th'];
elements.positionCounter.textContent = positions[gameState.position - 1] || `${gameState.position}th`;
// Update time
const minutes = Math.floor(gameState.raceTime / 60);
const seconds = Math.floor(gameState.raceTime % 60);
const milliseconds = Math.floor((gameState.raceTime % 1) * 100);
elements.timeCounter.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(2, '0')}`;
// Update weapon
elements.weaponDisplay.textContent = playerKart.weapon;
elements.weaponCount.textContent = `x${playerKart.weaponCount}`;
}
function finishRace() {
gameState.screen = 'results';
elements.gameHud.classList.add('hidden');
elements.resultsScreen.classList.remove('hidden');
// Calculate best lap
if (gameState.lapTimes.length > 0) {
gameState.bestLapTime = Math.min(...gameState.lapTimes);
}
// Display results
const positions = ['1st ๐Ÿ†', '2nd ๐Ÿฅˆ', '3rd ๐Ÿฅ‰', '4th'];
elements.resultPosition.textContent = positions[gameState.position - 1] || `${gameState.position}th`;
const totalMinutes = Math.floor(gameState.raceTime / 60);
const totalSeconds = Math.floor(gameState.raceTime % 60);
const totalMilliseconds = Math.floor((gameState.raceTime % 1) * 100);
elements.resultTime.textContent = `${totalMinutes}:${totalSeconds.toString().padStart(2, '0')}.${totalMilliseconds.toString().padStart(2, '0')}`;
const bestMinutes = Math.floor(gameState.bestLapTime / 60);
const bestSeconds = Math.floor(gameState.bestLapTime % 60);
const bestMilliseconds = Math.floor((gameState.bestLapTime % 1) * 100);
elements.resultBestLap.textContent = `${bestMinutes}:${bestSeconds.toString().padStart(2, '0')}.${bestMilliseconds.toString().padStart(2, '0')}`;
// Save progress
try {
const saveData = JSON.parse(localStorage.getItem('whackySave')) || { cupsCompleted: 0, totalPoints: 0 };
if (gameState.position === 1) {
saveData.cupsCompleted = Math.max(saveData.cupsCompleted, 1);
saveData.totalPoints += 10;
}
localStorage.setItem('whackySave', JSON.stringify(saveData));
} catch (e) {
console.error('Failed to save game progress', e);
}
}
function drawGame() {
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Calculate camera position (follow player)
const playerKart = gameState.karts.find(k => k.isPlayer);
const cameraX = playerKart ? playerKart.x - canvas.width/2 : 0;
const cameraY = playerKart ? playerKart.y - canvas.height/2 : 0;
// Draw track
drawTrack(cameraX, cameraY);
// Draw mines
drawMines(cameraX, cameraY);
// Draw projectiles
drawProjectiles(cameraX, cameraY);
// Draw explosions
drawExplosions(cameraX, cameraY);
// Draw karts
drawKarts(cameraX, cameraY);
// Draw minimap (if player exists)
if (playerKart) {
drawMinimap();
}
}
function drawTrack(cameraX, cameraY) {
const startX = Math.max(0, Math.floor(cameraX / TILE_SIZE) - 1);
const startY = Math.max(0, Math.floor(cameraY / TILE_SIZE) - 1);
const endX = Math.min(gameState.track[0].length, Math.ceil((cameraX + canvas.width) / TILE_SIZE) + 1);
const endY = Math.min(gameState.track.length, Math.ceil((cameraY + canvas.height) / TILE_SIZE) + 1);
for (let y = startY; y < endY; y++) {
for (let x = startX; x < endX; x++) {
const tileType = gameState.track[y][x];
const tileInfo = TRACK_TYPES[tileType] || TRACK_TYPES['G'];
// Draw tile background
ctx.fillStyle = tileInfo.color;
ctx.fillRect(
x * TILE_SIZE - cameraX,
y * TILE_SIZE - cameraY,
TILE_SIZE,
TILE_SIZE
);
// Draw tile decorations
if (tileType === 'P') {
drawEmoji(
'โญ',
x * TILE_SIZE + TILE_SIZE/2 - cameraX,
y * TILE_SIZE + TILE_SIZE/2 - cameraY,
TILE_SIZE * 0.8
);
} else if (tileType === 'S') {
drawEmoji(
'๐Ÿšฆ',
x * TILE_SIZE + TILE_SIZE/2 - cameraX,
y * TILE_SIZE + TILE_SIZE/2 - cameraY,
TILE_SIZE * 0.8
);
} else if (tileType === 'F') {
drawEmoji(
'๐Ÿ',
x * TILE_SIZE + TILE_SIZE/2 - cameraX,
y * TILE_SIZE + TILE_SIZE/2 - cameraY,
TILE_SIZE * 0.8
);
}
}
}
}
function drawKarts(cameraX, cameraY) {
gameState.karts.forEach(kart => {
// Draw kart body
ctx.save();
ctx.translate(kart.x - cameraX, kart.y - cameraY);
ctx.rotate(kart.angle);
// Body
ctx.fillStyle = kart.isPlayer ? '#e53e3e' : '#3182ce';
ctx.fillRect(-KART_SIZE/2, -KART_SIZE/2, KART_SIZE, KART_SIZE);
// Details
ctx.fillStyle = '#000000';
ctx.fillRect(-KART_SIZE/2 + 5, -KART_SIZE/2 + 5, KART_SIZE - 10, KART_SIZE - 10);
// Restore transform
ctx.restore();
// Draw emoji on top
drawEmoji(
kart.emoji,
kart.x - cameraX,
kart.y - cameraY,
KART_SIZE * 1.2
);
// Draw name for AI
if (!kart.isPlayer) {
ctx.fillStyle = '#ffffff';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.fillText(
kart.emoji + ' AI',
kart.x - cameraX,
kart.y - cameraY + KART_SIZE + 15
);
}
});
}
function drawProjectiles(cameraX, cameraY) {
gameState.projectiles.forEach(proj => {
drawEmoji(
proj.emoji,
proj.x - cameraX,
proj.y - cameraY,
PROJECTILE_SIZE
);
});
}
function drawMines(cameraX, cameraY) {
gameState.mines.forEach(mine => {
drawEmoji(
mine.emoji,
mine.x - cameraX,
mine.y - cameraY,
mine.size
);
});
}
function drawExplosions(cameraX, cameraY) {
gameState.explosions.forEach(explosion => {
ctx.save();
ctx.globalAlpha = explosion.opacity;
drawEmoji(
explosion.emoji,
explosion.x - cameraX,
explosion.y - cameraY,
explosion.size
);
ctx.restore();
});
}
function drawEmoji(emoji, x, y, size) {
ctx.font = `${size}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(emoji, x, y);
}
function drawMinimap() {
const miniMapSize = 150;
const miniMapX = canvas.width - miniMapSize - 20;
const miniMapY = 20;
const scale = miniMapSize / (Math.max(gameState.track[0].length, gameState.track.length) * TILE_SIZE);
// Draw background
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
ctx.fillRect(miniMapX, miniMapY, miniMapSize, miniMapSize);
// Draw track
for (let y = 0; y < gameState.track.length; y++) {
for (let x = 0; x < gameState.track[y].length; x++) {
const tileType = gameState.track[y][x];
const tileInfo = TRACK_TYPES[tileType] || TRACK_TYPES['G'];
ctx.fillStyle = tileInfo.color;
ctx.fillRect(
miniMapX + x * TILE_SIZE * scale,
miniMapY + y * TILE_SIZE * scale,
Math.ceil(TILE_SIZE * scale),
Math.ceil(TILE_SIZE * scale)
);
}
}
// Draw karts
gameState.karts.forEach(kart => {
ctx.fillStyle = kart.isPlayer ? '#ff0000' : '#0000ff';
ctx.beginPath();
ctx.arc(
miniMapX + kart.x * scale,
miniMapY + kart.y * scale,
3,
0,
Math.PI * 2
);
ctx.fill();
});
// Draw border
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 2;
ctx.strokeRect(miniMapX, miniMapY, miniMapSize, miniMapSize);
}
// Initialize game
document.getElementById('player-frog').click();
</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=LukasBe/whacky-wheels-2d" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>