965 lines
28 KiB
JavaScript
965 lines
28 KiB
JavaScript
// Parameters
|
|
const rotationSpeedChange = 0.1;
|
|
const movementSpeedChange = 0.01;
|
|
const torpedoLaunchSpeed = 3;
|
|
const asteroidSpeedCoefficient = 1.5;
|
|
const updateFrequency = 1000 / 60;
|
|
const dangerousTorpedoTimeout = 1000;
|
|
const explosionDuration = 1000;
|
|
const explosionMaxRadius = 100;
|
|
const explosionMaxWidth = 20;
|
|
|
|
let score = 0;
|
|
|
|
let isGameRunning = false;
|
|
let isGamePaused = false;
|
|
let gameOver = false;
|
|
|
|
const canvas = document.getElementById("game-canvas");
|
|
const ctx = canvas.getContext("2d");
|
|
ctx.canvas.width = 1920;
|
|
ctx.canvas.height = 1080;
|
|
|
|
const getDegToRad = (degrees) => degrees * (Math.PI / 180);
|
|
|
|
const getRadToDeg = (radians) => radians * (180 / Math.PI);
|
|
|
|
const getDistance = (
|
|
{ position: { x: x1, y: y1 } },
|
|
{ position: { x: x2, y: y2 } }
|
|
) => ((x1 - x2) ** 2 + (y1 - y2) ** 2) ** 0.5;
|
|
|
|
const getRandomDirection = () => Math.random() * 360;
|
|
|
|
const getRandomAsteroidPosition = (starship) => {
|
|
const distanceFromStarship = ctx.canvas.height / 2;
|
|
const randomDirection = getRandomDirection();
|
|
let x =
|
|
starship.position.x +
|
|
Math.cos(getDegToRad(randomDirection)) * distanceFromStarship;
|
|
let y =
|
|
starship.position.y +
|
|
Math.sin(getDegToRad(randomDirection)) * distanceFromStarship;
|
|
if (x < 0) {
|
|
x += ctx.canvas.width;
|
|
}
|
|
if (x > ctx.canvas.width) {
|
|
x -= ctx.canvas.width;
|
|
}
|
|
if (y < 0) {
|
|
y += ctx.canvas.height;
|
|
}
|
|
if (y > ctx.canvas.height) {
|
|
y -= ctx.canvas.height;
|
|
}
|
|
return { x, y };
|
|
};
|
|
|
|
let starship;
|
|
let torpedoes;
|
|
let asteroids;
|
|
let explosions;
|
|
|
|
const starshipImage = new Image();
|
|
starshipImage.src = "./assets/starship.svg";
|
|
|
|
const torpedoImage = new Image();
|
|
torpedoImage.src = "./assets/torpedo.svg";
|
|
|
|
const asteroidBigImage = new Image();
|
|
asteroidBigImage.src = "./assets/asteroid-big.svg";
|
|
|
|
const asteroidMediumImage = new Image();
|
|
asteroidMediumImage.src = "./assets/asteroid-medium.svg";
|
|
|
|
const asteroidSmallImage = new Image();
|
|
asteroidSmallImage.src = "./assets/asteroid-small.svg";
|
|
|
|
const draw = () => {
|
|
// Clear the canvas
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// Draw the starship
|
|
if (!starship.exploded) {
|
|
ctx.save();
|
|
ctx.translate(starship.position.x, starship.position.y);
|
|
ctx.rotate(-1 * getDegToRad(starship.rotation - 90));
|
|
ctx.translate(-starship.position.x, -starship.position.y);
|
|
ctx.drawImage(
|
|
starshipImage,
|
|
starship.position.x - starshipImage.naturalWidth / 2,
|
|
starship.position.y - starshipImage.naturalHeight / 2,
|
|
starshipImage.naturalWidth,
|
|
starshipImage.naturalHeight
|
|
);
|
|
ctx.restore();
|
|
|
|
// Left edge
|
|
if (starship.position.x <= starshipImage.naturalWidth) {
|
|
ctx.save();
|
|
ctx.translate(
|
|
starship.position.x + ctx.canvas.width,
|
|
starship.position.y
|
|
);
|
|
ctx.rotate(-1 * getDegToRad(starship.rotation - 90));
|
|
ctx.translate(-starship.position.x, -starship.position.y);
|
|
ctx.drawImage(
|
|
starshipImage,
|
|
starship.position.x - starshipImage.naturalWidth / 2,
|
|
starship.position.y - starshipImage.naturalHeight / 2,
|
|
starshipImage.naturalWidth,
|
|
starshipImage.naturalHeight
|
|
);
|
|
ctx.restore();
|
|
}
|
|
|
|
// Right edge
|
|
if (starship.position.x >= ctx.canvas.width - starshipImage.naturalWidth) {
|
|
ctx.save();
|
|
ctx.translate(
|
|
starship.position.x - ctx.canvas.width,
|
|
starship.position.y
|
|
);
|
|
ctx.rotate(-1 * getDegToRad(starship.rotation - 90));
|
|
ctx.translate(-starship.position.x, -starship.position.y);
|
|
ctx.drawImage(
|
|
starshipImage,
|
|
starship.position.x - starshipImage.naturalWidth / 2,
|
|
starship.position.y - starshipImage.naturalHeight / 2,
|
|
starshipImage.naturalWidth,
|
|
starshipImage.naturalHeight
|
|
);
|
|
ctx.restore();
|
|
}
|
|
|
|
// Top edge
|
|
if (starship.position.y <= starshipImage.naturalHeight) {
|
|
ctx.save();
|
|
ctx.translate(
|
|
starship.position.x,
|
|
starship.position.y + ctx.canvas.height
|
|
);
|
|
ctx.rotate(-1 * getDegToRad(starship.rotation - 90));
|
|
ctx.translate(-starship.position.x, -starship.position.y);
|
|
ctx.drawImage(
|
|
starshipImage,
|
|
starship.position.x - starshipImage.naturalWidth / 2,
|
|
starship.position.y - starshipImage.naturalHeight / 2,
|
|
starshipImage.naturalWidth,
|
|
starshipImage.naturalHeight
|
|
);
|
|
ctx.restore();
|
|
}
|
|
|
|
// Bottom edge
|
|
if (
|
|
starship.position.y >=
|
|
ctx.canvas.height - starshipImage.naturalHeight
|
|
) {
|
|
ctx.save();
|
|
ctx.translate(
|
|
starship.position.x,
|
|
starship.position.y - ctx.canvas.height
|
|
);
|
|
ctx.rotate(-1 * getDegToRad(starship.rotation - 90));
|
|
ctx.translate(-starship.position.x, -starship.position.y);
|
|
ctx.drawImage(
|
|
starshipImage,
|
|
starship.position.x - starshipImage.naturalWidth / 2,
|
|
starship.position.y - starshipImage.naturalHeight / 2,
|
|
starshipImage.naturalWidth,
|
|
starshipImage.naturalHeight
|
|
);
|
|
ctx.restore();
|
|
}
|
|
|
|
// Top left corner
|
|
if (
|
|
starship.position.x <= starshipImage.naturalWidth &&
|
|
starship.position.y <= starshipImage.naturalHeight
|
|
) {
|
|
ctx.save();
|
|
ctx.translate(
|
|
starship.position.x + ctx.canvas.width,
|
|
starship.position.y + ctx.canvas.height
|
|
);
|
|
ctx.rotate(-1 * getDegToRad(starship.rotation - 90));
|
|
ctx.translate(-starship.position.x, -starship.position.y);
|
|
ctx.drawImage(
|
|
starshipImage,
|
|
starship.position.x - starshipImage.naturalWidth / 2,
|
|
starship.position.y - starshipImage.naturalHeight / 2,
|
|
starshipImage.naturalWidth,
|
|
starshipImage.naturalHeight
|
|
);
|
|
ctx.restore();
|
|
}
|
|
|
|
// Bottom left corner
|
|
if (
|
|
starship.position.x <= starshipImage.naturalWidth &&
|
|
starship.position.y >= ctx.canvas.height - starshipImage.naturalHeight
|
|
) {
|
|
ctx.save();
|
|
ctx.translate(
|
|
starship.position.x + ctx.canvas.width,
|
|
starship.position.y - ctx.canvas.height
|
|
);
|
|
ctx.rotate(-1 * getDegToRad(starship.rotation - 90));
|
|
ctx.translate(-starship.position.x, -starship.position.y);
|
|
ctx.drawImage(
|
|
starshipImage,
|
|
starship.position.x - starshipImage.naturalWidth / 2,
|
|
starship.position.y - starshipImage.naturalHeight / 2,
|
|
starshipImage.naturalWidth,
|
|
starshipImage.naturalHeight
|
|
);
|
|
ctx.restore();
|
|
}
|
|
|
|
// Top right corner
|
|
if (
|
|
starship.position.x >= ctx.canvas.width - starshipImage.naturalWidth &&
|
|
starship.position.y <= starshipImage.naturalHeight
|
|
) {
|
|
ctx.save();
|
|
ctx.translate(
|
|
starship.position.x - ctx.canvas.width,
|
|
starship.position.y + ctx.canvas.height
|
|
);
|
|
ctx.rotate(-1 * getDegToRad(starship.rotation - 90));
|
|
ctx.translate(-starship.position.x, -starship.position.y);
|
|
ctx.drawImage(
|
|
starshipImage,
|
|
starship.position.x - starshipImage.naturalWidth / 2,
|
|
starship.position.y - starshipImage.naturalHeight / 2,
|
|
starshipImage.naturalWidth,
|
|
starshipImage.naturalHeight
|
|
);
|
|
ctx.restore();
|
|
}
|
|
|
|
// Bottom right corner
|
|
if (
|
|
starship.position.y >= ctx.canvas.height - starshipImage.naturalHeight &&
|
|
starship.position.x >= ctx.canvas.width - starshipImage.naturalWidth
|
|
) {
|
|
ctx.save();
|
|
ctx.translate(
|
|
starship.position.x - ctx.canvas.width,
|
|
starship.position.y - ctx.canvas.height
|
|
);
|
|
ctx.rotate(-1 * getDegToRad(starship.rotation - 90));
|
|
ctx.translate(-starship.position.x, -starship.position.y);
|
|
ctx.drawImage(
|
|
starshipImage,
|
|
starship.position.x - starshipImage.naturalWidth / 2,
|
|
starship.position.y - starshipImage.naturalHeight / 2,
|
|
starshipImage.naturalWidth,
|
|
starshipImage.naturalHeight
|
|
);
|
|
ctx.restore();
|
|
}
|
|
}
|
|
|
|
// Draw the torpedoes
|
|
torpedoes.forEach((torpedo) => {
|
|
ctx.drawImage(
|
|
torpedoImage,
|
|
torpedo.position.x - torpedoImage.naturalWidth / 2,
|
|
torpedo.position.y - torpedoImage.naturalHeight / 2,
|
|
torpedoImage.naturalWidth,
|
|
torpedoImage.naturalHeight
|
|
);
|
|
});
|
|
|
|
// Draw the asteroids
|
|
asteroids.forEach((asteroid) => {
|
|
let asteroidImage;
|
|
if (asteroid.size === 2) {
|
|
asteroidImage = asteroidBigImage;
|
|
}
|
|
if (asteroid.size === 1) {
|
|
asteroidImage = asteroidMediumImage;
|
|
}
|
|
if (asteroid.size === 0) {
|
|
asteroidImage = asteroidSmallImage;
|
|
}
|
|
ctx.save();
|
|
ctx.translate(asteroid.position.x, asteroid.position.y);
|
|
ctx.rotate(-1 * getDegToRad(asteroid.rotation - 90));
|
|
ctx.translate(-asteroid.position.x, -asteroid.position.y);
|
|
ctx.drawImage(
|
|
asteroidImage,
|
|
asteroid.position.x - asteroidImage.naturalWidth / 2,
|
|
asteroid.position.y - asteroidImage.naturalHeight / 2,
|
|
asteroidImage.naturalWidth,
|
|
asteroidImage.naturalHeight
|
|
);
|
|
ctx.restore();
|
|
|
|
// Left edge
|
|
if (asteroid.position.x <= asteroidImage.naturalWidth) {
|
|
ctx.save();
|
|
ctx.translate(
|
|
asteroid.position.x + ctx.canvas.width,
|
|
asteroid.position.y
|
|
);
|
|
ctx.rotate(-1 * getDegToRad(asteroid.rotation - 90));
|
|
ctx.translate(-asteroid.position.x, -asteroid.position.y);
|
|
ctx.drawImage(
|
|
asteroidImage,
|
|
asteroid.position.x - asteroidImage.naturalWidth / 2,
|
|
asteroid.position.y - asteroidImage.naturalHeight / 2,
|
|
asteroidImage.naturalWidth,
|
|
asteroidImage.naturalHeight
|
|
);
|
|
ctx.restore();
|
|
}
|
|
|
|
// Right edge
|
|
if (asteroid.position.x >= ctx.canvas.width - asteroidImage.naturalWidth) {
|
|
ctx.save();
|
|
ctx.translate(
|
|
asteroid.position.x - ctx.canvas.width,
|
|
asteroid.position.y
|
|
);
|
|
ctx.rotate(-1 * getDegToRad(asteroid.rotation - 90));
|
|
ctx.translate(-asteroid.position.x, -asteroid.position.y);
|
|
ctx.drawImage(
|
|
asteroidImage,
|
|
asteroid.position.x - asteroidImage.naturalWidth / 2,
|
|
asteroid.position.y - asteroidImage.naturalHeight / 2,
|
|
asteroidImage.naturalWidth,
|
|
asteroidImage.naturalHeight
|
|
);
|
|
ctx.restore();
|
|
}
|
|
|
|
// Top edge
|
|
if (asteroid.position.y <= asteroidImage.naturalHeight) {
|
|
ctx.save();
|
|
ctx.translate(
|
|
asteroid.position.x,
|
|
asteroid.position.y + ctx.canvas.height
|
|
);
|
|
ctx.rotate(-1 * getDegToRad(asteroid.rotation - 90));
|
|
ctx.translate(-asteroid.position.x, -asteroid.position.y);
|
|
ctx.drawImage(
|
|
asteroidImage,
|
|
asteroid.position.x - asteroidImage.naturalWidth / 2,
|
|
asteroid.position.y - asteroidImage.naturalHeight / 2,
|
|
asteroidImage.naturalWidth,
|
|
asteroidImage.naturalHeight
|
|
);
|
|
ctx.restore();
|
|
}
|
|
|
|
// Bottom edge
|
|
if (
|
|
asteroid.position.y >=
|
|
ctx.canvas.height - asteroidImage.naturalHeight
|
|
) {
|
|
ctx.save();
|
|
ctx.translate(
|
|
asteroid.position.x,
|
|
asteroid.position.y - ctx.canvas.height
|
|
);
|
|
ctx.rotate(-1 * getDegToRad(asteroid.rotation - 90));
|
|
ctx.translate(-asteroid.position.x, -asteroid.position.y);
|
|
ctx.drawImage(
|
|
asteroidImage,
|
|
asteroid.position.x - asteroidImage.naturalWidth / 2,
|
|
asteroid.position.y - asteroidImage.naturalHeight / 2,
|
|
asteroidImage.naturalWidth,
|
|
asteroidImage.naturalHeight
|
|
);
|
|
ctx.restore();
|
|
}
|
|
|
|
// Top left corner
|
|
if (
|
|
asteroid.position.x <= asteroidImage.naturalWidth &&
|
|
asteroid.position.y <= asteroidImage.naturalHeight
|
|
) {
|
|
ctx.save();
|
|
ctx.translate(
|
|
asteroid.position.x + ctx.canvas.width,
|
|
asteroid.position.y + ctx.canvas.height
|
|
);
|
|
ctx.rotate(-1 * getDegToRad(asteroid.rotation - 90));
|
|
ctx.translate(-asteroid.position.x, -asteroid.position.y);
|
|
ctx.drawImage(
|
|
asteroidImage,
|
|
asteroid.position.x - asteroidImage.naturalWidth / 2,
|
|
asteroid.position.y - asteroidImage.naturalHeight / 2,
|
|
asteroidImage.naturalWidth,
|
|
asteroidImage.naturalHeight
|
|
);
|
|
ctx.restore();
|
|
}
|
|
|
|
// Bottom left corner
|
|
if (
|
|
asteroid.position.x <= asteroidImage.naturalWidth &&
|
|
asteroid.position.y >= ctx.canvas.height - asteroidImage.naturalHeight
|
|
) {
|
|
ctx.save();
|
|
ctx.translate(
|
|
asteroid.position.x + ctx.canvas.width,
|
|
asteroid.position.y - ctx.canvas.height
|
|
);
|
|
ctx.rotate(-1 * getDegToRad(asteroid.rotation - 90));
|
|
ctx.translate(-asteroid.position.x, -asteroid.position.y);
|
|
ctx.drawImage(
|
|
asteroidImage,
|
|
asteroid.position.x - asteroidImage.naturalWidth / 2,
|
|
asteroid.position.y - asteroidImage.naturalHeight / 2,
|
|
asteroidImage.naturalWidth,
|
|
asteroidImage.naturalHeight
|
|
);
|
|
ctx.restore();
|
|
}
|
|
|
|
// Top right corner
|
|
if (
|
|
asteroid.position.x >= ctx.canvas.width - asteroidImage.naturalWidth &&
|
|
asteroid.position.y <= asteroidImage.naturalHeight
|
|
) {
|
|
ctx.save();
|
|
ctx.translate(
|
|
asteroid.position.x - ctx.canvas.width,
|
|
asteroid.position.y + ctx.canvas.height
|
|
);
|
|
ctx.rotate(-1 * getDegToRad(asteroid.rotation - 90));
|
|
ctx.translate(-asteroid.position.x, -asteroid.position.y);
|
|
ctx.drawImage(
|
|
asteroidImage,
|
|
asteroid.position.x - asteroidImage.naturalWidth / 2,
|
|
asteroid.position.y - asteroidImage.naturalHeight / 2,
|
|
asteroidImage.naturalWidth,
|
|
asteroidImage.naturalHeight
|
|
);
|
|
ctx.restore();
|
|
}
|
|
|
|
// Bottom right corner
|
|
if (
|
|
asteroid.position.x >= ctx.canvas.width - asteroidImage.naturalWidth &&
|
|
asteroid.position.y >= ctx.canvas.height - asteroidImage.naturalHeight
|
|
) {
|
|
ctx.save();
|
|
ctx.translate(
|
|
asteroid.position.x - ctx.canvas.width,
|
|
asteroid.position.y - ctx.canvas.height
|
|
);
|
|
ctx.rotate(-1 * getDegToRad(asteroid.rotation - 90));
|
|
ctx.translate(-asteroid.position.x, -asteroid.position.y);
|
|
ctx.drawImage(
|
|
asteroidImage,
|
|
asteroid.position.x - asteroidImage.naturalWidth / 2,
|
|
asteroid.position.y - asteroidImage.naturalHeight / 2,
|
|
asteroidImage.naturalWidth,
|
|
asteroidImage.naturalHeight
|
|
);
|
|
ctx.restore();
|
|
}
|
|
});
|
|
|
|
// Draw the explosions
|
|
const now = Date.now();
|
|
explosions.forEach((explosion) => {
|
|
const explosionProgress = (now - explosion.startedAt) / explosionDuration;
|
|
const explosionRadius =
|
|
torpedoImage.naturalWidth + explosionProgress * explosionMaxRadius;
|
|
ctx.beginPath();
|
|
ctx.arc(
|
|
explosion.position.x,
|
|
explosion.position.y,
|
|
explosionRadius,
|
|
0,
|
|
2 * Math.PI
|
|
);
|
|
ctx.strokeStyle = `hsla(0, 0%, 100%, ${Math.max(
|
|
0.75 * 1 - explosionProgress,
|
|
0
|
|
)})`;
|
|
ctx.lineWidth = 1 + explosionProgress * explosionMaxWidth;
|
|
ctx.stroke();
|
|
});
|
|
|
|
// Request next frame
|
|
if (isGameRunning && !isGamePaused) {
|
|
requestAnimationFrame(draw);
|
|
}
|
|
};
|
|
|
|
let rotationMomentumChangeInterval = null;
|
|
let forwardSpeedChangeInterval = null;
|
|
|
|
const fireTorpedo = () => {
|
|
starship.canFire = false;
|
|
setTimeout(() => {
|
|
starship.canFire = true;
|
|
}, 500);
|
|
const moveVector = {
|
|
x:
|
|
(Math.cos(getDegToRad(starship.rotation)) || 0) *
|
|
(starship.forwardSpeed + torpedoLaunchSpeed),
|
|
y:
|
|
(Math.sin(getDegToRad(starship.rotation)) || 0) *
|
|
(starship.forwardSpeed + torpedoLaunchSpeed),
|
|
};
|
|
const driftVector = {
|
|
x:
|
|
(Math.cos(getDegToRad(starship.driftDirection)) || 0) *
|
|
starship.driftSpeed,
|
|
y:
|
|
(Math.sin(getDegToRad(starship.driftDirection)) || 0) *
|
|
starship.driftSpeed,
|
|
};
|
|
const deltaXDrift = driftVector.x + moveVector.x;
|
|
const deltaYDrift = driftVector.y + moveVector.y;
|
|
let direction;
|
|
if (starship.driftSpeed === 0) {
|
|
direction = starship.rotation;
|
|
} else {
|
|
direction =
|
|
getRadToDeg(Math.atan(deltaYDrift / deltaXDrift || 0)) +
|
|
(deltaXDrift < 0 ? 180 : 0);
|
|
}
|
|
const speed = (deltaXDrift ** 2 + deltaYDrift ** 2) ** 0.5;
|
|
torpedoes.push({
|
|
position: {
|
|
x: starship.position.x,
|
|
y: starship.position.y,
|
|
},
|
|
direction,
|
|
speed,
|
|
detonated: false,
|
|
launchedAt: Date.now(),
|
|
});
|
|
};
|
|
|
|
window.addEventListener("keydown", (event) => {
|
|
if (!isGameRunning) {
|
|
return;
|
|
}
|
|
if (event.key === "p" || event.key === "P") {
|
|
if (isGamePaused) {
|
|
document.getElementById("paused").classList.add("hidden");
|
|
isGamePaused = false;
|
|
clock = setInterval(tick, updateFrequency);
|
|
requestAnimationFrame(draw);
|
|
} else {
|
|
if (gameOver) {
|
|
return;
|
|
}
|
|
document.getElementById("paused").classList.remove("hidden");
|
|
isGamePaused = true;
|
|
clearInterval(clock);
|
|
}
|
|
}
|
|
if (isGamePaused) {
|
|
return;
|
|
}
|
|
if (event.key === "ArrowLeft" || event.key === "a" || event.key === "A") {
|
|
starship.rotationMomentum += rotationSpeedChange;
|
|
clearInterval(rotationMomentumChangeInterval);
|
|
rotationMomentumChangeInterval = setInterval(() => {
|
|
starship.rotationMomentum += rotationSpeedChange;
|
|
}, 50);
|
|
}
|
|
if (event.key === "ArrowRight" || event.key === "d" || event.key === "D") {
|
|
starship.rotationMomentum -= rotationSpeedChange;
|
|
clearInterval(rotationMomentumChangeInterval);
|
|
rotationMomentumChangeInterval = setInterval(() => {
|
|
starship.rotationMomentum -= rotationSpeedChange;
|
|
}, 50);
|
|
}
|
|
if (event.key === "ArrowUp" || event.key === "w" || event.key === "W") {
|
|
if (starship.forwardSpeed < 0) {
|
|
starship.forwardSpeed = 0;
|
|
}
|
|
starship.forwardSpeed += movementSpeedChange;
|
|
clearInterval(forwardSpeedChangeInterval);
|
|
forwardSpeedChangeInterval = setInterval(() => {
|
|
starship.forwardSpeed += movementSpeedChange;
|
|
}, 50);
|
|
}
|
|
if (event.key === "ArrowDown" || event.key === "s" || event.key === "S") {
|
|
if (starship.forwardSpeed > 0) {
|
|
starship.forwardSpeed = 0;
|
|
}
|
|
starship.forwardSpeed -= movementSpeedChange;
|
|
clearInterval(forwardSpeedChangeInterval);
|
|
forwardSpeedChangeInterval = setInterval(() => {
|
|
starship.forwardSpeed -= movementSpeedChange;
|
|
}, 50);
|
|
}
|
|
if (event.key === " " && starship.canFire) {
|
|
fireTorpedo();
|
|
}
|
|
});
|
|
|
|
document.addEventListener("click", () => {
|
|
if (!isGameRunning) {
|
|
return;
|
|
}
|
|
if (starship.canFire) {
|
|
fireTorpedo();
|
|
}
|
|
});
|
|
|
|
window.addEventListener("keyup", (event) => {
|
|
if (!isGameRunning) {
|
|
return;
|
|
}
|
|
if (event.key === "ArrowLeft" || event.key === "a" || event.key === "A") {
|
|
clearInterval(rotationMomentumChangeInterval);
|
|
}
|
|
if (event.key === "ArrowRight" || event.key === "d" || event.key === "D") {
|
|
clearInterval(rotationMomentumChangeInterval);
|
|
}
|
|
if (event.key === "ArrowUp" || event.key === "w" || event.key === "W") {
|
|
clearInterval(forwardSpeedChangeInterval);
|
|
starship.forwardSpeed = 0;
|
|
}
|
|
if (event.key === "ArrowDown" || event.key === "s" || event.key === "S") {
|
|
clearInterval(forwardSpeedChangeInterval);
|
|
starship.forwardSpeed = 0;
|
|
}
|
|
});
|
|
|
|
const tick = () => {
|
|
// Move starship
|
|
starship.rotation += starship.rotationMomentum;
|
|
const moveVector = {
|
|
x: (Math.cos(getDegToRad(starship.rotation)) || 0) * starship.forwardSpeed,
|
|
y: (Math.sin(getDegToRad(starship.rotation)) || 0) * starship.forwardSpeed,
|
|
};
|
|
const driftVector = {
|
|
x:
|
|
(Math.cos(getDegToRad(starship.driftDirection)) || 0) *
|
|
starship.driftSpeed,
|
|
y:
|
|
(Math.sin(getDegToRad(starship.driftDirection)) || 0) *
|
|
starship.driftSpeed,
|
|
};
|
|
const deltaXDrift = driftVector.x + moveVector.x;
|
|
const deltaYDrift = driftVector.y + moveVector.y;
|
|
if (starship.driftSpeed === 0) {
|
|
starship.driftDirection = starship.rotation;
|
|
} else {
|
|
starship.driftDirection =
|
|
getRadToDeg(Math.atan(deltaYDrift / deltaXDrift || 0)) +
|
|
(deltaXDrift < 0 ? 180 : 0);
|
|
}
|
|
starship.driftSpeed = (deltaXDrift ** 2 + deltaYDrift ** 2) ** 0.5;
|
|
starship.position.x +=
|
|
starship.driftSpeed * Math.cos(getDegToRad(starship.driftDirection)) || 0;
|
|
if (starship.position.x < 0) {
|
|
starship.position.x += ctx.canvas.width;
|
|
}
|
|
if (starship.position.x > ctx.canvas.width) {
|
|
starship.position.x -= ctx.canvas.width;
|
|
}
|
|
starship.position.y -=
|
|
starship.driftSpeed * Math.sin(getDegToRad(starship.driftDirection)) || 0;
|
|
if (starship.position.y < 0) {
|
|
starship.position.y += ctx.canvas.height;
|
|
}
|
|
if (starship.position.y > ctx.canvas.height) {
|
|
starship.position.y -= ctx.canvas.height;
|
|
}
|
|
|
|
// Move torpedoes
|
|
torpedoes.forEach((torpedo) => {
|
|
torpedo.position.x +=
|
|
torpedo.speed * Math.cos(getDegToRad(torpedo.direction)) || 0;
|
|
if (torpedo.position.x < 0) {
|
|
torpedo.position.x += ctx.canvas.width;
|
|
}
|
|
if (torpedo.position.x > ctx.canvas.width) {
|
|
torpedo.position.x -= ctx.canvas.width;
|
|
}
|
|
torpedo.position.y -=
|
|
torpedo.speed * Math.sin(getDegToRad(torpedo.direction)) || 0;
|
|
if (torpedo.position.y < 0) {
|
|
torpedo.position.y += ctx.canvas.height;
|
|
}
|
|
if (torpedo.position.y > ctx.canvas.height) {
|
|
torpedo.position.y -= ctx.canvas.height;
|
|
}
|
|
});
|
|
|
|
// Move asteroids
|
|
asteroids.forEach((asteroid) => {
|
|
const asteroidSpeed = (3 - asteroid.size) * asteroidSpeedCoefficient;
|
|
const deltaPosition = {
|
|
x: asteroidSpeed * Math.cos(getDegToRad(asteroid.direction)) || 0,
|
|
y: asteroidSpeed * Math.sin(getDegToRad(asteroid.direction)) || 0,
|
|
};
|
|
asteroid.position.x += deltaPosition.x;
|
|
if (asteroid.position.x < 0) {
|
|
asteroid.position.x += ctx.canvas.width;
|
|
}
|
|
if (asteroid.position.x > ctx.canvas.width) {
|
|
asteroid.position.x -= ctx.canvas.width;
|
|
}
|
|
asteroid.position.y -= deltaPosition.y;
|
|
if (asteroid.position.y < 0) {
|
|
asteroid.position.y += ctx.canvas.height;
|
|
}
|
|
if (asteroid.position.y > ctx.canvas.height) {
|
|
asteroid.position.y -= ctx.canvas.height;
|
|
}
|
|
asteroid.rotation += asteroidSpeed;
|
|
if (asteroid.rotation > 360) {
|
|
asteroid.rotation -= 360;
|
|
}
|
|
});
|
|
|
|
const now = Date.now();
|
|
|
|
// Detect torpedo collisions
|
|
torpedoes.forEach((torpedo, torpedoIndex) => {
|
|
// Torpedo hitting asteroid
|
|
asteroids.forEach((asteroid) => {
|
|
let asteroidImage;
|
|
if (asteroid.size === 2) {
|
|
asteroidImage = asteroidBigImage;
|
|
}
|
|
if (asteroid.size === 1) {
|
|
asteroidImage = asteroidMediumImage;
|
|
}
|
|
if (asteroid.size === 0) {
|
|
asteroidImage = asteroidSmallImage;
|
|
}
|
|
if (getDistance(torpedo, asteroid) <= asteroidImage.naturalWidth / 2) {
|
|
torpedo.detonated = true;
|
|
asteroid.exploded = true;
|
|
explosions.push({
|
|
position: {
|
|
x: torpedo.position.x,
|
|
y: torpedo.position.y,
|
|
},
|
|
startedAt: now,
|
|
});
|
|
if (!starship.exploded) {
|
|
score++;
|
|
}
|
|
document.getElementById("score").innerHTML = score;
|
|
}
|
|
});
|
|
|
|
// Torpedo hitting another torpedo
|
|
torpedoes.forEach((otherTorpedo, otherTorpedoIndex) => {
|
|
if (torpedoIndex !== otherTorpedoIndex) {
|
|
if (
|
|
getDistance(torpedo, otherTorpedo) <=
|
|
torpedoImage.naturalWidth / 2
|
|
) {
|
|
torpedo.detonated = true;
|
|
otherTorpedo.detonated = true;
|
|
explosions.push({
|
|
position: {
|
|
x: torpedo.position.x,
|
|
y: torpedo.position.y,
|
|
},
|
|
startedAt: now,
|
|
});
|
|
explosions.push({
|
|
position: {
|
|
x: otherTorpedo.position.x,
|
|
y: otherTorpedo.position.y,
|
|
},
|
|
startedAt: now,
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
// Torpedo hitting starship
|
|
if (
|
|
!torpedo.detonated &&
|
|
!starship.exploded &&
|
|
now - torpedo.launchedAt > dangerousTorpedoTimeout
|
|
) {
|
|
if (getDistance(torpedo, starship) <= starshipImage.naturalWidth / 2) {
|
|
starship.exploded = true;
|
|
torpedo.detonated = true;
|
|
explosions.push({
|
|
position: {
|
|
x: starship.position.x,
|
|
y: starship.position.y,
|
|
},
|
|
startedAt: now,
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
asteroids.forEach((asteroid) => {
|
|
if (!asteroid.exploded && !starship.exploded) {
|
|
// Asteroid hitting starship
|
|
let asteroidImage;
|
|
if (asteroid.size === 2) {
|
|
asteroidImage = asteroidBigImage;
|
|
}
|
|
if (asteroid.size === 1) {
|
|
asteroidImage = asteroidMediumImage;
|
|
}
|
|
if (asteroid.size === 0) {
|
|
asteroidImage = asteroidSmallImage;
|
|
}
|
|
if (
|
|
getDistance(asteroid, starship) <=
|
|
asteroidImage.naturalWidth / 2 + starshipImage.naturalWidth / 2
|
|
) {
|
|
starship.exploded = true;
|
|
explosions.push({
|
|
position: {
|
|
x: starship.position.x,
|
|
y: starship.position.y,
|
|
},
|
|
startedAt: now,
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
// Get rid of detonated torpedoes
|
|
torpedoes = torpedoes.filter((torpedo) => !torpedo.detonated);
|
|
|
|
// Get rid of invisible explosions
|
|
explosions = explosions.filter(
|
|
(explosion) => now - explosion.startedAt <= explosionDuration
|
|
);
|
|
|
|
asteroids = asteroids
|
|
.map((asteroid) => {
|
|
if (!asteroid.exploded) {
|
|
return asteroid;
|
|
}
|
|
|
|
// Remove exploded small asteroids
|
|
if (asteroid.size === 0) {
|
|
return [];
|
|
}
|
|
|
|
// Split exploded asteroids
|
|
return [
|
|
{
|
|
size: asteroid.size - 1,
|
|
position: { x: asteroid.position.x, y: asteroid.position.y },
|
|
direction: getRandomDirection(),
|
|
rotation: getRandomDirection(),
|
|
exploded: false,
|
|
},
|
|
{
|
|
size: asteroid.size - 1,
|
|
position: { x: asteroid.position.x, y: asteroid.position.y },
|
|
direction: getRandomDirection(),
|
|
rotation: getRandomDirection(),
|
|
exploded: false,
|
|
},
|
|
];
|
|
})
|
|
.flat();
|
|
|
|
// Create new asteroid if the last one has exploded
|
|
if (asteroids.length === 0) {
|
|
asteroids = [
|
|
{
|
|
size: 2,
|
|
position: getRandomAsteroidPosition(starship),
|
|
direction: getRandomDirection(),
|
|
rotation: getRandomDirection(),
|
|
exploded: false,
|
|
},
|
|
];
|
|
}
|
|
|
|
// Game over when starship has exploded
|
|
if (starship.exploded && !gameOver) {
|
|
gameOver = true;
|
|
clearInterval(rotationMomentumChangeInterval);
|
|
clearInterval(rotationMomentumChangeInterval);
|
|
clearInterval(forwardSpeedChangeInterval);
|
|
clearInterval(forwardSpeedChangeInterval);
|
|
setTimeout(() => {
|
|
isGameRunning = false;
|
|
clearInterval(clock);
|
|
document.getElementById("gameOver").classList.remove("hidden");
|
|
document.getElementById("finalScore").innerHTML = score;
|
|
document.getElementById("restart").tabIndex = 0;
|
|
document.getElementById("restart").focus();
|
|
}, explosionDuration * 2);
|
|
}
|
|
};
|
|
|
|
const startGame = () => {
|
|
// Hide splash screen
|
|
document.getElementById("splash").classList.add("hidden");
|
|
document.getElementById("start").tabIndex = -1;
|
|
document.getElementById("start").blur();
|
|
document.getElementById("gameOver").classList.add("hidden");
|
|
document.getElementById("restart").tabIndex = -1;
|
|
document.getElementById("restart").blur();
|
|
|
|
// Reset score
|
|
score = 0;
|
|
document.getElementById("score").innerHTML = "0";
|
|
|
|
// Reset ship
|
|
starship = {
|
|
position: {
|
|
x: ctx.canvas.width / 2,
|
|
y: ctx.canvas.height / 2,
|
|
},
|
|
rotation: 90,
|
|
driftDirection: 0,
|
|
driftSpeed: 0,
|
|
rotationMomentum: 0,
|
|
forwardSpeed: 0,
|
|
canFire: true,
|
|
exploded: false,
|
|
};
|
|
|
|
// Reset torpedoes
|
|
torpedoes = [];
|
|
|
|
// Reset asteroids
|
|
asteroids = [
|
|
{
|
|
size: 2,
|
|
position: getRandomAsteroidPosition(starship),
|
|
direction: getRandomDirection(),
|
|
rotation: getRandomDirection(),
|
|
exploded: false,
|
|
},
|
|
];
|
|
explosions = [];
|
|
|
|
// Start clock
|
|
isGameRunning = true;
|
|
isGamePaused = false;
|
|
gameOver = false;
|
|
clock = setInterval(tick, updateFrequency);
|
|
requestAnimationFrame(draw);
|
|
};
|
|
|
|
// Set up splash screen
|
|
document.getElementById("start").focus();
|
|
document.getElementById("start").addEventListener("click", (event) => {
|
|
event.stopPropagation();
|
|
startGame();
|
|
});
|
|
|
|
// Set up game over screen
|
|
document.getElementById("restart").addEventListener("click", (event) => {
|
|
event.stopPropagation();
|
|
startGame();
|
|
});
|