├── .github ├── FUNDING.yml └── workflows │ └── main.yml ├── .gitignore ├── .replit ├── LICENSE ├── README.md ├── css ├── core.css └── typeography.css ├── index.html ├── js ├── spaceinvaders.js └── starfield.js ├── makefile ├── screenshot.jpg └── sounds ├── bang.wav ├── explosion.wav ├── invaderkilled.wav └── shoot.wav /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # Support 'GitHub Sponsors' funding. 2 | github: dwmkerr 3 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Publish to GitHub Pages 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | build-and-deploy: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@master 12 | 13 | - name: Build and Deploy 14 | uses: JamesIves/github-pages-deploy-action@releases/v2 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | BASE_BRANCH: master # Deploy from master... 18 | BRANCH: gh-pages # ...to GitHub pages... 19 | BUILD_SCRIPT: make build # ...run the build action... 20 | FOLDER: artifacts/dist # ...copy the distribution folder. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | spaceinvaders.sublime-project 2 | spaceinvaders.sublime-workspace 3 | -------------------------------------------------------------------------------- /.replit: -------------------------------------------------------------------------------- 1 | language = "html" 2 | run = "" 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Dave Kerr 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Space Invaders 2 | 3 | The classic Space Invaders game written in JavaScript as a learning exercise. 4 | 5 | No jQuery or any other third party libraries, just raw JavaScript, CSS and HTML. 6 | 7 | See it Live: [https://dwmkerr.github.io/spaceinvaders/](https://dwmkerr.github.io/spaceinvaders/) 8 | 9 | [![Space Invaders Screenshot](./screenshot.jpg "Space Invaders Screenshot")](https://dwmkerr.github.io/spaceinvaders/) 10 | 11 | ## Intro 12 | 13 | [![Run on Repl.it](https://repl.it/badge/github/dwmkerr/spaceinvaders)](https://repl.it/github/dwmkerr/spaceinvaders) 14 | 15 | What's there to say? It's Space Invaders in JavaScript! 16 | 17 | Create the game, give it a `div` to draw to, tell it when the keyboard is mashed and that's all you need to add Space Invaders to a website. 18 | 19 | This is a simple learning exercise, so the JavaScript is deliberate kept all one file. There's no linting, testing, CI, or anything like that. If you want to see such patterns in front-end JavaScript, check out something like [angular-modal-service](https://github.com/dwmkerr/angular-modal-service). 20 | 21 | ## Adding Space Invaders to a Web Page 22 | 23 | First, drop the `spaceinvaders.js` file into the website. 24 | 25 | Now add a canvas to the page. 26 | 27 | ```html 28 | 29 | ``` 30 | 31 | Next, add the Space Invaders game code. You create the game, initialise it with the canvas, start it and make sure you tell it when a key is pressed or released. That's it! 32 | 33 | ```html 34 | 68 | ``` 69 | 70 | ## References 71 | 72 | Other bits and pieces that are useful can be dropped here. 73 | 74 | - The sounds came from [http://www.classicgaming.cc/classics/spaceinvaders/sounds.php](http://www.classicgaming.cc/classics/spaceinvaders/sounds.php) 75 | 76 | ## Publishing 77 | 78 | On changes to the `master` branch, the GitHub Pages site will be automatically updated. 79 | -------------------------------------------------------------------------------- /css/core.css: -------------------------------------------------------------------------------- 1 | /* core.css */ 2 | /* lean and simple css reset, based on Tantek Celik's reset - see comments below. */ 3 | /* (c) 2004-2010 Tantek Çelik. Some Rights Reserved. http://tantek.com */ 4 | /* This style sheet is licensed under a Creative Commons License. */ 5 | /* http://creativecommons.org/licenses/by/2.0 */ 6 | 7 | /* Purpose: undo some of the default styling of common browsers */ 8 | 9 | 10 | :link,:visited,ins { text-decoration:none } 11 | nav ul,nav ol { list-style:none } 12 | dl,ul,ol,li, 13 | h1,h2,h3,h4,h5,h6, 14 | html,body,pre,p,blockquote, 15 | form,fieldset,input,label 16 | { margin:0; padding:0 } 17 | abbr, img, object, 18 | a img,:link img,:visited img, 19 | a object,:link object,:visited object 20 | { border:0 } 21 | address,abbr { font-style:normal } 22 | iframe:not(.auto-link) { display:none ! important; visibility:hidden ! important; margin-left: -10000px ! important } 23 | a { 24 | color: #0088cc; 25 | text-decoration: none; 26 | } 27 | 28 | a:hover, 29 | a:focus { 30 | color: #005580; 31 | text-decoration: underline; 32 | } -------------------------------------------------------------------------------- /css/typeography.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 3 | font-size: 14px; 4 | line-height: 20px; 5 | color: #333333; 6 | } 7 | 8 | h1, 9 | h2, 10 | h3, 11 | h4, 12 | h5, 13 | h6 { 14 | margin: 10px 0; 15 | font-family: inherit; 16 | font-weight: bold; 17 | line-height: 20px; 18 | color: inherit; 19 | text-rendering: optimizelegibility; 20 | } 21 | 22 | h1, 23 | h2, 24 | h3 { 25 | line-height: 40px; 26 | } 27 | 28 | h1 { 29 | font-size: 38.5px; 30 | } 31 | 32 | h2 { 33 | font-size: 31.5px; 34 | } 35 | 36 | h3 { 37 | font-size: 24.5px; 38 | } 39 | 40 | h4 { 41 | font-size: 17.5px; 42 | } 43 | 44 | h5 { 45 | font-size: 14px; 46 | } 47 | 48 | h6 { 49 | font-size: 11.9px; 50 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Space Invaders 5 | 6 | 7 | 41 | 42 | 43 |
44 |
45 | 46 |
47 |
48 |

Move with arrow keys or swipe, fire with the space bar or touch. The invaders get faster and drop 49 | more bombs as you complete each level!

50 |

mute | 51 | spaceinvaders on github | 52 | more projects | dwmkerr.com

53 |
54 | 55 | 56 | 57 | 111 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /js/spaceinvaders.js: -------------------------------------------------------------------------------- 1 | /* 2 | spaceinvaders.js 3 | 4 | the core logic for the space invaders game. 5 | 6 | */ 7 | 8 | /* 9 | Game Class 10 | 11 | The Game class represents a Space Invaders game. 12 | Create an instance of it, change any of the default values 13 | in the settings, and call 'start' to run the game. 14 | 15 | Call 'initialise' before 'start' to set the canvas the game 16 | will draw to. 17 | 18 | Call 'moveShip' or 'shipFire' to control the ship. 19 | 20 | Listen for 'gameWon' or 'gameLost' events to handle the game 21 | ending. 22 | */ 23 | 24 | // Constants for the keyboard. 25 | var KEY_LEFT = 37; 26 | var KEY_RIGHT = 39; 27 | var KEY_SPACE = 32; 28 | 29 | // Creates an instance of the Game class. 30 | function Game() { 31 | 32 | // Set the initial config. 33 | this.config = { 34 | bombRate: 0.05, 35 | bombMinVelocity: 50, 36 | bombMaxVelocity: 50, 37 | invaderInitialVelocity: 25, 38 | invaderAcceleration: 0, 39 | invaderDropDistance: 20, 40 | rocketVelocity: 120, 41 | rocketMaxFireRate: 2, 42 | gameWidth: 400, 43 | gameHeight: 300, 44 | fps: 50, 45 | debugMode: false, 46 | invaderRanks: 5, 47 | invaderFiles: 10, 48 | shipSpeed: 120, 49 | levelDifficultyMultiplier: 0.2, 50 | pointsPerInvader: 5, 51 | limitLevelIncrease: 25 52 | }; 53 | 54 | // All state is in the variables below. 55 | this.lives = 3; 56 | this.width = 0; 57 | this.height = 0; 58 | this.gameBounds = {left: 0, top: 0, right: 0, bottom: 0}; 59 | this.intervalId = 0; 60 | this.score = 0; 61 | this.level = 1; 62 | 63 | // The state stack. 64 | this.stateStack = []; 65 | 66 | // Input/output 67 | this.pressedKeys = {}; 68 | this.gameCanvas = null; 69 | 70 | // All sounds. 71 | this.sounds = null; 72 | 73 | // The previous x position, used for touch. 74 | this.previousX = 0; 75 | } 76 | 77 | // Initialis the Game with a canvas. 78 | Game.prototype.initialise = function(gameCanvas) { 79 | 80 | // Set the game canvas. 81 | this.gameCanvas = gameCanvas; 82 | 83 | // Set the game width and height. 84 | this.width = gameCanvas.width; 85 | this.height = gameCanvas.height; 86 | 87 | // Set the state game bounds. 88 | this.gameBounds = { 89 | left: gameCanvas.width / 2 - this.config.gameWidth / 2, 90 | right: gameCanvas.width / 2 + this.config.gameWidth / 2, 91 | top: gameCanvas.height / 2 - this.config.gameHeight / 2, 92 | bottom: gameCanvas.height / 2 + this.config.gameHeight / 2, 93 | }; 94 | }; 95 | 96 | Game.prototype.moveToState = function(state) { 97 | 98 | // If we are in a state, leave it. 99 | if(this.currentState() && this.currentState().leave) { 100 | this.currentState().leave(game); 101 | this.stateStack.pop(); 102 | } 103 | 104 | // If there's an enter function for the new state, call it. 105 | if(state.enter) { 106 | state.enter(game); 107 | } 108 | 109 | // Set the current state. 110 | this.stateStack.pop(); 111 | this.stateStack.push(state); 112 | }; 113 | 114 | // Start the Game. 115 | Game.prototype.start = function() { 116 | 117 | // Move into the 'welcome' state. 118 | this.moveToState(new WelcomeState()); 119 | 120 | // Set the game variables. 121 | this.lives = 3; 122 | this.config.debugMode = /debug=true/.test(window.location.href); 123 | 124 | // Start the game loop. 125 | var game = this; 126 | this.intervalId = setInterval(function () { GameLoop(game);}, 1000 / this.config.fps); 127 | 128 | }; 129 | 130 | // Returns the current state. 131 | Game.prototype.currentState = function() { 132 | return this.stateStack.length > 0 ? this.stateStack[this.stateStack.length - 1] : null; 133 | }; 134 | 135 | // Mutes or unmutes the game. 136 | Game.prototype.mute = function(mute) { 137 | 138 | // If we've been told to mute, mute. 139 | if(mute === true) { 140 | this.sounds.mute = true; 141 | } else if (mute === false) { 142 | this.sounds.mute = false; 143 | } else { 144 | // Toggle mute instead... 145 | this.sounds.mute = this.sounds.mute ? false : true; 146 | } 147 | }; 148 | 149 | // The main loop. 150 | function GameLoop(game) { 151 | var currentState = game.currentState(); 152 | if(currentState) { 153 | 154 | // Delta t is the time to update/draw. 155 | var dt = 1 / game.config.fps; 156 | 157 | // Get the drawing context. 158 | var ctx = this.gameCanvas.getContext("2d"); 159 | 160 | // Update if we have an update function. Also draw 161 | // if we have a draw function. 162 | if(currentState.update) { 163 | currentState.update(game, dt); 164 | } 165 | if(currentState.draw) { 166 | currentState.draw(game, dt, ctx); 167 | } 168 | } 169 | } 170 | 171 | Game.prototype.pushState = function(state) { 172 | 173 | // If there's an enter function for the new state, call it. 174 | if(state.enter) { 175 | state.enter(game); 176 | } 177 | // Set the current state. 178 | this.stateStack.push(state); 179 | }; 180 | 181 | Game.prototype.popState = function() { 182 | 183 | // Leave and pop the state. 184 | if(this.currentState()) { 185 | if(this.currentState().leave) { 186 | this.currentState().leave(game); 187 | } 188 | 189 | // Set the current state. 190 | this.stateStack.pop(); 191 | } 192 | }; 193 | 194 | // The stop function stops the game. 195 | Game.prototype.stop = function Stop() { 196 | clearInterval(this.intervalId); 197 | }; 198 | 199 | // Inform the game a key is down. 200 | Game.prototype.keyDown = function(keyCode) { 201 | this.pressedKeys[keyCode] = true; 202 | // Delegate to the current state too. 203 | if(this.currentState() && this.currentState().keyDown) { 204 | this.currentState().keyDown(this, keyCode); 205 | } 206 | }; 207 | 208 | Game.prototype.touchstart = function(s) { 209 | if(this.currentState() && this.currentState().keyDown) { 210 | this.currentState().keyDown(this, KEY_SPACE); 211 | } 212 | }; 213 | 214 | Game.prototype.touchend = function(s) { 215 | delete this.pressedKeys[KEY_RIGHT]; 216 | delete this.pressedKeys[KEY_LEFT]; 217 | }; 218 | 219 | Game.prototype.touchmove = function(e) { 220 | var currentX = e.changedTouches[0].pageX; 221 | if (this.previousX > 0) { 222 | if (currentX > this.previousX) { 223 | delete this.pressedKeys[KEY_LEFT]; 224 | this.pressedKeys[KEY_RIGHT] = true; 225 | } else { 226 | delete this.pressedKeys[KEY_RIGHT]; 227 | this.pressedKeys[KEY_LEFT] = true; 228 | } 229 | } 230 | this.previousX = currentX; 231 | }; 232 | 233 | // Inform the game a key is up. 234 | Game.prototype.keyUp = function(keyCode) { 235 | delete this.pressedKeys[keyCode]; 236 | // Delegate to the current state too. 237 | if(this.currentState() && this.currentState().keyUp) { 238 | this.currentState().keyUp(this, keyCode); 239 | } 240 | }; 241 | 242 | function WelcomeState() { 243 | 244 | } 245 | 246 | WelcomeState.prototype.enter = function(game) { 247 | 248 | // Create and load the sounds. 249 | game.sounds = new Sounds(); 250 | game.sounds.init(); 251 | game.sounds.loadSound('shoot', 'sounds/shoot.wav'); 252 | game.sounds.loadSound('bang', 'sounds/bang.wav'); 253 | game.sounds.loadSound('explosion', 'sounds/explosion.wav'); 254 | }; 255 | 256 | WelcomeState.prototype.update = function (game, dt) { 257 | 258 | 259 | }; 260 | 261 | WelcomeState.prototype.draw = function(game, dt, ctx) { 262 | 263 | // Clear the background. 264 | ctx.clearRect(0, 0, game.width, game.height); 265 | 266 | ctx.font="30px Arial"; 267 | ctx.fillStyle = '#ffffff'; 268 | ctx.textBaseline="middle"; 269 | ctx.textAlign="center"; 270 | ctx.fillText("Space Invaders", game.width / 2, game.height/2 - 40); 271 | ctx.font="16px Arial"; 272 | 273 | ctx.fillText("Press 'Space' or touch to start.", game.width / 2, game.height/2); 274 | }; 275 | 276 | WelcomeState.prototype.keyDown = function(game, keyCode) { 277 | if(keyCode == KEY_SPACE) { 278 | // Space starts the game. 279 | game.level = 1; 280 | game.score = 0; 281 | game.lives = 3; 282 | game.moveToState(new LevelIntroState(game.level)); 283 | } 284 | }; 285 | 286 | function GameOverState() { 287 | 288 | } 289 | 290 | GameOverState.prototype.update = function(game, dt) { 291 | 292 | }; 293 | 294 | GameOverState.prototype.draw = function(game, dt, ctx) { 295 | 296 | // Clear the background. 297 | ctx.clearRect(0, 0, game.width, game.height); 298 | 299 | ctx.font="30px Arial"; 300 | ctx.fillStyle = '#ffffff'; 301 | ctx.textBaseline="center"; 302 | ctx.textAlign="center"; 303 | ctx.fillText("Game Over!", game.width / 2, game.height/2 - 40); 304 | ctx.font="16px Arial"; 305 | ctx.fillText("You scored " + game.score + " and got to level " + game.level, game.width / 2, game.height/2); 306 | ctx.font="16px Arial"; 307 | ctx.fillText("Press 'Space' to play again.", game.width / 2, game.height/2 + 40); 308 | }; 309 | 310 | GameOverState.prototype.keyDown = function(game, keyCode) { 311 | if(keyCode == KEY_SPACE) { 312 | // Space restarts the game. 313 | game.lives = 3; 314 | game.score = 0; 315 | game.level = 1; 316 | game.moveToState(new LevelIntroState(1)); 317 | } 318 | }; 319 | 320 | // Create a PlayState with the game config and the level you are on. 321 | function PlayState(config, level) { 322 | this.config = config; 323 | this.level = level; 324 | 325 | // Game state. 326 | this.invaderCurrentVelocity = 10; 327 | this.invaderCurrentDropDistance = 0; 328 | this.invadersAreDropping = false; 329 | this.lastRocketTime = null; 330 | 331 | // Game entities. 332 | this.ship = null; 333 | this.invaders = []; 334 | this.rockets = []; 335 | this.bombs = []; 336 | } 337 | 338 | PlayState.prototype.enter = function(game) { 339 | 340 | // Create the ship. 341 | this.ship = new Ship(game.width / 2, game.gameBounds.bottom); 342 | 343 | // Setup initial state. 344 | this.invaderCurrentVelocity = 10; 345 | this.invaderCurrentDropDistance = 0; 346 | this.invadersAreDropping = false; 347 | 348 | // Set the ship speed for this level, as well as invader params. 349 | var levelMultiplier = this.level * this.config.levelDifficultyMultiplier; 350 | var limitLevel = (this.level < this.config.limitLevelIncrease ? this.level : this.config.limitLevelIncrease); 351 | this.shipSpeed = this.config.shipSpeed; 352 | this.invaderInitialVelocity = this.config.invaderInitialVelocity + 1.5 * (levelMultiplier * this.config.invaderInitialVelocity); 353 | this.bombRate = this.config.bombRate + (levelMultiplier * this.config.bombRate); 354 | this.bombMinVelocity = this.config.bombMinVelocity + (levelMultiplier * this.config.bombMinVelocity); 355 | this.bombMaxVelocity = this.config.bombMaxVelocity + (levelMultiplier * this.config.bombMaxVelocity); 356 | this.rocketMaxFireRate = this.config.rocketMaxFireRate + 0.4 * limitLevel; 357 | 358 | // Create the invaders. 359 | var ranks = this.config.invaderRanks + 0.1 * limitLevel; 360 | var files = this.config.invaderFiles + 0.2 * limitLevel; 361 | var invaders = []; 362 | for(var rank = 0; rank < ranks; rank++){ 363 | for(var file = 0; file < files; file++) { 364 | invaders.push(new Invader( 365 | (game.width / 2) + ((files/2 - file) * 200 / files), 366 | (game.gameBounds.top + rank * 20), 367 | rank, file, 'Invader')); 368 | } 369 | } 370 | this.invaders = invaders; 371 | this.invaderCurrentVelocity = this.invaderInitialVelocity; 372 | this.invaderVelocity = {x: -this.invaderInitialVelocity, y:0}; 373 | this.invaderNextVelocity = null; 374 | }; 375 | 376 | PlayState.prototype.update = function(game, dt) { 377 | 378 | // If the left or right arrow keys are pressed, move 379 | // the ship. Check this on ticks rather than via a keydown 380 | // event for smooth movement, otherwise the ship would move 381 | // more like a text editor caret. 382 | if(game.pressedKeys[KEY_LEFT]) { 383 | this.ship.x -= this.shipSpeed * dt; 384 | } 385 | if(game.pressedKeys[KEY_RIGHT]) { 386 | this.ship.x += this.shipSpeed * dt; 387 | } 388 | if(game.pressedKeys[KEY_SPACE]) { 389 | this.fireRocket(); 390 | } 391 | 392 | // Keep the ship in bounds. 393 | if(this.ship.x < game.gameBounds.left) { 394 | this.ship.x = game.gameBounds.left; 395 | } 396 | if(this.ship.x > game.gameBounds.right) { 397 | this.ship.x = game.gameBounds.right; 398 | } 399 | 400 | // Move each bomb. 401 | for(var i=0; i this.height) { 407 | this.bombs.splice(i--, 1); 408 | } 409 | } 410 | 411 | // Move each rocket. 412 | for(i=0; i game.gameBounds.right) { 432 | hitRight = true; 433 | } 434 | else if(hitBottom == false && newy > game.gameBounds.bottom) { 435 | hitBottom = true; 436 | } 437 | 438 | if(!hitLeft && !hitRight && !hitBottom) { 439 | invader.x = newx; 440 | invader.y = newy; 441 | } 442 | } 443 | 444 | // Update invader velocities. 445 | if(this.invadersAreDropping) { 446 | this.invaderCurrentDropDistance += this.invaderVelocity.y * dt; 447 | if(this.invaderCurrentDropDistance >= this.config.invaderDropDistance) { 448 | this.invadersAreDropping = false; 449 | this.invaderVelocity = this.invaderNextVelocity; 450 | this.invaderCurrentDropDistance = 0; 451 | } 452 | } 453 | // If we've hit the left, move down then right. 454 | if(hitLeft) { 455 | this.invaderCurrentVelocity += this.config.invaderAcceleration; 456 | this.invaderVelocity = {x: 0, y:this.invaderCurrentVelocity }; 457 | this.invadersAreDropping = true; 458 | this.invaderNextVelocity = {x: this.invaderCurrentVelocity , y:0}; 459 | } 460 | // If we've hit the right, move down then left. 461 | if(hitRight) { 462 | this.invaderCurrentVelocity += this.config.invaderAcceleration; 463 | this.invaderVelocity = {x: 0, y:this.invaderCurrentVelocity }; 464 | this.invadersAreDropping = true; 465 | this.invaderNextVelocity = {x: -this.invaderCurrentVelocity , y:0}; 466 | } 467 | // If we've hit the bottom, it's game over. 468 | if(hitBottom) { 469 | game.lives = 0; 470 | } 471 | 472 | // Check for rocket/invader collisions. 473 | for(i=0; i= (invader.x - invader.width/2) && rocket.x <= (invader.x + invader.width/2) && 481 | rocket.y >= (invader.y - invader.height/2) && rocket.y <= (invader.y + invader.height/2)) { 482 | 483 | // Remove the rocket, set 'bang' so we don't process 484 | // this rocket again. 485 | this.rockets.splice(j--, 1); 486 | bang = true; 487 | game.score += this.config.pointsPerInvader; 488 | break; 489 | } 490 | } 491 | if(bang) { 492 | this.invaders.splice(i--, 1); 493 | game.sounds.playSound('bang'); 494 | } 495 | } 496 | 497 | // Find all of the front rank invaders. 498 | var frontRankInvaders = {}; 499 | for(var i=0; i Math.random()) { 515 | // Fire! 516 | this.bombs.push(new Bomb(invader.x, invader.y + invader.height / 2, 517 | this.bombMinVelocity + Math.random()*(this.bombMaxVelocity - this.bombMinVelocity))); 518 | } 519 | } 520 | 521 | // Check for bomb/ship collisions. 522 | for(var i=0; i= (this.ship.x - this.ship.width/2) && bomb.x <= (this.ship.x + this.ship.width/2) && 525 | bomb.y >= (this.ship.y - this.ship.height/2) && bomb.y <= (this.ship.y + this.ship.height/2)) { 526 | this.bombs.splice(i--, 1); 527 | game.lives--; 528 | game.sounds.playSound('explosion'); 529 | } 530 | 531 | } 532 | 533 | // Check for invader/ship collisions. 534 | for(var i=0; i (this.ship.x - this.ship.width/2) && 537 | (invader.x - invader.width/2) < (this.ship.x + this.ship.width/2) && 538 | (invader.y + invader.height/2) > (this.ship.y - this.ship.height/2) && 539 | (invader.y - invader.height/2) < (this.ship.y + this.ship.height/2)) { 540 | // Dead by collision! 541 | game.lives = 0; 542 | game.sounds.playSound('explosion'); 543 | } 544 | } 545 | 546 | // Check for failure 547 | if(game.lives <= 0) { 548 | game.moveToState(new GameOverState()); 549 | } 550 | 551 | // Check for victory 552 | if(this.invaders.length === 0) { 553 | game.score += this.level * 50; 554 | game.level += 1; 555 | game.moveToState(new LevelIntroState(game.level)); 556 | } 557 | }; 558 | 559 | PlayState.prototype.draw = function(game, dt, ctx) { 560 | 561 | // Clear the background. 562 | ctx.clearRect(0, 0, game.width, game.height); 563 | 564 | // Draw ship. 565 | ctx.fillStyle = '#999999'; 566 | ctx.fillRect(this.ship.x - (this.ship.width / 2), this.ship.y - (this.ship.height / 2), this.ship.width, this.ship.height); 567 | 568 | // Draw invaders. 569 | ctx.fillStyle = '#006600'; 570 | for(var i=0; i (1000 / this.rocketMaxFireRate)) 631 | { 632 | // Add a rocket. 633 | this.rockets.push(new Rocket(this.ship.x, this.ship.y - 12, this.config.rocketVelocity)); 634 | this.lastRocketTime = (new Date()).valueOf(); 635 | 636 | // Play the 'shoot' sound. 637 | game.sounds.playSound('shoot'); 638 | } 639 | }; 640 | 641 | function PauseState() { 642 | 643 | } 644 | 645 | PauseState.prototype.keyDown = function(game, keyCode) { 646 | 647 | if(keyCode == 80) { 648 | // Pop the pause state. 649 | game.popState(); 650 | } 651 | }; 652 | 653 | PauseState.prototype.draw = function(game, dt, ctx) { 654 | 655 | // Clear the background. 656 | ctx.clearRect(0, 0, game.width, game.height); 657 | 658 | ctx.font="14px Arial"; 659 | ctx.fillStyle = '#ffffff'; 660 | ctx.textBaseline="middle"; 661 | ctx.textAlign="center"; 662 | ctx.fillText("Paused", game.width / 2, game.height/2); 663 | return; 664 | }; 665 | 666 | /* 667 | Level Intro State 668 | 669 | The Level Intro state shows a 'Level X' message and 670 | a countdown for the level. 671 | */ 672 | function LevelIntroState(level) { 673 | this.level = level; 674 | this.countdownMessage = "3"; 675 | } 676 | 677 | LevelIntroState.prototype.update = function(game, dt) { 678 | 679 | // Update the countdown. 680 | if(this.countdown === undefined) { 681 | this.countdown = 3; // countdown from 3 secs 682 | } 683 | this.countdown -= dt; 684 | 685 | if(this.countdown < 2) { 686 | this.countdownMessage = "2"; 687 | } 688 | if(this.countdown < 1) { 689 | this.countdownMessage = "1"; 690 | } 691 | if(this.countdown <= 0) { 692 | // Move to the next level, popping this state. 693 | game.moveToState(new PlayState(game.config, this.level)); 694 | } 695 | 696 | }; 697 | 698 | LevelIntroState.prototype.draw = function(game, dt, ctx) { 699 | 700 | // Clear the background. 701 | ctx.clearRect(0, 0, game.width, game.height); 702 | 703 | ctx.font="36px Arial"; 704 | ctx.fillStyle = '#ffffff'; 705 | ctx.textBaseline="middle"; 706 | ctx.textAlign="center"; 707 | ctx.fillText("Level " + this.level, game.width / 2, game.height/2); 708 | ctx.font="24px Arial"; 709 | ctx.fillText("Ready in " + this.countdownMessage, game.width / 2, game.height/2 + 36); 710 | return; 711 | }; 712 | 713 | 714 | /* 715 | 716 | Ship 717 | 718 | The ship has a position and that's about it. 719 | 720 | */ 721 | function Ship(x, y) { 722 | this.x = x; 723 | this.y = y; 724 | this.width = 20; 725 | this.height = 16; 726 | } 727 | 728 | /* 729 | Rocket 730 | 731 | Fired by the ship, they've got a position, velocity and state. 732 | 733 | */ 734 | function Rocket(x, y, velocity) { 735 | this.x = x; 736 | this.y = y; 737 | this.velocity = velocity; 738 | } 739 | 740 | /* 741 | Bomb 742 | 743 | Dropped by invaders, they've got position, velocity. 744 | 745 | */ 746 | function Bomb(x, y, velocity) { 747 | this.x = x; 748 | this.y = y; 749 | this.velocity = velocity; 750 | } 751 | 752 | /* 753 | Invader 754 | 755 | Invader's have position, type, rank/file and that's about it. 756 | */ 757 | 758 | function Invader(x, y, rank, file, type) { 759 | this.x = x; 760 | this.y = y; 761 | this.rank = rank; 762 | this.file = file; 763 | this.type = type; 764 | this.width = 18; 765 | this.height = 14; 766 | } 767 | 768 | /* 769 | Game State 770 | 771 | A Game State is simply an update and draw proc. 772 | When a game is in the state, the update and draw procs are 773 | called, with a dt value (dt is delta time, i.e. the number) 774 | of seconds to update or draw). 775 | 776 | */ 777 | function GameState(updateProc, drawProc, keyDown, keyUp, enter, leave) { 778 | this.updateProc = updateProc; 779 | this.drawProc = drawProc; 780 | this.keyDown = keyDown; 781 | this.keyUp = keyUp; 782 | this.enter = enter; 783 | this.leave = leave; 784 | } 785 | 786 | /* 787 | 788 | Sounds 789 | 790 | The sounds class is used to asynchronously load sounds and allow 791 | them to be played. 792 | 793 | */ 794 | function Sounds() { 795 | 796 | // The audio context. 797 | this.audioContext = null; 798 | 799 | // The actual set of loaded sounds. 800 | this.sounds = {}; 801 | } 802 | 803 | Sounds.prototype.init = function() { 804 | 805 | // Create the audio context, paying attention to webkit browsers. 806 | context = window.AudioContext || window.webkitAudioContext; 807 | this.audioContext = new context(); 808 | this.mute = false; 809 | }; 810 | 811 | Sounds.prototype.loadSound = function(name, url) { 812 | 813 | // Reference to ourselves for closures. 814 | var self = this; 815 | 816 | // Create an entry in the sounds object. 817 | this.sounds[name] = null; 818 | 819 | // Create an asynchronous request for the sound. 820 | var req = new XMLHttpRequest(); 821 | req.open('GET', url, true); 822 | req.responseType = 'arraybuffer'; 823 | req.onload = function() { 824 | self.audioContext.decodeAudioData(req.response, function(buffer) { 825 | self.sounds[name] = {buffer: buffer}; 826 | }); 827 | }; 828 | try { 829 | req.send(); 830 | } catch(e) { 831 | console.log("An exception occured getting sound the sound " + name + " this might be " + 832 | "because the page is running from the file system, not a webserver."); 833 | console.log(e); 834 | } 835 | }; 836 | 837 | Sounds.prototype.playSound = function(name) { 838 | 839 | // If we've not got the sound, don't bother playing it. 840 | if(this.sounds[name] === undefined || this.sounds[name] === null || this.mute === true) { 841 | return; 842 | } 843 | 844 | // Create a sound source, set the buffer, connect to the speakers and 845 | // play the sound. 846 | var source = this.audioContext.createBufferSource(); 847 | source.buffer = this.sounds[name].buffer; 848 | source.connect(this.audioContext.destination); 849 | source.start(0); 850 | }; 851 | -------------------------------------------------------------------------------- /js/starfield.js: -------------------------------------------------------------------------------- 1 | /* 2 | Starfield lets you take a div and turn it into a starfield. 3 | 4 | */ 5 | 6 | // Define the starfield class. 7 | function Starfield() { 8 | this.fps = 30; 9 | this.canvas = null; 10 | this.width = 0; 11 | this.width = 0; 12 | this.minVelocity = 15; 13 | this.maxVelocity = 30; 14 | this.stars = 100; 15 | this.intervalId = 0; 16 | } 17 | 18 | // The main function - initialises the starfield. 19 | Starfield.prototype.initialise = function(div) { 20 | var self = this; 21 | 22 | // Store the div. 23 | this.containerDiv = div; 24 | self.width = window.innerWidth; 25 | self.height = window.innerHeight; 26 | 27 | window.onresize = function(event) { 28 | self.width = window.innerWidth; 29 | self.height = window.innerHeight; 30 | self.canvas.width = self.width; 31 | self.canvas.height = self.height; 32 | self.draw(); 33 | } 34 | 35 | // Create the canvas. 36 | var canvas = document.createElement('canvas'); 37 | div.appendChild(canvas); 38 | this.canvas = canvas; 39 | this.canvas.width = this.width; 40 | this.canvas.height = this.height; 41 | }; 42 | 43 | Starfield.prototype.start = function() { 44 | 45 | // Create the stars. 46 | var stars = []; 47 | for(var i=0; i this.height) { 73 | this.stars[i] = new Star(Math.random()*this.width, 0, Math.random()*3+1, 74 | (Math.random()*(this.maxVelocity - this.minVelocity))+this.minVelocity); 75 | } 76 | } 77 | }; 78 | 79 | Starfield.prototype.draw = function() { 80 | 81 | // Get the drawing context. 82 | var ctx = this.canvas.getContext("2d"); 83 | 84 | // Draw the background. 85 | ctx.fillStyle = '#000000'; 86 | ctx.fillRect(0, 0, this.width, this.height); 87 | 88 | // Draw stars. 89 | ctx.fillStyle = '#ffffff'; 90 | for(var i=0; i