Skip to content

Instantly share code, notes, and snippets.

@straker
Last active September 11, 2024 17:17
Show Gist options
  • Save straker/afc4e2a30b6df772a5f9f6ef01751d41 to your computer and use it in GitHub Desktop.
Save straker/afc4e2a30b6df772a5f9f6ef01751d41 to your computer and use it in GitHub Desktop.
Basic Missile Command HTML and JavaScript Game

Basic Missile Command HTML and JavaScript Game

This is a basic implementation of the Atari Missile Command game, but it's missing a few things intentionally and they're left as further exploration for the reader.

Further Exploration

  • Score
    • When a missile explodes (not a counter-missile), the score should increase by 25
    • At the end of the level the player should earn 5 bonus points for each missile still in a silo and 100 bonus points for each city still alive
    • See https://atariage.com/manual_html_page.php?SoftwareLabelID=306 Scoring section
  • Bonus Targets
    • In each level there are bonus targets that move across the screen that award the player with additional points
  • Limit Counter-missile fire rate
    • The player should only be able to fire 3 counter-missiles at a time
  • Limit Missile city damage

Important note: I will answer questions about the code but will not add more features or answer questions about adding more features. This series is meant to give a basic outline of the game but nothing more.

License

(CC0 1.0 Universal) You're free to use this game and code in any project, personal or commercial. There's no need to ask permission before using these. Giving attribution is not required, but appreciated.

Other Basic Games

Support

Basic HTML Games are made possible by users like you. When you become a Patron, you get access to behind the scenes development logs, the ability to vote on which games I work on next, and early access to the next Basic HTML Game.

Top Patrons

  • Karar Al-Remahy
  • UnbrandedTech
  • Innkeeper Games
  • Nezteb
<!DOCTYPE html>
<html>
<head>
<title>Basic Missile Command HTML Game</title>
<meta charset="UTF-8">
<style>
html, body {
height: 100%;
margin: 0;
}
body {
background: black;
display: flex;
align-items: center;
justify-content: center;
}
canvas {
cursor: crosshair;
border: 1px solid white;
}
</style>
</head>
<body>
<canvas width="800" height="550" id="game"></canvas>
<script>
const canvas = document.getElementById('game');
const context = canvas.getContext('2d');
const groundY = 500; // y position of where the ground starts
const cityWidth = 45; // how wide a city rect is
const cityHeight = 25; // how tall a city rect is
const cityY = groundY - cityHeight; // y position of the city
const siloY = groundY - 30; // y position of the top of the silo
const missileSize = 4; // the radius/size of a missile
const missileSpeed = 1; // how fast a missile moves
const counterMissileSpeed = 15; // how fast a counter-missile moves
// information about each missile
let missiles = [];
let counterMissiles = [];
// information about each explosion
let explosions = [];
// how many missiles to spawn at each interval of the level (in this
// case spawn 4 missiles at the start of level 1 and 4 more missiles
// at the next interval of level 1)
const levels = [ [4, 4] ];
let currLevel = 0;
let currInterval = 0;
// the x/y position of all cities and if the city is currently alive
let cities = [
{ x: 140, y: cityY, alive: true },
{ x: 220, y: cityY, alive: true },
{ x: 300, y: cityY, alive: true },
{ x: 500, y: cityY, alive: true },
{ x: 580, y: cityY, alive: true },
{ x: 660, y: cityY, alive: true }
];
// the x position of each of the 3 silos
const siloPos = [ 55, canvas.width / 2, 745 ];
// the x/y position of each silo, the number of missiles left, and if
// it is still alive
let silos = [
{ x: siloPos[0], y: siloY, missiles: 10, alive: true },
{ x: siloPos[1], y: siloY, missiles: 10, alive: true },
{ x: siloPos[2], y: siloY, missiles: 10, alive: true }
];
// the x/y position of each missile spawn point. missiles spawn
// directly above each city and silo plus the two edges
const missileSpawns = cities
.concat(silos)
.concat([{ x: 0, y: 0 }, { x: canvas.width, y: 0 }])
.map(pos => ({ x: pos.x, y: 0 }));
// return a random integer between min (inclusive) and max (inclusive)
// @see https://stackoverflow.com/a/1527820/2124254
function randInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// get the angle between two points
function angleBetweenPoints(source, target) {
// atan2 returns the counter-clockwise angle in respect to the
// x-axis, but the canvas rotation system is based on the y-axis
// (rotation of 0 = up).
// so we need to add a quarter rotation to return a
// counter-clockwise rotation in respect to the y-axis
return Math.atan2(target.y - source.y, target.x - source.x) + Math.PI / 2;
}
// distance between two points
function distance(source, target) {
return Math.hypot(source.x - target.x, source.y - target.y);
}
// spawn a missile by choosing a spawn point and a target.
// a missile can target any city or silo
function spawnMissile() {
const targets = cities.concat(silos);
const randSpawn = randInt(0, missileSpawns.length - 1);
const randTarget = randInt(0, targets.length - 1);
const start = missileSpawns[randSpawn];
const target = targets[randTarget];
const angle = angleBetweenPoints(start, target);
missiles.push({
start, // where the missile started
target, // where the missile is going
pos: { x: start.x, y: start.y }, // current position
alive: true, // if we should still draw the missile
// used to update the position every frame
dx: missileSpeed * Math.sin(angle),
dy: missileSpeed * -Math.cos(angle)
});
}
// game loop
// start at -2 seconds (time is in milliseconds) to give the player 1
// second before the missiles start
let lastTime = -2000;
function loop(time) {
requestAnimationFrame(loop);
context.clearRect(0,0,canvas.width,canvas.height);
// spawn missiles every interval of 3 seconds (if the level allows
// more missiles)
if (time - lastTime > 3000 && currInterval < levels[currLevel].length) {
for (let i = 0; i < levels[currLevel][currInterval]; i++) {
spawnMissile();
}
currInterval++;
lastTime = time;
}
// draw cities
context.fillStyle = 'blue';
cities.forEach(city => {
// center the city on the x position
context.fillRect(city.x - cityWidth / 2, city.y, cityWidth, cityHeight);
});
// draw ground and silos
context.fillStyle = 'yellow';
context.beginPath();
context.moveTo(0, canvas.height);
context.lineTo(0, groundY);
// draw each silo hill
siloPos.forEach(x => {
context.lineTo(x - 40, groundY);
context.lineTo(x - 20, siloY);
context.lineTo(x + 20, siloY);
context.lineTo(x + 40, groundY);
});
context.lineTo(canvas.width, groundY);
context.lineTo(canvas.width, canvas.height);
context.fill();
// draw the number of counter-missiles each silo
context.fillStyle = 'black';
silos.forEach(silo => {
// draw missiles in a triangular shape by incrementing how many
// missiles we can draw per row
let missilesPerRow = 1;
let count = 0;
let x = silo.x;
let y = silo.y + 5;
for (let i = 0; i < silo.missiles; i++) {
context.fillRect(x, y, 4, 10);
x += 12;
if (++count === missilesPerRow) {
x = silo.x - 6 * count;
missilesPerRow++;
y += 7;
count = 0;
}
}
});
// update and draw missiles
context.strokeStyle = 'red';
context.lineWidth = 2;
// update color based on time so it "blinks"
// by dividing by a number and seeing if it's odd or even we can
// change the speed of the blinking
context.fillStyle = 'white';
if (Math.round(time / 2) % 2 === 0) {
context.fillStyle = 'black';
}
missiles.forEach(missile => {
missile.pos.x += missile.dx;
missile.pos.y += missile.dy;
// check if the missile hit an explosion by doing a circle-circle
// collision check
explosions.forEach(explosion => {
const dist = distance(explosion, missile.pos);
if (dist < missileSize + explosion.size) {
missile.alive = false;
}
});
// if missile is close the the target we blow it up
const dist = distance(missile.pos, missile.target);
if (dist < missileSpeed) {
missile.alive = false;
missile.target.alive = false;
}
if (missile.alive) {
context.beginPath();
context.moveTo(missile.start.x, missile.start.y);
context.lineTo(missile.pos.x, missile.pos.y);
context.stroke();
// center the head of the missile to the x/y position
context.fillRect(missile.pos.x - missileSize / 2, missile.pos.y - missileSize / 2, missileSize, missileSize);
}
// a dead missile spawns an explosion
else {
explosions.push({
x: missile.pos.x,
y: missile.pos.y,
size: 2,
dir: 1,
alive: true
});
}
});
// update and draw counter missiles
context.strokeStyle = 'blue';
context.fillStyle = 'white';
counterMissiles.forEach(missile => {
missile.pos.x += missile.dx;
missile.pos.y += missile.dy;
// if missile is close the the target we blow it up
const dist = distance(missile.pos, missile.target);
if (dist < counterMissileSpeed) {
missile.alive = false;
explosions.push({
x: missile.pos.x,
y: missile.pos.y,
size: 2,
dir: 1,
alive: true
});
}
else {
context.beginPath();
context.moveTo(missile.start.x, missile.start.y);
context.lineTo(missile.pos.x, missile.pos.y);
context.stroke();
context.fillRect(missile.pos.x - 2, missile.pos.y - 2, 4, 4);
}
});
// update and draw explosions
explosions.forEach(explosion => {
explosion.size += 0.35 * explosion.dir;
// change the direction of the explosion to wane
if (explosion.size > 30) {
explosion.dir = -1;
}
// remove the explosion
if (explosion.size <= 0) {
explosion.alive = false;
}
else {
context.fillStyle = 'white';
if (Math.round(time / 3) % 2 === 0) {
context.fillStyle = 'blue';
}
context.beginPath();
context.arc(explosion.x, explosion.y, explosion.size, 0, 2 * Math.PI);
context.fill();
}
});
// remove dead missiles, explosions, cities, and silos
missiles = missiles.filter(missile => missile.alive);
counterMissiles = counterMissiles.filter(missile => missile.alive);
explosions = explosions.filter(explosion => explosion.alive);
cities = cities.filter(city => city.alive);
silos = silos.filter(silo => silo.alive);
}
// listen to mouse events to fire counter-missiles
canvas.addEventListener('click', e => {
// get the x/y position of the mouse pointer by subtracting the x/y
// position of the canvas element from the x/y position of the
// pointer
const x = e.clientX - e.target.offsetLeft;
const y = e.clientY - e.target.offsetTop;
// determine which silo is closest to the pointer and fire a
// counter-missile from it
let launchSilo = null;
let siloDistance = Infinity; // start at the largest number
silos.forEach(silo => {
const dist = distance({ x, y }, silo);
if (dist < siloDistance && silo.missiles) {
siloDistance = dist;
launchSilo = silo;
}
});
if (launchSilo) {
const start = { x: launchSilo.x, y: launchSilo.y };
const target = { x, y };
const angle = angleBetweenPoints(start, target);
launchSilo.missiles--;
counterMissiles.push({
start,
target,
pos: { x: launchSilo.x, y: launchSilo. y},
dx: counterMissileSpeed * Math.sin(angle),
dy: counterMissileSpeed * -Math.cos(angle),
alive: true
});
}
});
// start the game
requestAnimationFrame(loop);
</script>
</body>
</html>
@max1444
Copy link

max1444 commented Mar 31, 2022

hi can you please make a 2048 game in html thank you if you cant could you add levels to break out

@straker
Copy link
Author

straker commented Apr 1, 2022

@max1444 I can add 2048 onto the next poll.

@max1444
Copy link

max1444 commented Apr 1, 2022

@max1444 I can add 2048 onto the next poll.

@straker thank you im looking forward to it

@max1444
Copy link

max1444 commented Apr 1, 2022

@straker what poll

@max1444
Copy link

max1444 commented Apr 1, 2022

@straker i am about to leave somwhere so please notify me when you finish

@max1444
Copy link

max1444 commented Apr 1, 2022

where is the poll @straker

@straker
Copy link
Author

straker commented Apr 1, 2022

@max1444 Polls are available for Patrons.

@max1444
Copy link

max1444 commented Apr 1, 2022

@straker i am not a patron

@max1444
Copy link

max1444 commented Apr 1, 2022

also i don’t have any money i am still in 6th grade

@Henscantfly
Copy link

Is there a level swichter button

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment