├── .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 |
55 | Start Button 56 | Start 66 |
67 |
68 |
69 | 70 | 71 |
88 |
89 |

Game Over

90 |

91 | 300 92 |

93 |

Points

94 | 95 | 96 |
100 | Restart Button 101 | Restart 111 |
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 | --------------------------------------------------------------------------------