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) {
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 window.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 && e.button < 2 && e.type == Runner.events.MOUSEUP &&
699 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') {
803 * @param {SoundBuffer} soundBuffer
805 playSound: function(soundBuffer) {
807 var sourceNode = this.audioContext.createBufferSource();
808 sourceNode.buffer = soundBuffer;
809 sourceNode.connect(this.audioContext.destination);
817 * Updates the canvas size taking into
818 * account the backing store pixel ratio and
819 * the device pixel ratio.
821 * See article by Paul Lewis:
822 * http://www.html5rocks.com/en/tutorials/canvas/hidpi/
824 * @param {HTMLCanvasElement} canvas
825 * @param {number} opt_width
826 * @param {number} opt_height
827 * @return {boolean} Whether the canvas was scaled.
829 Runner.updateCanvasScaling = function(canvas, opt_width, opt_height) {
830 var context = canvas.getContext('2d');
832 // Query the various pixel ratios
833 var devicePixelRatio = Math.floor(window.devicePixelRatio) || 1;
834 var backingStoreRatio = Math.floor(context.webkitBackingStorePixelRatio) || 1;
835 var ratio = devicePixelRatio / backingStoreRatio;
837 // Upscale the canvas if the two ratios don't match
838 if (devicePixelRatio !== backingStoreRatio) {
839 var oldWidth = opt_width || canvas.width;
840 var oldHeight = opt_height || canvas.height;
842 canvas.width = oldWidth * ratio;
843 canvas.height = oldHeight * ratio;
845 canvas.style.width = oldWidth + 'px';
846 canvas.style.height = oldHeight + 'px';
848 // Scale the context to counter the fact that we've manually scaled
849 // our canvas element.
850 context.scale(ratio, ratio);
852 } else if (devicePixelRatio == 1) {
853 // Reset the canvas width / height. Fixes scaling bug when the page is
854 // zoomed and the devicePixelRatio changes accordingly.
855 canvas.style.width = canvas.width + 'px';
856 canvas.style.height = canvas.height + 'px';
864 * @param {number} min
865 * @param {number} max
868 function getRandomNum(min, max) {
869 return Math.floor(Math.random() * (max - min + 1)) + min;
874 * Vibrate on mobile devices.
875 * @param {number} duration Duration of the vibration in milliseconds.
877 function vibrate(duration) {
878 if (IS_MOBILE && window.navigator.vibrate) {
879 window.navigator.vibrate(duration);
885 * Create canvas element.
886 * @param {HTMLElement} container Element to append canvas to.
887 * @param {number} width
888 * @param {number} height
889 * @param {string} opt_classname
890 * @return {HTMLCanvasElement}
892 function createCanvas(container, width, height, opt_classname) {
893 var canvas = document.createElement('canvas');
894 canvas.className = opt_classname ? Runner.classes.CANVAS + ' ' +
895 opt_classname : Runner.classes.CANVAS;
896 canvas.width = width;
897 canvas.height = height;
898 container.appendChild(canvas);
905 * Decodes the base 64 audio to ArrayBuffer used by Web Audio.
906 * @param {string} base64String
908 function decodeBase64ToArrayBuffer(base64String) {
909 var len = (base64String.length / 4) * 3;
910 var str = atob(base64String);
911 var arrayBuffer = new ArrayBuffer(len);
912 var bytes = new Uint8Array(arrayBuffer);
914 for (var i = 0; i < len; i++) {
915 bytes[i] = str.charCodeAt(i);
922 * Return the current timestamp.
925 function getTimeStamp() {
926 return IS_IOS ? new Date().getTime() : performance.now();
930 //******************************************************************************
935 * @param {!HTMLCanvasElement} canvas
936 * @param {Object} textImgPos
937 * @param {Object} restartImgPos
938 * @param {!Object} dimensions Canvas dimensions.
941 function GameOverPanel(canvas, textImgPos, restartImgPos, dimensions) {
942 this.canvas = canvas;
943 this.canvasCtx = canvas.getContext('2d');
944 this.canvasDimensions = dimensions;
945 this.textImgPos = textImgPos;
946 this.restartImgPos = restartImgPos;
952 * Dimensions used in the panel.
955 GameOverPanel.dimensions = {
965 GameOverPanel.prototype = {
967 * Update the panel dimensions.
968 * @param {number} width New canvas width.
969 * @param {number} opt_height Optional new canvas height.
971 updateDimensions: function(width, opt_height) {
972 this.canvasDimensions.WIDTH = width;
974 this.canvasDimensions.HEIGHT = opt_height;
982 var dimensions = GameOverPanel.dimensions;
984 var centerX = this.canvasDimensions.WIDTH / 2;
987 var textSourceX = dimensions.TEXT_X;
988 var textSourceY = dimensions.TEXT_Y;
989 var textSourceWidth = dimensions.TEXT_WIDTH;
990 var textSourceHeight = dimensions.TEXT_HEIGHT;
992 var textTargetX = Math.round(centerX - (dimensions.TEXT_WIDTH / 2));
993 var textTargetY = Math.round((this.canvasDimensions.HEIGHT - 25) / 3);
994 var textTargetWidth = dimensions.TEXT_WIDTH;
995 var textTargetHeight = dimensions.TEXT_HEIGHT;
997 var restartSourceWidth = dimensions.RESTART_WIDTH;
998 var restartSourceHeight = dimensions.RESTART_HEIGHT;
999 var restartTargetX = centerX - (dimensions.RESTART_WIDTH / 2);
1000 var restartTargetY = this.canvasDimensions.HEIGHT / 2;
1005 textSourceWidth *= 2;
1006 textSourceHeight *= 2;
1007 restartSourceWidth *= 2;
1008 restartSourceHeight *= 2;
1011 textSourceX += this.textImgPos.x;
1012 textSourceY += this.textImgPos.y;
1014 // Game over text from sprite.
1015 this.canvasCtx.drawImage(Runner.imageSprite,
1016 textSourceX, textSourceY, textSourceWidth, textSourceHeight,
1017 textTargetX, textTargetY, textTargetWidth, textTargetHeight);
1020 this.canvasCtx.drawImage(Runner.imageSprite,
1021 this.restartImgPos.x, this.restartImgPos.y,
1022 restartSourceWidth, restartSourceHeight,
1023 restartTargetX, restartTargetY, dimensions.RESTART_WIDTH,
1024 dimensions.RESTART_HEIGHT);
1029 //******************************************************************************
1032 * Check for a collision.
1033 * @param {!Obstacle} obstacle
1034 * @param {!Trex} tRex T-rex object.
1035 * @param {HTMLCanvasContext} opt_canvasCtx Optional canvas context for drawing
1037 * @return {Array<CollisionBox>}
1039 function checkForCollision(obstacle, tRex, opt_canvasCtx) {
1040 var obstacleBoxXPos = Runner.defaultDimensions.WIDTH + obstacle.xPos;
1042 // Adjustments are made to the bounding box as there is a 1 pixel white
1043 // border around the t-rex and obstacles.
1044 var tRexBox = new CollisionBox(
1047 tRex.config.WIDTH - 2,
1048 tRex.config.HEIGHT - 2);
1050 var obstacleBox = new CollisionBox(
1053 obstacle.typeConfig.width * obstacle.size - 2,
1054 obstacle.typeConfig.height - 2);
1057 if (opt_canvasCtx) {
1058 drawCollisionBoxes(opt_canvasCtx, tRexBox, obstacleBox);
1061 // Simple outer bounds check.
1062 if (boxCompare(tRexBox, obstacleBox)) {
1063 var collisionBoxes = obstacle.collisionBoxes;
1064 var tRexCollisionBoxes = tRex.ducking ?
1065 Trex.collisionBoxes.DUCKING : Trex.collisionBoxes.RUNNING;
1067 // Detailed axis aligned box check.
1068 for (var t = 0; t < tRexCollisionBoxes.length; t++) {
1069 for (var i = 0; i < collisionBoxes.length; i++) {
1070 // Adjust the box to actual positions.
1072 createAdjustedCollisionBox(tRexCollisionBoxes[t], tRexBox);
1073 var adjObstacleBox =
1074 createAdjustedCollisionBox(collisionBoxes[i], obstacleBox);
1075 var crashed = boxCompare(adjTrexBox, adjObstacleBox);
1077 // Draw boxes for debug.
1078 if (opt_canvasCtx) {
1079 drawCollisionBoxes(opt_canvasCtx, adjTrexBox, adjObstacleBox);
1083 return [adjTrexBox, adjObstacleBox];
1093 * Adjust the collision box.
1094 * @param {!CollisionBox} box The original box.
1095 * @param {!CollisionBox} adjustment Adjustment box.
1096 * @return {CollisionBox} The adjusted collision box object.
1098 function createAdjustedCollisionBox(box, adjustment) {
1099 return new CollisionBox(
1100 box.x + adjustment.x,
1101 box.y + adjustment.y,
1108 * Draw the collision boxes for debug.
1110 function drawCollisionBoxes(canvasCtx, tRexBox, obstacleBox) {
1112 canvasCtx.strokeStyle = '#f00';
1113 canvasCtx.strokeRect(tRexBox.x, tRexBox.y, tRexBox.width, tRexBox.height);
1115 canvasCtx.strokeStyle = '#0f0';
1116 canvasCtx.strokeRect(obstacleBox.x, obstacleBox.y,
1117 obstacleBox.width, obstacleBox.height);
1118 canvasCtx.restore();
1123 * Compare two collision boxes for a collision.
1124 * @param {CollisionBox} tRexBox
1125 * @param {CollisionBox} obstacleBox
1126 * @return {boolean} Whether the boxes intersected.
1128 function boxCompare(tRexBox, obstacleBox) {
1129 var crashed = false;
1130 var tRexBoxX = tRexBox.x;
1131 var tRexBoxY = tRexBox.y;
1133 var obstacleBoxX = obstacleBox.x;
1134 var obstacleBoxY = obstacleBox.y;
1136 // Axis-Aligned Bounding Box method.
1137 if (tRexBox.x < obstacleBoxX + obstacleBox.width &&
1138 tRexBox.x + tRexBox.width > obstacleBoxX &&
1139 tRexBox.y < obstacleBox.y + obstacleBox.height &&
1140 tRexBox.height + tRexBox.y > obstacleBox.y) {
1148 //******************************************************************************
1151 * Collision box object.
1152 * @param {number} x X position.
1153 * @param {number} y Y Position.
1154 * @param {number} w Width.
1155 * @param {number} h Height.
1157 function CollisionBox(x, y, w, h) {
1165 //******************************************************************************
1169 * @param {HTMLCanvasCtx} canvasCtx
1170 * @param {Obstacle.type} type
1171 * @param {Object} spritePos Obstacle position in sprite.
1172 * @param {Object} dimensions
1173 * @param {number} gapCoefficient Mutipler in determining the gap.
1174 * @param {number} speed
1176 function Obstacle(canvasCtx, type, spriteImgPos, dimensions,
1177 gapCoefficient, speed) {
1179 this.canvasCtx = canvasCtx;
1180 this.spritePos = spriteImgPos;
1181 this.typeConfig = type;
1182 this.gapCoefficient = gapCoefficient;
1183 this.size = getRandomNum(1, Obstacle.MAX_OBSTACLE_LENGTH);
1184 this.dimensions = dimensions;
1185 this.remove = false;
1189 this.collisionBoxes = [];
1191 this.speedOffset = 0;
1193 // For animated obstacles.
1194 this.currentFrame = 0;
1201 * Coefficient for calculating the maximum gap.
1204 Obstacle.MAX_GAP_COEFFICIENT = 1.5;
1207 * Maximum obstacle grouping count.
1210 Obstacle.MAX_OBSTACLE_LENGTH = 3,
1213 Obstacle.prototype = {
1215 * Initialise the DOM for the obstacle.
1216 * @param {number} speed
1218 init: function(speed) {
1219 this.cloneCollisionBoxes();
1221 // Only allow sizing if we're at the right speed.
1222 if (this.size > 1 && this.typeConfig.multipleSpeed > speed) {
1226 this.width = this.typeConfig.width * this.size;
1227 this.xPos = this.dimensions.WIDTH - this.width;
1229 // Check if obstacle can be positioned at various heights.
1230 if (Array.isArray(this.typeConfig.yPos)) {
1231 var yPosConfig = IS_MOBILE ? this.typeConfig.yPosMobile :
1232 this.typeConfig.yPos;
1233 this.yPos = yPosConfig[getRandomNum(0, yPosConfig.length - 1)];
1235 this.yPos = this.typeConfig.yPos;
1240 // Make collision box adjustments,
1241 // Central box is adjusted to the size as one box.
1242 // ____ ______ ________
1243 // _| |-| _| |-| _| |-|
1244 // | |<->| | | |<--->| | | |<----->| |
1245 // | | 1 | | | | 2 | | | | 3 | |
1246 // |_|___|_| |_|_____|_| |_|_______|_|
1248 if (this.size > 1) {
1249 this.collisionBoxes[1].width = this.width - this.collisionBoxes[0].width -
1250 this.collisionBoxes[2].width;
1251 this.collisionBoxes[2].x = this.width - this.collisionBoxes[2].width;
1254 // For obstacles that go at a different speed from the horizon.
1255 if (this.typeConfig.speedOffset) {
1256 this.speedOffset = Math.random() > 0.5 ? this.typeConfig.speedOffset :
1257 -this.typeConfig.speedOffset;
1260 this.gap = this.getGap(this.gapCoefficient, speed);
1264 * Draw and crop based on size.
1267 var sourceWidth = this.typeConfig.width;
1268 var sourceHeight = this.typeConfig.height;
1271 sourceWidth = sourceWidth * 2;
1272 sourceHeight = sourceHeight * 2;
1275 // X position in sprite.
1276 var sourceX = (sourceWidth * this.size) * (0.5 * (this.size - 1)) +
1279 // Animation frames.
1280 if (this.currentFrame > 0) {
1281 sourceX += sourceWidth * this.currentFrame;
1284 this.canvasCtx.drawImage(Runner.imageSprite,
1285 sourceX, this.spritePos.y,
1286 sourceWidth * this.size, sourceHeight,
1287 this.xPos, this.yPos,
1288 this.typeConfig.width * this.size, this.typeConfig.height);
1292 * Obstacle frame update.
1293 * @param {number} deltaTime
1294 * @param {number} speed
1296 update: function(deltaTime, speed) {
1298 if (this.typeConfig.speedOffset) {
1299 speed += this.speedOffset;
1301 this.xPos -= Math.floor((speed * FPS / 1000) * deltaTime);
1304 if (this.typeConfig.numFrames) {
1305 this.timer += deltaTime;
1306 if (this.timer >= this.typeConfig.frameRate) {
1308 this.currentFrame == this.typeConfig.numFrames - 1 ?
1309 0 : this.currentFrame + 1;
1315 if (!this.isVisible()) {
1322 * Calculate a random gap size.
1323 * - Minimum gap gets wider as speed increses
1324 * @param {number} gapCoefficient
1325 * @param {number} speed
1326 * @return {number} The gap size.
1328 getGap: function(gapCoefficient, speed) {
1329 var minGap = Math.round(this.width * speed +
1330 this.typeConfig.minGap * gapCoefficient);
1331 var maxGap = Math.round(minGap * Obstacle.MAX_GAP_COEFFICIENT);
1332 return getRandomNum(minGap, maxGap);
1336 * Check if obstacle is visible.
1337 * @return {boolean} Whether the obstacle is in the game area.
1339 isVisible: function() {
1340 return this.xPos + this.width > 0;
1344 * Make a copy of the collision boxes, since these will change based on
1345 * obstacle type and size.
1347 cloneCollisionBoxes: function() {
1348 var collisionBoxes = this.typeConfig.collisionBoxes;
1350 for (var i = collisionBoxes.length - 1; i >= 0; i--) {
1351 this.collisionBoxes[i] = new CollisionBox(collisionBoxes[i].x,
1352 collisionBoxes[i].y, collisionBoxes[i].width,
1353 collisionBoxes[i].height);
1360 * Obstacle definitions.
1361 * minGap: minimum pixel space betweeen obstacles.
1362 * multipleSpeed: Speed at which multiples are allowed.
1363 * speedOffset: speed faster / slower than the horizon.
1364 * minSpeed: Minimum speed which the obstacle can make an appearance.
1368 type: 'CACTUS_SMALL',
1376 new CollisionBox(0, 7, 5, 27),
1377 new CollisionBox(4, 0, 6, 34),
1378 new CollisionBox(10, 4, 7, 14)
1382 type: 'CACTUS_LARGE',
1390 new CollisionBox(0, 12, 7, 38),
1391 new CollisionBox(8, 0, 7, 49),
1392 new CollisionBox(13, 10, 10, 38)
1396 type: 'PTERODACTYL',
1399 yPos: [ 100, 75, 50 ], // Variable height.
1400 yPosMobile: [ 100, 50 ], // Variable height mobile.
1405 new CollisionBox(15, 15, 16, 5),
1406 new CollisionBox(18, 21, 24, 6),
1407 new CollisionBox(2, 14, 4, 3),
1408 new CollisionBox(6, 10, 4, 7),
1409 new CollisionBox(10, 8, 6, 9)
1418 //******************************************************************************
1420 * T-rex game character.
1421 * @param {HTMLCanvas} canvas
1422 * @param {Object} spritePos Positioning within image sprite.
1425 function Trex(canvas, spritePos) {
1426 this.canvas = canvas;
1427 this.canvasCtx = canvas.getContext('2d');
1428 this.spritePos = spritePos;
1431 // Position when on the ground.
1432 this.groundYPos = 0;
1433 this.currentFrame = 0;
1434 this.currentAnimFrames = [];
1435 this.blinkDelay = 0;
1436 this.animStartTime = 0;
1438 this.msPerFrame = 1000 / FPS;
1439 this.config = Trex.config;
1441 this.status = Trex.status.WAITING;
1443 this.jumping = false;
1444 this.ducking = false;
1445 this.jumpVelocity = 0;
1446 this.reachedMinHeight = false;
1447 this.speedDrop = false;
1456 * T-rex player config.
1464 INIITAL_JUMP_VELOCITY: -10,
1465 INTRO_DURATION: 1500,
1466 MAX_JUMP_HEIGHT: 30,
1467 MIN_JUMP_HEIGHT: 30,
1468 SPEED_DROP_COEFFICIENT: 3,
1477 * Used in collision detection.
1478 * @type {Array<CollisionBox>}
1480 Trex.collisionBoxes = {
1482 new CollisionBox(1, 18, 55, 25)
1485 new CollisionBox(22, 0, 17, 16),
1486 new CollisionBox(1, 18, 30, 9),
1487 new CollisionBox(10, 35, 14, 8),
1488 new CollisionBox(1, 24, 29, 5),
1489 new CollisionBox(5, 30, 21, 4),
1490 new CollisionBox(9, 34, 15, 4)
1508 * Blinking coefficient.
1511 Trex.BLINK_TIMING = 7000;
1515 * Animation config for different states.
1521 msPerFrame: 1000 / 3
1525 msPerFrame: 1000 / 12
1529 msPerFrame: 1000 / 60
1533 msPerFrame: 1000 / 60
1537 msPerFrame: 1000 / 8
1544 * T-rex player initaliser.
1545 * Sets the t-rex to blink at random intervals.
1548 this.blinkDelay = this.setBlinkDelay();
1549 this.groundYPos = Runner.defaultDimensions.HEIGHT - this.config.HEIGHT -
1550 Runner.config.BOTTOM_PAD;
1551 this.yPos = this.groundYPos;
1552 this.minJumpHeight = this.groundYPos - this.config.MIN_JUMP_HEIGHT;
1555 this.update(0, Trex.status.WAITING);
1559 * Setter for the jump velocity.
1560 * The approriate drop velocity is also set.
1562 setJumpVelocity: function(setting) {
1563 this.config.INIITAL_JUMP_VELOCITY = -setting;
1564 this.config.DROP_VELOCITY = -setting / 2;
1568 * Set the animation status.
1569 * @param {!number} deltaTime
1570 * @param {Trex.status} status Optional status to switch to.
1572 update: function(deltaTime, opt_status) {
1573 this.timer += deltaTime;
1575 // Update the status.
1577 this.status = opt_status;
1578 this.currentFrame = 0;
1579 this.msPerFrame = Trex.animFrames[opt_status].msPerFrame;
1580 this.currentAnimFrames = Trex.animFrames[opt_status].frames;
1582 if (opt_status == Trex.status.WAITING) {
1583 this.animStartTime = getTimeStamp();
1584 this.setBlinkDelay();
1588 // Game intro animation, T-rex moves in from the left.
1589 if (this.playingIntro && this.xPos < this.config.START_X_POS) {
1590 this.xPos += Math.round((this.config.START_X_POS /
1591 this.config.INTRO_DURATION) * deltaTime);
1594 if (this.status == Trex.status.WAITING) {
1595 this.blink(getTimeStamp());
1597 this.draw(this.currentAnimFrames[this.currentFrame], 0);
1600 // Update the frame position.
1601 if (this.timer >= this.msPerFrame) {
1602 this.currentFrame = this.currentFrame ==
1603 this.currentAnimFrames.length - 1 ? 0 : this.currentFrame + 1;
1607 // Speed drop becomes duck if the down key is still being pressed.
1608 if (this.speedDrop && this.yPos == this.groundYPos) {
1609 this.speedDrop = false;
1615 * Draw the t-rex to a particular position.
1619 draw: function(x, y) {
1622 var sourceWidth = this.ducking && this.status != Trex.status.CRASHED ?
1623 this.config.WIDTH_DUCK : this.config.WIDTH;
1624 var sourceHeight = this.config.HEIGHT;
1633 // Adjustments for sprite sheet position.
1634 sourceX += this.spritePos.x;
1635 sourceY += this.spritePos.y;
1638 if (this.ducking && this.status != Trex.status.CRASHED) {
1639 this.canvasCtx.drawImage(Runner.imageSprite, sourceX, sourceY,
1640 sourceWidth, sourceHeight,
1641 this.xPos, this.yPos,
1642 this.config.WIDTH_DUCK, this.config.HEIGHT);
1644 // Crashed whilst ducking. Trex is standing up so needs adjustment.
1645 if (this.ducking && this.status == Trex.status.CRASHED) {
1648 // Standing / running
1649 this.canvasCtx.drawImage(Runner.imageSprite, sourceX, sourceY,
1650 sourceWidth, sourceHeight,
1651 this.xPos, this.yPos,
1652 this.config.WIDTH, this.config.HEIGHT);
1657 * Sets a random time for the blink to happen.
1659 setBlinkDelay: function() {
1660 this.blinkDelay = Math.ceil(Math.random() * Trex.BLINK_TIMING);
1664 * Make t-rex blink at random intervals.
1665 * @param {number} time Current time in milliseconds.
1667 blink: function(time) {
1668 var deltaTime = time - this.animStartTime;
1670 if (deltaTime >= this.blinkDelay) {
1671 this.draw(this.currentAnimFrames[this.currentFrame], 0);
1673 if (this.currentFrame == 1) {
1674 // Set new random delay to blink.
1675 this.setBlinkDelay();
1676 this.animStartTime = time;
1682 * Initialise a jump.
1683 * @param {number} speed
1685 startJump: function(speed) {
1686 if (!this.jumping) {
1687 this.update(0, Trex.status.JUMPING);
1688 // Tweak the jump velocity based on the speed.
1689 this.jumpVelocity = this.config.INIITAL_JUMP_VELOCITY - (speed / 10);
1690 this.jumping = true;
1691 this.reachedMinHeight = false;
1692 this.speedDrop = false;
1697 * Jump is complete, falling down.
1699 endJump: function() {
1700 if (this.reachedMinHeight &&
1701 this.jumpVelocity < this.config.DROP_VELOCITY) {
1702 this.jumpVelocity = this.config.DROP_VELOCITY;
1707 * Update frame for a jump.
1708 * @param {number} deltaTime
1709 * @param {number} speed
1711 updateJump: function(deltaTime, speed) {
1712 var msPerFrame = Trex.animFrames[this.status].msPerFrame;
1713 var framesElapsed = deltaTime / msPerFrame;
1715 // Speed drop makes Trex fall faster.
1716 if (this.speedDrop) {
1717 this.yPos += Math.round(this.jumpVelocity *
1718 this.config.SPEED_DROP_COEFFICIENT * framesElapsed);
1720 this.yPos += Math.round(this.jumpVelocity * framesElapsed);
1723 this.jumpVelocity += this.config.GRAVITY * framesElapsed;
1725 // Minimum height has been reached.
1726 if (this.yPos < this.minJumpHeight || this.speedDrop) {
1727 this.reachedMinHeight = true;
1730 // Reached max height
1731 if (this.yPos < this.config.MAX_JUMP_HEIGHT || this.speedDrop) {
1735 // Back down at ground level. Jump completed.
1736 if (this.yPos > this.groundYPos) {
1741 this.update(deltaTime);
1745 * Set the speed drop. Immediately cancels the current jump.
1747 setSpeedDrop: function() {
1748 this.speedDrop = true;
1749 this.jumpVelocity = 1;
1753 * @param {boolean} isDucking.
1755 setDuck: function(isDucking) {
1756 if (isDucking && this.status != Trex.status.DUCKING) {
1757 this.update(0, Trex.status.DUCKING);
1758 this.ducking = true;
1759 } else if (this.status == Trex.status.DUCKING) {
1760 this.update(0, Trex.status.RUNNING);
1761 this.ducking = false;
1766 * Reset the t-rex to running at start of game.
1769 this.yPos = this.groundYPos;
1770 this.jumpVelocity = 0;
1771 this.jumping = false;
1772 this.ducking = false;
1773 this.update(0, Trex.status.RUNNING);
1774 this.midair = false;
1775 this.speedDrop = false;
1781 //******************************************************************************
1784 * Handles displaying the distance meter.
1785 * @param {!HTMLCanvasElement} canvas
1786 * @param {Object} spritePos Image position in sprite.
1787 * @param {number} canvasWidth
1790 function DistanceMeter(canvas, spritePos, canvasWidth) {
1791 this.canvas = canvas;
1792 this.canvasCtx = canvas.getContext('2d');
1793 this.image = Runner.imageSprite;
1794 this.spritePos = spritePos;
1798 this.currentDistance = 0;
1801 this.container = null;
1804 this.acheivement = false;
1805 this.defaultString = '';
1806 this.flashTimer = 0;
1807 this.flashIterations = 0;
1809 this.config = DistanceMeter.config;
1810 this.maxScoreUnits = this.config.MAX_DISTANCE_UNITS;
1811 this.init(canvasWidth);
1818 DistanceMeter.dimensions = {
1826 * Y positioning of the digits in the sprite sheet.
1827 * X position is always 0.
1828 * @type {Array<number>}
1830 DistanceMeter.yPos = [0, 13, 27, 40, 53, 67, 80, 93, 107, 120];
1834 * Distance meter config.
1837 DistanceMeter.config = {
1838 // Number of digits.
1839 MAX_DISTANCE_UNITS: 5,
1841 // Distance that causes achievement animation.
1842 ACHIEVEMENT_DISTANCE: 100,
1844 // Used for conversion from pixel distance to a scaled unit.
1847 // Flash duration in milliseconds.
1848 FLASH_DURATION: 1000 / 4,
1850 // Flash iterations for achievement animation.
1855 DistanceMeter.prototype = {
1857 * Initialise the distance meter to '00000'.
1858 * @param {number} width Canvas width in px.
1860 init: function(width) {
1861 var maxDistanceStr = '';
1863 this.calcXPos(width);
1864 this.maxScore = this.maxScoreUnits;
1865 for (var i = 0; i < this.maxScoreUnits; i++) {
1867 this.defaultString += '0';
1868 maxDistanceStr += '9';
1871 this.maxScore = parseInt(maxDistanceStr);
1875 * Calculate the xPos in the canvas.
1876 * @param {number} canvasWidth
1878 calcXPos: function(canvasWidth) {
1879 this.x = canvasWidth - (DistanceMeter.dimensions.DEST_WIDTH *
1880 (this.maxScoreUnits + 1));
1884 * Draw a digit to canvas.
1885 * @param {number} digitPos Position of the digit.
1886 * @param {number} value Digit value 0-9.
1887 * @param {boolean} opt_highScore Whether drawing the high score.
1889 draw: function(digitPos, value, opt_highScore) {
1890 var sourceWidth = DistanceMeter.dimensions.WIDTH;
1891 var sourceHeight = DistanceMeter.dimensions.HEIGHT;
1892 var sourceX = DistanceMeter.dimensions.WIDTH * value;
1895 var targetX = digitPos * DistanceMeter.dimensions.DEST_WIDTH;
1896 var targetY = this.y;
1897 var targetWidth = DistanceMeter.dimensions.WIDTH;
1898 var targetHeight = DistanceMeter.dimensions.HEIGHT;
1900 // For high DPI we 2x source values.
1907 sourceX += this.spritePos.x;
1908 sourceY += this.spritePos.y;
1910 this.canvasCtx.save();
1912 if (opt_highScore) {
1913 // Left of the current score.
1914 var highScoreX = this.x - (this.maxScoreUnits * 2) *
1915 DistanceMeter.dimensions.WIDTH;
1916 this.canvasCtx.translate(highScoreX, this.y);
1918 this.canvasCtx.translate(this.x, this.y);
1921 this.canvasCtx.drawImage(this.image, sourceX, sourceY,
1922 sourceWidth, sourceHeight,
1924 targetWidth, targetHeight
1927 this.canvasCtx.restore();
1931 * Covert pixel distance to a 'real' distance.
1932 * @param {number} distance Pixel distance ran.
1933 * @return {number} The 'real' distance ran.
1935 getActualDistance: function(distance) {
1936 return distance ? Math.round(distance * this.config.COEFFICIENT) : 0;
1940 * Update the distance meter.
1941 * @param {number} distance
1942 * @param {number} deltaTime
1943 * @return {boolean} Whether the acheivement sound fx should be played.
1945 update: function(deltaTime, distance) {
1947 var playSound = false;
1949 if (!this.acheivement) {
1950 distance = this.getActualDistance(distance);
1952 // Score has gone beyond the initial digit count.
1953 if (distance > this.maxScore && this.maxScoreUnits ==
1954 this.config.MAX_DISTANCE_UNITS) {
1955 this.maxScoreUnits++;
1956 this.maxScore = parseInt(this.maxScore + '9');
1962 // Acheivement unlocked
1963 if (distance % this.config.ACHIEVEMENT_DISTANCE == 0) {
1964 // Flash score and play sound.
1965 this.acheivement = true;
1966 this.flashTimer = 0;
1970 // Create a string representation of the distance with leading 0.
1971 var distanceStr = (this.defaultString +
1972 distance).substr(-this.maxScoreUnits);
1973 this.digits = distanceStr.split('');
1975 this.digits = this.defaultString.split('');
1978 // Control flashing of the score on reaching acheivement.
1979 if (this.flashIterations <= this.config.FLASH_ITERATIONS) {
1980 this.flashTimer += deltaTime;
1982 if (this.flashTimer < this.config.FLASH_DURATION) {
1984 } else if (this.flashTimer >
1985 this.config.FLASH_DURATION * 2) {
1986 this.flashTimer = 0;
1987 this.flashIterations++;
1990 this.acheivement = false;
1991 this.flashIterations = 0;
1992 this.flashTimer = 0;
1996 // Draw the digits if not flashing.
1998 for (var i = this.digits.length - 1; i >= 0; i--) {
1999 this.draw(i, parseInt(this.digits[i]));
2003 this.drawHighScore();
2009 * Draw the high score.
2011 drawHighScore: function() {
2012 this.canvasCtx.save();
2013 this.canvasCtx.globalAlpha = .8;
2014 for (var i = this.highScore.length - 1; i >= 0; i--) {
2015 this.draw(i, parseInt(this.highScore[i], 10), true);
2017 this.canvasCtx.restore();
2021 * Set the highscore as a array string.
2022 * Position of char in the sprite: H - 10, I - 11.
2023 * @param {number} distance Distance ran in pixels.
2025 setHighScore: function(distance) {
2026 distance = this.getActualDistance(distance);
2027 var highScoreStr = (this.defaultString +
2028 distance).substr(-this.maxScoreUnits);
2030 this.highScore = ['10', '11', ''].concat(highScoreStr.split(''));
2034 * Reset the distance meter back to '00000'.
2038 this.acheivement = false;
2043 //******************************************************************************
2046 * Cloud background item.
2047 * Similar to an obstacle object but without collision boxes.
2048 * @param {HTMLCanvasElement} canvas Canvas element.
2049 * @param {Object} spritePos Position of image in sprite.
2050 * @param {number} containerWidth
2052 function Cloud(canvas, spritePos, containerWidth) {
2053 this.canvas = canvas;
2054 this.canvasCtx = this.canvas.getContext('2d');
2055 this.spritePos = spritePos;
2056 this.containerWidth = containerWidth;
2057 this.xPos = containerWidth;
2059 this.remove = false;
2060 this.cloudGap = getRandomNum(Cloud.config.MIN_CLOUD_GAP,
2061 Cloud.config.MAX_CLOUD_GAP);
2068 * Cloud object config.
2083 * Initialise the cloud. Sets the Cloud height.
2086 this.yPos = getRandomNum(Cloud.config.MAX_SKY_LEVEL,
2087 Cloud.config.MIN_SKY_LEVEL);
2095 this.canvasCtx.save();
2096 var sourceWidth = Cloud.config.WIDTH;
2097 var sourceHeight = Cloud.config.HEIGHT;
2100 sourceWidth = sourceWidth * 2;
2101 sourceHeight = sourceHeight * 2;
2104 this.canvasCtx.drawImage(Runner.imageSprite, this.spritePos.x,
2106 sourceWidth, sourceHeight,
2107 this.xPos, this.yPos,
2108 Cloud.config.WIDTH, Cloud.config.HEIGHT);
2110 this.canvasCtx.restore();
2114 * Update the cloud position.
2115 * @param {number} speed
2117 update: function(speed) {
2119 this.xPos -= Math.ceil(speed);
2122 // Mark as removeable if no longer in the canvas.
2123 if (!this.isVisible()) {
2130 * Check if the cloud is visible on the stage.
2133 isVisible: function() {
2134 return this.xPos + Cloud.config.WIDTH > 0;
2139 //******************************************************************************
2143 * Consists of two connecting lines. Randomly assigns a flat / bumpy horizon.
2144 * @param {HTMLCanvasElement} canvas
2145 * @param {Object} spritePos Horizon position in sprite.
2148 function HorizonLine(canvas, spritePos) {
2149 this.spritePos = spritePos;
2150 this.canvas = canvas;
2151 this.canvasCtx = canvas.getContext('2d');
2152 this.sourceDimensions = {};
2153 this.dimensions = HorizonLine.dimensions;
2154 this.sourceXPos = [this.spritePos.x, this.spritePos.x +
2155 this.dimensions.WIDTH];
2158 this.bumpThreshold = 0.5;
2160 this.setSourceDimensions();
2166 * Horizon line dimensions.
2169 HorizonLine.dimensions = {
2176 HorizonLine.prototype = {
2178 * Set the source dimensions of the horizon line.
2180 setSourceDimensions: function() {
2182 for (var dimension in HorizonLine.dimensions) {
2184 if (dimension != 'YPOS') {
2185 this.sourceDimensions[dimension] =
2186 HorizonLine.dimensions[dimension] * 2;
2189 this.sourceDimensions[dimension] =
2190 HorizonLine.dimensions[dimension];
2192 this.dimensions[dimension] = HorizonLine.dimensions[dimension];
2195 this.xPos = [0, HorizonLine.dimensions.WIDTH];
2196 this.yPos = HorizonLine.dimensions.YPOS;
2200 * Return the crop x position of a type.
2202 getRandomType: function() {
2203 return Math.random() > this.bumpThreshold ? this.dimensions.WIDTH : 0;
2207 * Draw the horizon line.
2210 this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[0],
2212 this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
2213 this.xPos[0], this.yPos,
2214 this.dimensions.WIDTH, this.dimensions.HEIGHT);
2216 this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[1],
2218 this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
2219 this.xPos[1], this.yPos,
2220 this.dimensions.WIDTH, this.dimensions.HEIGHT);
2224 * Update the x position of an indivdual piece of the line.
2225 * @param {number} pos Line position.
2226 * @param {number} increment
2228 updateXPos: function(pos, increment) {
2230 var line2 = pos == 0 ? 1 : 0;
2232 this.xPos[line1] -= increment;
2233 this.xPos[line2] = this.xPos[line1] + this.dimensions.WIDTH;
2235 if (this.xPos[line1] <= -this.dimensions.WIDTH) {
2236 this.xPos[line1] += this.dimensions.WIDTH * 2;
2237 this.xPos[line2] = this.xPos[line1] - this.dimensions.WIDTH;
2238 this.sourceXPos[line1] = this.getRandomType() + this.spritePos.x;
2243 * Update the horizon line.
2244 * @param {number} deltaTime
2245 * @param {number} speed
2247 update: function(deltaTime, speed) {
2248 var increment = Math.floor(speed * (FPS / 1000) * deltaTime);
2250 if (this.xPos[0] <= 0) {
2251 this.updateXPos(0, increment);
2253 this.updateXPos(1, increment);
2259 * Reset horizon to the starting position.
2263 this.xPos[1] = HorizonLine.dimensions.WIDTH;
2268 //******************************************************************************
2271 * Horizon background class.
2272 * @param {HTMLCanvasElement} canvas
2273 * @param {Object} spritePos Sprite positioning.
2274 * @param {Object} dimensions Canvas dimensions.
2275 * @param {number} gapCoefficient
2278 function Horizon(canvas, spritePos, dimensions, gapCoefficient) {
2279 this.canvas = canvas;
2280 this.canvasCtx = this.canvas.getContext('2d');
2281 this.config = Horizon.config;
2282 this.dimensions = dimensions;
2283 this.gapCoefficient = gapCoefficient;
2284 this.obstacles = [];
2285 this.obstacleHistory = [];
2286 this.horizonOffsets = [0, 0];
2287 this.cloudFrequency = this.config.CLOUD_FREQUENCY;
2288 this.spritePos = spritePos;
2292 this.cloudSpeed = this.config.BG_CLOUD_SPEED;
2295 this.horizonLine = null;
2306 BG_CLOUD_SPEED: 0.2,
2307 BUMPY_THRESHOLD: .3,
2308 CLOUD_FREQUENCY: .5,
2314 Horizon.prototype = {
2316 * Initialise the horizon. Just add the line and a cloud. No obstacles.
2320 this.horizonLine = new HorizonLine(this.canvas, this.spritePos.HORIZON);
2324 * @param {number} deltaTime
2325 * @param {number} currentSpeed
2326 * @param {boolean} updateObstacles Used as an override to prevent
2327 * the obstacles from being updated / added. This happens in the
2330 update: function(deltaTime, currentSpeed, updateObstacles) {
2331 this.runningTime += deltaTime;
2332 this.horizonLine.update(deltaTime, currentSpeed);
2333 this.updateClouds(deltaTime, currentSpeed);
2335 if (updateObstacles) {
2336 this.updateObstacles(deltaTime, currentSpeed);
2341 * Update the cloud positions.
2342 * @param {number} deltaTime
2343 * @param {number} currentSpeed
2345 updateClouds: function(deltaTime, speed) {
2346 var cloudSpeed = this.cloudSpeed / 1000 * deltaTime * speed;
2347 var numClouds = this.clouds.length;
2350 for (var i = numClouds - 1; i >= 0; i--) {
2351 this.clouds[i].update(cloudSpeed);
2354 var lastCloud = this.clouds[numClouds - 1];
2356 // Check for adding a new cloud.
2357 if (numClouds < this.config.MAX_CLOUDS &&
2358 (this.dimensions.WIDTH - lastCloud.xPos) > lastCloud.cloudGap &&
2359 this.cloudFrequency > Math.random()) {
2363 // Remove expired clouds.
2364 this.clouds = this.clouds.filter(function(obj) {
2371 * Update the obstacle positions.
2372 * @param {number} deltaTime
2373 * @param {number} currentSpeed
2375 updateObstacles: function(deltaTime, currentSpeed) {
2376 // Obstacles, move to Horizon layer.
2377 var updatedObstacles = this.obstacles.slice(0);
2379 for (var i = 0; i < this.obstacles.length; i++) {
2380 var obstacle = this.obstacles[i];
2381 obstacle.update(deltaTime, currentSpeed);
2383 // Clean up existing obstacles.
2384 if (obstacle.remove) {
2385 updatedObstacles.shift();
2388 this.obstacles = updatedObstacles;
2390 if (this.obstacles.length > 0) {
2391 var lastObstacle = this.obstacles[this.obstacles.length - 1];
2393 if (lastObstacle && !lastObstacle.followingObstacleCreated &&
2394 lastObstacle.isVisible() &&
2395 (lastObstacle.xPos + lastObstacle.width + lastObstacle.gap) <
2396 this.dimensions.WIDTH) {
2397 this.addNewObstacle(currentSpeed);
2398 lastObstacle.followingObstacleCreated = true;
2401 // Create new obstacles.
2402 this.addNewObstacle(currentSpeed);
2407 * Add a new obstacle.
2408 * @param {number} currentSpeed
2410 addNewObstacle: function(currentSpeed) {
2411 var obstacleTypeIndex = getRandomNum(0, Obstacle.types.length - 1);
2412 var obstacleType = Obstacle.types[obstacleTypeIndex];
2414 // Check for multiples of the same type of obstacle.
2415 // Also check obstacle is available at current speed.
2416 if (this.duplicateObstacleCheck(obstacleType.type) ||
2417 currentSpeed < obstacleType.minSpeed) {
2418 this.addNewObstacle(currentSpeed);
2420 var obstacleSpritePos = this.spritePos[obstacleType.type];
2422 this.obstacles.push(new Obstacle(this.canvasCtx, obstacleType,
2423 obstacleSpritePos, this.dimensions,
2424 this.gapCoefficient, currentSpeed));
2426 this.obstacleHistory.unshift(obstacleType.type);
2428 if (this.obstacleHistory.length > 1) {
2429 this.obstacleHistory.splice(Runner.config.MAX_OBSTACLE_DUPLICATION);
2435 * Returns whether the previous two obstacles are the same as the next one.
2436 * Maximum duplication is set in config value MAX_OBSTACLE_DUPLICATION.
2439 duplicateObstacleCheck: function(nextObstacleType) {
2440 var duplicateCount = 0;
2442 for (var i = 0; i < this.obstacleHistory.length; i++) {
2443 duplicateCount = this.obstacleHistory[i] == nextObstacleType ?
2444 duplicateCount + 1 : 0;
2446 return duplicateCount >= Runner.config.MAX_OBSTACLE_DUPLICATION;
2450 * Reset the horizon layer.
2451 * Remove existing obstacles and reposition the horizon line.
2454 this.obstacles = [];
2455 this.horizonLine.reset();
2459 * Update the canvas width and scaling.
2460 * @param {number} width Canvas width.
2461 * @param {number} height Canvas height.
2463 resize: function(width, height) {
2464 this.canvas.width = width;
2465 this.canvas.height = height;
2469 * Add a new cloud to the horizon.
2471 addCloud: function() {
2472 this.clouds.push(new Cloud(this.canvas, this.spritePos.CLOUD,
2473 this.dimensions.WIDTH));