Guest User

dino.js

a guest
Apr 4th, 2020
568
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // Copyright (c) 2014 The Chromium Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4.  
  5. /**
  6.  * T-Rex runner.
  7.  * @param {string} outerContainerId Outer containing element id.
  8.  * @param {!Object=} opt_config
  9.  * @constructor
  10.  * @implements {EventListener}
  11.  * @export
  12.  */
  13. function Runner(outerContainerId, opt_config) {
  14.   // Singleton
  15.   if (Runner.instance_) {
  16.     return Runner.instance_;
  17.   }
  18.   Runner.instance_ = this;
  19.  
  20.   this.outerContainerEl = document.querySelector(outerContainerId);
  21.   this.containerEl = null;
  22.   this.snackbarEl = null;
  23.   // A div to intercept touch events. Only set while (playing && useTouch).
  24.   this.touchController = null;
  25.  
  26.   this.config = opt_config || Runner.config;
  27.   // Logical dimensions of the container.
  28.   this.dimensions = Runner.defaultDimensions;
  29.  
  30.   this.canvas = null;
  31.   this.canvasCtx = null;
  32.  
  33.   this.tRex = null;
  34.  
  35.   this.distanceMeter = null;
  36.   this.distanceRan = 0;
  37.  
  38.   this.highestScore = 0;
  39.   this.syncHighestScore = false;
  40.  
  41.   this.time = 0;
  42.   this.runningTime = 0;
  43.   this.msPerFrame = 1000 / FPS;
  44.   this.currentSpeed = this.config.SPEED;
  45.  
  46.   this.obstacles = [];
  47.  
  48.   this.activated = false; // Whether the easter egg has been activated.
  49.   this.playing = false; // Whether the game is currently in play state.
  50.   this.crashed = false;
  51.   this.paused = false;
  52.   this.inverted = false;
  53.   this.invertTimer = 0;
  54.   this.resizeTimerId_ = null;
  55.  
  56.   this.playCount = 0;
  57.  
  58.   // Sound FX.
  59.   this.audioBuffer = null;
  60.   /** @type {Object} */
  61.   this.soundFx = {};
  62.  
  63.   // Global web audio context for playing sounds.
  64.   this.audioContext = null;
  65.  
  66.   // Images.
  67.   this.images = {};
  68.   this.imagesLoaded = 0;
  69.  
  70.   // Gamepad state.
  71.   this.pollingGamepads = false;
  72.   this.gamepadIndex = undefined;
  73.   this.previousGamepad = null;
  74.  
  75.   if (this.isDisabled()) {
  76.     this.setupDisabledRunner();
  77.   } else {
  78.     this.loadImages();
  79.  
  80.     window['initializeEasterEggHighScore'] =
  81.         this.initializeHighScore.bind(this);
  82.   }
  83. }
  84.  
  85. /**
  86.  * Default game width.
  87.  * @const
  88.  */
  89. const DEFAULT_WIDTH = 600;
  90.  
  91. /**
  92.  * Frames per second.
  93.  * @const
  94.  */
  95. const FPS = 60;
  96.  
  97. /** @const */
  98. const IS_HIDPI = window.devicePixelRatio > 1;
  99.  
  100. /** @const */
  101. // iPads are returning "MacIntel" for iOS 13 (devices & simulators).
  102. // Chrome on macOS also returns "MacIntel" for navigator.platform,
  103. // but navigator.userAgent includes /Safari/.
  104. // TODO(crbug.com/998999): Fix navigator.userAgent such that it reliably
  105. // returns an agent string containing "CriOS".
  106. const IS_IOS = /CriOS/.test(window.navigator.userAgent) ||
  107.     /iPad|iPhone|iPod|MacIntel/.test(window.navigator.platform) &&
  108.         !(/Safari/.test(window.navigator.userAgent));
  109.  
  110. /** @const */
  111. const IS_MOBILE = /Android/.test(window.navigator.userAgent) || IS_IOS;
  112.  
  113. /** @const */
  114. const ARCADE_MODE_URL = 'chrome://dino/';
  115.  
  116. /**
  117.  * Default game configuration.
  118.  */
  119. Runner.config = {
  120.   ACCELERATION: 0.001,
  121.   BG_CLOUD_SPEED: 0.2,
  122.   BOTTOM_PAD: 10,
  123.   // Scroll Y threshold at which the game can be activated.
  124.   CANVAS_IN_VIEW_OFFSET: -10,
  125.   CLEAR_TIME: 3000,
  126.   CLOUD_FREQUENCY: 0.5,
  127.   GAMEOVER_CLEAR_TIME: 750,
  128.   GAP_COEFFICIENT: 0.6,
  129.   GRAVITY: 0.6,
  130.   INITIAL_JUMP_VELOCITY: 12,
  131.   INVERT_FADE_DURATION: 12000,
  132.   INVERT_DISTANCE: 700,
  133.   MAX_BLINK_COUNT: 3,
  134.   MAX_CLOUDS: 6,
  135.   MAX_OBSTACLE_LENGTH: 3,
  136.   MAX_OBSTACLE_DUPLICATION: 2,
  137.   MAX_SPEED: 13,
  138.   MIN_JUMP_HEIGHT: 35,
  139.   MOBILE_SPEED_COEFFICIENT: 1.2,
  140.   RESOURCE_TEMPLATE_ID: 'audio-resources',
  141.   SPEED: 6,
  142.   SPEED_DROP_COEFFICIENT: 3,
  143.   ARCADE_MODE_INITIAL_TOP_POSITION: 35,
  144.   ARCADE_MODE_TOP_POSITION_PERCENT: 0.1
  145. };
  146.  
  147.  
  148. /**
  149.  * Default dimensions.
  150.  */
  151. Runner.defaultDimensions = {
  152.   WIDTH: DEFAULT_WIDTH,
  153.   HEIGHT: 150
  154. };
  155.  
  156.  
  157. /**
  158.  * CSS class names.
  159.  * @enum {string}
  160.  */
  161. Runner.classes = {
  162.   ARCADE_MODE: 'arcade-mode',
  163.   CANVAS: 'runner-canvas',
  164.   CONTAINER: 'runner-container',
  165.   CRASHED: 'crashed',
  166.   ICON: 'icon-offline',
  167.   INVERTED: 'inverted',
  168.   SNACKBAR: 'snackbar',
  169.   SNACKBAR_SHOW: 'snackbar-show',
  170.   TOUCH_CONTROLLER: 'controller'
  171. };
  172.  
  173.  
  174. /**
  175.  * Sprite definition layout of the spritesheet.
  176.  * @enum {Object}
  177.  */
  178. Runner.spriteDefinition = {
  179.   LDPI: {
  180.     CACTUS_LARGE: {x: 332, y: 2},
  181.     CACTUS_SMALL: {x: 228, y: 2},
  182.     CLOUD: {x: 86, y: 2},
  183.     HORIZON: {x: 2, y: 54},
  184.     MOON: {x: 484, y: 2},
  185.     PTERODACTYL: {x: 134, y: 2},
  186.     RESTART: {x: 2, y: 2},
  187.     TEXT_SPRITE: {x: 655, y: 2},
  188.     TREX: {x: 848, y: 2},
  189.     STAR: {x: 645, y: 2}
  190.   },
  191.   HDPI: {
  192.     CACTUS_LARGE: {x: 652, y: 2},
  193.     CACTUS_SMALL: {x: 446, y: 2},
  194.     CLOUD: {x: 166, y: 2},
  195.     HORIZON: {x: 2, y: 104},
  196.     MOON: {x: 954, y: 2},
  197.     PTERODACTYL: {x: 260, y: 2},
  198.     RESTART: {x: 2, y: 2},
  199.     TEXT_SPRITE: {x: 1294, y: 2},
  200.     TREX: {x: 1678, y: 2},
  201.     STAR: {x: 1276, y: 2}
  202.   }
  203. };
  204.  
  205.  
  206. /**
  207.  * Sound FX. Reference to the ID of the audio tag on interstitial page.
  208.  * @enum {string}
  209.  */
  210. Runner.sounds = {
  211.   BUTTON_PRESS: 'offline-sound-press',
  212.   HIT: 'offline-sound-hit',
  213.   SCORE: 'offline-sound-reached'
  214. };
  215.  
  216.  
  217. /**
  218.  * Key code mapping.
  219.  * @enum {Object}
  220.  */
  221. Runner.keycodes = {
  222.   JUMP: {'38': 1, '32': 1},  // Up, spacebar
  223.   DUCK: {'40': 1},  // Down
  224.   RESTART: {'13': 1}  // Enter
  225. };
  226.  
  227.  
  228. /**
  229.  * Runner event names.
  230.  * @enum {string}
  231.  */
  232. Runner.events = {
  233.   ANIM_END: 'webkitAnimationEnd',
  234.   CLICK: 'click',
  235.   KEYDOWN: 'keydown',
  236.   KEYUP: 'keyup',
  237.   POINTERDOWN: 'pointerdown',
  238.   POINTERUP: 'pointerup',
  239.   RESIZE: 'resize',
  240.   TOUCHEND: 'touchend',
  241.   TOUCHSTART: 'touchstart',
  242.   VISIBILITY: 'visibilitychange',
  243.   BLUR: 'blur',
  244.   FOCUS: 'focus',
  245.   LOAD: 'load',
  246.   GAMEPADCONNECTED: 'gamepadconnected',
  247. };
  248.  
  249. Runner.prototype = {
  250.   /**
  251.    * Whether the easter egg has been disabled. CrOS enterprise enrolled devices.
  252.    * @return {boolean}
  253.    */
  254.   isDisabled: function() {
  255.     return loadTimeData && loadTimeData.valueExists('disabledEasterEgg');
  256.   },
  257.  
  258.   /**
  259.    * For disabled instances, set up a snackbar with the disabled message.
  260.    */
  261.   setupDisabledRunner: function() {
  262.     this.containerEl = document.createElement('div');
  263.     this.containerEl.className = Runner.classes.SNACKBAR;
  264.     this.containerEl.textContent = loadTimeData.getValue('disabledEasterEgg');
  265.     this.outerContainerEl.appendChild(this.containerEl);
  266.  
  267.     // Show notification when the activation key is pressed.
  268.     document.addEventListener(Runner.events.KEYDOWN, function(e) {
  269.       if (Runner.keycodes.JUMP[e.keyCode]) {
  270.         this.containerEl.classList.add(Runner.classes.SNACKBAR_SHOW);
  271.         document.querySelector('.icon').classList.add('icon-disabled');
  272.       }
  273.     }.bind(this));
  274.   },
  275.  
  276.   /**
  277.    * Setting individual settings for debugging.
  278.    * @param {string} setting
  279.    * @param {number|string} value
  280.    */
  281.   updateConfigSetting: function(setting, value) {
  282.     if (setting in this.config && value != undefined) {
  283.       this.config[setting] = value;
  284.  
  285.       switch (setting) {
  286.         case 'GRAVITY':
  287.         case 'MIN_JUMP_HEIGHT':
  288.         case 'SPEED_DROP_COEFFICIENT':
  289.           this.tRex.config[setting] = value;
  290.           break;
  291.         case 'INITIAL_JUMP_VELOCITY':
  292.           this.tRex.setJumpVelocity(value);
  293.           break;
  294.         case 'SPEED':
  295.           this.setSpeed(/** @type {number} */ (value));
  296.           break;
  297.       }
  298.     }
  299.   },
  300.  
  301.   /**
  302.    * Cache the appropriate image sprite from the page and get the sprite sheet
  303.    * definition.
  304.    */
  305.   loadImages: function() {
  306.     if (IS_HIDPI) {
  307.       Runner.imageSprite = /** @type {HTMLImageElement} */
  308.           (document.getElementById('offline-resources-2x'));
  309.       this.spriteDef = Runner.spriteDefinition.HDPI;
  310.     } else {
  311.       Runner.imageSprite = /** @type {HTMLImageElement} */
  312.           (document.getElementById('offline-resources-1x'));
  313.       this.spriteDef = Runner.spriteDefinition.LDPI;
  314.     }
  315.  
  316.     if (Runner.imageSprite.complete) {
  317.       this.init();
  318.     } else {
  319.       // If the images are not yet loaded, add a listener.
  320.       Runner.imageSprite.addEventListener(Runner.events.LOAD,
  321.           this.init.bind(this));
  322.     }
  323.   },
  324.  
  325.   /**
  326.    * Load and decode base 64 encoded sounds.
  327.    */
  328.   loadSounds: function() {
  329.     if (!IS_IOS) {
  330.       this.audioContext = new AudioContext();
  331.  
  332.       const resourceTemplate =
  333.           document.getElementById(this.config.RESOURCE_TEMPLATE_ID).content;
  334.  
  335.       for (const sound in Runner.sounds) {
  336.         let soundSrc =
  337.             resourceTemplate.getElementById(Runner.sounds[sound]).src;
  338.         soundSrc = soundSrc.substr(soundSrc.indexOf(',') + 1);
  339.         const buffer = decodeBase64ToArrayBuffer(soundSrc);
  340.  
  341.         // Async, so no guarantee of order in array.
  342.         this.audioContext.decodeAudioData(buffer, function(index, audioData) {
  343.             this.soundFx[index] = audioData;
  344.           }.bind(this, sound));
  345.       }
  346.     }
  347.   },
  348.  
  349.   /**
  350.    * Sets the game speed. Adjust the speed accordingly if on a smaller screen.
  351.    * @param {number=} opt_speed
  352.    */
  353.   setSpeed: function(opt_speed) {
  354.     const speed = opt_speed || this.currentSpeed;
  355.  
  356.     // Reduce the speed on smaller mobile screens.
  357.     if (this.dimensions.WIDTH < DEFAULT_WIDTH) {
  358.       const mobileSpeed = speed * this.dimensions.WIDTH / DEFAULT_WIDTH *
  359.           this.config.MOBILE_SPEED_COEFFICIENT;
  360.       this.currentSpeed = mobileSpeed > speed ? speed : mobileSpeed;
  361.     } else if (opt_speed) {
  362.       this.currentSpeed = opt_speed;
  363.     }
  364.   },
  365.  
  366.   /**
  367.    * Game initialiser.
  368.    */
  369.   init: function() {
  370.     // Hide the static icon.
  371.     document.querySelector('.' + Runner.classes.ICON).style.visibility =
  372.         'hidden';
  373.  
  374.     this.adjustDimensions();
  375.     this.setSpeed();
  376.  
  377.     this.containerEl = document.createElement('div');
  378.     this.containerEl.className = Runner.classes.CONTAINER;
  379.  
  380.     // Player canvas container.
  381.     this.canvas = createCanvas(this.containerEl, this.dimensions.WIDTH,
  382.         this.dimensions.HEIGHT);
  383.  
  384.     this.canvasCtx =
  385.         /** @type {CanvasRenderingContext2D} */ (this.canvas.getContext('2d'));
  386.     this.canvasCtx.fillStyle = '#f7f7f7';
  387.     this.canvasCtx.fill();
  388.     Runner.updateCanvasScaling(this.canvas);
  389.  
  390.     // Horizon contains clouds, obstacles and the ground.
  391.     this.horizon = new Horizon(this.canvas, this.spriteDef, this.dimensions,
  392.         this.config.GAP_COEFFICIENT);
  393.  
  394.     // Distance meter
  395.     this.distanceMeter = new DistanceMeter(this.canvas,
  396.           this.spriteDef.TEXT_SPRITE, this.dimensions.WIDTH);
  397.  
  398.     // Draw t-rex
  399.     this.tRex = new Trex(this.canvas, this.spriteDef.TREX);
  400.  
  401.     this.outerContainerEl.appendChild(this.containerEl);
  402.  
  403.     this.startListening();
  404.     this.update();
  405.  
  406.     window.addEventListener(Runner.events.RESIZE,
  407.         this.debounceResize.bind(this));
  408.  
  409.     // Handle dark mode
  410.     const darkModeMediaQuery =
  411.         window.matchMedia('(prefers-color-scheme: dark)');
  412.     this.isDarkMode = darkModeMediaQuery && darkModeMediaQuery.matches;
  413.     darkModeMediaQuery.addListener((e) => {
  414.       this.isDarkMode = e.matches;
  415.     });
  416.   },
  417.  
  418.   /**
  419.    * Create the touch controller. A div that covers whole screen.
  420.    */
  421.   createTouchController: function() {
  422.     this.touchController = document.createElement('div');
  423.     this.touchController.className = Runner.classes.TOUCH_CONTROLLER;
  424.     this.touchController.addEventListener(Runner.events.TOUCHSTART, this);
  425.     this.touchController.addEventListener(Runner.events.TOUCHEND, this);
  426.     this.outerContainerEl.appendChild(this.touchController);
  427.   },
  428.  
  429.   /**
  430.    * Debounce the resize event.
  431.    */
  432.   debounceResize: function() {
  433.     if (!this.resizeTimerId_) {
  434.       this.resizeTimerId_ =
  435.           setInterval(this.adjustDimensions.bind(this), 250);
  436.     }
  437.   },
  438.  
  439.   /**
  440.    * Adjust game space dimensions on resize.
  441.    */
  442.   adjustDimensions: function() {
  443.     clearInterval(this.resizeTimerId_);
  444.     this.resizeTimerId_ = null;
  445.  
  446.     const boxStyles = window.getComputedStyle(this.outerContainerEl);
  447.     const padding = Number(boxStyles.paddingLeft.substr(0,
  448.         boxStyles.paddingLeft.length - 2));
  449.  
  450.     this.dimensions.WIDTH = this.outerContainerEl.offsetWidth - padding * 2;
  451.     if (this.isArcadeMode()) {
  452.       this.dimensions.WIDTH = Math.min(DEFAULT_WIDTH, this.dimensions.WIDTH);
  453.       if (this.activated) {
  454.         this.setArcadeModeContainerScale();
  455.       }
  456.     }
  457.  
  458.     // Redraw the elements back onto the canvas.
  459.     if (this.canvas) {
  460.       this.canvas.width = this.dimensions.WIDTH;
  461.       this.canvas.height = this.dimensions.HEIGHT;
  462.  
  463.       Runner.updateCanvasScaling(this.canvas);
  464.  
  465.       this.distanceMeter.calcXPos(this.dimensions.WIDTH);
  466.       this.clearCanvas();
  467.       this.horizon.update(0, 0, true);
  468.       this.tRex.update(0);
  469.  
  470.       // Outer container and distance meter.
  471.       if (this.playing || this.crashed || this.paused) {
  472.         this.containerEl.style.width = this.dimensions.WIDTH + 'px';
  473.         this.containerEl.style.height = this.dimensions.HEIGHT + 'px';
  474.         this.distanceMeter.update(0, Math.ceil(this.distanceRan));
  475.         this.stop();
  476.       } else {
  477.         this.tRex.draw(0, 0);
  478.       }
  479.  
  480.       // Game over panel.
  481.       if (this.crashed && this.gameOverPanel) {
  482.         this.gameOverPanel.updateDimensions(this.dimensions.WIDTH);
  483.         this.gameOverPanel.draw();
  484.       }
  485.     }
  486.   },
  487.  
  488.   /**
  489.    * Play the game intro.
  490.    * Canvas container width expands out to the full width.
  491.    */
  492.   playIntro: function() {
  493.     if (!this.activated && !this.crashed) {
  494.       this.playingIntro = true;
  495.       this.tRex.playingIntro = true;
  496.  
  497.       // CSS animation definition.
  498.       const keyframes = '@-webkit-keyframes intro { ' +
  499.             'from { width:' + Trex.config.WIDTH + 'px }' +
  500.             'to { width: ' + this.dimensions.WIDTH + 'px }' +
  501.           '}';
  502.       document.styleSheets[0].insertRule(keyframes, 0);
  503.  
  504.       this.containerEl.addEventListener(Runner.events.ANIM_END,
  505.           this.startGame.bind(this));
  506.  
  507.       this.containerEl.style.webkitAnimation = 'intro .4s ease-out 1 both';
  508.       this.containerEl.style.width = this.dimensions.WIDTH + 'px';
  509.  
  510.       this.setPlayStatus(true);
  511.       this.activated = true;
  512.     } else if (this.crashed) {
  513.       this.restart();
  514.     }
  515.   },
  516.  
  517.  
  518.   /**
  519.    * Update the game status to started.
  520.    */
  521.   startGame: function() {
  522.     if (this.isArcadeMode()) {
  523.       this.setArcadeMode();
  524.     }
  525.     this.runningTime = 0;
  526.     this.playingIntro = false;
  527.     this.tRex.playingIntro = false;
  528.     this.containerEl.style.webkitAnimation = '';
  529.     this.playCount++;
  530.  
  531.     // Handle tabbing off the page. Pause the current game.
  532.     document.addEventListener(Runner.events.VISIBILITY,
  533.           this.onVisibilityChange.bind(this));
  534.  
  535.     window.addEventListener(Runner.events.BLUR,
  536.           this.onVisibilityChange.bind(this));
  537.  
  538.     window.addEventListener(Runner.events.FOCUS,
  539.           this.onVisibilityChange.bind(this));
  540.   },
  541.  
  542.   clearCanvas: function() {
  543.     this.canvasCtx.clearRect(0, 0, this.dimensions.WIDTH,
  544.         this.dimensions.HEIGHT);
  545.   },
  546.  
  547.   /**
  548.    * Checks whether the canvas area is in the viewport of the browser
  549.    * through the current scroll position.
  550.    * @return boolean.
  551.    */
  552.   isCanvasInView: function() {
  553.     return this.containerEl.getBoundingClientRect().top >
  554.         Runner.config.CANVAS_IN_VIEW_OFFSET;
  555.   },
  556.  
  557.   /**
  558.    * Update the game frame and schedules the next one.
  559.    */
  560.   update: function() {
  561.     this.updatePending = false;
  562.  
  563.     const now = getTimeStamp();
  564.     let deltaTime = now - (this.time || now);
  565.  
  566.     this.time = now;
  567.  
  568.     if (this.playing) {
  569.       this.clearCanvas();
  570.  
  571.       if (this.tRex.jumping) {
  572.         this.tRex.updateJump(deltaTime);
  573.       }
  574.  
  575.       this.runningTime += deltaTime;
  576.       const hasObstacles = this.runningTime > this.config.CLEAR_TIME;
  577.  
  578.       // First jump triggers the intro.
  579.       if (this.tRex.jumpCount == 1 && !this.playingIntro) {
  580.         this.playIntro();
  581.       }
  582.  
  583.       // The horizon doesn't move until the intro is over.
  584.       if (this.playingIntro) {
  585.         this.horizon.update(0, this.currentSpeed, hasObstacles);
  586.       } else {
  587.         const showNightMode = this.isDarkMode ^ this.inverted;
  588.         deltaTime = !this.activated ? 0 : deltaTime;
  589.         this.horizon.update(
  590.             deltaTime, this.currentSpeed, hasObstacles, showNightMode);
  591.       }
  592.  
  593.       // Check for collisions.
  594.       const collision = hasObstacles &&
  595.           checkForCollision(this.horizon.obstacles[0], this.tRex);
  596.  
  597.       if (!collision) {
  598.         this.distanceRan += this.currentSpeed * deltaTime / this.msPerFrame;
  599.  
  600.         if (this.currentSpeed < this.config.MAX_SPEED) {
  601.           this.currentSpeed += this.config.ACCELERATION;
  602.         }
  603.       } else {
  604.         this.gameOver();
  605.       }
  606.  
  607.       const playAchievementSound = this.distanceMeter.update(deltaTime,
  608.           Math.ceil(this.distanceRan));
  609.  
  610.       if (playAchievementSound) {
  611.         this.playSound(this.soundFx.SCORE);
  612.       }
  613.  
  614.       // Night mode.
  615.       if (this.invertTimer > this.config.INVERT_FADE_DURATION) {
  616.         this.invertTimer = 0;
  617.         this.invertTrigger = false;
  618.         this.invert(false);
  619.       } else if (this.invertTimer) {
  620.         this.invertTimer += deltaTime;
  621.       } else {
  622.         const actualDistance =
  623.             this.distanceMeter.getActualDistance(Math.ceil(this.distanceRan));
  624.  
  625.         if (actualDistance > 0) {
  626.           this.invertTrigger = !(actualDistance %
  627.               this.config.INVERT_DISTANCE);
  628.  
  629.           if (this.invertTrigger && this.invertTimer === 0) {
  630.             this.invertTimer += deltaTime;
  631.             this.invert(false);
  632.           }
  633.         }
  634.       }
  635.     }
  636.  
  637.     if (this.playing || (!this.activated &&
  638.         this.tRex.blinkCount < Runner.config.MAX_BLINK_COUNT)) {
  639.       this.tRex.update(deltaTime);
  640.       this.scheduleNextUpdate();
  641.     }
  642.   },
  643.  
  644.   /**
  645.    * Event handler.
  646.    * @param {Event} e
  647.    */
  648.   handleEvent: function(e) {
  649.     return (function(evtType, events) {
  650.       switch (evtType) {
  651.         case events.KEYDOWN:
  652.         case events.TOUCHSTART:
  653.         case events.POINTERDOWN:
  654.           this.onKeyDown(e);
  655.           break;
  656.         case events.KEYUP:
  657.         case events.TOUCHEND:
  658.         case events.POINTERUP:
  659.           this.onKeyUp(e);
  660.           break;
  661.         case events.GAMEPADCONNECTED:
  662.           this.onGamepadConnected(e);
  663.           break;
  664.       }
  665.     }.bind(this))(e.type, Runner.events);
  666.   },
  667.  
  668.   /**
  669.    * Bind relevant key / mouse / touch listeners.
  670.    */
  671.   startListening: function() {
  672.     // Keys.
  673.     document.addEventListener(Runner.events.KEYDOWN, this);
  674.     document.addEventListener(Runner.events.KEYUP, this);
  675.  
  676.     // Touch / pointer.
  677.     this.containerEl.addEventListener(Runner.events.TOUCHSTART, this);
  678.     document.addEventListener(Runner.events.POINTERDOWN, this);
  679.     document.addEventListener(Runner.events.POINTERUP, this);
  680.  
  681.     if (this.isArcadeMode()) {
  682.       // Gamepad
  683.       window.addEventListener(Runner.events.GAMEPADCONNECTED, this);
  684.     }
  685.   },
  686.  
  687.   /**
  688.    * Remove all listeners.
  689.    */
  690.   stopListening: function() {
  691.     document.removeEventListener(Runner.events.KEYDOWN, this);
  692.     document.removeEventListener(Runner.events.KEYUP, this);
  693.  
  694.     if (this.touchController) {
  695.       this.touchController.removeEventListener(Runner.events.TOUCHSTART, this);
  696.       this.touchController.removeEventListener(Runner.events.TOUCHEND, this);
  697.     }
  698.  
  699.     this.containerEl.removeEventListener(Runner.events.TOUCHSTART, this);
  700.     document.removeEventListener(Runner.events.POINTERDOWN, this);
  701.     document.removeEventListener(Runner.events.POINTERUP, this);
  702.  
  703.     if (this.isArcadeMode()) {
  704.       window.removeEventListener(Runner.events.GAMEPADCONNECTED, this);
  705.     }
  706.   },
  707.  
  708.   /**
  709.    * Process keydown.
  710.    * @param {Event} e
  711.    */
  712.   onKeyDown: function(e) {
  713.     // Prevent native page scrolling whilst tapping on mobile.
  714.     if (IS_MOBILE && this.playing) {
  715.       e.preventDefault();
  716.     }
  717.  
  718.     if (this.isCanvasInView()) {
  719.       if (!this.crashed && !this.paused) {
  720.         if (Runner.keycodes.JUMP[e.keyCode] ||
  721.             e.type == Runner.events.TOUCHSTART) {
  722.           e.preventDefault();
  723.           // Starting the game for the first time.
  724.           if (!this.playing) {
  725.             // Started by touch so create a touch controller.
  726.             if (!this.touchController && e.type == Runner.events.TOUCHSTART) {
  727.               this.createTouchController();
  728.             }
  729.             this.loadSounds();
  730.             this.setPlayStatus(true);
  731.             this.update();
  732.             if (window.errorPageController) {
  733.               errorPageController.trackEasterEgg();
  734.             }
  735.           }
  736.           // Start jump.
  737.           if (!this.tRex.jumping && !this.tRex.ducking) {
  738.             this.playSound(this.soundFx.BUTTON_PRESS);
  739.             this.tRex.startJump(this.currentSpeed);
  740.           }
  741.         } else if (this.playing && Runner.keycodes.DUCK[e.keyCode]) {
  742.           e.preventDefault();
  743.           if (this.tRex.jumping) {
  744.             // Speed drop, activated only when jump key is not pressed.
  745.             this.tRex.setSpeedDrop();
  746.           } else if (!this.tRex.jumping && !this.tRex.ducking) {
  747.             // Duck.
  748.             this.tRex.setDuck(true);
  749.           }
  750.         }
  751.         // iOS only triggers touchstart and no pointer events.
  752.       } else if (IS_IOS && this.crashed && e.type == Runner.events.TOUCHSTART &&
  753.           e.currentTarget == this.containerEl) {
  754.         this.handleGameOverClicks(e);
  755.       }
  756.     }
  757.   },
  758.  
  759.   /**
  760.    * Process key up.
  761.    * @param {Event} e
  762.    */
  763.   onKeyUp: function(e) {
  764.     const keyCode = String(e.keyCode);
  765.     const isjumpKey = Runner.keycodes.JUMP[keyCode] ||
  766.        e.type == Runner.events.TOUCHEND ||
  767.        e.type == Runner.events.POINTERUP;
  768.  
  769.     if (this.isRunning() && isjumpKey) {
  770.       this.tRex.endJump();
  771.     } else if (Runner.keycodes.DUCK[keyCode]) {
  772.       this.tRex.speedDrop = false;
  773.       this.tRex.setDuck(false);
  774.     } else if (this.crashed) {
  775.       // Check that enough time has elapsed before allowing jump key to restart.
  776.       const deltaTime = getTimeStamp() - this.time;
  777.  
  778.       if (this.isCanvasInView() &&
  779.           (Runner.keycodes.RESTART[keyCode] || this.isLeftClickOnCanvas(e) ||
  780.           (deltaTime >= this.config.GAMEOVER_CLEAR_TIME &&
  781.           Runner.keycodes.JUMP[keyCode]))) {
  782.         this.handleGameOverClicks(e);
  783.       }
  784.     } else if (this.paused && isjumpKey) {
  785.       // Reset the jump state
  786.       this.tRex.reset();
  787.       this.play();
  788.     }
  789.   },
  790.  
  791.   /**
  792.    * Process gamepad connected event.
  793.    * @param {Event} e
  794.    */
  795.   onGamepadConnected: function(e) {
  796.     if (!this.pollingGamepads) {
  797.       this.pollGamepadState();
  798.     }
  799.   },
  800.  
  801.   /**
  802.    * rAF loop for gamepad polling.
  803.    */
  804.   pollGamepadState: function() {
  805.     const gamepads = navigator.getGamepads();
  806.     this.pollActiveGamepad(gamepads);
  807.  
  808.     this.pollingGamepads = true;
  809.     requestAnimationFrame(this.pollGamepadState.bind(this));
  810.   },
  811.  
  812.   /**
  813.    * Polls for a gamepad with the jump button pressed. If one is found this
  814.    * becomes the "active" gamepad and all others are ignored.
  815.    * @param {!Array<Gamepad>} gamepads
  816.    */
  817.   pollForActiveGamepad: function(gamepads) {
  818.     for (let i = 0; i < gamepads.length; ++i) {
  819.       if (gamepads[i] && gamepads[i].buttons.length > 0 &&
  820.           gamepads[i].buttons[0].pressed) {
  821.         this.gamepadIndex = i;
  822.         this.pollActiveGamepad(gamepads);
  823.         return;
  824.       }
  825.     }
  826.   },
  827.  
  828.   /**
  829.    * Polls the chosen gamepad for button presses and generates KeyboardEvents
  830.    * to integrate with the rest of the game logic.
  831.    * @param {!Array<Gamepad>} gamepads
  832.    */
  833.   pollActiveGamepad: function(gamepads) {
  834.     if (this.gamepadIndex === undefined) {
  835.       this.pollForActiveGamepad(gamepads);
  836.       return;
  837.     }
  838.  
  839.     const gamepad = gamepads[this.gamepadIndex];
  840.     if (!gamepad) {
  841.       this.gamepadIndex = undefined;
  842.       this.pollForActiveGamepad(gamepads);
  843.       return;
  844.     }
  845.  
  846.     // The gamepad specification defines the typical mapping of physical buttons
  847.     // to button indicies: https://w3c.github.io/gamepad/#remapping
  848.     this.pollGamepadButton(gamepad, 0, 38);  // Jump
  849.     if (gamepad.buttons.length >= 2) {
  850.       this.pollGamepadButton(gamepad, 1, 40);  // Duck
  851.     }
  852.     if (gamepad.buttons.length >= 10) {
  853.       this.pollGamepadButton(gamepad, 9, 13);  // Restart
  854.     }
  855.  
  856.     this.previousGamepad = gamepad;
  857.   },
  858.  
  859.   /**
  860.    * Generates a key event based on a gamepad button.
  861.    * @param {!Gamepad} gamepad
  862.    * @param {number} buttonIndex
  863.    * @param {number} keyCode
  864.    */
  865.   pollGamepadButton: function(gamepad, buttonIndex, keyCode) {
  866.     const state = gamepad.buttons[buttonIndex].pressed;
  867.     let previousState = false;
  868.     if (this.previousGamepad) {
  869.       previousState = this.previousGamepad.buttons[buttonIndex].pressed;
  870.     }
  871.     // Generate key events on the rising and falling edge of a button press.
  872.     if (state != previousState) {
  873.       const e = new KeyboardEvent(state ? Runner.events.KEYDOWN
  874.                                       : Runner.events.KEYUP,
  875.                                 { keyCode: keyCode });
  876.       document.dispatchEvent(e);
  877.     }
  878.   },
  879.  
  880.   /**
  881.    * Handle interactions on the game over screen state.
  882.    * A user is able to tap the high score twice to reset it.
  883.    * @param {Event} e
  884.    */
  885.   handleGameOverClicks: function(e) {
  886.     e.preventDefault();
  887.     if (this.distanceMeter.hasClickedOnHighScore(e) && this.highestScore) {
  888.       if (this.distanceMeter.isHighScoreFlashing()) {
  889.         // Subsequent click, reset the high score.
  890.         this.saveHighScore(0, true);
  891.         this.distanceMeter.resetHighScore();
  892.       } else {
  893.         // First click, flash the high score.
  894.         this.distanceMeter.startHighScoreFlashing();
  895.       }
  896.     } else {
  897.       this.distanceMeter.cancelHighScoreFlashing();
  898.       this.restart();
  899.     }
  900.   },
  901.  
  902.   /**
  903.    * Returns whether the event was a left click on canvas.
  904.    * On Windows right click is registered as a click.
  905.    * @param {Event} e
  906.    * @return {boolean}
  907.    */
  908.   isLeftClickOnCanvas: function(e) {
  909.     return e.button != null && e.button < 2 &&
  910.         e.type == Runner.events.POINTERUP && e.target == this.canvas;
  911.   },
  912.  
  913.   /**
  914.    * RequestAnimationFrame wrapper.
  915.    */
  916.   scheduleNextUpdate: function() {
  917.     if (!this.updatePending) {
  918.       this.updatePending = true;
  919.       this.raqId = requestAnimationFrame(this.update.bind(this));
  920.     }
  921.   },
  922.  
  923.   /**
  924.    * Whether the game is running.
  925.    * @return {boolean}
  926.    */
  927.   isRunning: function() {
  928.     return !!this.raqId;
  929.   },
  930.  
  931.   /**
  932.    * Set the initial high score as stored in the user's profile.
  933.    * @param {number} highScore
  934.    */
  935.   initializeHighScore: function(highScore) {
  936.     this.syncHighestScore = true;
  937.     highScore = Math.ceil(highScore);
  938.     if (highScore < this.highestScore) {
  939.       if (window.errorPageController) {
  940.         errorPageController.updateEasterEggHighScore(this.highestScore);
  941.       }
  942.       return;
  943.     }
  944.     this.highestScore = highScore;
  945.     this.distanceMeter.setHighScore(this.highestScore);
  946.   },
  947.  
  948.   /**
  949.    * Sets the current high score and saves to the profile if available.
  950.    * @param {number} distanceRan Total distance ran.
  951.    * @param {boolean=} opt_resetScore Whether to reset the score.
  952.    */
  953.   saveHighScore: function(distanceRan, opt_resetScore) {
  954.     this.highestScore = Math.ceil(distanceRan);
  955.     this.distanceMeter.setHighScore(this.highestScore);
  956.  
  957.     // Store the new high score in the profile.
  958.     if (this.syncHighestScore && window.errorPageController) {
  959.       if (opt_resetScore) {
  960.         errorPageController.resetEasterEggHighScore();
  961.       } else {
  962.         errorPageController.updateEasterEggHighScore(this.highestScore);
  963.       }
  964.     }
  965.   },
  966.  
  967.   /**
  968.    * Game over state.
  969.    */
  970.   gameOver: function() {
  971.     this.playSound(this.soundFx.HIT);
  972.     vibrate(200);
  973.  
  974.     this.stop();
  975.     this.crashed = true;
  976.     this.distanceMeter.achievement = false;
  977.  
  978.     this.tRex.update(100, Trex.status.CRASHED);
  979.  
  980.     // Game over panel.
  981.     if (!this.gameOverPanel) {
  982.       if (this.canvas) {
  983.         this.gameOverPanel = new GameOverPanel(this.canvas,
  984.             this.spriteDef.TEXT_SPRITE, this.spriteDef.RESTART,
  985.             this.dimensions);
  986.       }
  987.     } else {
  988.       this.gameOverPanel.draw();
  989.     }
  990.  
  991.     // Update the high score.
  992.     if (this.distanceRan > this.highestScore) {
  993.       this.saveHighScore(this.distanceRan);
  994.     }
  995.  
  996.     // Reset the time clock.
  997.     this.time = getTimeStamp();
  998.   },
  999.  
  1000.   stop: function() {
  1001.     this.setPlayStatus(false);
  1002.     this.paused = true;
  1003.     cancelAnimationFrame(this.raqId);
  1004.     this.raqId = 0;
  1005.   },
  1006.  
  1007.   play: function() {
  1008.     if (!this.crashed) {
  1009.       this.setPlayStatus(true);
  1010.       this.paused = false;
  1011.       this.tRex.update(0, Trex.status.RUNNING);
  1012.       this.time = getTimeStamp();
  1013.       this.update();
  1014.     }
  1015.   },
  1016.  
  1017.   restart: function() {
  1018.     if (!this.raqId) {
  1019.       this.playCount++;
  1020.       this.runningTime = 0;
  1021.       this.setPlayStatus(true);
  1022.       this.paused = false;
  1023.       this.crashed = false;
  1024.       this.distanceRan = 0;
  1025.       this.setSpeed(this.config.SPEED);
  1026.       this.time = getTimeStamp();
  1027.       this.containerEl.classList.remove(Runner.classes.CRASHED);
  1028.       this.clearCanvas();
  1029.       this.distanceMeter.reset();
  1030.       this.horizon.reset();
  1031.       this.tRex.reset();
  1032.       this.playSound(this.soundFx.BUTTON_PRESS);
  1033.       this.invert(true);
  1034.       this.bdayFlashTimer = null;
  1035.       this.update();
  1036.     }
  1037.   },
  1038.  
  1039.   setPlayStatus: function(isPlaying) {
  1040.     if (this.touchController) {
  1041.       this.touchController.classList.toggle(HIDDEN_CLASS, !isPlaying);
  1042.     }
  1043.     this.playing = isPlaying;
  1044.   },
  1045.  
  1046.   /**
  1047.    * Whether the game should go into arcade mode.
  1048.    * @return {boolean}
  1049.    */
  1050.   isArcadeMode: function() {
  1051.     return document.title == ARCADE_MODE_URL;
  1052.   },
  1053.  
  1054.   /**
  1055.    * Hides offline messaging for a fullscreen game only experience.
  1056.    */
  1057.   setArcadeMode: function() {
  1058.     document.body.classList.add(Runner.classes.ARCADE_MODE);
  1059.     this.setArcadeModeContainerScale();
  1060.   },
  1061.  
  1062.   /**
  1063.    * Sets the scaling for arcade mode.
  1064.    */
  1065.   setArcadeModeContainerScale: function() {
  1066.     const windowHeight = window.innerHeight;
  1067.     const scaleHeight = windowHeight / this.dimensions.HEIGHT;
  1068.     const scaleWidth = window.innerWidth / this.dimensions.WIDTH;
  1069.     const scale = Math.max(1, Math.min(scaleHeight, scaleWidth));
  1070.     const scaledCanvasHeight = this.dimensions.HEIGHT * scale;
  1071.     // Positions the game container at 10% of the available vertical window
  1072.     // height minus the game container height.
  1073.     const translateY = Math.ceil(Math.max(0, (windowHeight - scaledCanvasHeight -
  1074.         Runner.config.ARCADE_MODE_INITIAL_TOP_POSITION) *
  1075.         Runner.config.ARCADE_MODE_TOP_POSITION_PERCENT)) *
  1076.         window.devicePixelRatio;
  1077.     this.containerEl.style.transform = 'scale(' + scale + ') translateY(' +
  1078.         translateY + 'px)';
  1079.   },
  1080.  
  1081.   /**
  1082.    * Pause the game if the tab is not in focus.
  1083.    */
  1084.   onVisibilityChange: function(e) {
  1085.     if (document.hidden || document.webkitHidden || e.type == 'blur' ||
  1086.       document.visibilityState != 'visible') {
  1087.       this.stop();
  1088.     } else if (!this.crashed) {
  1089.       this.tRex.reset();
  1090.       this.play();
  1091.     }
  1092.   },
  1093.  
  1094.   /**
  1095.    * Play a sound.
  1096.    * @param {AudioBuffer} soundBuffer
  1097.    */
  1098.   playSound: function(soundBuffer) {
  1099.     if (soundBuffer) {
  1100.       const sourceNode = this.audioContext.createBufferSource();
  1101.       sourceNode.buffer = soundBuffer;
  1102.       sourceNode.connect(this.audioContext.destination);
  1103.       sourceNode.start(0);
  1104.     }
  1105.   },
  1106.  
  1107.   /**
  1108.    * Inverts the current page / canvas colors.
  1109.    * @param {boolean} reset Whether to reset colors.
  1110.    */
  1111.   invert: function(reset) {
  1112.     const htmlEl = document.firstElementChild;
  1113.  
  1114.     if (reset) {
  1115.       htmlEl.classList.toggle(Runner.classes.INVERTED,
  1116.           false);
  1117.       this.invertTimer = 0;
  1118.       this.inverted = false;
  1119.     } else {
  1120.       this.inverted = htmlEl.classList.toggle(
  1121.           Runner.classes.INVERTED, this.invertTrigger);
  1122.     }
  1123.   }
  1124. };
  1125.  
  1126.  
  1127. /**
  1128.  * Updates the canvas size taking into
  1129.  * account the backing store pixel ratio and
  1130.  * the device pixel ratio.
  1131.  *
  1132.  * See article by Paul Lewis:
  1133.  * http://www.html5rocks.com/en/tutorials/canvas/hidpi/
  1134.  *
  1135.  * @param {HTMLCanvasElement} canvas
  1136.  * @param {number=} opt_width
  1137.  * @param {number=} opt_height
  1138.  * @return {boolean} Whether the canvas was scaled.
  1139.  */
  1140. Runner.updateCanvasScaling = function(canvas, opt_width, opt_height) {
  1141.   const context =
  1142.       /** @type {CanvasRenderingContext2D} */ (canvas.getContext('2d'));
  1143.  
  1144.   // Query the various pixel ratios
  1145.   const devicePixelRatio = Math.floor(window.devicePixelRatio) || 1;
  1146.   /** @suppress {missingProperties} */
  1147.   const backingStoreRatio =
  1148.       Math.floor(context.webkitBackingStorePixelRatio) || 1;
  1149.   const ratio = devicePixelRatio / backingStoreRatio;
  1150.  
  1151.   // Upscale the canvas if the two ratios don't match
  1152.   if (devicePixelRatio !== backingStoreRatio) {
  1153.     const oldWidth = opt_width || canvas.width;
  1154.     const oldHeight = opt_height || canvas.height;
  1155.  
  1156.     canvas.width = oldWidth * ratio;
  1157.     canvas.height = oldHeight * ratio;
  1158.  
  1159.     canvas.style.width = oldWidth + 'px';
  1160.     canvas.style.height = oldHeight + 'px';
  1161.  
  1162.     // Scale the context to counter the fact that we've manually scaled
  1163.     // our canvas element.
  1164.     context.scale(ratio, ratio);
  1165.     return true;
  1166.   } else if (devicePixelRatio == 1) {
  1167.     // Reset the canvas width / height. Fixes scaling bug when the page is
  1168.     // zoomed and the devicePixelRatio changes accordingly.
  1169.     canvas.style.width = canvas.width + 'px';
  1170.     canvas.style.height = canvas.height + 'px';
  1171.   }
  1172.   return false;
  1173. };
  1174.  
  1175.  
  1176. /**
  1177.  * Get random number.
  1178.  * @param {number} min
  1179.  * @param {number} max
  1180.  */
  1181. function getRandomNum(min, max) {
  1182.   return Math.floor(Math.random() * (max - min + 1)) + min;
  1183. }
  1184.  
  1185.  
  1186. /**
  1187.  * Vibrate on mobile devices.
  1188.  * @param {number} duration Duration of the vibration in milliseconds.
  1189.  */
  1190. function vibrate(duration) {
  1191.   if (IS_MOBILE && window.navigator.vibrate) {
  1192.     window.navigator.vibrate(duration);
  1193.   }
  1194. }
  1195.  
  1196.  
  1197. /**
  1198.  * Create canvas element.
  1199.  * @param {Element} container Element to append canvas to.
  1200.  * @param {number} width
  1201.  * @param {number} height
  1202.  * @param {string=} opt_classname
  1203.  * @return {HTMLCanvasElement}
  1204.  */
  1205. function createCanvas(container, width, height, opt_classname) {
  1206.   const canvas =
  1207.       /** @type {!HTMLCanvasElement} */ (document.createElement('canvas'));
  1208.   canvas.className = opt_classname ? Runner.classes.CANVAS + ' ' +
  1209.       opt_classname : Runner.classes.CANVAS;
  1210.   canvas.width = width;
  1211.   canvas.height = height;
  1212.   container.appendChild(canvas);
  1213.  
  1214.   return canvas;
  1215. }
  1216.  
  1217.  
  1218. /**
  1219.  * Decodes the base 64 audio to ArrayBuffer used by Web Audio.
  1220.  * @param {string} base64String
  1221.  */
  1222. function decodeBase64ToArrayBuffer(base64String) {
  1223.   const len = (base64String.length / 4) * 3;
  1224.   const str = atob(base64String);
  1225.   const arrayBuffer = new ArrayBuffer(len);
  1226.   const bytes = new Uint8Array(arrayBuffer);
  1227.  
  1228.   for (let i = 0; i < len; i++) {
  1229.     bytes[i] = str.charCodeAt(i);
  1230.   }
  1231.   return bytes.buffer;
  1232. }
  1233.  
  1234.  
  1235. /**
  1236.  * Return the current timestamp.
  1237.  * @return {number}
  1238.  */
  1239. function getTimeStamp() {
  1240.   return IS_IOS ? new Date().getTime() : performance.now();
  1241. }
  1242.  
  1243.  
  1244. //******************************************************************************
  1245.  
  1246.  
  1247. /**
  1248.  * Game over panel.
  1249.  * @param {!HTMLCanvasElement} canvas
  1250.  * @param {Object} textImgPos
  1251.  * @param {Object} restartImgPos
  1252.  * @param {!Object} dimensions Canvas dimensions.
  1253.  * @constructor
  1254.  */
  1255. function GameOverPanel(canvas, textImgPos, restartImgPos, dimensions) {
  1256.   this.canvas = canvas;
  1257.   this.canvasCtx =
  1258.       /** @type {CanvasRenderingContext2D} */ (canvas.getContext('2d'));
  1259.   this.canvasDimensions = dimensions;
  1260.   this.textImgPos = textImgPos;
  1261.   this.restartImgPos = restartImgPos;
  1262.   this.draw();
  1263. }
  1264.  
  1265.  
  1266. /**
  1267.  * Dimensions used in the panel.
  1268.  * @enum {number}
  1269.  */
  1270. GameOverPanel.dimensions = {
  1271.   TEXT_X: 0,
  1272.   TEXT_Y: 13,
  1273.   TEXT_WIDTH: 191,
  1274.   TEXT_HEIGHT: 11,
  1275.   RESTART_WIDTH: 36,
  1276.   RESTART_HEIGHT: 32
  1277. };
  1278.  
  1279.  
  1280. GameOverPanel.prototype = {
  1281.   /**
  1282.    * Update the panel dimensions.
  1283.    * @param {number} width New canvas width.
  1284.    * @param {number} opt_height Optional new canvas height.
  1285.    */
  1286.   updateDimensions: function(width, opt_height) {
  1287.     this.canvasDimensions.WIDTH = width;
  1288.     if (opt_height) {
  1289.       this.canvasDimensions.HEIGHT = opt_height;
  1290.     }
  1291.   },
  1292.  
  1293.   /**
  1294.    * Draw the panel.
  1295.    */
  1296.   draw: function() {
  1297.     const dimensions = GameOverPanel.dimensions;
  1298.  
  1299.     const centerX = this.canvasDimensions.WIDTH / 2;
  1300.  
  1301.     // Game over text.
  1302.     let textSourceX = dimensions.TEXT_X;
  1303.     let textSourceY = dimensions.TEXT_Y;
  1304.     let textSourceWidth = dimensions.TEXT_WIDTH;
  1305.     let textSourceHeight = dimensions.TEXT_HEIGHT;
  1306.  
  1307.     const textTargetX = Math.round(centerX - (dimensions.TEXT_WIDTH / 2));
  1308.     const textTargetY = Math.round((this.canvasDimensions.HEIGHT - 25) / 3);
  1309.     const textTargetWidth = dimensions.TEXT_WIDTH;
  1310.     const textTargetHeight = dimensions.TEXT_HEIGHT;
  1311.  
  1312.     let restartSourceWidth = dimensions.RESTART_WIDTH;
  1313.     let restartSourceHeight = dimensions.RESTART_HEIGHT;
  1314.     const restartTargetX = centerX - (dimensions.RESTART_WIDTH / 2);
  1315.     const restartTargetY = this.canvasDimensions.HEIGHT / 2;
  1316.  
  1317.     if (IS_HIDPI) {
  1318.       textSourceY *= 2;
  1319.       textSourceX *= 2;
  1320.       textSourceWidth *= 2;
  1321.       textSourceHeight *= 2;
  1322.       restartSourceWidth *= 2;
  1323.       restartSourceHeight *= 2;
  1324.     }
  1325.  
  1326.     textSourceX += this.textImgPos.x;
  1327.     textSourceY += this.textImgPos.y;
  1328.  
  1329.     // Game over text from sprite.
  1330.     this.canvasCtx.drawImage(Runner.imageSprite,
  1331.         textSourceX, textSourceY, textSourceWidth, textSourceHeight,
  1332.         textTargetX, textTargetY, textTargetWidth, textTargetHeight);
  1333.  
  1334.     // Restart button.
  1335.     this.canvasCtx.drawImage(Runner.imageSprite,
  1336.         this.restartImgPos.x, this.restartImgPos.y,
  1337.         restartSourceWidth, restartSourceHeight,
  1338.         restartTargetX, restartTargetY, dimensions.RESTART_WIDTH,
  1339.         dimensions.RESTART_HEIGHT);
  1340.   }
  1341. };
  1342.  
  1343.  
  1344. //******************************************************************************
  1345.  
  1346. /**
  1347.  * Check for a collision.
  1348.  * @param {!Obstacle} obstacle
  1349.  * @param {!Trex} tRex T-rex object.
  1350.  * @param {CanvasRenderingContext2D=} opt_canvasCtx Optional canvas context for
  1351.  *    drawing collision boxes.
  1352.  * @return {Array<CollisionBox>|undefined}
  1353.  */
  1354. function checkForCollision(obstacle, tRex, opt_canvasCtx) {
  1355.   const obstacleBoxXPos = Runner.defaultDimensions.WIDTH + obstacle.xPos;
  1356.  
  1357.   // Adjustments are made to the bounding box as there is a 1 pixel white
  1358.   // border around the t-rex and obstacles.
  1359.   const tRexBox = new CollisionBox(
  1360.       tRex.xPos + 1,
  1361.       tRex.yPos + 1,
  1362.       tRex.config.WIDTH - 2,
  1363.       tRex.config.HEIGHT - 2);
  1364.  
  1365.   const obstacleBox = new CollisionBox(
  1366.       obstacle.xPos + 1,
  1367.       obstacle.yPos + 1,
  1368.       obstacle.typeConfig.width * obstacle.size - 2,
  1369.       obstacle.typeConfig.height - 2);
  1370.  
  1371.   // Debug outer box
  1372.   if (opt_canvasCtx) {
  1373.     drawCollisionBoxes(opt_canvasCtx, tRexBox, obstacleBox);
  1374.   }
  1375.  
  1376.   // Simple outer bounds check.
  1377.   if (boxCompare(tRexBox, obstacleBox)) {
  1378.     const collisionBoxes = obstacle.collisionBoxes;
  1379.     const tRexCollisionBoxes = tRex.ducking ?
  1380.         Trex.collisionBoxes.DUCKING : Trex.collisionBoxes.RUNNING;
  1381.  
  1382.     // Detailed axis aligned box check.
  1383.     for (let t = 0; t < tRexCollisionBoxes.length; t++) {
  1384.       for (let i = 0; i < collisionBoxes.length; i++) {
  1385.         // Adjust the box to actual positions.
  1386.         const adjTrexBox =
  1387.             createAdjustedCollisionBox(tRexCollisionBoxes[t], tRexBox);
  1388.         const adjObstacleBox =
  1389.             createAdjustedCollisionBox(collisionBoxes[i], obstacleBox);
  1390.         const crashed = boxCompare(adjTrexBox, adjObstacleBox);
  1391.  
  1392.         // Draw boxes for debug.
  1393.         if (opt_canvasCtx) {
  1394.           drawCollisionBoxes(opt_canvasCtx, adjTrexBox, adjObstacleBox);
  1395.         }
  1396.  
  1397.         if (crashed) {
  1398.           return [adjTrexBox, adjObstacleBox];
  1399.         }
  1400.       }
  1401.     }
  1402.   }
  1403. }
  1404.  
  1405.  
  1406. /**
  1407.  * Adjust the collision box.
  1408.  * @param {!CollisionBox} box The original box.
  1409.  * @param {!CollisionBox} adjustment Adjustment box.
  1410.  * @return {CollisionBox} The adjusted collision box object.
  1411.  */
  1412. function createAdjustedCollisionBox(box, adjustment) {
  1413.   return new CollisionBox(
  1414.       box.x + adjustment.x,
  1415.       box.y + adjustment.y,
  1416.       box.width,
  1417.       box.height);
  1418. }
  1419.  
  1420.  
  1421. /**
  1422.  * Draw the collision boxes for debug.
  1423.  */
  1424. function drawCollisionBoxes(canvasCtx, tRexBox, obstacleBox) {
  1425.   canvasCtx.save();
  1426.   canvasCtx.strokeStyle = '#f00';
  1427.   canvasCtx.strokeRect(tRexBox.x, tRexBox.y, tRexBox.width, tRexBox.height);
  1428.  
  1429.   canvasCtx.strokeStyle = '#0f0';
  1430.   canvasCtx.strokeRect(obstacleBox.x, obstacleBox.y,
  1431.       obstacleBox.width, obstacleBox.height);
  1432.   canvasCtx.restore();
  1433. }
  1434.  
  1435.  
  1436. /**
  1437.  * Compare two collision boxes for a collision.
  1438.  * @param {CollisionBox} tRexBox
  1439.  * @param {CollisionBox} obstacleBox
  1440.  * @return {boolean} Whether the boxes intersected.
  1441.  */
  1442. function boxCompare(tRexBox, obstacleBox) {
  1443.   let crashed = false;
  1444.   const tRexBoxX = tRexBox.x;
  1445.   const tRexBoxY = tRexBox.y;
  1446.  
  1447.   const obstacleBoxX = obstacleBox.x;
  1448.   const obstacleBoxY = obstacleBox.y;
  1449.  
  1450.   // Axis-Aligned Bounding Box method.
  1451.   if (tRexBox.x < obstacleBoxX + obstacleBox.width &&
  1452.       tRexBox.x + tRexBox.width > obstacleBoxX &&
  1453.       tRexBox.y < obstacleBox.y + obstacleBox.height &&
  1454.       tRexBox.height + tRexBox.y > obstacleBox.y) {
  1455.     crashed = true;
  1456.   }
  1457.  
  1458.   return crashed;
  1459. }
  1460.  
  1461.  
  1462. //******************************************************************************
  1463.  
  1464. /**
  1465.  * Collision box object.
  1466.  * @param {number} x X position.
  1467.  * @param {number} y Y Position.
  1468.  * @param {number} w Width.
  1469.  * @param {number} h Height.
  1470.  * @constructor
  1471.  */
  1472. function CollisionBox(x, y, w, h) {
  1473.   this.x = x;
  1474.   this.y = y;
  1475.   this.width = w;
  1476.   this.height = h;
  1477. }
  1478.  
  1479.  
  1480. //******************************************************************************
  1481.  
  1482. /**
  1483.  * Obstacle.
  1484.  * @param {CanvasRenderingContext2D} canvasCtx
  1485.  * @param {ObstacleType} type
  1486.  * @param {Object} spriteImgPos Obstacle position in sprite.
  1487.  * @param {Object} dimensions
  1488.  * @param {number} gapCoefficient Mutipler in determining the gap.
  1489.  * @param {number} speed
  1490.  * @param {number=} opt_xOffset
  1491.  * @constructor
  1492.  */
  1493. function Obstacle(canvasCtx, type, spriteImgPos, dimensions,
  1494.     gapCoefficient, speed, opt_xOffset) {
  1495.  
  1496.   this.canvasCtx = canvasCtx;
  1497.   this.spritePos = spriteImgPos;
  1498.   this.typeConfig = type;
  1499.   this.gapCoefficient = gapCoefficient;
  1500.   this.size = getRandomNum(1, Obstacle.MAX_OBSTACLE_LENGTH);
  1501.   this.dimensions = dimensions;
  1502.   this.remove = false;
  1503.   this.xPos = dimensions.WIDTH + (opt_xOffset || 0);
  1504.   this.yPos = 0;
  1505.   this.width = 0;
  1506.   this.collisionBoxes = [];
  1507.   this.gap = 0;
  1508.   this.speedOffset = 0;
  1509.  
  1510.   // For animated obstacles.
  1511.   this.currentFrame = 0;
  1512.   this.timer = 0;
  1513.  
  1514.   this.init(speed);
  1515. }
  1516.  
  1517. /**
  1518.  * Coefficient for calculating the maximum gap.
  1519.  * @const
  1520.  */
  1521. Obstacle.MAX_GAP_COEFFICIENT = 1.5;
  1522.  
  1523. /**
  1524.  * Maximum obstacle grouping count.
  1525.  * @const
  1526.  */
  1527. Obstacle.MAX_OBSTACLE_LENGTH = 3,
  1528.  
  1529.  
  1530. Obstacle.prototype = {
  1531.   /**
  1532.    * Initialise the DOM for the obstacle.
  1533.    * @param {number} speed
  1534.    */
  1535.   init: function(speed) {
  1536.     this.cloneCollisionBoxes();
  1537.  
  1538.     // Only allow sizing if we're at the right speed.
  1539.     if (this.size > 1 && this.typeConfig.multipleSpeed > speed) {
  1540.       this.size = 1;
  1541.     }
  1542.  
  1543.     this.width = this.typeConfig.width * this.size;
  1544.  
  1545.     // Check if obstacle can be positioned at various heights.
  1546.     if (Array.isArray(this.typeConfig.yPos))  {
  1547.       const yPosConfig = IS_MOBILE ? this.typeConfig.yPosMobile :
  1548.           this.typeConfig.yPos;
  1549.       this.yPos = yPosConfig[getRandomNum(0, yPosConfig.length - 1)];
  1550.     } else {
  1551.       this.yPos = this.typeConfig.yPos;
  1552.     }
  1553.  
  1554.     this.draw();
  1555.  
  1556.     // Make collision box adjustments,
  1557.     // Central box is adjusted to the size as one box.
  1558.     //      ____        ______        ________
  1559.     //    _|   |-|    _|     |-|    _|       |-|
  1560.     //   | |<->| |   | |<--->| |   | |<----->| |
  1561.     //   | | 1 | |   | |  2  | |   | |   3   | |
  1562.     //   |_|___|_|   |_|_____|_|   |_|_______|_|
  1563.     //
  1564.     if (this.size > 1) {
  1565.       this.collisionBoxes[1].width = this.width - this.collisionBoxes[0].width -
  1566.           this.collisionBoxes[2].width;
  1567.       this.collisionBoxes[2].x = this.width - this.collisionBoxes[2].width;
  1568.     }
  1569.  
  1570.     // For obstacles that go at a different speed from the horizon.
  1571.     if (this.typeConfig.speedOffset) {
  1572.       this.speedOffset = Math.random() > 0.5 ? this.typeConfig.speedOffset :
  1573.           -this.typeConfig.speedOffset;
  1574.     }
  1575.  
  1576.     this.gap = this.getGap(this.gapCoefficient, speed);
  1577.   },
  1578.  
  1579.   /**
  1580.    * Draw and crop based on size.
  1581.    */
  1582.   draw: function() {
  1583.     let sourceWidth = this.typeConfig.width;
  1584.     let sourceHeight = this.typeConfig.height;
  1585.  
  1586.     if (IS_HIDPI) {
  1587.       sourceWidth = sourceWidth * 2;
  1588.       sourceHeight = sourceHeight * 2;
  1589.     }
  1590.  
  1591.     // X position in sprite.
  1592.     let sourceX = (sourceWidth * this.size) * (0.5 * (this.size - 1)) +
  1593.         this.spritePos.x;
  1594.  
  1595.     // Animation frames.
  1596.     if (this.currentFrame > 0) {
  1597.       sourceX += sourceWidth * this.currentFrame;
  1598.     }
  1599.  
  1600.     this.canvasCtx.drawImage(Runner.imageSprite,
  1601.       sourceX, this.spritePos.y,
  1602.       sourceWidth * this.size, sourceHeight,
  1603.       this.xPos, this.yPos,
  1604.       this.typeConfig.width * this.size, this.typeConfig.height);
  1605.   },
  1606.  
  1607.   /**
  1608.    * Obstacle frame update.
  1609.    * @param {number} deltaTime
  1610.    * @param {number} speed
  1611.    */
  1612.   update: function(deltaTime, speed) {
  1613.     if (!this.remove) {
  1614.       if (this.typeConfig.speedOffset) {
  1615.         speed += this.speedOffset;
  1616.       }
  1617.       this.xPos -= Math.floor((speed * FPS / 1000) * deltaTime);
  1618.  
  1619.       // Update frame
  1620.       if (this.typeConfig.numFrames) {
  1621.         this.timer += deltaTime;
  1622.         if (this.timer >= this.typeConfig.frameRate) {
  1623.           this.currentFrame =
  1624.               this.currentFrame == this.typeConfig.numFrames - 1 ?
  1625.               0 : this.currentFrame + 1;
  1626.           this.timer = 0;
  1627.         }
  1628.       }
  1629.       this.draw();
  1630.  
  1631.       if (!this.isVisible()) {
  1632.         this.remove = true;
  1633.       }
  1634.     }
  1635.   },
  1636.  
  1637.   /**
  1638.    * Calculate a random gap size.
  1639.    * - Minimum gap gets wider as speed increses
  1640.    * @param {number} gapCoefficient
  1641.    * @param {number} speed
  1642.    * @return {number} The gap size.
  1643.    */
  1644.   getGap: function(gapCoefficient, speed) {
  1645.     const minGap = Math.round(this.width * speed +
  1646.           this.typeConfig.minGap * gapCoefficient);
  1647.     const maxGap = Math.round(minGap * Obstacle.MAX_GAP_COEFFICIENT);
  1648.     return getRandomNum(minGap, maxGap);
  1649.   },
  1650.  
  1651.   /**
  1652.    * Check if obstacle is visible.
  1653.    * @return {boolean} Whether the obstacle is in the game area.
  1654.    */
  1655.   isVisible: function() {
  1656.     return this.xPos + this.width > 0;
  1657.   },
  1658.  
  1659.   /**
  1660.    * Make a copy of the collision boxes, since these will change based on
  1661.    * obstacle type and size.
  1662.    */
  1663.   cloneCollisionBoxes: function() {
  1664.     const collisionBoxes = this.typeConfig.collisionBoxes;
  1665.  
  1666.     for (let i = collisionBoxes.length - 1; i >= 0; i--) {
  1667.       this.collisionBoxes[i] = new CollisionBox(collisionBoxes[i].x,
  1668.           collisionBoxes[i].y, collisionBoxes[i].width,
  1669.           collisionBoxes[i].height);
  1670.     }
  1671.   }
  1672. };
  1673.  
  1674. /**
  1675.  * Obstacle definitions.
  1676.  * minGap: minimum pixel space betweeen obstacles.
  1677.  * multipleSpeed: Speed at which multiples are allowed.
  1678.  * speedOffset: speed faster / slower than the horizon.
  1679.  * minSpeed: Minimum speed which the obstacle can make an appearance.
  1680.  *
  1681.  * @typedef {{
  1682.  *   type: string,
  1683.  *   width: number,
  1684.  *   height: number,
  1685.  *   yPos: number,
  1686.  *   multipleSpeed: number,
  1687.  *   minGap: number,
  1688.  *   minSpeed: number,
  1689.  *   collisionBoxes: Array<CollisionBox>,
  1690.  * }}
  1691.  */
  1692. let ObstacleType;
  1693.  
  1694. /** @type {Array<ObstacleType>} */
  1695. Obstacle.types = [
  1696.   {
  1697.     type: 'CACTUS_SMALL',
  1698.     width: 17,
  1699.     height: 35,
  1700.     yPos: 105,
  1701.     multipleSpeed: 4,
  1702.     minGap: 120,
  1703.     minSpeed: 0,
  1704.     collisionBoxes: [
  1705.       new CollisionBox(0, 7, 5, 27),
  1706.       new CollisionBox(4, 0, 6, 34),
  1707.       new CollisionBox(10, 4, 7, 14)
  1708.     ]
  1709.   },
  1710.   {
  1711.     type: 'CACTUS_LARGE',
  1712.     width: 25,
  1713.     height: 50,
  1714.     yPos: 90,
  1715.     multipleSpeed: 7,
  1716.     minGap: 120,
  1717.     minSpeed: 0,
  1718.     collisionBoxes: [
  1719.       new CollisionBox(0, 12, 7, 38),
  1720.       new CollisionBox(8, 0, 7, 49),
  1721.       new CollisionBox(13, 10, 10, 38)
  1722.     ]
  1723.   },
  1724.   {
  1725.     type: 'PTERODACTYL',
  1726.     width: 46,
  1727.     height: 40,
  1728.     yPos: [ 100, 75, 50 ], // Variable height.
  1729.     yPosMobile: [ 100, 50 ], // Variable height mobile.
  1730.     multipleSpeed: 999,
  1731.     minSpeed: 8.5,
  1732.     minGap: 150,
  1733.     collisionBoxes: [
  1734.       new CollisionBox(15, 15, 16, 5),
  1735.       new CollisionBox(18, 21, 24, 6),
  1736.       new CollisionBox(2, 14, 4, 3),
  1737.       new CollisionBox(6, 10, 4, 7),
  1738.       new CollisionBox(10, 8, 6, 9)
  1739.     ],
  1740.     numFrames: 2,
  1741.     frameRate: 1000/6,
  1742.     speedOffset: .8
  1743.   }
  1744. ];
  1745.  
  1746.  
  1747. //******************************************************************************
  1748. /**
  1749.  * T-rex game character.
  1750.  * @param {HTMLCanvasElement} canvas
  1751.  * @param {Object} spritePos Positioning within image sprite.
  1752.  * @constructor
  1753.  */
  1754. function Trex(canvas, spritePos) {
  1755.   this.canvas = canvas;
  1756.   this.canvasCtx =
  1757.       /** @type {CanvasRenderingContext2D} */ (canvas.getContext('2d'));
  1758.   this.spritePos = spritePos;
  1759.   this.xPos = 0;
  1760.   this.yPos = 0;
  1761.   this.xInitialPos = 0;
  1762.   // Position when on the ground.
  1763.   this.groundYPos = 0;
  1764.   this.currentFrame = 0;
  1765.   this.currentAnimFrames = [];
  1766.   this.blinkDelay = 0;
  1767.   this.blinkCount = 0;
  1768.   this.animStartTime = 0;
  1769.   this.timer = 0;
  1770.   this.msPerFrame = 1000 / FPS;
  1771.   this.config = Trex.config;
  1772.   // Current status.
  1773.   this.status = Trex.status.WAITING;
  1774.   this.jumping = false;
  1775.   this.ducking = false;
  1776.   this.jumpVelocity = 0;
  1777.   this.reachedMinHeight = false;
  1778.   this.speedDrop = false;
  1779.   this.jumpCount = 0;
  1780.   this.jumpspotX = 0;
  1781.  
  1782.   this.init();
  1783. }
  1784.  
  1785.  
  1786. /**
  1787.  * T-rex player config.
  1788.  */
  1789. Trex.config = {
  1790.   DROP_VELOCITY: -5,
  1791.   GRAVITY: 0.6,
  1792.   HEIGHT: 47,
  1793.   HEIGHT_DUCK: 25,
  1794.   INIITAL_JUMP_VELOCITY: -10,
  1795.   INTRO_DURATION: 1500,
  1796.   MAX_JUMP_HEIGHT: 30,
  1797.   MIN_JUMP_HEIGHT: 30,
  1798.   SPEED_DROP_COEFFICIENT: 3,
  1799.   SPRITE_WIDTH: 262,
  1800.   START_X_POS: 50,
  1801.   WIDTH: 44,
  1802.   WIDTH_DUCK: 59
  1803. };
  1804.  
  1805.  
  1806. /**
  1807.  * Used in collision detection.
  1808.  * @enum {Array<CollisionBox>}
  1809.  */
  1810. Trex.collisionBoxes = {
  1811.   DUCKING: [
  1812.     new CollisionBox(1, 18, 55, 25)
  1813.   ],
  1814.   RUNNING: [
  1815.     new CollisionBox(22, 0, 17, 16),
  1816.     new CollisionBox(1, 18, 30, 9),
  1817.     new CollisionBox(10, 35, 14, 8),
  1818.     new CollisionBox(1, 24, 29, 5),
  1819.     new CollisionBox(5, 30, 21, 4),
  1820.     new CollisionBox(9, 34, 15, 4)
  1821.   ]
  1822. };
  1823.  
  1824.  
  1825. /**
  1826.  * Animation states.
  1827.  * @enum {string}
  1828.  */
  1829. Trex.status = {
  1830.   CRASHED: 'CRASHED',
  1831.   DUCKING: 'DUCKING',
  1832.   JUMPING: 'JUMPING',
  1833.   RUNNING: 'RUNNING',
  1834.   WAITING: 'WAITING'
  1835. };
  1836.  
  1837. /**
  1838.  * Blinking coefficient.
  1839.  * @const
  1840.  */
  1841. Trex.BLINK_TIMING = 7000;
  1842.  
  1843.  
  1844. /**
  1845.  * Animation config for different states.
  1846.  * @enum {Object}
  1847.  */
  1848. Trex.animFrames = {
  1849.   WAITING: {
  1850.     frames: [44, 0],
  1851.     msPerFrame: 1000 / 3
  1852.   },
  1853.   RUNNING: {
  1854.     frames: [88, 132],
  1855.     msPerFrame: 1000 / 12
  1856.   },
  1857.   CRASHED: {
  1858.     frames: [220],
  1859.     msPerFrame: 1000 / 60
  1860.   },
  1861.   JUMPING: {
  1862.     frames: [0],
  1863.     msPerFrame: 1000 / 60
  1864.   },
  1865.   DUCKING: {
  1866.     frames: [264, 323],
  1867.     msPerFrame: 1000 / 8
  1868.   }
  1869. };
  1870.  
  1871.  
  1872. Trex.prototype = {
  1873.   /**
  1874.    * T-rex player initaliser.
  1875.    * Sets the t-rex to blink at random intervals.
  1876.    */
  1877.   init: function() {
  1878.     this.groundYPos = Runner.defaultDimensions.HEIGHT - this.config.HEIGHT -
  1879.         Runner.config.BOTTOM_PAD;
  1880.     this.yPos = this.groundYPos;
  1881.     this.minJumpHeight = this.groundYPos - this.config.MIN_JUMP_HEIGHT;
  1882.  
  1883.     this.draw(0, 0);
  1884.     this.update(0, Trex.status.WAITING);
  1885.   },
  1886.  
  1887.   /**
  1888.    * Setter for the jump velocity.
  1889.    * The approriate drop velocity is also set.
  1890.    * @param {number} setting
  1891.    */
  1892.   setJumpVelocity: function(setting) {
  1893.     this.config.INIITAL_JUMP_VELOCITY = -setting;
  1894.     this.config.DROP_VELOCITY = -setting / 2;
  1895.   },
  1896.  
  1897.   /**
  1898.    * Set the animation status.
  1899.    * @param {!number} deltaTime
  1900.    * @param {Trex.status=} opt_status Optional status to switch to.
  1901.    */
  1902.   update: function(deltaTime, opt_status) {
  1903.     this.timer += deltaTime;
  1904.  
  1905.     // Update the status.
  1906.     if (opt_status) {
  1907.       this.status = opt_status;
  1908.       this.currentFrame = 0;
  1909.       this.msPerFrame = Trex.animFrames[opt_status].msPerFrame;
  1910.       this.currentAnimFrames = Trex.animFrames[opt_status].frames;
  1911.  
  1912.       if (opt_status == Trex.status.WAITING) {
  1913.         this.animStartTime = getTimeStamp();
  1914.         this.setBlinkDelay();
  1915.       }
  1916.     }
  1917.  
  1918.     // Game intro animation, T-rex moves in from the left.
  1919.     if (this.playingIntro && this.xPos < this.config.START_X_POS) {
  1920.       this.xPos += Math.round((this.config.START_X_POS /
  1921.           this.config.INTRO_DURATION) * deltaTime);
  1922.       this.xInitialPos = this.xPos;
  1923.     }
  1924.  
  1925.     if (this.status == Trex.status.WAITING) {
  1926.       this.blink(getTimeStamp());
  1927.     } else {
  1928.       this.draw(this.currentAnimFrames[this.currentFrame], 0);
  1929.     }
  1930.  
  1931.     // Update the frame position.
  1932.     if (this.timer >= this.msPerFrame) {
  1933.       this.currentFrame = this.currentFrame ==
  1934.           this.currentAnimFrames.length - 1 ? 0 : this.currentFrame + 1;
  1935.       this.timer = 0;
  1936.     }
  1937.  
  1938.     // Speed drop becomes duck if the down key is still being pressed.
  1939.     if (this.speedDrop && this.yPos == this.groundYPos) {
  1940.       this.speedDrop = false;
  1941.       this.setDuck(true);
  1942.     }
  1943.   },
  1944.  
  1945.   /**
  1946.    * Draw the t-rex to a particular position.
  1947.    * @param {number} x
  1948.    * @param {number} y
  1949.    */
  1950.   draw: function(x, y) {
  1951.     let sourceX = x;
  1952.     let sourceY = y;
  1953.     let sourceWidth = this.ducking && this.status != Trex.status.CRASHED ?
  1954.         this.config.WIDTH_DUCK : this.config.WIDTH;
  1955.     let sourceHeight = this.config.HEIGHT;
  1956.     const outputHeight = sourceHeight;
  1957.  
  1958.     if (IS_HIDPI) {
  1959.       sourceX *= 2;
  1960.       sourceY *= 2;
  1961.       sourceWidth *= 2;
  1962.       sourceHeight *= 2;
  1963.     }
  1964.  
  1965.     // Adjustments for sprite sheet position.
  1966.     sourceX += this.spritePos.x;
  1967.     sourceY += this.spritePos.y;
  1968.  
  1969.     // Ducking.
  1970.     if (this.ducking && this.status != Trex.status.CRASHED) {
  1971.       this.canvasCtx.drawImage(Runner.imageSprite, sourceX, sourceY,
  1972.           sourceWidth, sourceHeight,
  1973.           this.xPos, this.yPos,
  1974.           this.config.WIDTH_DUCK, outputHeight);
  1975.     } else {
  1976.       // Crashed whilst ducking. Trex is standing up so needs adjustment.
  1977.       if (this.ducking && this.status == Trex.status.CRASHED) {
  1978.         this.xPos++;
  1979.       }
  1980.       // Standing / running
  1981.       this.canvasCtx.drawImage(Runner.imageSprite, sourceX, sourceY,
  1982.           sourceWidth, sourceHeight,
  1983.           this.xPos, this.yPos,
  1984.           this.config.WIDTH, outputHeight);
  1985.     }
  1986.     this.canvasCtx.globalAlpha = 1;
  1987.   },
  1988.  
  1989.   /**
  1990.    * Sets a random time for the blink to happen.
  1991.    */
  1992.   setBlinkDelay: function() {
  1993.     this.blinkDelay = Math.ceil(Math.random() * Trex.BLINK_TIMING);
  1994.   },
  1995.  
  1996.   /**
  1997.    * Make t-rex blink at random intervals.
  1998.    * @param {number} time Current time in milliseconds.
  1999.    */
  2000.   blink: function(time) {
  2001.     const deltaTime = time - this.animStartTime;
  2002.  
  2003.     if (deltaTime >= this.blinkDelay) {
  2004.       this.draw(this.currentAnimFrames[this.currentFrame], 0);
  2005.  
  2006.       if (this.currentFrame == 1) {
  2007.         // Set new random delay to blink.
  2008.         this.setBlinkDelay();
  2009.         this.animStartTime = time;
  2010.         this.blinkCount++;
  2011.       }
  2012.     }
  2013.   },
  2014.  
  2015.   /**
  2016.    * Initialise a jump.
  2017.    * @param {number} speed
  2018.    */
  2019.   startJump: function(speed) {
  2020.     if (!this.jumping) {
  2021.       this.update(0, Trex.status.JUMPING);
  2022.       // Tweak the jump velocity based on the speed.
  2023.       this.jumpVelocity = this.config.INIITAL_JUMP_VELOCITY - (speed / 10);
  2024.       this.jumping = true;
  2025.       this.reachedMinHeight = false;
  2026.       this.speedDrop = false;
  2027.     }
  2028.   },
  2029.  
  2030.   /**
  2031.    * Jump is complete, falling down.
  2032.    */
  2033.   endJump: function() {
  2034.     if (this.reachedMinHeight &&
  2035.         this.jumpVelocity < this.config.DROP_VELOCITY) {
  2036.       this.jumpVelocity = this.config.DROP_VELOCITY;
  2037.     }
  2038.   },
  2039.  
  2040.   /**
  2041.    * Update frame for a jump.
  2042.    * @param {number} deltaTime
  2043.    */
  2044.   updateJump: function(deltaTime) {
  2045.     const msPerFrame = Trex.animFrames[this.status].msPerFrame;
  2046.     const framesElapsed = deltaTime / msPerFrame;
  2047.  
  2048.     // Speed drop makes Trex fall faster.
  2049.     if (this.speedDrop) {
  2050.       this.yPos += Math.round(this.jumpVelocity *
  2051.           this.config.SPEED_DROP_COEFFICIENT * framesElapsed);
  2052.     } else {
  2053.       this.yPos += Math.round(this.jumpVelocity * framesElapsed);
  2054.     }
  2055.  
  2056.     this.jumpVelocity += this.config.GRAVITY * framesElapsed;
  2057.  
  2058.     // Minimum height has been reached.
  2059.     if (this.yPos < this.minJumpHeight || this.speedDrop) {
  2060.       this.reachedMinHeight = true;
  2061.     }
  2062.  
  2063.     // Reached max height
  2064.     if (this.yPos < this.config.MAX_JUMP_HEIGHT || this.speedDrop) {
  2065.       this.endJump();
  2066.     }
  2067.  
  2068.     // Back down at ground level. Jump completed.
  2069.     if (this.yPos > this.groundYPos) {
  2070.       this.reset();
  2071.       this.jumpCount++;
  2072.     }
  2073.   },
  2074.  
  2075.   /**
  2076.    * Set the speed drop. Immediately cancels the current jump.
  2077.    */
  2078.   setSpeedDrop: function() {
  2079.     this.speedDrop = true;
  2080.     this.jumpVelocity = 1;
  2081.   },
  2082.  
  2083.   /**
  2084.    * @param {boolean} isDucking
  2085.    */
  2086.   setDuck: function(isDucking) {
  2087.     if (isDucking && this.status != Trex.status.DUCKING) {
  2088.       this.update(0, Trex.status.DUCKING);
  2089.       this.ducking = true;
  2090.     } else if (this.status == Trex.status.DUCKING) {
  2091.       this.update(0, Trex.status.RUNNING);
  2092.       this.ducking = false;
  2093.     }
  2094.   },
  2095.  
  2096.   /**
  2097.    * Reset the t-rex to running at start of game.
  2098.    */
  2099.   reset: function() {
  2100.     this.xPos = this.xInitialPos;
  2101.     this.yPos = this.groundYPos;
  2102.     this.jumpVelocity = 0;
  2103.     this.jumping = false;
  2104.     this.ducking = false;
  2105.     this.update(0, Trex.status.RUNNING);
  2106.     this.midair = false;
  2107.     this.speedDrop = false;
  2108.     this.jumpCount = 0;
  2109.   }
  2110. };
  2111.  
  2112.  
  2113. //******************************************************************************
  2114.  
  2115. /**
  2116.  * Handles displaying the distance meter.
  2117.  * @param {!HTMLCanvasElement} canvas
  2118.  * @param {Object} spritePos Image position in sprite.
  2119.  * @param {number} canvasWidth
  2120.  * @constructor
  2121.  */
  2122. function DistanceMeter(canvas, spritePos, canvasWidth) {
  2123.   this.canvas = canvas;
  2124.   this.canvasCtx =
  2125.       /** @type {CanvasRenderingContext2D} */ (canvas.getContext('2d'));
  2126.   this.image = Runner.imageSprite;
  2127.   this.spritePos = spritePos;
  2128.   this.x = 0;
  2129.   this.y = 5;
  2130.  
  2131.   this.currentDistance = 0;
  2132.   this.maxScore = 0;
  2133.   this.highScore = '0';
  2134.   this.container = null;
  2135.  
  2136.   this.digits = [];
  2137.   this.achievement = false;
  2138.   this.defaultString = '';
  2139.   this.flashTimer = 0;
  2140.   this.flashIterations = 0;
  2141.   this.invertTrigger = false;
  2142.   this.flashingRafId = null;
  2143.   this.highScoreBounds = {};
  2144.   this.highScoreFlashing = false;
  2145.  
  2146.   this.config = DistanceMeter.config;
  2147.   this.maxScoreUnits = this.config.MAX_DISTANCE_UNITS;
  2148.   this.init(canvasWidth);
  2149. }
  2150.  
  2151.  
  2152. /**
  2153.  * @enum {number}
  2154.  */
  2155. DistanceMeter.dimensions = {
  2156.   WIDTH: 10,
  2157.   HEIGHT: 13,
  2158.   DEST_WIDTH: 11
  2159. };
  2160.  
  2161.  
  2162. /**
  2163.  * Y positioning of the digits in the sprite sheet.
  2164.  * X position is always 0.
  2165.  * @type {Array<number>}
  2166.  */
  2167. DistanceMeter.yPos = [0, 13, 27, 40, 53, 67, 80, 93, 107, 120];
  2168.  
  2169.  
  2170. /**
  2171.  * Distance meter config.
  2172.  * @enum {number}
  2173.  */
  2174. DistanceMeter.config = {
  2175.   // Number of digits.
  2176.   MAX_DISTANCE_UNITS: 5,
  2177.  
  2178.   // Distance that causes achievement animation.
  2179.   ACHIEVEMENT_DISTANCE: 100,
  2180.  
  2181.   // Used for conversion from pixel distance to a scaled unit.
  2182.   COEFFICIENT: 0.025,
  2183.  
  2184.   // Flash duration in milliseconds.
  2185.   FLASH_DURATION: 1000 / 4,
  2186.  
  2187.   // Flash iterations for achievement animation.
  2188.   FLASH_ITERATIONS: 3,
  2189.  
  2190.   // Padding around the high score hit area.
  2191.   HIGH_SCORE_HIT_AREA_PADDING: 4
  2192. };
  2193.  
  2194.  
  2195. DistanceMeter.prototype = {
  2196.   /**
  2197.    * Initialise the distance meter to '00000'.
  2198.    * @param {number} width Canvas width in px.
  2199.    */
  2200.   init: function(width) {
  2201.     let maxDistanceStr = '';
  2202.  
  2203.     this.calcXPos(width);
  2204.     this.maxScore = this.maxScoreUnits;
  2205.     for (let i = 0; i < this.maxScoreUnits; i++) {
  2206.       this.draw(i, 0);
  2207.       this.defaultString += '0';
  2208.       maxDistanceStr += '9';
  2209.     }
  2210.  
  2211.     this.maxScore = parseInt(maxDistanceStr, 10);
  2212.   },
  2213.  
  2214.   /**
  2215.    * Calculate the xPos in the canvas.
  2216.    * @param {number} canvasWidth
  2217.    */
  2218.   calcXPos: function(canvasWidth) {
  2219.     this.x = canvasWidth - (DistanceMeter.dimensions.DEST_WIDTH *
  2220.         (this.maxScoreUnits + 1));
  2221.   },
  2222.  
  2223.   /**
  2224.    * Draw a digit to canvas.
  2225.    * @param {number} digitPos Position of the digit.
  2226.    * @param {number} value Digit value 0-9.
  2227.    * @param {boolean=} opt_highScore Whether drawing the high score.
  2228.    */
  2229.   draw: function(digitPos, value, opt_highScore) {
  2230.     let sourceWidth = DistanceMeter.dimensions.WIDTH;
  2231.     let sourceHeight = DistanceMeter.dimensions.HEIGHT;
  2232.     let sourceX = DistanceMeter.dimensions.WIDTH * value;
  2233.     let sourceY = 0;
  2234.  
  2235.     const targetX = digitPos * DistanceMeter.dimensions.DEST_WIDTH;
  2236.     const targetY = this.y;
  2237.     const targetWidth = DistanceMeter.dimensions.WIDTH;
  2238.     const targetHeight = DistanceMeter.dimensions.HEIGHT;
  2239.  
  2240.     // For high DPI we 2x source values.
  2241.     if (IS_HIDPI) {
  2242.       sourceWidth *= 2;
  2243.       sourceHeight *= 2;
  2244.       sourceX *= 2;
  2245.     }
  2246.  
  2247.     sourceX += this.spritePos.x;
  2248.     sourceY += this.spritePos.y;
  2249.  
  2250.     this.canvasCtx.save();
  2251.  
  2252.     if (opt_highScore) {
  2253.       // Left of the current score.
  2254.       const highScoreX = this.x - (this.maxScoreUnits * 2) *
  2255.           DistanceMeter.dimensions.WIDTH;
  2256.       this.canvasCtx.translate(highScoreX, this.y);
  2257.     } else {
  2258.       this.canvasCtx.translate(this.x, this.y);
  2259.     }
  2260.  
  2261.     this.canvasCtx.drawImage(this.image, sourceX, sourceY,
  2262.         sourceWidth, sourceHeight,
  2263.         targetX, targetY,
  2264.         targetWidth, targetHeight
  2265.       );
  2266.  
  2267.     this.canvasCtx.restore();
  2268.   },
  2269.  
  2270.   /**
  2271.    * Covert pixel distance to a 'real' distance.
  2272.    * @param {number} distance Pixel distance ran.
  2273.    * @return {number} The 'real' distance ran.
  2274.    */
  2275.   getActualDistance: function(distance) {
  2276.     return distance ? Math.round(distance * this.config.COEFFICIENT) : 0;
  2277.   },
  2278.  
  2279.   /**
  2280.    * Update the distance meter.
  2281.    * @param {number} distance
  2282.    * @param {number} deltaTime
  2283.    * @return {boolean} Whether the acheivement sound fx should be played.
  2284.    */
  2285.   update: function(deltaTime, distance) {
  2286.     let paint = true;
  2287.     let playSound = false;
  2288.  
  2289.     if (!this.achievement) {
  2290.       distance = this.getActualDistance(distance);
  2291.       // Score has gone beyond the initial digit count.
  2292.       if (distance > this.maxScore && this.maxScoreUnits ==
  2293.         this.config.MAX_DISTANCE_UNITS) {
  2294.         this.maxScoreUnits++;
  2295.         this.maxScore = parseInt(this.maxScore + '9', 10);
  2296.       } else {
  2297.         this.distance = 0;
  2298.       }
  2299.  
  2300.       if (distance > 0) {
  2301.         // Acheivement unlocked
  2302.         if (distance % this.config.ACHIEVEMENT_DISTANCE == 0) {
  2303.           // Flash score and play sound.
  2304.           this.achievement = true;
  2305.           this.flashTimer = 0;
  2306.           playSound = true;
  2307.         }
  2308.  
  2309.         // Create a string representation of the distance with leading 0.
  2310.         const distanceStr = (this.defaultString +
  2311.             distance).substr(-this.maxScoreUnits);
  2312.         this.digits = distanceStr.split('');
  2313.       } else {
  2314.         this.digits = this.defaultString.split('');
  2315.       }
  2316.     } else {
  2317.       // Control flashing of the score on reaching acheivement.
  2318.       if (this.flashIterations <= this.config.FLASH_ITERATIONS) {
  2319.         this.flashTimer += deltaTime;
  2320.  
  2321.         if (this.flashTimer < this.config.FLASH_DURATION) {
  2322.           paint = false;
  2323.         } else if (this.flashTimer >
  2324.             this.config.FLASH_DURATION * 2) {
  2325.           this.flashTimer = 0;
  2326.           this.flashIterations++;
  2327.         }
  2328.       } else {
  2329.         this.achievement = false;
  2330.         this.flashIterations = 0;
  2331.         this.flashTimer = 0;
  2332.       }
  2333.     }
  2334.  
  2335.     // Draw the digits if not flashing.
  2336.     if (paint) {
  2337.       for (let i = this.digits.length - 1; i >= 0; i--) {
  2338.         this.draw(i, parseInt(this.digits[i], 10));
  2339.       }
  2340.     }
  2341.  
  2342.     this.drawHighScore();
  2343.     return playSound;
  2344.   },
  2345.  
  2346.   /**
  2347.    * Draw the high score.
  2348.    */
  2349.   drawHighScore: function() {
  2350.     this.canvasCtx.save();
  2351.     this.canvasCtx.globalAlpha = .8;
  2352.     for (let i = this.highScore.length - 1; i >= 0; i--) {
  2353.       this.draw(i, parseInt(this.highScore[i], 10), true);
  2354.     }
  2355.     this.canvasCtx.restore();
  2356.   },
  2357.  
  2358.   /**
  2359.    * Set the highscore as a array string.
  2360.    * Position of char in the sprite: H - 10, I - 11.
  2361.    * @param {number} distance Distance ran in pixels.
  2362.    */
  2363.   setHighScore: function(distance) {
  2364.     distance = this.getActualDistance(distance);
  2365.     const highScoreStr = (this.defaultString +
  2366.         distance).substr(-this.maxScoreUnits);
  2367.  
  2368.     this.highScore = ['10', '11', ''].concat(highScoreStr.split(''));
  2369.   },
  2370.  
  2371.  
  2372.   /**
  2373.    * Whether a clicked is in the high score area.
  2374.    * @param {Event} e Event object.
  2375.    * @return {boolean} Whether the click was in the high score bounds.
  2376.    */
  2377.   hasClickedOnHighScore: function(e) {
  2378.     let x = 0;
  2379.     let y = 0;
  2380.  
  2381.     if (e.touches) {
  2382.       // Bounds for touch differ from pointer.
  2383.       const canvasBounds = this.canvas.getBoundingClientRect();
  2384.       x = e.touches[0].clientX - canvasBounds.left;
  2385.       y = e.touches[0].clientY - canvasBounds.top;
  2386.     } else {
  2387.       x = e.offsetX;
  2388.       y = e.offsetY;
  2389.     }
  2390.  
  2391.     this.highScoreBounds = this.getHighScoreBounds();
  2392.     return x >= this.highScoreBounds.x && x <=
  2393.         this.highScoreBounds.x + this.highScoreBounds.width &&
  2394.         y >= this.highScoreBounds.y && y <=
  2395.         this.highScoreBounds.y + this.highScoreBounds.height;
  2396.   },
  2397.  
  2398.   /**
  2399.    * Get the bounding box for the high score.
  2400.    * @return {Object} Object with x, y, width and height properties.
  2401.    */
  2402.   getHighScoreBounds: function() {
  2403.     return {
  2404.       x: (this.x - (this.maxScoreUnits * 2) *
  2405.           DistanceMeter.dimensions.WIDTH) -
  2406.           DistanceMeter.config.HIGH_SCORE_HIT_AREA_PADDING,
  2407.       y: this.y,
  2408.       width: DistanceMeter.dimensions.WIDTH * (this.highScore.length + 1) +
  2409.           DistanceMeter.config.HIGH_SCORE_HIT_AREA_PADDING,
  2410.       height: DistanceMeter.dimensions.HEIGHT +
  2411.           (DistanceMeter.config.HIGH_SCORE_HIT_AREA_PADDING * 2)
  2412.     };
  2413.   },
  2414.  
  2415.   /**
  2416.    * Animate flashing the high score to indicate ready for resetting.
  2417.    * The flashing stops following this.config.FLASH_ITERATIONS x 2 flashes.
  2418.    */
  2419.   flashHighScore: function() {
  2420.     const now = getTimeStamp();
  2421.     const deltaTime = now - (this.frameTimeStamp || now);
  2422.     let paint = true;
  2423.     this.frameTimeStamp = now;
  2424.  
  2425.     // Reached the max number of flashes.
  2426.     if (this.flashIterations > this.config.FLASH_ITERATIONS * 2) {
  2427.       this.cancelHighScoreFlashing();
  2428.       return;
  2429.     }
  2430.  
  2431.     this.flashTimer += deltaTime;
  2432.  
  2433.     if (this.flashTimer < this.config.FLASH_DURATION) {
  2434.       paint = false;
  2435.     } else if (this.flashTimer > this.config.FLASH_DURATION * 2) {
  2436.       this.flashTimer = 0;
  2437.       this.flashIterations++;
  2438.     }
  2439.  
  2440.     if (paint) {
  2441.       this.drawHighScore();
  2442.     } else {
  2443.       this.clearHighScoreBounds();
  2444.     }
  2445.     // Frame update.
  2446.     this.flashingRafId =
  2447.         requestAnimationFrame(this.flashHighScore.bind(this));
  2448.   },
  2449.  
  2450.   /**
  2451.    * Draw empty rectangle over high score.
  2452.    */
  2453.   clearHighScoreBounds: function() {
  2454.     this.canvasCtx.save();
  2455.     this.canvasCtx.fillStyle = '#fff';
  2456.     this.canvasCtx.rect(this.highScoreBounds.x, this.highScoreBounds.y,
  2457.         this.highScoreBounds.width, this.highScoreBounds.height);
  2458.     this.canvasCtx.fill();
  2459.     this.canvasCtx.restore();
  2460.   },
  2461.  
  2462.   /**
  2463.    * Starts the flashing of the high score.
  2464.    */
  2465.   startHighScoreFlashing() {
  2466.     this.highScoreFlashing = true;
  2467.     this.flashHighScore();
  2468.   },
  2469.  
  2470.   /**
  2471.    * Whether high score is flashing.
  2472.    * @return {boolean}
  2473.    */
  2474.   isHighScoreFlashing() {
  2475.     return this.highScoreFlashing;
  2476.   },
  2477.  
  2478.   /**
  2479.    * Stop flashing the high score.
  2480.    */
  2481.   cancelHighScoreFlashing: function() {
  2482.     if (this.flashingRafId) {
  2483.       cancelAnimationFrame(this.flashingRafId);
  2484.     }
  2485.     this.flashIterations = 0;
  2486.     this.flashTimer = 0;
  2487.     this.highScoreFlashing = false;
  2488.     this.clearHighScoreBounds();
  2489.     this.drawHighScore();
  2490.   },
  2491.  
  2492.   /**
  2493.    * Clear the high score.
  2494.    */
  2495.   resetHighScore: function() {
  2496.     this.setHighScore(0);
  2497.     this.cancelHighScoreFlashing();
  2498.   },
  2499.  
  2500.   /**
  2501.    * Reset the distance meter back to '00000'.
  2502.    */
  2503.   reset: function() {
  2504.     this.update(0, 0);
  2505.     this.achievement = false;
  2506.   }
  2507. };
  2508.  
  2509.  
  2510. //******************************************************************************
  2511.  
  2512. /**
  2513.  * Cloud background item.
  2514.  * Similar to an obstacle object but without collision boxes.
  2515.  * @param {HTMLCanvasElement} canvas Canvas element.
  2516.  * @param {Object} spritePos Position of image in sprite.
  2517.  * @param {number} containerWidth
  2518.  * @constructor
  2519.  */
  2520. function Cloud(canvas, spritePos, containerWidth) {
  2521.   this.canvas = canvas;
  2522.   this.canvasCtx =
  2523.       /** @type {CanvasRenderingContext2D} */ (this.canvas.getContext('2d'));
  2524.   this.spritePos = spritePos;
  2525.   this.containerWidth = containerWidth;
  2526.   this.xPos = containerWidth;
  2527.   this.yPos = 0;
  2528.   this.remove = false;
  2529.   this.cloudGap = getRandomNum(Cloud.config.MIN_CLOUD_GAP,
  2530.       Cloud.config.MAX_CLOUD_GAP);
  2531.  
  2532.   this.init();
  2533. }
  2534.  
  2535.  
  2536. /**
  2537.  * Cloud object config.
  2538.  * @enum {number}
  2539.  */
  2540. Cloud.config = {
  2541.   HEIGHT: 14,
  2542.   MAX_CLOUD_GAP: 400,
  2543.   MAX_SKY_LEVEL: 30,
  2544.   MIN_CLOUD_GAP: 100,
  2545.   MIN_SKY_LEVEL: 71,
  2546.   WIDTH: 46
  2547. };
  2548.  
  2549.  
  2550. Cloud.prototype = {
  2551.   /**
  2552.    * Initialise the cloud. Sets the Cloud height.
  2553.    */
  2554.   init: function() {
  2555.     this.yPos = getRandomNum(Cloud.config.MAX_SKY_LEVEL,
  2556.         Cloud.config.MIN_SKY_LEVEL);
  2557.     this.draw();
  2558.   },
  2559.  
  2560.   /**
  2561.    * Draw the cloud.
  2562.    */
  2563.   draw: function() {
  2564.     this.canvasCtx.save();
  2565.     let sourceWidth = Cloud.config.WIDTH;
  2566.     let sourceHeight = Cloud.config.HEIGHT;
  2567.     const outputWidth = sourceWidth;
  2568.     const outputHeight = sourceHeight;
  2569.     if (IS_HIDPI) {
  2570.       sourceWidth = sourceWidth * 2;
  2571.       sourceHeight = sourceHeight * 2;
  2572.     }
  2573.  
  2574.     this.canvasCtx.drawImage(Runner.imageSprite, this.spritePos.x,
  2575.         this.spritePos.y,
  2576.         sourceWidth, sourceHeight,
  2577.         this.xPos, this.yPos,
  2578.         outputWidth, outputHeight);
  2579.  
  2580.     this.canvasCtx.restore();
  2581.   },
  2582.  
  2583.   /**
  2584.    * Update the cloud position.
  2585.    * @param {number} speed
  2586.    */
  2587.   update: function(speed) {
  2588.     if (!this.remove) {
  2589.       this.xPos -= Math.ceil(speed);
  2590.       this.draw();
  2591.  
  2592.       // Mark as removeable if no longer in the canvas.
  2593.       if (!this.isVisible()) {
  2594.         this.remove = true;
  2595.       }
  2596.     }
  2597.   },
  2598.  
  2599.   /**
  2600.    * Check if the cloud is visible on the stage.
  2601.    * @return {boolean}
  2602.    */
  2603.   isVisible: function() {
  2604.     return this.xPos + Cloud.config.WIDTH > 0;
  2605.   }
  2606. };
  2607.  
  2608.  
  2609. //******************************************************************************
  2610.  
  2611. /**
  2612.  * Nightmode shows a moon and stars on the horizon.
  2613.  * @param {HTMLCanvasElement} canvas
  2614.  * @param {number} spritePos
  2615.  * @param {number} containerWidth
  2616.  * @constructor
  2617.  */
  2618. function NightMode(canvas, spritePos, containerWidth) {
  2619.   this.spritePos = spritePos;
  2620.   this.canvas = canvas;
  2621.   this.canvasCtx =
  2622.       /** @type {CanvasRenderingContext2D} */ (canvas.getContext('2d'));
  2623.   this.xPos = containerWidth - 50;
  2624.   this.yPos = 30;
  2625.   this.currentPhase = 0;
  2626.   this.opacity = 0;
  2627.   this.containerWidth = containerWidth;
  2628.   this.stars = [];
  2629.   this.drawStars = false;
  2630.   this.placeStars();
  2631. }
  2632.  
  2633. /**
  2634.  * @enum {number}
  2635.  */
  2636. NightMode.config = {
  2637.   FADE_SPEED: 0.035,
  2638.   HEIGHT: 40,
  2639.   MOON_SPEED: 0.25,
  2640.   NUM_STARS: 2,
  2641.   STAR_SIZE: 9,
  2642.   STAR_SPEED: 0.3,
  2643.   STAR_MAX_Y: 70,
  2644.   WIDTH: 20
  2645. };
  2646.  
  2647. NightMode.phases = [140, 120, 100, 60, 40, 20, 0];
  2648.  
  2649. NightMode.prototype = {
  2650.   /**
  2651.    * Update moving moon, changing phases.
  2652.    * @param {boolean} activated Whether night mode is activated.
  2653.    */
  2654.   update: function(activated) {
  2655.     // Moon phase.
  2656.     if (activated && this.opacity == 0) {
  2657.       this.currentPhase++;
  2658.  
  2659.       if (this.currentPhase >= NightMode.phases.length) {
  2660.         this.currentPhase = 0;
  2661.       }
  2662.     }
  2663.  
  2664.     // Fade in / out.
  2665.     if (activated && (this.opacity < 1 || this.opacity == 0)) {
  2666.       this.opacity += NightMode.config.FADE_SPEED;
  2667.     } else if (this.opacity > 0) {
  2668.       this.opacity -= NightMode.config.FADE_SPEED;
  2669.     }
  2670.  
  2671.     // Set moon positioning.
  2672.     if (this.opacity > 0) {
  2673.       this.xPos = this.updateXPos(this.xPos, NightMode.config.MOON_SPEED);
  2674.  
  2675.       // Update stars.
  2676.       if (this.drawStars) {
  2677.          for (let i = 0; i < NightMode.config.NUM_STARS; i++) {
  2678.             this.stars[i].x = this.updateXPos(this.stars[i].x,
  2679.                 NightMode.config.STAR_SPEED);
  2680.          }
  2681.       }
  2682.       this.draw();
  2683.     } else {
  2684.       this.opacity = 0;
  2685.       this.placeStars();
  2686.     }
  2687.     this.drawStars = true;
  2688.   },
  2689.  
  2690.   updateXPos: function(currentPos, speed) {
  2691.     if (currentPos < -NightMode.config.WIDTH) {
  2692.       currentPos = this.containerWidth;
  2693.     } else {
  2694.       currentPos -= speed;
  2695.     }
  2696.     return currentPos;
  2697.   },
  2698.  
  2699.   draw: function() {
  2700.     let moonSourceWidth = this.currentPhase == 3 ? NightMode.config.WIDTH * 2 :
  2701.          NightMode.config.WIDTH;
  2702.     let moonSourceHeight = NightMode.config.HEIGHT;
  2703.     let moonSourceX = this.spritePos.x + NightMode.phases[this.currentPhase];
  2704.     const moonOutputWidth = moonSourceWidth;
  2705.     let starSize = NightMode.config.STAR_SIZE;
  2706.     let starSourceX = Runner.spriteDefinition.LDPI.STAR.x;
  2707.  
  2708.     if (IS_HIDPI) {
  2709.       moonSourceWidth *= 2;
  2710.       moonSourceHeight *= 2;
  2711.       moonSourceX = this.spritePos.x +
  2712.           (NightMode.phases[this.currentPhase] * 2);
  2713.       starSize *= 2;
  2714.       starSourceX = Runner.spriteDefinition.HDPI.STAR.x;
  2715.     }
  2716.  
  2717.     this.canvasCtx.save();
  2718.     this.canvasCtx.globalAlpha = this.opacity;
  2719.  
  2720.     // Stars.
  2721.     if (this.drawStars) {
  2722.       for (let i = 0; i < NightMode.config.NUM_STARS; i++) {
  2723.         this.canvasCtx.drawImage(Runner.imageSprite,
  2724.             starSourceX, this.stars[i].sourceY, starSize, starSize,
  2725.             Math.round(this.stars[i].x), this.stars[i].y,
  2726.             NightMode.config.STAR_SIZE, NightMode.config.STAR_SIZE);
  2727.       }
  2728.     }
  2729.  
  2730.     // Moon.
  2731.     this.canvasCtx.drawImage(Runner.imageSprite, moonSourceX,
  2732.         this.spritePos.y, moonSourceWidth, moonSourceHeight,
  2733.         Math.round(this.xPos), this.yPos,
  2734.         moonOutputWidth, NightMode.config.HEIGHT);
  2735.  
  2736.     this.canvasCtx.globalAlpha = 1;
  2737.     this.canvasCtx.restore();
  2738.   },
  2739.  
  2740.   // Do star placement.
  2741.   placeStars: function() {
  2742.     const segmentSize = Math.round(this.containerWidth /
  2743.         NightMode.config.NUM_STARS);
  2744.  
  2745.     for (let i = 0; i < NightMode.config.NUM_STARS; i++) {
  2746.       this.stars[i] = {};
  2747.       this.stars[i].x = getRandomNum(segmentSize * i, segmentSize * (i + 1));
  2748.       this.stars[i].y = getRandomNum(0, NightMode.config.STAR_MAX_Y);
  2749.  
  2750.       if (IS_HIDPI) {
  2751.         this.stars[i].sourceY = Runner.spriteDefinition.HDPI.STAR.y +
  2752.             NightMode.config.STAR_SIZE * 2 * i;
  2753.       } else {
  2754.         this.stars[i].sourceY = Runner.spriteDefinition.LDPI.STAR.y +
  2755.             NightMode.config.STAR_SIZE * i;
  2756.       }
  2757.     }
  2758.   },
  2759.  
  2760.   reset: function() {
  2761.     this.currentPhase = 0;
  2762.     this.opacity = 0;
  2763.     this.update(false);
  2764.   }
  2765.  
  2766. };
  2767.  
  2768.  
  2769. //******************************************************************************
  2770.  
  2771. /**
  2772.  * Horizon Line.
  2773.  * Consists of two connecting lines. Randomly assigns a flat / bumpy horizon.
  2774.  * @param {HTMLCanvasElement} canvas
  2775.  * @param {Object} spritePos Horizon position in sprite.
  2776.  * @constructor
  2777.  */
  2778. function HorizonLine(canvas, spritePos) {
  2779.   this.spritePos = spritePos;
  2780.   this.canvas = canvas;
  2781.   this.canvasCtx =
  2782.       /** @type {CanvasRenderingContext2D} */ (canvas.getContext('2d'));
  2783.   this.sourceDimensions = {};
  2784.   this.dimensions = HorizonLine.dimensions;
  2785.   this.sourceXPos = [this.spritePos.x, this.spritePos.x +
  2786.       this.dimensions.WIDTH];
  2787.   this.xPos = [];
  2788.   this.yPos = 0;
  2789.   this.bumpThreshold = 0.5;
  2790.  
  2791.   this.setSourceDimensions();
  2792.   this.draw();
  2793. }
  2794.  
  2795.  
  2796. /**
  2797.  * Horizon line dimensions.
  2798.  * @enum {number}
  2799.  */
  2800. HorizonLine.dimensions = {
  2801.   WIDTH: 600,
  2802.   HEIGHT: 12,
  2803.   YPOS: 127
  2804. };
  2805.  
  2806.  
  2807. HorizonLine.prototype = {
  2808.   /**
  2809.    * Set the source dimensions of the horizon line.
  2810.    */
  2811.   setSourceDimensions: function() {
  2812.  
  2813.     for (const dimension in HorizonLine.dimensions) {
  2814.       if (IS_HIDPI) {
  2815.         if (dimension != 'YPOS') {
  2816.           this.sourceDimensions[dimension] =
  2817.               HorizonLine.dimensions[dimension] * 2;
  2818.         }
  2819.       } else {
  2820.         this.sourceDimensions[dimension] =
  2821.             HorizonLine.dimensions[dimension];
  2822.       }
  2823.       this.dimensions[dimension] = HorizonLine.dimensions[dimension];
  2824.     }
  2825.  
  2826.     this.xPos = [0, HorizonLine.dimensions.WIDTH];
  2827.     this.yPos = HorizonLine.dimensions.YPOS;
  2828.   },
  2829.  
  2830.   /**
  2831.    * Return the crop x position of a type.
  2832.    */
  2833.   getRandomType: function() {
  2834.     return Math.random() > this.bumpThreshold ? this.dimensions.WIDTH : 0;
  2835.   },
  2836.  
  2837.   /**
  2838.    * Draw the horizon line.
  2839.    */
  2840.   draw: function() {
  2841.     this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[0],
  2842.         this.spritePos.y,
  2843.         this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
  2844.         this.xPos[0], this.yPos,
  2845.         this.dimensions.WIDTH, this.dimensions.HEIGHT);
  2846.  
  2847.     this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[1],
  2848.         this.spritePos.y,
  2849.         this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
  2850.         this.xPos[1], this.yPos,
  2851.         this.dimensions.WIDTH, this.dimensions.HEIGHT);
  2852.   },
  2853.  
  2854.   /**
  2855.    * Update the x position of an indivdual piece of the line.
  2856.    * @param {number} pos Line position.
  2857.    * @param {number} increment
  2858.    */
  2859.   updateXPos: function(pos, increment) {
  2860.     const line1 = pos;
  2861.     const line2 = pos == 0 ? 1 : 0;
  2862.  
  2863.     this.xPos[line1] -= increment;
  2864.     this.xPos[line2] = this.xPos[line1] + this.dimensions.WIDTH;
  2865.  
  2866.     if (this.xPos[line1] <= -this.dimensions.WIDTH) {
  2867.       this.xPos[line1] += this.dimensions.WIDTH * 2;
  2868.       this.xPos[line2] = this.xPos[line1] - this.dimensions.WIDTH;
  2869.       this.sourceXPos[line1] = this.getRandomType() + this.spritePos.x;
  2870.     }
  2871.   },
  2872.  
  2873.   /**
  2874.    * Update the horizon line.
  2875.    * @param {number} deltaTime
  2876.    * @param {number} speed
  2877.    */
  2878.   update: function(deltaTime, speed) {
  2879.     const increment = Math.floor(speed * (FPS / 1000) * deltaTime);
  2880.  
  2881.     if (this.xPos[0] <= 0) {
  2882.       this.updateXPos(0, increment);
  2883.     } else {
  2884.       this.updateXPos(1, increment);
  2885.     }
  2886.     this.draw();
  2887.   },
  2888.  
  2889.   /**
  2890.    * Reset horizon to the starting position.
  2891.    */
  2892.   reset: function() {
  2893.     this.xPos[0] = 0;
  2894.     this.xPos[1] = HorizonLine.dimensions.WIDTH;
  2895.   }
  2896. };
  2897.  
  2898.  
  2899. //******************************************************************************
  2900.  
  2901. /**
  2902.  * Horizon background class.
  2903.  * @param {HTMLCanvasElement} canvas
  2904.  * @param {Object} spritePos Sprite positioning.
  2905.  * @param {Object} dimensions Canvas dimensions.
  2906.  * @param {number} gapCoefficient
  2907.  * @constructor
  2908.  */
  2909. function Horizon(canvas, spritePos, dimensions, gapCoefficient) {
  2910.   this.canvas = canvas;
  2911.   this.canvasCtx =
  2912.       /** @type {CanvasRenderingContext2D} */ (this.canvas.getContext('2d'));
  2913.   this.config = Horizon.config;
  2914.   this.dimensions = dimensions;
  2915.   this.gapCoefficient = gapCoefficient;
  2916.   this.obstacles = [];
  2917.   this.obstacleHistory = [];
  2918.   this.horizonOffsets = [0, 0];
  2919.   this.cloudFrequency = this.config.CLOUD_FREQUENCY;
  2920.   this.spritePos = spritePos;
  2921.   this.nightMode = null;
  2922.  
  2923.   // Cloud
  2924.   this.clouds = [];
  2925.   this.cloudSpeed = this.config.BG_CLOUD_SPEED;
  2926.  
  2927.   // Horizon
  2928.   this.horizonLine = null;
  2929.   this.init();
  2930. }
  2931.  
  2932.  
  2933. /**
  2934.  * Horizon config.
  2935.  * @enum {number}
  2936.  */
  2937. Horizon.config = {
  2938.   BG_CLOUD_SPEED: 0.2,
  2939.   BUMPY_THRESHOLD: .3,
  2940.   CLOUD_FREQUENCY: .5,
  2941.   HORIZON_HEIGHT: 16,
  2942.   MAX_CLOUDS: 6
  2943. };
  2944.  
  2945.  
  2946. Horizon.prototype = {
  2947.   /**
  2948.    * Initialise the horizon. Just add the line and a cloud. No obstacles.
  2949.    */
  2950.   init: function() {
  2951.     this.addCloud();
  2952.     this.horizonLine = new HorizonLine(this.canvas, this.spritePos.HORIZON);
  2953.     this.nightMode = new NightMode(this.canvas, this.spritePos.MOON,
  2954.         this.dimensions.WIDTH);
  2955.   },
  2956.  
  2957.   /**
  2958.    * @param {number} deltaTime
  2959.    * @param {number} currentSpeed
  2960.    * @param {boolean} updateObstacles Used as an override to prevent
  2961.    *     the obstacles from being updated / added. This happens in the
  2962.    *     ease in section.
  2963.    * @param {boolean} showNightMode Night mode activated.
  2964.    */
  2965.   update: function(deltaTime, currentSpeed, updateObstacles, showNightMode) {
  2966.     this.runningTime += deltaTime;
  2967.     this.horizonLine.update(deltaTime, currentSpeed);
  2968.     this.nightMode.update(showNightMode);
  2969.     this.updateClouds(deltaTime, currentSpeed);
  2970.  
  2971.     if (updateObstacles) {
  2972.       this.updateObstacles(deltaTime, currentSpeed);
  2973.     }
  2974.   },
  2975.  
  2976.   /**
  2977.    * Update the cloud positions.
  2978.    * @param {number} deltaTime
  2979.    * @param {number} speed
  2980.    */
  2981.   updateClouds: function(deltaTime, speed) {
  2982.     const cloudSpeed = this.cloudSpeed / 1000 * deltaTime * speed;
  2983.     const numClouds = this.clouds.length;
  2984.  
  2985.     if (numClouds) {
  2986.       for (let i = numClouds - 1; i >= 0; i--) {
  2987.         this.clouds[i].update(cloudSpeed);
  2988.       }
  2989.  
  2990.       const lastCloud = this.clouds[numClouds - 1];
  2991.  
  2992.       // Check for adding a new cloud.
  2993.       if (numClouds < this.config.MAX_CLOUDS &&
  2994.           (this.dimensions.WIDTH - lastCloud.xPos) > lastCloud.cloudGap &&
  2995.           this.cloudFrequency > Math.random()) {
  2996.         this.addCloud();
  2997.       }
  2998.  
  2999.       // Remove expired clouds.
  3000.       this.clouds = this.clouds.filter(function(obj) {
  3001.         return !obj.remove;
  3002.       });
  3003.     } else {
  3004.       this.addCloud();
  3005.     }
  3006.   },
  3007.  
  3008.   /**
  3009.    * Update the obstacle positions.
  3010.    * @param {number} deltaTime
  3011.    * @param {number} currentSpeed
  3012.    */
  3013.   updateObstacles: function(deltaTime, currentSpeed) {
  3014.     // Obstacles, move to Horizon layer.
  3015.     const updatedObstacles = this.obstacles.slice(0);
  3016.  
  3017.     for (let i = 0; i < this.obstacles.length; i++) {
  3018.       const obstacle = this.obstacles[i];
  3019.       obstacle.update(deltaTime, currentSpeed);
  3020.  
  3021.       // Clean up existing obstacles.
  3022.       if (obstacle.remove) {
  3023.         updatedObstacles.shift();
  3024.       }
  3025.     }
  3026.     this.obstacles = updatedObstacles;
  3027.  
  3028.     if (this.obstacles.length > 0) {
  3029.       const lastObstacle = this.obstacles[this.obstacles.length - 1];
  3030.  
  3031.       if (lastObstacle && !lastObstacle.followingObstacleCreated &&
  3032.           lastObstacle.isVisible() &&
  3033.           (lastObstacle.xPos + lastObstacle.width + lastObstacle.gap) <
  3034.           this.dimensions.WIDTH) {
  3035.         this.addNewObstacle(currentSpeed);
  3036.         lastObstacle.followingObstacleCreated = true;
  3037.       }
  3038.     } else {
  3039.       // Create new obstacles.
  3040.       this.addNewObstacle(currentSpeed);
  3041.     }
  3042.   },
  3043.  
  3044.   removeFirstObstacle: function() {
  3045.     this.obstacles.shift();
  3046.   },
  3047.  
  3048.   /**
  3049.    * Add a new obstacle.
  3050.    * @param {number} currentSpeed
  3051.    */
  3052.   addNewObstacle: function(currentSpeed) {
  3053.     const obstacleTypeIndex = getRandomNum(0, Obstacle.types.length - 1);
  3054.     const obstacleType = Obstacle.types[obstacleTypeIndex];
  3055.  
  3056.     // Check for multiples of the same type of obstacle.
  3057.     // Also check obstacle is available at current speed.
  3058.     if (this.duplicateObstacleCheck(obstacleType.type) ||
  3059.         currentSpeed < obstacleType.minSpeed) {
  3060.       this.addNewObstacle(currentSpeed);
  3061.     } else {
  3062.       const obstacleSpritePos = this.spritePos[obstacleType.type];
  3063.  
  3064.       this.obstacles.push(new Obstacle(this.canvasCtx, obstacleType,
  3065.           obstacleSpritePos, this.dimensions,
  3066.           this.gapCoefficient, currentSpeed, obstacleType.width));
  3067.  
  3068.       this.obstacleHistory.unshift(obstacleType.type);
  3069.  
  3070.       if (this.obstacleHistory.length > 1) {
  3071.         this.obstacleHistory.splice(Runner.config.MAX_OBSTACLE_DUPLICATION);
  3072.       }
  3073.     }
  3074.   },
  3075.  
  3076.   /**
  3077.    * Returns whether the previous two obstacles are the same as the next one.
  3078.    * Maximum duplication is set in config value MAX_OBSTACLE_DUPLICATION.
  3079.    * @return {boolean}
  3080.    */
  3081.   duplicateObstacleCheck: function(nextObstacleType) {
  3082.     let duplicateCount = 0;
  3083.  
  3084.     for (let i = 0; i < this.obstacleHistory.length; i++) {
  3085.       duplicateCount = this.obstacleHistory[i] == nextObstacleType ?
  3086.           duplicateCount + 1 : 0;
  3087.     }
  3088.     return duplicateCount >= Runner.config.MAX_OBSTACLE_DUPLICATION;
  3089.   },
  3090.  
  3091.   /**
  3092.    * Reset the horizon layer.
  3093.    * Remove existing obstacles and reposition the horizon line.
  3094.    */
  3095.   reset: function() {
  3096.     this.obstacles = [];
  3097.     this.horizonLine.reset();
  3098.     this.nightMode.reset();
  3099.   },
  3100.  
  3101.   /**
  3102.    * Update the canvas width and scaling.
  3103.    * @param {number} width Canvas width.
  3104.    * @param {number} height Canvas height.
  3105.    */
  3106.   resize: function(width, height) {
  3107.     this.canvas.width = width;
  3108.     this.canvas.height = height;
  3109.   },
  3110.  
  3111.   /**
  3112.    * Add a new cloud to the horizon.
  3113.    */
  3114.   addCloud: function() {
  3115.     this.clouds.push(new Cloud(this.canvas, this.spritePos.CLOUD,
  3116.         this.dimensions.WIDTH));
  3117.   }
  3118. };
Add Comment
Please, Sign In to add comment