├── .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 | 
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 |
--------------------------------------------------------------------------------