spaceinvaders.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828
  1. // Constants for the keyboard.
  2. var KEY_LEFT = 37;
  3. var KEY_RIGHT = 39;
  4. var KEY_SPACE = 32;
  5. // Creates an instance of the Game class.
  6. function Game() {
  7. // Set the initial config.
  8. this.config = {
  9. bombRate: 0.05,
  10. bombMinVelocity: 50,
  11. bombMaxVelocity: 50,
  12. invaderInitialVelocity: 25,
  13. invaderAcceleration: 0,
  14. invaderDropDistance: 20,
  15. rocketVelocity: 120,
  16. rocketMaxFireRate: 2,
  17. gameWidth: 400,
  18. gameHeight: 300,
  19. fps: 50,
  20. debugMode: false,
  21. invaderRanks: 5,
  22. invaderFiles: 10,
  23. shipSpeed: 120,
  24. levelDifficultyMultiplier: 0.2,
  25. pointsPerInvader: 5,
  26. limitLevelIncrease: 25
  27. };
  28. // All state is in the variables below.
  29. this.lives = 3;
  30. this.width = 0;
  31. this.height = 0;
  32. this.gameBounds = {left: 0, top: 0, right: 0, bottom: 0};
  33. this.intervalId = 0;
  34. this.score = 0;
  35. this.level = 1;
  36. // The state stack.
  37. this.stateStack = [];
  38. // Input/output
  39. this.pressedKeys = {};
  40. this.gameCanvas = null;
  41. // All sounds.
  42. this.sounds = null;
  43. // The previous x position, used for touch.
  44. this.previousX = 0;
  45. }
  46. // Initialis the Game with a canvas.
  47. Game.prototype.initialise = function(gameCanvas) {
  48. // Set the game canvas.
  49. this.gameCanvas = gameCanvas;
  50. // Set the game width and height.
  51. this.width = gameCanvas.width;
  52. this.height = gameCanvas.height;
  53. // Set the state game bounds.
  54. this.gameBounds = {
  55. left: gameCanvas.width / 2 - this.config.gameWidth / 2,
  56. right: gameCanvas.width / 2 + this.config.gameWidth / 2,
  57. top: gameCanvas.height / 2 - this.config.gameHeight / 2,
  58. bottom: gameCanvas.height / 2 + this.config.gameHeight / 2,
  59. };
  60. };
  61. Game.prototype.moveToState = function(state) {
  62. // If we are in a state, leave it.
  63. if(this.currentState() && this.currentState().leave) {
  64. this.currentState().leave(game);
  65. this.stateStack.pop();
  66. }
  67. // If there's an enter function for the new state, call it.
  68. if(state.enter) {
  69. state.enter(game);
  70. }
  71. // Set the current state.
  72. this.stateStack.pop();
  73. this.stateStack.push(state);
  74. };
  75. // Start the Game.
  76. Game.prototype.start = function() {
  77. // Move into the 'welcome' state.
  78. this.moveToState(new WelcomeState());
  79. // Set the game variables.
  80. this.lives = 3;
  81. this.config.debugMode = /debug=true/.test(window.location.href);
  82. // Start the game loop.
  83. var game = this;
  84. this.intervalId = setInterval(function () { GameLoop(game);}, 1000 / this.config.fps);
  85. };
  86. // Returns the current state.
  87. Game.prototype.currentState = function() {
  88. return this.stateStack.length > 0 ? this.stateStack[this.stateStack.length - 1] : null;
  89. };
  90. // Mutes or unmutes the game.
  91. Game.prototype.mute = function(mute) {
  92. // If we've been told to mute, mute.
  93. if(mute === true) {
  94. this.sounds.mute = true;
  95. } else if (mute === false) {
  96. this.sounds.mute = false;
  97. } else {
  98. // Toggle mute instead...
  99. this.sounds.mute = this.sounds.mute ? false : true;
  100. }
  101. };
  102. // The main loop.
  103. function GameLoop(game) {
  104. var currentState = game.currentState();
  105. if(currentState) {
  106. // Delta t is the time to update/draw.
  107. var dt = 1 / game.config.fps;
  108. // Get the drawing context.
  109. var ctx = this.gameCanvas.getContext("2d");
  110. // Update if we have an update function. Also draw
  111. // if we have a draw function.
  112. if(currentState.update) {
  113. currentState.update(game, dt);
  114. }
  115. if(currentState.draw) {
  116. currentState.draw(game, dt, ctx);
  117. }
  118. }
  119. }
  120. Game.prototype.pushState = function(state) {
  121. // If there's an enter function for the new state, call it.
  122. if(state.enter) {
  123. state.enter(game);
  124. }
  125. // Set the current state.
  126. this.stateStack.push(state);
  127. };
  128. Game.prototype.popState = function() {
  129. // Leave and pop the state.
  130. if(this.currentState()) {
  131. if(this.currentState().leave) {
  132. this.currentState().leave(game);
  133. }
  134. // Set the current state.
  135. this.stateStack.pop();
  136. }
  137. };
  138. // The stop function stops the game.
  139. Game.prototype.stop = function Stop() {
  140. clearInterval(this.intervalId);
  141. };
  142. // Inform the game a key is down.
  143. Game.prototype.keyDown = function(keyCode) {
  144. this.pressedKeys[keyCode] = true;
  145. // Delegate to the current state too.
  146. if(this.currentState() && this.currentState().keyDown) {
  147. this.currentState().keyDown(this, keyCode);
  148. }
  149. };
  150. Game.prototype.touchstart = function(s) {
  151. if(this.currentState() && this.currentState().keyDown) {
  152. this.currentState().keyDown(this, KEY_SPACE);
  153. }
  154. };
  155. Game.prototype.touchend = function(s) {
  156. delete this.pressedKeys[KEY_RIGHT];
  157. delete this.pressedKeys[KEY_LEFT];
  158. };
  159. Game.prototype.touchmove = function(e) {
  160. var currentX = e.changedTouches[0].pageX;
  161. if (this.previousX > 0) {
  162. if (currentX > this.previousX) {
  163. delete this.pressedKeys[KEY_LEFT];
  164. this.pressedKeys[KEY_RIGHT] = true;
  165. } else {
  166. delete this.pressedKeys[KEY_RIGHT];
  167. this.pressedKeys[KEY_LEFT] = true;
  168. }
  169. }
  170. this.previousX = currentX;
  171. };
  172. // Inform the game a key is up.
  173. Game.prototype.keyUp = function(keyCode) {
  174. delete this.pressedKeys[keyCode];
  175. // Delegate to the current state too.
  176. if(this.currentState() && this.currentState().keyUp) {
  177. this.currentState().keyUp(this, keyCode);
  178. }
  179. };
  180. function WelcomeState() {
  181. }
  182. WelcomeState.prototype.enter = function(game) {
  183. // Create and load the sounds.
  184. game.sounds = new Sounds();
  185. game.sounds.init();
  186. game.sounds.loadSound('shoot', 'sounds/shoot.wav');
  187. game.sounds.loadSound('bang', 'sounds/bang.wav');
  188. game.sounds.loadSound('explosion', 'sounds/explosion.wav');
  189. };
  190. WelcomeState.prototype.update = function (game, dt) {
  191. };
  192. WelcomeState.prototype.draw = function(game, dt, ctx) {
  193. // Clear the background.
  194. ctx.clearRect(0, 0, game.width, game.height);
  195. ctx.font="30px Arial";
  196. ctx.fillStyle = '#ffffff';
  197. ctx.textBaseline="middle";
  198. ctx.textAlign="center";
  199. ctx.fillText("Space Invaders", game.width / 2, game.height/2 - 40);
  200. ctx.font="16px Arial";
  201. ctx.fillText("Press 'Space' or touch to start.", game.width / 2, game.height/2);
  202. };
  203. WelcomeState.prototype.keyDown = function(game, keyCode) {
  204. if(keyCode == KEY_SPACE) {
  205. // Space starts the game.
  206. game.level = 1;
  207. game.score = 0;
  208. game.lives = 3;
  209. game.moveToState(new LevelIntroState(game.level));
  210. }
  211. };
  212. function GameOverState() {
  213. }
  214. GameOverState.prototype.update = function(game, dt) {
  215. };
  216. GameOverState.prototype.draw = function(game, dt, ctx) {
  217. // Clear the background.
  218. ctx.clearRect(0, 0, game.width, game.height);
  219. ctx.font="30px Arial";
  220. ctx.fillStyle = '#ffffff';
  221. ctx.textBaseline="center";
  222. ctx.textAlign="center";
  223. ctx.fillText("Game Over!", game.width / 2, game.height/2 - 40);
  224. ctx.font="16px Arial";
  225. ctx.fillText("You scored " + game.score + " and got to level " + game.level, game.width / 2, game.height/2);
  226. ctx.font="16px Arial";
  227. ctx.fillText("Press 'Space' to play again.", game.width / 2, game.height/2 + 40);
  228. };
  229. GameOverState.prototype.keyDown = function(game, keyCode) {
  230. if(keyCode == KEY_SPACE) {
  231. // Space restarts the game.
  232. game.lives = 3;
  233. game.score = 0;
  234. game.level = 1;
  235. game.moveToState(new LevelIntroState(1));
  236. }
  237. };
  238. // Create a PlayState with the game config and the level you are on.
  239. function PlayState(config, level) {
  240. this.config = config;
  241. this.level = level;
  242. // Game state.
  243. this.invaderCurrentVelocity = 10;
  244. this.invaderCurrentDropDistance = 0;
  245. this.invadersAreDropping = false;
  246. this.lastRocketTime = null;
  247. // Game entities.
  248. this.ship = null;
  249. this.invaders = [];
  250. this.rockets = [];
  251. this.bombs = [];
  252. }
  253. PlayState.prototype.enter = function(game) {
  254. // Create the ship.
  255. this.ship = new Ship(game.width / 2, game.gameBounds.bottom);
  256. // Setup initial state.
  257. this.invaderCurrentVelocity = 10;
  258. this.invaderCurrentDropDistance = 0;
  259. this.invadersAreDropping = false;
  260. // Set the ship speed for this level, as well as invader params.
  261. var levelMultiplier = this.level * this.config.levelDifficultyMultiplier;
  262. var limitLevel = (this.level < this.config.limitLevelIncrease ? this.level : this.config.limitLevelIncrease);
  263. this.shipSpeed = this.config.shipSpeed;
  264. this.invaderInitialVelocity = this.config.invaderInitialVelocity + 1.5 * (levelMultiplier * this.config.invaderInitialVelocity);
  265. this.bombRate = this.config.bombRate + (levelMultiplier * this.config.bombRate);
  266. this.bombMinVelocity = this.config.bombMinVelocity + (levelMultiplier * this.config.bombMinVelocity);
  267. this.bombMaxVelocity = this.config.bombMaxVelocity + (levelMultiplier * this.config.bombMaxVelocity);
  268. this.rocketMaxFireRate = this.config.rocketMaxFireRate + 0.4 * limitLevel;
  269. // Create the invaders.
  270. var ranks = this.config.invaderRanks + 0.1 * limitLevel;
  271. var files = this.config.invaderFiles + 0.2 * limitLevel;
  272. var invaders = [];
  273. for(var rank = 0; rank < ranks; rank++){
  274. for(var file = 0; file < files; file++) {
  275. invaders.push(new Invader(
  276. (game.width / 2) + ((files/2 - file) * 200 / files),
  277. (game.gameBounds.top + rank * 20),
  278. rank, file, 'Invader'));
  279. }
  280. }
  281. this.invaders = invaders;
  282. this.invaderCurrentVelocity = this.invaderInitialVelocity;
  283. this.invaderVelocity = {x: -this.invaderInitialVelocity, y:0};
  284. this.invaderNextVelocity = null;
  285. };
  286. PlayState.prototype.update = function(game, dt) {
  287. // If the left or right arrow keys are pressed, move
  288. // the ship. Check this on ticks rather than via a keydown
  289. // event for smooth movement, otherwise the ship would move
  290. // more like a text editor caret.
  291. if(game.pressedKeys[KEY_LEFT]) {
  292. this.ship.x -= this.shipSpeed * dt;
  293. }
  294. if(game.pressedKeys[KEY_RIGHT]) {
  295. this.ship.x += this.shipSpeed * dt;
  296. }
  297. if(game.pressedKeys[KEY_SPACE]) {
  298. this.fireRocket();
  299. }
  300. // Keep the ship in bounds.
  301. if(this.ship.x < game.gameBounds.left) {
  302. this.ship.x = game.gameBounds.left;
  303. }
  304. if(this.ship.x > game.gameBounds.right) {
  305. this.ship.x = game.gameBounds.right;
  306. }
  307. // Move each bomb.
  308. for(var i=0; i<this.bombs.length; i++) {
  309. var bomb = this.bombs[i];
  310. bomb.y += dt * bomb.velocity;
  311. // If the rocket has gone off the screen remove it.
  312. if(bomb.y > this.height) {
  313. this.bombs.splice(i--, 1);
  314. }
  315. }
  316. // Move each rocket.
  317. for(i=0; i<this.rockets.length; i++) {
  318. var rocket = this.rockets[i];
  319. rocket.y -= dt * rocket.velocity;
  320. // If the rocket has gone off the screen remove it.
  321. if(rocket.y < 0) {
  322. this.rockets.splice(i--, 1);
  323. }
  324. }
  325. // Move the invaders.
  326. var hitLeft = false, hitRight = false, hitBottom = false;
  327. for(i=0; i<this.invaders.length; i++) {
  328. var invader = this.invaders[i];
  329. var newx = invader.x + this.invaderVelocity.x * dt;
  330. var newy = invader.y + this.invaderVelocity.y * dt;
  331. if(hitLeft == false && newx < game.gameBounds.left) {
  332. hitLeft = true;
  333. }
  334. else if(hitRight == false && newx > game.gameBounds.right) {
  335. hitRight = true;
  336. }
  337. else if(hitBottom == false && newy > game.gameBounds.bottom) {
  338. hitBottom = true;
  339. }
  340. if(!hitLeft && !hitRight && !hitBottom) {
  341. invader.x = newx;
  342. invader.y = newy;
  343. }
  344. }
  345. // Update invader velocities.
  346. if(this.invadersAreDropping) {
  347. this.invaderCurrentDropDistance += this.invaderVelocity.y * dt;
  348. if(this.invaderCurrentDropDistance >= this.config.invaderDropDistance) {
  349. this.invadersAreDropping = false;
  350. this.invaderVelocity = this.invaderNextVelocity;
  351. this.invaderCurrentDropDistance = 0;
  352. }
  353. }
  354. // If we've hit the left, move down then right.
  355. if(hitLeft) {
  356. this.invaderCurrentVelocity += this.config.invaderAcceleration;
  357. this.invaderVelocity = {x: 0, y:this.invaderCurrentVelocity };
  358. this.invadersAreDropping = true;
  359. this.invaderNextVelocity = {x: this.invaderCurrentVelocity , y:0};
  360. }
  361. // If we've hit the right, move down then left.
  362. if(hitRight) {
  363. this.invaderCurrentVelocity += this.config.invaderAcceleration;
  364. this.invaderVelocity = {x: 0, y:this.invaderCurrentVelocity };
  365. this.invadersAreDropping = true;
  366. this.invaderNextVelocity = {x: -this.invaderCurrentVelocity , y:0};
  367. }
  368. // If we've hit the bottom, it's game over.
  369. if(hitBottom) {
  370. game.lives = 0;
  371. }
  372. // Check for rocket/invader collisions.
  373. for(i=0; i<this.invaders.length; i++) {
  374. var invader = this.invaders[i];
  375. var bang = false;
  376. for(var j=0; j<this.rockets.length; j++){
  377. var rocket = this.rockets[j];
  378. if(rocket.x >= (invader.x - invader.width/2) && rocket.x <= (invader.x + invader.width/2) &&
  379. rocket.y >= (invader.y - invader.height/2) && rocket.y <= (invader.y + invader.height/2)) {
  380. // Remove the rocket, set 'bang' so we don't process
  381. // this rocket again.
  382. this.rockets.splice(j--, 1);
  383. bang = true;
  384. game.score += this.config.pointsPerInvader;
  385. break;
  386. }
  387. }
  388. if(bang) {
  389. this.invaders.splice(i--, 1);
  390. game.sounds.playSound('bang');
  391. }
  392. }
  393. // Find all of the front rank invaders.
  394. var frontRankInvaders = {};
  395. for(var i=0; i<this.invaders.length; i++) {
  396. var invader = this.invaders[i];
  397. // If we have no invader for game file, or the invader
  398. // for game file is futher behind, set the front
  399. // rank invader to game one.
  400. if(!frontRankInvaders[invader.file] || frontRankInvaders[invader.file].rank < invader.rank) {
  401. frontRankInvaders[invader.file] = invader;
  402. }
  403. }
  404. // Give each front rank invader a chance to drop a bomb.
  405. for(var i=0; i<this.config.invaderFiles; i++) {
  406. var invader = frontRankInvaders[i];
  407. if(!invader) continue;
  408. var chance = this.bombRate * dt;
  409. if(chance > Math.random()) {
  410. // Fire!
  411. this.bombs.push(new Bomb(invader.x, invader.y + invader.height / 2,
  412. this.bombMinVelocity + Math.random()*(this.bombMaxVelocity - this.bombMinVelocity)));
  413. }
  414. }
  415. // Check for bomb/ship collisions.
  416. for(var i=0; i<this.bombs.length; i++) {
  417. var bomb = this.bombs[i];
  418. if(bomb.x >= (this.ship.x - this.ship.width/2) && bomb.x <= (this.ship.x + this.ship.width/2) &&
  419. bomb.y >= (this.ship.y - this.ship.height/2) && bomb.y <= (this.ship.y + this.ship.height/2)) {
  420. this.bombs.splice(i--, 1);
  421. game.lives--;
  422. game.sounds.playSound('explosion');
  423. }
  424. }
  425. // Check for invader/ship collisions.
  426. for(var i=0; i<this.invaders.length; i++) {
  427. var invader = this.invaders[i];
  428. if((invader.x + invader.width/2) > (this.ship.x - this.ship.width/2) &&
  429. (invader.x - invader.width/2) < (this.ship.x + this.ship.width/2) &&
  430. (invader.y + invader.height/2) > (this.ship.y - this.ship.height/2) &&
  431. (invader.y - invader.height/2) < (this.ship.y + this.ship.height/2)) {
  432. // Dead by collision!
  433. game.lives = 0;
  434. game.sounds.playSound('explosion');
  435. }
  436. }
  437. // Check for failure
  438. if(game.lives <= 0) {
  439. game.moveToState(new GameOverState());
  440. }
  441. // Check for victory
  442. if(this.invaders.length === 0) {
  443. game.score += this.level * 50;
  444. game.level += 1;
  445. game.moveToState(new LevelIntroState(game.level));
  446. }
  447. };
  448. PlayState.prototype.draw = function(game, dt, ctx) {
  449. // Clear the background.
  450. ctx.clearRect(0, 0, game.width, game.height);
  451. // Draw ship.
  452. ctx.fillStyle = '#999999';
  453. ctx.fillRect(this.ship.x - (this.ship.width / 2), this.ship.y - (this.ship.height / 2), this.ship.width, this.ship.height);
  454. // Draw invaders.
  455. ctx.fillStyle = '#006600';
  456. for(var i=0; i<this.invaders.length; i++) {
  457. var invader = this.invaders[i];
  458. ctx.fillRect(invader.x - invader.width/2, invader.y - invader.height/2, invader.width, invader.height);
  459. }
  460. // Draw bombs.
  461. ctx.fillStyle = '#ff5555';
  462. for(var i=0; i<this.bombs.length; i++) {
  463. var bomb = this.bombs[i];
  464. ctx.fillRect(bomb.x - 2, bomb.y - 2, 4, 4);
  465. }
  466. // Draw rockets.
  467. ctx.fillStyle = '#ff0000';
  468. for(var i=0; i<this.rockets.length; i++) {
  469. var rocket = this.rockets[i];
  470. ctx.fillRect(rocket.x, rocket.y - 2, 1, 4);
  471. }
  472. // Draw info.
  473. var textYpos = game.gameBounds.bottom + ((game.height - game.gameBounds.bottom) / 2) + 14/2;
  474. ctx.font="14px Arial";
  475. ctx.fillStyle = '#ffffff';
  476. var info = "Lives: " + game.lives;
  477. ctx.textAlign = "left";
  478. ctx.fillText(info, game.gameBounds.left, textYpos);
  479. info = "Score: " + game.score + ", Level: " + game.level;
  480. ctx.textAlign = "right";
  481. ctx.fillText(info, game.gameBounds.right, textYpos);
  482. // If we're in debug mode, draw bounds.
  483. if(this.config.debugMode) {
  484. ctx.strokeStyle = '#ff0000';
  485. ctx.strokeRect(0,0,game.width, game.height);
  486. ctx.strokeRect(game.gameBounds.left, game.gameBounds.top,
  487. game.gameBounds.right - game.gameBounds.left,
  488. game.gameBounds.bottom - game.gameBounds.top);
  489. }
  490. };
  491. PlayState.prototype.keyDown = function(game, keyCode) {
  492. if(keyCode == KEY_SPACE) {
  493. // Fire!
  494. this.fireRocket();
  495. }
  496. if(keyCode == 80) {
  497. // Push the pause state.
  498. game.pushState(new PauseState());
  499. }
  500. };
  501. PlayState.prototype.keyUp = function(game, keyCode) {
  502. };
  503. PlayState.prototype.fireRocket = function() {
  504. // If we have no last rocket time, or the last rocket time
  505. // is older than the max rocket rate, we can fire.
  506. if(this.lastRocketTime === null || ((new Date()).valueOf() - this.lastRocketTime) > (1000 / this.rocketMaxFireRate))
  507. {
  508. // Add a rocket.
  509. this.rockets.push(new Rocket(this.ship.x, this.ship.y - 12, this.config.rocketVelocity));
  510. this.lastRocketTime = (new Date()).valueOf();
  511. // Play the 'shoot' sound.
  512. game.sounds.playSound('shoot');
  513. }
  514. };
  515. function PauseState() {
  516. }
  517. PauseState.prototype.keyDown = function(game, keyCode) {
  518. if(keyCode == 80) {
  519. // Pop the pause state.
  520. game.popState();
  521. }
  522. };
  523. PauseState.prototype.draw = function(game, dt, ctx) {
  524. // Clear the background.
  525. ctx.clearRect(0, 0, game.width, game.height);
  526. ctx.font="14px Arial";
  527. ctx.fillStyle = '#ffffff';
  528. ctx.textBaseline="middle";
  529. ctx.textAlign="center";
  530. ctx.fillText("Paused", game.width / 2, game.height/2);
  531. return;
  532. };
  533. /*
  534. Level Intro State
  535. The Level Intro state shows a 'Level X' message and
  536. a countdown for the level.
  537. */
  538. function LevelIntroState(level) {
  539. this.level = level;
  540. this.countdownMessage = "3";
  541. }
  542. LevelIntroState.prototype.update = function(game, dt) {
  543. // Update the countdown.
  544. if(this.countdown === undefined) {
  545. this.countdown = 3; // countdown from 3 secs
  546. }
  547. this.countdown -= dt;
  548. if(this.countdown < 2) {
  549. this.countdownMessage = "2";
  550. }
  551. if(this.countdown < 1) {
  552. this.countdownMessage = "1";
  553. }
  554. if(this.countdown <= 0) {
  555. // Move to the next level, popping this state.
  556. game.moveToState(new PlayState(game.config, this.level));
  557. }
  558. };
  559. LevelIntroState.prototype.draw = function(game, dt, ctx) {
  560. // Clear the background.
  561. ctx.clearRect(0, 0, game.width, game.height);
  562. ctx.font="36px Arial";
  563. ctx.fillStyle = '#ffffff';
  564. ctx.textBaseline="middle";
  565. ctx.textAlign="center";
  566. ctx.fillText("Level " + this.level, game.width / 2, game.height/2);
  567. ctx.font="24px Arial";
  568. ctx.fillText("Ready in " + this.countdownMessage, game.width / 2, game.height/2 + 36);
  569. return;
  570. };
  571. /*
  572. Ship
  573. The ship has a position and that's about it.
  574. */
  575. function Ship(x, y) {
  576. this.x = x;
  577. this.y = y;
  578. this.width = 20;
  579. this.height = 16;
  580. }
  581. /*
  582. Rocket
  583. Fired by the ship, they've got a position, velocity and state.
  584. */
  585. function Rocket(x, y, velocity) {
  586. this.x = x;
  587. this.y = y;
  588. this.velocity = velocity;
  589. }
  590. /*
  591. Bomb
  592. Dropped by invaders, they've got position, velocity.
  593. */
  594. function Bomb(x, y, velocity) {
  595. this.x = x;
  596. this.y = y;
  597. this.velocity = velocity;
  598. }
  599. /*
  600. Invader
  601. Invader's have position, type, rank/file and that's about it.
  602. */
  603. function Invader(x, y, rank, file, type) {
  604. this.x = x;
  605. this.y = y;
  606. this.rank = rank;
  607. this.file = file;
  608. this.type = type;
  609. this.width = 18;
  610. this.height = 14;
  611. }
  612. /*
  613. Game State
  614. A Game State is simply an update and draw proc.
  615. When a game is in the state, the update and draw procs are
  616. called, with a dt value (dt is delta time, i.e. the number)
  617. of seconds to update or draw).
  618. */
  619. function GameState(updateProc, drawProc, keyDown, keyUp, enter, leave) {
  620. this.updateProc = updateProc;
  621. this.drawProc = drawProc;
  622. this.keyDown = keyDown;
  623. this.keyUp = keyUp;
  624. this.enter = enter;
  625. this.leave = leave;
  626. }
  627. /*
  628. Sounds
  629. The sounds class is used to asynchronously load sounds and allow
  630. them to be played.
  631. */
  632. function Sounds() {
  633. // The audio context.
  634. this.audioContext = null;
  635. // The actual set of loaded sounds.
  636. this.sounds = {};
  637. }
  638. Sounds.prototype.init = function() {
  639. // Create the audio context, paying attention to webkit browsers.
  640. context = window.AudioContext || window.webkitAudioContext;
  641. this.audioContext = new context();
  642. this.mute = false;
  643. };
  644. Sounds.prototype.loadSound = function(name, url) {
  645. // Reference to ourselves for closures.
  646. var self = this;
  647. // Create an entry in the sounds object.
  648. this.sounds[name] = null;
  649. // Create an asynchronous request for the sound.
  650. var req = new XMLHttpRequest();
  651. req.open('GET', url, true);
  652. req.responseType = 'arraybuffer';
  653. req.onload = function() {
  654. self.audioContext.decodeAudioData(req.response, function(buffer) {
  655. self.sounds[name] = {buffer: buffer};
  656. });
  657. };
  658. try {
  659. req.send();
  660. } catch(e) {
  661. console.log("An exception occured getting sound the sound " + name + " this might be " +
  662. "because the page is running from the file system, not a webserver.");
  663. console.log(e);
  664. }
  665. };
  666. Sounds.prototype.playSound = function(name) {
  667. // If we've not got the sound, don't bother playing it.
  668. if(this.sounds[name] === undefined || this.sounds[name] === null || this.mute === true) {
  669. return;
  670. }
  671. // Create a sound source, set the buffer, connect to the speakers and
  672. // play the sound.
  673. var source = this.audioContext.createBufferSource();
  674. source.buffer = this.sounds[name].buffer;
  675. source.connect(this.audioContext.destination);
  676. source.start(0);
  677. };