Add index.html
This commit is contained in:
commit
1667dc1088
482
index.html
Normal file
482
index.html
Normal file
@ -0,0 +1,482 @@
|
||||
<!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>
|
Loading…
Reference in New Issue
Block a user