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>
|