Path: blob/main/static/src/gs/public/dinosaur/index.js
1333 views
// Copyright (c) 2014 The Chromium Authors. All rights reserved.1// Use of this source code is governed by a BSD-style license that can be2// found in the LICENSE file.3// extract from chromium source code by @liuwayong4(function () {5'use strict';6/**7* T-Rex runner.8* @param {string} outerContainerId Outer containing element id.9* @param {Object} opt_config10* @constructor11* @export12*/13function Runner(outerContainerId, opt_config) {14// Singleton15if (Runner.instance_) {16return Runner.instance_;17}18Runner.instance_ = this;1920this.outerContainerEl = document.querySelector(outerContainerId);21this.containerEl = null;22this.snackbarEl = null;23this.detailsButton = this.outerContainerEl.querySelector('#details-button');2425this.config = opt_config || Runner.config;2627this.dimensions = Runner.defaultDimensions;2829this.canvas = null;30this.canvasCtx = null;3132this.tRex = null;3334this.distanceMeter = null;35this.distanceRan = 0;3637this.highestScore = 0;3839this.time = 0;40this.runningTime = 0;41this.msPerFrame = 1000 / FPS;42this.currentSpeed = this.config.SPEED;4344this.obstacles = [];4546this.activated = false; // Whether the easter egg has been activated.47this.playing = false; // Whether the game is currently in play state.48this.crashed = false;49this.paused = false;50this.inverted = false;51this.invertTimer = 0;52this.resizeTimerId_ = null;5354this.playCount = 0;5556// Sound FX.57this.audioBuffer = null;58this.soundFx = {};5960// Global web audio context for playing sounds.61this.audioContext = null;6263// Images.64this.images = {};65this.imagesLoaded = 0;6667if (this.isDisabled()) {68this.setupDisabledRunner();69} else {70this.loadImages();71}72}73window['Runner'] = Runner;747576/**77* Default game width.78* @const79*/80var DEFAULT_WIDTH = 600;8182/**83* Frames per second.84* @const85*/86var FPS = 60;8788/** @const */89var IS_HIDPI = window.devicePixelRatio > 1;9091/** @const */92var IS_IOS = /iPad|iPhone|iPod/.test(window.navigator.platform);9394/** @const */95var IS_MOBILE = /Android/.test(window.navigator.userAgent) || IS_IOS;9697/** @const */98var IS_TOUCH_ENABLED = 'ontouchstart' in window;99100/**101* Default game configuration.102* @enum {number}103*/104Runner.config = {105ACCELERATION: 0.001,106BG_CLOUD_SPEED: 0.2,107BOTTOM_PAD: 10,108CLEAR_TIME: 3000,109CLOUD_FREQUENCY: 0.5,110GAMEOVER_CLEAR_TIME: 750,111GAP_COEFFICIENT: 0.6,112GRAVITY: 0.6,113INITIAL_JUMP_VELOCITY: 12,114INVERT_FADE_DURATION: 12000,115INVERT_DISTANCE: 700,116MAX_BLINK_COUNT: 3,117MAX_CLOUDS: 6,118MAX_OBSTACLE_LENGTH: 3,119MAX_OBSTACLE_DUPLICATION: 2,120MAX_SPEED: 13,121MIN_JUMP_HEIGHT: 35,122MOBILE_SPEED_COEFFICIENT: 1.2,123RESOURCE_TEMPLATE_ID: 'audio-resources',124SPEED: 6,125SPEED_DROP_COEFFICIENT: 3126};127128129/**130* Default dimensions.131* @enum {string}132*/133Runner.defaultDimensions = {134WIDTH: DEFAULT_WIDTH,135HEIGHT: 150136};137138139/**140* CSS class names.141* @enum {string}142*/143Runner.classes = {144CANVAS: 'runner-canvas',145CONTAINER: 'runner-container',146CRASHED: 'crashed',147ICON: 'icon-offline',148INVERTED: 'inverted',149SNACKBAR: 'snackbar',150SNACKBAR_SHOW: 'snackbar-show',151TOUCH_CONTROLLER: 'controller'152};153154155/**156* Sprite definition layout of the spritesheet.157* @enum {Object}158*/159Runner.spriteDefinition = {160LDPI: {161CACTUS_LARGE: { x: 332, y: 2 },162CACTUS_SMALL: { x: 228, y: 2 },163CLOUD: { x: 86, y: 2 },164HORIZON: { x: 2, y: 54 },165MOON: { x: 484, y: 2 },166PTERODACTYL: { x: 134, y: 2 },167RESTART: { x: 2, y: 2 },168TEXT_SPRITE: { x: 655, y: 2 },169TREX: { x: 848, y: 2 },170STAR: { x: 645, y: 2 }171},172HDPI: {173CACTUS_LARGE: { x: 652, y: 2 },174CACTUS_SMALL: { x: 446, y: 2 },175CLOUD: { x: 166, y: 2 },176HORIZON: { x: 2, y: 104 },177MOON: { x: 954, y: 2 },178PTERODACTYL: { x: 260, y: 2 },179RESTART: { x: 2, y: 2 },180TEXT_SPRITE: { x: 1294, y: 2 },181TREX: { x: 1678, y: 2 },182STAR: { x: 1276, y: 2 }183}184};185186187/**188* Sound FX. Reference to the ID of the audio tag on interstitial page.189* @enum {string}190*/191Runner.sounds = {192BUTTON_PRESS: 'offline-sound-press',193HIT: 'offline-sound-hit',194SCORE: 'offline-sound-reached'195};196197198/**199* Key code mapping.200* @enum {Object}201*/202Runner.keycodes = {203JUMP: { '38': 1, '32': 1 }, // Up, spacebar204DUCK: { '40': 1 }, // Down205RESTART: { '13': 1 } // Enter206};207208209/**210* Runner event names.211* @enum {string}212*/213Runner.events = {214ANIM_END: 'webkitAnimationEnd',215CLICK: 'click',216KEYDOWN: 'keydown',217KEYUP: 'keyup',218MOUSEDOWN: 'mousedown',219MOUSEUP: 'mouseup',220RESIZE: 'resize',221TOUCHEND: 'touchend',222TOUCHSTART: 'touchstart',223VISIBILITY: 'visibilitychange',224BLUR: 'blur',225FOCUS: 'focus',226LOAD: 'load'227};228229230Runner.prototype = {231/**232* Whether the easter egg has been disabled. CrOS enterprise enrolled devices.233* @return {boolean}234*/235isDisabled: function () {236// return loadTimeData && loadTimeData.valueExists('disabledEasterEgg');237return false;238},239240/**241* For disabled instances, set up a snackbar with the disabled message.242*/243setupDisabledRunner: function () {244this.containerEl = document.createElement('div');245this.containerEl.className = Runner.classes.SNACKBAR;246this.containerEl.textContent = loadTimeData.getValue('disabledEasterEgg');247this.outerContainerEl.appendChild(this.containerEl);248249// Show notification when the activation key is pressed.250document.addEventListener(Runner.events.KEYDOWN, function (e) {251if (Runner.keycodes.JUMP[e.keyCode]) {252this.containerEl.classList.add(Runner.classes.SNACKBAR_SHOW);253document.querySelector('.icon').classList.add('icon-disabled');254}255}.bind(this));256},257258/**259* Setting individual settings for debugging.260* @param {string} setting261* @param {*} value262*/263updateConfigSetting: function (setting, value) {264if (setting in this.config && value != undefined) {265this.config[setting] = value;266267switch (setting) {268case 'GRAVITY':269case 'MIN_JUMP_HEIGHT':270case 'SPEED_DROP_COEFFICIENT':271this.tRex.config[setting] = value;272break;273case 'INITIAL_JUMP_VELOCITY':274this.tRex.setJumpVelocity(value);275break;276case 'SPEED':277this.setSpeed(value);278break;279}280}281},282283/**284* Cache the appropriate image sprite from the page and get the sprite sheet285* definition.286*/287loadImages: function () {288if (IS_HIDPI) {289Runner.imageSprite = document.getElementById('offline-resources-2x');290this.spriteDef = Runner.spriteDefinition.HDPI;291} else {292Runner.imageSprite = document.getElementById('offline-resources-1x');293this.spriteDef = Runner.spriteDefinition.LDPI;294}295296if (Runner.imageSprite.complete) {297this.init();298} else {299// If the images are not yet loaded, add a listener.300Runner.imageSprite.addEventListener(Runner.events.LOAD,301this.init.bind(this));302}303},304305/**306* Load and decode base 64 encoded sounds.307*/308loadSounds: function () {309if (!IS_IOS) {310this.audioContext = new AudioContext();311312var resourceTemplate =313document.getElementById(this.config.RESOURCE_TEMPLATE_ID).content;314315for (var sound in Runner.sounds) {316var soundSrc =317resourceTemplate.getElementById(Runner.sounds[sound]).src;318soundSrc = soundSrc.substr(soundSrc.indexOf(',') + 1);319var buffer = decodeBase64ToArrayBuffer(soundSrc);320321// Async, so no guarantee of order in array.322this.audioContext.decodeAudioData(buffer, function (index, audioData) {323this.soundFx[index] = audioData;324}.bind(this, sound));325}326}327},328329/**330* Sets the game speed. Adjust the speed accordingly if on a smaller screen.331* @param {number} opt_speed332*/333setSpeed: function (opt_speed) {334var speed = opt_speed || this.currentSpeed;335336// Reduce the speed on smaller mobile screens.337if (this.dimensions.WIDTH < DEFAULT_WIDTH) {338var mobileSpeed = speed * this.dimensions.WIDTH / DEFAULT_WIDTH *339this.config.MOBILE_SPEED_COEFFICIENT;340this.currentSpeed = mobileSpeed > speed ? speed : mobileSpeed;341} else if (opt_speed) {342this.currentSpeed = opt_speed;343}344},345346/**347* Game initialiser.348*/349init: function () {350// Hide the static icon.351document.querySelector('.' + Runner.classes.ICON).style.visibility =352'hidden';353354this.adjustDimensions();355this.setSpeed();356357this.containerEl = document.createElement('div');358this.containerEl.className = Runner.classes.CONTAINER;359360// Player canvas container.361this.canvas = createCanvas(this.containerEl, this.dimensions.WIDTH,362this.dimensions.HEIGHT, Runner.classes.PLAYER);363364this.canvasCtx = this.canvas.getContext('2d');365this.canvasCtx.fillStyle = '#f7f7f7';366this.canvasCtx.fill();367Runner.updateCanvasScaling(this.canvas);368369// Horizon contains clouds, obstacles and the ground.370this.horizon = new Horizon(this.canvas, this.spriteDef, this.dimensions,371this.config.GAP_COEFFICIENT);372373// Distance meter374this.distanceMeter = new DistanceMeter(this.canvas,375this.spriteDef.TEXT_SPRITE, this.dimensions.WIDTH);376377// Draw t-rex378this.tRex = new Trex(this.canvas, this.spriteDef.TREX);379380this.outerContainerEl.appendChild(this.containerEl);381382if (IS_MOBILE) {383this.createTouchController();384}385386this.startListening();387this.update();388389window.addEventListener(Runner.events.RESIZE,390this.debounceResize.bind(this));391},392393/**394* Create the touch controller. A div that covers whole screen.395*/396createTouchController: function () {397this.touchController = document.createElement('div');398this.touchController.className = Runner.classes.TOUCH_CONTROLLER;399this.outerContainerEl.appendChild(this.touchController);400},401402/**403* Debounce the resize event.404*/405debounceResize: function () {406if (!this.resizeTimerId_) {407this.resizeTimerId_ =408setInterval(this.adjustDimensions.bind(this), 250);409}410},411412/**413* Adjust game space dimensions on resize.414*/415adjustDimensions: function () {416clearInterval(this.resizeTimerId_);417this.resizeTimerId_ = null;418419var boxStyles = window.getComputedStyle(this.outerContainerEl);420var padding = Number(boxStyles.paddingLeft.substr(0,421boxStyles.paddingLeft.length - 2));422423this.dimensions.WIDTH = this.outerContainerEl.offsetWidth - padding * 2;424425// Redraw the elements back onto the canvas.426if (this.canvas) {427this.canvas.width = this.dimensions.WIDTH;428this.canvas.height = this.dimensions.HEIGHT;429430Runner.updateCanvasScaling(this.canvas);431432this.distanceMeter.calcXPos(this.dimensions.WIDTH);433this.clearCanvas();434this.horizon.update(0, 0, true);435this.tRex.update(0);436437// Outer container and distance meter.438if (this.playing || this.crashed || this.paused) {439this.containerEl.style.width = this.dimensions.WIDTH + 'px';440this.containerEl.style.height = this.dimensions.HEIGHT + 'px';441this.distanceMeter.update(0, Math.ceil(this.distanceRan));442this.stop();443} else {444this.tRex.draw(0, 0);445}446447// Game over panel.448if (this.crashed && this.gameOverPanel) {449this.gameOverPanel.updateDimensions(this.dimensions.WIDTH);450this.gameOverPanel.draw();451}452}453},454455/**456* Play the game intro.457* Canvas container width expands out to the full width.458*/459playIntro: function () {460if (!this.activated && !this.crashed) {461this.playingIntro = true;462this.tRex.playingIntro = true;463464// CSS animation definition.465var keyframes = '@-webkit-keyframes intro { ' +466'from { width:' + Trex.config.WIDTH + 'px }' +467'to { width: ' + this.dimensions.WIDTH + 'px }' +468'}';469470// create a style sheet to put the keyframe rule in471// and then place the style sheet in the html head472var sheet = document.createElement('style');473sheet.innerHTML = keyframes;474document.head.appendChild(sheet);475476this.containerEl.addEventListener(Runner.events.ANIM_END,477this.startGame.bind(this));478479this.containerEl.style.webkitAnimation = 'intro .4s ease-out 1 both';480this.containerEl.style.width = this.dimensions.WIDTH + 'px';481482// if (this.touchController) {483// this.outerContainerEl.appendChild(this.touchController);484// }485this.playing = true;486this.activated = true;487} else if (this.crashed) {488this.restart();489}490},491492493/**494* Update the game status to started.495*/496startGame: function () {497this.runningTime = 0;498this.playingIntro = false;499this.tRex.playingIntro = false;500this.containerEl.style.webkitAnimation = '';501this.playCount++;502503// Handle tabbing off the page. Pause the current game.504document.addEventListener(Runner.events.VISIBILITY,505this.onVisibilityChange.bind(this));506507window.addEventListener(Runner.events.BLUR,508this.onVisibilityChange.bind(this));509510window.addEventListener(Runner.events.FOCUS,511this.onVisibilityChange.bind(this));512},513514clearCanvas: function () {515this.canvasCtx.clearRect(0, 0, this.dimensions.WIDTH,516this.dimensions.HEIGHT);517},518519/**520* Update the game frame and schedules the next one.521*/522update: function () {523this.updatePending = false;524525var now = getTimeStamp();526var deltaTime = now - (this.time || now);527this.time = now;528529if (this.playing) {530this.clearCanvas();531532if (this.tRex.jumping) {533this.tRex.updateJump(deltaTime);534}535536this.runningTime += deltaTime;537var hasObstacles = this.runningTime > this.config.CLEAR_TIME;538539// First jump triggers the intro.540if (this.tRex.jumpCount == 1 && !this.playingIntro) {541this.playIntro();542}543544// The horizon doesn't move until the intro is over.545if (this.playingIntro) {546this.horizon.update(0, this.currentSpeed, hasObstacles);547} else {548deltaTime = !this.activated ? 0 : deltaTime;549this.horizon.update(deltaTime, this.currentSpeed, hasObstacles,550this.inverted);551}552553// Check for collisions.554var collision = hasObstacles &&555checkForCollision(this.horizon.obstacles[0], this.tRex);556557if (!collision) {558this.distanceRan += this.currentSpeed * deltaTime / this.msPerFrame;559560if (this.currentSpeed < this.config.MAX_SPEED) {561this.currentSpeed += this.config.ACCELERATION;562}563} else {564this.gameOver();565}566567var playAchievementSound = this.distanceMeter.update(deltaTime,568Math.ceil(this.distanceRan));569570if (playAchievementSound) {571this.playSound(this.soundFx.SCORE);572}573574// Night mode.575if (this.invertTimer > this.config.INVERT_FADE_DURATION) {576this.invertTimer = 0;577this.invertTrigger = false;578this.invert();579} else if (this.invertTimer) {580this.invertTimer += deltaTime;581} else {582var actualDistance =583this.distanceMeter.getActualDistance(Math.ceil(this.distanceRan));584585if (actualDistance > 0) {586this.invertTrigger = !(actualDistance %587this.config.INVERT_DISTANCE);588589if (this.invertTrigger && this.invertTimer === 0) {590this.invertTimer += deltaTime;591this.invert();592}593}594}595}596597if (this.playing || (!this.activated &&598this.tRex.blinkCount < Runner.config.MAX_BLINK_COUNT)) {599this.tRex.update(deltaTime);600this.scheduleNextUpdate();601}602},603604/**605* Event handler.606*/607handleEvent: function (e) {608return (function (evtType, events) {609switch (evtType) {610case events.KEYDOWN:611case events.TOUCHSTART:612case events.MOUSEDOWN:613this.onKeyDown(e);614break;615case events.KEYUP:616case events.TOUCHEND:617case events.MOUSEUP:618this.onKeyUp(e);619break;620}621}.bind(this))(e.type, Runner.events);622},623624/**625* Bind relevant key / mouse / touch listeners.626*/627startListening: function () {628// Keys.629document.addEventListener(Runner.events.KEYDOWN, this);630document.addEventListener(Runner.events.KEYUP, this);631632if (IS_MOBILE) {633// Mobile only touch devices.634this.touchController.addEventListener(Runner.events.TOUCHSTART, this);635this.touchController.addEventListener(Runner.events.TOUCHEND, this);636this.containerEl.addEventListener(Runner.events.TOUCHSTART, this);637} else {638// Mouse.639document.addEventListener(Runner.events.MOUSEDOWN, this);640document.addEventListener(Runner.events.MOUSEUP, this);641}642},643644/**645* Remove all listeners.646*/647stopListening: function () {648document.removeEventListener(Runner.events.KEYDOWN, this);649document.removeEventListener(Runner.events.KEYUP, this);650651if (IS_MOBILE) {652this.touchController.removeEventListener(Runner.events.TOUCHSTART, this);653this.touchController.removeEventListener(Runner.events.TOUCHEND, this);654this.containerEl.removeEventListener(Runner.events.TOUCHSTART, this);655} else {656document.removeEventListener(Runner.events.MOUSEDOWN, this);657document.removeEventListener(Runner.events.MOUSEUP, this);658}659},660661/**662* Process keydown.663* @param {Event} e664*/665onKeyDown: function (e) {666// Prevent native page scrolling whilst tapping on mobile.667if (IS_MOBILE && this.playing) {668e.preventDefault();669}670671if (e.target != this.detailsButton) {672if (!this.crashed && (Runner.keycodes.JUMP[e.keyCode] ||673e.type == Runner.events.TOUCHSTART)) {674if (!this.playing) {675this.loadSounds();676this.playing = true;677this.update();678if (window.errorPageController) {679errorPageController.trackEasterEgg();680}681}682// Play sound effect and jump on starting the game for the first time.683if (!this.tRex.jumping && !this.tRex.ducking) {684this.playSound(this.soundFx.BUTTON_PRESS);685this.tRex.startJump(this.currentSpeed);686}687}688689if (this.crashed && e.type == Runner.events.TOUCHSTART &&690e.currentTarget == this.containerEl) {691this.restart();692}693}694695if (this.playing && !this.crashed && Runner.keycodes.DUCK[e.keyCode]) {696e.preventDefault();697if (this.tRex.jumping) {698// Speed drop, activated only when jump key is not pressed.699this.tRex.setSpeedDrop();700} else if (!this.tRex.jumping && !this.tRex.ducking) {701// Duck.702this.tRex.setDuck(true);703}704}705},706707708/**709* Process key up.710* @param {Event} e711*/712onKeyUp: function (e) {713var keyCode = String(e.keyCode);714var isjumpKey = Runner.keycodes.JUMP[keyCode] ||715e.type == Runner.events.TOUCHEND ||716e.type == Runner.events.MOUSEDOWN;717718if (this.isRunning() && isjumpKey) {719this.tRex.endJump();720} else if (Runner.keycodes.DUCK[keyCode]) {721this.tRex.speedDrop = false;722this.tRex.setDuck(false);723} else if (this.crashed) {724// Check that enough time has elapsed before allowing jump key to restart.725var deltaTime = getTimeStamp() - this.time;726727if (Runner.keycodes.RESTART[keyCode] || this.isLeftClickOnCanvas(e) ||728(deltaTime >= this.config.GAMEOVER_CLEAR_TIME &&729Runner.keycodes.JUMP[keyCode])) {730this.restart();731}732} else if (this.paused && isjumpKey) {733// Reset the jump state734this.tRex.reset();735this.play();736}737},738739/**740* Returns whether the event was a left click on canvas.741* On Windows right click is registered as a click.742* @param {Event} e743* @return {boolean}744*/745isLeftClickOnCanvas: function (e) {746return e.button != null && e.button < 2 &&747e.type == Runner.events.MOUSEUP && e.target == this.canvas;748},749750/**751* RequestAnimationFrame wrapper.752*/753scheduleNextUpdate: function () {754if (!this.updatePending) {755this.updatePending = true;756this.raqId = requestAnimationFrame(this.update.bind(this));757}758},759760/**761* Whether the game is running.762* @return {boolean}763*/764isRunning: function () {765return !!this.raqId;766},767768/**769* Game over state.770*/771gameOver: function () {772this.playSound(this.soundFx.HIT);773vibrate(200);774775this.stop();776this.crashed = true;777this.distanceMeter.acheivement = false;778779this.tRex.update(100, Trex.status.CRASHED);780781// Game over panel.782if (!this.gameOverPanel) {783this.gameOverPanel = new GameOverPanel(this.canvas,784this.spriteDef.TEXT_SPRITE, this.spriteDef.RESTART,785this.dimensions);786} else {787this.gameOverPanel.draw();788}789790// Update the high score.791if (this.distanceRan > this.highestScore) {792this.highestScore = Math.ceil(this.distanceRan);793this.distanceMeter.setHighScore(this.highestScore);794}795796// Reset the time clock.797this.time = getTimeStamp();798},799800stop: function () {801this.playing = false;802this.paused = true;803cancelAnimationFrame(this.raqId);804this.raqId = 0;805},806807play: function () {808if (!this.crashed) {809this.playing = true;810this.paused = false;811this.tRex.update(0, Trex.status.RUNNING);812this.time = getTimeStamp();813this.update();814}815},816817restart: function () {818if (!this.raqId) {819this.playCount++;820this.runningTime = 0;821this.playing = true;822this.crashed = false;823this.distanceRan = 0;824this.setSpeed(this.config.SPEED);825this.time = getTimeStamp();826this.containerEl.classList.remove(Runner.classes.CRASHED);827this.clearCanvas();828this.distanceMeter.reset(this.highestScore);829this.horizon.reset();830this.tRex.reset();831this.playSound(this.soundFx.BUTTON_PRESS);832this.invert(true);833this.update();834}835},836837/**838* Pause the game if the tab is not in focus.839*/840onVisibilityChange: function (e) {841if (document.hidden || document.webkitHidden || e.type == 'blur' ||842document.visibilityState != 'visible') {843this.stop();844} else if (!this.crashed) {845this.tRex.reset();846this.play();847}848},849850/**851* Play a sound.852* @param {SoundBuffer} soundBuffer853*/854playSound: function (soundBuffer) {855if (soundBuffer) {856var sourceNode = this.audioContext.createBufferSource();857sourceNode.buffer = soundBuffer;858sourceNode.connect(this.audioContext.destination);859sourceNode.start(0);860}861},862863/**864* Inverts the current page / canvas colors.865* @param {boolean} Whether to reset colors.866*/867invert: function (reset) {868if (reset) {869document.body.classList.toggle(Runner.classes.INVERTED, false);870this.invertTimer = 0;871this.inverted = false;872} else {873this.inverted = document.body.classList.toggle(Runner.classes.INVERTED,874this.invertTrigger);875}876}877};878879880/**881* Updates the canvas size taking into882* account the backing store pixel ratio and883* the device pixel ratio.884*885* See article by Paul Lewis:886* http://www.html5rocks.com/en/tutorials/canvas/hidpi/887*888* @param {HTMLCanvasElement} canvas889* @param {number} opt_width890* @param {number} opt_height891* @return {boolean} Whether the canvas was scaled.892*/893Runner.updateCanvasScaling = function (canvas, opt_width, opt_height) {894var context = canvas.getContext('2d');895896// Query the various pixel ratios897var devicePixelRatio = Math.floor(window.devicePixelRatio) || 1;898var backingStoreRatio = Math.floor(context.webkitBackingStorePixelRatio) || 1;899var ratio = devicePixelRatio / backingStoreRatio;900901// Upscale the canvas if the two ratios don't match902if (devicePixelRatio !== backingStoreRatio) {903var oldWidth = opt_width || canvas.width;904var oldHeight = opt_height || canvas.height;905906canvas.width = oldWidth * ratio;907canvas.height = oldHeight * ratio;908909canvas.style.width = oldWidth + 'px';910canvas.style.height = oldHeight + 'px';911912// Scale the context to counter the fact that we've manually scaled913// our canvas element.914context.scale(ratio, ratio);915return true;916} else if (devicePixelRatio == 1) {917// Reset the canvas width / height. Fixes scaling bug when the page is918// zoomed and the devicePixelRatio changes accordingly.919canvas.style.width = canvas.width + 'px';920canvas.style.height = canvas.height + 'px';921}922return false;923};924925926/**927* Get random number.928* @param {number} min929* @param {number} max930* @param {number}931*/932function getRandomNum(min, max) {933return Math.floor(Math.random() * (max - min + 1)) + min;934}935936937/**938* Vibrate on mobile devices.939* @param {number} duration Duration of the vibration in milliseconds.940*/941function vibrate(duration) {942if (IS_MOBILE && window.navigator.vibrate) {943window.navigator.vibrate(duration);944}945}946947948/**949* Create canvas element.950* @param {HTMLElement} container Element to append canvas to.951* @param {number} width952* @param {number} height953* @param {string} opt_classname954* @return {HTMLCanvasElement}955*/956function createCanvas(container, width, height, opt_classname) {957var canvas = document.createElement('canvas');958canvas.className = opt_classname ? Runner.classes.CANVAS + ' ' +959opt_classname : Runner.classes.CANVAS;960canvas.width = width;961canvas.height = height;962container.appendChild(canvas);963964return canvas;965}966967968/**969* Decodes the base 64 audio to ArrayBuffer used by Web Audio.970* @param {string} base64String971*/972function decodeBase64ToArrayBuffer(base64String) {973var len = (base64String.length / 4) * 3;974var str = atob(base64String);975var arrayBuffer = new ArrayBuffer(len);976var bytes = new Uint8Array(arrayBuffer);977978for (var i = 0; i < len; i++) {979bytes[i] = str.charCodeAt(i);980}981return bytes.buffer;982}983984985/**986* Return the current timestamp.987* @return {number}988*/989function getTimeStamp() {990return IS_IOS ? new Date().getTime() : performance.now();991}992993994//******************************************************************************995996997/**998* Game over panel.999* @param {!HTMLCanvasElement} canvas1000* @param {Object} textImgPos1001* @param {Object} restartImgPos1002* @param {!Object} dimensions Canvas dimensions.1003* @constructor1004*/1005function GameOverPanel(canvas, textImgPos, restartImgPos, dimensions) {1006this.canvas = canvas;1007this.canvasCtx = canvas.getContext('2d');1008this.canvasDimensions = dimensions;1009this.textImgPos = textImgPos;1010this.restartImgPos = restartImgPos;1011this.draw();1012};101310141015/**1016* Dimensions used in the panel.1017* @enum {number}1018*/1019GameOverPanel.dimensions = {1020TEXT_X: 0,1021TEXT_Y: 13,1022TEXT_WIDTH: 191,1023TEXT_HEIGHT: 11,1024RESTART_WIDTH: 36,1025RESTART_HEIGHT: 321026};102710281029GameOverPanel.prototype = {1030/**1031* Update the panel dimensions.1032* @param {number} width New canvas width.1033* @param {number} opt_height Optional new canvas height.1034*/1035updateDimensions: function (width, opt_height) {1036this.canvasDimensions.WIDTH = width;1037if (opt_height) {1038this.canvasDimensions.HEIGHT = opt_height;1039}1040},10411042/**1043* Draw the panel.1044*/1045draw: function () {1046var dimensions = GameOverPanel.dimensions;10471048var centerX = this.canvasDimensions.WIDTH / 2;10491050// Game over text.1051var textSourceX = dimensions.TEXT_X;1052var textSourceY = dimensions.TEXT_Y;1053var textSourceWidth = dimensions.TEXT_WIDTH;1054var textSourceHeight = dimensions.TEXT_HEIGHT;10551056var textTargetX = Math.round(centerX - (dimensions.TEXT_WIDTH / 2));1057var textTargetY = Math.round((this.canvasDimensions.HEIGHT - 25) / 3);1058var textTargetWidth = dimensions.TEXT_WIDTH;1059var textTargetHeight = dimensions.TEXT_HEIGHT;10601061var restartSourceWidth = dimensions.RESTART_WIDTH;1062var restartSourceHeight = dimensions.RESTART_HEIGHT;1063var restartTargetX = centerX - (dimensions.RESTART_WIDTH / 2);1064var restartTargetY = this.canvasDimensions.HEIGHT / 2;10651066if (IS_HIDPI) {1067textSourceY *= 2;1068textSourceX *= 2;1069textSourceWidth *= 2;1070textSourceHeight *= 2;1071restartSourceWidth *= 2;1072restartSourceHeight *= 2;1073}10741075textSourceX += this.textImgPos.x;1076textSourceY += this.textImgPos.y;10771078// Game over text from sprite.1079this.canvasCtx.drawImage(Runner.imageSprite,1080textSourceX, textSourceY, textSourceWidth, textSourceHeight,1081textTargetX, textTargetY, textTargetWidth, textTargetHeight);10821083// Restart button.1084this.canvasCtx.drawImage(Runner.imageSprite,1085this.restartImgPos.x, this.restartImgPos.y,1086restartSourceWidth, restartSourceHeight,1087restartTargetX, restartTargetY, dimensions.RESTART_WIDTH,1088dimensions.RESTART_HEIGHT);1089}1090};109110921093//******************************************************************************10941095/**1096* Check for a collision.1097* @param {!Obstacle} obstacle1098* @param {!Trex} tRex T-rex object.1099* @param {HTMLCanvasContext} opt_canvasCtx Optional canvas context for drawing1100* collision boxes.1101* @return {Array<CollisionBox>}1102*/1103function checkForCollision(obstacle, tRex, opt_canvasCtx) {1104var obstacleBoxXPos = Runner.defaultDimensions.WIDTH + obstacle.xPos;11051106// Adjustments are made to the bounding box as there is a 1 pixel white1107// border around the t-rex and obstacles.1108var tRexBox = new CollisionBox(1109tRex.xPos + 1,1110tRex.yPos + 1,1111tRex.config.WIDTH - 2,1112tRex.config.HEIGHT - 2);11131114var obstacleBox = new CollisionBox(1115obstacle.xPos + 1,1116obstacle.yPos + 1,1117obstacle.typeConfig.width * obstacle.size - 2,1118obstacle.typeConfig.height - 2);11191120// Debug outer box1121if (opt_canvasCtx) {1122drawCollisionBoxes(opt_canvasCtx, tRexBox, obstacleBox);1123}11241125// Simple outer bounds check.1126if (boxCompare(tRexBox, obstacleBox)) {1127var collisionBoxes = obstacle.collisionBoxes;1128var tRexCollisionBoxes = tRex.ducking ?1129Trex.collisionBoxes.DUCKING : Trex.collisionBoxes.RUNNING;11301131// Detailed axis aligned box check.1132for (var t = 0; t < tRexCollisionBoxes.length; t++) {1133for (var i = 0; i < collisionBoxes.length; i++) {1134// Adjust the box to actual positions.1135var adjTrexBox =1136createAdjustedCollisionBox(tRexCollisionBoxes[t], tRexBox);1137var adjObstacleBox =1138createAdjustedCollisionBox(collisionBoxes[i], obstacleBox);1139var crashed = boxCompare(adjTrexBox, adjObstacleBox);11401141// Draw boxes for debug.1142if (opt_canvasCtx) {1143drawCollisionBoxes(opt_canvasCtx, adjTrexBox, adjObstacleBox);1144}11451146if (crashed) {1147return [adjTrexBox, adjObstacleBox];1148}1149}1150}1151}1152return false;1153};115411551156/**1157* Adjust the collision box.1158* @param {!CollisionBox} box The original box.1159* @param {!CollisionBox} adjustment Adjustment box.1160* @return {CollisionBox} The adjusted collision box object.1161*/1162function createAdjustedCollisionBox(box, adjustment) {1163return new CollisionBox(1164box.x + adjustment.x,1165box.y + adjustment.y,1166box.width,1167box.height);1168};116911701171/**1172* Draw the collision boxes for debug.1173*/1174function drawCollisionBoxes(canvasCtx, tRexBox, obstacleBox) {1175canvasCtx.save();1176canvasCtx.strokeStyle = '#f00';1177canvasCtx.strokeRect(tRexBox.x, tRexBox.y, tRexBox.width, tRexBox.height);11781179canvasCtx.strokeStyle = '#0f0';1180canvasCtx.strokeRect(obstacleBox.x, obstacleBox.y,1181obstacleBox.width, obstacleBox.height);1182canvasCtx.restore();1183};118411851186/**1187* Compare two collision boxes for a collision.1188* @param {CollisionBox} tRexBox1189* @param {CollisionBox} obstacleBox1190* @return {boolean} Whether the boxes intersected.1191*/1192function boxCompare(tRexBox, obstacleBox) {1193var crashed = false;1194var tRexBoxX = tRexBox.x;1195var tRexBoxY = tRexBox.y;11961197var obstacleBoxX = obstacleBox.x;1198var obstacleBoxY = obstacleBox.y;11991200// Axis-Aligned Bounding Box method.1201if (tRexBox.x < obstacleBoxX + obstacleBox.width &&1202tRexBox.x + tRexBox.width > obstacleBoxX &&1203tRexBox.y < obstacleBox.y + obstacleBox.height &&1204tRexBox.height + tRexBox.y > obstacleBox.y) {1205crashed = true;1206}12071208return crashed;1209};121012111212//******************************************************************************12131214/**1215* Collision box object.1216* @param {number} x X position.1217* @param {number} y Y Position.1218* @param {number} w Width.1219* @param {number} h Height.1220*/1221function CollisionBox(x, y, w, h) {1222this.x = x;1223this.y = y;1224this.width = w;1225this.height = h;1226};122712281229//******************************************************************************12301231/**1232* Obstacle.1233* @param {HTMLCanvasCtx} canvasCtx1234* @param {Obstacle.type} type1235* @param {Object} spritePos Obstacle position in sprite.1236* @param {Object} dimensions1237* @param {number} gapCoefficient Mutipler in determining the gap.1238* @param {number} speed1239* @param {number} opt_xOffset1240*/1241function Obstacle(canvasCtx, type, spriteImgPos, dimensions,1242gapCoefficient, speed, opt_xOffset) {12431244this.canvasCtx = canvasCtx;1245this.spritePos = spriteImgPos;1246this.typeConfig = type;1247this.gapCoefficient = gapCoefficient;1248this.size = getRandomNum(1, Obstacle.MAX_OBSTACLE_LENGTH);1249this.dimensions = dimensions;1250this.remove = false;1251this.xPos = dimensions.WIDTH + (opt_xOffset || 0);1252this.yPos = 0;1253this.width = 0;1254this.collisionBoxes = [];1255this.gap = 0;1256this.speedOffset = 0;12571258// For animated obstacles.1259this.currentFrame = 0;1260this.timer = 0;12611262this.init(speed);1263};12641265/**1266* Coefficient for calculating the maximum gap.1267* @const1268*/1269Obstacle.MAX_GAP_COEFFICIENT = 1.5;12701271/**1272* Maximum obstacle grouping count.1273* @const1274*/1275Obstacle.MAX_OBSTACLE_LENGTH = 3,127612771278Obstacle.prototype = {1279/**1280* Initialise the DOM for the obstacle.1281* @param {number} speed1282*/1283init: function (speed) {1284this.cloneCollisionBoxes();12851286// Only allow sizing if we're at the right speed.1287if (this.size > 1 && this.typeConfig.multipleSpeed > speed) {1288this.size = 1;1289}12901291this.width = this.typeConfig.width * this.size;12921293// Check if obstacle can be positioned at various heights.1294if (Array.isArray(this.typeConfig.yPos)) {1295var yPosConfig = IS_MOBILE ? this.typeConfig.yPosMobile :1296this.typeConfig.yPos;1297this.yPos = yPosConfig[getRandomNum(0, yPosConfig.length - 1)];1298} else {1299this.yPos = this.typeConfig.yPos;1300}13011302this.draw();13031304// Make collision box adjustments,1305// Central box is adjusted to the size as one box.1306// ____ ______ ________1307// _| |-| _| |-| _| |-|1308// | |<->| | | |<--->| | | |<----->| |1309// | | 1 | | | | 2 | | | | 3 | |1310// |_|___|_| |_|_____|_| |_|_______|_|1311//1312if (this.size > 1) {1313this.collisionBoxes[1].width = this.width - this.collisionBoxes[0].width -1314this.collisionBoxes[2].width;1315this.collisionBoxes[2].x = this.width - this.collisionBoxes[2].width;1316}13171318// For obstacles that go at a different speed from the horizon.1319if (this.typeConfig.speedOffset) {1320this.speedOffset = Math.random() > 0.5 ? this.typeConfig.speedOffset :1321-this.typeConfig.speedOffset;1322}13231324this.gap = this.getGap(this.gapCoefficient, speed);1325},13261327/**1328* Draw and crop based on size.1329*/1330draw: function () {1331var sourceWidth = this.typeConfig.width;1332var sourceHeight = this.typeConfig.height;13331334if (IS_HIDPI) {1335sourceWidth = sourceWidth * 2;1336sourceHeight = sourceHeight * 2;1337}13381339// X position in sprite.1340var sourceX = (sourceWidth * this.size) * (0.5 * (this.size - 1)) +1341this.spritePos.x;13421343// Animation frames.1344if (this.currentFrame > 0) {1345sourceX += sourceWidth * this.currentFrame;1346}13471348this.canvasCtx.drawImage(Runner.imageSprite,1349sourceX, this.spritePos.y,1350sourceWidth * this.size, sourceHeight,1351this.xPos, this.yPos,1352this.typeConfig.width * this.size, this.typeConfig.height);1353},13541355/**1356* Obstacle frame update.1357* @param {number} deltaTime1358* @param {number} speed1359*/1360update: function (deltaTime, speed) {1361if (!this.remove) {1362if (this.typeConfig.speedOffset) {1363speed += this.speedOffset;1364}1365this.xPos -= Math.floor((speed * FPS / 1000) * deltaTime);13661367// Update frame1368if (this.typeConfig.numFrames) {1369this.timer += deltaTime;1370if (this.timer >= this.typeConfig.frameRate) {1371this.currentFrame =1372this.currentFrame == this.typeConfig.numFrames - 1 ?13730 : this.currentFrame + 1;1374this.timer = 0;1375}1376}1377this.draw();13781379if (!this.isVisible()) {1380this.remove = true;1381}1382}1383},13841385/**1386* Calculate a random gap size.1387* - Minimum gap gets wider as speed increses1388* @param {number} gapCoefficient1389* @param {number} speed1390* @return {number} The gap size.1391*/1392getGap: function (gapCoefficient, speed) {1393var minGap = Math.round(this.width * speed +1394this.typeConfig.minGap * gapCoefficient);1395var maxGap = Math.round(minGap * Obstacle.MAX_GAP_COEFFICIENT);1396return getRandomNum(minGap, maxGap);1397},13981399/**1400* Check if obstacle is visible.1401* @return {boolean} Whether the obstacle is in the game area.1402*/1403isVisible: function () {1404return this.xPos + this.width > 0;1405},14061407/**1408* Make a copy of the collision boxes, since these will change based on1409* obstacle type and size.1410*/1411cloneCollisionBoxes: function () {1412var collisionBoxes = this.typeConfig.collisionBoxes;14131414for (var i = collisionBoxes.length - 1; i >= 0; i--) {1415this.collisionBoxes[i] = new CollisionBox(collisionBoxes[i].x,1416collisionBoxes[i].y, collisionBoxes[i].width,1417collisionBoxes[i].height);1418}1419}1420};142114221423/**1424* Obstacle definitions.1425* minGap: minimum pixel space betweeen obstacles.1426* multipleSpeed: Speed at which multiples are allowed.1427* speedOffset: speed faster / slower than the horizon.1428* minSpeed: Minimum speed which the obstacle can make an appearance.1429*/1430Obstacle.types = [1431{1432type: 'CACTUS_SMALL',1433width: 17,1434height: 35,1435yPos: 105,1436multipleSpeed: 4,1437minGap: 120,1438minSpeed: 0,1439collisionBoxes: [1440new CollisionBox(0, 7, 5, 27),1441new CollisionBox(4, 0, 6, 34),1442new CollisionBox(10, 4, 7, 14)1443]1444},1445{1446type: 'CACTUS_LARGE',1447width: 25,1448height: 50,1449yPos: 90,1450multipleSpeed: 7,1451minGap: 120,1452minSpeed: 0,1453collisionBoxes: [1454new CollisionBox(0, 12, 7, 38),1455new CollisionBox(8, 0, 7, 49),1456new CollisionBox(13, 10, 10, 38)1457]1458},1459{1460type: 'PTERODACTYL',1461width: 46,1462height: 40,1463yPos: [100, 75, 50], // Variable height.1464yPosMobile: [100, 50], // Variable height mobile.1465multipleSpeed: 999,1466minSpeed: 8.5,1467minGap: 150,1468collisionBoxes: [1469new CollisionBox(15, 15, 16, 5),1470new CollisionBox(18, 21, 24, 6),1471new CollisionBox(2, 14, 4, 3),1472new CollisionBox(6, 10, 4, 7),1473new CollisionBox(10, 8, 6, 9)1474],1475numFrames: 2,1476frameRate: 1000 / 6,1477speedOffset: .81478}1479];148014811482//******************************************************************************1483/**1484* T-rex game character.1485* @param {HTMLCanvas} canvas1486* @param {Object} spritePos Positioning within image sprite.1487* @constructor1488*/1489function Trex(canvas, spritePos) {1490this.canvas = canvas;1491this.canvasCtx = canvas.getContext('2d');1492this.spritePos = spritePos;1493this.xPos = 0;1494this.yPos = 0;1495// Position when on the ground.1496this.groundYPos = 0;1497this.currentFrame = 0;1498this.currentAnimFrames = [];1499this.blinkDelay = 0;1500this.blinkCount = 0;1501this.animStartTime = 0;1502this.timer = 0;1503this.msPerFrame = 1000 / FPS;1504this.config = Trex.config;1505// Current status.1506this.status = Trex.status.WAITING;15071508this.jumping = false;1509this.ducking = false;1510this.jumpVelocity = 0;1511this.reachedMinHeight = false;1512this.speedDrop = false;1513this.jumpCount = 0;1514this.jumpspotX = 0;15151516this.init();1517};151815191520/**1521* T-rex player config.1522* @enum {number}1523*/1524Trex.config = {1525DROP_VELOCITY: -5,1526GRAVITY: 0.6,1527HEIGHT: 47,1528HEIGHT_DUCK: 25,1529INIITAL_JUMP_VELOCITY: -10,1530INTRO_DURATION: 1500,1531MAX_JUMP_HEIGHT: 30,1532MIN_JUMP_HEIGHT: 30,1533SPEED_DROP_COEFFICIENT: 3,1534SPRITE_WIDTH: 262,1535START_X_POS: 50,1536WIDTH: 44,1537WIDTH_DUCK: 591538};153915401541/**1542* Used in collision detection.1543* @type {Array<CollisionBox>}1544*/1545Trex.collisionBoxes = {1546DUCKING: [1547new CollisionBox(1, 18, 55, 25)1548],1549RUNNING: [1550new CollisionBox(22, 0, 17, 16),1551new CollisionBox(1, 18, 30, 9),1552new CollisionBox(10, 35, 14, 8),1553new CollisionBox(1, 24, 29, 5),1554new CollisionBox(5, 30, 21, 4),1555new CollisionBox(9, 34, 15, 4)1556]1557};155815591560/**1561* Animation states.1562* @enum {string}1563*/1564Trex.status = {1565CRASHED: 'CRASHED',1566DUCKING: 'DUCKING',1567JUMPING: 'JUMPING',1568RUNNING: 'RUNNING',1569WAITING: 'WAITING'1570};15711572/**1573* Blinking coefficient.1574* @const1575*/1576Trex.BLINK_TIMING = 7000;157715781579/**1580* Animation config for different states.1581* @enum {Object}1582*/1583Trex.animFrames = {1584WAITING: {1585frames: [44, 0],1586msPerFrame: 1000 / 31587},1588RUNNING: {1589frames: [88, 132],1590msPerFrame: 1000 / 121591},1592CRASHED: {1593frames: [220],1594msPerFrame: 1000 / 601595},1596JUMPING: {1597frames: [0],1598msPerFrame: 1000 / 601599},1600DUCKING: {1601frames: [264, 323],1602msPerFrame: 1000 / 81603}1604};160516061607Trex.prototype = {1608/**1609* T-rex player initaliser.1610* Sets the t-rex to blink at random intervals.1611*/1612init: function () {1613this.groundYPos = Runner.defaultDimensions.HEIGHT - this.config.HEIGHT -1614Runner.config.BOTTOM_PAD;1615this.yPos = this.groundYPos;1616this.minJumpHeight = this.groundYPos - this.config.MIN_JUMP_HEIGHT;16171618this.draw(0, 0);1619this.update(0, Trex.status.WAITING);1620},16211622/**1623* Setter for the jump velocity.1624* The approriate drop velocity is also set.1625*/1626setJumpVelocity: function (setting) {1627this.config.INIITAL_JUMP_VELOCITY = -setting;1628this.config.DROP_VELOCITY = -setting / 2;1629},16301631/**1632* Set the animation status.1633* @param {!number} deltaTime1634* @param {Trex.status} status Optional status to switch to.1635*/1636update: function (deltaTime, opt_status) {1637this.timer += deltaTime;16381639// Update the status.1640if (opt_status) {1641this.status = opt_status;1642this.currentFrame = 0;1643this.msPerFrame = Trex.animFrames[opt_status].msPerFrame;1644this.currentAnimFrames = Trex.animFrames[opt_status].frames;16451646if (opt_status == Trex.status.WAITING) {1647this.animStartTime = getTimeStamp();1648this.setBlinkDelay();1649}1650}16511652// Game intro animation, T-rex moves in from the left.1653if (this.playingIntro && this.xPos < this.config.START_X_POS) {1654this.xPos += Math.round((this.config.START_X_POS /1655this.config.INTRO_DURATION) * deltaTime);1656}16571658if (this.status == Trex.status.WAITING) {1659this.blink(getTimeStamp());1660} else {1661this.draw(this.currentAnimFrames[this.currentFrame], 0);1662}16631664// Update the frame position.1665if (this.timer >= this.msPerFrame) {1666this.currentFrame = this.currentFrame ==1667this.currentAnimFrames.length - 1 ? 0 : this.currentFrame + 1;1668this.timer = 0;1669}16701671// Speed drop becomes duck if the down key is still being pressed.1672if (this.speedDrop && this.yPos == this.groundYPos) {1673this.speedDrop = false;1674this.setDuck(true);1675}1676},16771678/**1679* Draw the t-rex to a particular position.1680* @param {number} x1681* @param {number} y1682*/1683draw: function (x, y) {1684var sourceX = x;1685var sourceY = y;1686var sourceWidth = this.ducking && this.status != Trex.status.CRASHED ?1687this.config.WIDTH_DUCK : this.config.WIDTH;1688var sourceHeight = this.config.HEIGHT;16891690if (IS_HIDPI) {1691sourceX *= 2;1692sourceY *= 2;1693sourceWidth *= 2;1694sourceHeight *= 2;1695}16961697// Adjustments for sprite sheet position.1698sourceX += this.spritePos.x;1699sourceY += this.spritePos.y;17001701// Ducking.1702if (this.ducking && this.status != Trex.status.CRASHED) {1703this.canvasCtx.drawImage(Runner.imageSprite, sourceX, sourceY,1704sourceWidth, sourceHeight,1705this.xPos, this.yPos,1706this.config.WIDTH_DUCK, this.config.HEIGHT);1707} else {1708// Crashed whilst ducking. Trex is standing up so needs adjustment.1709if (this.ducking && this.status == Trex.status.CRASHED) {1710this.xPos++;1711}1712// Standing / running1713this.canvasCtx.drawImage(Runner.imageSprite, sourceX, sourceY,1714sourceWidth, sourceHeight,1715this.xPos, this.yPos,1716this.config.WIDTH, this.config.HEIGHT);1717}1718},17191720/**1721* Sets a random time for the blink to happen.1722*/1723setBlinkDelay: function () {1724this.blinkDelay = Math.ceil(Math.random() * Trex.BLINK_TIMING);1725},17261727/**1728* Make t-rex blink at random intervals.1729* @param {number} time Current time in milliseconds.1730*/1731blink: function (time) {1732var deltaTime = time - this.animStartTime;17331734if (deltaTime >= this.blinkDelay) {1735this.draw(this.currentAnimFrames[this.currentFrame], 0);17361737if (this.currentFrame == 1) {1738// Set new random delay to blink.1739this.setBlinkDelay();1740this.animStartTime = time;1741this.blinkCount++;1742}1743}1744},17451746/**1747* Initialise a jump.1748* @param {number} speed1749*/1750startJump: function (speed) {1751if (!this.jumping) {1752this.update(0, Trex.status.JUMPING);1753// Tweak the jump velocity based on the speed.1754this.jumpVelocity = this.config.INIITAL_JUMP_VELOCITY - (speed / 10);1755this.jumping = true;1756this.reachedMinHeight = false;1757this.speedDrop = false;1758}1759},17601761/**1762* Jump is complete, falling down.1763*/1764endJump: function () {1765if (this.reachedMinHeight &&1766this.jumpVelocity < this.config.DROP_VELOCITY) {1767this.jumpVelocity = this.config.DROP_VELOCITY;1768}1769},17701771/**1772* Update frame for a jump.1773* @param {number} deltaTime1774* @param {number} speed1775*/1776updateJump: function (deltaTime, speed) {1777var msPerFrame = Trex.animFrames[this.status].msPerFrame;1778var framesElapsed = deltaTime / msPerFrame;17791780// Speed drop makes Trex fall faster.1781if (this.speedDrop) {1782this.yPos += Math.round(this.jumpVelocity *1783this.config.SPEED_DROP_COEFFICIENT * framesElapsed);1784} else {1785this.yPos += Math.round(this.jumpVelocity * framesElapsed);1786}17871788this.jumpVelocity += this.config.GRAVITY * framesElapsed;17891790// Minimum height has been reached.1791if (this.yPos < this.minJumpHeight || this.speedDrop) {1792this.reachedMinHeight = true;1793}17941795// Reached max height1796if (this.yPos < this.config.MAX_JUMP_HEIGHT || this.speedDrop) {1797this.endJump();1798}17991800// Back down at ground level. Jump completed.1801if (this.yPos > this.groundYPos) {1802this.reset();1803this.jumpCount++;1804}18051806this.update(deltaTime);1807},18081809/**1810* Set the speed drop. Immediately cancels the current jump.1811*/1812setSpeedDrop: function () {1813this.speedDrop = true;1814this.jumpVelocity = 1;1815},18161817/**1818* @param {boolean} isDucking.1819*/1820setDuck: function (isDucking) {1821if (isDucking && this.status != Trex.status.DUCKING) {1822this.update(0, Trex.status.DUCKING);1823this.ducking = true;1824} else if (this.status == Trex.status.DUCKING) {1825this.update(0, Trex.status.RUNNING);1826this.ducking = false;1827}1828},18291830/**1831* Reset the t-rex to running at start of game.1832*/1833reset: function () {1834this.yPos = this.groundYPos;1835this.jumpVelocity = 0;1836this.jumping = false;1837this.ducking = false;1838this.update(0, Trex.status.RUNNING);1839this.midair = false;1840this.speedDrop = false;1841this.jumpCount = 0;1842}1843};184418451846//******************************************************************************18471848/**1849* Handles displaying the distance meter.1850* @param {!HTMLCanvasElement} canvas1851* @param {Object} spritePos Image position in sprite.1852* @param {number} canvasWidth1853* @constructor1854*/1855function DistanceMeter(canvas, spritePos, canvasWidth) {1856this.canvas = canvas;1857this.canvasCtx = canvas.getContext('2d');1858this.image = Runner.imageSprite;1859this.spritePos = spritePos;1860this.x = 0;1861this.y = 5;18621863this.currentDistance = 0;1864this.maxScore = 0;1865this.highScore = 0;1866this.container = null;18671868this.digits = [];1869this.acheivement = false;1870this.defaultString = '';1871this.flashTimer = 0;1872this.flashIterations = 0;1873this.invertTrigger = false;18741875this.config = DistanceMeter.config;1876this.maxScoreUnits = this.config.MAX_DISTANCE_UNITS;1877this.init(canvasWidth);1878};187918801881/**1882* @enum {number}1883*/1884DistanceMeter.dimensions = {1885WIDTH: 10,1886HEIGHT: 13,1887DEST_WIDTH: 111888};188918901891/**1892* Y positioning of the digits in the sprite sheet.1893* X position is always 0.1894* @type {Array<number>}1895*/1896DistanceMeter.yPos = [0, 13, 27, 40, 53, 67, 80, 93, 107, 120];189718981899/**1900* Distance meter config.1901* @enum {number}1902*/1903DistanceMeter.config = {1904// Number of digits.1905MAX_DISTANCE_UNITS: 5,19061907// Distance that causes achievement animation.1908ACHIEVEMENT_DISTANCE: 100,19091910// Used for conversion from pixel distance to a scaled unit.1911COEFFICIENT: 0.025,19121913// Flash duration in milliseconds.1914FLASH_DURATION: 1000 / 4,19151916// Flash iterations for achievement animation.1917FLASH_ITERATIONS: 31918};191919201921DistanceMeter.prototype = {1922/**1923* Initialise the distance meter to '00000'.1924* @param {number} width Canvas width in px.1925*/1926init: function (width) {1927var maxDistanceStr = '';19281929this.calcXPos(width);1930this.maxScore = this.maxScoreUnits;1931for (var i = 0; i < this.maxScoreUnits; i++) {1932this.draw(i, 0);1933this.defaultString += '0';1934maxDistanceStr += '9';1935}19361937this.maxScore = parseInt(maxDistanceStr);1938},19391940/**1941* Calculate the xPos in the canvas.1942* @param {number} canvasWidth1943*/1944calcXPos: function (canvasWidth) {1945this.x = canvasWidth - (DistanceMeter.dimensions.DEST_WIDTH *1946(this.maxScoreUnits + 1));1947},19481949/**1950* Draw a digit to canvas.1951* @param {number} digitPos Position of the digit.1952* @param {number} value Digit value 0-9.1953* @param {boolean} opt_highScore Whether drawing the high score.1954*/1955draw: function (digitPos, value, opt_highScore) {1956var sourceWidth = DistanceMeter.dimensions.WIDTH;1957var sourceHeight = DistanceMeter.dimensions.HEIGHT;1958var sourceX = DistanceMeter.dimensions.WIDTH * value;1959var sourceY = 0;19601961var targetX = digitPos * DistanceMeter.dimensions.DEST_WIDTH;1962var targetY = this.y;1963var targetWidth = DistanceMeter.dimensions.WIDTH;1964var targetHeight = DistanceMeter.dimensions.HEIGHT;19651966// For high DPI we 2x source values.1967if (IS_HIDPI) {1968sourceWidth *= 2;1969sourceHeight *= 2;1970sourceX *= 2;1971}19721973sourceX += this.spritePos.x;1974sourceY += this.spritePos.y;19751976this.canvasCtx.save();19771978if (opt_highScore) {1979// Left of the current score.1980var highScoreX = this.x - (this.maxScoreUnits * 2) *1981DistanceMeter.dimensions.WIDTH;1982this.canvasCtx.translate(highScoreX, this.y);1983} else {1984this.canvasCtx.translate(this.x, this.y);1985}19861987this.canvasCtx.drawImage(this.image, sourceX, sourceY,1988sourceWidth, sourceHeight,1989targetX, targetY,1990targetWidth, targetHeight1991);19921993this.canvasCtx.restore();1994},19951996/**1997* Covert pixel distance to a 'real' distance.1998* @param {number} distance Pixel distance ran.1999* @return {number} The 'real' distance ran.2000*/2001getActualDistance: function (distance) {2002return distance ? Math.round(distance * this.config.COEFFICIENT) : 0;2003},20042005/**2006* Update the distance meter.2007* @param {number} distance2008* @param {number} deltaTime2009* @return {boolean} Whether the acheivement sound fx should be played.2010*/2011update: function (deltaTime, distance) {2012var paint = true;2013var playSound = false;20142015if (!this.acheivement) {2016distance = this.getActualDistance(distance);2017// Score has gone beyond the initial digit count.2018if (distance > this.maxScore && this.maxScoreUnits ==2019this.config.MAX_DISTANCE_UNITS) {2020this.maxScoreUnits++;2021this.maxScore = parseInt(this.maxScore + '9');2022} else {2023this.distance = 0;2024}20252026if (distance > 0) {2027// Acheivement unlocked2028if (distance % this.config.ACHIEVEMENT_DISTANCE == 0) {2029// Flash score and play sound.2030this.acheivement = true;2031this.flashTimer = 0;2032playSound = true;2033}20342035// Create a string representation of the distance with leading 0.2036var distanceStr = (this.defaultString +2037distance).substr(-this.maxScoreUnits);2038this.digits = distanceStr.split('');2039} else {2040this.digits = this.defaultString.split('');2041}2042} else {2043// Control flashing of the score on reaching acheivement.2044if (this.flashIterations <= this.config.FLASH_ITERATIONS) {2045this.flashTimer += deltaTime;20462047if (this.flashTimer < this.config.FLASH_DURATION) {2048paint = false;2049} else if (this.flashTimer >2050this.config.FLASH_DURATION * 2) {2051this.flashTimer = 0;2052this.flashIterations++;2053}2054} else {2055this.acheivement = false;2056this.flashIterations = 0;2057this.flashTimer = 0;2058}2059}20602061// Draw the digits if not flashing.2062if (paint) {2063for (var i = this.digits.length - 1; i >= 0; i--) {2064this.draw(i, parseInt(this.digits[i]));2065}2066}20672068this.drawHighScore();2069return playSound;2070},20712072/**2073* Draw the high score.2074*/2075drawHighScore: function () {2076this.canvasCtx.save();2077this.canvasCtx.globalAlpha = .8;2078for (var i = this.highScore.length - 1; i >= 0; i--) {2079this.draw(i, parseInt(this.highScore[i], 10), true);2080}2081this.canvasCtx.restore();2082},20832084/**2085* Set the highscore as a array string.2086* Position of char in the sprite: H - 10, I - 11.2087* @param {number} distance Distance ran in pixels.2088*/2089setHighScore: function (distance) {2090distance = this.getActualDistance(distance);2091var highScoreStr = (this.defaultString +2092distance).substr(-this.maxScoreUnits);20932094this.highScore = ['10', '11', ''].concat(highScoreStr.split(''));2095},20962097/**2098* Reset the distance meter back to '00000'.2099*/2100reset: function () {2101this.update(0);2102this.acheivement = false;2103}2104};210521062107//******************************************************************************21082109/**2110* Cloud background item.2111* Similar to an obstacle object but without collision boxes.2112* @param {HTMLCanvasElement} canvas Canvas element.2113* @param {Object} spritePos Position of image in sprite.2114* @param {number} containerWidth2115*/2116function Cloud(canvas, spritePos, containerWidth) {2117this.canvas = canvas;2118this.canvasCtx = this.canvas.getContext('2d');2119this.spritePos = spritePos;2120this.containerWidth = containerWidth;2121this.xPos = containerWidth;2122this.yPos = 0;2123this.remove = false;2124this.cloudGap = getRandomNum(Cloud.config.MIN_CLOUD_GAP,2125Cloud.config.MAX_CLOUD_GAP);21262127this.init();2128};212921302131/**2132* Cloud object config.2133* @enum {number}2134*/2135Cloud.config = {2136HEIGHT: 14,2137MAX_CLOUD_GAP: 400,2138MAX_SKY_LEVEL: 30,2139MIN_CLOUD_GAP: 100,2140MIN_SKY_LEVEL: 71,2141WIDTH: 462142};214321442145Cloud.prototype = {2146/**2147* Initialise the cloud. Sets the Cloud height.2148*/2149init: function () {2150this.yPos = getRandomNum(Cloud.config.MAX_SKY_LEVEL,2151Cloud.config.MIN_SKY_LEVEL);2152this.draw();2153},21542155/**2156* Draw the cloud.2157*/2158draw: function () {2159this.canvasCtx.save();2160var sourceWidth = Cloud.config.WIDTH;2161var sourceHeight = Cloud.config.HEIGHT;21622163if (IS_HIDPI) {2164sourceWidth = sourceWidth * 2;2165sourceHeight = sourceHeight * 2;2166}21672168this.canvasCtx.drawImage(Runner.imageSprite, this.spritePos.x,2169this.spritePos.y,2170sourceWidth, sourceHeight,2171this.xPos, this.yPos,2172Cloud.config.WIDTH, Cloud.config.HEIGHT);21732174this.canvasCtx.restore();2175},21762177/**2178* Update the cloud position.2179* @param {number} speed2180*/2181update: function (speed) {2182if (!this.remove) {2183this.xPos -= Math.ceil(speed);2184this.draw();21852186// Mark as removeable if no longer in the canvas.2187if (!this.isVisible()) {2188this.remove = true;2189}2190}2191},21922193/**2194* Check if the cloud is visible on the stage.2195* @return {boolean}2196*/2197isVisible: function () {2198return this.xPos + Cloud.config.WIDTH > 0;2199}2200};220122022203//******************************************************************************22042205/**2206* Nightmode shows a moon and stars on the horizon.2207*/2208function NightMode(canvas, spritePos, containerWidth) {2209this.spritePos = spritePos;2210this.canvas = canvas;2211this.canvasCtx = canvas.getContext('2d');2212this.xPos = containerWidth - 50;2213this.yPos = 30;2214this.currentPhase = 0;2215this.opacity = 0;2216this.containerWidth = containerWidth;2217this.stars = [];2218this.drawStars = false;2219this.placeStars();2220};22212222/**2223* @enum {number}2224*/2225NightMode.config = {2226FADE_SPEED: 0.035,2227HEIGHT: 40,2228MOON_SPEED: 0.25,2229NUM_STARS: 2,2230STAR_SIZE: 9,2231STAR_SPEED: 0.3,2232STAR_MAX_Y: 70,2233WIDTH: 202234};22352236NightMode.phases = [140, 120, 100, 60, 40, 20, 0];22372238NightMode.prototype = {2239/**2240* Update moving moon, changing phases.2241* @param {boolean} activated Whether night mode is activated.2242* @param {number} delta2243*/2244update: function (activated, delta) {2245// Moon phase.2246if (activated && this.opacity == 0) {2247this.currentPhase++;22482249if (this.currentPhase >= NightMode.phases.length) {2250this.currentPhase = 0;2251}2252}22532254// Fade in / out.2255if (activated && (this.opacity < 1 || this.opacity == 0)) {2256this.opacity += NightMode.config.FADE_SPEED;2257} else if (this.opacity > 0) {2258this.opacity -= NightMode.config.FADE_SPEED;2259}22602261// Set moon positioning.2262if (this.opacity > 0) {2263this.xPos = this.updateXPos(this.xPos, NightMode.config.MOON_SPEED);22642265// Update stars.2266if (this.drawStars) {2267for (var i = 0; i < NightMode.config.NUM_STARS; i++) {2268this.stars[i].x = this.updateXPos(this.stars[i].x,2269NightMode.config.STAR_SPEED);2270}2271}2272this.draw();2273} else {2274this.opacity = 0;2275this.placeStars();2276}2277this.drawStars = true;2278},22792280updateXPos: function (currentPos, speed) {2281if (currentPos < -NightMode.config.WIDTH) {2282currentPos = this.containerWidth;2283} else {2284currentPos -= speed;2285}2286return currentPos;2287},22882289draw: function () {2290var moonSourceWidth = this.currentPhase == 3 ? NightMode.config.WIDTH * 2 :2291NightMode.config.WIDTH;2292var moonSourceHeight = NightMode.config.HEIGHT;2293var moonSourceX = this.spritePos.x + NightMode.phases[this.currentPhase];2294var moonOutputWidth = moonSourceWidth;2295var starSize = NightMode.config.STAR_SIZE;2296var starSourceX = Runner.spriteDefinition.LDPI.STAR.x;22972298if (IS_HIDPI) {2299moonSourceWidth *= 2;2300moonSourceHeight *= 2;2301moonSourceX = this.spritePos.x +2302(NightMode.phases[this.currentPhase] * 2);2303starSize *= 2;2304starSourceX = Runner.spriteDefinition.HDPI.STAR.x;2305}23062307this.canvasCtx.save();2308this.canvasCtx.globalAlpha = this.opacity;23092310// Stars.2311if (this.drawStars) {2312for (var i = 0; i < NightMode.config.NUM_STARS; i++) {2313this.canvasCtx.drawImage(Runner.imageSprite,2314starSourceX, this.stars[i].sourceY, starSize, starSize,2315Math.round(this.stars[i].x), this.stars[i].y,2316NightMode.config.STAR_SIZE, NightMode.config.STAR_SIZE);2317}2318}23192320// Moon.2321this.canvasCtx.drawImage(Runner.imageSprite, moonSourceX,2322this.spritePos.y, moonSourceWidth, moonSourceHeight,2323Math.round(this.xPos), this.yPos,2324moonOutputWidth, NightMode.config.HEIGHT);23252326this.canvasCtx.globalAlpha = 1;2327this.canvasCtx.restore();2328},23292330// Do star placement.2331placeStars: function () {2332var segmentSize = Math.round(this.containerWidth /2333NightMode.config.NUM_STARS);23342335for (var i = 0; i < NightMode.config.NUM_STARS; i++) {2336this.stars[i] = {};2337this.stars[i].x = getRandomNum(segmentSize * i, segmentSize * (i + 1));2338this.stars[i].y = getRandomNum(0, NightMode.config.STAR_MAX_Y);23392340if (IS_HIDPI) {2341this.stars[i].sourceY = Runner.spriteDefinition.HDPI.STAR.y +2342NightMode.config.STAR_SIZE * 2 * i;2343} else {2344this.stars[i].sourceY = Runner.spriteDefinition.LDPI.STAR.y +2345NightMode.config.STAR_SIZE * i;2346}2347}2348},23492350reset: function () {2351this.currentPhase = 0;2352this.opacity = 0;2353this.update(false);2354}23552356};235723582359//******************************************************************************23602361/**2362* Horizon Line.2363* Consists of two connecting lines. Randomly assigns a flat / bumpy horizon.2364* @param {HTMLCanvasElement} canvas2365* @param {Object} spritePos Horizon position in sprite.2366* @constructor2367*/2368function HorizonLine(canvas, spritePos) {2369this.spritePos = spritePos;2370this.canvas = canvas;2371this.canvasCtx = canvas.getContext('2d');2372this.sourceDimensions = {};2373this.dimensions = HorizonLine.dimensions;2374this.sourceXPos = [this.spritePos.x, this.spritePos.x +2375this.dimensions.WIDTH];2376this.xPos = [];2377this.yPos = 0;2378this.bumpThreshold = 0.5;23792380this.setSourceDimensions();2381this.draw();2382};238323842385/**2386* Horizon line dimensions.2387* @enum {number}2388*/2389HorizonLine.dimensions = {2390WIDTH: 600,2391HEIGHT: 12,2392YPOS: 1272393};239423952396HorizonLine.prototype = {2397/**2398* Set the source dimensions of the horizon line.2399*/2400setSourceDimensions: function () {24012402for (var dimension in HorizonLine.dimensions) {2403if (IS_HIDPI) {2404if (dimension != 'YPOS') {2405this.sourceDimensions[dimension] =2406HorizonLine.dimensions[dimension] * 2;2407}2408} else {2409this.sourceDimensions[dimension] =2410HorizonLine.dimensions[dimension];2411}2412this.dimensions[dimension] = HorizonLine.dimensions[dimension];2413}24142415this.xPos = [0, HorizonLine.dimensions.WIDTH];2416this.yPos = HorizonLine.dimensions.YPOS;2417},24182419/**2420* Return the crop x position of a type.2421*/2422getRandomType: function () {2423return Math.random() > this.bumpThreshold ? this.dimensions.WIDTH : 0;2424},24252426/**2427* Draw the horizon line.2428*/2429draw: function () {2430this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[0],2431this.spritePos.y,2432this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,2433this.xPos[0], this.yPos,2434this.dimensions.WIDTH, this.dimensions.HEIGHT);24352436this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[1],2437this.spritePos.y,2438this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,2439this.xPos[1], this.yPos,2440this.dimensions.WIDTH, this.dimensions.HEIGHT);2441},24422443/**2444* Update the x position of an indivdual piece of the line.2445* @param {number} pos Line position.2446* @param {number} increment2447*/2448updateXPos: function (pos, increment) {2449var line1 = pos;2450var line2 = pos == 0 ? 1 : 0;24512452this.xPos[line1] -= increment;2453this.xPos[line2] = this.xPos[line1] + this.dimensions.WIDTH;24542455if (this.xPos[line1] <= -this.dimensions.WIDTH) {2456this.xPos[line1] += this.dimensions.WIDTH * 2;2457this.xPos[line2] = this.xPos[line1] - this.dimensions.WIDTH;2458this.sourceXPos[line1] = this.getRandomType() + this.spritePos.x;2459}2460},24612462/**2463* Update the horizon line.2464* @param {number} deltaTime2465* @param {number} speed2466*/2467update: function (deltaTime, speed) {2468var increment = Math.floor(speed * (FPS / 1000) * deltaTime);24692470if (this.xPos[0] <= 0) {2471this.updateXPos(0, increment);2472} else {2473this.updateXPos(1, increment);2474}2475this.draw();2476},24772478/**2479* Reset horizon to the starting position.2480*/2481reset: function () {2482this.xPos[0] = 0;2483this.xPos[1] = HorizonLine.dimensions.WIDTH;2484}2485};248624872488//******************************************************************************24892490/**2491* Horizon background class.2492* @param {HTMLCanvasElement} canvas2493* @param {Object} spritePos Sprite positioning.2494* @param {Object} dimensions Canvas dimensions.2495* @param {number} gapCoefficient2496* @constructor2497*/2498function Horizon(canvas, spritePos, dimensions, gapCoefficient) {2499this.canvas = canvas;2500this.canvasCtx = this.canvas.getContext('2d');2501this.config = Horizon.config;2502this.dimensions = dimensions;2503this.gapCoefficient = gapCoefficient;2504this.obstacles = [];2505this.obstacleHistory = [];2506this.horizonOffsets = [0, 0];2507this.cloudFrequency = this.config.CLOUD_FREQUENCY;2508this.spritePos = spritePos;2509this.nightMode = null;25102511// Cloud2512this.clouds = [];2513this.cloudSpeed = this.config.BG_CLOUD_SPEED;25142515// Horizon2516this.horizonLine = null;2517this.init();2518};251925202521/**2522* Horizon config.2523* @enum {number}2524*/2525Horizon.config = {2526BG_CLOUD_SPEED: 0.2,2527BUMPY_THRESHOLD: .3,2528CLOUD_FREQUENCY: .5,2529HORIZON_HEIGHT: 16,2530MAX_CLOUDS: 62531};253225332534Horizon.prototype = {2535/**2536* Initialise the horizon. Just add the line and a cloud. No obstacles.2537*/2538init: function () {2539this.addCloud();2540this.horizonLine = new HorizonLine(this.canvas, this.spritePos.HORIZON);2541this.nightMode = new NightMode(this.canvas, this.spritePos.MOON,2542this.dimensions.WIDTH);2543},25442545/**2546* @param {number} deltaTime2547* @param {number} currentSpeed2548* @param {boolean} updateObstacles Used as an override to prevent2549* the obstacles from being updated / added. This happens in the2550* ease in section.2551* @param {boolean} showNightMode Night mode activated.2552*/2553update: function (deltaTime, currentSpeed, updateObstacles, showNightMode) {2554this.runningTime += deltaTime;2555this.horizonLine.update(deltaTime, currentSpeed);2556this.nightMode.update(showNightMode);2557this.updateClouds(deltaTime, currentSpeed);25582559if (updateObstacles) {2560this.updateObstacles(deltaTime, currentSpeed);2561}2562},25632564/**2565* Update the cloud positions.2566* @param {number} deltaTime2567* @param {number} currentSpeed2568*/2569updateClouds: function (deltaTime, speed) {2570var cloudSpeed = this.cloudSpeed / 1000 * deltaTime * speed;2571var numClouds = this.clouds.length;25722573if (numClouds) {2574for (var i = numClouds - 1; i >= 0; i--) {2575this.clouds[i].update(cloudSpeed);2576}25772578var lastCloud = this.clouds[numClouds - 1];25792580// Check for adding a new cloud.2581if (numClouds < this.config.MAX_CLOUDS &&2582(this.dimensions.WIDTH - lastCloud.xPos) > lastCloud.cloudGap &&2583this.cloudFrequency > Math.random()) {2584this.addCloud();2585}25862587// Remove expired clouds.2588this.clouds = this.clouds.filter(function (obj) {2589return !obj.remove;2590});2591} else {2592this.addCloud();2593}2594},25952596/**2597* Update the obstacle positions.2598* @param {number} deltaTime2599* @param {number} currentSpeed2600*/2601updateObstacles: function (deltaTime, currentSpeed) {2602// Obstacles, move to Horizon layer.2603var updatedObstacles = this.obstacles.slice(0);26042605for (var i = 0; i < this.obstacles.length; i++) {2606var obstacle = this.obstacles[i];2607obstacle.update(deltaTime, currentSpeed);26082609// Clean up existing obstacles.2610if (obstacle.remove) {2611updatedObstacles.shift();2612}2613}2614this.obstacles = updatedObstacles;26152616if (this.obstacles.length > 0) {2617var lastObstacle = this.obstacles[this.obstacles.length - 1];26182619if (lastObstacle && !lastObstacle.followingObstacleCreated &&2620lastObstacle.isVisible() &&2621(lastObstacle.xPos + lastObstacle.width + lastObstacle.gap) <2622this.dimensions.WIDTH) {2623this.addNewObstacle(currentSpeed);2624lastObstacle.followingObstacleCreated = true;2625}2626} else {2627// Create new obstacles.2628this.addNewObstacle(currentSpeed);2629}2630},26312632removeFirstObstacle: function () {2633this.obstacles.shift();2634},26352636/**2637* Add a new obstacle.2638* @param {number} currentSpeed2639*/2640addNewObstacle: function (currentSpeed) {2641var obstacleTypeIndex = getRandomNum(0, Obstacle.types.length - 1);2642var obstacleType = Obstacle.types[obstacleTypeIndex];26432644// Check for multiples of the same type of obstacle.2645// Also check obstacle is available at current speed.2646if (this.duplicateObstacleCheck(obstacleType.type) ||2647currentSpeed < obstacleType.minSpeed) {2648this.addNewObstacle(currentSpeed);2649} else {2650var obstacleSpritePos = this.spritePos[obstacleType.type];26512652this.obstacles.push(new Obstacle(this.canvasCtx, obstacleType,2653obstacleSpritePos, this.dimensions,2654this.gapCoefficient, currentSpeed, obstacleType.width));26552656this.obstacleHistory.unshift(obstacleType.type);26572658if (this.obstacleHistory.length > 1) {2659this.obstacleHistory.splice(Runner.config.MAX_OBSTACLE_DUPLICATION);2660}2661}2662},26632664/**2665* Returns whether the previous two obstacles are the same as the next one.2666* Maximum duplication is set in config value MAX_OBSTACLE_DUPLICATION.2667* @return {boolean}2668*/2669duplicateObstacleCheck: function (nextObstacleType) {2670var duplicateCount = 0;26712672for (var i = 0; i < this.obstacleHistory.length; i++) {2673duplicateCount = this.obstacleHistory[i] == nextObstacleType ?2674duplicateCount + 1 : 0;2675}2676return duplicateCount >= Runner.config.MAX_OBSTACLE_DUPLICATION;2677},26782679/**2680* Reset the horizon layer.2681* Remove existing obstacles and reposition the horizon line.2682*/2683reset: function () {2684this.obstacles = [];2685this.horizonLine.reset();2686this.nightMode.reset();2687},26882689/**2690* Update the canvas width and scaling.2691* @param {number} width Canvas width.2692* @param {number} height Canvas height.2693*/2694resize: function (width, height) {2695this.canvas.width = width;2696this.canvas.height = height;2697},26982699/**2700* Add a new cloud to the horizon.2701*/2702addCloud: function () {2703this.clouds.push(new Cloud(this.canvas, this.spritePos.CLOUD,2704this.dimensions.WIDTH));2705}2706};2707})();270827092710function onDocumentLoad() {2711new Runner('.interstitial-wrapper');2712}27132714document.addEventListener('DOMContentLoaded', onDocumentLoad);271527162717