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

952 lines
27 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PlaSim</title>
<style>
body,
html {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
canvas {
display: block;
}
</style>
</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>
</div>
<canvas id="gameCanvas"></canvas>
<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>
</body>
</html>