02/12/2023

This commit is contained in:
2023-12-02 19:45:57 +01:00
commit b516324a8d
73 changed files with 3873 additions and 0 deletions

21
space-drifter/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Bence A. Tóth
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

13
space-drifter/README.md Normal file
View File

@@ -0,0 +1,13 @@
# Space Drifter :rocket:
A pretty darn difficult asteroid shooter game with realistic inertia and unconventional controls.
## Play the game
You can [play the game here](https://bence-toth.github.io/space-drifter/).
Have fun!
## License
[MIT](LICENSE). Do what you will.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 73 KiB

View File

@@ -0,0 +1,4 @@
<svg height="10" width="10" version="1.1" xmlns="http://www.w3.org/2000/svg">
<circle cx="5" cy="5" r="5" fill="#00ccff" />
<circle cx="5" cy="5" r="2.5" fill="#4ddbff" />
</svg>

After

Width:  |  Height:  |  Size: 182 B

964
space-drifter/game.js Normal file
View File

@@ -0,0 +1,964 @@
// 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();
});

112
space-drifter/index.html Normal file
View File

@@ -0,0 +1,112 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Space Drifter</title>
<link rel="stylesheet" href="styles.css" />
<link rel="icon" href="assets/starship.svg" />
<meta property="og:title" content="Space Drifter" />
<meta property="og:type" content="article" />
<meta
property="og:image"
content="https://raw.githubusercontent.com/bence-toth/space-drifter/main/assets/space-drifter-cover.jpg"
/>
<meta
property="og:url"
content="https://bence-toth.github.io/space-drifter/"
/>
<meta name="twitter:card" content="summary_large_image" />
<meta
property="og:description"
content="A pretty darn difficult asteroid shooter game with realistic inertia and unconventional controls."
/>
<meta property="og:site_name" content="Space Drifter" />
<meta name="twitter:image:alt" content="Space Drifter" />
<meta
name="description"
content="A pretty darn difficult asteroid shooter game with realistic inertia and unconventional controls."
/>
<meta name="keywords" content="Space Shooter, Game" />
<meta name="author" content="Bence A. Tóth" />
</head>
<body>
<div id="splash">
<div class="innerWrapper">
<h1>Space Drifter</h1>
<button id="start">
<span>Start game</span>
<div class="line top"></div>
<div class="line right"></div>
<div class="line bottom"></div>
<div class="line left"></div>
</button>
<div class="controls">
<h3>Controls</h3>
<dl>
<dt>Fire thrusters</dt>
<dd>
<div class="wasd">
<kbd>W</kbd>
<kbd>A</kbd>
<kbd>S</kbd>
<kbd>D</kbd>
</div>
<div class="wasd">
<kbd></kbd>
<kbd></kbd>
<kbd></kbd>
<kbd></kbd>
</div>
</dd>
<dt>Launch torpedo</dt>
<dd>
<kbd>Space</kbd>
<kbd>Mouse click</kbd>
</dd>
<dt>Pause/Resume</dt>
<dd>
<kbd>P</kbd>
</dd>
</dl>
</div>
<footer>
<nav>
by
<a
href="https://github.com/bence-toth/space-drifter"
target="_blank"
rel="noopener"
>Bence A. Tóth</a
>
</nav>
</footer>
</div>
</div>
<div id="paused" class="hidden">
<div class="innerWrapper">
<h2>Game paused</h2>
<p>Press <kbd>P</kbd> to resume</p>
</div>
</div>
<div id="gameOver" class="hidden">
<div class="innerWrapper">
<h2>Game Over</h2>
<h3>Your score: <span id="finalScore"></span></h3>
<button id="restart" tabindex="-1">
<span>Restart game</span>
<div class="line top"></div>
<div class="line right"></div>
<div class="line bottom"></div>
<div class="line left"></div>
</button>
</div>
</div>
<div class="canvasWrapper">
<canvas id="game-canvas"></canvas>
<div id="score">0</div>
</div>
<script src="game.js"></script>
</body>
</html>

297
space-drifter/styles.css Normal file
View File

@@ -0,0 +1,297 @@
@import url("https://fonts.googleapis.com/css2?family=Audiowide&display=swap");
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Audiowide", serif;
line-height: 1;
}
html,
body {
width: 100%;
height: 100%;
}
body {
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background-color: hsl(0, 0%, 5%);
color: hsl(0, 0%, 95%);
user-select: none;
}
.canvasWrapper {
max-width: 95%;
max-height: 95%;
aspect-ratio: 16 / 9;
border: 2px solid hsl(0, 0%, 10%);
background-color: black;
position: relative;
display: flex;
background-image: url(assets/stars.png);
background-position: center center;
background-size: 50vmin;
}
canvas {
width: 100%;
height: 100%;
}
#score {
position: absolute;
top: 0;
right: 0;
color: white;
padding: 1.5vmin;
font-size: 3vmin;
line-height: 1;
}
#splash,
#gameOver,
#paused {
position: fixed;
width: 100%;
height: 100%;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background-image: url(assets/stars.png);
background-position: center center;
background-size: 50vmin;
z-index: 1;
transition: opacity 0.5s;
}
#splash.hidden,
#gameOver.hidden,
#paused.hidden {
opacity: 0;
pointer-events: none;
}
.innerWrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3vmin;
}
#splash h1,
#gameOver h2,
#paused h2 {
font-size: 10vmin;
}
#paused h2 {
animation: twinkle 2s linear infinite;
}
@keyframes twinkle {
from {
opacity: 1;
}
45% {
opacity: 0.3333;
}
90% {
opacity: 1;
}
to {
opacity: 1;
}
}
#gameOver h3 {
font-size: 3vmin;
}
#paused p {
font-size: 3vmin;
}
button {
padding: 1.5vmin;
background-color: transparent;
color: inherit;
font-size: 1.5vmin;
outline: 0;
border: 2px solid hsl(0, 0%, 25%);
cursor: pointer;
position: relative;
margin-top: 1em;
}
button span {
opacity: 0.5;
transition: opacity 0.5s;
text-transform: uppercase;
}
button .line {
position: absolute;
background-color: white;
transition: transform 1s;
}
button .top,
button .bottom {
width: calc(100% + 4px);
height: 2px;
transform: scaleX(0);
}
button .left,
button .right {
width: 2px;
height: calc(100% + 4px);
transform: scaleY(0);
}
button .top {
top: -2px;
left: -2px;
transform-origin: left;
}
button .bottom {
bottom: -2px;
right: -2px;
transform-origin: right;
}
button .left {
left: -2px;
bottom: -2px;
transform-origin: bottom;
}
button .right {
right: -2px;
top: -2px;
transform-origin: top;
}
button:is(:hover, :focus, :active) .top,
button:is(:hover, :focus, :active) .bottom {
transform: scaleX(1);
}
button:is(:hover, :focus, :active) .left,
button:is(:hover, :focus, :active) .right {
transform: scaleY(1);
}
button:is(:hover, :focus, :active) span {
opacity: 1;
}
button:is(:hover, :focus, :active) span,
button:is(:hover, :focus, :active) .line {
transition-delay: 0.25s;
}
.controls {
margin-top: 5vmin;
border: 1px solid hsl(0, 0%, 50%);
background-color: hsl(0, 0%, 7.5%);
padding: 2vmin 4vmin 3vmin;
}
.controls h3 {
margin-bottom: 3vmin;
text-align: center;
font-size: 3vmin;
}
dl {
display: grid;
grid-template-columns: 1fr auto;
font-size: 2vmin;
gap: 3vmin 1.5vmin;
}
dt,
dd {
display: flex;
align-items: center;
gap: 1.5vmin;
}
dt {
justify-content: flex-start;
}
dd {
justify-content: center;
}
.wasd {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 0.5vmin;
grid-template-areas:
". w ."
"a s d";
}
.wasd kbd:nth-child(1) {
grid-area: w;
}
.wasd kbd:nth-child(2) {
grid-area: a;
}
.wasd kbd:nth-child(3) {
grid-area: s;
}
.wasd kbd:nth-child(4) {
grid-area: d;
}
kbd {
display: inline-block;
vertical-align: middle;
white-space: nowrap;
line-height: 1;
padding: 0.2em 0.4em;
font-size: 0.9em;
color: hsl(0, 0%, 20%);
background-color: hsl(0, 0%, 90%);
border: 1px solid hsl(0, 0%, 80%);
border-radius: 0.25em;
box-shadow: 0 0.05em 0 hsla(0, 0%, 0%, 20%),
0 0.1em 0 hsla(0, 0%, 100%, 50%) inset;
}
#splash footer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
text-align: center;
padding: 2vmin;
color: hsl(0, 0%, 60%);
}
a {
color: inherit;
outline: 0;
transition: color 0.5s;
}
a:hover,
a:active,
a:focus {
color: hsl(0, 0%, 100%);
}