2024-09-04 16:09:41 +03:00
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
<html lang="uk">
|
2024-09-04 19:47:31 +03:00
|
|
|
|
<head>
|
|
|
|
|
<meta charset="UTF-8" />
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
2024-09-04 16:09:41 +03:00
|
|
|
|
<title>PlaSim</title>
|
|
|
|
|
<style>
|
2024-09-04 19:47:31 +03:00
|
|
|
|
body,
|
|
|
|
|
html {
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding: 0;
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
canvas {
|
|
|
|
|
display: block;
|
|
|
|
|
}
|
2024-09-04 16:09:41 +03:00
|
|
|
|
</style>
|
2024-09-04 19:47:31 +03:00
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<div
|
|
|
|
|
id="controlSwitch"
|
|
|
|
|
style="
|
|
|
|
|
position: absolute;
|
|
|
|
|
font-family: Arial, Helvetica, sans-serif;
|
|
|
|
|
top: 10px;
|
|
|
|
|
right: 10px;
|
|
|
|
|
background: rgba(0, 0, 0, 0.5);
|
|
|
|
|
color: aliceblue;
|
|
|
|
|
padding: 10px;
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
"
|
|
|
|
|
>
|
|
|
|
|
<label>
|
|
|
|
|
<input type="radio" name="controlType" value="keyboard" checked />
|
|
|
|
|
Клавіатура
|
|
|
|
|
</label>
|
|
|
|
|
<label>
|
|
|
|
|
<input type="radio" name="controlType" value="gamepad" /> Геймпад
|
|
|
|
|
</label>
|
2024-09-04 16:09:41 +03:00
|
|
|
|
</div>
|
|
|
|
|
<canvas id="gameCanvas"></canvas>
|
|
|
|
|
<script>
|
2024-09-04 19:47:31 +03:00
|
|
|
|
let controlType = "keyboard";
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
let bullets = [];
|
|
|
|
|
let explosionMarks = [];
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
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();
|
2024-09-04 16:09:41 +03:00
|
|
|
|
}
|
2024-09-04 19:47:31 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
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,
|
|
|
|
|
});
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
playShootSound();
|
|
|
|
|
lastFireTime = currentTime;
|
|
|
|
|
}
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
function updateAndDrawExplosions() {
|
|
|
|
|
explosions = explosions.filter((particle) => {
|
|
|
|
|
particle.x += particle.vx;
|
|
|
|
|
particle.y += particle.vy;
|
|
|
|
|
particle.lifetime--;
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
ctx.beginPath();
|
|
|
|
|
ctx.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2);
|
|
|
|
|
ctx.fillStyle = particle.color;
|
|
|
|
|
ctx.fill();
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
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,
|
|
|
|
|
});
|
2024-09-04 16:09:41 +03:00
|
|
|
|
}
|
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
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();
|
|
|
|
|
}
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
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,
|
|
|
|
|
});
|
|
|
|
|
}
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
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
|
|
|
|
|
);
|
|
|
|
|
}
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
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
|
|
|
|
|
);
|
|
|
|
|
}
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
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);
|
|
|
|
|
}
|
2024-09-04 16:09:41 +03:00
|
|
|
|
}
|
2024-09-04 19:47:31 +03:00
|
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
if (!collision) {
|
|
|
|
|
tractor.x = newX;
|
|
|
|
|
tractor.y = newY;
|
|
|
|
|
tractor.angle += turn;
|
|
|
|
|
} else {
|
|
|
|
|
tractor.leftTrack = 0;
|
|
|
|
|
tractor.rightTrack = 0;
|
|
|
|
|
}
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
tractor.leftTrack *= tractor.friction;
|
|
|
|
|
tractor.rightTrack *= tractor.friction;
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
tractor.leftTrackOffset += leftSpeed;
|
|
|
|
|
tractor.rightTrackOffset += rightSpeed;
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
tractor.leftTrackOffset = tractor.leftTrackOffset % 20;
|
|
|
|
|
tractor.rightTrackOffset = tractor.rightTrackOffset % 20;
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
if (
|
|
|
|
|
(tractor.leftBrake === false && tractor.rightBrake === true) ||
|
|
|
|
|
(tractor.leftBrake === true && tractor.rightBrake === false)
|
|
|
|
|
) {
|
|
|
|
|
tractor.turnSpeed = 0.015;
|
|
|
|
|
} else {
|
|
|
|
|
tractor.turnSpeed = 0.008;
|
2024-09-04 16:09:41 +03:00
|
|
|
|
}
|
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
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();
|
2024-09-04 16:09:41 +03:00
|
|
|
|
}
|
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
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)
|
|
|
|
|
);
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
updateEngineSound();
|
|
|
|
|
}
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
const keys = {};
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
document.addEventListener("keydown", (event) => {
|
|
|
|
|
keys[event.key] = true;
|
|
|
|
|
if (event.key === "r" || event.key === "R") {
|
|
|
|
|
initGame();
|
2024-09-04 16:09:41 +03:00
|
|
|
|
}
|
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
if (event.key === " ") {
|
|
|
|
|
fireBullet();
|
|
|
|
|
}
|
|
|
|
|
});
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
});
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
function handleKeyboardInput() {
|
|
|
|
|
if (!keys["ArrowDown"]) {
|
|
|
|
|
tractor.leftBrake = false;
|
|
|
|
|
}
|
|
|
|
|
if (!keys["s"]) {
|
|
|
|
|
tractor.rightBrake = false;
|
|
|
|
|
}
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
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);
|
|
|
|
|
}
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
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
|
|
|
|
|
);
|
2024-09-04 16:09:41 +03:00
|
|
|
|
}
|
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
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;
|
|
|
|
|
}
|
2024-09-04 16:09:41 +03:00
|
|
|
|
}
|
2024-09-04 19:47:31 +03:00
|
|
|
|
}
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
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
|
|
|
|
|
);
|
2024-09-04 16:09:41 +03:00
|
|
|
|
}
|
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
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);
|
2024-09-04 16:09:41 +03:00
|
|
|
|
}
|
2024-09-04 19:47:31 +03:00
|
|
|
|
ctx.restore();
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
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();
|
|
|
|
|
}
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
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);
|
|
|
|
|
}
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
let gamepad = null;
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
function updateGamepadState() {
|
|
|
|
|
const gamepads = navigator.getGamepads
|
|
|
|
|
? navigator.getGamepads()
|
|
|
|
|
: navigator.webkitGetGamepads
|
|
|
|
|
? navigator.webkitGetGamepads()
|
|
|
|
|
: [];
|
|
|
|
|
gamepad = gamepads[0];
|
|
|
|
|
}
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
function handleGamepadInput() {
|
|
|
|
|
if (!gamepad) return;
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
if (!gamepad.buttons[5].pressed) {
|
|
|
|
|
lMut = false;
|
|
|
|
|
}
|
|
|
|
|
if (!gamepad.buttons[4].pressed) {
|
|
|
|
|
rMut = false;
|
|
|
|
|
}
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
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;
|
2024-09-04 16:09:41 +03:00
|
|
|
|
}
|
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
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;
|
|
|
|
|
}
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
if (gamepad.buttons[9].pressed) {
|
|
|
|
|
initGame();
|
2024-09-04 16:09:41 +03:00
|
|
|
|
}
|
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
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;
|
|
|
|
|
}
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
if (gamepad.buttons[7].pressed) {
|
|
|
|
|
fireBullet();
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
window.addEventListener("gamepadconnected", function (e) {
|
|
|
|
|
console.log("Gamepad connected:", e.gamepad.id);
|
|
|
|
|
gamepad = e.gamepad;
|
|
|
|
|
});
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
window.addEventListener("gamepaddisconnected", function (e) {
|
|
|
|
|
console.log("Gamepad disconnected:", e.gamepad.id);
|
|
|
|
|
gamepad = null;
|
|
|
|
|
});
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
requestAnimationFrame(gameLoop);
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
function stopEngineSound() {
|
|
|
|
|
if (engineOscillator) {
|
|
|
|
|
engineOscillator.stop();
|
|
|
|
|
engineOscillator = null;
|
2024-09-04 16:09:41 +03:00
|
|
|
|
}
|
2024-09-04 19:47:31 +03:00
|
|
|
|
}
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
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);
|
|
|
|
|
}
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
document.addEventListener("click", removePrompt);
|
|
|
|
|
}
|
2024-09-04 16:09:41 +03:00
|
|
|
|
|
2024-09-04 19:47:31 +03:00
|
|
|
|
showAudioPrompt();
|
2024-09-04 16:09:41 +03:00
|
|
|
|
</script>
|
2024-09-04 19:47:31 +03:00
|
|
|
|
</body>
|
2024-09-04 16:09:41 +03:00
|
|
|
|
</html>
|