Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(219)

Side by Side Diff: chrome/renderer/resources/offline.js

Issue 545973003: Update network error template to new design (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Address code review comments Created 6 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright (c) 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 (function() {
5 /**
arv (Not doing code reviews) 2014/09/16 14:20:20 Maybe 'use strict' to catch errors?
edwardjung 2014/09/17 12:12:34 Done.
6 * T-rex runner.
7 * @author Edward Jung (edwardjung@google.com)
8 */
9
10
11 /**
12 * T-Rex runner.
13 * @param {string} outerContainerId Outer containing element id.
14 * @param {object} opt_config Optional configuration object.
15 * @constructor
16 * @export
17 */
18 var Runner = function(outerContainerId, opt_config) {
arv (Not doing code reviews) 2014/09/16 14:20:21 Prefer function declarations where allowed. funct
edwardjung 2014/09/17 12:12:34 Done.
19 this.outerContainerEl = document.querySelector(outerContainerId);
20
21 this.containerEl = null;
22
23 this.config = opt_config || Runner.config;
24
25 this.dimensions = Runner.defaultDimensions;
26
27 this.canvas = null;
28 this.canvasCtx = null;
29
30 this.tRex = null;
31
32 this.distanceMeter = null;
33 this.distanceRan = 0;
34
35 this.highestScore = 0;
36
37 this.time = 0;
38 this.runningTime = 0;
39 this.msPerFrame = 1000 / Runner.FPS;
40 this.currentSpeed = this.config.SPEED;
41
42 this.obstacles = [];
43
44 this.started = false;
45 this.activated = false;
46 this.crashed = false;
47 this.paused = false;
48
49 this.resizeTimerId_ = null;
50
51 this.playCount = 0;
52
53 // Images.
54 this.images = {};
55 this.imagesLoaded = 0;
56 this.loadImages();
57
58 // Sound FX.
59 this.audioBuffer = null;
60 this.soundFx = {};
61
62 // Singleton
arv (Not doing code reviews) 2014/09/16 14:20:21 Do we need this singleton pattern enforced in the
edwardjung 2014/09/17 12:12:34 Done.
63 if (Runner.instance_) {
64 return Runner.instance_;
65 }
66 Runner.instance_ = this;
67 };
68 // Export for Closure compiler
69 window['AudioContext'] = window['AudioContext'];
70 window['Runner'] = Runner;
arv (Not doing code reviews) 2014/09/16 14:20:20 I think you should be able to just do @export on R
edwardjung 2014/09/17 12:12:35 As I'm no longer using the compiler, I'm removing
71 window['Runner']['Trex'] = Runner.Trex;
72 window['Runner']['Clouds'] = Runner.Clouds;
73 window['Runner']['Horizon'] = Runner.Horizon;
74 Runner.prototype['handleEvent'] = Runner.prototype.handleEvent;
75
76
77 /**
78 * Singleton getter.
79 * @return {Runner}
80 */
81 Runner.getInstance = function(container, opt_config) {
82 return Runner.instance_ || new Runner(container, opt_config);
83 };
84
85
86 /**
87 * Setting individual settings for debugging.
88 * @param {string} setting Setting name.
89 * @param {*} value Setting value to assign.
90 */
91 Runner.prototype.updateConfigSetting = function(setting, value) {
arv (Not doing code reviews) 2014/09/16 14:20:21 How about using an object literal so you do not ha
edwardjung 2014/09/17 12:12:34 Done, here and elsewhere
92 if (setting in this.config && value != undefined) {
93 this.config[setting] = value;
94
95 switch (setting) {
96 case 'GRAVITY':
97 case 'MIN_JUMP_HEIGHT':
98 case 'SPEED_DROP_COEFFICIENT':
99 this.tRex.config[setting] = value;
100 break;
101 case 'INITIAL_JUMP_VELOCITY':
102 this.tRex.setJumpVelocity(value);
103 break;
104 case 'SPEED':
105 this.setSpeed(value);
106 break;
107 }
108 }
109 };
110
111
112 /**
113 * Default game configuration.
114 * @enum {string}
115 */
116 Runner.config = {
117 ACCELERATION: 0.001,
118 BG_CLOUD_SPEED: 0.2,
119 BOTTOM_PAD: 10,
120 CLEAR_TIME: 3000,
121 CLOUD_FREQUENCY: 0.5,
122 GAMEOVER_CLEAR_TIME: 750,
123 GAP_COEFFICIENT: 0.6,
124 GRAVITY: 0.6,
125 INITIAL_JUMP_VELOCITY: 12,
126 MAX_CLOUDS: 6,
127 MAX_OBSTACLE_LENGTH: 3,
128 MAX_SPEED: 12,
129 MIN_JUMP_HEIGHT: 35,
130 MOBILE_SPEED_COEFFICIENT: 1.2,
131 SPEED: 6,
132 SPEED_DROP_COEFFICIENT: 3
133 };
134
135
136 // Default game width.
137 Runner.DEFAULT_WIDTH = 600;
arv (Not doing code reviews) 2014/09/16 14:20:21 Now that you have an IIFE, there is really no reas
edwardjung 2014/09/17 12:12:35 Done.
138
139 // Frames per second.
140 Runner.FPS = 60;
141
142
143 /**
144 * Default dimensions.
145 * @enum {string}
146 */
147 Runner.defaultDimensions = {
148 WIDTH: Runner.DEFAULT_WIDTH,
149 HEIGHT: 150
150 };
151
152
153 /**
154 * CSS class names.
155 * @enum {string}
156 */
157 Runner.classes = {
158 CANVAS: 'runner-canvas',
159 CONTAINER: 'runner-container',
160 CRASHED: 'crashed',
161 ICON: 'icon-offline',
162 TOUCH_CONTROLLER: 'controller'
163 };
164
165
166 /**
167 * Image source urls.
168 * @enum {string}
169 */
170 Runner.imageSources = {
171 LDPI: [
172 {name: 'CACTUS_LARGE', id: '1x-obstacle-large'},
173 {name: 'CACTUS_SMALL', id: '1x-obstacle-small'},
174 {name: 'CLOUD', id: '1x-cloud'},
175 {name: 'HORIZON', id: '1x-horizon'},
176 {name: 'RESTART', id: '1x-restart'},
177 {name: 'TEXT_SPRITE', id: '1x-text'},
178 {name: 'TREX', id: '1x-trex'}
179 ],
180 HDPI: [
181 {name: 'CACTUS_LARGE', id: '2x-obstacle-large'},
182 {name: 'CACTUS_SMALL', id: '2x-obstacle-small'},
183 {name: 'CLOUD', id: '2x-cloud'},
184 {name: 'HORIZON', id: '2x-horizon'},
185 {name: 'RESTART', id: '2x-restart'},
186 {name: 'TEXT_SPRITE', id: '2x-text'},
187 {name: 'TREX', id: '2x-trex'}
188 ]
189 };
190
191
192 /**
193 * Sound FX. Reference to the ID of the audio tag on interstitial page.
194 * @enum {string}
195 */
196 Runner.sounds = {
197 BUTTON_PRESS: 'offline-sound-press',
198 HIT: 'offline-sound-hit',
199 SCORE: 'offline-sound-reached'
200 };
201
202
203 /**
204 * Key code mapping.
205 * @enum {string}
206 */
207 Runner.keycodes = {
208 JUMP: { '38': 1, '32': 1 }, // Up, spacebar
209 DUCK: { '40': 1 }, // Down
210 RESTART: { '13': 1 } // Enter
211 };
212
213
214 /**
215 * Runner event names.
216 * @enum {string}
217 */
218 Runner.events = {
219 ANIM_END: 'webkitAnimationEnd',
220 CLICK: 'click',
221 KEYDOWN: 'keydown',
222 KEYUP: 'keyup',
223 MOUSEDOWN: 'mousedown',
224 MOUSEUP: 'mouseup',
225 RESIZE: 'resize',
226 TOUCHEND: 'touchend',
227 TOUCHSTART: 'touchstart',
228 VISIBILITY: 'visibilitychange',
229 BLUR: 'blur',
230 FOCUS: 'focus',
231 LOAD: 'load'
232 };
233
234
235 /**
236 * Global web audio context for playing sounds.
237 */
238 Runner.audioContext = new AudioContext();
arv (Not doing code reviews) 2014/09/16 14:20:21 Since you are using a singleton, maybe just create
edwardjung 2014/09/17 12:12:34 Refactored.
239
240 /**
241 * Load and cache the image assets from the page.
242 */
243 Runner.prototype.loadImages = function() {
244 var imageSources = Runner.isHDPI ? Runner.imageSources.HDPI :
245 Runner.imageSources.LDPI;
246
247 var numImages = imageSources.length;
248
249 for (var i = numImages - 1; i >= 0; i--) {
250 var imgSource = imageSources[i];
251 this.images[imgSource.name] = document.getElementById(imgSource.id);
252 }
253 this.init();
254 };
255
256
257 /**
258 * Load and decode base 64 encoded sounds.
259 */
260 Runner.prototype.loadSounds = function() {
261 for (var sound in Runner.sounds) {
262 var soundSrc = document.getElementById(Runner.sounds[sound]).src;
263 soundSrc = soundSrc.substr(soundSrc.indexOf(',') + 1);
264 var buffer = Runner.decodeBase64ToArrayBuffer(soundSrc);
arv (Not doing code reviews) 2014/09/16 14:20:21 Since you are doing the decoding here, you can put
edwardjung 2014/09/17 12:12:35 Done. Nice.
265
266 // Async, so no guarantee of order in array.
267 Runner.audioContext.decodeAudioData(buffer, function(index, audioData) {
268 this.soundFx[index] = audioData;
269 }.bind(this, sound));
270 }
271
272 };
273
274
275 /**
276 * Sets the game speed. Adjust the speed accordingly if on a smaller screen.
277 * @param {number} opt_speed Optional speed.
278 */
279 Runner.prototype.setSpeed = function(opt_speed) {
280 var speed = opt_speed || this.currentSpeed;
281
282 // Reduce the speed on smaller mobile screens.
283 if (this.dimensions.WIDTH < Runner.DEFAULT_WIDTH) {
284 var mobileSpeed = speed * ((this.dimensions.WIDTH / Runner.DEFAULT_WIDTH) *
285 this.config.MOBILE_SPEED_COEFFICIENT);
286 this.currentSpeed = mobileSpeed > speed ? speed : mobileSpeed;
287 } else if (opt_speed) {
288 this.currentSpeed = opt_speed;
289 }
290 };
291
292
293 /**
294 * Game initialiser.
295 */
296 Runner.prototype.init = function() {
297 // Hide the static icon.
298 document.querySelector('.' + Runner.classes.ICON).style.visibility = 'hidden';
299
300 this.adjustDimensions();
301 this.setSpeed();
302
303 this.containerEl = document.createElement('div');
304 this.containerEl.className = Runner.classes.CONTAINER;
305
306 // Player canvas container.
307 this.canvas = Runner.createCanvas(this.containerEl, this.dimensions.WIDTH,
308 this.dimensions.HEIGHT, Runner.classes.PLAYER);
309
310 this.canvasCtx = this.canvas.getContext('2d');
311 this.canvasCtx.fillStyle = '#f7f7f7';
312 this.canvasCtx.fill();
313 Runner.updateCanvasScaling(this.canvas);
314
315 // Horizon contains clouds, obstacles and the ground.
316 this.horizon = new Runner.Horizon(this.canvas, this.images, this.dimensions,
317 this.config.GAP_COEFFICIENT);
318
319 // Distance meter
320 this.distanceMeter = new Runner.DistanceMeter(this.canvas,
321 this.images['TEXT_SPRITE'], this.dimensions.WIDTH);
322
323 // Draw t-rex
324 this.tRex = new Runner.Trex(this.canvas, this.images['TREX']);
325
326 this.outerContainerEl.appendChild(this.containerEl);
327
328 if (Runner.isMobile) {
329 this.createTouchController();
330 }
331
332 this.startListening();
333 this.update();
334 this.loadSounds();
335
336 window.addEventListener(Runner.events.RESIZE,
337 this.debounceResize.bind(this), false);
338 };
339
340
341 /**
342 * Create the touch controller. A div that covers whole screen.
343 */
344 Runner.prototype.createTouchController = function() {
345 this.touchController = document.createElement('div');
346 this.touchController.className = Runner.classes.TOUCH_CONTROLLER;
347 };
348
349
350 /**
351 * Debounce the resize event.
352 */
353 Runner.prototype.debounceResize = function() {
354 if (!this.resizeTimerId_) {
355 this.resizeTimerId_ =
356 setInterval(this.adjustDimensions.bind(this), 250);
357 }
358 };
359
360
361 /**
362 * Adjust game space dimensions on resize.
363 */
364 Runner.prototype.adjustDimensions = function() {
365 clearInterval(this.resizeTimerId_);
366 this.resizeTimerId_ = null;
367
368 var boxStyles = window.getComputedStyle(this.outerContainerEl);
369 var padding = parseInt(boxStyles.paddingLeft.substr(0,
370 boxStyles.paddingLeft.length - 2));
371
372 this.dimensions.WIDTH = this.outerContainerEl.offsetWidth - (padding * 2);
373
374 // Redraw the elements back onto the canvas.
375 if (this.canvas) {
376 this.canvas.width = this.dimensions.WIDTH;
377 this.canvas.height = this.dimensions.HEIGHT;
378
379 Runner.updateCanvasScaling(this.canvas);
380
381 this.distanceMeter.calcXPos(this.dimensions.WIDTH);
382 this.clearCanvas();
383 this.horizon.update(0, 0, true);
384 this.tRex.update(0);
385
386 // Outer container and distance meter.
387 if (this.activated || this.crashed) {
388 this.containerEl.style.width = this.dimensions.WIDTH + 'px';
389 this.containerEl.style.height = this.dimensions.HEIGHT + 'px';
390 this.distanceMeter.update(0, Math.ceil(this.distanceRan));
391 this.stop();
392 } else {
393 this.tRex.draw(0, 0);
394 }
395
396 // Game over panel.
397 if (this.crashed && this.gameOverPanel) {
398 this.gameOverPanel.updateDimensions(this.dimensions.WIDTH);
399 this.gameOverPanel.draw();
400 }
401 }
402 };
403
404
405 /**
406 * Play the game intro.
407 * Canvas container width expands out to the full width.
408 */
409 Runner.prototype.playIntro = function() {
410 if (!this.started && !this.crashed) {
411 this.playingIntro = true;
412 this.tRex.playingIntro = true;
413
414 // CSS animation definition.
415 var keyframes = '@-webkit-keyframes intro { ' +
416 'from { width:' + Runner.Trex.config.WIDTH + 'px }' +
417 'to { width: ' + this.dimensions.WIDTH + 'px }' +
418 '}';
419 document.styleSheets[0].insertRule(keyframes, 0);
420
421 this.containerEl.addEventListener(Runner.events.ANIM_END,
422 this.startGame.bind(this));
423
424 this.containerEl.style.webkitAnimation = 'intro .4s ease-out 1 both';
425 this.containerEl.style.width = this.dimensions.WIDTH + 'px';
426
427 if (this.touchController) {
428 this.outerContainerEl.appendChild(this.touchController);
429 }
430 this.activated = true;
431 this.started = true;
432 } else if (this.crashed) {
433 this.restart();
434 }
435 };
436
437
438 /**
439 * Update the game status to started.
440 */
441 Runner.prototype.startGame = function() {
442 this.runningTime = 0;
443 this.playingIntro = false;
444 this.tRex.playingIntro = false;
445 this.containerEl.style.webkitAnimation = '';
446 this.playCount++;
447
448 // Handle tabbing off the page. Pause the current game.
449 window.addEventListener(Runner.events.VISIBILITY,
450 this.onVisibilityChange.bind(this), false);
451
452 window.addEventListener(Runner.events.BLUR,
453 this.onVisibilityChange.bind(this), false);
454
455 window.addEventListener(Runner.events.FOCUS,
456 this.onVisibilityChange.bind(this), false);
457 };
458
459
460 /**
461 * Clear the whole canvas.
462 */
463 Runner.prototype.clearCanvas = function() {
464 this.canvasCtx.clearRect(0, 0, this.dimensions.WIDTH, this.dimensions.HEIGHT);
465 };
466
467
468 /**
469 * Update the game frame.
470 */
471 Runner.prototype.update = function() {
472 this.drawPending = false;
473
474 var now = performance.now();
475 var deltaTime = now - (this.time || now);
476 this.time = now;
477
478 if (this.activated) {
479 this.clearCanvas();
480
481 if (this.tRex.jumping) {
482 this.tRex.updateJump(deltaTime, this.config);
483 }
484
485 this.runningTime += deltaTime;
486 var hasObstacles = this.runningTime > this.config.CLEAR_TIME;
487
488 // First jump triggers the intro.
489 if (this.tRex.jumpCount == 1 && !this.playingIntro) {
490 this.playIntro();
491 }
492
493 // The horizon doesn't move until the intro is over.
494 if (this.playingIntro) {
495 this.horizon.update(0, this.currentSpeed, hasObstacles);
496 } else {
497 deltaTime = !this.started ? 0 : deltaTime;
498 this.horizon.update(deltaTime, this.currentSpeed, hasObstacles);
499 }
500
501 // Check for collisions.
502 var collision = hasObstacles ?
503 Runner.Collision.check(this.horizon.obstacles[0], this.tRex) : false;
504
505 if (!collision) {
506 this.distanceRan += this.currentSpeed * (deltaTime / this.msPerFrame);
507
508 if (this.currentSpeed < this.config.MAX_SPEED) {
509 this.currentSpeed += this.config.ACCELERATION;
510 }
511 } else {
512 this.gameOver();
513 }
514
515 if (this.distanceMeter.getActualDistance(this.distanceRan) >
516 this.distanceMeter.maxScore) {
517 this.distanceRan = 0;
518 }
519
520 this.distanceMeter.update(deltaTime, Math.ceil(this.distanceRan),
521 this.soundFx.SCORE);
522 }
523
524 if (!this.crashed) {
525 this.tRex.update(deltaTime);
526 this.raq();
527 }
528 };
529
530
531 /**
532 * Updates the canvas size taking into
533 * account the backing store pixel ratio and
534 * the device pixel ratio.
535 *
536 * See article by Paul Lewis:
537 * http://www.html5rocks.com/en/tutorials/canvas/hidpi/
538 *
539 * @param {HTMLCanvasElement} canvas Canvas to scale.
540 * @param {number} opt_width Canvas to scale.
541 * @param {number} opt_width Canvas to scale.
542 * @return {boolean} Whether the canvas was scaled.
543 */
544 Runner.updateCanvasScaling = function(canvas, opt_width, opt_height) {
545
546 var context = canvas.getContext('2d');
547
548 // Query the various pixel ratios
549 var devicePixelRatio = Math.floor(window.devicePixelRatio) || 1;
550 var backingStoreRatio = Math.floor(context.webkitBackingStorePixelRatio) || 1;
551 var ratio = devicePixelRatio / backingStoreRatio;
552
553 // Upscale the canvas if the two ratios don't match
554 if (devicePixelRatio !== backingStoreRatio) {
555
556 var oldWidth = opt_width || canvas.width;
557 var oldHeight = opt_height || canvas.height;
558
559 canvas.width = oldWidth * ratio;
560 canvas.height = oldHeight * ratio;
561
562 canvas.style.width = oldWidth + 'px';
563 canvas.style.height = oldHeight + 'px';
564
565 // Scale the context to counter the fact that we've manually scaled
566 // our canvas element.
567 context.scale(ratio, ratio);
568 return true;
569 }
570 return false;
571 };
572
573
574 /**
575 * Draw the collision boxes on canvas, for debugging.
576 * @param {Runner.CollisionBox} collision T-Rex collison box.
577 * @param {Runner.CollisionBox} obstacle Obstacle collison box.
578 */
579 Runner.prototype.drawCollisionBoxes = function(collision, obstacle) {
580 this.canvasCtx.save();
581 this.canvasCtx.strokeStyle = '#f00';
582 this.canvasCtx.strokeRect(collision[0].x, collision[0].y,
583 collision[0].width, collision[0].height);
584
585 this.canvasCtx.strokeStyle = '#00f';
586 this.canvasCtx.strokeRect(collision[1].x, collision[1].y,
587 collision[1].width, collision[1].height);
588 this.canvasCtx.restore();
589 };
590
591
592 /**
593 * Event handler.
594 */
595 Runner.prototype.handleEvent = function(e) {
596 return (function(evtType, events) {
597 switch (evtType) {
598 case events.KEYDOWN:
599 case events.TOUCHSTART:
600 case events.MOUSEDOWN:
601 this.onKeyDown(e);
602 break;
603 case events.KEYUP:
604 case events.TOUCHEND:
605 case events.MOUSEUP:
606 this.onKeyUp(e);
607 break;
608 }
609 }.bind(this))(e.type, Runner.events);
610 };
611
612
613 /**
614 * Bind relevant key / mouse / touch listeners.
615 */
616 Runner.prototype.startListening = function() {
617 // Keys.
618 document.addEventListener(Runner.events.KEYDOWN, this);
619 document.addEventListener(Runner.events.KEYUP, this);
620
621 if (Runner.isMobile) {
622 // Mobile only touch devices.
623 this.touchController.addEventListener(Runner.events.TOUCHSTART, this);
624 this.touchController.addEventListener(Runner.events.TOUCHEND, this);
625 this.containerEl.addEventListener(Runner.events.TOUCHSTART, this);
626 } else {
627 // Mouse.
628 document.addEventListener(Runner.events.MOUSEDOWN, this, false);
629 document.addEventListener(Runner.events.MOUSEUP, this, false);
630 }
631 };
632
633
634 /**
635 * Remove all listeners.
636 */
637 Runner.prototype.stopListening = function() {
638 document.removeEventListener(Runner.events.KEYDOWN, this);
639 document.removeEventListener(Runner.events.KEYUP, this);
640
641 if (Runner.isMobile) {
642 this.touchController.removeEventListener(Runner.events.TOUCHSTART, this);
643 this.touchController.removeEventListener(Runner.events.TOUCHEND, this);
644 this.containerEl.removeEventListener(Runner.events.TOUCHSTART, this);
645 } else {
646 document.removeEventListener(Runner.events.MOUSEDOWN, this, false);
647 document.removeEventListener(Runner.events.MOUSEUP, this, false);
648 }
649 };
650
651
652 /**
653 * Process keydown.
654 * @param {Event} e The event object.
655 */
656 Runner.prototype.onKeyDown = function(e) {
657
658 if (!this.crashed && (Runner.keycodes.JUMP[String(e.keyCode)] ||
659 e.type == Runner.events.TOUCHSTART)) {
660 if (!this.activated) {
661 this.activated = true;
662 }
663
664 if (!this.tRex.jumping) {
665 Runner.playSound(this.soundFx.BUTTON_PRESS);
666 this.tRex.startJump();
667 }
668 }
669
670 if (this.crashed && e.type == Runner.events.TOUCHSTART &&
671 e.currentTarget == this.containerEl) {
672 this.restart();
673 }
674
675 // Speed drop, activated only when jump key is not pressed.
676 if (Runner.keycodes.DUCK[e.keyCode] && this.tRex.jumping) {
677 e.preventDefault();
678 this.tRex.setSpeedDrop();
679 }
680 };
681
682
683 /**
684 * Process key up.
685 * @param {Event} e The event object.
686 */
687 Runner.prototype.onKeyUp = function(e) {
688 var keyCode = String(e.keyCode);
689 var isjumpKey = Runner.keycodes.JUMP[keyCode] ||
690 e.type == Runner.events.TOUCHEND ||
691 e.type == Runner.events.MOUSEDOWN;
692
693 if (this.isRunning() && isjumpKey) {
694 this.tRex.endJump();
695 } else if (Runner.keycodes.DUCK[keyCode]) {
696 this.tRex.speedDrop = false;
697 } else if (this.crashed) {
698 // Check that enough time has elapsed before allowing jump key to restart.
699 var deltaTime = performance.now() - this.time;
700
701 if (Runner.keycodes.RESTART[keyCode] ||
702 (e.type == Runner.events.MOUSEUP && e.target == this.canvas) ||
703 (deltaTime >= this.config.GAMEOVER_CLEAR_TIME &&
704 Runner.keycodes.JUMP[keyCode])) {
705 this.restart();
706 }
707 } else if (this.paused && isjumpKey) {
708 this.play();
709 }
710 };
711
712
713 /**
714 * RequestAnimationFrame wrapper.
715 */
716 Runner.prototype.raq = function() {
717 if (!this.drawPending) {
718 this.drawPending = true;
719 this.raqId = requestAnimationFrame(this.update.bind(this));
720 }
721 };
722
723
724 /**
725 * Whether the game is running.
726 * @return {boolean}
727 */
728 Runner.prototype.isRunning = function() {
729 return !!this.raqId;
730 };
731
732
733 /**
734 * Get random number.
735 * @param {number} min Minimum number.
736 * @param {number} max Maximum number.
737 * @param {number} A random number.
738 */
739 Runner.getRandomNum = function(min, max) {
740 return Math.floor(Math.random() * (max - min + 1)) + min;
741 };
742
743
744 Runner.vibrate = function(duration) {
745 if (Runner.isMobile) {
746 window.navigator['vibrate'](duration);
747 }
748 };
749
750 /**
751 * Game over state.
752 */
753 Runner.prototype.gameOver = function() {
754 Runner.playSound(this.soundFx.HIT);
755 Runner.vibrate(200);
756
757 this.stop();
758 this.crashed = true;
759 this.distanceMeter.acheivement = false;
760
761 this.tRex.update(100, Runner.Trex.status.CRASHED);
762
763 // Game over panel.
764 if (!this.gameOverPanel) {
765 this.gameOverPanel = new Runner.GameOverPanel(this.canvas,
766 this.images['TEXT_SPRITE'], this.images['RESTART'],
767 this.dimensions);
768 } else {
769 this.gameOverPanel.draw();
770 }
771
772 // Update the high score.
773 if (this.distanceRan > this.highestScore) {
774 this.highestScore = Math.ceil(this.distanceRan);
775 this.distanceMeter.setHighScore(this.highestScore);
776 }
777
778 // Reset the time clock.
779 this.time = performance.now();
780 };
781
782
783 /**
784 * Stop the game.
785 */
786 Runner.prototype.stop = function() {
787 this.activated = false;
788 this.paused = true;
789 cancelAnimationFrame(this.raqId);
790 this.raqId = 0;
791 };
792
793
794 /**
795 * Play the game.
796 */
797 Runner.prototype.play = function() {
798 if (!this.crashed) {
799 this.activated = true;
800 this.paused = false;
801 this.tRex.update(0, Runner.Trex.status.RUNNING);
802 this.time = performance.now();
803 this.update();
804 }
805 };
806
807
808 /**
809 * Restart game.
810 */
811 Runner.prototype.restart = function() {
812 if (!this.raqId) {
813 this.playCount++;
814 this.runningTime = 0;
815 this.activated = true;
816 this.crashed = false;
817 this.distanceRan = 0;
818 this.setSpeed(this.config.SPEED);
819
820 this.time = performance.now();
821 this.containerEl.classList.remove(Runner.classes.CRASHED);
822 this.clearCanvas();
823 this.distanceMeter.reset(this.highestScore);
824 this.horizon.reset();
825 this.tRex.reset();
826 Runner.playSound(this.soundFx.BUTTON_PRESS);
827
828 this.update();
829 }
830 };
831
832
833 /**
834 * Pause the game if the tab is not in focus.
835 */
836 Runner.prototype.onVisibilityChange = function(e) {
837 if (document.hidden || document.webkitHidden || e.type == 'blur') {
838 this.stop();
839 } else {
840 this.play();
841 }
842 };
843
844
845 /**
846 * Whether the game is running.
847 * @return {boolean}
848 */
849 Runner.prototype.isRunning = function() {
850 return this.raqId ? true : false;
851 };
852
853
854 /**
855 * Create canvas element.
856 * @param {HTMLElement} container Container element to append canvas to.
857 * @param {number} width Width of canvas.
858 * @param {number} height Height of canvas
859 * @param {string} opt_classname Optional class name.
860 * @return {HTMLCanvasElement} The canvas element.
861 */
862 Runner.createCanvas = function(container, width, height, opt_classname) {
863 var canvas = document.createElement('canvas');
864 canvas.className = opt_classname ? Runner.classes.CANVAS + ' ' +
865 opt_classname : Runner.classes.CANVAS;
866 canvas.width = width;
867 canvas.height = height;
868 container.appendChild(canvas);
869
870 return canvas;
871 };
872
873
874 /**
875 * Play a sound.
876 * @param {SoundBuffer} soundBuffer
877 */
878 Runner.playSound = function(soundBuffer) {
879 if (soundBuffer) {
880 var sourceNode = Runner.audioContext['createBufferSource']();
881 sourceNode.buffer = soundBuffer;
882 sourceNode['connect'](Runner.audioContext['destination']);
883 sourceNode['start'](0);
884 }
885 };
886
887
888 /**
889 * Decodes the base 64 audio to ArrayBuffer used by Web Audio.
890 */
891 Runner.decodeBase64ToArrayBuffer = function(base64String) {
892
893 var len = (base64String.length / 4) * 3;
894 var str = atob(base64String);
895 var arrayBuffer = new ArrayBuffer(len);
896 var bytes = new Uint8Array(arrayBuffer);
897
898 for (var i = 0; i < len; i++) {
899 bytes[i] = str.charCodeAt(i);
900 }
901 return bytes.buffer;
902 };
903
904
905 Runner.test = {};
906
907 /**
908 * Test for mobile platform.
909 * @return {boolean} Whether the platform is mobile.
910 */
911 Runner.test.mobile = function() {
912 return window.navigator.userAgent.indexOf('Mobi') > -1;
913 };
914
915 /**
916 * Test for hi DPI.
917 * @return {boolean} Whether the device has a high device pixel ratio.
918 */
919 Runner.test.hiDPI = function() {
920 return window.devicePixelRatio > 1;
921 };
922
923
924 /**
925 * Test for touch.
926 * @return {boolean} Whether touch support is available.
927 */
928 Runner.test.touchEnabled = function() {
929 return 'ontouchstart' in window;
930 };
931
932
933 Runner.isHDPI = Runner.test.hiDPI();
934
935 Runner.isMobile = Runner.test.mobile();
936
937 Runner.isTouchEnabled = Runner.test.touchEnabled();
938
939
940 //******************************************************************************
941
942
943 /**
944 * Game over panel.
945 * @param {!HTMLCanvasElement} canvas Canvas element to draw on.
946 * @param {!HTMLImage} textSprite Text sprite Image
947 * @param {!HTMLImage} restartImg Restart image.
948 * @param {!Object} dimensions Dimensions of the canvas.
949 * @constructor
950 */
951 Runner.GameOverPanel = function(canvas, textSprite, restartImg, dimensions) {
952 this.canvas = canvas;
953
954 this.canvasCtx = canvas.getContext('2d');
955
956 this.canvasDimensions = dimensions;
957
958 this.textSprite = textSprite;
959
960 this.restartImg = restartImg;
961
962 this.draw();
963 };
964
965
966 /**
967 * Dimensions used in the panel.
968 * @enum {string}
969 */
970 Runner.GameOverPanel.dimensions = {
971 TEXT_X: 0,
972 TEXT_Y: 13,
973 TEXT_WIDTH: 191,
974 TEXT_HEIGHT: 11,
975 RESTART_WIDTH: 36,
976 RESTART_HEIGHT: 32
977 };
978
979
980 /**
981 * Update the panel dimensions.
982 * @param {number} width New canvas width.
983 * @param {number} opt_height Optional new canvas height.
984 */
985 Runner.GameOverPanel.prototype.updateDimensions = function(width, opt_height) {
986 this.canvasDimensions.WIDTH = width;
987 if (opt_height) {
988 this.canvasDimensions.HEIGHT = opt_height;
989 }
990 };
991
992
993 /**
994 * Draw the panel.
995 */
996 Runner.GameOverPanel.prototype.draw = function() {
997 var dimensions = Runner.GameOverPanel.dimensions;
998
999 var centerX = this.canvasDimensions.WIDTH / 2;
1000
1001 // Game over text.
1002 var textSourceX = dimensions.TEXT_X;
1003 var textSourceY = dimensions.TEXT_Y;
1004 var textSourceWidth = dimensions.TEXT_WIDTH;
1005 var textSourceHeight = dimensions.TEXT_HEIGHT;
1006
1007 var textTargetX = Math.round(centerX - (dimensions.TEXT_WIDTH / 2));
1008 var textTargetY = Math.round((this.canvasDimensions.HEIGHT - 25) / 3);
1009 var textTargetWidth = dimensions.TEXT_WIDTH;
1010 var textTargetHeight = dimensions.TEXT_HEIGHT;
1011
1012 var restartSourceWidth = dimensions.RESTART_WIDTH;
1013 var restartSourceHeight = dimensions.RESTART_HEIGHT;
1014 var restartTargetX = centerX - (dimensions.RESTART_WIDTH / 2);
1015 var restartTargetY = this.canvasDimensions.HEIGHT / 2;
1016
1017
1018 if (Runner.isHDPI) {
1019 textSourceY *= 2;
1020 textSourceX *= 2;
1021 textSourceWidth = textSourceWidth * 2;
1022 textSourceHeight = textSourceHeight * 2;
1023 restartSourceWidth = restartSourceWidth * 2;
1024 restartSourceHeight = restartSourceHeight * 2;
1025 }
1026
1027 // Game over text from sprite.
1028 this.canvasCtx.drawImage(this.textSprite,
1029 textSourceX, textSourceY, textSourceWidth, textSourceHeight,
1030 textTargetX, textTargetY, textTargetWidth, textTargetHeight);
1031
1032 // Restart button.
1033 this.canvasCtx.drawImage(this.restartImg, 0, 0,
1034 restartSourceWidth, restartSourceHeight,
1035 restartTargetX, restartTargetY, dimensions.RESTART_WIDTH,
1036 dimensions.RESTART_HEIGHT);
1037 };
1038
1039
1040 //******************************************************************************
1041
1042 /**
1043 * Collision checker utilities.
1044 */
1045 Runner.Collision = function() {};
1046
1047
1048 /**
1049 * Check for a collision.
1050 * @param {!Runner.Obstacle} obstacle Obstacle object.
1051 * @param {!Runner.Trex} tRex T-rex object.
1052 * @param {HTMLCanvasContext} opt_canvasCtx Optional canvas context for drawing
1053 * collision boxes.
1054 * @return {Array.<Runner.CollisionBox>}
1055 */
1056 Runner.Collision.check = function(obstacle, tRex, opt_canvasCtx) {
1057 var obstacleBoxXPos = Runner.defaultDimensions.WIDTH + obstacle.xPos;
1058
1059 // Adjustments are made to the bounding box as there is a 1 pixel white
1060 // border around the t-rex and obstacles.
1061 var tRexBox = new Runner.CollisionBox(
1062 tRex.xPos + 1,
1063 tRex.yPos + 1,
1064 tRex.config.WIDTH - 2,
1065 tRex.config.HEIGHT - 2
1066 );
1067
1068 var obstacleBox = new Runner.CollisionBox(
1069 obstacle.xPos + 1,
1070 obstacle.yPos + 1,
1071 obstacle.typeConfig.width * obstacle.size - 2,
1072 obstacle.typeConfig.height - 2
1073 );
1074
1075 // Debug outer box
1076 if (opt_canvasCtx) {
1077 Runner.Collision.drawCollisionBoxes(opt_canvasCtx, tRexBox, obstacleBox);
1078 }
1079
1080 // Simple outer bounds check.
1081 if (Runner.Collision.boxCompare(tRexBox, obstacleBox)) {
1082 var collisionBoxes = obstacle.collisionBoxes;
1083 var tRexCollisionBoxes = Runner.Trex.collisionBoxes;
1084
1085 // Detailed axis aligned box check.
1086 for (var t = 0, l = tRexCollisionBoxes.length; t < l; t++) {
1087 for (var i = 0, j = collisionBoxes.length; i < j; i++) {
1088 // Adjust the box to actual positions.
1089 var adjTrexBox =
1090 Runner.Collision.createAdjustedCollisionBox(tRexCollisionBoxes[t],
1091 tRexBox);
1092 var adjObstacleBox =
1093 Runner.Collision.createAdjustedCollisionBox(collisionBoxes[i],
1094 obstacleBox);
1095
1096 var crashed = Runner.Collision.boxCompare(adjTrexBox, adjObstacleBox);
1097
1098 // Draw boxes for debug.
1099 if (opt_canvasCtx) {
1100 Runner.Collision.drawCollisionBoxes(opt_canvasCtx,
1101 adjTrexBox, adjObstacleBox);
1102 }
1103
1104 if (crashed) {
1105 return [adjTrexBox, adjObstacleBox];
1106 }
1107 }
1108 }
1109 }
1110 return false;
1111 };
1112
1113
1114 /**
1115 * Adjust the collision box.
1116 * @param {!Runner.CollisionBox} box The original box.
1117 * @param {!Runner.CollisionBox} adjustment Adjustment box.
1118 * @return {Runner.CollisionBox} The adjusted collision box object.
1119 */
1120 Runner.Collision.createAdjustedCollisionBox = function(box, adjustment) {
1121 return new Runner.CollisionBox(
1122 box.x + adjustment.x,
1123 box.y + adjustment.y,
1124 box.width,
1125 box.height);
1126 };
1127
1128
1129 /**
1130 * Draw the collision box for debug.
1131 */
1132 Runner.Collision.drawCollisionBoxes =
1133 function(canvasCtx, tRexBox, obstacleBox) {
1134 canvasCtx.save();
1135 canvasCtx.strokeStyle = '#f00';
1136 canvasCtx.strokeRect(tRexBox.x, tRexBox.y,
1137 tRexBox.width, tRexBox.height);
1138
1139 canvasCtx.strokeStyle = '#0f0';
1140 canvasCtx.strokeRect(obstacleBox.x, obstacleBox.y,
1141 obstacleBox.width, obstacleBox.height);
1142 canvasCtx.restore();
1143 };
1144
1145
1146 /**
1147 * Compare two collision boxes for a collision.
1148 * @param {Runner.CollisionBox} tRexBox
1149 * @param {Runner.CollisionBox} obstacleBox
1150 * @return {boolean} Whether the boxes intersected.
1151 */
1152 Runner.Collision.boxCompare = function(tRexBox, obstacleBox) {
1153 var crashed = false;
1154 var tRexBoxX = tRexBox.x;
1155 var tRexBoxY = tRexBox.y;
1156
1157 var obstacleBoxX = obstacleBox.x;
1158 var obstacleBoxY = obstacleBox.y;
1159
1160 // Axis-Aligned Bounding Box method.
1161 if (tRexBox.x < obstacleBoxX + obstacleBox.width &&
1162 tRexBox.x + tRexBox.width > obstacleBoxX &&
1163 tRexBox.y < obstacleBox.y + obstacleBox.height &&
1164 tRexBox.height + tRexBox.y > obstacleBox.y) {
1165 crashed = true;
1166 }
1167
1168 return crashed;
1169 };
1170
1171
1172 //******************************************************************************
1173
1174 /**
1175 * Collision box object.
1176 * @param {number} x X position.
1177 * @param {number} y Y Position.
1178 * @param {number} w Width.
1179 * @param {number} h Height.
1180 */
1181 Runner.CollisionBox = function(x, y, w, h) {
1182 this.x = x;
1183 this.y = y;
1184 this.width = w;
1185 this.height = h;
1186 };
1187
1188
1189 //******************************************************************************
1190
1191 /**
1192 * Obstacle.
1193 * @param {HTMLCanvasCtx} canvasCtx Canvas context.
1194 * @param {Runner.Obstacle.type} type Type of object.
1195 * @param {image} obstacleImg Image sprite to use obstacle.
1196 * @param {Object} dimensions Obstacle dimensions.
1197 * @param {number} gapCoefficient Mutipler in determining the gap.
1198 * @param {number} speed Current game speed.
1199 */
1200 Runner.Obstacle = function(canvasCtx, type, obstacleImg, dimensions,
1201 gapCoefficient, speed) {
1202
1203 this.canvasCtx = canvasCtx;
1204
1205 this.image = obstacleImg;
1206
1207 this.typeConfig = type;
1208
1209 this.gapCoefficient = gapCoefficient;
1210
1211 this.size = Runner.getRandomNum(1, Runner.Obstacle.MAX_OBSTACLE_LENGTH);
1212
1213 this.dimensions = dimensions;
1214
1215 this.remove = false;
1216
1217 this.xPos = 0;
1218
1219 this.yPos = this.typeConfig.yPos;
1220
1221 this.width = 0;
1222
1223 this.collisionBoxes = [];
1224
1225 this.gap = 0;
1226
1227 this.init(speed);
1228 };
1229
1230 /**
1231 *
1232 */
1233 Runner.Obstacle.MAX_GAP_COEFFICIENT = 1.5;
1234
1235 /**
1236 * Maximum obstacle grouping count.
1237 */
1238 Runner.Obstacle.MAX_OBSTACLE_LENGTH = 3,
1239
1240
1241 /**
1242 * Initialise the DOM for the obstacle.
1243 * @param {number} speed
1244 */
1245 Runner.Obstacle.prototype.init = function(speed) {
1246 this.cloneCollisionBoxes();
1247
1248 // Only allow sizing if we're at the right speed.
1249 if (this.size > 1 && this.typeConfig.multipleSpeed > speed) {
1250 this.size = 1;
1251 }
1252
1253 this.width = this.typeConfig.width * this.size;
1254
1255 this.xPos = this.dimensions.WIDTH - this.width;
1256
1257 this.draw();
1258
1259 // Make collision box adjustments,
1260 // Central box is adjusted to the size as one box.
1261 // ____ ______ ________
1262 // _| |-| _| |-| _| |-|
1263 // | |<->| | | |<--->| | | |<----->| |
1264 // | | 1 | | | | 2 | | | | 3 | |
1265 // |_|___|_| |_|_____|_| |_|_______|_|
1266 //
1267 if (this.size > 1) {
1268 this.collisionBoxes[1].width = this.width - this.collisionBoxes[0].width -
1269 this.collisionBoxes[2].width;
1270 this.collisionBoxes[2].x = this.width - this.collisionBoxes[2].width;
1271 }
1272
1273 this.gap = this.getGap(this.gapCoefficient, speed);
1274 };
1275
1276
1277 /**
1278 * Draw and crop based on size.
1279 */
1280 Runner.Obstacle.prototype.draw = function() {
1281 // this.canvasCtx.save();
1282
1283 var sourceWidth = this.typeConfig.width;
1284 var sourceHeight = this.typeConfig.height;
1285
1286 if (Runner.isHDPI) {
1287 sourceWidth = sourceWidth * 2;
1288 sourceHeight = sourceHeight * 2;
1289 }
1290
1291 // Sprite
1292 var sourceX = (sourceWidth * this.size) * (0.5 * (this.size - 1));
1293 this.canvasCtx.drawImage(this.image,
1294 sourceX, 0,
1295 sourceWidth * this.size, sourceHeight,
1296 this.xPos, this.yPos,
1297 this.typeConfig.width * this.size, this.typeConfig.height);
1298 // this.canvasCtx.restore();
1299 };
1300
1301
1302 /**
1303 * Obstacle frame update.
1304 * @param {number} deltaTime Time in ms.
1305 * @param {number} speed Current speed.
1306 */
1307 Runner.Obstacle.prototype.update = function(deltaTime, speed) {
1308 if (!this.remove) {
1309 this.xPos -= Math.floor((speed * Runner.FPS / 1000) * deltaTime);
1310 this.draw();
1311
1312 if (!this.isVisible()) {
1313 this.remove = true;
1314 }
1315 }
1316 };
1317
1318
1319 /**
1320 * Calculate a random gap size.
1321 * - Minimum gap gets wider as speed increses
1322 * @param {number} gapCoefficient
1323 * @param {number} speed Current speed.
1324 * @return {number} The gap size.
1325 */
1326 Runner.Obstacle.prototype.getGap = function(gapCoefficient, speed) {
1327 var minGap = Math.round((this.width * speed) +
1328 (this.typeConfig.minGap * gapCoefficient));
1329 var maxGap = Math.round(minGap * Runner.Obstacle.MAX_GAP_COEFFICIENT);
1330 return Runner.getRandomNum(minGap, maxGap);
1331 };
1332
1333
1334
1335 /**
1336 * Check if obstacle is visible.
1337 * @return {boolean} Whether the obstacle is in the game area.
1338 */
1339 Runner.Obstacle.prototype.isVisible = function() {
1340 return (this.xPos + this.width) > 0;
1341 };
1342
1343
1344 /**
1345 * Make a copy of the collision boxes, since these will change based on
1346 * obstacle type and size.
1347 */
1348 Runner.Obstacle.prototype.cloneCollisionBoxes = function() {
1349 var collisionBoxes = this.typeConfig.collisionBoxes;
1350
1351 for (var i = collisionBoxes.length - 1; i >= 0; i--) {
1352 this.collisionBoxes[i] = new Runner.CollisionBox(collisionBoxes[i].x,
1353 collisionBoxes[i].y, collisionBoxes[i].width, collisionBoxes[i].height);
1354 }
1355 };
1356
1357
1358 /**
1359 * Obstacle definitions.
1360 * minGap: minimum pixel space betweeen obstacles.
1361 * multipleSpeed: Speed at which multiples are allowed.
1362 */
1363 Runner.Obstacle.types = [
1364 {
1365 type: 'CACTUS_SMALL',
1366 className: ' cactus cactus-small ',
1367 width: 17,
1368 height: 35,
1369 yPos: 105,
1370 multipleSpeed: 3,
1371 minGap: 120,
1372 collisionBoxes: [
1373 new Runner.CollisionBox(0, 7, 5, 27),
1374 new Runner.CollisionBox(4, 0, 6, 34),
1375 new Runner.CollisionBox(10, 4, 7, 14)
1376 ]
1377 },
1378 {
1379 type: 'CACTUS_LARGE',
1380 className: ' cactus cactus-large ',
1381 width: 25,
1382 height: 50,
1383 yPos: 90,
1384 multipleSpeed: 6,
1385 minGap: 120,
1386 collisionBoxes: [
1387 new Runner.CollisionBox(0, 12, 7, 38),
1388 new Runner.CollisionBox(8, 0, 7, 49),
1389 new Runner.CollisionBox(13, 10, 10, 38)
1390 ]
1391 }
1392 ];
1393
1394
1395 //******************************************************************************
1396 /**
1397 * T-rex game character.
1398 * @param {HTMLCanvas} canvas HTML Canvas.
1399 * @param {HTMLImage} image Character image.
1400 * @constructor
1401 */
1402 Runner.Trex = function(canvas, image) {
1403 this.canvas = canvas;
1404 this.canvasCtx = canvas.getContext('2d');
1405
1406 this.image = image;
1407
1408 this.xPos = 0;
1409
1410 this.yPos = 0;
1411
1412 // Position when on the ground.
1413 this.groundYPos = 0;
1414
1415 this.currentFrame = 0;
1416
1417 this.currentAnimFrames = [];
1418
1419 this.blinkDelay = 0;
1420
1421 this.animStartTime = 0;
1422
1423 this.timer = 0;
1424
1425 this.msPerFrame = 1000 / Runner.FPS;
1426
1427 this.config = Runner.Trex.config;
1428
1429 // Current status.
1430 this.status = Runner.Trex.status.WAITING;
1431
1432 this.jumping = false;
1433
1434 this.jumpVelocity = 0;
1435
1436 this.reachedMinHeight = false;
1437
1438 this.speedDrop = false;
1439
1440 this.jumpCount = 0;
1441
1442 this.jumpspotX = 0;
1443
1444 this.init();
1445 };
1446
1447
1448 /**
1449 * T-rex player config.
1450 */
1451 Runner.Trex.config = {
1452 DROP_VELOCITY: -5,
1453 GRAVITY: 0.6,
1454 HEIGHT: 47,
1455 INIITAL_JUMP_VELOCITY: -10,
1456 INTRO_DURATION: 1500,
1457 MAX_JUMP_HEIGHT: 30,
1458 MIN_JUMP_HEIGHT: 30,
1459 SPEED_DROP_COEFFICIENT: 3,
1460 SPRITE_WIDTH: 262,
1461 START_X_POS: 50,
1462 WIDTH: 44
1463 };
1464
1465
1466 /**
1467 * Used in collision detection.
1468 * @type {Array.<Runner.CollisionBox>}
1469 */
1470 Runner.Trex.collisionBoxes = [
1471 new Runner.CollisionBox(1, -1, 30, 26),
1472 new Runner.CollisionBox(32, 0, 8, 16),
1473 new Runner.CollisionBox(10, 35, 14, 8),
1474 new Runner.CollisionBox(1, 24, 29, 5),
1475 new Runner.CollisionBox(5, 30, 21, 4),
1476 new Runner.CollisionBox(9, 34, 15, 4)
1477 ];
1478
1479
1480 /**
1481 * Animation states.
1482 * @enum {string}
1483 */
1484 Runner.Trex.status = {
1485 CRASHED: 'crashed',
1486 JUMPING: 'jumping',
1487 RUNNING: 'running',
1488 WAITING: 'waiting'
1489 };
1490
1491 // Blinking coefficient.
1492 Runner.Trex.BLINK_TIMING = 7000;
1493
1494
1495 /**
1496 * Animation config for different states.
1497 */
1498 Runner.Trex.animFrames = {
1499 'waiting': {
1500 frames: [44, 0],
1501 msPerFrame: 1000 / 3
1502 },
1503 'running': {
1504 frames: [88, 132],
1505 msPerFrame: 1000 / 12
1506 },
1507 'crashed': {
1508 frames: [220],
1509 msPerFrame: 1000 / 60
1510 },
1511 'jumping': {
1512 frames: [0],
1513 msPerFrame: 1000 / 60
1514 }
1515 };
1516
1517 /**
1518 * T-rex player initaliser.
1519 * Sets the t-rex to blink at random intervals.
1520 */
1521 Runner.Trex.prototype.init = function() {
1522 this.blinkDelay = this.setBlinkDelay();
1523 this.groundYPos = Runner.defaultDimensions.HEIGHT - this.config.HEIGHT -
1524 Runner.config.BOTTOM_PAD;
1525 this.yPos = this.groundYPos;
1526
1527 this.minJumpHeight = this.groundYPos - this.config.MIN_JUMP_HEIGHT;
1528
1529 this.draw(0, 0);
1530 this.update(0, Runner.Trex.status.WAITING);
1531 };
1532
1533
1534 /**
1535 * Setter for the jump velocity.
1536 * The approriate drop velocity is also set.
1537 */
1538 Runner.Trex.prototype.setJumpVelocity = function(setting) {
1539 this.config.INIITAL_JUMP_VELOCITY = -setting;
1540 this.config.DROP_VELOCITY = -setting / 2;
1541 };
1542
1543
1544 /**
1545 * Set the animation status.
1546 * @param {!number} deltaTime Time since last update.
1547 * @param {Runner.Trex.status} status Optional status to switch to.
1548 */
1549 Runner.Trex.prototype.update = function(deltaTime, opt_status) {
1550 this.timer += deltaTime;
1551
1552 // Update the status.
1553 if (opt_status) {
1554 this.status = opt_status;
1555 this.currentFrame = 0;
1556 this.msPerFrame = Runner.Trex.animFrames[opt_status].msPerFrame;
1557 this.currentAnimFrames = Runner.Trex.animFrames[opt_status].frames;
1558
1559 if (opt_status == Runner.Trex.status.WAITING) {
1560 this.animStartTime = performance.now();
1561 this.setBlinkDelay();
1562 }
1563 }
1564
1565 // Game intro animation, t-rex moves in from the left.
1566 if (this.playingIntro && this.xPos < this.config.START_X_POS) {
1567 this.xPos += Math.round((this.config.START_X_POS /
1568 this.config.INTRO_DURATION) * deltaTime);
1569 }
1570
1571 if (this.status == Runner.Trex.status.WAITING) {
1572 this.blink(performance.now());
1573 } else {
1574 this.draw(this.currentAnimFrames[this.currentFrame], 0);
1575 }
1576
1577 // Update the frame position.
1578 if (this.timer >= this.msPerFrame) {
1579 this.currentFrame = this.currentFrame == this.currentAnimFrames.length - 1 ?
1580 0 : this.currentFrame + 1;
1581 this.timer = 0;
1582 }
1583 };
1584
1585
1586 /**
1587 * Draw the t-rex to a particular position.
1588 * @param {number} x X position.
1589 * @param {number} y Y position.
1590 */
1591 Runner.Trex.prototype.draw = function(x, y) {
1592 var sourceX = x;
1593 var sourceY = y;
1594 var sourceWidth = this.config.WIDTH;
1595 var sourceHeight = this.config.HEIGHT;
1596
1597 if (Runner.isHDPI) {
1598 sourceX *= 2;
1599 sourceY *= 2;
1600 sourceWidth *= 2;
1601 sourceHeight *= 2;
1602 }
1603
1604 this.canvasCtx.drawImage(this.image, sourceX, sourceY,
1605 sourceWidth, sourceHeight,
1606 this.xPos, this.yPos,
1607 this.config.WIDTH, this.config.HEIGHT);
1608 };
1609
1610
1611 /**
1612 * Sets a random time for the blink to happen.
1613 */
1614 Runner.Trex.prototype.setBlinkDelay = function() {
1615 this.blinkDelay = Math.ceil(Math.random() * Runner.Trex.BLINK_TIMING);
1616 };
1617
1618 /**
1619 * Make t-rex blink at random intervals.
1620 * @param {number} time Current time in milliseconds.
1621 */
1622 Runner.Trex.prototype.blink = function(time) {
1623 var deltaTime = time - this.animStartTime;
1624
1625 if (deltaTime >= this.blinkDelay) {
1626 this.draw(this.currentAnimFrames[this.currentFrame], 0);
1627
1628 if (this.currentFrame == 1) {
1629 // Set new random delay to blink.
1630 this.setBlinkDelay();
1631 this.animStartTime = time;
1632 }
1633 }
1634 };
1635
1636
1637 /**
1638 * Initialise a jump.
1639 */
1640 Runner.Trex.prototype.startJump = function() {
1641 if (!this.jumping) {
1642 this.update(0, Runner.Trex.status.JUMPING);
1643 this.jumpVelocity = this.config.INIITAL_JUMP_VELOCITY;
1644 this.jumping = true;
1645 this.reachedMinHeight = false;
1646 this.speedDrop = false;
1647 }
1648 };
1649
1650
1651 /**
1652 * Jump is complete, falling down.
1653 */
1654 Runner.Trex.prototype.endJump = function() {
1655 if (this.reachedMinHeight &&
1656 this.jumpVelocity < this.config.DROP_VELOCITY) {
1657 this.jumpVelocity = this.config.DROP_VELOCITY;
1658 }
1659 };
1660
1661
1662 /**
1663 * Update frame for a jump.
1664 * @param {number} deltaTime Time since last update.
1665 */
1666 Runner.Trex.prototype.updateJump = function(deltaTime) {
1667 var msPerFrame = Runner.Trex.animFrames[this.status].msPerFrame;
1668 var framesElapsed = deltaTime / msPerFrame;
1669
1670 // Speed drop makes Trex fall faster.
1671 if (this.speedDrop) {
1672 this.yPos += Math.round(this.jumpVelocity *
1673 this.config.SPEED_DROP_COEFFICIENT * framesElapsed);
1674 } else {
1675 this.yPos += Math.round(this.jumpVelocity * framesElapsed);
1676 }
1677
1678 this.jumpVelocity += this.config.GRAVITY * framesElapsed;
1679
1680 // Minimum height has been reached.
1681 if (this.yPos < this.minJumpHeight || this.speedDrop) {
1682 this.reachedMinHeight = true;
1683 }
1684
1685 // Reached max height
1686 if (this.yPos < this.config.MAX_JUMP_HEIGHT || this.speedDrop) {
1687 this.endJump();
1688 }
1689
1690 // Back down at ground level. Jump completed.
1691 if (this.yPos > this.groundYPos) {
1692 this.reset();
1693 this.jumpCount++;
1694 }
1695
1696 this.update(deltaTime);
1697 };
1698
1699
1700 /**
1701 * Set the speed drop. Immediately cancels the current jump.
1702 */
1703 Runner.Trex.prototype.setSpeedDrop = function() {
1704 this.speedDrop = true;
1705 this.jumpVelocity = 1;
1706 };
1707
1708
1709 /**
1710 * Reset the t-rex to running at start of game.
1711 */
1712 Runner.Trex.prototype.reset = function() {
1713 this.yPos = this.groundYPos;
1714 this.jumpVelocity = 0;
1715 this.jumping = false;
1716 this.update(0, Runner.Trex.status.RUNNING);
1717 this.midair = false;
1718 this.speedDrop = false;
1719 this.jumpCount = 0;
1720 };
1721
1722
1723 //******************************************************************************
1724
1725 /**
1726 * Handles displaying the distance meter.
1727 * @param {!HTMLCanvasElement} canvas Canvas element to draw to.
1728 * @param {!HTMLImage} spriteSheet Image sprite.
1729 * @param {number} width Width of the container
1730 * @constructor
1731 */
1732 Runner.DistanceMeter = function(canvas, spriteSheet, canvasWidth) {
1733 this.canvas = canvas;
1734
1735 this.canvasCtx = canvas.getContext('2d');
1736
1737 this.image = spriteSheet;
1738
1739 this.x = 0;
1740
1741 this.y = 5;
1742
1743 this.currentDistance = 0;
1744
1745 this.maxScore = 0;
1746
1747 this.highScore = 0;
1748
1749 this.container = null;
1750
1751 this.digits = [];
1752
1753 this.acheivement = false;
1754
1755 this.defaultString = '';
1756
1757 this.flashTimer = 0;
1758
1759 this.flashIterations = 0;
1760
1761 this.init(canvasWidth);
1762 };
1763
1764
1765 Runner.DistanceMeter.dimensions = {
1766 WIDTH: 10,
1767 HEIGHT: 13,
1768 DEST_WIDTH: 11
1769 };
1770
1771
1772 /**
1773 * Y positioning of the digits in the sprite sheet.
1774 * X position is always 0.
1775 */
1776 Runner.DistanceMeter.yPos = [0, 13, 27, 40, 53, 67, 80, 93, 107, 120];
1777
1778 /**
1779 * Number of digits.
1780 */
1781 Runner.DistanceMeter.MAX_DISTANCE_UNITS = 5;
1782
1783 /**
1784 * Distance that causes achievement animation.
1785 */
1786 Runner.DistanceMeter.ACHIEVEMENT_DISTANCE = 100;
1787
1788 /**
1789 * Used for conversion from pixel distance to a scaled unit.
1790 */
1791 Runner.DistanceMeter.COEFFICIENT = 0.025;
1792
1793 /**
1794 * Flash duration in milliseconds.
1795 */
1796 Runner.DistanceMeter.FLASH_DURATION = 1000 / 4;
1797
1798 /**
1799 * Flash iterations for achievement animation.
1800 */
1801 Runner.DistanceMeter.FLASH_ITERATIONS = 3;
1802
1803
1804 /**
1805 * Initialise the distance meter to '00000'.
1806 */
1807 Runner.DistanceMeter.prototype.init = function(width) {
1808 var maxDistanceStr = '';
1809
1810 this.calcXPos(width);
1811 this.maxScore = Runner.DistanceMeter.MAX_DISTANCE_UNITS;
1812 for (var i = 0; i < Runner.DistanceMeter.MAX_DISTANCE_UNITS; i++) {
1813 this.draw(i, 0);
1814 this.defaultString += '0';
1815 maxDistanceStr += '9';
1816 }
1817
1818 this.maxScore = parseInt(maxDistanceStr);
1819
1820 };
1821
1822
1823 /**
1824 * Calculate the xPos in the canvas.
1825 */
1826 Runner.DistanceMeter.prototype.calcXPos = function(canvasWidth) {
1827 this.x = canvasWidth - (Runner.DistanceMeter.dimensions.DEST_WIDTH *
1828 (Runner.DistanceMeter.MAX_DISTANCE_UNITS + 1));
1829 };
1830
1831
1832 /**
1833 * Draw a digit to canvas.
1834 * @param {number} digitPos Position of the digit.
1835 * @param {number} value Digit value 0-9.
1836 * @param {boolean} opt_highScore Whether we a drawing the high score.
1837 */
1838 Runner.DistanceMeter.prototype.draw = function(digitPos, value, opt_highScore) {
1839 var sourceWidth = Runner.DistanceMeter.dimensions.WIDTH;
1840 var sourceHeight = Runner.DistanceMeter.dimensions.HEIGHT;
1841 var sourceX = (Runner.DistanceMeter.dimensions.WIDTH) * value;
1842
1843 var targetX = (digitPos * (Runner.DistanceMeter.dimensions.DEST_WIDTH));
1844 var targetY = this.y;
1845 var targetWidth = Runner.DistanceMeter.dimensions.WIDTH;
1846 var targetHeight = Runner.DistanceMeter.dimensions.HEIGHT;
1847
1848 // For high DPI we 2x source values.
1849 if (Runner.isHDPI) {
1850 sourceWidth = sourceWidth * 2;
1851 sourceHeight = sourceHeight * 2;
1852 sourceX = sourceX * 2;
1853 }
1854
1855 this.canvasCtx.save();
1856
1857 if (opt_highScore) {
1858 // Left of the current score.
1859 var highScoreX = this.x - ((Runner.DistanceMeter.MAX_DISTANCE_UNITS * 2)) *
1860 Runner.DistanceMeter.dimensions.WIDTH;
1861
1862 this.canvasCtx.translate(highScoreX, this.y);
1863 } else {
1864 this.canvasCtx.translate(this.x, this.y);
1865 }
1866
1867 this.canvasCtx.drawImage(this.image, sourceX, 0,
1868 sourceWidth, sourceHeight,
1869 targetX, targetY,
1870 targetWidth, targetHeight
1871 );
1872
1873 this.canvasCtx.restore();
1874 };
1875
1876
1877 /**
1878 * Covert pixel distance to a 'real' distance.
1879 * @param {number} distance Pixel distance ran.
1880 * @return {number} The 'real' distance ran.
1881 */
1882 Runner.DistanceMeter.prototype.getActualDistance = function(distance) {
1883 return distance ?
1884 Math.round(distance * Runner.DistanceMeter.COEFFICIENT) : 0;
1885 };
1886
1887
1888 /**
1889 * Update the distance meter.
1890 * @param {number} deltaTime Time since last update.
1891 * @param {number} distance Distance to display.
1892 * @param {sound} sound Sound to play.
1893 */
1894 Runner.DistanceMeter.prototype.update = function(deltaTime, distance, sound) {
1895 var paint = true;
1896
1897 if (!this.acheivement) {
1898 distance = this.getActualDistance(distance);
1899
1900 if (distance > 0) {
1901 // Acheivement unlocked
1902 if (distance % Runner.DistanceMeter.ACHIEVEMENT_DISTANCE == 0) {
1903 // Flash score and play sound.
1904 this.acheivement = true;
1905 this.flashTimer = 0;
1906 Runner.playSound(sound);
1907 }
1908
1909 // Create a string representation of the distance with leading 0.
1910 var distanceStr = (this.defaultString +
1911 distance).substr(-Runner.DistanceMeter.MAX_DISTANCE_UNITS);
1912 this.digits = String(distanceStr).split('');
1913 } else {
1914 this.digits = this.defaultString.split('');
1915 }
1916 } else {
1917 // Control flashing of the score on reaching acheivement.
1918 if (this.flashIterations <= Runner.DistanceMeter.FLASH_ITERATIONS) {
1919 this.flashTimer += deltaTime;
1920
1921 if (this.flashTimer < Runner.DistanceMeter.FLASH_DURATION) {
1922 paint = false;
1923 } else if (this.flashTimer > (Runner.DistanceMeter.FLASH_DURATION * 2)) {
1924 this.flashTimer = 0;
1925 this.flashIterations++;
1926 }
1927 } else {
1928 this.acheivement = false;
1929 this.flashIterations = 0;
1930 this.flashTimer = 0;
1931 }
1932 }
1933
1934 // Draw the digits if not flashing.
1935 if (paint) {
1936 for (var i = this.digits.length - 1; i > -1; i--) {
1937 this.draw(i, parseInt(this.digits[i]));
1938 }
1939 }
1940
1941 this.drawHighScore();
1942
1943 return this.acheivement;
1944 };
1945
1946
1947 /**
1948 * Draw the high score.
1949 */
1950 Runner.DistanceMeter.prototype.drawHighScore = function() {
1951 this.canvasCtx.save();
1952 this.canvasCtx.globalAlpha = .8;
1953 for (var i = this.highScore.length - 1; i > -1; i--) {
1954 this.draw(i, parseInt(this.highScore[i]), true);
1955 }
1956 this.canvasCtx.restore();
1957 };
1958
1959
1960 /**
1961 * Set the highscore as a array string.
1962 * Position of char in the sprite: H - 10 in and I - 11.
1963 * @param {number} distance Distance in pixels.
1964 */
1965 Runner.DistanceMeter.prototype.setHighScore = function(distance) {
1966 distance = this.getActualDistance(distance);
1967 var highScoreStr = (this.defaultString +
1968 distance).substr(-Runner.DistanceMeter.MAX_DISTANCE_UNITS);
1969
1970 this.highScore = ['10', '11', ''].concat(highScoreStr.split(''));
1971 };
1972
1973
1974 /**
1975 * Reset the distance meter back to '00000'.
1976 */
1977 Runner.DistanceMeter.prototype.reset = function() {
1978 this.update(0);
1979 this.acheivement = false;
1980 };
1981
1982
1983
1984 //******************************************************************************
1985
1986 /**
1987 * Cloud background item.
1988 * Similar to an obstacle object but without collision boxes.
1989 * @param {HTMLCanvasElement} canvas Canvas element.
1990 * @param {Image} cloudImg Cloud image.
1991 * @param {number} containerWidth Width of container.
1992 */
1993 Runner.Cloud = function(canvas, cloudImg, containerWidth) {
1994 this.canvas = canvas;
1995
1996 this.canvasCtx = this.canvas.getContext('2d');
1997
1998 this.image = cloudImg;
1999
2000 this.containerWidth = containerWidth;
2001
2002 this.xPos = containerWidth;
2003
2004 this.yPos = 0;
2005
2006 this.remove = false;
2007
2008 this.cloudGap = Runner.getRandomNum(Runner.Cloud.config.MIN_CLOUD_GAP,
2009 Runner.Cloud.config.MAX_CLOUD_GAP);
2010
2011 this.init();
2012 };
2013
2014
2015 /**
2016 * Cloud object config.
2017 * @enum {string}
2018 */
2019 Runner.Cloud.config = {
2020 HEIGHT: 13,
2021 MAX_CLOUD_GAP: 400,
2022 MAX_SKY_LEVEL: 30,
2023 MIN_CLOUD_GAP: 100,
2024 MIN_SKY_LEVEL: 71,
2025 WIDTH: 46
2026 };
2027
2028
2029 /**
2030 * Initialise the cloud. Sets the Cloud height.
2031 */
2032 Runner.Cloud.prototype.init = function() {
2033 this.yPos = Runner.getRandomNum(Runner.Cloud.config.MAX_SKY_LEVEL,
2034 Runner.Cloud.config.MIN_SKY_LEVEL);
2035 this.draw();
2036 };
2037
2038
2039 /**
2040 * Draw the cloud.
2041 */
2042 Runner.Cloud.prototype.draw = function() {
2043 this.canvasCtx.save();
2044 var sourceWidth = Runner.Cloud.config.WIDTH;
2045 var sourceHeight = Runner.Cloud.config.HEIGHT;
2046
2047 if (Runner.isHDPI) {
2048 sourceWidth = sourceWidth * 2;
2049 sourceHeight = sourceHeight * 2;
2050 }
2051
2052 this.canvasCtx.drawImage(this.image, 0, 0,
2053 sourceWidth, sourceHeight,
2054 this.xPos, this.yPos,
2055 Runner.Cloud.config.WIDTH, Runner.Cloud.config.HEIGHT);
2056
2057 this.canvasCtx.restore();
2058 };
2059
2060
2061 /**
2062 * Update the cloud position.
2063 * @param {number} speed Speed.
2064 */
2065 Runner.Cloud.prototype.update = function(speed) {
2066 if (!this.remove) {
2067 this.xPos -= Math.ceil(speed);
2068 this.draw();
2069
2070 // Mark as removeable if no longer in the canvas.
2071 if (!this.isVisible()) {
2072 this.remove = true;
2073 }
2074 }
2075 };
2076
2077
2078 /**
2079 * Check if the cloud is in the stage.
2080 */
2081 Runner.Cloud.prototype.isVisible = function() {
2082 return (this.xPos + Runner.Cloud.config.WIDTH) > 0;
2083 };
2084
2085
2086 //******************************************************************************
2087
2088 /**
2089 * Horizon Line.
2090 * Consists of two connecting lines. Randomly assigns a flat / bumpy horizon.
2091 * @param {HTMLCanvasElement} canvas Canvas element.
2092 * @param {HTMLImage} bgImg Horizon line sprite.
2093 * @constructor
2094 */
2095 Runner.HorizonLine = function(canvas, bgImg) {
arv (Not doing code reviews) 2014/09/16 14:20:21 Why not just HorizontalLine? The IIFE creates the
edwardjung 2014/09/17 12:12:34 I guess this is from the habit of writing Closure
2096 this.image = bgImg;
2097 this.canvas = canvas;
2098 this.canvasCtx = canvas.getContext('2d');
2099 this.sourceDimensions = {};
2100 this.dimensions = Runner.HorizonLine.dimensions;
2101 this.sourceXPos = [0, this.dimensions.WIDTH];
2102 this.xPos = [];
2103 this.yPos = 0;
2104 this.bumpThreshold = 0.5;
2105
2106 this.setSourceDimensions();
2107 this.draw();
2108 };
2109
2110
2111 /**
2112 * Horizon line dimensions.
2113 * @enum {string}
2114 */
2115 Runner.HorizonLine.dimensions = {
2116 WIDTH: 600,
2117 HEIGHT: 12,
2118 YPOS: 127
2119 };
2120
2121
2122 /**
2123 * Set the source dimensions of the horizon line.
2124 */
2125 Runner.HorizonLine.prototype.setSourceDimensions = function() {
2126
2127 for (var dimension in Runner.HorizonLine.dimensions) {
2128 if (Runner.isHDPI) {
2129 if (dimension != 'YPOS') {
2130 this.sourceDimensions[dimension] =
2131 Runner.HorizonLine.dimensions[dimension] * 2;
2132 }
2133 } else {
2134 this.sourceDimensions[dimension] =
2135 Runner.HorizonLine.dimensions[dimension];
2136 }
2137 this.dimensions[dimension] = Runner.HorizonLine.dimensions[dimension];
2138 }
2139
2140 this.xPos = [0, Runner.HorizonLine.dimensions.WIDTH];
2141 this.yPos = Runner.HorizonLine.dimensions.YPOS;
2142 };
2143
2144
2145 /**
2146 * Return the crop x position of a type.
2147 */
2148 Runner.HorizonLine.prototype.getRandomType = function() {
2149 return (Math.random() > this.bumpThreshold) ? this.dimensions.WIDTH : 0;
2150 };
2151
2152
2153 /**
2154 * Draw the horizon line.
2155 */
2156 Runner.HorizonLine.prototype.draw = function() {
2157
2158 this.canvasCtx.drawImage(this.image, this.sourceXPos[0], 0,
2159 this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
2160 this.xPos[0], this.yPos,
2161 this.dimensions.WIDTH, this.dimensions.HEIGHT);
2162
2163 this.canvasCtx.drawImage(this.image, this.sourceXPos[1], 0,
2164 this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
2165 this.xPos[1], this.yPos,
2166 this.dimensions.WIDTH, this.dimensions.HEIGHT);
2167 };
2168
2169
2170 /**
2171 * Update the x position of an indivdual piece of the line.
2172 * @param {number} pos Line position.
2173 * @param {number} increment Amount to increment by.
2174 */
2175 Runner.HorizonLine.prototype.updateXPos = function(pos, increment) {
2176 var line1 = pos;
2177 var line2 = pos == 0 ? 1 : 0;
2178
2179 this.xPos[line1] -= increment;
2180 this.xPos[line2] = this.xPos[line1] + this.dimensions.WIDTH;
2181
2182 if (this.xPos[line1] <= -this.dimensions.WIDTH) {
2183 this.xPos[line1] += this.dimensions.WIDTH * 2;
2184 this.xPos[line2] = this.xPos[line1] - this.dimensions.WIDTH;
2185 this.sourceXPos[line1] = this.getRandomType();
2186 }
2187 };
2188
2189
2190 /**
2191 * Update the horizon line.
2192 * @param {number} deltaTime Time since last update.
2193 * @param {number} speed Speed.
2194 */
2195 Runner.HorizonLine.prototype.update = function(deltaTime, speed) {
2196 var increment = Math.floor((speed * (Runner.FPS / 1000)) * deltaTime);
2197
2198 if (this.xPos[0] <= 0) {
2199 this.updateXPos(0, increment);
2200 } else {
2201 this.updateXPos(1, increment);
2202 }
2203 this.draw();
2204 };
2205
2206
2207 /**
2208 * Reset horizon to the starting position.
2209 */
2210 Runner.HorizonLine.prototype.reset = function() {
2211 this.xPos[0] = 0;
2212 this.xPos[1] = Runner.HorizonLine.dimensions.WIDTH;
2213 };
2214
2215 //******************************************************************************
2216 /**
2217 * Horizon background class.
2218 * @param {HTMLCanvasElement} canvas
2219 * @param {Array.<HTMLImageElement>} images Array of images.
2220 * @param {object} dimensions Canvas dimensions.
2221 * @param {number} gapCoefficient Gap coefficient.
2222 * @constructor
2223 */
2224 Runner.Horizon = function(canvas, images, dimensions, gapCoefficient) {
2225
2226 this.canvas = canvas;
2227
2228 this.canvasCtx = this.canvas.getContext('2d');
2229
2230 this.config = Runner.Horizon.config;
2231
2232 this.dimensions = dimensions;
2233
2234 this.gapCoefficient = gapCoefficient;
2235
2236 // Cloud
2237 this.clouds = [];
2238 this.cloudImg = images['CLOUD'];
2239 this.cloudSpeed = this.config.BG_CLOUD_SPEED;
2240
2241 // Horizon
2242 this.horizonImg = images['HORIZON'];
2243 this.horizonLine = null;
2244
2245 // Obstacles
2246 this.obstacleImgs = {
2247 'CACTUS_SMALL': images['CACTUS_SMALL'],
arv (Not doing code reviews) 2014/09/16 14:20:21 Wrong indentation. this.obstacleImgs = { 'CACTU
edwardjung 2014/09/17 12:12:35 Fixed.
2248 'CACTUS_LARGE': images['CACTUS_LARGE']
2249 };
2250
2251 this.obstacles = [];
2252
2253 this.horizonOffsets = [0, 0];
2254
2255 this.cloudFrequency = this.config.CLOUD_FREQUENCY;
2256
2257 this.init();
2258 };
2259
2260
2261 /**
2262 * Horizon config.
2263 * @enum {string}
2264 */
2265 Runner.Horizon.config = {
2266 BG_CLOUD_SPEED: 0.2,
2267 BUMPY_THRESHOLD: .3,
2268 CLOUD_FREQUENCY: .5,
2269 HORIZON_HEIGHT: 16,
2270 MAX_CLOUDS: 6
2271 };
2272
2273
2274 /**
2275 * Initialise the horizon. Just add the line and a cloud. No obstacles.
2276 */
2277 Runner.Horizon.prototype.init = function() {
2278 this.addCloud();
2279
2280 this.horizonLine = new Runner.HorizonLine(this.canvas, this.horizonImg);
2281 };
2282
2283
2284 /**
2285 *
2286 * @param {number} deltaTime Time elapsed.
2287 * @param {number} currentSpeed Speed.
2288 * @param {boolean} updateObstacles Used as an override to prevent the obstacles
2289 * from being updated / added. This happens in the ease in section.
2290 */
2291 Runner.Horizon.prototype.update =
2292 function(deltaTime, currentSpeed, updateObstacles) {
2293
2294 this.runningTime += deltaTime;
2295
2296 this.horizonLine.update(deltaTime, currentSpeed);
2297 this.updateClouds(deltaTime, currentSpeed);
2298
2299 if (updateObstacles) {
2300 this.updateObstacles(deltaTime, currentSpeed);
2301 }
2302 };
2303
2304 /**
2305 * Update the cloud positions.
2306 * @param {number} deltaTime Time elapsed.
2307 * @param {number} currentSpeed Speed.
2308 */
2309 Runner.Horizon.prototype.updateClouds = function(deltaTime, speed) {
2310 var cloudSpeed = ((this.cloudSpeed / 1000) * deltaTime) * speed;
arv (Not doing code reviews) 2014/09/16 14:20:20 Too many parens var cloudSpeed = this.cloudSpeed
edwardjung 2014/09/17 12:12:34 Done.
2311 var numClouds = this.clouds.length;
2312
2313 if (numClouds) {
2314 for (var i = numClouds - 1; i >= 0; i--) {
2315 this.clouds[i].update(cloudSpeed);
2316 }
2317
2318 var lastCloud = this.clouds[numClouds - 1];
2319
2320 // Check for adding a new cloud.
2321 if (numClouds < this.config.MAX_CLOUDS &&
2322 (this.dimensions.WIDTH - lastCloud.xPos) > lastCloud.cloudGap &&
2323 this.cloudFrequency > Math.random()) {
2324 this.addCloud();
2325 }
2326
2327 // Remove expired clouds.
2328 this.clouds = this.clouds.filter(function(obj) {
2329 return !obj.remove;
2330 });
2331 }
2332 };
2333
2334
2335 /**
2336 * Update the obstacle positions.
2337 * @param {number} deltaTime Time elapsed.
2338 * @param {number} currentSpeed Speed.
2339 */
2340 Runner.Horizon.prototype.updateObstacles = function(deltaTime, currentSpeed) {
2341 // Obstacles, move to Horizon layer.
2342 var updatedObstacles = this.obstacles.slice(0);
2343
2344 for (var i = 0, j = this.obstacles.length; i < j; i++) {
2345 var obstacle = this.obstacles[i];
2346 obstacle.update(deltaTime, currentSpeed);
2347
2348 // Clean up existing obstacles.
2349 if (obstacle.remove) {
2350 updatedObstacles.shift();
2351 }
2352 }
2353 this.obstacles = updatedObstacles;
2354
2355 if (this.obstacles.length > 0) {
2356 var lastObstacle = this.obstacles[this.obstacles.length - 1];
2357
2358 if (lastObstacle && !lastObstacle.followingObstacleCreated &&
2359 lastObstacle.isVisible() &&
2360 (lastObstacle.xPos + (lastObstacle.width + lastObstacle.gap)) <
2361 this.dimensions.WIDTH) {
2362 this.addNewObstacle(currentSpeed);
2363 lastObstacle.followingObstacleCreated = true;
2364 }
2365 } else {
2366 // Create new obstacles.
2367 this.addNewObstacle(currentSpeed);
2368 }
2369 };
2370
2371
2372 /**
2373 * Add a new obstacle.
2374 * @param {number} currentSpeed Speed.
2375 */
2376 Runner.Horizon.prototype.addNewObstacle = function(currentSpeed) {
2377 var obstacleTypeIndex =
2378 Runner.getRandomNum(0, Runner.Obstacle.types.length - 1);
2379 var obstacleType = Runner.Obstacle.types[obstacleTypeIndex];
2380 var obstacleImg = this.obstacleImgs[obstacleType.type];
2381
2382 this.obstacles.push(new Runner.Obstacle(this.canvasCtx, obstacleType,
2383 obstacleImg, this.dimensions, this.gapCoefficient, currentSpeed));
2384 };
2385
2386
2387 /**
2388 * Reset the horizon layer.
2389 * Remove existing obstacles and reposition the horizon line.
2390 */
2391 Runner.Horizon.prototype.reset = function() {
2392 this.obstacles = [];
2393 this.horizonLine.reset();
2394 };
2395
2396
2397 /**
2398 * Update the canvas width and scaling.
2399 * @param {number} width Canvas width.
2400 * @param {number} height Canvas height.
2401 */
2402 Runner.Horizon.prototype.resize = function(width, height) {
2403 this.canvas.width = width;
2404 this.canvas.height = height;
2405 };
2406
2407
2408 /**
2409 * Add a new cloud to the horizon.
2410 */
2411 Runner.Horizon.prototype.addCloud = function() {
2412 this.clouds.push(new Runner.Cloud(this.canvas, this.cloudImg,
2413 this.dimensions.WIDTH));
2414 };
2415
2416 })();
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698