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%);
 | 
			
		||||
}
 | 
			
		||||