├── .gitignore ├── assets ├── nyanlooped.mp3 ├── nyanlooped.ogg ├── dag-sprite-sheet.png ├── nyan-sprite-sheet.png └── asteroids-screenshot.png ├── package.json ├── LICENSE ├── asteroids.html ├── README.md └── asteroids.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Misc. Other 2 | ############## 3 | .idea 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /assets/nyanlooped.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puppetlabs/asteroids/master/assets/nyanlooped.mp3 -------------------------------------------------------------------------------- /assets/nyanlooped.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puppetlabs/asteroids/master/assets/nyanlooped.ogg -------------------------------------------------------------------------------- /assets/dag-sprite-sheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puppetlabs/asteroids/master/assets/dag-sprite-sheet.png -------------------------------------------------------------------------------- /assets/nyan-sprite-sheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puppetlabs/asteroids/master/assets/nyan-sprite-sheet.png -------------------------------------------------------------------------------- /assets/asteroids-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puppetlabs/asteroids/master/assets/asteroids-screenshot.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "asteroids", 3 | "version": "1.0.0", 4 | "description": "Asteroids! ... Or something kind of like it.", 5 | "main": "asteroids.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/Xaxis/asteroids.git" 12 | }, 13 | "author": "Wil Neeley", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/Xaxis/asteroids/issues" 17 | }, 18 | "homepage": "https://github.com/Xaxis/asteroids#readme" 19 | } 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Wil Neeley 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 | 23 | -------------------------------------------------------------------------------- /asteroids.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Asteroids! 6 | 11 | 12 | 13 | 14 | 15 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Asteroids! ... Or something kind of like it. 2 | 3 | # Introduction 4 | 5 | In my free time I coded a version of Asteroids using JavaScript and the 6 | canvas element. This was just a little experiment so it's unlikely I'll 7 | improve it much more. I'm posting it on GitHub in case anyone finds any 8 | of the bits of code useful. Programming Asteroids is a really great way 9 | to get started understanding the basics of 2D video games. Enjoy! 10 | 11 | ![alt text](https://raw.githubusercontent.com/Xaxis/asteroids/master/assets/asteroids-screenshot.png "Asteroids in Nyan Mode") 12 | 13 | View [demo](http://boilerjs.com/misc/asteroids/asteroids.html). 14 | 15 | ## Up and running 16 | 17 | Include the asteroids.js file in the header of your page and add a canvas 18 | element with an id of `asteroids-container`, like this: 19 | 20 | ``` 21 | 22 | ``` 23 | 24 | Then include the following script to initialize the game. 25 | 26 | ``` 27 | 68 | ``` 69 | 70 | You can define the parameters and overall difficulty of each level you 71 | create. 72 | 73 | 74 | -------------------------------------------------------------------------------- /asteroids.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Asteroids! 3 | * 4 | * (a) Wil Neeley 5 | * (c) Code may be freely distributed under the MIT license. 6 | */ 7 | (function(root, factory) { 8 | if (typeof define === 'function' && define.amd) { 9 | define('asteroids', [], factory); 10 | } else { 11 | root.Asteroids = factory(); 12 | } 13 | })(this, function() { 14 | var Asteroids = function() { 15 | var as = {}; 16 | 17 | // Debug mode on or off 18 | as.debug_mode = false; 19 | 20 | // Reference the asteroids scene 21 | as.canvas = document.getElementById('asteroids-container'); 22 | 23 | // Stores a reference to the ship object 24 | as.ship = {}; 25 | 26 | // Indicates whether or not the ship is currently destroyed 27 | as.ship_exploded = false; 28 | 29 | // Ships exploding parts 30 | as.ship_exploding_parts = []; 31 | 32 | // Stores references to active missiles 33 | as.missiles = []; 34 | 35 | // Speed at which missiles fire 36 | as.missile_fire_rate = 250; 37 | 38 | // Stores references to asteroids in the scene 39 | as.asteroids = []; 40 | 41 | // Asteroid polygon coords 42 | as.asteroid_coords = { 43 | 1: [ 44 | [-39, -25, -33, -8, -38, 21, -23, 25, -13, 39, 24, 34, 38, 7, 33, -15, 38, -31, 16, -39, -4, -34, -16, -39], 45 | [-32, 35, -4, 32, 24, 38, 38, 23, 31, -4, 38, -25, 14, -39, -28, -31, -39, -16, -31, 4, -38, 22], 46 | [12, -39, -2, -26, -28, -37, -38, -14, -21, 9, -34, 34, -6, 38, 35, 23, 21, -14, 36, -25] 47 | ], 48 | 2: [ 49 | [-7, -19, -19, -15, -12, -5, -19, 0, -19, 13, -9, 19, 12, 16, 18, 11, 13, 6, 19, -1, 16, -17], 50 | [9, -19, 18, -8, 7, 0, 15, 15, -7, 13, -16, 17, -18, 3, -13, -6, -16, -17], 51 | [2, 18, 18, 10, 8, 0, 18, -13, 6, -18, -17, -14, -10, -3, -13, 15] 52 | ], 53 | 3: [ 54 | [-8, -8, -5, -1, -8, 3, 0, 9, 8, 4, 8, -5, 1, -9], 55 | [-6, 8, 1, 4, 8, 7, 10, -1, 4, -10, -8, -6, -4, 0], 56 | [-8, -9, -5, -2, -8, 5, 6, 8, 9, 6, 7, -3, 9, -9, 0, -7] 57 | ] 58 | }; 59 | 60 | // Direction keys asteroids can travel 61 | as.asteroid_dirs = ['ul', 'ur', 'dl', 'dr']; 62 | 63 | // Asteroids explosion particle objects 64 | as.asteroid_particles = []; 65 | 66 | // Stores references to UFOs in the scene 67 | as.ufos = []; 68 | 69 | // Reference active directional keys 70 | as.active_keys = { 71 | 37: false, 72 | 38: false, 73 | 39: false, 74 | 40: false 75 | }; 76 | 77 | // Store current game stats 78 | as.stats = { 79 | level: 1, 80 | score: 0, 81 | score_asteroid_gain: 20, 82 | score_ufo_gain: 100, 83 | score_death_loss: 50 84 | }; 85 | 86 | // Is the game active or paused? 87 | as.game_status = 'paused'; 88 | 89 | /** 90 | * Initialize game. 91 | */ 92 | as.init = function(options) { 93 | 94 | // Configure startup overrides (debug mode) 95 | as.debug_mode = options.debug_mode || false; 96 | 97 | // Are we going to be in nyan mode? 98 | as.nyan_mode = options.nyan_mode || false; 99 | 100 | // Nyan path override? 101 | as.nyan_path_override = options.nyan_path_override || ''; 102 | 103 | // Nyan sound? 104 | as.nyan_sound = options.nyan_sound || false; 105 | 106 | // Configure game levels (REQUIRED!) 107 | as.levels = options.levels; 108 | 109 | // Configure game completed callback 110 | as.gameCompletedCallback = options.gameCompleted; 111 | 112 | // Determine the total level count 113 | var total_count = 0; 114 | for (var c in as.levels) { 115 | total_count += 1; 116 | } 117 | as.stats.level_count = total_count; 118 | 119 | // Configure canvas 120 | as.canvas_w = as.canvas.width; 121 | as.canvas_h = as.canvas.height; 122 | as.ctx = as.canvas.getContext('2d'); 123 | window.addEventListener('keydown', as.shipInput); 124 | window.addEventListener('keyup', as.shipInput); 125 | as.startGameScreen(); 126 | }; 127 | 128 | /** 129 | * Game engine. 130 | */ 131 | as.core = { 132 | throttle: function(fn, delay, scope) { 133 | delay || (delay = 250); 134 | var 135 | last, 136 | deferTimer; 137 | return function() { 138 | var 139 | context = scope || this, 140 | now = +new Date, 141 | args = arguments; 142 | if (last && now < last + delay) { 143 | clearTimeout(deferTimer); 144 | deferTimer = setTimeout(function() { 145 | last = now; 146 | fn.apply(context, args); 147 | }, delay); 148 | } else { 149 | last = now; 150 | fn.apply(context, args); 151 | } 152 | } 153 | }, 154 | frame: function() { 155 | as.core.setDelta(); 156 | if (!as.core.render()) return false; 157 | as.core.animationFrame = window.requestAnimationFrame(as.core.frame); 158 | }, 159 | setDelta: function() { 160 | as.core.now = Date.now(); 161 | as.core.delta = (as.core.now - as.core.then) / 1000; 162 | as.core.then = as.core.now; 163 | }, 164 | render: function() { 165 | 166 | // Clear scene 167 | as.ctx.clearRect(0, 0, as.canvas_w, as.canvas_h); 168 | 169 | // Level cleared (all asteroids and UFOs destroyed)? 170 | if (!as.asteroids.length && !as.ufos.length) { 171 | window.cancelAnimationFrame(as.core.animationFrame); 172 | 173 | // End the game when all opponents are destroyed and all levels completed 174 | if (as.stats.level >= as.stats.level_count) { 175 | 176 | // End the game 177 | as.gameCompleted(); 178 | return false; 179 | } else { 180 | 181 | // Increment game level by how many? 182 | as.updateLevel(1); 183 | 184 | // Start game and next level 185 | as.startGame(); 186 | } 187 | } 188 | 189 | // Update the game score 190 | as.updateScore().draw(); 191 | 192 | // Update the game level 193 | as.updateLevel().draw(); 194 | 195 | // Render/handle ship interactions 196 | as.renderShip(); 197 | 198 | // Render/handle UFO interactions 199 | as.renderUfos(); 200 | 201 | // Render/handle asteroid interactions 202 | as.renderAsteroids(); 203 | 204 | // Return true otherwise the render loop is killed 205 | return true; 206 | }, 207 | sprite: function( options ) { 208 | var sprite = { 209 | ctx: options.ctx, 210 | width: options.image.width, 211 | height: options.image.height, 212 | image: options.image, 213 | scale: options.scale, 214 | w: options.image.width * options.scale, 215 | h: options.image.height * options.scale, 216 | frame_w: options.frame_w, 217 | frame_h: options.frame_h, 218 | frames: options.frames, 219 | frame_idx: options.frame_idx, 220 | frame_rate: options.frame_rate 221 | }; 222 | 223 | // Update frame index at defined animation interval 224 | sprite.interval = setInterval(function() { 225 | if (as.isDirectionalKeyActive()) { 226 | if (sprite.frame_idx < (sprite.frames-1)) { 227 | sprite.frame_idx += 1; 228 | } else { 229 | sprite.frame_idx = 0; 230 | } 231 | } 232 | }, sprite.frame_rate); 233 | 234 | // Return the animating sprite object 235 | return sprite; 236 | } 237 | }; 238 | 239 | /** 240 | * Generates ship parts. 241 | */ 242 | as.explodingShipParts = function( count ) { 243 | for (var pidx = 0; pidx < count; pidx++) { 244 | var 245 | rand_x = Math.ceil(Math.random() * as.ship.size * 3), 246 | rand_y = Math.ceil(Math.random() * as.ship.size * 3), 247 | rand_angle = Math.floor(Math.random() * 360), 248 | rand_turn = Math.floor(Math.random() * 2); 249 | as.ship_exploding_parts.push({ 250 | x: 0, 251 | y: 0, 252 | x2: rand_x, 253 | y2: rand_y, 254 | vx: .25, 255 | vy: .25, 256 | angle: rand_angle, 257 | speed: 0.05, 258 | alpha: 1, 259 | alpha_speed: 0.001, 260 | rotation_speed: 0.2, 261 | rotation_dir: rand_turn, 262 | draw: function() { 263 | as.ctx.strokeStyle = 'rgba(255, 255, 255, ' + this.alpha + ')'; 264 | as.ctx.lineWidth = 1; 265 | 266 | // Rotate exploding parts 267 | if (this.rotation_dir) { 268 | this.angle -= this.rotation_speed; 269 | } else { 270 | this.angle += this.rotation_speed; 271 | } 272 | if (this.angle > 360) this.angle = 0; 273 | if (this.angle < 0) this.angle = 360; 274 | as.ctx.save(); 275 | as.ctx.translate(this.x, this.y); 276 | as.ctx.rotate((Math.PI / 180 * (this.angle))); 277 | as.ctx.translate(-this.x, -this.y); 278 | 279 | // Draw ship part 280 | as.ctx.beginPath(); 281 | as.ctx.moveTo(this.x, this.y); 282 | as.ctx.lineTo(this.x + this.x2, this.y + this.y2); 283 | as.ctx.closePath(); 284 | as.ctx.stroke(); 285 | as.ctx.restore(); 286 | } 287 | }); 288 | } 289 | }; 290 | 291 | /** 292 | * Generates asteroid particles. 293 | */ 294 | as.explodingAsteroidParticles = function( count ) { 295 | as.asteroid_particles = []; 296 | for (var pidx = 0; pidx < count; pidx++) { 297 | as.asteroid_particles.push({ 298 | x: 0, 299 | y: 0, 300 | radius: 1, 301 | vx: .25, 302 | vy: .25, 303 | speed: 0.05, 304 | alpha: 1, 305 | alpha_speed: 0.001, 306 | draw: function() { 307 | as.ctx.fillStyle = 'rgba(255, 255, 255, ' + this.alpha + ')'; 308 | as.ctx.beginPath(); 309 | as.ctx.arc(this.x, this.y, this.radius, 0, 360); 310 | as.ctx.closePath(); 311 | as.ctx.fill(); 312 | } 313 | }); 314 | } 315 | }; 316 | 317 | /** 318 | * Generate the ship. 319 | */ 320 | as.createShip = function() { 321 | as.ship = { 322 | x: as.canvas_w / 2, 323 | y: as.canvas_h / 2, 324 | vx: 0, 325 | vy: 0, 326 | vx2: 0, 327 | vy2: 0, 328 | turn: 5, 329 | speed: 2.4, 330 | friction: 0.99, 331 | size: 10, 332 | color: 'rgba(255, 255, 255, 1)', 333 | angle: 0, 334 | rx: 0, 335 | ry: 0, 336 | radius: 0, 337 | draw: function() { 338 | as.ctx.strokeStyle = this.color; 339 | as.ctx.fillStyle = this.color; 340 | as.ctx.lineWidth = 2; 341 | as.ctx.beginPath(); 342 | 343 | // Rotate the ship 344 | if (this.angle > 360) this.angle = 0; 345 | if (this.angle < 0) this.angle = 360; 346 | as.ctx.save(); 347 | as.ctx.translate(this.x, this.y); 348 | as.ctx.rotate((Math.PI / 180 * (this.angle))); 349 | as.ctx.translate(-this.x, -this.y); 350 | 351 | // Rotate around center of ship 352 | // @todo - should I deal with this? 353 | //as.ctx.translate((this.x + (this.size * 1.5)), this.y); 354 | //as.ctx.translate(-(this.x + (this.size * 1.5)), -this.y); 355 | 356 | // Sides 357 | as.ctx.moveTo(this.x + this.size * 3, this.y + this.size); 358 | as.ctx.lineTo(this.x, this.y); 359 | as.ctx.lineTo(this.x + this.size * 3, this.y - this.size); 360 | 361 | // Back 362 | as.ctx.moveTo((this.x + this.size * 3) - 5, (this.y + this.size) - 2); 363 | as.ctx.lineTo((this.x + this.size * 3) - 5, (this.y - this.size) + 2); 364 | 365 | // Draw ship's outline 366 | as.ctx.closePath(); 367 | as.ctx.stroke(); 368 | 369 | // Draw thruster 370 | if (as.isDirectionalKeyActive()) { 371 | var thrust_x = (this.x + this.size * 3) - 5; 372 | var thrust_y = this.y; 373 | as.ctx.beginPath(); 374 | as.ctx.moveTo(thrust_x, thrust_y); 375 | as.ctx.lineTo(thrust_x + 10, thrust_y); 376 | as.ctx.lineTo(thrust_x, thrust_y - 4); 377 | as.ctx.moveTo(thrust_x, thrust_y); 378 | as.ctx.lineTo(thrust_x + 10, thrust_y); 379 | as.ctx.lineTo(thrust_x, thrust_y + 4); 380 | as.ctx.closePath(); 381 | as.ctx.fill(); 382 | } 383 | 384 | // Update collision circle info 385 | this.rx = this.x + 15; 386 | this.ry = this.y; 387 | this.radius = this.size * 2; 388 | 389 | // Draw collision rectangle around ship 390 | if (as.debug_mode) { 391 | as.ctx.strokeStyle = 'red'; 392 | as.ctx.lineWidth = 2; 393 | as.ctx.beginPath(); 394 | as.ctx.arc(this.rx, this.ry, this.radius, 0, 360); 395 | as.ctx.closePath(); 396 | as.ctx.stroke(); 397 | } 398 | 399 | // Restore transformation state 400 | as.ctx.restore(); 401 | } 402 | }; 403 | }; 404 | 405 | /** 406 | * Generate a cute little stick figure kitty ship. 407 | */ 408 | as.createNyan = function() { 409 | as.ship = { 410 | x: as.canvas_w / 2, 411 | y: as.canvas_h / 2, 412 | vx: 0, 413 | vy: 0, 414 | vx2: 0, 415 | vy2: 0, 416 | turn: 5, 417 | speed: 2.4, 418 | friction: 0.99, 419 | size: 14, 420 | color: 'rgba(255, 255, 0, 1)', 421 | angle: 0, 422 | rx: 0, 423 | ry: 0, 424 | radius: 0, 425 | nyan: null, 426 | draw: function() { 427 | 428 | // Rotate the ship 429 | if (this.angle > 360) this.angle = 0; 430 | if (this.angle < 0) this.angle = 360; 431 | as.ctx.save(); 432 | as.ctx.translate(this.x, this.y); 433 | as.ctx.rotate((Math.PI / 180 * (this.angle))); 434 | as.ctx.translate(-this.x, -this.y); 435 | 436 | // Draw thruster 437 | if (as.isDirectionalKeyActive() && this.nyan) { 438 | var thrust_x = (this.x + this.nyan.frame_w) - 10; 439 | var thrust_y = this.y - 7; 440 | var bow_size = 2.3; 441 | var bow_seg_size = 12; 442 | 443 | // Play the annoying nyan sound track 444 | if (as.nyan_sound) this.nyan.sound.play(); 445 | 446 | // Create offset line segments 447 | for (var i = 0; i <= 2; i++) { 448 | var even = i == i ? !(i%2) : 0; 449 | var bow_offset = ((even) ? (bow_size / 2) : 0); 450 | var timestamp = Date.now(); 451 | var timestamp_even = timestamp == timestamp ? !(timestamp%2) : 0; 452 | if (timestamp_even) bow_offset = bow_offset * -1; 453 | 454 | // Red 455 | as.ctx.beginPath(); 456 | as.ctx.fillStyle = 'rgba(255, 0, 0, 1)'; 457 | as.ctx.rect(thrust_x + (i * bow_seg_size), thrust_y + bow_offset, bow_seg_size, bow_size); 458 | as.ctx.fill(); 459 | as.ctx.closePath(); 460 | 461 | // Orange 462 | as.ctx.beginPath(); 463 | as.ctx.fillStyle = 'rgba(255, 153, 0, 1)'; 464 | as.ctx.rect( thrust_x + (i * bow_seg_size), thrust_y + (bow_size) + bow_offset, bow_seg_size, bow_size); 465 | as.ctx.fill(); 466 | as.ctx.closePath(); 467 | 468 | // Yellow 469 | as.ctx.beginPath(); 470 | as.ctx.fillStyle = 'rgba(255, 255, 0, 1)'; 471 | as.ctx.rect( thrust_x + (i * bow_seg_size), thrust_y + (bow_size * 2) + bow_offset, bow_seg_size, bow_size); 472 | as.ctx.fill(); 473 | as.ctx.closePath(); 474 | 475 | // Green 476 | as.ctx.beginPath(); 477 | as.ctx.fillStyle = 'rgba(51, 255, 0, 1)'; 478 | as.ctx.rect( thrust_x + (i * bow_seg_size), thrust_y + (bow_size * 3) + bow_offset, bow_seg_size, bow_size); 479 | as.ctx.fill(); 480 | as.ctx.closePath(); 481 | 482 | // Blue 483 | as.ctx.beginPath(); 484 | as.ctx.fillStyle = 'rgba(0, 153, 255, 1)'; 485 | as.ctx.rect( thrust_x + (i * bow_seg_size), thrust_y + (bow_size * 4) + bow_offset, bow_seg_size, bow_size); 486 | as.ctx.fill(); 487 | as.ctx.closePath(); 488 | 489 | // Purple 490 | as.ctx.beginPath(); 491 | as.ctx.fillStyle = 'rgba(102, 51, 255, 1)'; 492 | as.ctx.rect( thrust_x + (i * bow_seg_size), thrust_y + (bow_size * 5) + bow_offset, bow_seg_size, bow_size); 493 | as.ctx.fill(); 494 | as.ctx.closePath(); 495 | } 496 | } else if (this.nyan && as.nyan_sound) { 497 | this.nyan.sound.pause(); 498 | } 499 | 500 | // Create the kitty sprite if it doesn't yet exist. 501 | if (!this.nyan) { 502 | 503 | // Create nyan sprite 504 | var nyanImage = new Image(); 505 | nyanImage.src = as.nyan_path_override + 'assets/dag-sprite-sheet.png'; 506 | this.nyan = as.core.sprite({ 507 | ctx: as.ctx, 508 | width: 100, 509 | height: 100, 510 | image: nyanImage, 511 | scale: 1, 512 | frame_w: 36, 513 | frame_h: 20, 514 | frames: 1, 515 | frame_idx: 0, 516 | frame_rate: 100 517 | }); 518 | 519 | // Only create sound player when instructed 520 | if (as.nyan_sound) { 521 | 522 | // Create nyan sound file 523 | this.nyan.sound = document.createElement('audio'); 524 | if (this.nyan.sound.canPlayType('audio/mpeg') != '') { 525 | this.nyan.sound.src = as.nyan_path_override + 'assets/nyanlooped.mp3'; 526 | } else { 527 | this.nyan.sound.src = as.nyan_path_override + 'assets/nyanlooped.ogg'; 528 | } 529 | 530 | // Handle nyan sound event 531 | this.nyan.sound.addEventListener('ended', function() { 532 | // See: http://forestmist.org/2010/04/html5-audio-loops/ 533 | this.currentTime = 0; 534 | }, false); 535 | 536 | // Append the sound track element 537 | document.body.appendChild(this.nyan.sound); 538 | } 539 | } 540 | 541 | // Draw nyan 542 | var sprite_x = this.x; 543 | var sprite_y = this.y - 10; 544 | as.ctx.drawImage( 545 | this.nyan.image, 546 | this.nyan.frame_w * this.nyan.frame_idx, 547 | 0, 548 | this.nyan.frame_w, 549 | this.nyan.frame_h, 550 | sprite_x, 551 | sprite_y, 552 | this.nyan.frame_w, 553 | this.nyan.frame_h 554 | ); 555 | 556 | // Update collision circle info 557 | this.rx = this.x + 15; 558 | this.ry = this.y; 559 | this.radius = this.size; 560 | 561 | // Draw collision rectangle around ship 562 | if (as.debug_mode) { 563 | as.ctx.strokeStyle = 'red'; 564 | as.ctx.lineWidth = 2; 565 | as.ctx.beginPath(); 566 | as.ctx.arc(this.rx, this.ry, this.radius, 0, 360); 567 | as.ctx.closePath(); 568 | as.ctx.stroke(); 569 | } 570 | 571 | // Restore transformation state 572 | as.ctx.restore(); 573 | } 574 | } 575 | }, 576 | 577 | /** 578 | * Handle ship's keyboard input. 579 | */ 580 | as.shipInput = function(e) { 581 | e.preventDefault(); 582 | 583 | // Update active keys 584 | as.active_keys[e.keyCode] = e.type == 'keydown'; 585 | 586 | // Create a missile for firing 587 | if (e.type == 'keydown' && e.keyCode == 32) { 588 | if (!this.missileThrottle) { 589 | this.missileThrottle = as.core.throttle(function() { 590 | as.createMissile(as.ship); 591 | }, as.missile_fire_rate); 592 | } 593 | this.missileThrottle(); 594 | } 595 | 596 | // Reveal the ship after an explosion 597 | as.ship_exploded = false; 598 | as.ship.color = (as.nyan_mode) ? 'yellow' : 'white'; 599 | }; 600 | 601 | /** 602 | * Creates a missile pointing the direction the ship is pointing. 603 | */ 604 | as.createMissile = function( object ) { 605 | as.missiles.push({ 606 | x: object.x, 607 | y: object.y, 608 | x2: 0, 609 | y2: 0, 610 | r1: as.canvas_w, 611 | r2: 6, 612 | rx: 0, 613 | ry: 0, 614 | radius: 4, 615 | color: 'white', 616 | fired_from: object.angle ? 'ship' : 'ufo', 617 | theta: (180 + object.angle || Math.floor(Math.random() * 360)) % 360, 618 | draw: function() { 619 | this.x2 = this.x + this.r2 * Math.cos(Math.PI * this.theta / 180); 620 | this.y2 = this.y + this.r2 * Math.sin(Math.PI * this.theta / 180); 621 | as.ctx.strokeStyle = this.color; 622 | as.ctx.lineWidth = 4; 623 | as.ctx.beginPath(); 624 | as.ctx.moveTo(this.x, this.y); 625 | as.ctx.lineTo(this.x2, this.y2); 626 | as.ctx.stroke(); 627 | this.x = this.x2; 628 | this.y = this.y2; 629 | 630 | // Update collision circle 631 | this.rx = this.x; 632 | this.ry = this.y; 633 | 634 | // Draw collision rectangle around ship 635 | if (as.debug_mode) { 636 | as.ctx.strokeStyle = 'red'; 637 | as.ctx.lineWidth = 2; 638 | as.ctx.beginPath(); 639 | as.ctx.arc(this.rx, this.ry, this.radius, 0, 360); 640 | as.ctx.closePath(); 641 | as.ctx.stroke(); 642 | } 643 | } 644 | }); 645 | }; 646 | 647 | /** 648 | * Generates asteroids. 649 | */ 650 | as.createAsteroids = function( count, size, start_x, start_y, direction ) { 651 | var 652 | size_min = 1, 653 | size_max = 3, 654 | ast_len = as.asteroids.length, 655 | rot_speeds = [.25, .45, .65]; 656 | 657 | // Generate asteroids 658 | for (var i = ast_len; i < (ast_len + count); i++) { 659 | var 660 | ast_size = size || Math.floor(Math.random() * (size_max - size_min + 1)) + size_min, 661 | x = start_x || Math.floor(Math.random() * as.canvas_w), 662 | y = start_y || Math.floor(Math.random() * as.canvas_h), 663 | dir = direction || as.getRandomDirection(), 664 | coord_set = as.asteroid_coords[ast_size], 665 | coords = coord_set[Math.floor(Math.random() * (coord_set.length))], 666 | rotation_dir = Math.floor(Math.random() * 2); 667 | 668 | // Correct for asteroids not randomly placed in perimeter region 669 | if (!start_x && !start_y) { 670 | var 671 | x_side = (x > as.canvas_w * .15 && x < as.canvas_w * .85), 672 | y_side = (y > as.canvas_h * .15 && y < as.canvas_h * .85), 673 | x_pos = x, 674 | y_pos = y; 675 | 676 | // X coord not in perimeter region 677 | if (x_side) { 678 | if (Math.floor(Math.random() * 2)) { 679 | x_pos = Math.floor(Math.random() * (as.canvas_w * .15)); 680 | } else { 681 | x_pos = Math.floor(Math.random() * (as.canvas_w - (as.canvas_w * .85))) + (as.canvas_w * .85); 682 | } 683 | } 684 | 685 | // Y coord not in perimeter region 686 | if (y_side) { 687 | if (Math.floor(Math.random() * 2)) { 688 | y_pos = Math.floor(Math.random() * (as.canvas_h * .15)); 689 | } else { 690 | y_pos = Math.floor(Math.random() * (as.canvas_h - (as.canvas_h * .85))) + (as.canvas_h * .85); 691 | } 692 | } 693 | 694 | // Reposition only when both aren't in perimeter region 695 | if (x_side && y_side) { 696 | x = x_pos; 697 | y = y_pos; 698 | } 699 | } 700 | 701 | // Store new asteroid 702 | as.asteroids.push({ 703 | id: i, 704 | x: x, 705 | y: y, 706 | vx: 1, 707 | vy: 1, 708 | dir: dir, 709 | size: ast_size, 710 | color: 'white', 711 | coord_set_idx: ast_size, 712 | coords: coords, 713 | radius: 30, 714 | rx: 0, 715 | ry: 0, 716 | angle: 0, 717 | rotation_speed: rot_speeds[ast_size-1], 718 | rotation_dir: rotation_dir, 719 | draw: function() { 720 | as.ctx.lineWidth = 2; 721 | as.ctx.strokeStyle = this.color; 722 | 723 | // Rotate the ship 724 | if (this.angle > 360) this.angle = 0; 725 | if (this.angle < 0) this.angle = 360; 726 | as.ctx.save(); 727 | as.ctx.translate(this.x, this.y); 728 | as.ctx.rotate((Math.PI / 180 * (this.angle))); 729 | as.ctx.translate(-this.x, -this.y); 730 | if (this.rotation_dir) { 731 | this.angle += this.rotation_speed; 732 | } else { 733 | this.angle -= this.rotation_speed; 734 | } 735 | 736 | // Draw the asteroid 737 | as.ctx.beginPath(); 738 | as.ctx.moveTo(this.x, this.y); 739 | for (var aidx = 0; aidx < this.coords.length; aidx+=2) { 740 | as.ctx.lineTo(this.x + this.coords[aidx], this.y + this.coords[aidx+1]); 741 | } 742 | as.ctx.closePath(); 743 | as.ctx.stroke(); 744 | as.ctx.restore(); 745 | 746 | // Update collision circle info based on asteroid size 747 | this.rx = this.x; 748 | this.ry = this.y; 749 | switch (this.coord_set_idx) { 750 | case 1 : 751 | this.radius = 40; 752 | this.vx = .5; 753 | this.vy = .5; 754 | break; 755 | case 2 : 756 | this.radius = 22; 757 | this.vx = 1; 758 | this.vy = 1; 759 | break; 760 | case 3 : 761 | this.radius = 12; 762 | this.vx = 1.5; 763 | this.vy = 1.5; 764 | break; 765 | } 766 | 767 | // Draw collision circle around ship 768 | if (as.debug_mode) { 769 | as.ctx.strokeStyle = 'red'; 770 | as.ctx.lineWidth = 2; 771 | as.ctx.beginPath(); 772 | as.ctx.arc(this.rx, this.ry, this.radius, 0, 360); 773 | as.ctx.closePath(); 774 | as.ctx.stroke(); 775 | } 776 | } 777 | }); 778 | } 779 | }; 780 | 781 | /** 782 | * Creates a UFO enemy. 783 | */ 784 | as.createUfo = function( count ) { 785 | for (var i = 0; i < count; i++) { 786 | var 787 | start_x = Math.floor(Math.random() * as.canvas_w), 788 | start_y = Math.floor(Math.random() * as.canvas_h), 789 | rand_dir = Math.floor(Math.random() * 2), 790 | move_points = []; 791 | 792 | // Place starting location off screen 793 | if (rand_dir) { 794 | start_x += (as.canvas_h * 2); 795 | start_y += (as.canvas_h * 2); 796 | } else { 797 | start_x -= (as.canvas_h * 2); 798 | start_y -= (as.canvas_h * 2); 799 | } 800 | 801 | // Generate movement points 802 | for (var n = 0; n < 8; n++) { 803 | var 804 | mov_x = Math.floor(Math.random() * as.canvas_w), 805 | mov_y = Math.floor(Math.random() * as.canvas_h); 806 | move_points.push({x: mov_x, y: mov_y}); 807 | } 808 | 809 | // Add UFO to list 810 | as.ufos.push({ 811 | id: i, 812 | x: start_x, 813 | y: start_y, 814 | vx: 0, 815 | vy: 0, 816 | vx2: 0, 817 | vy2: 0, 818 | rx: 0, 819 | ry: 0, 820 | speed: 6, 821 | radius: 30, 822 | color: 'white', 823 | move_points: move_points, 824 | curr_point: 0, 825 | destroyed: false, 826 | paused: false, 827 | pauseMovement: function() { 828 | var 829 | level = as.levels[as.stats.level], 830 | ufo_ctx = this, 831 | rand_pause = Math.floor(Math.random() * (level.ufo_max_pause_time - level.ufo_min_pause_time) + level.ufo_min_pause_time); 832 | setTimeout(function() { 833 | ufo_ctx.paused = false; 834 | }, rand_pause); 835 | }, 836 | fireMissiles: function() { 837 | var 838 | level = as.levels[as.stats.level], 839 | ufo_ctx = this; 840 | this.missileThrottle = this.missileThrottle || as.core.throttle(function() { 841 | if (!ufo_ctx.destroyed) { 842 | as.createMissile(this); 843 | } 844 | }, level.ufo_missile_fire_rate); 845 | this.missileThrottle(); 846 | }, 847 | draw: function() { 848 | as.ctx.lineWidth = 2; 849 | as.ctx.strokeStyle = this.color; 850 | 851 | // Dome 852 | as.ctx.beginPath(); 853 | as.ctx.arc(this.x, this.y, this.radius-10, (Math.PI/180)*180, (Math.PI/180)*360, false); 854 | as.ctx.quadraticCurveTo(this.x, this.y+10, this.x-20, this.y); 855 | 856 | // Saucer 857 | as.ctx.moveTo(this.x-(this.radius-10), this.y); 858 | as.ctx.bezierCurveTo( 859 | this.x-(this.radius * 2), this.y+24, 860 | this.x+(this.radius * 2), this.y+24, 861 | this.x+20, this.y+1 862 | ); 863 | 864 | // Saucer windows 865 | as.ctx.moveTo(this.x-18, this.y+9); 866 | as.ctx.arc(this.x-18, this.y+9, 3, 0, 360); 867 | as.ctx.moveTo(this.x, this.y+12); 868 | as.ctx.arc(this.x, this.y+12, 3, 0, 360); 869 | as.ctx.moveTo(this.x+18, this.y+9); 870 | as.ctx.arc(this.x+18, this.y+9, 3, 0, 360); 871 | 872 | // Draw UFO 873 | as.ctx.closePath(); 874 | as.ctx.stroke(); 875 | 876 | // Update collision coords 877 | this.rx = this.x; 878 | this.ry = this.y; 879 | 880 | // Draw collision circle around ship 881 | if (as.debug_mode) { 882 | as.ctx.strokeStyle = 'red'; 883 | as.ctx.lineWidth = 2; 884 | as.ctx.beginPath(); 885 | as.ctx.arc(this.x, this.y, this.radius, 0, 360); 886 | as.ctx.closePath(); 887 | as.ctx.stroke(); 888 | } 889 | } 890 | }); 891 | } 892 | }; 893 | 894 | /** 895 | * Detects if an asteroid has gone out of the scene. 896 | */ 897 | as.asteroidOutOfBounds = function( asteroid ) { 898 | switch (true) { 899 | case ((asteroid.y + asteroid.radius) < 0) : 900 | asteroid.y = as.canvas_h; 901 | break; 902 | case (asteroid.y - asteroid.radius > as.canvas_h) : 903 | asteroid.y = -asteroid.radius; 904 | break; 905 | case ((asteroid.x + asteroid.radius) < 0) : 906 | asteroid.x = as.canvas_w; 907 | break; 908 | case (asteroid.x - asteroid.radius > as.canvas_w) : 909 | asteroid.x = -asteroid.radius; 910 | break; 911 | } 912 | }; 913 | 914 | /** 915 | * Detects if the ship has gone out of the scene. 916 | */ 917 | as.shipOutOfBounds = function( ship ) { 918 | switch (true) { 919 | case ((ship.y + (ship.radius * 2)) < 0) : 920 | ship.y = as.canvas_h; 921 | break; 922 | case (ship.y - (ship.radius * 2) > as.canvas_h) : 923 | ship.y = -ship.radius; 924 | break; 925 | case ((ship.x + (ship.radius * 2)) < 0) : 926 | ship.x = as.canvas_w; 927 | break; 928 | case (ship.x - (ship.radius * 2) > as.canvas_w) : 929 | ship.x = -ship.radius; 930 | break; 931 | } 932 | }; 933 | 934 | /** 935 | * A general circle collision detection method. 936 | */ 937 | as.isCircleCollision = function( obj1, obj2 ) { 938 | var 939 | dx = obj1.rx - obj2.rx, 940 | dy = obj1.ry - obj2.ry, 941 | dist = Math.sqrt(dx * dx + dy * dy); 942 | if (dist < obj1.radius + obj2.radius) { 943 | return true; 944 | } 945 | }; 946 | 947 | /** 948 | * Returns a string indicating the opposite direction. 949 | */ 950 | as.getOppositeDirection = function( direction ) { 951 | switch (direction) { 952 | case 'ul' : 953 | return 'dr'; 954 | break; 955 | case 'ur' : 956 | return 'dl'; 957 | break; 958 | case 'dl' : 959 | return 'ur'; 960 | break; 961 | case 'dr' : 962 | return 'ul'; 963 | break; 964 | } 965 | }; 966 | 967 | /** 968 | * Returns a string indicating the vertical opposite direction. 969 | */ 970 | as.getOppositeVerticalDirection = function( direction ) { 971 | switch (direction) { 972 | case 'ul' : 973 | return 'dl'; 974 | break; 975 | case 'ur' : 976 | return 'dr'; 977 | break; 978 | case 'dl' : 979 | return 'ul'; 980 | break; 981 | case 'dr' : 982 | return 'ur'; 983 | break; 984 | } 985 | }; 986 | 987 | /** 988 | * Returns a random string indicating direction. 989 | */ 990 | as.getRandomDirection = function() { 991 | var 992 | ridx = Math.floor(Math.random() * as.asteroid_dirs.length); 993 | return as.asteroid_dirs[ridx]; 994 | }; 995 | 996 | /** 997 | * Returns true when user is pressing a directional key. 998 | */ 999 | as.isDirectionalKeyActive = function() { 1000 | if (as.active_keys['37'] || as.active_keys['38'] || as.active_keys['39'] || (as.active_keys['40'])) return true; 1001 | }; 1002 | 1003 | /** 1004 | * Handles generating new asteroids based on an asteroid "explosion". 1005 | */ 1006 | as.asteroidExplosion = function( asteroid ) { 1007 | var 1008 | rand_num = Math.floor(Math.random() * 3) + 1, 1009 | rand_dist = function() { 1010 | return Math.floor(Math.random() * 30); 1011 | }; 1012 | 1013 | // Generate random number of new asteroids 1014 | for (var idx = 0; idx < rand_num; idx++) { 1015 | if (asteroid.size == 1) { 1016 | if (idx % 2) { 1017 | as.createAsteroids(1, 2, asteroid.x-rand_dist(), asteroid.y-rand_dist(), as.getRandomDirection()); 1018 | } else { 1019 | as.createAsteroids(1, 2, asteroid.x+rand_dist(), asteroid.y-rand_dist(), as.getRandomDirection()); 1020 | } 1021 | } 1022 | else if (asteroid.size == 2) { 1023 | if (idx % 2) { 1024 | as.createAsteroids(1, 3, asteroid.x-rand_dist(), asteroid.y-rand_dist(), as.getRandomDirection()); 1025 | } else { 1026 | as.createAsteroids(1, 3, asteroid.x-rand_dist(), asteroid.y+rand_dist(), as.getRandomDirection()); 1027 | } 1028 | } 1029 | } 1030 | 1031 | // Delete exploded asteroid 1032 | as.asteroids.splice(as.asteroids.indexOf(asteroid), 1); 1033 | }; 1034 | 1035 | /** 1036 | * Detect and handle ship explosion. 1037 | */ 1038 | as.detectShipExplosion = function( object ) { 1039 | if (as.isCircleCollision(object, as.ship) && !as.ship_exploding_parts.length && !as.ship_exploded) { 1040 | as.ship_exploded = true; 1041 | 1042 | // Hide the ship 1043 | as.ship.color = 'rgba(0,0,0,0)'; 1044 | 1045 | // Generate some exploding ship parts 1046 | as.explodingShipParts(6); 1047 | 1048 | // Decrease score 1049 | as.stats.score -= as.stats.score_death_loss; 1050 | 1051 | // Update initial parts properties 1052 | for (var spidx in as.ship_exploding_parts) { 1053 | var 1054 | part = as.ship_exploding_parts[spidx], 1055 | rand_angle = Math.floor(Math.random() * 360), 1056 | rand_speed = Math.random() * .1; 1057 | part.x = as.ship.x; 1058 | part.y = as.ship.y; 1059 | part.speed = rand_speed; 1060 | part.vx = part.speed * Math.cos(rand_angle * Math.PI / 180.0); 1061 | part.vy = part.speed * Math.sin(rand_angle * Math.PI / 180.0); 1062 | part.draw(); 1063 | } 1064 | 1065 | // Show ship color as red 1066 | setTimeout(function() { 1067 | as.ship.color = 'red'; 1068 | }, 1000); 1069 | } 1070 | }; 1071 | 1072 | /** 1073 | * Render/handle the asteroids and their interactions. 1074 | */ 1075 | as.renderAsteroids = function() { 1076 | for (var idx in as.asteroids) { 1077 | var asteroid = as.asteroids[idx]; 1078 | 1079 | // Handle asteroid going off screen 1080 | as.asteroidOutOfBounds(asteroid); 1081 | 1082 | // Detect ship/asteroid collisions 1083 | as.detectShipExplosion(asteroid); 1084 | 1085 | // Animate exploding ship parts 1086 | for (var spidx in as.ship_exploding_parts) { 1087 | var part = as.ship_exploding_parts[spidx]; 1088 | part.x += part.vx; 1089 | part.y += part.vy; 1090 | part.alpha -= part.alpha_speed; 1091 | part.draw(); 1092 | if (part.alpha <= 0) as.ship_exploding_parts.splice(pidx, 1); 1093 | } 1094 | 1095 | // Detect asteroid/missile collisions (fired from ship & UFOs) 1096 | for (var midx in as.missiles) { 1097 | var missile = as.missiles[midx]; 1098 | 1099 | // Detect exploding asteroids 1100 | if (as.isCircleCollision(asteroid, missile)) { 1101 | asteroid.color = 'red'; 1102 | 1103 | // Add to max score 1104 | if (missile.fired_from == 'ship') { 1105 | as.stats.score += as.stats.score_asteroid_gain; 1106 | } 1107 | 1108 | // Delete out of bound missiles 1109 | as.missiles.splice(midx, 1); 1110 | 1111 | // Handle asteroid missile impact 1112 | as.asteroidExplosion(asteroid); 1113 | 1114 | // Generate some explosion particles 1115 | as.explodingAsteroidParticles(5); 1116 | 1117 | // Update initial particle properties 1118 | if (as.asteroid_particles.length) { 1119 | for (var pidx in as.asteroid_particles) { 1120 | var 1121 | particle = as.asteroid_particles[pidx], 1122 | rand_angle = Math.floor(Math.random() * 360), 1123 | rand_speed = Math.random() * .1; 1124 | particle.x = asteroid.x; 1125 | particle.y = asteroid.y; 1126 | particle.speed = rand_speed; 1127 | particle.vx = particle.speed * Math.cos(rand_angle * Math.PI / 180.0); 1128 | particle.vy = particle.speed * Math.sin(rand_angle * Math.PI / 180.0); 1129 | particle.draw(); 1130 | } 1131 | } 1132 | } 1133 | } 1134 | 1135 | // Animate explosion particles 1136 | for (var pidx in as.asteroid_particles) { 1137 | var particle = as.asteroid_particles[pidx]; 1138 | particle.x += particle.vx; 1139 | particle.y += particle.vy; 1140 | particle.alpha -= particle.alpha_speed; 1141 | particle.draw(); 1142 | if (particle.alpha <= 0) as.asteroid_particles.splice(pidx, 1); 1143 | } 1144 | 1145 | // Detect asteroid/asteroid collisions 1146 | for (var aidx in as.asteroids) { 1147 | var asteroid2 = as.asteroids[aidx]; 1148 | if (as.isCircleCollision(asteroid, asteroid2)) { 1149 | asteroid.dir = as.getOppositeVerticalDirection(asteroid.dir); 1150 | asteroid2.dir = as.getOppositeVerticalDirection(asteroid2.dir); 1151 | } 1152 | } 1153 | 1154 | // Animate asteroids 1155 | asteroid.draw(); 1156 | switch (asteroid.dir) { 1157 | case 'ul' : 1158 | asteroid.x -= asteroid.vx - as.levels[as.stats.level].asteroid_speed; 1159 | asteroid.y -= asteroid.vy - as.levels[as.stats.level].asteroid_speed; 1160 | break; 1161 | case 'ur' : 1162 | asteroid.x += asteroid.vx + -as.levels[as.stats.level].asteroid_speed; 1163 | asteroid.y -= asteroid.vy - as.levels[as.stats.level].asteroid_speed; 1164 | break; 1165 | case 'dl' : 1166 | asteroid.x -= asteroid.vx - as.levels[as.stats.level].asteroid_speed; 1167 | asteroid.y += asteroid.vy + -as.levels[as.stats.level].asteroid_speed; 1168 | break; 1169 | case 'dr' : 1170 | asteroid.x += asteroid.vx + -as.levels[as.stats.level].asteroid_speed; 1171 | asteroid.y += asteroid.vy + -as.levels[as.stats.level].asteroid_speed; 1172 | break; 1173 | } 1174 | } 1175 | }; 1176 | 1177 | /** 1178 | * Render/handle the ship and its interactions. 1179 | */ 1180 | as.renderShip = function() { 1181 | 1182 | // Move ship up 1183 | if (as.active_keys['38']) { 1184 | 1185 | // Ship to fly towards angle it is pointing. Calculate coords in distance 1186 | var theta = (180 + as.ship.angle) % 360; 1187 | as.ship.vx2 = as.ship.x - (as.ship.x + as.ship.speed * Math.cos(Math.PI * theta / 180)); 1188 | as.ship.vy2 = as.ship.y - (as.ship.y + as.ship.speed * Math.sin(Math.PI * theta / 180)); 1189 | } 1190 | 1191 | // Apply ship friction to velocity 1192 | as.ship.vx2 *= as.ship.friction; 1193 | as.ship.vy2 *= as.ship.friction; 1194 | as.ship.x -= as.ship.vx2; 1195 | as.ship.y -= as.ship.vy2; 1196 | 1197 | // Rotate ship left 1198 | if (as.active_keys['37']) { 1199 | as.ship.angle -= as.ship.turn; 1200 | } 1201 | 1202 | // Rotate ship right 1203 | if (as.active_keys['39']) { 1204 | as.ship.angle += as.ship.turn; 1205 | } 1206 | 1207 | // Draw ship 1208 | as.shipOutOfBounds(as.ship); 1209 | as.ship.draw(); 1210 | 1211 | // Draw ship's firing missiles 1212 | for (var midx in as.missiles) { 1213 | var missile = as.missiles[midx]; 1214 | missile.draw(); 1215 | 1216 | // Explode UFO fired missile on ship 1217 | if (missile.fired_from == 'ufo') { 1218 | as.detectShipExplosion(missile); 1219 | } 1220 | 1221 | // Delete missile when off screen 1222 | if ( 1223 | missile.x >= as.canvas_w || 1224 | missile.x <= 0 || 1225 | missile.y >= as.canvas_h || 1226 | missile.y <= 0 1227 | ) { 1228 | as.missiles.splice(midx, 1); 1229 | } 1230 | } 1231 | }; 1232 | 1233 | /** 1234 | * Render/handle each level's UFOs and its interactions. 1235 | */ 1236 | as.renderUfos = function() { 1237 | var 1238 | level = as.levels[as.stats.level]; 1239 | 1240 | // Create UFOs if non have been deployed 1241 | if (!level.ufos_deployed) { 1242 | level.ufos_deployed = true; 1243 | as.createUfo(level.ufos); 1244 | } 1245 | 1246 | // Draw UFOs when active 1247 | for (var uidx in as.ufos) { 1248 | var 1249 | ufo = as.ufos[uidx], 1250 | dest = ufo.move_points[ufo.curr_point]; 1251 | 1252 | // Move the UFO to its target point 1253 | if (!ufo.paused) { 1254 | var 1255 | angle = Math.atan2(ufo.y - dest.y, ufo.x - dest.x) * 180 / Math.PI, 1256 | theta = (180 + angle) % 360; 1257 | 1258 | // Set UFO speed 1259 | ufo.speed = level.ufo_speed; 1260 | 1261 | // Animate until ship reaches area of destination 1262 | if (!((ufo.x > dest.x - 20) && (ufo.x < dest.x + 20))) { 1263 | 1264 | // Fly UFO towards its destination 1265 | ufo.vx2 = ufo.x - (ufo.x + ufo.speed * Math.cos(Math.PI * theta / 180)); 1266 | ufo.vy2 = ufo.y - (ufo.y + ufo.speed * Math.sin(Math.PI * theta / 180)); 1267 | ufo.x -= ufo.vx2; 1268 | ufo.y -= ufo.vy2; 1269 | } 1270 | 1271 | // Pause the UFO's movement and get it ready to move to the next point 1272 | else { 1273 | ufo.paused = true; 1274 | if (ufo.curr_point == ufo.move_points.length-1) { 1275 | ufo.curr_point = 0; 1276 | } else { 1277 | ufo.curr_point++; 1278 | } 1279 | 1280 | // Call UFO movement timeout method 1281 | ufo.pauseMovement(); 1282 | } 1283 | } 1284 | 1285 | // Initialize firing of UFO missiles 1286 | ufo.fireMissiles(); 1287 | 1288 | // Draw UFO 1289 | ufo.draw(); 1290 | 1291 | // Detect ship/ufo collisions 1292 | as.detectShipExplosion(ufo); 1293 | 1294 | // Detect ufo/missile collisions 1295 | for (var midx in as.missiles) { 1296 | var missile = as.missiles[midx]; 1297 | 1298 | // Detect missiles hitting UFO 1299 | if (as.isCircleCollision(ufo, missile) && missile.fired_from == 'ship') { 1300 | ufo.color = 'rgba(0, 0, 0, 0)'; 1301 | 1302 | // Mark the UFO as destroyed 1303 | ufo.destroyed = true; 1304 | 1305 | // Delete UFO. 1306 | as.ufos.splice(uidx, 1); 1307 | 1308 | // Add to max score 1309 | as.stats.score += as.stats.score_ufo_gain; 1310 | 1311 | // Delete out of bound missiles 1312 | as.missiles.splice(midx, 1); 1313 | 1314 | // Generate/update some explosion particles 1315 | as.explodingAsteroidParticles(5); 1316 | 1317 | // Update initial particle properties 1318 | for (var pidx in as.asteroid_particles) { 1319 | var 1320 | particle = as.asteroid_particles[pidx], 1321 | rand_angle = Math.floor(Math.random() * 360), 1322 | rand_speed = Math.random() * .1; 1323 | particle.x = ufo.x; 1324 | particle.y = ufo.y; 1325 | particle.speed = rand_speed; 1326 | particle.vx = particle.speed * Math.cos(rand_angle * Math.PI / 180.0); 1327 | particle.vy = particle.speed * Math.sin(rand_angle * Math.PI / 180.0); 1328 | particle.draw(); 1329 | } 1330 | } 1331 | } 1332 | } 1333 | }; 1334 | 1335 | /** 1336 | * Updates game score. 1337 | */ 1338 | as.updateScore = function( points ) { 1339 | if (points) { 1340 | as.stats.score += points; 1341 | } 1342 | return { 1343 | draw: function() { 1344 | var 1345 | str = "", 1346 | str_width = null, 1347 | pad = 30, 1348 | prepend = '', 1349 | negative = false; 1350 | if (as.stats.score < 0) { 1351 | negative = true; 1352 | } 1353 | for (var i = 0; i < (10 - as.stats.score.toString().replace("-", "").length); i++) { 1354 | prepend = prepend + "" + '0'; 1355 | } 1356 | str += prepend + as.stats.score.toString().replace("-", ""); 1357 | if (negative) str = "-" + str; 1358 | str = "SCORE: " + str; 1359 | str_width = as.ctx.measureText(str); 1360 | as.ctx.font = "16px courier"; 1361 | as.ctx.fillStyle = 'white'; 1362 | as.ctx.fillText(str, (str_width.width / 2) + pad, pad); 1363 | } 1364 | } 1365 | }; 1366 | 1367 | /** 1368 | * Updates the game level. 1369 | */ 1370 | as.updateLevel = function( level ) { 1371 | if (level) { 1372 | as.stats.level += level; 1373 | } 1374 | return { 1375 | draw: function() { 1376 | var 1377 | str = "LEVEL: " + as.stats.level, 1378 | str_width = as.ctx.measureText(str), 1379 | pad = 30; 1380 | if (!as.levels[as.stats.level].start_time) { 1381 | as.levels[as.stats.level].start_time = Date.now(); 1382 | } 1383 | as.ctx.font = "16px courier"; 1384 | as.ctx.fillStyle = 'white'; 1385 | as.ctx.fillText(str, as.canvas.width - (str_width.width / 2) - pad, pad); 1386 | } 1387 | } 1388 | }; 1389 | 1390 | /** 1391 | * Start the game. 1392 | */ 1393 | as.startGameScreen = function() { 1394 | var 1395 | game_slide_index = 0, 1396 | game_slides = [ 1397 | { 1398 | duration: 5000, 1399 | text: "Press SPACEBAR to start the game!" 1400 | }, 1401 | { 1402 | duration: 5000, 1403 | text: "Use ARROW keys to move and SPACE to fire." 1404 | } 1405 | ], 1406 | slideViewer = function( id ) { 1407 | var 1408 | slide_obj = game_slides[id]; 1409 | 1410 | // Update tracking index 1411 | if (id == game_slides.length-1) { 1412 | game_slide_index = 0; 1413 | } else { 1414 | game_slide_index++; 1415 | } 1416 | 1417 | // Add current slide 1418 | as.ctx.clearRect(0, 0, as.canvas_w, as.canvas_h); 1419 | as.ctx.font = "16px courier"; 1420 | as.ctx.fillStyle = 'white'; 1421 | as.ctx.textBaseline = 'middle'; 1422 | as.ctx.textAlign = "center"; 1423 | as.ctx.fillText(slide_obj.text, (as.canvas.width / 2), (as.canvas_h / 2)); 1424 | 1425 | // Initialize next slide after duration 1426 | setTimeout(function() { 1427 | if (as.game_status == 'paused') { 1428 | slideViewer(game_slide_index); 1429 | } 1430 | }, slide_obj.duration); 1431 | }; 1432 | 1433 | // Initialize the pre game slides 1434 | if (as.game_status == 'paused') { 1435 | slideViewer(0); 1436 | } 1437 | 1438 | // Attach game start event listener 1439 | window.addEventListener("keydown", function(e) { 1440 | if (e.keyCode == 32 && as.game_status == 'paused') { 1441 | as.game_status = 'active'; 1442 | as.startGame(); 1443 | } 1444 | }); 1445 | }; 1446 | 1447 | /** 1448 | * Start/restart the game 1449 | */ 1450 | as.startGame = function() { 1451 | as.core.then = Date.now(); 1452 | as.createAsteroids(as.levels[as.stats.level].asteroids); 1453 | if (as.nyan_mode) { 1454 | as.createNyan(); 1455 | } else { 1456 | as.createShip(); 1457 | } 1458 | as.core.frame(); 1459 | }; 1460 | 1461 | /** 1462 | * A callback to execute when the game is completed. 1463 | */ 1464 | as.gameCompleted = function() { 1465 | 1466 | // Clear scene/add game over message 1467 | as.ctx.clearRect(0, 0, as.canvas_w, as.canvas_h); 1468 | as.ctx.font = "16px courier"; 1469 | as.ctx.fillStyle = 'white'; 1470 | as.ctx.textBaseline = 'middle'; 1471 | as.ctx.textAlign = "center"; 1472 | as.ctx.fillText('Game completed!', (as.canvas.width / 2), (as.canvas_h / 2)); 1473 | as.ctx.fillText('Your score: ' + as.stats.score, (as.canvas.width / 2), (as.canvas_h / 2) + 30); 1474 | 1475 | // Kill the animation sequence 1476 | window.cancelAnimationFrame(as.core.animationFrame); 1477 | 1478 | // Game completed user callback 1479 | if (typeof as.gameCompletedCallback == 'function') { 1480 | as.gameCompletedCallback(); 1481 | } 1482 | }; 1483 | 1484 | return as; 1485 | }; 1486 | 1487 | return Asteroids; 1488 | }); 1489 | --------------------------------------------------------------------------------