483 lines
17 KiB
HTML
483 lines
17 KiB
HTML
|
<!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';
|
|||
|
|
|||
|
document.querySelectorAll('input[name="controlType"]').forEach((elem) => {
|
|||
|
elem.addEventListener("change", function(event) {
|
|||
|
controlType = event.target.value;
|
|||
|
});
|
|||
|
});
|
|||
|
|
|||
|
|
|||
|
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 lMut = false;
|
|||
|
let rMut = false;
|
|||
|
|
|||
|
function createMudTexture() {
|
|||
|
const textureCanvas = document.createElement('canvas');
|
|||
|
textureCanvas.width = 200;
|
|||
|
textureCanvas.height = 200;
|
|||
|
const textureCtx = textureCanvas.getContext('2d');
|
|||
|
|
|||
|
textureCtx.fillStyle = '#8B4513';
|
|||
|
textureCtx.fillRect(0, 0, 200, 200);
|
|||
|
|
|||
|
for (let i = 0; i < 1000; i++) {
|
|||
|
textureCtx.fillStyle = `rgba(${Math.random() * 50 + 100}, ${Math.random() * 30 + 50}, ${Math.random() * 20}, ${Math.random() * 0.5})`;
|
|||
|
textureCtx.beginPath();
|
|||
|
textureCtx.arc(Math.random() * 200, Math.random() * 200, Math.random() * 2, 0, Math.PI * 2);
|
|||
|
textureCtx.fill();
|
|||
|
}
|
|||
|
|
|||
|
mudPattern = ctx.createPattern(textureCanvas, 'repeat');
|
|||
|
}
|
|||
|
|
|||
|
function initGame() {
|
|||
|
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 = [];
|
|||
|
rightTrail = [];
|
|||
|
|
|||
|
obstacles = [];
|
|||
|
for (let i = 0; i < 10; i++) {
|
|||
|
obstacles.push({
|
|||
|
x: Math.random() * canvasWidth,
|
|||
|
y: Math.random() * canvasHeight,
|
|||
|
radius: 15
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
createMudTexture();
|
|||
|
}
|
|||
|
|
|||
|
initGame();
|
|||
|
|
|||
|
const maxTrailLength = 500;
|
|||
|
const trailLifetime = 5000;
|
|||
|
const trailInterval = 5;
|
|||
|
|
|||
|
let frameCount = 0;
|
|||
|
|
|||
|
function drawTractor() {
|
|||
|
ctx.save();
|
|||
|
ctx.translate(tractor.x, tractor.y);
|
|||
|
ctx.rotate(tractor.angle);
|
|||
|
|
|||
|
ctx.fillStyle = 'green';
|
|||
|
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.leftTrackOffset + 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.rightTrackOffset + 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();
|
|||
|
}
|
|||
|
|
|||
|
function drawObstacles() {
|
|||
|
ctx.fillStyle = 'orange';
|
|||
|
for (let obstacle of obstacles) {
|
|||
|
ctx.beginPath();
|
|||
|
ctx.arc(obstacle.x, obstacle.y, obstacle.radius, 0, Math.PI * 2);
|
|||
|
ctx.fill();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function drawMud() {
|
|||
|
ctx.fillStyle = mudPattern;
|
|||
|
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
|
|||
|
}
|
|||
|
|
|||
|
function drawTrail() {
|
|||
|
const currentTime = Date.now();
|
|||
|
|
|||
|
ctx.lineWidth = 3;
|
|||
|
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) {
|
|||
|
const dx = newX - obstacle.x;
|
|||
|
const dy = newY - obstacle.y;
|
|||
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|||
|
if (distance < tractor.width / 2 + obstacle.radius + 5) {
|
|||
|
collision = true;
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if (!collision) {
|
|||
|
tractor.x = newX;
|
|||
|
tractor.y = newY;
|
|||
|
tractor.angle += turn;
|
|||
|
}
|
|||
|
|
|||
|
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));
|
|||
|
}
|
|||
|
|
|||
|
const keys = {};
|
|||
|
|
|||
|
document.addEventListener('keydown', (event) => {
|
|||
|
keys[event.key] = true;
|
|||
|
if (event.key === 'r' || event.key === 'R') {
|
|||
|
initGame();
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
document.addEventListener('keyup', (event) => {
|
|||
|
keys[event.key] = false;
|
|||
|
if (event.key === 'q' || event.key === 'Q') {
|
|||
|
lMut = false;
|
|||
|
}
|
|||
|
if (event.key === 'e' || event.key === 'E') {
|
|||
|
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['q']) {
|
|||
|
if (lMut == false) {
|
|||
|
tractor.leftReverse = !tractor.leftReverse;
|
|||
|
tractor.leftTrack = 0;
|
|||
|
lMut = true;
|
|||
|
}
|
|||
|
}
|
|||
|
if (keys['e']) {
|
|||
|
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 - 20);
|
|||
|
}
|
|||
|
|
|||
|
let lastTime = 0;
|
|||
|
let fps = 0;
|
|||
|
function gameLoop(currentTime) {
|
|||
|
const deltaTime = currentTime - lastTime;
|
|||
|
lastTime = currentTime;
|
|||
|
|
|||
|
fps = 1000 / deltaTime;
|
|||
|
|
|||
|
handleInput();
|
|||
|
updateTractor();
|
|||
|
|
|||
|
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
|
|||
|
|
|||
|
drawMud();
|
|||
|
drawTrail();
|
|||
|
drawObstacles();
|
|||
|
drawTractor();
|
|||
|
drawStatus();
|
|||
|
drawInstructions();
|
|||
|
|
|||
|
ctx.font = '16px Arial';
|
|||
|
ctx.fillStyle = 'white';
|
|||
|
ctx.fillText(`FPS: ${fps.toFixed(2)}`, 10, 90);
|
|||
|
|
|||
|
frameCount++;
|
|||
|
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;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
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);
|
|||
|
|
|||
|
setInterval(updateGamepadState, 100);
|
|||
|
</script>
|
|||
|
</body>
|
|||
|
</html>
|