├── .gitignore
├── .prettierrc
├── audio.js
├── audio
├── backgroundMusic.wav
├── bomb.mp3
├── bonus.mp3
├── enemyShoot.wav
├── explode.wav
├── gameOver.mp3
├── select.mp3
├── shoot.wav
└── start.mp3
├── img
├── button.png
├── invader.png
├── spaceship.png
└── startScreenBackground.png
├── index.html
├── index.js
└── js
├── classes
├── Bomb.js
├── Grid.js
├── Invader.js
├── InvaderProjectile.js
├── Particle.js
├── Player.js
└── Projectile.js
└── util.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .ds_store
2 | todo.todo
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "semi": false,
4 | "trailingComma": "none",
5 | "overrides": [
6 | {
7 | "files": "*.html",
8 | "options": {
9 | "parser": "html"
10 | }
11 | }
12 | ]
13 | }
--------------------------------------------------------------------------------
/audio.js:
--------------------------------------------------------------------------------
1 | Howler.volume(0.5)
2 | const audio = {
3 | backgroundMusic: new Howl({
4 | src: './audio/backgroundMusic.wav',
5 | loop: true
6 | }),
7 | bomb: new Howl({
8 | src: './audio/bomb.mp3'
9 | }),
10 | bonus: new Howl({
11 | src: './audio/bonus.mp3',
12 | volume: 0.8
13 | }),
14 | enemyShoot: new Howl({
15 | src: './audio/enemyShoot.wav'
16 | }),
17 | explode: new Howl({
18 | src: './audio/explode.wav'
19 | }),
20 | gameOver: new Howl({
21 | src: './audio/gameOver.mp3'
22 | }),
23 | select: new Howl({
24 | src: './audio/select.mp3'
25 | }),
26 | shoot: new Howl({
27 | src: './audio/shoot.wav'
28 | }),
29 | start: new Howl({
30 | src: './audio/start.mp3'
31 | })
32 | }
33 |
--------------------------------------------------------------------------------
/audio/backgroundMusic.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chriscourses/space-invaders/dc011b2d665b15a9854e6c9457fd4218e98efe13/audio/backgroundMusic.wav
--------------------------------------------------------------------------------
/audio/bomb.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chriscourses/space-invaders/dc011b2d665b15a9854e6c9457fd4218e98efe13/audio/bomb.mp3
--------------------------------------------------------------------------------
/audio/bonus.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chriscourses/space-invaders/dc011b2d665b15a9854e6c9457fd4218e98efe13/audio/bonus.mp3
--------------------------------------------------------------------------------
/audio/enemyShoot.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chriscourses/space-invaders/dc011b2d665b15a9854e6c9457fd4218e98efe13/audio/enemyShoot.wav
--------------------------------------------------------------------------------
/audio/explode.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chriscourses/space-invaders/dc011b2d665b15a9854e6c9457fd4218e98efe13/audio/explode.wav
--------------------------------------------------------------------------------
/audio/gameOver.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chriscourses/space-invaders/dc011b2d665b15a9854e6c9457fd4218e98efe13/audio/gameOver.mp3
--------------------------------------------------------------------------------
/audio/select.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chriscourses/space-invaders/dc011b2d665b15a9854e6c9457fd4218e98efe13/audio/select.mp3
--------------------------------------------------------------------------------
/audio/shoot.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chriscourses/space-invaders/dc011b2d665b15a9854e6c9457fd4218e98efe13/audio/shoot.wav
--------------------------------------------------------------------------------
/audio/start.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chriscourses/space-invaders/dc011b2d665b15a9854e6c9457fd4218e98efe13/audio/start.mp3
--------------------------------------------------------------------------------
/img/button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chriscourses/space-invaders/dc011b2d665b15a9854e6c9457fd4218e98efe13/img/button.png
--------------------------------------------------------------------------------
/img/invader.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chriscourses/space-invaders/dc011b2d665b15a9854e6c9457fd4218e98efe13/img/invader.png
--------------------------------------------------------------------------------
/img/spaceship.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chriscourses/space-invaders/dc011b2d665b15a9854e6c9457fd4218e98efe13/img/spaceship.png
--------------------------------------------------------------------------------
/img/startScreenBackground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chriscourses/space-invaders/dc011b2d665b15a9854e6c9457fd4218e98efe13/img/startScreenBackground.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
26 | Score: 0
27 |
28 |
29 |
30 |
47 |
48 |
Space Invaders
49 |
50 |
51 |
67 |
68 |
69 |
70 |
71 |
88 |
89 |
Game Over
90 |
91 | 300
92 |
93 |
Points
94 |
95 |
96 |
112 |
113 |
114 |
120 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const scoreEl = document.querySelector('#scoreEl')
2 | const canvas = document.querySelector('canvas')
3 | const c = canvas.getContext('2d')
4 |
5 | canvas.width = 1024
6 | canvas.height = 576
7 |
8 | let player = new Player()
9 | let projectiles = []
10 | let grids = []
11 | let invaderProjectiles = []
12 | let particles = []
13 | let bombs = []
14 | let powerUps = []
15 |
16 | let keys = {
17 | a: {
18 | pressed: false
19 | },
20 | d: {
21 | pressed: false
22 | },
23 | space: {
24 | pressed: false
25 | }
26 | }
27 |
28 | let frames = 0
29 | let randomInterval = Math.floor(Math.random() * 500 + 500)
30 | let game = {
31 | over: false,
32 | active: true
33 | }
34 | let score = 0
35 |
36 | let spawnBuffer = 500
37 | let fps = 60
38 | let fpsInterval = 1000 / fps
39 | let msPrev = window.performance.now()
40 |
41 | function init() {
42 | player = new Player()
43 | projectiles = []
44 | grids = []
45 | invaderProjectiles = []
46 | particles = []
47 | bombs = []
48 | powerUps = []
49 |
50 | keys = {
51 | a: {
52 | pressed: false
53 | },
54 | d: {
55 | pressed: false
56 | },
57 | space: {
58 | pressed: false
59 | }
60 | }
61 |
62 | frames = 0
63 | randomInterval = Math.floor(Math.random() * 500 + 500)
64 | game = {
65 | over: false,
66 | active: true
67 | }
68 | score = 0
69 | document.querySelector('#finalScore').innerHTML = score
70 | document.querySelector('#scoreEl').innerHTML = score
71 |
72 | for (let i = 0; i < 100; i++) {
73 | particles.push(
74 | new Particle({
75 | position: {
76 | x: Math.random() * canvas.width,
77 | y: Math.random() * canvas.height
78 | },
79 | velocity: {
80 | x: 0,
81 | y: 0.3
82 | },
83 | radius: Math.random() * 2,
84 | color: 'white'
85 | })
86 | )
87 | }
88 | }
89 |
90 | function endGame() {
91 | console.log('you lose')
92 | audio.gameOver.play()
93 |
94 | // Makes player disappear
95 | setTimeout(() => {
96 | player.opacity = 0
97 | game.over = true
98 | }, 0)
99 |
100 | // stops game altogether
101 | setTimeout(() => {
102 | game.active = false
103 | document.querySelector('#restartScreen').style.display = 'flex'
104 | document.querySelector('#finalScore').innerHTML = score
105 | }, 2000)
106 |
107 | createParticles({
108 | object: player,
109 | color: 'white',
110 | fades: true
111 | })
112 | }
113 |
114 | function animate() {
115 | if (!game.active) return
116 | requestAnimationFrame(animate)
117 |
118 | const msNow = window.performance.now()
119 | const elapsed = msNow - msPrev
120 |
121 | if (elapsed < fpsInterval) return
122 |
123 | msPrev = msNow - (elapsed % fpsInterval) // 3.34
124 |
125 | c.fillStyle = 'black'
126 | c.fillRect(0, 0, canvas.width, canvas.height)
127 |
128 | for (let i = powerUps.length - 1; i >= 0; i--) {
129 | const powerUp = powerUps[i]
130 |
131 | if (powerUp.position.x - powerUp.radius >= canvas.width)
132 | powerUps.splice(i, 1)
133 | else powerUp.update()
134 | }
135 |
136 | // spawn powerups
137 | if (frames % 500 === 0) {
138 | powerUps.push(
139 | new PowerUp({
140 | position: {
141 | x: 0,
142 | y: Math.random() * 300 + 15
143 | },
144 | velocity: {
145 | x: 5,
146 | y: 0
147 | }
148 | })
149 | )
150 | }
151 |
152 | // spawn bombs
153 | if (frames % 200 === 0 && bombs.length < 3) {
154 | bombs.push(
155 | new Bomb({
156 | position: {
157 | x: randomBetween(Bomb.radius, canvas.width - Bomb.radius),
158 | y: randomBetween(Bomb.radius, canvas.height - Bomb.radius)
159 | },
160 | velocity: {
161 | x: (Math.random() - 0.5) * 6,
162 | y: (Math.random() - 0.5) * 6
163 | }
164 | })
165 | )
166 | }
167 |
168 | for (let i = bombs.length - 1; i >= 0; i--) {
169 | const bomb = bombs[i]
170 |
171 | if (bomb.opacity <= 0) {
172 | bombs.splice(i, 1)
173 | } else bomb.update()
174 | }
175 |
176 | player.update()
177 |
178 | for (let i = player.particles.length - 1; i >= 0; i--) {
179 | const particle = player.particles[i]
180 | particle.update()
181 |
182 | if (particle.opacity === 0) player.particles[i].splice(i, 1)
183 | }
184 |
185 | particles.forEach((particle, i) => {
186 | if (particle.position.y - particle.radius >= canvas.height) {
187 | particle.position.x = Math.random() * canvas.width
188 | particle.position.y = -particle.radius
189 | }
190 |
191 | if (particle.opacity <= 0) {
192 | setTimeout(() => {
193 | particles.splice(i, 1)
194 | }, 0)
195 | } else {
196 | particle.update()
197 | }
198 | })
199 |
200 | invaderProjectiles.forEach((invaderProjectile, index) => {
201 | if (
202 | invaderProjectile.position.y + invaderProjectile.height >=
203 | canvas.height
204 | ) {
205 | setTimeout(() => {
206 | invaderProjectiles.splice(index, 1)
207 | }, 0)
208 | } else invaderProjectile.update()
209 |
210 | // projectile hits player
211 | if (
212 | rectangularCollision({
213 | rectangle1: invaderProjectile,
214 | rectangle2: player
215 | })
216 | ) {
217 | invaderProjectiles.splice(index, 1)
218 | endGame()
219 | }
220 | })
221 |
222 | for (let i = projectiles.length - 1; i >= 0; i--) {
223 | const projectile = projectiles[i]
224 |
225 | for (let j = bombs.length - 1; j >= 0; j--) {
226 | const bomb = bombs[j]
227 |
228 | // if projectile touches bomb, remove projectile
229 | if (
230 | Math.hypot(
231 | projectile.position.x - bomb.position.x,
232 | projectile.position.y - bomb.position.y
233 | ) <
234 | projectile.radius + bomb.radius &&
235 | !bomb.active
236 | ) {
237 | projectiles.splice(i, 1)
238 | bomb.explode()
239 | }
240 | }
241 |
242 | for (let j = powerUps.length - 1; j >= 0; j--) {
243 | const powerUp = powerUps[j]
244 |
245 | // if projectile touches bomb, remove projectile
246 | if (
247 | Math.hypot(
248 | projectile.position.x - powerUp.position.x,
249 | projectile.position.y - powerUp.position.y
250 | ) <
251 | projectile.radius + powerUp.radius
252 | ) {
253 | projectiles.splice(i, 1)
254 | powerUps.splice(j, 1)
255 | player.powerUp = 'MachineGun'
256 | console.log('powerup started')
257 | audio.bonus.play()
258 |
259 | setTimeout(() => {
260 | player.powerUp = null
261 | console.log('powerup ended')
262 | }, 5000)
263 | }
264 | }
265 |
266 | if (projectile.position.y + projectile.radius <= 0) {
267 | projectiles.splice(i, 1)
268 | } else {
269 | projectile.update()
270 | }
271 | }
272 |
273 | grids.forEach((grid, gridIndex) => {
274 | grid.update()
275 |
276 | // spawn projectiles
277 | if (frames % 100 === 0 && grid.invaders.length > 0) {
278 | grid.invaders[Math.floor(Math.random() * grid.invaders.length)].shoot(
279 | invaderProjectiles
280 | )
281 | }
282 |
283 | for (let i = grid.invaders.length - 1; i >= 0; i--) {
284 | const invader = grid.invaders[i]
285 | invader.update({ velocity: grid.velocity })
286 |
287 | for (let j = bombs.length - 1; j >= 0; j--) {
288 | const bomb = bombs[j]
289 |
290 | const invaderRadius = 15
291 |
292 | // if bomb touches invader, remove invader
293 | if (
294 | Math.hypot(
295 | invader.position.x - bomb.position.x,
296 | invader.position.y - bomb.position.y
297 | ) <
298 | invaderRadius + bomb.radius &&
299 | bomb.active
300 | ) {
301 | score += 50
302 | scoreEl.innerHTML = score
303 |
304 | grid.invaders.splice(i, 1)
305 | createScoreLabel({
306 | object: invader,
307 | score: 50
308 | })
309 |
310 | createParticles({
311 | object: invader,
312 | fades: true
313 | })
314 | }
315 | }
316 |
317 | // projectiles hit enemy
318 | projectiles.forEach((projectile, j) => {
319 | if (
320 | projectile.position.y - projectile.radius <=
321 | invader.position.y + invader.height &&
322 | projectile.position.x + projectile.radius >= invader.position.x &&
323 | projectile.position.x - projectile.radius <=
324 | invader.position.x + invader.width &&
325 | projectile.position.y + projectile.radius >= invader.position.y
326 | ) {
327 | setTimeout(() => {
328 | const invaderFound = grid.invaders.find(
329 | (invader2) => invader2 === invader
330 | )
331 | const projectileFound = projectiles.find(
332 | (projectile2) => projectile2 === projectile
333 | )
334 |
335 | // remove invader and projectile
336 | if (invaderFound && projectileFound) {
337 | score += 100
338 | scoreEl.innerHTML = score
339 |
340 | // dynamic score labels
341 | createScoreLabel({
342 | object: invader
343 | })
344 |
345 | createParticles({
346 | object: invader,
347 | fades: true
348 | })
349 |
350 | // singular projectile hits an enemy
351 | audio.explode.play()
352 | grid.invaders.splice(i, 1)
353 | projectiles.splice(j, 1)
354 |
355 | if (grid.invaders.length > 0) {
356 | const firstInvader = grid.invaders[0]
357 | const lastInvader = grid.invaders[grid.invaders.length - 1]
358 |
359 | grid.width =
360 | lastInvader.position.x -
361 | firstInvader.position.x +
362 | lastInvader.width
363 | grid.position.x = firstInvader.position.x
364 | } else {
365 | grids.splice(gridIndex, 1)
366 | }
367 | }
368 | }, 0)
369 | }
370 | })
371 |
372 | // remove player if invaders touch it
373 | if (
374 | rectangularCollision({
375 | rectangle1: invader,
376 | rectangle2: player
377 | }) &&
378 | !game.over
379 | )
380 | endGame()
381 | } // end looping over grid.invaders
382 | })
383 |
384 | if (keys.a.pressed && player.position.x >= 0) {
385 | player.velocity.x = -7
386 | player.rotation = -0.15
387 | } else if (
388 | keys.d.pressed &&
389 | player.position.x + player.width <= canvas.width
390 | ) {
391 | player.velocity.x = 7
392 | player.rotation = 0.15
393 | } else {
394 | player.velocity.x = 0
395 | player.rotation = 0
396 | }
397 |
398 | // spawning enemies
399 | if (frames % randomInterval === 0) {
400 | spawnBuffer = spawnBuffer < 0 ? 100 : spawnBuffer
401 | grids.push(new Grid())
402 | randomInterval = Math.floor(Math.random() * 500 + spawnBuffer)
403 | frames = 0
404 | spawnBuffer -= 100
405 | }
406 |
407 | if (
408 | keys.space.pressed &&
409 | player.powerUp === 'MachineGun' &&
410 | frames % 2 === 0 &&
411 | !game.over
412 | ) {
413 | if (frames % 6 === 0) audio.shoot.play()
414 | projectiles.push(
415 | new Projectile({
416 | position: {
417 | x: player.position.x + player.width / 2,
418 | y: player.position.y
419 | },
420 | velocity: {
421 | x: 0,
422 | y: -10
423 | },
424 | color: 'yellow'
425 | })
426 | )
427 | }
428 |
429 | frames++
430 | }
431 |
432 | document.querySelector('#startButton').addEventListener('click', () => {
433 | audio.backgroundMusic.play()
434 | audio.start.play()
435 |
436 | document.querySelector('#startScreen').style.display = 'none'
437 | document.querySelector('#scoreContainer').style.display = 'block'
438 | init()
439 | animate()
440 | })
441 |
442 | document.querySelector('#restartButton').addEventListener('click', () => {
443 | audio.select.play()
444 | document.querySelector('#restartScreen').style.display = 'none'
445 | init()
446 | animate()
447 | })
448 |
449 | addEventListener('keydown', ({ key }) => {
450 | if (game.over) return
451 |
452 | switch (key) {
453 | case 'a':
454 | keys.a.pressed = true
455 | break
456 | case 'd':
457 | keys.d.pressed = true
458 | break
459 | case ' ':
460 | keys.space.pressed = true
461 |
462 | if (player.powerUp === 'MachineGun') return
463 |
464 | audio.shoot.play()
465 | projectiles.push(
466 | new Projectile({
467 | position: {
468 | x: player.position.x + player.width / 2,
469 | y: player.position.y
470 | },
471 | velocity: {
472 | x: 0,
473 | y: -10
474 | }
475 | })
476 | )
477 |
478 | break
479 | }
480 | })
481 |
482 | addEventListener('keyup', ({ key }) => {
483 | switch (key) {
484 | case 'a':
485 | keys.a.pressed = false
486 | break
487 | case 'd':
488 | keys.d.pressed = false
489 | break
490 | case ' ':
491 | keys.space.pressed = false
492 |
493 | break
494 | }
495 | })
496 |
--------------------------------------------------------------------------------
/js/classes/Bomb.js:
--------------------------------------------------------------------------------
1 | class Bomb {
2 | static radius = 30
3 | constructor({ position, velocity }) {
4 | this.position = position
5 | this.velocity = velocity
6 | this.radius = 0
7 | this.color = 'red'
8 | this.opacity = 1
9 | this.active = false
10 |
11 | gsap.to(this, {
12 | radius: 30
13 | })
14 | }
15 |
16 | draw() {
17 | c.save()
18 | c.globalAlpha = this.opacity
19 | c.beginPath()
20 | c.arc(this.position.x, this.position.y, this.radius, 0, Math.PI * 2)
21 | c.closePath()
22 | c.fillStyle = this.color
23 | c.fill()
24 | c.restore()
25 | }
26 |
27 | update() {
28 | this.draw()
29 | this.position.x += this.velocity.x
30 | this.position.y += this.velocity.y
31 |
32 | if (
33 | this.position.x + this.radius + this.velocity.x >= canvas.width ||
34 | this.position.x - this.radius + this.velocity.x <= 0
35 | ) {
36 | this.velocity.x = -this.velocity.x
37 | } else if (
38 | this.position.y + this.radius + this.velocity.y >= canvas.height ||
39 | this.position.y - this.radius + this.velocity.y <= 0
40 | )
41 | this.velocity.y = -this.velocity.y
42 | }
43 |
44 | explode() {
45 | audio.bomb.play()
46 | this.active = true
47 | this.velocity.x = 0
48 | this.velocity.y = 0
49 | gsap.to(this, {
50 | radius: 200,
51 | color: 'white'
52 | })
53 |
54 | gsap.to(this, {
55 | delay: 0.1,
56 | opacity: 0,
57 | duration: 0.15
58 | })
59 | }
60 | }
61 |
62 | class PowerUp {
63 | constructor({ position, velocity }) {
64 | this.position = position
65 | this.velocity = velocity
66 | this.radius = 15
67 | }
68 |
69 | draw() {
70 | c.beginPath()
71 | c.arc(this.position.x, this.position.y, this.radius, 0, Math.PI * 2)
72 | c.fillStyle = 'yellow'
73 | c.fill()
74 | c.closePath()
75 | }
76 |
77 | update() {
78 | this.draw()
79 | this.position.x += this.velocity.x
80 | this.position.y += this.velocity.y
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/js/classes/Grid.js:
--------------------------------------------------------------------------------
1 | class Grid {
2 | constructor() {
3 | this.position = {
4 | x: 0,
5 | y: 0
6 | }
7 |
8 | this.velocity = {
9 | x: 3,
10 | y: 0
11 | }
12 |
13 | this.invaders = []
14 |
15 | const columns = Math.floor(Math.random() * 10 + 5)
16 | const rows = Math.floor(Math.random() * 5 + 2)
17 |
18 | this.width = columns * 30
19 |
20 | for (let x = 0; x < columns; x++) {
21 | for (let y = 0; y < rows; y++) {
22 | this.invaders.push(
23 | new Invader({
24 | position: {
25 | x: x * 30,
26 | y: y * 30
27 | }
28 | })
29 | )
30 | }
31 | }
32 | }
33 |
34 | update() {
35 | this.position.x += this.velocity.x
36 | this.position.y += this.velocity.y
37 |
38 | this.velocity.y = 0
39 |
40 | if (this.position.x + this.width >= canvas.width || this.position.x <= 0) {
41 | this.velocity.x = -this.velocity.x * 1.15
42 | this.velocity.y = 30
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/js/classes/Invader.js:
--------------------------------------------------------------------------------
1 | class Invader {
2 | constructor({ position }) {
3 | this.velocity = {
4 | x: 0,
5 | y: 0
6 | }
7 |
8 | const image = new Image()
9 | image.src = './img/invader.png'
10 | image.onload = () => {
11 | const scale = 1
12 | this.image = image
13 | this.width = image.width * scale
14 | this.height = image.height * scale
15 | this.position = {
16 | x: position.x,
17 | y: position.y
18 | }
19 | }
20 | }
21 |
22 | draw() {
23 | // c.fillStyle = 'red'
24 | // c.fillRect(this.position.x, this.position.y, this.width, this.height)
25 |
26 | c.drawImage(
27 | this.image,
28 | this.position.x,
29 | this.position.y,
30 | this.width,
31 | this.height
32 | )
33 | }
34 |
35 | update({ velocity }) {
36 | if (this.image) {
37 | this.draw()
38 | this.position.x += velocity.x
39 | this.position.y += velocity.y
40 | }
41 | }
42 |
43 | shoot(invaderProjectiles) {
44 | audio.enemyShoot.play()
45 | invaderProjectiles.push(
46 | new InvaderProjectile({
47 | position: {
48 | x: this.position.x + this.width / 2,
49 | y: this.position.y + this.height
50 | },
51 | velocity: {
52 | x: 0,
53 | y: 5
54 | }
55 | })
56 | )
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/js/classes/InvaderProjectile.js:
--------------------------------------------------------------------------------
1 | class InvaderProjectile {
2 | constructor({ position, velocity }) {
3 | this.position = position
4 | this.velocity = velocity
5 |
6 | this.width = 3
7 | this.height = 10
8 | }
9 |
10 | draw() {
11 | c.fillStyle = 'white'
12 | c.fillRect(this.position.x, this.position.y, this.width, this.height)
13 | }
14 |
15 | update() {
16 | this.draw()
17 | this.position.x += this.velocity.x
18 | this.position.y += this.velocity.y
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/js/classes/Particle.js:
--------------------------------------------------------------------------------
1 | class Particle {
2 | constructor({ position, velocity, radius, color, fades }) {
3 | this.position = position
4 | this.velocity = velocity
5 |
6 | this.radius = radius
7 | this.color = color
8 | this.opacity = 1
9 | this.fades = fades
10 | }
11 |
12 | draw() {
13 | c.save()
14 | c.globalAlpha = this.opacity
15 | c.beginPath()
16 | c.arc(this.position.x, this.position.y, this.radius, 0, Math.PI * 2)
17 | c.fillStyle = this.color
18 | c.fill()
19 | c.closePath()
20 | c.restore()
21 | }
22 |
23 | update() {
24 | this.draw()
25 | this.position.x += this.velocity.x
26 | this.position.y += this.velocity.y
27 |
28 | if (this.fades) this.opacity -= 0.01
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/js/classes/Player.js:
--------------------------------------------------------------------------------
1 | class Player {
2 | constructor() {
3 | this.velocity = {
4 | x: 0,
5 | y: 0
6 | }
7 |
8 | this.rotation = 0
9 | this.opacity = 1
10 |
11 | const image = new Image()
12 | image.src = './img/spaceship.png'
13 | image.onload = () => {
14 | const scale = 0.15
15 | this.image = image
16 | this.width = image.width * scale
17 | this.height = image.height * scale
18 | this.position = {
19 | x: canvas.width / 2 - this.width / 2,
20 | y: canvas.height - this.height - 20
21 | }
22 | }
23 |
24 | this.particles = []
25 | this.frames = 0
26 | }
27 |
28 | draw() {
29 | // c.fillStyle = 'red'
30 | // c.fillRect(this.position.x, this.position.y, this.width, this.height)
31 |
32 | c.save()
33 | c.globalAlpha = this.opacity
34 | c.translate(
35 | player.position.x + player.width / 2,
36 | player.position.y + player.height / 2
37 | )
38 | c.rotate(this.rotation)
39 |
40 | c.translate(
41 | -player.position.x - player.width / 2,
42 | -player.position.y - player.height / 2
43 | )
44 |
45 | c.drawImage(
46 | this.image,
47 | this.position.x,
48 | this.position.y,
49 | this.width,
50 | this.height
51 | )
52 | c.restore()
53 | }
54 |
55 | update() {
56 | if (!this.image) return
57 |
58 | this.draw()
59 | this.position.x += this.velocity.x
60 |
61 | if (this.opacity !== 1) return
62 |
63 | this.frames++
64 | if (this.frames % 2 === 0) {
65 | this.particles.push(
66 | new Particle({
67 | position: {
68 | x: this.position.x + this.width / 2,
69 | y: this.position.y + this.height
70 | },
71 | velocity: {
72 | x: (Math.random() - 0.5) * 1.5,
73 | y: 1.4
74 | },
75 | radius: Math.random() * 2,
76 | color: 'white',
77 | fades: true
78 | })
79 | )
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/js/classes/Projectile.js:
--------------------------------------------------------------------------------
1 | class Projectile {
2 | constructor({ position, velocity, color = 'red' }) {
3 | this.position = position
4 | this.velocity = velocity
5 |
6 | this.radius = 4
7 | this.color = color
8 | }
9 |
10 | draw() {
11 | c.beginPath()
12 | c.arc(this.position.x, this.position.y, this.radius, 0, Math.PI * 2)
13 | c.fillStyle = this.color
14 | c.fill()
15 | c.closePath()
16 | }
17 |
18 | update() {
19 | this.draw()
20 | this.position.x += this.velocity.x
21 | this.position.y += this.velocity.y
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/js/util.js:
--------------------------------------------------------------------------------
1 | function randomBetween(min, max) {
2 | return Math.random() * (max - min) + min
3 | }
4 |
5 | function createScoreLabel({ score = 100, object }) {
6 | const scoreLabel = document.createElement('label')
7 | scoreLabel.innerHTML = score
8 | scoreLabel.style.position = 'absolute'
9 | scoreLabel.style.color = 'white'
10 | scoreLabel.style.top = object.position.y + 'px'
11 | scoreLabel.style.left = object.position.x + 'px'
12 | scoreLabel.style.userSelect = 'none'
13 | document.querySelector('#parentDiv').appendChild(scoreLabel)
14 |
15 | gsap.to(scoreLabel, {
16 | opacity: 0,
17 | y: -30,
18 | duration: 0.75,
19 | onComplete: () => {
20 | document.querySelector('#parentDiv').removeChild(scoreLabel)
21 | }
22 | })
23 | }
24 |
25 | function rectangularCollision({ rectangle1, rectangle2 }) {
26 | return (
27 | rectangle1.position.y + rectangle1.height >= rectangle2.position.y &&
28 | rectangle1.position.x + rectangle1.width >= rectangle2.position.x &&
29 | rectangle1.position.x <= rectangle2.position.x + rectangle2.width
30 | )
31 | }
32 |
33 | function createParticles({ object, color, fades }) {
34 | for (let i = 0; i < 15; i++) {
35 | particles.push(
36 | new Particle({
37 | position: {
38 | x: object.position.x + object.width / 2,
39 | y: object.position.y + object.height / 2
40 | },
41 | velocity: {
42 | x: (Math.random() - 0.5) * 2,
43 | y: (Math.random() - 0.5) * 2
44 | },
45 | radius: Math.random() * 3,
46 | color: color || '#BAA0DE',
47 | fades
48 | })
49 | )
50 | }
51 | }
52 |
--------------------------------------------------------------------------------