├── .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 | [](https://dwmkerr.github.io/spaceinvaders/)
10 |
11 | ## Intro
12 |
13 | [](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 |
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