-
Notifications
You must be signed in to change notification settings - Fork 2
/
missile Command.html
351 lines (302 loc) · 9.8 KB
/
missile Command.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
<!DOCTYPE html>
<html>
<head>
<title>Missile Command</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>