Files
traktorrr/index.html
2024-09-04 19:47:31 +03:00

27 KiB
Raw Blame History

<html lang="uk"> <head> <style> body, html { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; } canvas { display: block; } </style> </head>
Клавіатура Геймпад
<script> let controlType = "keyboard"; let bullets = []; let explosionMarks = []; document.querySelectorAll('input[name="controlType"]').forEach((elem) => { elem.addEventListener("change", function (event) { controlType = event.target.value; }); }); const wallTexture = new Image(); wallTexture.src = "wall.png"; const obstacleTexture = new Image(); obstacleTexture.src = "obs.png"; const canvas = document.getElementById("gameCanvas"); const ctx = canvas.getContext("2d"); let canvasWidth, canvasHeight; let tractor, leftTrail, rightTrail, obstacles, mudPattern; function resizeCanvas() { canvasWidth = window.innerWidth; canvasHeight = window.innerHeight; canvas.width = canvasWidth; canvas.height = canvasHeight; createMudTexture(); } window.addEventListener("resize", resizeCanvas); resizeCanvas(); let lastFireTime = 0; const fireInterval = 100; let lMut = false; let rMut = false; let explosions = []; let audioContext; let isSoundInitialized = false; let engineOscillator; let engineGainNode; function initializeSound() { if (!audioContext) { audioContext = new (window.AudioContext || window.webkitAudioContext)(); isSoundInitialized = true; initializeEngineSound(); } } function initializeEngineSound() { if (!isSoundInitialized) return; engineOscillator = audioContext.createOscillator(); engineGainNode = audioContext.createGain(); engineOscillator.type = "triangle"; engineOscillator.frequency.setValueAtTime(10, audioContext.currentTime); engineGainNode.gain.setValueAtTime(0, audioContext.currentTime); engineOscillator.connect(engineGainNode); engineGainNode.connect(audioContext.destination); engineOscillator.start(); } function updateEngineSound() { if (!engineOscillator) return; const speed = Math.sqrt( tractor.leftTrack * tractor.leftTrack + tractor.rightTrack * tractor.rightTrack ); const maxSpeed = tractor.speed * 2; const minFreq = 20; const maxFreq = 60; const frequency = minFreq + (maxFreq - minFreq) * (speed / maxSpeed); engineOscillator.frequency.setTargetAtTime( frequency, audioContext.currentTime, 0.1 ); const volume = speed / maxSpeed; engineGainNode.gain.setTargetAtTime( volume * 0.8, audioContext.currentTime, 0.1 ); } function playShootSound() { if (!isSoundInitialized) return; const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); oscillator.type = "square"; oscillator.frequency.setValueAtTime(150, audioContext.currentTime); oscillator.frequency.exponentialRampToValueAtTime( 0.01, audioContext.currentTime + 0.2 ); gainNode.gain.setValueAtTime(1, audioContext.currentTime); gainNode.gain.exponentialRampToValueAtTime( 0.01, audioContext.currentTime + 0.2 ); oscillator.connect(gainNode); gainNode.connect(audioContext.destination); oscillator.start(); oscillator.stop(audioContext.currentTime + 0.1); } function playExplosionSound() { if (!isSoundInitialized) return; const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); oscillator.type = "sawtooth"; oscillator.frequency.setValueAtTime(100, audioContext.currentTime); oscillator.frequency.exponentialRampToValueAtTime( 0.01, audioContext.currentTime + 0.5 ); gainNode.gain.setValueAtTime(1, audioContext.currentTime); gainNode.gain.exponentialRampToValueAtTime( 0.01, audioContext.currentTime + 0.5 ); oscillator.connect(gainNode); gainNode.connect(audioContext.destination); oscillator.start(); oscillator.stop(audioContext.currentTime + 0.5); } function stopEngineSound() { if (engineOscillator) { engineOscillator.stop(); engineOscillator = null; } } function fireBullet() { const currentTime = Date.now(); if (currentTime - lastFireTime < fireInterval) { return; } const bulletSpeed = Math.random() * 5 + 5; const bulletSize = 10; bullets.push({ x: tractor.x + Math.sin(tractor.angle) * (tractor.height / 2), y: tractor.y - Math.cos(tractor.angle) * (tractor.height / 2), angle: tractor.angle, speed: bulletSpeed, size: bulletSize, lifetime: 1000, }); playShootSound(); lastFireTime = currentTime; } function updateAndDrawExplosions() { explosions = explosions.filter((particle) => { particle.x += particle.vx; particle.y += particle.vy; particle.lifetime--; ctx.beginPath(); ctx.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2); ctx.fillStyle = particle.color; ctx.fill(); return particle.lifetime > 0; }); } function updateBullets() { bullets = bullets.filter((bullet) => { if (bullet.lifetime <= 0) { createExplosion(bullet.x, bullet.y); playExplosionSound(); return false; } bullet.x += Math.sin(bullet.angle) * bullet.speed; bullet.y -= Math.cos(bullet.angle) * bullet.speed; bullet.lifetime -= 25; return ( bullet.x >= 0 && bullet.x <= canvasWidth && bullet.y >= 0 && bullet.y <= canvasHeight ); }); } function createExplosion(x, y) { const particleCount = 10; for (let i = 0; i < particleCount; i++) { const angle = Math.random() * Math.PI * 1.2; const speed = Math.random() * 2 + 1; explosions.push({ x: x, y: y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, radius: Math.random() * 3 + 1, color: `hsl(${Math.random() * 60 + 15}, 100%, 50%)`, lifetime: 10, }); } explosionMarks.push({ x: x, y: y, radius: Math.random() * 5 + 5, color: `rgba(${Math.floor(Math.random() * 56) + 50}, ${ Math.floor(Math.random() * 56) + 50 }, ${Math.floor(Math.random() * 56) + 50}, 0.7)`, }); } function drawExplosionMarks() { for (let mark of explosionMarks) { ctx.beginPath(); ctx.arc(mark.x, mark.y, mark.radius, 0, Math.PI * 2); ctx.fillStyle = mark.color; ctx.fill(); } } function drawBullets() { ctx.fillStyle = "#505070"; for (let bullet of bullets) { ctx.fillRect( bullet.x - bullet.size / 2, bullet.y - bullet.size / 2, bullet.size, bullet.size ); } } function createMudTexture() { const textureCanvas = document.createElement("canvas"); textureCanvas.width = 500; textureCanvas.height = 500; const textureCtx = textureCanvas.getContext("2d"); textureCtx.fillStyle = "#8dff6c"; textureCtx.fillRect(0, 0, 500, 500); for (let i = 0; i < 1000; i++) { textureCtx.fillStyle = `rgba(${Math.random() * 20 + 20}, ${ Math.random() * 90 + 100 }, ${Math.random() * 20}, ${Math.random() * 0.5})`; textureCtx.beginPath(); textureCtx.arc( Math.random() * 500, Math.random() * 500, Math.random() * 2, 0, Math.PI * 20 ); textureCtx.fill(); } mudPattern = ctx.createPattern(textureCanvas, "repeat"); } function initGame() { tileSize = 64; tractor = { x: canvasWidth / 2, y: canvasHeight / 2, width: 60, height: 80, angle: 0, leftTrack: 0, rightTrack: 0, speed: 1.5, turnSpeed: 0.008, friction: 0.92, acceleration: 0.2, braking: 0.4, leftReverse: false, rightReverse: false, leftTrackOffset: 0, rightTrackOffset: 0, leftBrake: false, rightBrake: false, }; leftTrail = []; explosionMarks = []; rightTrail = []; obstacles = []; for (let i = 0; i < 10; i++) { obstacles.push({ x: Math.random() * (canvasWidth - tileSize * 2 - 32) + tileSize, y: Math.random() * (canvasHeight - tileSize * 2 - 46) + tileSize, width: 32, height: 46, }); } stopEngineSound(); initializeEngineSound(); createMudTexture(); } initGame(); const maxTrailLength = 5000; const trailLifetime = 50000; const trailInterval = 4; let frameCount = 0; function drawTractor() { ctx.save(); ctx.translate(tractor.x, tractor.y); ctx.rotate(tractor.angle); ctx.fillStyle = "#333"; ctx.fillRect( -tractor.width / 2, -tractor.height / 2, tractor.width, tractor.height ); const trackHeight = tractor.height; const trackWidth = 7; const segmentHeight = 7; ctx.globalAlpha = 0.8; ctx.fillStyle = "#333"; for (let i = 0; i < trackHeight / segmentHeight; i++) { const alpha = Math.max( 0.5, 1 - Math.abs((tractor.rightTrackOffset + i * segmentHeight) % 20) / 10 ); ctx.globalAlpha = alpha; ctx.fillRect( -tractor.width / 2 - trackWidth, -tractor.height / 2 + i * segmentHeight, trackWidth, segmentHeight - 2 ); } ctx.fillStyle = "#333"; for (let i = 0; i < trackHeight / segmentHeight; i++) { const alpha = Math.max( 0.5, 1 - Math.abs((tractor.leftTrackOffset + i * segmentHeight) % 20) / 10 ); ctx.globalAlpha = alpha; ctx.fillRect( tractor.width / 2, -tractor.height / 2 + i * segmentHeight, trackWidth, segmentHeight - 2 ); } ctx.globalAlpha = 1; ctx.fillStyle = "lightblue"; ctx.fillRect( -tractor.width / 4, -tractor.height / 4, tractor.width / 2, tractor.height / 2 ); ctx.beginPath(); ctx.moveTo(0, -tractor.height / 2 - 20); ctx.lineTo(10, -tractor.height / 2 - 10); ctx.lineTo(-10, -tractor.height / 2 - 10); ctx.closePath(); ctx.fillStyle = "red"; ctx.fill(); ctx.restore(); } const shadowCanvas = document.createElement("canvas"); shadowCanvas.width = 32; shadowCanvas.height = 46; const shadowCtx = shadowCanvas.getContext("2d"); obstacleTexture.onload = function () { shadowCtx.drawImage(obstacleTexture, 0, 0); }; function drawObstacles() { const t = Date.now() * 0.002; for (let obstacle of obstacles) { const yOffset = Math.sin(t + obstacle.x * 0.1) * 2; ctx.save(); ctx.shadowColor = "rgba(0, 0, 0, 0.3)"; ctx.shadowBlur = 10; ctx.shadowOffsetX = 5; ctx.shadowOffsetY = 5; ctx.drawImage( obstacleTexture, obstacle.x, obstacle.y - yOffset, obstacle.width, obstacle.height ); ctx.restore(); } } function drawMud() { ctx.fillStyle = mudPattern; ctx.fillRect(0, 0, canvasWidth, canvasHeight); } function drawTrail() { const currentTime = Date.now(); ctx.lineWidth = 10; ctx.strokeStyle = "rgba(60, 30, 15, 0.5)"; for (let trail of [leftTrail, rightTrail]) { ctx.beginPath(); for (let i = 0; i < trail.length; i++) { const alpha = Math.max( 0, 1 - (currentTime - trail[i].time) / trailLifetime ); if (alpha > 0) { ctx.globalAlpha = alpha; if (i === 0) { ctx.moveTo(trail[i].x, trail[i].y); } else { ctx.lineTo(trail[i].x, trail[i].y); } } } ctx.stroke(); } ctx.globalAlpha = 1; const cutoffTime = currentTime - trailLifetime; leftTrail = leftTrail.filter((point) => point.time > cutoffTime); rightTrail = rightTrail.filter((point) => point.time > cutoffTime); } function updateTractor() { const leftSpeed = tractor.leftTrack * (tractor.leftReverse ? -1 : 1); const rightSpeed = tractor.rightTrack * (tractor.rightReverse ? -1 : 1); const forwardSpeed = (leftSpeed + rightSpeed) / 2; const turn = (rightSpeed - leftSpeed) * tractor.turnSpeed; const newX = tractor.x + Math.sin(tractor.angle) * forwardSpeed; const newY = tractor.y - Math.cos(tractor.angle) * forwardSpeed; let collision = false; for (let obstacle of obstacles) { if ( newX - tractor.width / 2 < obstacle.x + obstacle.width && newX + tractor.width / 2 > obstacle.x && newY - tractor.height / 2 < obstacle.y + obstacle.height && newY + tractor.height / 2 > obstacle.y ) { collision = true; break; } } if (!collision) { tractor.x = newX; tractor.y = newY; tractor.angle += turn; } else { tractor.leftTrack = 0; tractor.rightTrack = 0; } tractor.leftTrack *= tractor.friction; tractor.rightTrack *= tractor.friction; tractor.leftTrackOffset += leftSpeed; tractor.rightTrackOffset += rightSpeed; tractor.leftTrackOffset = tractor.leftTrackOffset % 20; tractor.rightTrackOffset = tractor.rightTrackOffset % 20; if ( (tractor.leftBrake === false && tractor.rightBrake === true) || (tractor.leftBrake === true && tractor.rightBrake === false) ) { tractor.turnSpeed = 0.015; } else { tractor.turnSpeed = 0.008; } if (frameCount % trailInterval === 0) { const currentTime = Date.now(); const leftTrackPos = { x: tractor.x + (Math.sin(tractor.angle + Math.PI / 2) * tractor.width) / 2 + Math.sin(tractor.angle) * (tractor.leftReverse ? tractor.height / 2 : -tractor.height / 2), y: tractor.y - (Math.cos(tractor.angle + Math.PI / 2) * tractor.width) / 2 - Math.cos(tractor.angle) * (tractor.leftReverse ? tractor.height / 2 : -tractor.height / 2), time: currentTime, }; const rightTrackPos = { x: tractor.x + (Math.sin(tractor.angle - Math.PI / 2) * tractor.width) / 2 + Math.sin(tractor.angle) * (tractor.rightReverse ? tractor.height / 2 : -tractor.height / 2), y: tractor.y - (Math.cos(tractor.angle - Math.PI / 2) * tractor.width) / 2 - Math.cos(tractor.angle) * (tractor.rightReverse ? tractor.height / 2 : -tractor.height / 2), time: currentTime, }; leftTrail.push(leftTrackPos); rightTrail.push(rightTrackPos); if (leftTrail.length > maxTrailLength) leftTrail.shift(); if (rightTrail.length > maxTrailLength) rightTrail.shift(); } tractor.x = Math.max( tractor.width / 2, Math.min(canvasWidth - tractor.width / 2, tractor.x) ); tractor.y = Math.max( tractor.height / 2, Math.min(canvasHeight - tractor.height / 2, tractor.y) ); updateEngineSound(); } const keys = {}; document.addEventListener("keydown", (event) => { keys[event.key] = true; if (event.key === "r" || event.key === "R") { initGame(); } if (event.key === " ") { fireBullet(); } }); document.addEventListener("keyup", (event) => { keys[event.key] = false; if (event.key === "e" || event.key === "E") { lMut = false; } if (event.key === "q" || event.key === "Q") { rMut = false; } }); function handleKeyboardInput() { if (!keys["ArrowDown"]) { tractor.leftBrake = false; } if (!keys["s"]) { tractor.rightBrake = false; } if (keys["ArrowUp"]) { tractor.leftBrake = false; tractor.leftTrack = Math.min( tractor.leftTrack + tractor.acceleration, tractor.speed ); } else if (keys["ArrowDown"]) { tractor.leftBrake = true; tractor.leftTrack = Math.max(tractor.leftTrack - tractor.braking, 0); } if (keys["w"]) { tractor.rightBrake = false; tractor.rightTrack = Math.min( tractor.rightTrack + tractor.acceleration, tractor.speed ); } else if (keys["s"]) { tractor.rightBrake = true; tractor.rightTrack = Math.max( tractor.rightTrack - tractor.braking, 0 ); } if (keys["e"]) { if (lMut == false) { tractor.leftReverse = !tractor.leftReverse; tractor.leftTrack = 0; lMut = true; } } if (keys["q"]) { if (rMut == false) { tractor.rightReverse = !tractor.rightReverse; tractor.rightTrack = 0; rMut = true; } } } function handleInput() { if (controlType === "gamepad") { handleGamepadInput(); } else { handleKeyboardInput(); } } function drawStatus() { ctx.font = "16px Arial"; ctx.fillStyle = "white"; ctx.fillText( `Права вісь: ${ tractor.leftReverse ? "Реверс" : "Вперед" } (${tractor.leftTrack.toFixed(2)})`, 10, 30 ); ctx.fillText( `Ліва вісь: ${ tractor.rightReverse ? "Реверс" : "Вперед" } (${tractor.rightTrack.toFixed(2)})`, 10, 60 ); } function drawInstructions() { ctx.font = "14px Arial"; ctx.fillStyle = "white"; ctx.fillText("Керування:", 10, canvasHeight - 180); ctx.fillText("W - рух лівої гусениці", 10, canvasHeight - 160); ctx.fillText("S - гальмування лівої гусениці", 10, canvasHeight - 140); ctx.fillText("↑ - рух правої гусениці", 10, canvasHeight - 120); ctx.fillText("↓ - гальмування правої гусениці", 10, canvasHeight - 100); ctx.fillText("Q - реверс лівої гусениці", 10, canvasHeight - 80); ctx.fillText("E - реверс правої гусениці", 10, canvasHeight - 60); ctx.fillText("R - рестарт гри", 10, canvasHeight - 40); ctx.fillText("Пробіл - стріляти", 10, canvasHeight - 200); ctx.fillText( "Джойстик: Ліва вісь - ліва гусениця, Права вісь - права гусениця", 10, canvasHeight - 20 ); } function drawFrame() { const tileSize = 64; for (let x = 0; x < canvasWidth; x += tileSize) { ctx.drawImage(wallTexture, x, 0, tileSize, tileSize); ctx.drawImage( wallTexture, x, canvasHeight - tileSize, tileSize, tileSize ); } ctx.save(); ctx.translate(0, 0); ctx.rotate(Math.PI / 2); for (let y = 0; y < canvasHeight; y += tileSize) { ctx.drawImage(wallTexture, y, -tileSize, tileSize, tileSize); } ctx.restore(); ctx.save(); ctx.translate(canvasWidth, 0); ctx.rotate(Math.PI / 2); for (let y = 0; y < canvasHeight; y += tileSize) { ctx.drawImage(wallTexture, y, 0, tileSize, tileSize); } ctx.restore(); } function limitExplosionMarks(maxMarks = 50) { if (explosionMarks.length > maxMarks) { explosionMarks = explosionMarks.slice(-maxMarks); } } let lastTime = 0; let fps = 0; function gameLoop(currentTime) { const deltaTime = currentTime - lastTime; lastTime = currentTime; fps = 1000 / deltaTime; handleInput(); updateTractor(); updateBullets(); ctx.clearRect(0, 0, canvasWidth, canvasHeight); drawMud(); drawTrail(); drawExplosionMarks(); drawFrame(); drawObstacles(); drawTractor(); drawStatus(); drawInstructions(); drawBullets(); updateAndDrawExplosions(); ctx.font = "16px Arial"; ctx.fillStyle = "white"; ctx.fillText(`FPS: ${fps.toFixed(2)}`, 10, 90); frameCount++; limitExplosionMarks(); requestAnimationFrame(gameLoop); } let gamepad = null; function updateGamepadState() { const gamepads = navigator.getGamepads ? navigator.getGamepads() : navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []; gamepad = gamepads[0]; } function handleGamepadInput() { if (!gamepad) return; if (!gamepad.buttons[5].pressed) { lMut = false; } if (!gamepad.buttons[4].pressed) { rMut = false; } const leftAxisY = gamepad.axes[3]; const rightAxisY = gamepad.axes[1]; const deadzone = 0.1; if (Math.abs(leftAxisY) > deadzone) { if (leftAxisY < 0) { tractor.leftTrack = Math.min( tractor.leftTrack - leftAxisY * tractor.acceleration, tractor.speed ); tractor.leftBrake = false; } else { tractor.leftTrack = Math.max( tractor.leftTrack - tractor.braking, 0 ); tractor.leftBrake = true; } } else { //tractor.leftTrack = 0; tractor.leftBrake = false; } if (Math.abs(rightAxisY) > deadzone) { if (rightAxisY < 0) { tractor.rightTrack = Math.min( tractor.rightTrack - rightAxisY * tractor.acceleration, tractor.speed ); tractor.rightBrake = false; } else { tractor.rightTrack = Math.max( tractor.rightTrack - tractor.braking, 0 ); tractor.rightBrake = true; } } else { //tractor.rightTrack = 0; tractor.rightBrake = false; } if (gamepad.buttons[9].pressed) { initGame(); } if (gamepad.buttons[5].pressed && !lMut) { tractor.leftReverse = !tractor.leftReverse; tractor.leftTrack = 0; lMut = true; } if (gamepad.buttons[4].pressed && !rMut) { tractor.rightReverse = !tractor.rightReverse; tractor.rightTrack = 0; rMut = true; } if (gamepad.buttons[7].pressed) { fireBullet(); } } window.addEventListener("gamepadconnected", function (e) { console.log("Gamepad connected:", e.gamepad.id); gamepad = e.gamepad; }); window.addEventListener("gamepaddisconnected", function (e) { console.log("Gamepad disconnected:", e.gamepad.id); gamepad = null; }); requestAnimationFrame(gameLoop); function stopEngineSound() { if (engineOscillator) { engineOscillator.stop(); engineOscillator = null; } } document.addEventListener("click", function initAudio() { if (!isSoundInitialized) { initializeSound(); document.removeEventListener("click", initAudio); } }); setInterval(updateGamepadState, 100); function showAudioPrompt() { const promptDiv = document.createElement("div"); promptDiv.style.position = "absolute"; promptDiv.style.top = "50%"; promptDiv.style.left = "50%"; promptDiv.style.transform = "translate(-50%, -50%)"; promptDiv.style.background = "rgba(0, 0, 0, 0.7)"; promptDiv.style.color = "white"; promptDiv.style.fontFamily = "Arial, sans-serif"; promptDiv.style.padding = "20px"; promptDiv.style.borderRadius = "10px"; promptDiv.style.fontSize = "20px"; promptDiv.style.textAlign = "center"; promptDiv.innerHTML = "Клікніть будь-де для запуску звуку"; document.body.appendChild(promptDiv); function removePrompt() { document.body.removeChild(promptDiv); document.removeEventListener("click", removePrompt); } document.addEventListener("click", removePrompt); } showAudioPrompt(); </script> </html>