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