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.
8 * @param {string} outerContainerId Outer containing element id.
9 * @param {Object} opt_config
13 function Runner(outerContainerId, opt_config) {
15 if (Runner.instance_) {
16 return Runner.instance_;
18 Runner.instance_ = this;
20 this.outerContainerEl = document.querySelector(outerContainerId);
21 this.containerEl = null;
22 this.snackbarEl = null;
23 this.detailsButton = this.outerContainerEl.querySelector('#details-button');
25 this.config = opt_config || Runner.config;
27 this.dimensions = Runner.defaultDimensions;
30 this.canvasCtx = null;
34 this.distanceMeter = null;
37 this.highestScore = 0;
41 this.msPerFrame = 1000 / FPS;
42 this.currentSpeed = this.config.SPEED;
47 this.activated = false;
51 this.resizeTimerId_ = null;
56 this.audioBuffer = null;
59 // Global web audio context for playing sounds.
60 this.audioContext = null;
64 this.imagesLoaded = 0;
66 if (this.isDisabled()) {
67 this.setupDisabledRunner();
72 window['Runner'] = Runner;
79 var DEFAULT_WIDTH = 600;
88 var IS_HIDPI = window.devicePixelRatio > 1;
91 var IS_IOS = window.navigator.userAgent.indexOf('CriOS') > -1 ||
92 window.navigator.userAgent == 'UIWebViewForStaticFileContent';
95 var IS_MOBILE = window.navigator.userAgent.indexOf('Mobi') > -1 || IS_IOS;
98 var IS_TOUCH_ENABLED = 'ontouchstart' in window;
101 * Default game configuration.
109 CLOUD_FREQUENCY: 0.5,
110 GAMEOVER_CLEAR_TIME: 750,
111 GAP_COEFFICIENT: 0.6,
113 INITIAL_JUMP_VELOCITY: 12,
115 MAX_OBSTACLE_LENGTH: 3,
116 MAX_OBSTACLE_DUPLICATION: 2,
119 MOBILE_SPEED_COEFFICIENT: 1.2,
120 RESOURCE_TEMPLATE_ID: 'audio-resources',
122 SPEED_DROP_COEFFICIENT: 3
127 * Default dimensions.
130 Runner.defaultDimensions = {
131 WIDTH: DEFAULT_WIDTH,
141 CANVAS: 'runner-canvas',
142 CONTAINER: 'runner-container',
144 ICON: 'icon-offline',
145 SNACKBAR: 'snackbar',
146 SNACKBAR_SHOW: 'snackbar-show',
147 TOUCH_CONTROLLER: 'controller'
152 * Sprite definition layout of the spritesheet.
155 Runner.spriteDefinition = {
157 CACTUS_LARGE: {x: 332, y: 2},
158 CACTUS_SMALL: {x: 228, y: 2},
159 CLOUD: {x: 86, y: 2},
160 HORIZON: {x: 2, y: 54},
161 PTERODACTYL: {x: 134, y: 2},
162 RESTART: {x: 2, y: 2},
163 TEXT_SPRITE: {x: 484, y: 2},
167 CACTUS_LARGE: {x: 652,y: 2},
168 CACTUS_SMALL: {x: 446,y: 2},
169 CLOUD: {x: 166,y: 2},
170 HORIZON: {x: 2,y: 104},
171 PTERODACTYL: {x: 260,y: 2},
172 RESTART: {x: 2,y: 2},
173 TEXT_SPRITE: {x: 954,y: 2},
180 * Sound FX. Reference to the ID of the audio tag on interstitial page.
184 BUTTON_PRESS: 'offline-sound-press',
185 HIT: 'offline-sound-hit',
186 SCORE: 'offline-sound-reached'
195 JUMP: {'38': 1, '32': 1}, // Up, spacebar
196 DUCK: {'40': 1}, // Down
197 RESTART: {'13': 1} // Enter
202 * Runner event names.
206 ANIM_END: 'webkitAnimationEnd',
210 MOUSEDOWN: 'mousedown',
213 TOUCHEND: 'touchend',
214 TOUCHSTART: 'touchstart',
215 VISIBILITY: 'visibilitychange',
224 * Whether the easter egg has been disabled. CrOS enterprise enrolled devices.
227 isDisabled: function() {
228 return loadTimeData && loadTimeData.valueExists('disabledEasterEgg');
232 * For disabled instances, set up a snackbar with the disabled message.
234 setupDisabledRunner: function() {
235 this.containerEl = document.createElement('div');
236 this.containerEl.className = Runner.classes.SNACKBAR;
237 this.containerEl.textContent = loadTimeData.getValue('disabledEasterEgg');
238 this.outerContainerEl.appendChild(this.containerEl);
240 // Show notification when the activation key is pressed.
241 document.addEventListener(Runner.events.KEYDOWN, function(e) {
242 if (Runner.keycodes.JUMP[e.keyCode]) {
243 this.containerEl.classList.add(Runner.classes.SNACKBAR_SHOW);
244 document.querySelector('.icon').classList.add('icon-disabled');
250 * Setting individual settings for debugging.
251 * @param {string} setting
254 updateConfigSetting: function(setting, value) {
255 if (setting in this.config && value != undefined) {
256 this.config[setting] = value;
260 case 'MIN_JUMP_HEIGHT':
261 case 'SPEED_DROP_COEFFICIENT':
262 this.tRex.config[setting] = value;
264 case 'INITIAL_JUMP_VELOCITY':
265 this.tRex.setJumpVelocity(value);
268 this.setSpeed(value);
275 * Cache the appropriate image sprite from the page and get the sprite sheet
278 loadImages: function() {
280 Runner.imageSprite = document.getElementById('offline-resources-2x');
281 this.spriteDef = Runner.spriteDefinition.HDPI;
283 Runner.imageSprite = document.getElementById('offline-resources-1x');
284 this.spriteDef = Runner.spriteDefinition.LDPI;
291 * Load and decode base 64 encoded sounds.
293 loadSounds: function() {
295 this.audioContext = new AudioContext();
297 var resourceTemplate =
298 document.getElementById(this.config.RESOURCE_TEMPLATE_ID).content;
300 for (var sound in Runner.sounds) {
302 resourceTemplate.getElementById(Runner.sounds[sound]).src;
303 soundSrc = soundSrc.substr(soundSrc.indexOf(',') + 1);
304 var buffer = decodeBase64ToArrayBuffer(soundSrc);
306 // Async, so no guarantee of order in array.
307 this.audioContext.decodeAudioData(buffer, function(index, audioData) {
308 this.soundFx[index] = audioData;
309 }.bind(this, sound));
315 * Sets the game speed. Adjust the speed accordingly if on a smaller screen.
316 * @param {number} opt_speed
318 setSpeed: function(opt_speed) {
319 var speed = opt_speed || this.currentSpeed;
321 // Reduce the speed on smaller mobile screens.
322 if (this.dimensions.WIDTH < DEFAULT_WIDTH) {
323 var mobileSpeed = speed * this.dimensions.WIDTH / DEFAULT_WIDTH *
324 this.config.MOBILE_SPEED_COEFFICIENT;
325 this.currentSpeed = mobileSpeed > speed ? speed : mobileSpeed;
326 } else if (opt_speed) {
327 this.currentSpeed = opt_speed;
335 // Hide the static icon.
336 document.querySelector('.' + Runner.classes.ICON).style.visibility =
339 this.adjustDimensions();
342 this.containerEl = document.createElement('div');
343 this.containerEl.className = Runner.classes.CONTAINER;
345 // Player canvas container.
346 this.canvas = createCanvas(this.containerEl, this.dimensions.WIDTH,
347 this.dimensions.HEIGHT, Runner.classes.PLAYER);
349 this.canvasCtx = this.canvas.getContext('2d');
350 this.canvasCtx.fillStyle = '#f7f7f7';
351 this.canvasCtx.fill();
352 Runner.updateCanvasScaling(this.canvas);
354 // Horizon contains clouds, obstacles and the ground.
355 this.horizon = new Horizon(this.canvas, this.spriteDef, this.dimensions,
356 this.config.GAP_COEFFICIENT);
359 this.distanceMeter = new DistanceMeter(this.canvas,
360 this.spriteDef.TEXT_SPRITE, this.dimensions.WIDTH);
363 this.tRex = new Trex(this.canvas, this.spriteDef.TREX);
365 this.outerContainerEl.appendChild(this.containerEl);
368 this.createTouchController();
371 this.startListening();
374 window.addEventListener(Runner.events.RESIZE,
375 this.debounceResize.bind(this));
379 * Create the touch controller. A div that covers whole screen.
381 createTouchController: function() {
382 this.touchController = document.createElement('div');
383 this.touchController.className = Runner.classes.TOUCH_CONTROLLER;
387 * Debounce the resize event.
389 debounceResize: function() {
390 if (!this.resizeTimerId_) {
391 this.resizeTimerId_ =
392 setInterval(this.adjustDimensions.bind(this), 250);
397 * Adjust game space dimensions on resize.
399 adjustDimensions: function() {
400 clearInterval(this.resizeTimerId_);
401 this.resizeTimerId_ = null;
403 var boxStyles = window.getComputedStyle(this.outerContainerEl);
404 var padding = Number(boxStyles.paddingLeft.substr(0,
405 boxStyles.paddingLeft.length - 2));
407 this.dimensions.WIDTH = this.outerContainerEl.offsetWidth - padding * 2;
409 // Redraw the elements back onto the canvas.
411 this.canvas.width = this.dimensions.WIDTH;
412 this.canvas.height = this.dimensions.HEIGHT;
414 Runner.updateCanvasScaling(this.canvas);
416 this.distanceMeter.calcXPos(this.dimensions.WIDTH);
418 this.horizon.update(0, 0, true);
421 // Outer container and distance meter.
422 if (this.activated || this.crashed || this.paused) {
423 this.containerEl.style.width = this.dimensions.WIDTH + 'px';
424 this.containerEl.style.height = this.dimensions.HEIGHT + 'px';
425 this.distanceMeter.update(0, Math.ceil(this.distanceRan));
428 this.tRex.draw(0, 0);
432 if (this.crashed && this.gameOverPanel) {
433 this.gameOverPanel.updateDimensions(this.dimensions.WIDTH);
434 this.gameOverPanel.draw();
440 * Play the game intro.
441 * Canvas container width expands out to the full width.
443 playIntro: function() {
444 if (!this.started && !this.crashed) {
445 this.playingIntro = true;
446 this.tRex.playingIntro = true;
448 // CSS animation definition.
449 var keyframes = '@-webkit-keyframes intro { ' +
450 'from { width:' + Trex.config.WIDTH + 'px }' +
451 'to { width: ' + this.dimensions.WIDTH + 'px }' +
453 document.styleSheets[0].insertRule(keyframes, 0);
455 this.containerEl.addEventListener(Runner.events.ANIM_END,
456 this.startGame.bind(this));
458 this.containerEl.style.webkitAnimation = 'intro .4s ease-out 1 both';
459 this.containerEl.style.width = this.dimensions.WIDTH + 'px';
461 if (this.touchController) {
462 this.outerContainerEl.appendChild(this.touchController);
464 this.activated = true;
466 } else if (this.crashed) {
473 * Update the game status to started.
475 startGame: function() {
476 this.runningTime = 0;
477 this.playingIntro = false;
478 this.tRex.playingIntro = false;
479 this.containerEl.style.webkitAnimation = '';
482 // Handle tabbing off the page. Pause the current game.
483 document.addEventListener(Runner.events.VISIBILITY,
484 this.onVisibilityChange.bind(this));
486 window.addEventListener(Runner.events.BLUR,
487 this.onVisibilityChange.bind(this));
489 window.addEventListener(Runner.events.FOCUS,
490 this.onVisibilityChange.bind(this));
493 clearCanvas: function() {
494 this.canvasCtx.clearRect(0, 0, this.dimensions.WIDTH,
495 this.dimensions.HEIGHT);
499 * Update the game frame.
502 this.drawPending = false;
504 var now = getTimeStamp();
505 var deltaTime = now - (this.time || now);
508 if (this.activated) {
511 if (this.tRex.jumping) {
512 this.tRex.updateJump(deltaTime);
515 this.runningTime += deltaTime;
516 var hasObstacles = this.runningTime > this.config.CLEAR_TIME;
518 // First jump triggers the intro.
519 if (this.tRex.jumpCount == 1 && !this.playingIntro) {
523 // The horizon doesn't move until the intro is over.
524 if (this.playingIntro) {
525 this.horizon.update(0, this.currentSpeed, hasObstacles);
527 deltaTime = !this.started ? 0 : deltaTime;
528 this.horizon.update(deltaTime, this.currentSpeed, hasObstacles);
531 // Check for collisions.
532 var collision = hasObstacles &&
533 checkForCollision(this.horizon.obstacles[0], this.tRex);
536 this.distanceRan += this.currentSpeed * deltaTime / this.msPerFrame;
538 if (this.currentSpeed < this.config.MAX_SPEED) {
539 this.currentSpeed += this.config.ACCELERATION;
545 var playAcheivementSound = this.distanceMeter.update(deltaTime,
546 Math.ceil(this.distanceRan));
548 if (playAcheivementSound) {
549 this.playSound(this.soundFx.SCORE);
554 this.tRex.update(deltaTime);
562 handleEvent: function(e) {
563 return (function(evtType, events) {
566 case events.TOUCHSTART:
567 case events.MOUSEDOWN:
571 case events.TOUCHEND:
576 }.bind(this))(e.type, Runner.events);
580 * Bind relevant key / mouse / touch listeners.
582 startListening: function() {
584 document.addEventListener(Runner.events.KEYDOWN, this);
585 document.addEventListener(Runner.events.KEYUP, this);
588 // Mobile only touch devices.
589 this.touchController.addEventListener(Runner.events.TOUCHSTART, this);
590 this.touchController.addEventListener(Runner.events.TOUCHEND, this);
591 this.containerEl.addEventListener(Runner.events.TOUCHSTART, this);
594 document.addEventListener(Runner.events.MOUSEDOWN, this);
595 document.addEventListener(Runner.events.MOUSEUP, this);
600 * Remove all listeners.
602 stopListening: function() {
603 document.removeEventListener(Runner.events.KEYDOWN, this);
604 document.removeEventListener(Runner.events.KEYUP, this);
607 this.touchController.removeEventListener(Runner.events.TOUCHSTART, this);
608 this.touchController.removeEventListener(Runner.events.TOUCHEND, this);
609 this.containerEl.removeEventListener(Runner.events.TOUCHSTART, this);
611 document.removeEventListener(Runner.events.MOUSEDOWN, this);
612 document.removeEventListener(Runner.events.MOUSEUP, this);
620 onKeyDown: function(e) {
621 // Prevent native page scrolling whilst tapping on mobile.
626 if (e.target != this.detailsButton) {
627 if (!this.crashed && (Runner.keycodes.JUMP[e.keyCode] ||
628 e.type == Runner.events.TOUCHSTART)) {
629 if (!this.activated) {
631 this.activated = true;
632 errorPageController.trackEasterEgg();
635 if (!this.tRex.jumping && !this.tRex.ducking) {
636 this.playSound(this.soundFx.BUTTON_PRESS);
637 this.tRex.startJump(this.currentSpeed);
641 if (this.crashed && e.type == Runner.events.TOUCHSTART &&
642 e.currentTarget == this.containerEl) {
647 if (this.activated && !this.crashed && Runner.keycodes.DUCK[e.keyCode]) {
649 if (this.tRex.jumping) {
650 // Speed drop, activated only when jump key is not pressed.
651 this.tRex.setSpeedDrop();
652 } else if (!this.tRex.jumping && !this.tRex.ducking) {
654 this.tRex.setDuck(true);
664 onKeyUp: function(e) {
665 var keyCode = String(e.keyCode);
666 var isjumpKey = Runner.keycodes.JUMP[keyCode] ||
667 e.type == Runner.events.TOUCHEND ||
668 e.type == Runner.events.MOUSEDOWN;
670 if (this.isRunning() && isjumpKey) {
672 } else if (Runner.keycodes.DUCK[keyCode]) {
673 this.tRex.speedDrop = false;
674 this.tRex.setDuck(false);
675 } else if (this.crashed) {
676 // Check that enough time has elapsed before allowing jump key to restart.
677 var deltaTime = getTimeStamp() - this.time;
679 if (Runner.keycodes.RESTART[keyCode] || this.isLeftClickOnCanvas(e) ||
680 (deltaTime >= this.config.GAMEOVER_CLEAR_TIME &&
681 Runner.keycodes.JUMP[keyCode])) {
684 } else if (this.paused && isjumpKey) {
685 // Reset the jump state
692 * Returns whether the event was a left click on canvas.
693 * On Windows right click is registered as a click.
697 isLeftClickOnCanvas: function(e) {
698 return e.button != null && e.button < 2 &&
699 e.type == Runner.events.MOUSEUP && e.target == this.canvas;
703 * RequestAnimationFrame wrapper.
706 if (!this.drawPending) {
707 this.drawPending = true;
708 this.raqId = requestAnimationFrame(this.update.bind(this));
713 * Whether the game is running.
716 isRunning: function() {
723 gameOver: function() {
724 this.playSound(this.soundFx.HIT);
729 this.distanceMeter.acheivement = false;
731 this.tRex.update(100, Trex.status.CRASHED);
734 if (!this.gameOverPanel) {
735 this.gameOverPanel = new GameOverPanel(this.canvas,
736 this.spriteDef.TEXT_SPRITE, this.spriteDef.RESTART,
739 this.gameOverPanel.draw();
742 // Update the high score.
743 if (this.distanceRan > this.highestScore) {
744 this.highestScore = Math.ceil(this.distanceRan);
745 this.distanceMeter.setHighScore(this.highestScore);
748 // Reset the time clock.
749 this.time = getTimeStamp();
753 this.activated = false;
755 cancelAnimationFrame(this.raqId);
761 this.activated = true;
763 this.tRex.update(0, Trex.status.RUNNING);
764 this.time = getTimeStamp();
769 restart: function() {
772 this.runningTime = 0;
773 this.activated = true;
774 this.crashed = false;
775 this.distanceRan = 0;
776 this.setSpeed(this.config.SPEED);
778 this.time = getTimeStamp();
779 this.containerEl.classList.remove(Runner.classes.CRASHED);
781 this.distanceMeter.reset(this.highestScore);
782 this.horizon.reset();
784 this.playSound(this.soundFx.BUTTON_PRESS);
791 * Pause the game if the tab is not in focus.
793 onVisibilityChange: function(e) {
794 if (document.hidden || document.webkitHidden || e.type == 'blur') {
796 } else if (!this.crashed) {
804 * @param {SoundBuffer} soundBuffer
806 playSound: function(soundBuffer) {
808 var sourceNode = this.audioContext.createBufferSource();
809 sourceNode.buffer = soundBuffer;
810 sourceNode.connect(this.audioContext.destination);
818 * Updates the canvas size taking into
819 * account the backing store pixel ratio and
820 * the device pixel ratio.
822 * See article by Paul Lewis:
823 * http://www.html5rocks.com/en/tutorials/canvas/hidpi/
825 * @param {HTMLCanvasElement} canvas
826 * @param {number} opt_width
827 * @param {number} opt_height
828 * @return {boolean} Whether the canvas was scaled.
830 Runner.updateCanvasScaling = function(canvas, opt_width, opt_height) {
831 var context = canvas.getContext('2d');
833 // Query the various pixel ratios
834 var devicePixelRatio = Math.floor(window.devicePixelRatio) || 1;
835 var backingStoreRatio = Math.floor(context.webkitBackingStorePixelRatio) || 1;
836 var ratio = devicePixelRatio / backingStoreRatio;
838 // Upscale the canvas if the two ratios don't match
839 if (devicePixelRatio !== backingStoreRatio) {
840 var oldWidth = opt_width || canvas.width;
841 var oldHeight = opt_height || canvas.height;
843 canvas.width = oldWidth * ratio;
844 canvas.height = oldHeight * ratio;
846 canvas.style.width = oldWidth + 'px';
847 canvas.style.height = oldHeight + 'px';
849 // Scale the context to counter the fact that we've manually scaled
850 // our canvas element.
851 context.scale(ratio, ratio);
853 } else if (devicePixelRatio == 1) {
854 // Reset the canvas width / height. Fixes scaling bug when the page is
855 // zoomed and the devicePixelRatio changes accordingly.
856 canvas.style.width = canvas.width + 'px';
857 canvas.style.height = canvas.height + 'px';
865 * @param {number} min
866 * @param {number} max
869 function getRandomNum(min, max) {
870 return Math.floor(Math.random() * (max - min + 1)) + min;
875 * Vibrate on mobile devices.
876 * @param {number} duration Duration of the vibration in milliseconds.
878 function vibrate(duration) {
879 if (IS_MOBILE && window.navigator.vibrate) {
880 window.navigator.vibrate(duration);
886 * Create canvas element.
887 * @param {HTMLElement} container Element to append canvas to.
888 * @param {number} width
889 * @param {number} height
890 * @param {string} opt_classname
891 * @return {HTMLCanvasElement}
893 function createCanvas(container, width, height, opt_classname) {
894 var canvas = document.createElement('canvas');
895 canvas.className = opt_classname ? Runner.classes.CANVAS + ' ' +
896 opt_classname : Runner.classes.CANVAS;
897 canvas.width = width;
898 canvas.height = height;
899 container.appendChild(canvas);
906 * Decodes the base 64 audio to ArrayBuffer used by Web Audio.
907 * @param {string} base64String
909 function decodeBase64ToArrayBuffer(base64String) {
910 var len = (base64String.length / 4) * 3;
911 var str = atob(base64String);
912 var arrayBuffer = new ArrayBuffer(len);
913 var bytes = new Uint8Array(arrayBuffer);
915 for (var i = 0; i < len; i++) {
916 bytes[i] = str.charCodeAt(i);
923 * Return the current timestamp.
926 function getTimeStamp() {
927 return IS_IOS ? new Date().getTime() : performance.now();
931 //******************************************************************************
936 * @param {!HTMLCanvasElement} canvas
937 * @param {Object} textImgPos
938 * @param {Object} restartImgPos
939 * @param {!Object} dimensions Canvas dimensions.
942 function GameOverPanel(canvas, textImgPos, restartImgPos, dimensions) {
943 this.canvas = canvas;
944 this.canvasCtx = canvas.getContext('2d');
945 this.canvasDimensions = dimensions;
946 this.textImgPos = textImgPos;
947 this.restartImgPos = restartImgPos;
953 * Dimensions used in the panel.
956 GameOverPanel.dimensions = {
966 GameOverPanel.prototype = {
968 * Update the panel dimensions.
969 * @param {number} width New canvas width.
970 * @param {number} opt_height Optional new canvas height.
972 updateDimensions: function(width, opt_height) {
973 this.canvasDimensions.WIDTH = width;
975 this.canvasDimensions.HEIGHT = opt_height;
983 var dimensions = GameOverPanel.dimensions;
985 var centerX = this.canvasDimensions.WIDTH / 2;
988 var textSourceX = dimensions.TEXT_X;
989 var textSourceY = dimensions.TEXT_Y;
990 var textSourceWidth = dimensions.TEXT_WIDTH;
991 var textSourceHeight = dimensions.TEXT_HEIGHT;
993 var textTargetX = Math.round(centerX - (dimensions.TEXT_WIDTH / 2));
994 var textTargetY = Math.round((this.canvasDimensions.HEIGHT - 25) / 3);
995 var textTargetWidth = dimensions.TEXT_WIDTH;
996 var textTargetHeight = dimensions.TEXT_HEIGHT;
998 var restartSourceWidth = dimensions.RESTART_WIDTH;
999 var restartSourceHeight = dimensions.RESTART_HEIGHT;
1000 var restartTargetX = centerX - (dimensions.RESTART_WIDTH / 2);
1001 var restartTargetY = this.canvasDimensions.HEIGHT / 2;
1006 textSourceWidth *= 2;
1007 textSourceHeight *= 2;
1008 restartSourceWidth *= 2;
1009 restartSourceHeight *= 2;
1012 textSourceX += this.textImgPos.x;
1013 textSourceY += this.textImgPos.y;
1015 // Game over text from sprite.
1016 this.canvasCtx.drawImage(Runner.imageSprite,
1017 textSourceX, textSourceY, textSourceWidth, textSourceHeight,
1018 textTargetX, textTargetY, textTargetWidth, textTargetHeight);
1021 this.canvasCtx.drawImage(Runner.imageSprite,
1022 this.restartImgPos.x, this.restartImgPos.y,
1023 restartSourceWidth, restartSourceHeight,
1024 restartTargetX, restartTargetY, dimensions.RESTART_WIDTH,
1025 dimensions.RESTART_HEIGHT);
1030 //******************************************************************************
1033 * Check for a collision.
1034 * @param {!Obstacle} obstacle
1035 * @param {!Trex} tRex T-rex object.
1036 * @param {HTMLCanvasContext} opt_canvasCtx Optional canvas context for drawing
1038 * @return {Array<CollisionBox>}
1040 function checkForCollision(obstacle, tRex, opt_canvasCtx) {
1041 var obstacleBoxXPos = Runner.defaultDimensions.WIDTH + obstacle.xPos;
1043 // Adjustments are made to the bounding box as there is a 1 pixel white
1044 // border around the t-rex and obstacles.
1045 var tRexBox = new CollisionBox(
1048 tRex.config.WIDTH - 2,
1049 tRex.config.HEIGHT - 2);
1051 var obstacleBox = new CollisionBox(
1054 obstacle.typeConfig.width * obstacle.size - 2,
1055 obstacle.typeConfig.height - 2);
1058 if (opt_canvasCtx) {
1059 drawCollisionBoxes(opt_canvasCtx, tRexBox, obstacleBox);
1062 // Simple outer bounds check.
1063 if (boxCompare(tRexBox, obstacleBox)) {
1064 var collisionBoxes = obstacle.collisionBoxes;
1065 var tRexCollisionBoxes = tRex.ducking ?
1066 Trex.collisionBoxes.DUCKING : Trex.collisionBoxes.RUNNING;
1068 // Detailed axis aligned box check.
1069 for (var t = 0; t < tRexCollisionBoxes.length; t++) {
1070 for (var i = 0; i < collisionBoxes.length; i++) {
1071 // Adjust the box to actual positions.
1073 createAdjustedCollisionBox(tRexCollisionBoxes[t], tRexBox);
1074 var adjObstacleBox =
1075 createAdjustedCollisionBox(collisionBoxes[i], obstacleBox);
1076 var crashed = boxCompare(adjTrexBox, adjObstacleBox);
1078 // Draw boxes for debug.
1079 if (opt_canvasCtx) {
1080 drawCollisionBoxes(opt_canvasCtx, adjTrexBox, adjObstacleBox);
1084 return [adjTrexBox, adjObstacleBox];
1094 * Adjust the collision box.
1095 * @param {!CollisionBox} box The original box.
1096 * @param {!CollisionBox} adjustment Adjustment box.
1097 * @return {CollisionBox} The adjusted collision box object.
1099 function createAdjustedCollisionBox(box, adjustment) {
1100 return new CollisionBox(
1101 box.x + adjustment.x,
1102 box.y + adjustment.y,
1109 * Draw the collision boxes for debug.
1111 function drawCollisionBoxes(canvasCtx, tRexBox, obstacleBox) {
1113 canvasCtx.strokeStyle = '#f00';
1114 canvasCtx.strokeRect(tRexBox.x, tRexBox.y, tRexBox.width, tRexBox.height);
1116 canvasCtx.strokeStyle = '#0f0';
1117 canvasCtx.strokeRect(obstacleBox.x, obstacleBox.y,
1118 obstacleBox.width, obstacleBox.height);
1119 canvasCtx.restore();
1124 * Compare two collision boxes for a collision.
1125 * @param {CollisionBox} tRexBox
1126 * @param {CollisionBox} obstacleBox
1127 * @return {boolean} Whether the boxes intersected.
1129 function boxCompare(tRexBox, obstacleBox) {
1130 var crashed = false;
1131 var tRexBoxX = tRexBox.x;
1132 var tRexBoxY = tRexBox.y;
1134 var obstacleBoxX = obstacleBox.x;
1135 var obstacleBoxY = obstacleBox.y;
1137 // Axis-Aligned Bounding Box method.
1138 if (tRexBox.x < obstacleBoxX + obstacleBox.width &&
1139 tRexBox.x + tRexBox.width > obstacleBoxX &&
1140 tRexBox.y < obstacleBox.y + obstacleBox.height &&
1141 tRexBox.height + tRexBox.y > obstacleBox.y) {
1149 //******************************************************************************
1152 * Collision box object.
1153 * @param {number} x X position.
1154 * @param {number} y Y Position.
1155 * @param {number} w Width.
1156 * @param {number} h Height.
1158 function CollisionBox(x, y, w, h) {
1166 //******************************************************************************
1170 * @param {HTMLCanvasCtx} canvasCtx
1171 * @param {Obstacle.type} type
1172 * @param {Object} spritePos Obstacle position in sprite.
1173 * @param {Object} dimensions
1174 * @param {number} gapCoefficient Mutipler in determining the gap.
1175 * @param {number} speed
1177 function Obstacle(canvasCtx, type, spriteImgPos, dimensions,
1178 gapCoefficient, speed) {
1180 this.canvasCtx = canvasCtx;
1181 this.spritePos = spriteImgPos;
1182 this.typeConfig = type;
1183 this.gapCoefficient = gapCoefficient;
1184 this.size = getRandomNum(1, Obstacle.MAX_OBSTACLE_LENGTH);
1185 this.dimensions = dimensions;
1186 this.remove = false;
1190 this.collisionBoxes = [];
1192 this.speedOffset = 0;
1194 // For animated obstacles.
1195 this.currentFrame = 0;
1202 * Coefficient for calculating the maximum gap.
1205 Obstacle.MAX_GAP_COEFFICIENT = 1.5;
1208 * Maximum obstacle grouping count.
1211 Obstacle.MAX_OBSTACLE_LENGTH = 3,
1214 Obstacle.prototype = {
1216 * Initialise the DOM for the obstacle.
1217 * @param {number} speed
1219 init: function(speed) {
1220 this.cloneCollisionBoxes();
1222 // Only allow sizing if we're at the right speed.
1223 if (this.size > 1 && this.typeConfig.multipleSpeed > speed) {
1227 this.width = this.typeConfig.width * this.size;
1228 this.xPos = this.dimensions.WIDTH - this.width;
1230 // Check if obstacle can be positioned at various heights.
1231 if (Array.isArray(this.typeConfig.yPos)) {
1232 var yPosConfig = IS_MOBILE ? this.typeConfig.yPosMobile :
1233 this.typeConfig.yPos;
1234 this.yPos = yPosConfig[getRandomNum(0, yPosConfig.length - 1)];
1236 this.yPos = this.typeConfig.yPos;
1241 // Make collision box adjustments,
1242 // Central box is adjusted to the size as one box.
1243 // ____ ______ ________
1244 // _| |-| _| |-| _| |-|
1245 // | |<->| | | |<--->| | | |<----->| |
1246 // | | 1 | | | | 2 | | | | 3 | |
1247 // |_|___|_| |_|_____|_| |_|_______|_|
1249 if (this.size > 1) {
1250 this.collisionBoxes[1].width = this.width - this.collisionBoxes[0].width -
1251 this.collisionBoxes[2].width;
1252 this.collisionBoxes[2].x = this.width - this.collisionBoxes[2].width;
1255 // For obstacles that go at a different speed from the horizon.
1256 if (this.typeConfig.speedOffset) {
1257 this.speedOffset = Math.random() > 0.5 ? this.typeConfig.speedOffset :
1258 -this.typeConfig.speedOffset;
1261 this.gap = this.getGap(this.gapCoefficient, speed);
1265 * Draw and crop based on size.
1268 var sourceWidth = this.typeConfig.width;
1269 var sourceHeight = this.typeConfig.height;
1272 sourceWidth = sourceWidth * 2;
1273 sourceHeight = sourceHeight * 2;
1276 // X position in sprite.
1277 var sourceX = (sourceWidth * this.size) * (0.5 * (this.size - 1)) +
1280 // Animation frames.
1281 if (this.currentFrame > 0) {
1282 sourceX += sourceWidth * this.currentFrame;
1285 this.canvasCtx.drawImage(Runner.imageSprite,
1286 sourceX, this.spritePos.y,
1287 sourceWidth * this.size, sourceHeight,
1288 this.xPos, this.yPos,
1289 this.typeConfig.width * this.size, this.typeConfig.height);
1293 * Obstacle frame update.
1294 * @param {number} deltaTime
1295 * @param {number} speed
1297 update: function(deltaTime, speed) {
1299 if (this.typeConfig.speedOffset) {
1300 speed += this.speedOffset;
1302 this.xPos -= Math.floor((speed * FPS / 1000) * deltaTime);
1305 if (this.typeConfig.numFrames) {
1306 this.timer += deltaTime;
1307 if (this.timer >= this.typeConfig.frameRate) {
1309 this.currentFrame == this.typeConfig.numFrames - 1 ?
1310 0 : this.currentFrame + 1;
1316 if (!this.isVisible()) {
1323 * Calculate a random gap size.
1324 * - Minimum gap gets wider as speed increses
1325 * @param {number} gapCoefficient
1326 * @param {number} speed
1327 * @return {number} The gap size.
1329 getGap: function(gapCoefficient, speed) {
1330 var minGap = Math.round(this.width * speed +
1331 this.typeConfig.minGap * gapCoefficient);
1332 var maxGap = Math.round(minGap * Obstacle.MAX_GAP_COEFFICIENT);
1333 return getRandomNum(minGap, maxGap);
1337 * Check if obstacle is visible.
1338 * @return {boolean} Whether the obstacle is in the game area.
1340 isVisible: function() {
1341 return this.xPos + this.width > 0;
1345 * Make a copy of the collision boxes, since these will change based on
1346 * obstacle type and size.
1348 cloneCollisionBoxes: function() {
1349 var collisionBoxes = this.typeConfig.collisionBoxes;
1351 for (var i = collisionBoxes.length - 1; i >= 0; i--) {
1352 this.collisionBoxes[i] = new CollisionBox(collisionBoxes[i].x,
1353 collisionBoxes[i].y, collisionBoxes[i].width,
1354 collisionBoxes[i].height);
1361 * Obstacle definitions.
1362 * minGap: minimum pixel space betweeen obstacles.
1363 * multipleSpeed: Speed at which multiples are allowed.
1364 * speedOffset: speed faster / slower than the horizon.
1365 * minSpeed: Minimum speed which the obstacle can make an appearance.
1369 type: 'CACTUS_SMALL',
1377 new CollisionBox(0, 7, 5, 27),
1378 new CollisionBox(4, 0, 6, 34),
1379 new CollisionBox(10, 4, 7, 14)
1383 type: 'CACTUS_LARGE',
1391 new CollisionBox(0, 12, 7, 38),
1392 new CollisionBox(8, 0, 7, 49),
1393 new CollisionBox(13, 10, 10, 38)
1397 type: 'PTERODACTYL',
1400 yPos: [ 100, 75, 50 ], // Variable height.
1401 yPosMobile: [ 100, 50 ], // Variable height mobile.
1406 new CollisionBox(15, 15, 16, 5),
1407 new CollisionBox(18, 21, 24, 6),
1408 new CollisionBox(2, 14, 4, 3),
1409 new CollisionBox(6, 10, 4, 7),
1410 new CollisionBox(10, 8, 6, 9)
1419 //******************************************************************************
1421 * T-rex game character.
1422 * @param {HTMLCanvas} canvas
1423 * @param {Object} spritePos Positioning within image sprite.
1426 function Trex(canvas, spritePos) {
1427 this.canvas = canvas;
1428 this.canvasCtx = canvas.getContext('2d');
1429 this.spritePos = spritePos;
1432 // Position when on the ground.
1433 this.groundYPos = 0;
1434 this.currentFrame = 0;
1435 this.currentAnimFrames = [];
1436 this.blinkDelay = 0;
1437 this.animStartTime = 0;
1439 this.msPerFrame = 1000 / FPS;
1440 this.config = Trex.config;
1442 this.status = Trex.status.WAITING;
1444 this.jumping = false;
1445 this.ducking = false;
1446 this.jumpVelocity = 0;
1447 this.reachedMinHeight = false;
1448 this.speedDrop = false;
1457 * T-rex player config.
1465 INIITAL_JUMP_VELOCITY: -10,
1466 INTRO_DURATION: 1500,
1467 MAX_JUMP_HEIGHT: 30,
1468 MIN_JUMP_HEIGHT: 30,
1469 SPEED_DROP_COEFFICIENT: 3,
1478 * Used in collision detection.
1479 * @type {Array<CollisionBox>}
1481 Trex.collisionBoxes = {
1483 new CollisionBox(1, 18, 55, 25)
1486 new CollisionBox(22, 0, 17, 16),
1487 new CollisionBox(1, 18, 30, 9),
1488 new CollisionBox(10, 35, 14, 8),
1489 new CollisionBox(1, 24, 29, 5),
1490 new CollisionBox(5, 30, 21, 4),
1491 new CollisionBox(9, 34, 15, 4)
1509 * Blinking coefficient.
1512 Trex.BLINK_TIMING = 7000;
1516 * Animation config for different states.
1522 msPerFrame: 1000 / 3
1526 msPerFrame: 1000 / 12
1530 msPerFrame: 1000 / 60
1534 msPerFrame: 1000 / 60
1538 msPerFrame: 1000 / 8
1545 * T-rex player initaliser.
1546 * Sets the t-rex to blink at random intervals.
1549 this.blinkDelay = this.setBlinkDelay();
1550 this.groundYPos = Runner.defaultDimensions.HEIGHT - this.config.HEIGHT -
1551 Runner.config.BOTTOM_PAD;
1552 this.yPos = this.groundYPos;
1553 this.minJumpHeight = this.groundYPos - this.config.MIN_JUMP_HEIGHT;
1556 this.update(0, Trex.status.WAITING);
1560 * Setter for the jump velocity.
1561 * The approriate drop velocity is also set.
1563 setJumpVelocity: function(setting) {
1564 this.config.INIITAL_JUMP_VELOCITY = -setting;
1565 this.config.DROP_VELOCITY = -setting / 2;
1569 * Set the animation status.
1570 * @param {!number} deltaTime
1571 * @param {Trex.status} status Optional status to switch to.
1573 update: function(deltaTime, opt_status) {
1574 this.timer += deltaTime;
1576 // Update the status.
1578 this.status = opt_status;
1579 this.currentFrame = 0;
1580 this.msPerFrame = Trex.animFrames[opt_status].msPerFrame;
1581 this.currentAnimFrames = Trex.animFrames[opt_status].frames;
1583 if (opt_status == Trex.status.WAITING) {
1584 this.animStartTime = getTimeStamp();
1585 this.setBlinkDelay();
1589 // Game intro animation, T-rex moves in from the left.
1590 if (this.playingIntro && this.xPos < this.config.START_X_POS) {
1591 this.xPos += Math.round((this.config.START_X_POS /
1592 this.config.INTRO_DURATION) * deltaTime);
1595 if (this.status == Trex.status.WAITING) {
1596 this.blink(getTimeStamp());
1598 this.draw(this.currentAnimFrames[this.currentFrame], 0);
1601 // Update the frame position.
1602 if (this.timer >= this.msPerFrame) {
1603 this.currentFrame = this.currentFrame ==
1604 this.currentAnimFrames.length - 1 ? 0 : this.currentFrame + 1;
1608 // Speed drop becomes duck if the down key is still being pressed.
1609 if (this.speedDrop && this.yPos == this.groundYPos) {
1610 this.speedDrop = false;
1616 * Draw the t-rex to a particular position.
1620 draw: function(x, y) {
1623 var sourceWidth = this.ducking && this.status != Trex.status.CRASHED ?
1624 this.config.WIDTH_DUCK : this.config.WIDTH;
1625 var sourceHeight = this.config.HEIGHT;
1634 // Adjustments for sprite sheet position.
1635 sourceX += this.spritePos.x;
1636 sourceY += this.spritePos.y;
1639 if (this.ducking && this.status != Trex.status.CRASHED) {
1640 this.canvasCtx.drawImage(Runner.imageSprite, sourceX, sourceY,
1641 sourceWidth, sourceHeight,
1642 this.xPos, this.yPos,
1643 this.config.WIDTH_DUCK, this.config.HEIGHT);
1645 // Crashed whilst ducking. Trex is standing up so needs adjustment.
1646 if (this.ducking && this.status == Trex.status.CRASHED) {
1649 // Standing / running
1650 this.canvasCtx.drawImage(Runner.imageSprite, sourceX, sourceY,
1651 sourceWidth, sourceHeight,
1652 this.xPos, this.yPos,
1653 this.config.WIDTH, this.config.HEIGHT);
1658 * Sets a random time for the blink to happen.
1660 setBlinkDelay: function() {
1661 this.blinkDelay = Math.ceil(Math.random() * Trex.BLINK_TIMING);
1665 * Make t-rex blink at random intervals.
1666 * @param {number} time Current time in milliseconds.
1668 blink: function(time) {
1669 var deltaTime = time - this.animStartTime;
1671 if (deltaTime >= this.blinkDelay) {
1672 this.draw(this.currentAnimFrames[this.currentFrame], 0);
1674 if (this.currentFrame == 1) {
1675 // Set new random delay to blink.
1676 this.setBlinkDelay();
1677 this.animStartTime = time;
1683 * Initialise a jump.
1684 * @param {number} speed
1686 startJump: function(speed) {
1687 if (!this.jumping) {
1688 this.update(0, Trex.status.JUMPING);
1689 // Tweak the jump velocity based on the speed.
1690 this.jumpVelocity = this.config.INIITAL_JUMP_VELOCITY - (speed / 10);
1691 this.jumping = true;
1692 this.reachedMinHeight = false;
1693 this.speedDrop = false;
1698 * Jump is complete, falling down.
1700 endJump: function() {
1701 if (this.reachedMinHeight &&
1702 this.jumpVelocity < this.config.DROP_VELOCITY) {
1703 this.jumpVelocity = this.config.DROP_VELOCITY;
1708 * Update frame for a jump.
1709 * @param {number} deltaTime
1710 * @param {number} speed
1712 updateJump: function(deltaTime, speed) {
1713 var msPerFrame = Trex.animFrames[this.status].msPerFrame;
1714 var framesElapsed = deltaTime / msPerFrame;
1716 // Speed drop makes Trex fall faster.
1717 if (this.speedDrop) {
1718 this.yPos += Math.round(this.jumpVelocity *
1719 this.config.SPEED_DROP_COEFFICIENT * framesElapsed);
1721 this.yPos += Math.round(this.jumpVelocity * framesElapsed);
1724 this.jumpVelocity += this.config.GRAVITY * framesElapsed;
1726 // Minimum height has been reached.
1727 if (this.yPos < this.minJumpHeight || this.speedDrop) {
1728 this.reachedMinHeight = true;
1731 // Reached max height
1732 if (this.yPos < this.config.MAX_JUMP_HEIGHT || this.speedDrop) {
1736 // Back down at ground level. Jump completed.
1737 if (this.yPos > this.groundYPos) {
1742 this.update(deltaTime);
1746 * Set the speed drop. Immediately cancels the current jump.
1748 setSpeedDrop: function() {
1749 this.speedDrop = true;
1750 this.jumpVelocity = 1;
1754 * @param {boolean} isDucking.
1756 setDuck: function(isDucking) {
1757 if (isDucking && this.status != Trex.status.DUCKING) {
1758 this.update(0, Trex.status.DUCKING);
1759 this.ducking = true;
1760 } else if (this.status == Trex.status.DUCKING) {
1761 this.update(0, Trex.status.RUNNING);
1762 this.ducking = false;
1767 * Reset the t-rex to running at start of game.
1770 this.yPos = this.groundYPos;
1771 this.jumpVelocity = 0;
1772 this.jumping = false;
1773 this.ducking = false;
1774 this.update(0, Trex.status.RUNNING);
1775 this.midair = false;
1776 this.speedDrop = false;
1782 //******************************************************************************
1785 * Handles displaying the distance meter.
1786 * @param {!HTMLCanvasElement} canvas
1787 * @param {Object} spritePos Image position in sprite.
1788 * @param {number} canvasWidth
1791 function DistanceMeter(canvas, spritePos, canvasWidth) {
1792 this.canvas = canvas;
1793 this.canvasCtx = canvas.getContext('2d');
1794 this.image = Runner.imageSprite;
1795 this.spritePos = spritePos;
1799 this.currentDistance = 0;
1802 this.container = null;
1805 this.acheivement = false;
1806 this.defaultString = '';
1807 this.flashTimer = 0;
1808 this.flashIterations = 0;
1810 this.config = DistanceMeter.config;
1811 this.maxScoreUnits = this.config.MAX_DISTANCE_UNITS;
1812 this.init(canvasWidth);
1819 DistanceMeter.dimensions = {
1827 * Y positioning of the digits in the sprite sheet.
1828 * X position is always 0.
1829 * @type {Array<number>}
1831 DistanceMeter.yPos = [0, 13, 27, 40, 53, 67, 80, 93, 107, 120];
1835 * Distance meter config.
1838 DistanceMeter.config = {
1839 // Number of digits.
1840 MAX_DISTANCE_UNITS: 5,
1842 // Distance that causes achievement animation.
1843 ACHIEVEMENT_DISTANCE: 100,
1845 // Used for conversion from pixel distance to a scaled unit.
1848 // Flash duration in milliseconds.
1849 FLASH_DURATION: 1000 / 4,
1851 // Flash iterations for achievement animation.
1856 DistanceMeter.prototype = {
1858 * Initialise the distance meter to '00000'.
1859 * @param {number} width Canvas width in px.
1861 init: function(width) {
1862 var maxDistanceStr = '';
1864 this.calcXPos(width);
1865 this.maxScore = this.maxScoreUnits;
1866 for (var i = 0; i < this.maxScoreUnits; i++) {
1868 this.defaultString += '0';
1869 maxDistanceStr += '9';
1872 this.maxScore = parseInt(maxDistanceStr);
1876 * Calculate the xPos in the canvas.
1877 * @param {number} canvasWidth
1879 calcXPos: function(canvasWidth) {
1880 this.x = canvasWidth - (DistanceMeter.dimensions.DEST_WIDTH *
1881 (this.maxScoreUnits + 1));
1885 * Draw a digit to canvas.
1886 * @param {number} digitPos Position of the digit.
1887 * @param {number} value Digit value 0-9.
1888 * @param {boolean} opt_highScore Whether drawing the high score.
1890 draw: function(digitPos, value, opt_highScore) {
1891 var sourceWidth = DistanceMeter.dimensions.WIDTH;
1892 var sourceHeight = DistanceMeter.dimensions.HEIGHT;
1893 var sourceX = DistanceMeter.dimensions.WIDTH * value;
1896 var targetX = digitPos * DistanceMeter.dimensions.DEST_WIDTH;
1897 var targetY = this.y;
1898 var targetWidth = DistanceMeter.dimensions.WIDTH;
1899 var targetHeight = DistanceMeter.dimensions.HEIGHT;
1901 // For high DPI we 2x source values.
1908 sourceX += this.spritePos.x;
1909 sourceY += this.spritePos.y;
1911 this.canvasCtx.save();
1913 if (opt_highScore) {
1914 // Left of the current score.
1915 var highScoreX = this.x - (this.maxScoreUnits * 2) *
1916 DistanceMeter.dimensions.WIDTH;
1917 this.canvasCtx.translate(highScoreX, this.y);
1919 this.canvasCtx.translate(this.x, this.y);
1922 this.canvasCtx.drawImage(this.image, sourceX, sourceY,
1923 sourceWidth, sourceHeight,
1925 targetWidth, targetHeight
1928 this.canvasCtx.restore();
1932 * Covert pixel distance to a 'real' distance.
1933 * @param {number} distance Pixel distance ran.
1934 * @return {number} The 'real' distance ran.
1936 getActualDistance: function(distance) {
1937 return distance ? Math.round(distance * this.config.COEFFICIENT) : 0;
1941 * Update the distance meter.
1942 * @param {number} distance
1943 * @param {number} deltaTime
1944 * @return {boolean} Whether the acheivement sound fx should be played.
1946 update: function(deltaTime, distance) {
1948 var playSound = false;
1950 if (!this.acheivement) {
1951 distance = this.getActualDistance(distance);
1953 // Score has gone beyond the initial digit count.
1954 if (distance > this.maxScore && this.maxScoreUnits ==
1955 this.config.MAX_DISTANCE_UNITS) {
1956 this.maxScoreUnits++;
1957 this.maxScore = parseInt(this.maxScore + '9');
1963 // Acheivement unlocked
1964 if (distance % this.config.ACHIEVEMENT_DISTANCE == 0) {
1965 // Flash score and play sound.
1966 this.acheivement = true;
1967 this.flashTimer = 0;
1971 // Create a string representation of the distance with leading 0.
1972 var distanceStr = (this.defaultString +
1973 distance).substr(-this.maxScoreUnits);
1974 this.digits = distanceStr.split('');
1976 this.digits = this.defaultString.split('');
1979 // Control flashing of the score on reaching acheivement.
1980 if (this.flashIterations <= this.config.FLASH_ITERATIONS) {
1981 this.flashTimer += deltaTime;
1983 if (this.flashTimer < this.config.FLASH_DURATION) {
1985 } else if (this.flashTimer >
1986 this.config.FLASH_DURATION * 2) {
1987 this.flashTimer = 0;
1988 this.flashIterations++;
1991 this.acheivement = false;
1992 this.flashIterations = 0;
1993 this.flashTimer = 0;
1997 // Draw the digits if not flashing.
1999 for (var i = this.digits.length - 1; i >= 0; i--) {
2000 this.draw(i, parseInt(this.digits[i]));
2004 this.drawHighScore();
2010 * Draw the high score.
2012 drawHighScore: function() {
2013 this.canvasCtx.save();
2014 this.canvasCtx.globalAlpha = .8;
2015 for (var i = this.highScore.length - 1; i >= 0; i--) {
2016 this.draw(i, parseInt(this.highScore[i], 10), true);
2018 this.canvasCtx.restore();
2022 * Set the highscore as a array string.
2023 * Position of char in the sprite: H - 10, I - 11.
2024 * @param {number} distance Distance ran in pixels.
2026 setHighScore: function(distance) {
2027 distance = this.getActualDistance(distance);
2028 var highScoreStr = (this.defaultString +
2029 distance).substr(-this.maxScoreUnits);
2031 this.highScore = ['10', '11', ''].concat(highScoreStr.split(''));
2035 * Reset the distance meter back to '00000'.
2039 this.acheivement = false;
2044 //******************************************************************************
2047 * Cloud background item.
2048 * Similar to an obstacle object but without collision boxes.
2049 * @param {HTMLCanvasElement} canvas Canvas element.
2050 * @param {Object} spritePos Position of image in sprite.
2051 * @param {number} containerWidth
2053 function Cloud(canvas, spritePos, containerWidth) {
2054 this.canvas = canvas;
2055 this.canvasCtx = this.canvas.getContext('2d');
2056 this.spritePos = spritePos;
2057 this.containerWidth = containerWidth;
2058 this.xPos = containerWidth;
2060 this.remove = false;
2061 this.cloudGap = getRandomNum(Cloud.config.MIN_CLOUD_GAP,
2062 Cloud.config.MAX_CLOUD_GAP);
2069 * Cloud object config.
2084 * Initialise the cloud. Sets the Cloud height.
2087 this.yPos = getRandomNum(Cloud.config.MAX_SKY_LEVEL,
2088 Cloud.config.MIN_SKY_LEVEL);
2096 this.canvasCtx.save();
2097 var sourceWidth = Cloud.config.WIDTH;
2098 var sourceHeight = Cloud.config.HEIGHT;
2101 sourceWidth = sourceWidth * 2;
2102 sourceHeight = sourceHeight * 2;
2105 this.canvasCtx.drawImage(Runner.imageSprite, this.spritePos.x,
2107 sourceWidth, sourceHeight,
2108 this.xPos, this.yPos,
2109 Cloud.config.WIDTH, Cloud.config.HEIGHT);
2111 this.canvasCtx.restore();
2115 * Update the cloud position.
2116 * @param {number} speed
2118 update: function(speed) {
2120 this.xPos -= Math.ceil(speed);
2123 // Mark as removeable if no longer in the canvas.
2124 if (!this.isVisible()) {
2131 * Check if the cloud is visible on the stage.
2134 isVisible: function() {
2135 return this.xPos + Cloud.config.WIDTH > 0;
2140 //******************************************************************************
2144 * Consists of two connecting lines. Randomly assigns a flat / bumpy horizon.
2145 * @param {HTMLCanvasElement} canvas
2146 * @param {Object} spritePos Horizon position in sprite.
2149 function HorizonLine(canvas, spritePos) {
2150 this.spritePos = spritePos;
2151 this.canvas = canvas;
2152 this.canvasCtx = canvas.getContext('2d');
2153 this.sourceDimensions = {};
2154 this.dimensions = HorizonLine.dimensions;
2155 this.sourceXPos = [this.spritePos.x, this.spritePos.x +
2156 this.dimensions.WIDTH];
2159 this.bumpThreshold = 0.5;
2161 this.setSourceDimensions();
2167 * Horizon line dimensions.
2170 HorizonLine.dimensions = {
2177 HorizonLine.prototype = {
2179 * Set the source dimensions of the horizon line.
2181 setSourceDimensions: function() {
2183 for (var dimension in HorizonLine.dimensions) {
2185 if (dimension != 'YPOS') {
2186 this.sourceDimensions[dimension] =
2187 HorizonLine.dimensions[dimension] * 2;
2190 this.sourceDimensions[dimension] =
2191 HorizonLine.dimensions[dimension];
2193 this.dimensions[dimension] = HorizonLine.dimensions[dimension];
2196 this.xPos = [0, HorizonLine.dimensions.WIDTH];
2197 this.yPos = HorizonLine.dimensions.YPOS;
2201 * Return the crop x position of a type.
2203 getRandomType: function() {
2204 return Math.random() > this.bumpThreshold ? this.dimensions.WIDTH : 0;
2208 * Draw the horizon line.
2211 this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[0],
2213 this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
2214 this.xPos[0], this.yPos,
2215 this.dimensions.WIDTH, this.dimensions.HEIGHT);
2217 this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[1],
2219 this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
2220 this.xPos[1], this.yPos,
2221 this.dimensions.WIDTH, this.dimensions.HEIGHT);
2225 * Update the x position of an indivdual piece of the line.
2226 * @param {number} pos Line position.
2227 * @param {number} increment
2229 updateXPos: function(pos, increment) {
2231 var line2 = pos == 0 ? 1 : 0;
2233 this.xPos[line1] -= increment;
2234 this.xPos[line2] = this.xPos[line1] + this.dimensions.WIDTH;
2236 if (this.xPos[line1] <= -this.dimensions.WIDTH) {
2237 this.xPos[line1] += this.dimensions.WIDTH * 2;
2238 this.xPos[line2] = this.xPos[line1] - this.dimensions.WIDTH;
2239 this.sourceXPos[line1] = this.getRandomType() + this.spritePos.x;
2244 * Update the horizon line.
2245 * @param {number} deltaTime
2246 * @param {number} speed
2248 update: function(deltaTime, speed) {
2249 var increment = Math.floor(speed * (FPS / 1000) * deltaTime);
2251 if (this.xPos[0] <= 0) {
2252 this.updateXPos(0, increment);
2254 this.updateXPos(1, increment);
2260 * Reset horizon to the starting position.
2264 this.xPos[1] = HorizonLine.dimensions.WIDTH;
2269 //******************************************************************************
2272 * Horizon background class.
2273 * @param {HTMLCanvasElement} canvas
2274 * @param {Object} spritePos Sprite positioning.
2275 * @param {Object} dimensions Canvas dimensions.
2276 * @param {number} gapCoefficient
2279 function Horizon(canvas, spritePos, dimensions, gapCoefficient) {
2280 this.canvas = canvas;
2281 this.canvasCtx = this.canvas.getContext('2d');
2282 this.config = Horizon.config;
2283 this.dimensions = dimensions;
2284 this.gapCoefficient = gapCoefficient;
2285 this.obstacles = [];
2286 this.obstacleHistory = [];
2287 this.horizonOffsets = [0, 0];
2288 this.cloudFrequency = this.config.CLOUD_FREQUENCY;
2289 this.spritePos = spritePos;
2293 this.cloudSpeed = this.config.BG_CLOUD_SPEED;
2296 this.horizonLine = null;
2307 BG_CLOUD_SPEED: 0.2,
2308 BUMPY_THRESHOLD: .3,
2309 CLOUD_FREQUENCY: .5,
2315 Horizon.prototype = {
2317 * Initialise the horizon. Just add the line and a cloud. No obstacles.
2321 this.horizonLine = new HorizonLine(this.canvas, this.spritePos.HORIZON);
2325 * @param {number} deltaTime
2326 * @param {number} currentSpeed
2327 * @param {boolean} updateObstacles Used as an override to prevent
2328 * the obstacles from being updated / added. This happens in the
2331 update: function(deltaTime, currentSpeed, updateObstacles) {
2332 this.runningTime += deltaTime;
2333 this.horizonLine.update(deltaTime, currentSpeed);
2334 this.updateClouds(deltaTime, currentSpeed);
2336 if (updateObstacles) {
2337 this.updateObstacles(deltaTime, currentSpeed);
2342 * Update the cloud positions.
2343 * @param {number} deltaTime
2344 * @param {number} currentSpeed
2346 updateClouds: function(deltaTime, speed) {
2347 var cloudSpeed = this.cloudSpeed / 1000 * deltaTime * speed;
2348 var numClouds = this.clouds.length;
2351 for (var i = numClouds - 1; i >= 0; i--) {
2352 this.clouds[i].update(cloudSpeed);
2355 var lastCloud = this.clouds[numClouds - 1];
2357 // Check for adding a new cloud.
2358 if (numClouds < this.config.MAX_CLOUDS &&
2359 (this.dimensions.WIDTH - lastCloud.xPos) > lastCloud.cloudGap &&
2360 this.cloudFrequency > Math.random()) {
2364 // Remove expired clouds.
2365 this.clouds = this.clouds.filter(function(obj) {
2372 * Update the obstacle positions.
2373 * @param {number} deltaTime
2374 * @param {number} currentSpeed
2376 updateObstacles: function(deltaTime, currentSpeed) {
2377 // Obstacles, move to Horizon layer.
2378 var updatedObstacles = this.obstacles.slice(0);
2380 for (var i = 0; i < this.obstacles.length; i++) {
2381 var obstacle = this.obstacles[i];
2382 obstacle.update(deltaTime, currentSpeed);
2384 // Clean up existing obstacles.
2385 if (obstacle.remove) {
2386 updatedObstacles.shift();
2389 this.obstacles = updatedObstacles;
2391 if (this.obstacles.length > 0) {
2392 var lastObstacle = this.obstacles[this.obstacles.length - 1];
2394 if (lastObstacle && !lastObstacle.followingObstacleCreated &&
2395 lastObstacle.isVisible() &&
2396 (lastObstacle.xPos + lastObstacle.width + lastObstacle.gap) <
2397 this.dimensions.WIDTH) {
2398 this.addNewObstacle(currentSpeed);
2399 lastObstacle.followingObstacleCreated = true;
2402 // Create new obstacles.
2403 this.addNewObstacle(currentSpeed);
2408 * Add a new obstacle.
2409 * @param {number} currentSpeed
2411 addNewObstacle: function(currentSpeed) {
2412 var obstacleTypeIndex = getRandomNum(0, Obstacle.types.length - 1);
2413 var obstacleType = Obstacle.types[obstacleTypeIndex];
2415 // Check for multiples of the same type of obstacle.
2416 // Also check obstacle is available at current speed.
2417 if (this.duplicateObstacleCheck(obstacleType.type) ||
2418 currentSpeed < obstacleType.minSpeed) {
2419 this.addNewObstacle(currentSpeed);
2421 var obstacleSpritePos = this.spritePos[obstacleType.type];
2423 this.obstacles.push(new Obstacle(this.canvasCtx, obstacleType,
2424 obstacleSpritePos, this.dimensions,
2425 this.gapCoefficient, currentSpeed));
2427 this.obstacleHistory.unshift(obstacleType.type);
2429 if (this.obstacleHistory.length > 1) {
2430 this.obstacleHistory.splice(Runner.config.MAX_OBSTACLE_DUPLICATION);
2436 * Returns whether the previous two obstacles are the same as the next one.
2437 * Maximum duplication is set in config value MAX_OBSTACLE_DUPLICATION.
2440 duplicateObstacleCheck: function(nextObstacleType) {
2441 var duplicateCount = 0;
2443 for (var i = 0; i < this.obstacleHistory.length; i++) {
2444 duplicateCount = this.obstacleHistory[i] == nextObstacleType ?
2445 duplicateCount + 1 : 0;
2447 return duplicateCount >= Runner.config.MAX_OBSTACLE_DUPLICATION;
2451 * Reset the horizon layer.
2452 * Remove existing obstacles and reposition the horizon line.
2455 this.obstacles = [];
2456 this.horizonLine.reset();
2460 * Update the canvas width and scaling.
2461 * @param {number} width Canvas width.
2462 * @param {number} height Canvas height.
2464 resize: function(width, height) {
2465 this.canvas.width = width;
2466 this.canvas.height = height;
2470 * Add a new cloud to the horizon.
2472 addCloud: function() {
2473 this.clouds.push(new Cloud(this.canvas, this.spritePos.CLOUD,
2474 this.dimensions.WIDTH));