02/12/2023
21
space-drifter/LICENSE
Normal 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
@@ -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.
|
121
space-drifter/assets/asteroid-big.svg
Normal file
After Width: | Height: | Size: 18 KiB |
122
space-drifter/assets/asteroid-medium.svg
Normal file
After Width: | Height: | Size: 18 KiB |
122
space-drifter/assets/asteroid-small.svg
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
space-drifter/assets/space-drifter-cover.jpg
Normal file
After Width: | Height: | Size: 79 KiB |
BIN
space-drifter/assets/stars.png
Normal file
After Width: | Height: | Size: 75 KiB |
53
space-drifter/assets/starship.svg
Normal file
After Width: | Height: | Size: 73 KiB |
4
space-drifter/assets/torpedo.svg
Normal 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
@@ -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
@@ -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
@@ -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%);
|
||||
}
|