-
-
Save washingtonsoares/05c3d112182de7d5742eb572c90d197b to your computer and use it in GitHub Desktop.
| // Copyright (c) 2014 The Chromium Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| (function() { | |
| 'use strict'; | |
| /** | |
| * T-Rex runner. | |
| * @param {string} outerContainerId Outer containing element id. | |
| * @param {Object} opt_config | |
| * @constructor | |
| * @export | |
| */ | |
| function Runner(outerContainerId, opt_config) { | |
| // Singleton | |
| if (Runner.instance_) { | |
| return Runner.instance_; | |
| } | |
| Runner.instance_ = this; | |
| this.outerContainerEl = document.querySelector(outerContainerId); | |
| this.containerEl = null; | |
| this.snackbarEl = null; | |
| // A div to intercept touch events. Only set while (playing && useTouch). | |
| this.touchController = null; | |
| this.config = opt_config || Runner.config; | |
| // Logical dimensions of the container. | |
| this.dimensions = Runner.defaultDimensions; | |
| this.canvas = null; | |
| this.canvasCtx = null; | |
| this.tRex = null; | |
| this.distanceMeter = null; | |
| this.distanceRan = 0; | |
| this.highestScore = 0; | |
| this.syncHighestScore = false; | |
| this.time = 0; | |
| this.runningTime = 0; | |
| this.msPerFrame = 1000 / FPS; | |
| this.currentSpeed = this.config.SPEED; | |
| this.obstacles = []; | |
| this.activated = false; // Whether the easter egg has been activated. | |
| this.playing = false; // Whether the game is currently in play state. | |
| this.crashed = false; | |
| this.paused = false; | |
| this.inverted = false; | |
| this.invertTimer = 0; | |
| this.resizeTimerId_ = null; | |
| this.playCount = 0; | |
| // Sound FX. | |
| this.audioBuffer = null; | |
| this.soundFx = {}; | |
| // Global web audio context for playing sounds. | |
| this.audioContext = null; | |
| // Images. | |
| this.images = {}; | |
| this.imagesLoaded = 0; | |
| if (this.isDisabled()) { | |
| this.setupDisabledRunner(); | |
| } else { | |
| this.loadImages(); | |
| window['initializeEasterEggHighScore'] = | |
| this.initializeHighScore.bind(this); | |
| } | |
| } | |
| window['Runner'] = Runner; | |
| /** | |
| * Default game width. | |
| * @const | |
| */ | |
| var DEFAULT_WIDTH = 600; | |
| /** | |
| * Frames per second. | |
| * @const | |
| */ | |
| var FPS = 60; | |
| /** @const */ | |
| var IS_HIDPI = window.devicePixelRatio > 1; | |
| /** @const */ | |
| // iPads are returning "MacIntel" for iOS 13 (devices & simulators). | |
| // Chrome on macOS also returns "MacIntel" for navigator.platform, | |
| // but navigator.userAgent includes /Safari/. | |
| // TODO(crbug.com/998999): Fix navigator.userAgent such that it reliably | |
| // returns an agent string containing "CriOS". | |
| var IS_IOS = /iPad|iPhone|iPod|MacIntel/.test(window.navigator.platform) && | |
| !(/Safari/.test(window.navigator.userAgent)); | |
| /** @const */ | |
| var IS_MOBILE = /Android/.test(window.navigator.userAgent) || IS_IOS; | |
| /** @const */ | |
| var ARCADE_MODE_URL = 'chrome://dino/'; | |
| /** | |
| * Default game configuration. | |
| * @enum {number} | |
| */ | |
| Runner.config = { | |
| ACCELERATION: 0.001, | |
| BG_CLOUD_SPEED: 0.2, | |
| BOTTOM_PAD: 10, | |
| // Scroll Y threshold at which the game can be activated. | |
| CANVAS_IN_VIEW_OFFSET: -10, | |
| CLEAR_TIME: 3000, | |
| CLOUD_FREQUENCY: 0.5, | |
| GAMEOVER_CLEAR_TIME: 750, | |
| GAP_COEFFICIENT: 0.6, | |
| GRAVITY: 0.6, | |
| INITIAL_JUMP_VELOCITY: 12, | |
| INVERT_FADE_DURATION: 12000, | |
| INVERT_DISTANCE: 700, | |
| MAX_BLINK_COUNT: 3, | |
| MAX_CLOUDS: 6, | |
| MAX_OBSTACLE_LENGTH: 3, | |
| MAX_OBSTACLE_DUPLICATION: 2, | |
| MAX_SPEED: 13, | |
| MIN_JUMP_HEIGHT: 35, | |
| MOBILE_SPEED_COEFFICIENT: 1.2, | |
| RESOURCE_TEMPLATE_ID: 'audio-resources', | |
| SPEED: 6, | |
| SPEED_DROP_COEFFICIENT: 3, | |
| ARCADE_MODE_INITIAL_TOP_POSITION: 35, | |
| ARCADE_MODE_TOP_POSITION_PERCENT: 0.1 | |
| }; | |
| /** | |
| * Default dimensions. | |
| * @enum {string} | |
| */ | |
| Runner.defaultDimensions = { | |
| WIDTH: DEFAULT_WIDTH, | |
| HEIGHT: 150 | |
| }; | |
| /** | |
| * CSS class names. | |
| * @enum {string} | |
| */ | |
| Runner.classes = { | |
| ARCADE_MODE: 'arcade-mode', | |
| CANVAS: 'runner-canvas', | |
| CONTAINER: 'runner-container', | |
| CRASHED: 'crashed', | |
| ICON: 'icon-offline', | |
| INVERTED: 'inverted', | |
| SNACKBAR: 'snackbar', | |
| SNACKBAR_SHOW: 'snackbar-show', | |
| TOUCH_CONTROLLER: 'controller' | |
| }; | |
| /** | |
| * Sprite definition layout of the spritesheet. | |
| * @enum {Object} | |
| */ | |
| Runner.spriteDefinition = { | |
| LDPI: { | |
| CACTUS_LARGE: {x: 332, y: 2}, | |
| CACTUS_SMALL: {x: 228, y: 2}, | |
| CLOUD: {x: 86, y: 2}, | |
| HORIZON: {x: 2, y: 54}, | |
| MOON: {x: 484, y: 2}, | |
| PTERODACTYL: {x: 134, y: 2}, | |
| RESTART: {x: 2, y: 2}, | |
| TEXT_SPRITE: {x: 655, y: 2}, | |
| TREX: {x: 848, y: 2}, | |
| STAR: {x: 645, y: 2} | |
| }, | |
| HDPI: { | |
| CACTUS_LARGE: {x: 652, y: 2}, | |
| CACTUS_SMALL: {x: 446, y: 2}, | |
| CLOUD: {x: 166, y: 2}, | |
| HORIZON: {x: 2, y: 104}, | |
| MOON: {x: 954, y: 2}, | |
| PTERODACTYL: {x: 260, y: 2}, | |
| RESTART: {x: 2, y: 2}, | |
| TEXT_SPRITE: {x: 1294, y: 2}, | |
| TREX: {x: 1678, y: 2}, | |
| STAR: {x: 1276, y: 2} | |
| } | |
| }; | |
| /** | |
| * Sound FX. Reference to the ID of the audio tag on interstitial page. | |
| * @enum {string} | |
| */ | |
| Runner.sounds = { | |
| BUTTON_PRESS: 'offline-sound-press', | |
| HIT: 'offline-sound-hit', | |
| SCORE: 'offline-sound-reached' | |
| }; | |
| /** | |
| * Key code mapping. | |
| * @enum {Object} | |
| */ | |
| Runner.keycodes = { | |
| JUMP: {'38': 1, '32': 1}, // Up, spacebar | |
| DUCK: {'40': 1}, // Down | |
| RESTART: {'13': 1} // Enter | |
| }; | |
| /** | |
| * Runner event names. | |
| * @enum {string} | |
| */ | |
| Runner.events = { | |
| ANIM_END: 'webkitAnimationEnd', | |
| CLICK: 'click', | |
| KEYDOWN: 'keydown', | |
| KEYUP: 'keyup', | |
| POINTERDOWN: 'pointerdown', | |
| POINTERUP: 'pointerup', | |
| RESIZE: 'resize', | |
| TOUCHEND: 'touchend', | |
| TOUCHSTART: 'touchstart', | |
| VISIBILITY: 'visibilitychange', | |
| BLUR: 'blur', | |
| FOCUS: 'focus', | |
| LOAD: 'load' | |
| }; | |
| Runner.prototype = { | |
| /** | |
| * Whether the easter egg has been disabled. CrOS enterprise enrolled devices. | |
| * @return {boolean} | |
| */ | |
| isDisabled: function() { | |
| return loadTimeData && loadTimeData.valueExists('disabledEasterEgg'); | |
| }, | |
| /** | |
| * For disabled instances, set up a snackbar with the disabled message. | |
| */ | |
| setupDisabledRunner: function() { | |
| this.containerEl = document.createElement('div'); | |
| this.containerEl.className = Runner.classes.SNACKBAR; | |
| this.containerEl.textContent = loadTimeData.getValue('disabledEasterEgg'); | |
| this.outerContainerEl.appendChild(this.containerEl); | |
| // Show notification when the activation key is pressed. | |
| document.addEventListener(Runner.events.KEYDOWN, function(e) { | |
| if (Runner.keycodes.JUMP[e.keyCode]) { | |
| this.containerEl.classList.add(Runner.classes.SNACKBAR_SHOW); | |
| document.querySelector('.icon').classList.add('icon-disabled'); | |
| } | |
| }.bind(this)); | |
| }, | |
| /** | |
| * Setting individual settings for debugging. | |
| * @param {string} setting | |
| * @param {*} value | |
| */ | |
| updateConfigSetting: function(setting, value) { | |
| if (setting in this.config && value != undefined) { | |
| this.config[setting] = value; | |
| switch (setting) { | |
| case 'GRAVITY': | |
| case 'MIN_JUMP_HEIGHT': | |
| case 'SPEED_DROP_COEFFICIENT': | |
| this.tRex.config[setting] = value; | |
| break; | |
| case 'INITIAL_JUMP_VELOCITY': | |
| this.tRex.setJumpVelocity(value); | |
| break; | |
| case 'SPEED': | |
| this.setSpeed(value); | |
| break; | |
| } | |
| } | |
| }, | |
| /** | |
| * Cache the appropriate image sprite from the page and get the sprite sheet | |
| * definition. | |
| */ | |
| loadImages: function() { | |
| if (IS_HIDPI) { | |
| Runner.imageSprite = document.getElementById('offline-resources-2x'); | |
| this.spriteDef = Runner.spriteDefinition.HDPI; | |
| } else { | |
| Runner.imageSprite = document.getElementById('offline-resources-1x'); | |
| this.spriteDef = Runner.spriteDefinition.LDPI; | |
| } | |
| if (Runner.imageSprite.complete) { | |
| this.init(); | |
| } else { | |
| // If the images are not yet loaded, add a listener. | |
| Runner.imageSprite.addEventListener(Runner.events.LOAD, | |
| this.init.bind(this)); | |
| } | |
| }, | |
| /** | |
| * Load and decode base 64 encoded sounds. | |
| */ | |
| loadSounds: function() { | |
| if (!IS_IOS) { | |
| this.audioContext = new AudioContext(); | |
| var resourceTemplate = | |
| document.getElementById(this.config.RESOURCE_TEMPLATE_ID).content; | |
| for (var sound in Runner.sounds) { | |
| var soundSrc = | |
| resourceTemplate.getElementById(Runner.sounds[sound]).src; | |
| soundSrc = soundSrc.substr(soundSrc.indexOf(',') + 1); | |
| var buffer = decodeBase64ToArrayBuffer(soundSrc); | |
| // Async, so no guarantee of order in array. | |
| this.audioContext.decodeAudioData(buffer, function(index, audioData) { | |
| this.soundFx[index] = audioData; | |
| }.bind(this, sound)); | |
| } | |
| } | |
| }, | |
| /** | |
| * Sets the game speed. Adjust the speed accordingly if on a smaller screen. | |
| * @param {number} opt_speed | |
| */ | |
| setSpeed: function(opt_speed) { | |
| var speed = opt_speed || this.currentSpeed; | |
| // Reduce the speed on smaller mobile screens. | |
| if (this.dimensions.WIDTH < DEFAULT_WIDTH) { | |
| var mobileSpeed = speed * this.dimensions.WIDTH / DEFAULT_WIDTH * | |
| this.config.MOBILE_SPEED_COEFFICIENT; | |
| this.currentSpeed = mobileSpeed > speed ? speed : mobileSpeed; | |
| } else if (opt_speed) { | |
| this.currentSpeed = opt_speed; | |
| } | |
| }, | |
| /** | |
| * Game initialiser. | |
| */ | |
| init: function() { | |
| // Hide the static icon. | |
| document.querySelector('.' + Runner.classes.ICON).style.visibility = | |
| 'hidden'; | |
| this.adjustDimensions(); | |
| this.setSpeed(); | |
| this.containerEl = document.createElement('div'); | |
| this.containerEl.className = Runner.classes.CONTAINER; | |
| // Player canvas container. | |
| this.canvas = createCanvas(this.containerEl, this.dimensions.WIDTH, | |
| this.dimensions.HEIGHT, Runner.classes.PLAYER); | |
| this.canvasCtx = this.canvas.getContext('2d'); | |
| this.canvasCtx.fillStyle = '#f7f7f7'; | |
| this.canvasCtx.fill(); | |
| Runner.updateCanvasScaling(this.canvas); | |
| // Horizon contains clouds, obstacles and the ground. | |
| this.horizon = new Horizon(this.canvas, this.spriteDef, this.dimensions, | |
| this.config.GAP_COEFFICIENT); | |
| // Distance meter | |
| this.distanceMeter = new DistanceMeter(this.canvas, | |
| this.spriteDef.TEXT_SPRITE, this.dimensions.WIDTH); | |
| // Draw t-rex | |
| this.tRex = new Trex(this.canvas, this.spriteDef.TREX); | |
| this.outerContainerEl.appendChild(this.containerEl); | |
| // | |
| this.startListening(); | |
| this.update(); | |
| window.addEventListener(Runner.events.RESIZE, | |
| this.debounceResize.bind(this)); | |
| }, | |
| /** | |
| * Create the touch controller. A div that covers whole screen. | |
| */ | |
| createTouchController: function() { | |
| this.touchController = document.createElement('div'); | |
| this.touchController.className = Runner.classes.TOUCH_CONTROLLER; | |
| this.touchController.addEventListener(Runner.events.TOUCHSTART, this); | |
| this.touchController.addEventListener(Runner.events.TOUCHEND, this); | |
| this.outerContainerEl.appendChild(this.touchController); | |
| }, | |
| /** | |
| * Debounce the resize event. | |
| */ | |
| debounceResize: function() { | |
| if (!this.resizeTimerId_) { | |
| this.resizeTimerId_ = | |
| setInterval(this.adjustDimensions.bind(this), 250); | |
| } | |
| }, | |
| /** | |
| * Adjust game space dimensions on resize. | |
| */ | |
| adjustDimensions: function() { | |
| clearInterval(this.resizeTimerId_); | |
| this.resizeTimerId_ = null; | |
| var boxStyles = window.getComputedStyle(this.outerContainerEl); | |
| var padding = Number(boxStyles.paddingLeft.substr(0, | |
| boxStyles.paddingLeft.length - 2)); | |
| this.dimensions.WIDTH = this.outerContainerEl.offsetWidth - padding * 2; | |
| if (this.isArcadeMode()) { | |
| this.dimensions.WIDTH = Math.min(DEFAULT_WIDTH, this.dimensions.WIDTH); | |
| if (this.activated) { | |
| this.setArcadeModeContainerScale(); | |
| } | |
| } | |
| // Redraw the elements back onto the canvas. | |
| if (this.canvas) { | |
| this.canvas.width = this.dimensions.WIDTH; | |
| this.canvas.height = this.dimensions.HEIGHT; | |
| Runner.updateCanvasScaling(this.canvas); | |
| this.distanceMeter.calcXPos(this.dimensions.WIDTH); | |
| this.clearCanvas(); | |
| this.horizon.update(0, 0, true); | |
| this.tRex.update(0); | |
| // Outer container and distance meter. | |
| if (this.playing || this.crashed || this.paused) { | |
| this.containerEl.style.width = this.dimensions.WIDTH + 'px'; | |
| this.containerEl.style.height = this.dimensions.HEIGHT + 'px'; | |
| this.distanceMeter.update(0, Math.ceil(this.distanceRan)); | |
| this.stop(); | |
| } else { | |
| this.tRex.draw(0, 0); | |
| } | |
| // Game over panel. | |
| if (this.crashed && this.gameOverPanel) { | |
| this.gameOverPanel.updateDimensions(this.dimensions.WIDTH); | |
| this.gameOverPanel.draw(); | |
| } | |
| } | |
| }, | |
| /** | |
| * Play the game intro. | |
| * Canvas container width expands out to the full width. | |
| */ | |
| playIntro: function() { | |
| if (!this.activated && !this.crashed) { | |
| this.playingIntro = true; | |
| this.tRex.playingIntro = true; | |
| // | |
| // CSS animation definition. | |
| var keyframes = '@-webkit-keyframes intro { ' + | |
| 'from { width:' + Trex.config.WIDTH + 'px }' + | |
| 'to { width: ' + this.dimensions.WIDTH + 'px }' + | |
| '}'; | |
| document.styleSheets[0].insertRule(keyframes, 0); | |
| this.containerEl.addEventListener(Runner.events.ANIM_END, | |
| this.startGame.bind(this)); | |
| this.containerEl.style.webkitAnimation = 'intro .4s ease-out 1 both'; | |
| this.containerEl.style.width = this.dimensions.WIDTH + 'px'; | |
| this.setPlayStatus(true); | |
| this.activated = true; | |
| } else if (this.crashed) { | |
| this.restart(); | |
| } | |
| }, | |
| /** | |
| * Update the game status to started. | |
| */ | |
| startGame: function() { | |
| if (this.isArcadeMode()) { | |
| this.setArcadeMode(); | |
| } | |
| this.runningTime = 0; | |
| this.playingIntro = false; | |
| this.tRex.playingIntro = false; | |
| this.containerEl.style.webkitAnimation = ''; | |
| this.playCount++; | |
| // Handle tabbing off the page. Pause the current game. | |
| document.addEventListener(Runner.events.VISIBILITY, | |
| this.onVisibilityChange.bind(this)); | |
| window.addEventListener(Runner.events.BLUR, | |
| this.onVisibilityChange.bind(this)); | |
| window.addEventListener(Runner.events.FOCUS, | |
| this.onVisibilityChange.bind(this)); | |
| }, | |
| clearCanvas: function() { | |
| this.canvasCtx.clearRect(0, 0, this.dimensions.WIDTH, | |
| this.dimensions.HEIGHT); | |
| }, | |
| /** | |
| * Checks whether the canvas area is in the viewport of the browser | |
| * through the current scroll position. | |
| * @return boolean. | |
| */ | |
| isCanvasInView: function() { | |
| return this.containerEl.getBoundingClientRect().top > | |
| Runner.config.CANVAS_IN_VIEW_OFFSET; | |
| }, | |
| /** | |
| * Update the game frame and schedules the next one. | |
| */ | |
| update: function() { | |
| this.updatePending = false; | |
| var now = getTimeStamp(); | |
| var deltaTime = now - (this.time || now); | |
| this.time = now; | |
| if (this.playing) { | |
| this.clearCanvas(); | |
| if (this.tRex.jumping) { | |
| this.tRex.updateJump(deltaTime); | |
| } | |
| this.runningTime += deltaTime; | |
| var hasObstacles = this.runningTime > this.config.CLEAR_TIME; | |
| // First jump triggers the intro. | |
| if (this.tRex.jumpCount == 1 && !this.playingIntro) { | |
| this.playIntro(); | |
| } | |
| // The horizon doesn't move until the intro is over. | |
| if (this.playingIntro) { | |
| this.horizon.update(0, this.currentSpeed, hasObstacles); | |
| } else { | |
| deltaTime = !this.activated ? 0 : deltaTime; | |
| this.horizon.update(deltaTime, this.currentSpeed, hasObstacles, | |
| this.inverted); | |
| } | |
| // Check for collisions. | |
| var collision = hasObstacles && | |
| checkForCollision(this.horizon.obstacles[0], this.tRex); | |
| if (!collision) { | |
| this.distanceRan += this.currentSpeed * deltaTime / this.msPerFrame; | |
| if (this.currentSpeed < this.config.MAX_SPEED) { | |
| this.currentSpeed += this.config.ACCELERATION; | |
| } | |
| } else { | |
| this.gameOver(); | |
| } | |
| var playAchievementSound = this.distanceMeter.update(deltaTime, | |
| Math.ceil(this.distanceRan)); | |
| if (playAchievementSound) { | |
| this.playSound(this.soundFx.SCORE); | |
| } | |
| // Night mode. | |
| if (this.invertTimer > this.config.INVERT_FADE_DURATION) { | |
| this.invertTimer = 0; | |
| this.invertTrigger = false; | |
| this.invert(); | |
| } else if (this.invertTimer) { | |
| this.invertTimer += deltaTime; | |
| } else { | |
| var actualDistance = | |
| this.distanceMeter.getActualDistance(Math.ceil(this.distanceRan)); | |
| if (actualDistance > 0) { | |
| this.invertTrigger = !(actualDistance % | |
| this.config.INVERT_DISTANCE); | |
| if (this.invertTrigger && this.invertTimer === 0) { | |
| this.invertTimer += deltaTime; | |
| this.invert(); | |
| } | |
| } | |
| } | |
| } | |
| if (this.playing || (!this.activated && | |
| this.tRex.blinkCount < Runner.config.MAX_BLINK_COUNT)) { | |
| this.tRex.update(deltaTime); | |
| this.scheduleNextUpdate(); | |
| } | |
| }, | |
| /** | |
| * Event handler. | |
| */ | |
| handleEvent: function(e) { | |
| return (function(evtType, events) { | |
| switch (evtType) { | |
| case events.KEYDOWN: | |
| case events.TOUCHSTART: | |
| case events.POINTERDOWN: | |
| this.onKeyDown(e); | |
| break; | |
| case events.KEYUP: | |
| case events.TOUCHEND: | |
| case events.POINTERUP: | |
| this.onKeyUp(e); | |
| break; | |
| } | |
| }.bind(this))(e.type, Runner.events); | |
| }, | |
| /** | |
| * Bind relevant key / mouse / touch listeners. | |
| */ | |
| startListening: function() { | |
| // Keys. | |
| document.addEventListener(Runner.events.KEYDOWN, this); | |
| document.addEventListener(Runner.events.KEYUP, this); | |
| // Touch / pointer. | |
| this.containerEl.addEventListener(Runner.events.TOUCHSTART, this); | |
| document.addEventListener(Runner.events.POINTERDOWN, this); | |
| document.addEventListener(Runner.events.POINTERUP, this); | |
| }, | |
| /** | |
| * Remove all listeners. | |
| */ | |
| stopListening: function() { | |
| document.removeEventListener(Runner.events.KEYDOWN, this); | |
| document.removeEventListener(Runner.events.KEYUP, this); | |
| if (this.touchController) { | |
| this.touchController.removeEventListener(Runner.events.TOUCHSTART, this); | |
| this.touchController.removeEventListener(Runner.events.TOUCHEND, this); | |
| } | |
| this.containerEl.removeEventListener(Runner.events.TOUCHSTART, this); | |
| document.removeEventListener(Runner.events.POINTERDOWN, this); | |
| document.removeEventListener(Runner.events.POINTERUP, this); | |
| }, | |
| /** | |
| * Process keydown. | |
| * @param {Event} e | |
| */ | |
| onKeyDown: function(e) { | |
| // Prevent native page scrolling whilst tapping on mobile. | |
| if (IS_MOBILE && this.playing) { | |
| e.preventDefault(); | |
| } | |
| if (this.isCanvasInView()) { | |
| if (!this.crashed && !this.paused) { | |
| if (Runner.keycodes.JUMP[e.keyCode] || | |
| e.type == Runner.events.TOUCHSTART) { | |
| e.preventDefault(); | |
| // Starting the game for the first time. | |
| if (!this.playing) { | |
| // Started by touch so create a touch controller. | |
| if (!this.touchController && e.type == Runner.events.TOUCHSTART) { | |
| this.createTouchController(); | |
| } | |
| this.loadSounds(); | |
| this.setPlayStatus(true); | |
| this.update(); | |
| if (window.errorPageController) { | |
| errorPageController.trackEasterEgg(); | |
| } | |
| } | |
| // Start jump. | |
| if (!this.tRex.jumping && !this.tRex.ducking) { | |
| this.playSound(this.soundFx.BUTTON_PRESS); | |
| this.tRex.startJump(this.currentSpeed); | |
| } | |
| } else if (this.playing && Runner.keycodes.DUCK[e.keyCode]) { | |
| e.preventDefault(); | |
| if (this.tRex.jumping) { | |
| // Speed drop, activated only when jump key is not pressed. | |
| this.tRex.setSpeedDrop(); | |
| } else if (!this.tRex.jumping && !this.tRex.ducking) { | |
| // Duck. | |
| this.tRex.setDuck(true); | |
| } | |
| } | |
| // iOS only triggers touchstart and no pointer events. | |
| } else if (IS_IOS && this.crashed && e.type == Runner.events.TOUCHSTART && | |
| e.currentTarget == this.containerEl) { | |
| this.handleGameOverClicks(e); | |
| } | |
| } | |
| }, | |
| /** | |
| * Process key up. | |
| * @param {Event} e | |
| */ | |
| onKeyUp: function(e) { | |
| var keyCode = String(e.keyCode); | |
| var isjumpKey = Runner.keycodes.JUMP[keyCode] || | |
| e.type == Runner.events.TOUCHEND || | |
| e.type == Runner.events.POINTERUP; | |
| if (this.isRunning() && isjumpKey) { | |
| this.tRex.endJump(); | |
| } else if (Runner.keycodes.DUCK[keyCode]) { | |
| this.tRex.speedDrop = false; | |
| this.tRex.setDuck(false); | |
| } else if (this.crashed) { | |
| // Check that enough time has elapsed before allowing jump key to restart. | |
| var deltaTime = getTimeStamp() - this.time; | |
| if (this.isCanvasInView() && | |
| (Runner.keycodes.RESTART[keyCode] || this.isLeftClickOnCanvas(e) || | |
| (deltaTime >= this.config.GAMEOVER_CLEAR_TIME && | |
| Runner.keycodes.JUMP[keyCode]))) { | |
| this.handleGameOverClicks(e); | |
| } | |
| } else if (this.paused && isjumpKey) { | |
| // Reset the jump state | |
| this.tRex.reset(); | |
| this.play(); | |
| } | |
| }, | |
| /** | |
| * Handle interactions on the game over screen state. | |
| * A user is able to tap the high score twice to reset it. | |
| * @param {Event} e | |
| */ | |
| handleGameOverClicks: function(e) { | |
| e.preventDefault(); | |
| if (this.distanceMeter.hasClickedOnHighScore(e) && this.highestScore) { | |
| if (this.distanceMeter.isHighScoreFlashing()) { | |
| // Subsequent click, reset the high score. | |
| this.saveHighScore(0, true); | |
| this.distanceMeter.resetHighScore(); | |
| } else { | |
| // First click, flash the high score. | |
| this.distanceMeter.startHighScoreFlashing(); | |
| } | |
| } else { | |
| this.distanceMeter.cancelHighScoreFlashing(); | |
| this.restart(); | |
| } | |
| }, | |
| /** | |
| * Returns whether the event was a left click on canvas. | |
| * On Windows right click is registered as a click. | |
| * @param {Event} e | |
| * @return {boolean} | |
| */ | |
| isLeftClickOnCanvas: function(e) { | |
| return e.button != null && e.button < 2 && | |
| e.type == Runner.events.POINTERUP && e.target == this.canvas; | |
| }, | |
| /** | |
| * RequestAnimationFrame wrapper. | |
| */ | |
| scheduleNextUpdate: function() { | |
| if (!this.updatePending) { | |
| this.updatePending = true; | |
| this.raqId = requestAnimationFrame(this.update.bind(this)); | |
| } | |
| }, | |
| /** | |
| * Whether the game is running. | |
| * @return {boolean} | |
| */ | |
| isRunning: function() { | |
| return !!this.raqId; | |
| }, | |
| /** | |
| * Set the initial high score as stored in the user's profile. | |
| * @param {integer} highScore | |
| */ | |
| initializeHighScore: function(highScore) { | |
| this.syncHighestScore = true; | |
| highScore = Math.ceil(highScore); | |
| if (highScore < this.highestScore) { | |
| if (window.errorPageController) { | |
| errorPageController.updateEasterEggHighScore(this.highestScore); | |
| } | |
| return; | |
| } | |
| this.highestScore = highScore; | |
| this.distanceMeter.setHighScore(this.highestScore); | |
| }, | |
| /** | |
| * Sets the current high score and saves to the profile if available. | |
| * @param {number} distanceRan Total distance ran. | |
| * @param {boolean} opt_resetScore Whether to reset the score. | |
| */ | |
| saveHighScore: function(distanceRan, opt_resetScore) { | |
| this.highestScore = Math.ceil(distanceRan); | |
| this.distanceMeter.setHighScore(this.highestScore); | |
| // Store the new high score in the profile. | |
| if (this.syncHighestScore && window.errorPageController) { | |
| if (opt_resetScore) { | |
| errorPageController.resetEasterEggHighScore(); | |
| } else { | |
| errorPageController.updateEasterEggHighScore(this.highestScore); | |
| } | |
| } | |
| }, | |
| /** | |
| * Game over state. | |
| */ | |
| gameOver: function() { | |
| this.playSound(this.soundFx.HIT); | |
| vibrate(200); | |
| this.stop(); | |
| this.crashed = true; | |
| this.distanceMeter.achievement = false; | |
| this.tRex.update(100, Trex.status.CRASHED); | |
| // Game over panel. | |
| if (!this.gameOverPanel) { | |
| this.gameOverPanel = new GameOverPanel(this.canvas, | |
| this.spriteDef.TEXT_SPRITE, this.spriteDef.RESTART, | |
| this.dimensions); | |
| } else { | |
| this.gameOverPanel.draw(); | |
| } | |
| // Update the high score. | |
| if (this.distanceRan > this.highestScore) { | |
| this.saveHighScore(this.distanceRan); | |
| } | |
| // Reset the time clock. | |
| this.time = getTimeStamp(); | |
| }, | |
| stop: function() { | |
| this.setPlayStatus(false); | |
| this.paused = true; | |
| cancelAnimationFrame(this.raqId); | |
| this.raqId = 0; | |
| }, | |
| play: function() { | |
| if (!this.crashed) { | |
| this.setPlayStatus(true); | |
| this.paused = false; | |
| this.tRex.update(0, Trex.status.RUNNING); | |
| this.time = getTimeStamp(); | |
| this.update(); | |
| } | |
| }, | |
| restart: function() { | |
| if (!this.raqId) { | |
| this.playCount++; | |
| this.runningTime = 0; | |
| this.setPlayStatus(true); | |
| this.paused = false; | |
| this.crashed = false; | |
| this.distanceRan = 0; | |
| this.setSpeed(this.config.SPEED); | |
| this.time = getTimeStamp(); | |
| this.containerEl.classList.remove(Runner.classes.CRASHED); | |
| this.clearCanvas(); | |
| this.distanceMeter.reset(this.highestScore); | |
| this.horizon.reset(); | |
| this.tRex.reset(); | |
| this.playSound(this.soundFx.BUTTON_PRESS); | |
| this.invert(true); | |
| this.bdayFlashTimer = null; | |
| this.update(); | |
| } | |
| }, | |
| setPlayStatus: function(isPlaying) { | |
| if (this.touchController) | |
| this.touchController.classList.toggle(HIDDEN_CLASS, !isPlaying); | |
| this.playing = isPlaying; | |
| }, | |
| /** | |
| * Whether the game should go into arcade mode. | |
| * @return {boolean} | |
| */ | |
| isArcadeMode: function() { | |
| return document.title == ARCADE_MODE_URL; | |
| }, | |
| /** | |
| * Hides offline messaging for a fullscreen game only experience. | |
| */ | |
| setArcadeMode: function() { | |
| document.body.classList.add(Runner.classes.ARCADE_MODE); | |
| this.setArcadeModeContainerScale(); | |
| }, | |
| /** | |
| * Sets the scaling for arcade mode. | |
| */ | |
| setArcadeModeContainerScale: function() { | |
| var windowHeight = window.innerHeight; | |
| var scaleHeight = windowHeight / this.dimensions.HEIGHT; | |
| var scaleWidth = window.innerWidth / this.dimensions.WIDTH; | |
| var scale = Math.max(1, Math.min(scaleHeight, scaleWidth)); | |
| var scaledCanvasHeight = this.dimensions.HEIGHT * scale; | |
| // Positions the game container at 10% of the available vertical window | |
| // height minus the game container height. | |
| var translateY = Math.ceil(Math.max(0, (windowHeight - scaledCanvasHeight - | |
| Runner.config.ARCADE_MODE_INITIAL_TOP_POSITION) * | |
| Runner.config.ARCADE_MODE_TOP_POSITION_PERCENT)) * | |
| window.devicePixelRatio; | |
| // | |
| this.containerEl.style.transform = 'scale(' + scale + ') translateY(' + | |
| translateY + 'px)'; | |
| }, | |
| /** | |
| * Pause the game if the tab is not in focus. | |
| */ | |
| onVisibilityChange: function(e) { | |
| if (document.hidden || document.webkitHidden || e.type == 'blur' || | |
| document.visibilityState != 'visible') { | |
| this.stop(); | |
| } else if (!this.crashed) { | |
| this.tRex.reset(); | |
| this.play(); | |
| } | |
| }, | |
| /** | |
| * Play a sound. | |
| * @param {SoundBuffer} soundBuffer | |
| */ | |
| playSound: function(soundBuffer) { | |
| if (soundBuffer) { | |
| var sourceNode = this.audioContext.createBufferSource(); | |
| sourceNode.buffer = soundBuffer; | |
| sourceNode.connect(this.audioContext.destination); | |
| sourceNode.start(0); | |
| } | |
| }, | |
| /** | |
| * Inverts the current page / canvas colors. | |
| * @param {boolean} Whether to reset colors. | |
| */ | |
| invert: function(reset) { | |
| let htmlEl = document.firstElementChild; | |
| if (reset) { | |
| htmlEl.classList.toggle(Runner.classes.INVERTED, | |
| false); | |
| this.invertTimer = 0; | |
| this.inverted = false; | |
| } else { | |
| this.inverted = htmlEl.classList.toggle( | |
| Runner.classes.INVERTED, this.invertTrigger); | |
| } | |
| } | |
| }; | |
| /** | |
| * Updates the canvas size taking into | |
| * account the backing store pixel ratio and | |
| * the device pixel ratio. | |
| * | |
| * See article by Paul Lewis: | |
| * http://www.html5rocks.com/en/tutorials/canvas/hidpi/ | |
| * | |
| * @param {HTMLCanvasElement} canvas | |
| * @param {number} opt_width | |
| * @param {number} opt_height | |
| * @return {boolean} Whether the canvas was scaled. | |
| */ | |
| Runner.updateCanvasScaling = function(canvas, opt_width, opt_height) { | |
| var context = canvas.getContext('2d'); | |
| // Query the various pixel ratios | |
| var devicePixelRatio = Math.floor(window.devicePixelRatio) || 1; | |
| var backingStoreRatio = Math.floor(context.webkitBackingStorePixelRatio) || 1; | |
| var ratio = devicePixelRatio / backingStoreRatio; | |
| // Upscale the canvas if the two ratios don't match | |
| if (devicePixelRatio !== backingStoreRatio) { | |
| var oldWidth = opt_width || canvas.width; | |
| var oldHeight = opt_height || canvas.height; | |
| canvas.width = oldWidth * ratio; | |
| canvas.height = oldHeight * ratio; | |
| canvas.style.width = oldWidth + 'px'; | |
| canvas.style.height = oldHeight + 'px'; | |
| // Scale the context to counter the fact that we've manually scaled | |
| // our canvas element. | |
| context.scale(ratio, ratio); | |
| return true; | |
| } else if (devicePixelRatio == 1) { | |
| // Reset the canvas width / height. Fixes scaling bug when the page is | |
| // zoomed and the devicePixelRatio changes accordingly. | |
| canvas.style.width = canvas.width + 'px'; | |
| canvas.style.height = canvas.height + 'px'; | |
| } | |
| return false; | |
| }; | |
| /** | |
| * Get random number. | |
| * @param {number} min | |
| * @param {number} max | |
| * @param {number} | |
| */ | |
| function getRandomNum(min, max) { | |
| return Math.floor(Math.random() * (max - min + 1)) + min; | |
| } | |
| /** | |
| * Vibrate on mobile devices. | |
| * @param {number} duration Duration of the vibration in milliseconds. | |
| */ | |
| function vibrate(duration) { | |
| if (IS_MOBILE && window.navigator.vibrate) { | |
| window.navigator.vibrate(duration); | |
| } | |
| } | |
| /** | |
| * Create canvas element. | |
| * @param {HTMLElement} container Element to append canvas to. | |
| * @param {number} width | |
| * @param {number} height | |
| * @param {string} opt_classname | |
| * @return {HTMLCanvasElement} | |
| */ | |
| function createCanvas(container, width, height, opt_classname) { | |
| var canvas = document.createElement('canvas'); | |
| canvas.className = opt_classname ? Runner.classes.CANVAS + ' ' + | |
| opt_classname : Runner.classes.CANVAS; | |
| canvas.width = width; | |
| canvas.height = height; | |
| container.appendChild(canvas); | |
| return canvas; | |
| } | |
| /** | |
| * Decodes the base 64 audio to ArrayBuffer used by Web Audio. | |
| * @param {string} base64String | |
| */ | |
| function decodeBase64ToArrayBuffer(base64String) { | |
| var len = (base64String.length / 4) * 3; | |
| var str = atob(base64String); | |
| var arrayBuffer = new ArrayBuffer(len); | |
| var bytes = new Uint8Array(arrayBuffer); | |
| for (var i = 0; i < len; i++) { | |
| bytes[i] = str.charCodeAt(i); | |
| } | |
| return bytes.buffer; | |
| } | |
| /** | |
| * Return the current timestamp. | |
| * @return {number} | |
| */ | |
| function getTimeStamp() { | |
| return IS_IOS ? new Date().getTime() : performance.now(); | |
| } | |
| //****************************************************************************** | |
| /** | |
| * Game over panel. | |
| * @param {!HTMLCanvasElement} canvas | |
| * @param {Object} textImgPos | |
| * @param {Object} restartImgPos | |
| * @param {!Object} dimensions Canvas dimensions. | |
| * @constructor | |
| */ | |
| function GameOverPanel(canvas, textImgPos, restartImgPos, dimensions) { | |
| this.canvas = canvas; | |
| this.canvasCtx = canvas.getContext('2d'); | |
| this.canvasDimensions = dimensions; | |
| this.textImgPos = textImgPos; | |
| this.restartImgPos = restartImgPos; | |
| this.draw(); | |
| }; | |
| /** | |
| * Dimensions used in the panel. | |
| * @enum {number} | |
| */ | |
| GameOverPanel.dimensions = { | |
| TEXT_X: 0, | |
| TEXT_Y: 13, | |
| TEXT_WIDTH: 191, | |
| TEXT_HEIGHT: 11, | |
| RESTART_WIDTH: 36, | |
| RESTART_HEIGHT: 32 | |
| }; | |
| GameOverPanel.prototype = { | |
| /** | |
| * Update the panel dimensions. | |
| * @param {number} width New canvas width. | |
| * @param {number} opt_height Optional new canvas height. | |
| */ | |
| updateDimensions: function(width, opt_height) { | |
| this.canvasDimensions.WIDTH = width; | |
| if (opt_height) { | |
| this.canvasDimensions.HEIGHT = opt_height; | |
| } | |
| }, | |
| /** | |
| * Draw the panel. | |
| */ | |
| draw: function() { | |
| var dimensions = GameOverPanel.dimensions; | |
| var centerX = this.canvasDimensions.WIDTH / 2; | |
| // Game over text. | |
| var textSourceX = dimensions.TEXT_X; | |
| var textSourceY = dimensions.TEXT_Y; | |
| var textSourceWidth = dimensions.TEXT_WIDTH; | |
| var textSourceHeight = dimensions.TEXT_HEIGHT; | |
| var textTargetX = Math.round(centerX - (dimensions.TEXT_WIDTH / 2)); | |
| var textTargetY = Math.round((this.canvasDimensions.HEIGHT - 25) / 3); | |
| var textTargetWidth = dimensions.TEXT_WIDTH; | |
| var textTargetHeight = dimensions.TEXT_HEIGHT; | |
| var restartSourceWidth = dimensions.RESTART_WIDTH; | |
| var restartSourceHeight = dimensions.RESTART_HEIGHT; | |
| var restartTargetX = centerX - (dimensions.RESTART_WIDTH / 2); | |
| var restartTargetY = this.canvasDimensions.HEIGHT / 2; | |
| if (IS_HIDPI) { | |
| textSourceY *= 2; | |
| textSourceX *= 2; | |
| textSourceWidth *= 2; | |
| textSourceHeight *= 2; | |
| restartSourceWidth *= 2; | |
| restartSourceHeight *= 2; | |
| } | |
| textSourceX += this.textImgPos.x; | |
| textSourceY += this.textImgPos.y; | |
| // Game over text from sprite. | |
| this.canvasCtx.drawImage(Runner.imageSprite, | |
| textSourceX, textSourceY, textSourceWidth, textSourceHeight, | |
| textTargetX, textTargetY, textTargetWidth, textTargetHeight); | |
| // Restart button. | |
| this.canvasCtx.drawImage(Runner.imageSprite, | |
| this.restartImgPos.x, this.restartImgPos.y, | |
| restartSourceWidth, restartSourceHeight, | |
| restartTargetX, restartTargetY, dimensions.RESTART_WIDTH, | |
| dimensions.RESTART_HEIGHT); | |
| } | |
| }; | |
| //****************************************************************************** | |
| /** | |
| * Check for a collision. | |
| * @param {!Obstacle} obstacle | |
| * @param {!Trex} tRex T-rex object. | |
| * @param {HTMLCanvasContext} opt_canvasCtx Optional canvas context for drawing | |
| * collision boxes. | |
| * @return {Array<CollisionBox>} | |
| */ | |
| function checkForCollision(obstacle, tRex, opt_canvasCtx) { | |
| var obstacleBoxXPos = Runner.defaultDimensions.WIDTH + obstacle.xPos; | |
| // Adjustments are made to the bounding box as there is a 1 pixel white | |
| // border around the t-rex and obstacles. | |
| var tRexBox = new CollisionBox( | |
| tRex.xPos + 1, | |
| tRex.yPos + 1, | |
| tRex.config.WIDTH - 2, | |
| tRex.config.HEIGHT - 2); | |
| var obstacleBox = new CollisionBox( | |
| obstacle.xPos + 1, | |
| obstacle.yPos + 1, | |
| obstacle.typeConfig.width * obstacle.size - 2, | |
| obstacle.typeConfig.height - 2); | |
| // Debug outer box | |
| if (opt_canvasCtx) { | |
| drawCollisionBoxes(opt_canvasCtx, tRexBox, obstacleBox); | |
| } | |
| // Simple outer bounds check. | |
| if (boxCompare(tRexBox, obstacleBox)) { | |
| var collisionBoxes = obstacle.collisionBoxes; | |
| var tRexCollisionBoxes = tRex.ducking ? | |
| Trex.collisionBoxes.DUCKING : Trex.collisionBoxes.RUNNING; | |
| // Detailed axis aligned box check. | |
| for (var t = 0; t < tRexCollisionBoxes.length; t++) { | |
| for (var i = 0; i < collisionBoxes.length; i++) { | |
| // Adjust the box to actual positions. | |
| var adjTrexBox = | |
| createAdjustedCollisionBox(tRexCollisionBoxes[t], tRexBox); | |
| var adjObstacleBox = | |
| createAdjustedCollisionBox(collisionBoxes[i], obstacleBox); | |
| var crashed = boxCompare(adjTrexBox, adjObstacleBox); | |
| // Draw boxes for debug. | |
| if (opt_canvasCtx) { | |
| drawCollisionBoxes(opt_canvasCtx, adjTrexBox, adjObstacleBox); | |
| } | |
| if (crashed) { | |
| return [adjTrexBox, adjObstacleBox]; | |
| } | |
| } | |
| } | |
| } | |
| return false; | |
| }; | |
| /** | |
| * Adjust the collision box. | |
| * @param {!CollisionBox} box The original box. | |
| * @param {!CollisionBox} adjustment Adjustment box. | |
| * @return {CollisionBox} The adjusted collision box object. | |
| */ | |
| function createAdjustedCollisionBox(box, adjustment) { | |
| return new CollisionBox( | |
| box.x + adjustment.x, | |
| box.y + adjustment.y, | |
| box.width, | |
| box.height); | |
| }; | |
| /** | |
| * Draw the collision boxes for debug. | |
| */ | |
| function drawCollisionBoxes(canvasCtx, tRexBox, obstacleBox) { | |
| canvasCtx.save(); | |
| canvasCtx.strokeStyle = '#f00'; | |
| canvasCtx.strokeRect(tRexBox.x, tRexBox.y, tRexBox.width, tRexBox.height); | |
| canvasCtx.strokeStyle = '#0f0'; | |
| canvasCtx.strokeRect(obstacleBox.x, obstacleBox.y, | |
| obstacleBox.width, obstacleBox.height); | |
| canvasCtx.restore(); | |
| }; | |
| /** | |
| * Compare two collision boxes for a collision. | |
| * @param {CollisionBox} tRexBox | |
| * @param {CollisionBox} obstacleBox | |
| * @return {boolean} Whether the boxes intersected. | |
| */ | |
| function boxCompare(tRexBox, obstacleBox) { | |
| var crashed = false; | |
| var tRexBoxX = tRexBox.x; | |
| var tRexBoxY = tRexBox.y; | |
| var obstacleBoxX = obstacleBox.x; | |
| var obstacleBoxY = obstacleBox.y; | |
| // Axis-Aligned Bounding Box method. | |
| if (tRexBox.x < obstacleBoxX + obstacleBox.width && | |
| tRexBox.x + tRexBox.width > obstacleBoxX && | |
| tRexBox.y < obstacleBox.y + obstacleBox.height && | |
| tRexBox.height + tRexBox.y > obstacleBox.y) { | |
| crashed = true; | |
| } | |
| return crashed; | |
| }; | |
| //****************************************************************************** | |
| /** | |
| * Collision box object. | |
| * @param {number} x X position. | |
| * @param {number} y Y Position. | |
| * @param {number} w Width. | |
| * @param {number} h Height. | |
| */ | |
| function CollisionBox(x, y, w, h) { | |
| this.x = x; | |
| this.y = y; | |
| this.width = w; | |
| this.height = h; | |
| }; | |
| //****************************************************************************** | |
| /** | |
| * Obstacle. | |
| * @param {HTMLCanvasCtx} canvasCtx | |
| * @param {Obstacle.type} type | |
| * @param {Object} spritePos Obstacle position in sprite. | |
| * @param {Object} dimensions | |
| * @param {number} gapCoefficient Mutipler in determining the gap. | |
| * @param {number} speed | |
| * @param {number} opt_xOffset | |
| */ | |
| function Obstacle(canvasCtx, type, spriteImgPos, dimensions, | |
| gapCoefficient, speed, opt_xOffset) { | |
| this.canvasCtx = canvasCtx; | |
| this.spritePos = spriteImgPos; | |
| this.typeConfig = type; | |
| this.gapCoefficient = gapCoefficient; | |
| this.size = getRandomNum(1, Obstacle.MAX_OBSTACLE_LENGTH); | |
| this.dimensions = dimensions; | |
| this.remove = false; | |
| this.xPos = dimensions.WIDTH + (opt_xOffset || 0); | |
| this.yPos = 0; | |
| this.width = 0; | |
| this.collisionBoxes = []; | |
| this.gap = 0; | |
| this.speedOffset = 0; | |
| // For animated obstacles. | |
| this.currentFrame = 0; | |
| this.timer = 0; | |
| this.init(speed); | |
| }; | |
| /** | |
| * Coefficient for calculating the maximum gap. | |
| * @const | |
| */ | |
| Obstacle.MAX_GAP_COEFFICIENT = 1.5; | |
| /** | |
| * Maximum obstacle grouping count. | |
| * @const | |
| */ | |
| Obstacle.MAX_OBSTACLE_LENGTH = 3, | |
| Obstacle.prototype = { | |
| /** | |
| * Initialise the DOM for the obstacle. | |
| * @param {number} speed | |
| */ | |
| init: function(speed) { | |
| this.cloneCollisionBoxes(); | |
| // Only allow sizing if we're at the right speed. | |
| if (this.size > 1 && this.typeConfig.multipleSpeed > speed) { | |
| this.size = 1; | |
| } | |
| this.width = this.typeConfig.width * this.size; | |
| // Check if obstacle can be positioned at various heights. | |
| if (Array.isArray(this.typeConfig.yPos)) { | |
| var yPosConfig = IS_MOBILE ? this.typeConfig.yPosMobile : | |
| this.typeConfig.yPos; | |
| this.yPos = yPosConfig[getRandomNum(0, yPosConfig.length - 1)]; | |
| } else { | |
| this.yPos = this.typeConfig.yPos; | |
| } | |
| this.draw(); | |
| // Make collision box adjustments, | |
| // Central box is adjusted to the size as one box. | |
| // ____ ______ ________ | |
| // _| |-| _| |-| _| |-| | |
| // | |<->| | | |<--->| | | |<----->| | | |
| // | | 1 | | | | 2 | | | | 3 | | | |
| // |_|___|_| |_|_____|_| |_|_______|_| | |
| // | |
| if (this.size > 1) { | |
| this.collisionBoxes[1].width = this.width - this.collisionBoxes[0].width - | |
| this.collisionBoxes[2].width; | |
| this.collisionBoxes[2].x = this.width - this.collisionBoxes[2].width; | |
| } | |
| // For obstacles that go at a different speed from the horizon. | |
| if (this.typeConfig.speedOffset) { | |
| this.speedOffset = Math.random() > 0.5 ? this.typeConfig.speedOffset : | |
| -this.typeConfig.speedOffset; | |
| } | |
| this.gap = this.getGap(this.gapCoefficient, speed); | |
| }, | |
| /** | |
| * Draw and crop based on size. | |
| */ | |
| draw: function() { | |
| var sourceWidth = this.typeConfig.width; | |
| var sourceHeight = this.typeConfig.height; | |
| if (IS_HIDPI) { | |
| sourceWidth = sourceWidth * 2; | |
| sourceHeight = sourceHeight * 2; | |
| } | |
| // X position in sprite. | |
| var sourceX = (sourceWidth * this.size) * (0.5 * (this.size - 1)) + | |
| this.spritePos.x; | |
| // Animation frames. | |
| if (this.currentFrame > 0) { | |
| sourceX += sourceWidth * this.currentFrame; | |
| } | |
| this.canvasCtx.drawImage(Runner.imageSprite, | |
| sourceX, this.spritePos.y, | |
| sourceWidth * this.size, sourceHeight, | |
| this.xPos, this.yPos, | |
| this.typeConfig.width * this.size, this.typeConfig.height); | |
| }, | |
| /** | |
| * Obstacle frame update. | |
| * @param {number} deltaTime | |
| * @param {number} speed | |
| */ | |
| update: function(deltaTime, speed) { | |
| if (!this.remove) { | |
| if (this.typeConfig.speedOffset) { | |
| speed += this.speedOffset; | |
| } | |
| this.xPos -= Math.floor((speed * FPS / 1000) * deltaTime); | |
| // Update frame | |
| if (this.typeConfig.numFrames) { | |
| this.timer += deltaTime; | |
| if (this.timer >= this.typeConfig.frameRate) { | |
| this.currentFrame = | |
| this.currentFrame == this.typeConfig.numFrames - 1 ? | |
| 0 : this.currentFrame + 1; | |
| this.timer = 0; | |
| } | |
| } | |
| this.draw(); | |
| if (!this.isVisible()) { | |
| this.remove = true; | |
| } | |
| } | |
| }, | |
| /** | |
| * Calculate a random gap size. | |
| * - Minimum gap gets wider as speed increses | |
| * @param {number} gapCoefficient | |
| * @param {number} speed | |
| * @return {number} The gap size. | |
| */ | |
| getGap: function(gapCoefficient, speed) { | |
| var minGap = Math.round(this.width * speed + | |
| this.typeConfig.minGap * gapCoefficient); | |
| var maxGap = Math.round(minGap * Obstacle.MAX_GAP_COEFFICIENT); | |
| return getRandomNum(minGap, maxGap); | |
| }, | |
| /** | |
| * Check if obstacle is visible. | |
| * @return {boolean} Whether the obstacle is in the game area. | |
| */ | |
| isVisible: function() { | |
| return this.xPos + this.width > 0; | |
| }, | |
| /** | |
| * Make a copy of the collision boxes, since these will change based on | |
| * obstacle type and size. | |
| */ | |
| cloneCollisionBoxes: function() { | |
| var collisionBoxes = this.typeConfig.collisionBoxes; | |
| for (var i = collisionBoxes.length - 1; i >= 0; i--) { | |
| this.collisionBoxes[i] = new CollisionBox(collisionBoxes[i].x, | |
| collisionBoxes[i].y, collisionBoxes[i].width, | |
| collisionBoxes[i].height); | |
| } | |
| } | |
| }; | |
| /** | |
| * Obstacle definitions. | |
| * minGap: minimum pixel space betweeen obstacles. | |
| * multipleSpeed: Speed at which multiples are allowed. | |
| * speedOffset: speed faster / slower than the horizon. | |
| * minSpeed: Minimum speed which the obstacle can make an appearance. | |
| */ | |
| Obstacle.types = [ | |
| { | |
| type: 'CACTUS_SMALL', | |
| width: 17, | |
| height: 35, | |
| yPos: 105, | |
| multipleSpeed: 4, | |
| minGap: 120, | |
| minSpeed: 0, | |
| collisionBoxes: [ | |
| new CollisionBox(0, 7, 5, 27), | |
| new CollisionBox(4, 0, 6, 34), | |
| new CollisionBox(10, 4, 7, 14) | |
| ] | |
| }, | |
| { | |
| type: 'CACTUS_LARGE', | |
| width: 25, | |
| height: 50, | |
| yPos: 90, | |
| multipleSpeed: 7, | |
| minGap: 120, | |
| minSpeed: 0, | |
| collisionBoxes: [ | |
| new CollisionBox(0, 12, 7, 38), | |
| new CollisionBox(8, 0, 7, 49), | |
| new CollisionBox(13, 10, 10, 38) | |
| ] | |
| }, | |
| { | |
| type: 'PTERODACTYL', | |
| width: 46, | |
| height: 40, | |
| yPos: [ 100, 75, 50 ], // Variable height. | |
| yPosMobile: [ 100, 50 ], // Variable height mobile. | |
| multipleSpeed: 999, | |
| minSpeed: 8.5, | |
| minGap: 150, | |
| collisionBoxes: [ | |
| new CollisionBox(15, 15, 16, 5), | |
| new CollisionBox(18, 21, 24, 6), | |
| new CollisionBox(2, 14, 4, 3), | |
| new CollisionBox(6, 10, 4, 7), | |
| new CollisionBox(10, 8, 6, 9) | |
| ], | |
| numFrames: 2, | |
| frameRate: 1000/6, | |
| speedOffset: .8 | |
| } | |
| ]; | |
| //****************************************************************************** | |
| /** | |
| * T-rex game character. | |
| * @param {HTMLCanvas} canvas | |
| * @param {Object} spritePos Positioning within image sprite. | |
| * @constructor | |
| */ | |
| function Trex(canvas, spritePos) { | |
| this.canvas = canvas; | |
| this.canvasCtx = canvas.getContext('2d'); | |
| this.spritePos = spritePos; | |
| this.xPos = 0; | |
| this.yPos = 0; | |
| // Position when on the ground. | |
| this.groundYPos = 0; | |
| this.currentFrame = 0; | |
| this.currentAnimFrames = []; | |
| this.blinkDelay = 0; | |
| this.blinkCount = 0; | |
| this.animStartTime = 0; | |
| this.timer = 0; | |
| this.msPerFrame = 1000 / FPS; | |
| this.config = Trex.config; | |
| // Current status. | |
| this.status = Trex.status.WAITING; | |
| this.jumping = false; | |
| this.ducking = false; | |
| this.jumpVelocity = 0; | |
| this.reachedMinHeight = false; | |
| this.speedDrop = false; | |
| this.jumpCount = 0; | |
| this.jumpspotX = 0; | |
| this.init(); | |
| }; | |
| /** | |
| * T-rex player config. | |
| * @enum {number} | |
| */ | |
| Trex.config = { | |
| DROP_VELOCITY: -5, | |
| GRAVITY: 0.6, | |
| HEIGHT: 47, | |
| HEIGHT_DUCK: 25, | |
| INIITAL_JUMP_VELOCITY: -10, | |
| INTRO_DURATION: 1500, | |
| MAX_JUMP_HEIGHT: 30, | |
| MIN_JUMP_HEIGHT: 30, | |
| SPEED_DROP_COEFFICIENT: 3, | |
| SPRITE_WIDTH: 262, | |
| START_X_POS: 50, | |
| WIDTH: 44, | |
| WIDTH_DUCK: 59 | |
| }; | |
| /** | |
| * Used in collision detection. | |
| * @type {Array<CollisionBox>} | |
| */ | |
| Trex.collisionBoxes = { | |
| DUCKING: [ | |
| new CollisionBox(1, 18, 55, 25) | |
| ], | |
| RUNNING: [ | |
| new CollisionBox(22, 0, 17, 16), | |
| new CollisionBox(1, 18, 30, 9), | |
| new CollisionBox(10, 35, 14, 8), | |
| new CollisionBox(1, 24, 29, 5), | |
| new CollisionBox(5, 30, 21, 4), | |
| new CollisionBox(9, 34, 15, 4) | |
| ] | |
| }; | |
| /** | |
| * Animation states. | |
| * @enum {string} | |
| */ | |
| Trex.status = { | |
| CRASHED: 'CRASHED', | |
| DUCKING: 'DUCKING', | |
| JUMPING: 'JUMPING', | |
| RUNNING: 'RUNNING', | |
| WAITING: 'WAITING' | |
| }; | |
| /** | |
| * Blinking coefficient. | |
| * @const | |
| */ | |
| Trex.BLINK_TIMING = 7000; | |
| /** | |
| * Animation config for different states. | |
| * @enum {Object} | |
| */ | |
| Trex.animFrames = { | |
| WAITING: { | |
| frames: [44, 0], | |
| msPerFrame: 1000 / 3 | |
| }, | |
| RUNNING: { | |
| frames: [88, 132], | |
| msPerFrame: 1000 / 12 | |
| }, | |
| CRASHED: { | |
| frames: [220], | |
| msPerFrame: 1000 / 60 | |
| }, | |
| JUMPING: { | |
| frames: [0], | |
| msPerFrame: 1000 / 60 | |
| }, | |
| DUCKING: { | |
| frames: [264, 323], | |
| msPerFrame: 1000 / 8 | |
| } | |
| }; | |
| Trex.prototype = { | |
| /** | |
| * T-rex player initaliser. | |
| * Sets the t-rex to blink at random intervals. | |
| */ | |
| init: function() { | |
| this.groundYPos = Runner.defaultDimensions.HEIGHT - this.config.HEIGHT - | |
| Runner.config.BOTTOM_PAD; | |
| this.yPos = this.groundYPos; | |
| this.minJumpHeight = this.groundYPos - this.config.MIN_JUMP_HEIGHT; | |
| this.draw(0, 0); | |
| this.update(0, Trex.status.WAITING); | |
| }, | |
| /** | |
| * Setter for the jump velocity. | |
| * The approriate drop velocity is also set. | |
| */ | |
| setJumpVelocity: function(setting) { | |
| this.config.INIITAL_JUMP_VELOCITY = -setting; | |
| this.config.DROP_VELOCITY = -setting / 2; | |
| }, | |
| /** | |
| * Set the animation status. | |
| * @param {!number} deltaTime | |
| * @param {Trex.status} status Optional status to switch to. | |
| */ | |
| update: function(deltaTime, opt_status) { | |
| this.timer += deltaTime; | |
| // Update the status. | |
| if (opt_status) { | |
| this.status = opt_status; | |
| this.currentFrame = 0; | |
| this.msPerFrame = Trex.animFrames[opt_status].msPerFrame; | |
| this.currentAnimFrames = Trex.animFrames[opt_status].frames; | |
| if (opt_status == Trex.status.WAITING) { | |
| this.animStartTime = getTimeStamp(); | |
| this.setBlinkDelay(); | |
| } | |
| } | |
| // Game intro animation, T-rex moves in from the left. | |
| if (this.playingIntro && this.xPos < this.config.START_X_POS) { | |
| this.xPos += Math.round((this.config.START_X_POS / | |
| this.config.INTRO_DURATION) * deltaTime); | |
| } | |
| if (this.status == Trex.status.WAITING) { | |
| this.blink(getTimeStamp()); | |
| } else { | |
| this.draw(this.currentAnimFrames[this.currentFrame], 0); | |
| } | |
| // Update the frame position. | |
| if (this.timer >= this.msPerFrame) { | |
| this.currentFrame = this.currentFrame == | |
| this.currentAnimFrames.length - 1 ? 0 : this.currentFrame + 1; | |
| this.timer = 0; | |
| } | |
| // Speed drop becomes duck if the down key is still being pressed. | |
| if (this.speedDrop && this.yPos == this.groundYPos) { | |
| this.speedDrop = false; | |
| this.setDuck(true); | |
| } | |
| }, | |
| /** | |
| * Draw the t-rex to a particular position. | |
| * @param {number} x | |
| * @param {number} y | |
| */ | |
| draw: function(x, y) { | |
| var sourceX = x; | |
| var sourceY = y; | |
| var sourceWidth = this.ducking && this.status != Trex.status.CRASHED ? | |
| this.config.WIDTH_DUCK : this.config.WIDTH; | |
| var sourceHeight = this.config.HEIGHT; | |
| var outputHeight = sourceHeight; | |
| if (IS_HIDPI) { | |
| sourceX *= 2; | |
| sourceY *= 2; | |
| sourceWidth *= 2; | |
| sourceHeight *= 2; | |
| } | |
| // Adjustments for sprite sheet position. | |
| sourceX += this.spritePos.x; | |
| sourceY += this.spritePos.y; | |
| // Ducking. | |
| if (this.ducking && this.status != Trex.status.CRASHED) { | |
| this.canvasCtx.drawImage(Runner.imageSprite, sourceX, sourceY, | |
| sourceWidth, sourceHeight, | |
| this.xPos, this.yPos, | |
| this.config.WIDTH_DUCK, outputHeight); | |
| } else { | |
| // Crashed whilst ducking. Trex is standing up so needs adjustment. | |
| if (this.ducking && this.status == Trex.status.CRASHED) { | |
| this.xPos++; | |
| } | |
| // Standing / running | |
| this.canvasCtx.drawImage(Runner.imageSprite, sourceX, sourceY, | |
| sourceWidth, sourceHeight, | |
| this.xPos, this.yPos, | |
| this.config.WIDTH, outputHeight); | |
| } | |
| this.canvasCtx.globalAlpha = 1; | |
| }, | |
| /** | |
| * Sets a random time for the blink to happen. | |
| */ | |
| setBlinkDelay: function() { | |
| this.blinkDelay = Math.ceil(Math.random() * Trex.BLINK_TIMING); | |
| }, | |
| /** | |
| * Make t-rex blink at random intervals. | |
| * @param {number} time Current time in milliseconds. | |
| */ | |
| blink: function(time) { | |
| var deltaTime = time - this.animStartTime; | |
| if (deltaTime >= this.blinkDelay) { | |
| this.draw(this.currentAnimFrames[this.currentFrame], 0); | |
| if (this.currentFrame == 1) { | |
| // Set new random delay to blink. | |
| this.setBlinkDelay(); | |
| this.animStartTime = time; | |
| this.blinkCount++; | |
| } | |
| } | |
| }, | |
| /** | |
| * Initialise a jump. | |
| * @param {number} speed | |
| */ | |
| startJump: function(speed) { | |
| if (!this.jumping) { | |
| this.update(0, Trex.status.JUMPING); | |
| // Tweak the jump velocity based on the speed. | |
| this.jumpVelocity = this.config.INIITAL_JUMP_VELOCITY - (speed / 10); | |
| this.jumping = true; | |
| this.reachedMinHeight = false; | |
| this.speedDrop = false; | |
| } | |
| }, | |
| /** | |
| * Jump is complete, falling down. | |
| */ | |
| endJump: function() { | |
| if (this.reachedMinHeight && | |
| this.jumpVelocity < this.config.DROP_VELOCITY) { | |
| this.jumpVelocity = this.config.DROP_VELOCITY; | |
| } | |
| }, | |
| /** | |
| * Update frame for a jump. | |
| * @param {number} deltaTime | |
| * @param {number} speed | |
| */ | |
| updateJump: function(deltaTime, speed) { | |
| var msPerFrame = Trex.animFrames[this.status].msPerFrame; | |
| var framesElapsed = deltaTime / msPerFrame; | |
| // Speed drop makes Trex fall faster. | |
| if (this.speedDrop) { | |
| this.yPos += Math.round(this.jumpVelocity * | |
| this.config.SPEED_DROP_COEFFICIENT * framesElapsed); | |
| } else { | |
| this.yPos += Math.round(this.jumpVelocity * framesElapsed); | |
| } | |
| this.jumpVelocity += this.config.GRAVITY * framesElapsed; | |
| // Minimum height has been reached. | |
| if (this.yPos < this.minJumpHeight || this.speedDrop) { | |
| this.reachedMinHeight = true; | |
| } | |
| // Reached max height | |
| if (this.yPos < this.config.MAX_JUMP_HEIGHT || this.speedDrop) { | |
| this.endJump(); | |
| } | |
| // Back down at ground level. Jump completed. | |
| if (this.yPos > this.groundYPos) { | |
| this.reset(); | |
| this.jumpCount++; | |
| } | |
| }, | |
| /** | |
| * Set the speed drop. Immediately cancels the current jump. | |
| */ | |
| setSpeedDrop: function() { | |
| this.speedDrop = true; | |
| this.jumpVelocity = 1; | |
| }, | |
| /** | |
| * @param {boolean} isDucking. | |
| */ | |
| setDuck: function(isDucking) { | |
| if (isDucking && this.status != Trex.status.DUCKING) { | |
| this.update(0, Trex.status.DUCKING); | |
| this.ducking = true; | |
| } else if (this.status == Trex.status.DUCKING) { | |
| this.update(0, Trex.status.RUNNING); | |
| this.ducking = false; | |
| } | |
| }, | |
| /** | |
| * Reset the t-rex to running at start of game. | |
| */ | |
| reset: function() { | |
| this.yPos = this.groundYPos; | |
| this.jumpVelocity = 0; | |
| this.jumping = false; | |
| this.ducking = false; | |
| this.update(0, Trex.status.RUNNING); | |
| this.midair = false; | |
| this.speedDrop = false; | |
| this.jumpCount = 0; | |
| } | |
| }; | |
| //****************************************************************************** | |
| /** | |
| * Handles displaying the distance meter. | |
| * @param {!HTMLCanvasElement} canvas | |
| * @param {Object} spritePos Image position in sprite. | |
| * @param {number} canvasWidth | |
| * @constructor | |
| */ | |
| function DistanceMeter(canvas, spritePos, canvasWidth) { | |
| this.canvas = canvas; | |
| this.canvasCtx = canvas.getContext('2d'); | |
| this.image = Runner.imageSprite; | |
| this.spritePos = spritePos; | |
| this.x = 0; | |
| this.y = 5; | |
| this.currentDistance = 0; | |
| this.maxScore = 0; | |
| this.highScore = 0; | |
| this.container = null; | |
| this.digits = []; | |
| this.achievement = false; | |
| this.defaultString = ''; | |
| this.flashTimer = 0; | |
| this.flashIterations = 0; | |
| this.invertTrigger = false; | |
| this.flashingRafId = null; | |
| this.highScoreBounds = {}; | |
| this.highScoreFlashing = false; | |
| this.config = DistanceMeter.config; | |
| this.maxScoreUnits = this.config.MAX_DISTANCE_UNITS; | |
| this.init(canvasWidth); | |
| }; | |
| /** | |
| * @enum {number} | |
| */ | |
| DistanceMeter.dimensions = { | |
| WIDTH: 10, | |
| HEIGHT: 13, | |
| DEST_WIDTH: 11 | |
| }; | |
| /** | |
| * Y positioning of the digits in the sprite sheet. | |
| * X position is always 0. | |
| * @type {Array<number>} | |
| */ | |
| DistanceMeter.yPos = [0, 13, 27, 40, 53, 67, 80, 93, 107, 120]; | |
| /** | |
| * Distance meter config. | |
| * @enum {number} | |
| */ | |
| DistanceMeter.config = { | |
| // Number of digits. | |
| MAX_DISTANCE_UNITS: 5, | |
| // Distance that causes achievement animation. | |
| ACHIEVEMENT_DISTANCE: 100, | |
| // Used for conversion from pixel distance to a scaled unit. | |
| COEFFICIENT: 0.025, | |
| // Flash duration in milliseconds. | |
| FLASH_DURATION: 1000 / 4, | |
| // Flash iterations for achievement animation. | |
| FLASH_ITERATIONS: 3, | |
| // Padding around the high score hit area. | |
| HIGH_SCORE_HIT_AREA_PADDING: 4 | |
| }; | |
| DistanceMeter.prototype = { | |
| /** | |
| * Initialise the distance meter to '00000'. | |
| * @param {number} width Canvas width in px. | |
| */ | |
| init: function(width) { | |
| var maxDistanceStr = ''; | |
| this.calcXPos(width); | |
| this.maxScore = this.maxScoreUnits; | |
| for (var i = 0; i < this.maxScoreUnits; i++) { | |
| this.draw(i, 0); | |
| this.defaultString += '0'; | |
| maxDistanceStr += '9'; | |
| } | |
| this.maxScore = parseInt(maxDistanceStr); | |
| }, | |
| /** | |
| * Calculate the xPos in the canvas. | |
| * @param {number} canvasWidth | |
| */ | |
| calcXPos: function(canvasWidth) { | |
| this.x = canvasWidth - (DistanceMeter.dimensions.DEST_WIDTH * | |
| (this.maxScoreUnits + 1)); | |
| }, | |
| /** | |
| * Draw a digit to canvas. | |
| * @param {number} digitPos Position of the digit. | |
| * @param {number} value Digit value 0-9. | |
| * @param {boolean} opt_highScore Whether drawing the high score. | |
| */ | |
| draw: function(digitPos, value, opt_highScore) { | |
| var sourceWidth = DistanceMeter.dimensions.WIDTH; | |
| var sourceHeight = DistanceMeter.dimensions.HEIGHT; | |
| var sourceX = DistanceMeter.dimensions.WIDTH * value; | |
| var sourceY = 0; | |
| var targetX = digitPos * DistanceMeter.dimensions.DEST_WIDTH; | |
| var targetY = this.y; | |
| var targetWidth = DistanceMeter.dimensions.WIDTH; | |
| var targetHeight = DistanceMeter.dimensions.HEIGHT; | |
| // For high DPI we 2x source values. | |
| if (IS_HIDPI) { | |
| sourceWidth *= 2; | |
| sourceHeight *= 2; | |
| sourceX *= 2; | |
| } | |
| sourceX += this.spritePos.x; | |
| sourceY += this.spritePos.y; | |
| this.canvasCtx.save(); | |
| if (opt_highScore) { | |
| // Left of the current score. | |
| var highScoreX = this.x - (this.maxScoreUnits * 2) * | |
| DistanceMeter.dimensions.WIDTH; | |
| this.canvasCtx.translate(highScoreX, this.y); | |
| } else { | |
| this.canvasCtx.translate(this.x, this.y); | |
| } | |
| this.canvasCtx.drawImage(this.image, sourceX, sourceY, | |
| sourceWidth, sourceHeight, | |
| targetX, targetY, | |
| targetWidth, targetHeight | |
| ); | |
| this.canvasCtx.restore(); | |
| }, | |
| /** | |
| * Covert pixel distance to a 'real' distance. | |
| * @param {number} distance Pixel distance ran. | |
| * @return {number} The 'real' distance ran. | |
| */ | |
| getActualDistance: function(distance) { | |
| return distance ? Math.round(distance * this.config.COEFFICIENT) : 0; | |
| }, | |
| /** | |
| * Update the distance meter. | |
| * @param {number} distance | |
| * @param {number} deltaTime | |
| * @return {boolean} Whether the acheivement sound fx should be played. | |
| */ | |
| update: function(deltaTime, distance) { | |
| var paint = true; | |
| var playSound = false; | |
| if (!this.achievement) { | |
| distance = this.getActualDistance(distance); | |
| // Score has gone beyond the initial digit count. | |
| if (distance > this.maxScore && this.maxScoreUnits == | |
| this.config.MAX_DISTANCE_UNITS) { | |
| this.maxScoreUnits++; | |
| this.maxScore = parseInt(this.maxScore + '9'); | |
| } else { | |
| this.distance = 0; | |
| } | |
| if (distance > 0) { | |
| // Acheivement unlocked | |
| if (distance % this.config.ACHIEVEMENT_DISTANCE == 0) { | |
| // Flash score and play sound. | |
| this.achievement = true; | |
| this.flashTimer = 0; | |
| playSound = true; | |
| } | |
| // Create a string representation of the distance with leading 0. | |
| var distanceStr = (this.defaultString + | |
| distance).substr(-this.maxScoreUnits); | |
| this.digits = distanceStr.split(''); | |
| } else { | |
| this.digits = this.defaultString.split(''); | |
| } | |
| } else { | |
| // Control flashing of the score on reaching acheivement. | |
| if (this.flashIterations <= this.config.FLASH_ITERATIONS) { | |
| this.flashTimer += deltaTime; | |
| if (this.flashTimer < this.config.FLASH_DURATION) { | |
| paint = false; | |
| } else if (this.flashTimer > | |
| this.config.FLASH_DURATION * 2) { | |
| this.flashTimer = 0; | |
| this.flashIterations++; | |
| } | |
| } else { | |
| this.achievement = false; | |
| this.flashIterations = 0; | |
| this.flashTimer = 0; | |
| } | |
| } | |
| // Draw the digits if not flashing. | |
| if (paint) { | |
| for (var i = this.digits.length - 1; i >= 0; i--) { | |
| this.draw(i, parseInt(this.digits[i])); | |
| } | |
| } | |
| this.drawHighScore(); | |
| return playSound; | |
| }, | |
| /** | |
| * Draw the high score. | |
| */ | |
| drawHighScore: function() { | |
| this.canvasCtx.save(); | |
| this.canvasCtx.globalAlpha = .8; | |
| for (var i = this.highScore.length - 1; i >= 0; i--) { | |
| this.draw(i, parseInt(this.highScore[i], 10), true); | |
| } | |
| this.canvasCtx.restore(); | |
| }, | |
| /** | |
| * Set the highscore as a array string. | |
| * Position of char in the sprite: H - 10, I - 11. | |
| * @param {number} distance Distance ran in pixels. | |
| */ | |
| setHighScore: function(distance) { | |
| distance = this.getActualDistance(distance); | |
| var highScoreStr = (this.defaultString + | |
| distance).substr(-this.maxScoreUnits); | |
| this.highScore = ['10', '11', ''].concat(highScoreStr.split('')); | |
| }, | |
| /** | |
| * Whether a clicked is in the high score area. | |
| * @param {TouchEvent|ClickEvent} e Event object. | |
| * @return {boolean} Whether the click was in the high score bounds. | |
| */ | |
| hasClickedOnHighScore: function(e) { | |
| var x = 0; | |
| var y = 0; | |
| if (e.touches) { | |
| // Bounds for touch differ from pointer. | |
| var canvasBounds = this.canvas.getBoundingClientRect(); | |
| x = e.touches[0].clientX - canvasBounds.left; | |
| y = e.touches[0].clientY - canvasBounds.top; | |
| } else { | |
| x = e.offsetX; | |
| y = e.offsetY; | |
| } | |
| this.highScoreBounds = this.getHighScoreBounds(); | |
| return x >= this.highScoreBounds.x && x <= | |
| this.highScoreBounds.x + this.highScoreBounds.width && | |
| y >= this.highScoreBounds.y && y <= | |
| this.highScoreBounds.y + this.highScoreBounds.height; | |
| }, | |
| /** | |
| * Get the bounding box for the high score. | |
| * @return {Object} Object with x, y, width and height properties. | |
| */ | |
| getHighScoreBounds: function() { | |
| return { | |
| x: (this.x - (this.maxScoreUnits * 2) * | |
| DistanceMeter.dimensions.WIDTH) - | |
| DistanceMeter.config.HIGH_SCORE_HIT_AREA_PADDING, | |
| y: this.y, | |
| width: DistanceMeter.dimensions.WIDTH * (this.highScore.length + 1) + | |
| DistanceMeter.config.HIGH_SCORE_HIT_AREA_PADDING, | |
| height: DistanceMeter.dimensions.HEIGHT + | |
| (DistanceMeter.config.HIGH_SCORE_HIT_AREA_PADDING * 2) | |
| }; | |
| }, | |
| /** | |
| * Animate flashing the high score to indicate ready for resetting. | |
| * The flashing stops following this.config.FLASH_ITERATIONS x 2 flashes. | |
| */ | |
| flashHighScore: function() { | |
| var now = getTimeStamp(); | |
| var deltaTime = now - (this.frameTimeStamp || now); | |
| var paint = true; | |
| this.frameTimeStamp = now; | |
| // Reached the max number of flashes. | |
| if (this.flashIterations > this.config.FLASH_ITERATIONS * 2) { | |
| this.cancelHighScoreFlashing(); | |
| return; | |
| } | |
| this.flashTimer += deltaTime; | |
| if (this.flashTimer < this.config.FLASH_DURATION) { | |
| paint = false; | |
| } else if (this.flashTimer > this.config.FLASH_DURATION * 2) { | |
| this.flashTimer = 0; | |
| this.flashIterations++; | |
| } | |
| if (paint) { | |
| this.drawHighScore(); | |
| } else { | |
| this.clearHighScoreBounds(); | |
| } | |
| // Frame update. | |
| this.flashingRafId = | |
| requestAnimationFrame(this.flashHighScore.bind(this)); | |
| }, | |
| /** | |
| * Draw empty rectangle over high score. | |
| */ | |
| clearHighScoreBounds: function() { | |
| this.canvasCtx.save(); | |
| this.canvasCtx.fillStyle = '#fff'; | |
| this.canvasCtx.rect(this.highScoreBounds.x, this.highScoreBounds.y, | |
| this.highScoreBounds.width, this.highScoreBounds.height); | |
| this.canvasCtx.fill(); | |
| this.canvasCtx.restore(); | |
| }, | |
| /** | |
| * Starts the flashing of the high score. | |
| */ | |
| startHighScoreFlashing() { | |
| this.highScoreFlashing = true; | |
| this.flashHighScore(); | |
| }, | |
| /** | |
| * Whether high score is flashing. | |
| * @return {boolean} | |
| */ | |
| isHighScoreFlashing() { | |
| return this.highScoreFlashing; | |
| }, | |
| /** | |
| * Stop flashing the high score. | |
| */ | |
| cancelHighScoreFlashing: function() { | |
| cancelAnimationFrame(this.flashingRafId); | |
| this.flashIterations = 0; | |
| this.flashTimer = 0; | |
| this.highScoreFlashing = false; | |
| this.clearHighScoreBounds(); | |
| this.drawHighScore(); | |
| }, | |
| /** | |
| * Clear the high score. | |
| */ | |
| resetHighScore: function() { | |
| this.setHighScore(0); | |
| this.cancelHighScoreFlashing(); | |
| }, | |
| /** | |
| * Reset the distance meter back to '00000'. | |
| */ | |
| reset: function() { | |
| this.update(0); | |
| this.achievement = false; | |
| } | |
| }; | |
| //****************************************************************************** | |
| /** | |
| * Cloud background item. | |
| * Similar to an obstacle object but without collision boxes. | |
| * @param {HTMLCanvasElement} canvas Canvas element. | |
| * @param {Object} spritePos Position of image in sprite. | |
| * @param {number} containerWidth | |
| */ | |
| function Cloud(canvas, spritePos, containerWidth) { | |
| this.canvas = canvas; | |
| this.canvasCtx = this.canvas.getContext('2d'); | |
| this.spritePos = spritePos; | |
| this.containerWidth = containerWidth; | |
| this.xPos = containerWidth; | |
| this.yPos = 0; | |
| this.remove = false; | |
| this.cloudGap = getRandomNum(Cloud.config.MIN_CLOUD_GAP, | |
| Cloud.config.MAX_CLOUD_GAP); | |
| this.init(); | |
| }; | |
| /** | |
| * Cloud object config. | |
| * @enum {number} | |
| */ | |
| Cloud.config = { | |
| HEIGHT: 14, | |
| MAX_CLOUD_GAP: 400, | |
| MAX_SKY_LEVEL: 30, | |
| MIN_CLOUD_GAP: 100, | |
| MIN_SKY_LEVEL: 71, | |
| WIDTH: 46 | |
| }; | |
| Cloud.prototype = { | |
| /** | |
| * Initialise the cloud. Sets the Cloud height. | |
| */ | |
| init: function() { | |
| this.yPos = getRandomNum(Cloud.config.MAX_SKY_LEVEL, | |
| Cloud.config.MIN_SKY_LEVEL); | |
| this.draw(); | |
| }, | |
| /** | |
| * Draw the cloud. | |
| */ | |
| draw: function() { | |
| this.canvasCtx.save(); | |
| var sourceWidth = Cloud.config.WIDTH; | |
| var sourceHeight = Cloud.config.HEIGHT; | |
| var outputWidth = sourceWidth; | |
| var outputHeight = sourceHeight; | |
| if (IS_HIDPI) { | |
| sourceWidth = sourceWidth * 2; | |
| sourceHeight = sourceHeight * 2; | |
| } | |
| this.canvasCtx.drawImage(Runner.imageSprite, this.spritePos.x, | |
| this.spritePos.y, | |
| sourceWidth, sourceHeight, | |
| this.xPos, this.yPos, | |
| outputWidth, outputHeight); | |
| this.canvasCtx.restore(); | |
| }, | |
| /** | |
| * Update the cloud position. | |
| * @param {number} speed | |
| */ | |
| update: function(speed) { | |
| if (!this.remove) { | |
| this.xPos -= Math.ceil(speed); | |
| this.draw(); | |
| // Mark as removeable if no longer in the canvas. | |
| if (!this.isVisible()) { | |
| this.remove = true; | |
| } | |
| } | |
| }, | |
| /** | |
| * Check if the cloud is visible on the stage. | |
| * @return {boolean} | |
| */ | |
| isVisible: function() { | |
| return this.xPos + Cloud.config.WIDTH > 0; | |
| } | |
| }; | |
| //****************************************************************************** | |
| /** | |
| * Nightmode shows a moon and stars on the horizon. | |
| */ | |
| function NightMode(canvas, spritePos, containerWidth) { | |
| this.spritePos = spritePos; | |
| this.canvas = canvas; | |
| this.canvasCtx = canvas.getContext('2d'); | |
| this.xPos = containerWidth - 50; | |
| this.yPos = 30; | |
| this.currentPhase = 0; | |
| this.opacity = 0; | |
| this.containerWidth = containerWidth; | |
| this.stars = []; | |
| this.drawStars = false; | |
| this.placeStars(); | |
| }; | |
| /** | |
| * @enum {number} | |
| */ | |
| NightMode.config = { | |
| FADE_SPEED: 0.035, | |
| HEIGHT: 40, | |
| MOON_SPEED: 0.25, | |
| NUM_STARS: 2, | |
| STAR_SIZE: 9, | |
| STAR_SPEED: 0.3, | |
| STAR_MAX_Y: 70, | |
| WIDTH: 20 | |
| }; | |
| NightMode.phases = [140, 120, 100, 60, 40, 20, 0]; | |
| NightMode.prototype = { | |
| /** | |
| * Update moving moon, changing phases. | |
| * @param {boolean} activated Whether night mode is activated. | |
| * @param {number} delta | |
| */ | |
| update: function(activated, delta) { | |
| // Moon phase. | |
| if (activated && this.opacity == 0) { | |
| this.currentPhase++; | |
| if (this.currentPhase >= NightMode.phases.length) { | |
| this.currentPhase = 0; | |
| } | |
| } | |
| // Fade in / out. | |
| if (activated && (this.opacity < 1 || this.opacity == 0)) { | |
| this.opacity += NightMode.config.FADE_SPEED; | |
| } else if (this.opacity > 0) { | |
| this.opacity -= NightMode.config.FADE_SPEED; | |
| } | |
| // Set moon positioning. | |
| if (this.opacity > 0) { | |
| this.xPos = this.updateXPos(this.xPos, NightMode.config.MOON_SPEED); | |
| // Update stars. | |
| if (this.drawStars) { | |
| for (var i = 0; i < NightMode.config.NUM_STARS; i++) { | |
| this.stars[i].x = this.updateXPos(this.stars[i].x, | |
| NightMode.config.STAR_SPEED); | |
| } | |
| } | |
| this.draw(); | |
| } else { | |
| this.opacity = 0; | |
| this.placeStars(); | |
| } | |
| this.drawStars = true; | |
| }, | |
| updateXPos: function(currentPos, speed) { | |
| if (currentPos < -NightMode.config.WIDTH) { | |
| currentPos = this.containerWidth; | |
| } else { | |
| currentPos -= speed; | |
| } | |
| return currentPos; | |
| }, | |
| draw: function() { | |
| var moonSourceWidth = this.currentPhase == 3 ? NightMode.config.WIDTH * 2 : | |
| NightMode.config.WIDTH; | |
| var moonSourceHeight = NightMode.config.HEIGHT; | |
| var moonSourceX = this.spritePos.x + NightMode.phases[this.currentPhase]; | |
| var moonOutputWidth = moonSourceWidth; | |
| var starSize = NightMode.config.STAR_SIZE; | |
| var starSourceX = Runner.spriteDefinition.LDPI.STAR.x; | |
| if (IS_HIDPI) { | |
| moonSourceWidth *= 2; | |
| moonSourceHeight *= 2; | |
| moonSourceX = this.spritePos.x + | |
| (NightMode.phases[this.currentPhase] * 2); | |
| starSize *= 2; | |
| starSourceX = Runner.spriteDefinition.HDPI.STAR.x; | |
| } | |
| this.canvasCtx.save(); | |
| this.canvasCtx.globalAlpha = this.opacity; | |
| // Stars. | |
| if (this.drawStars) { | |
| for (var i = 0; i < NightMode.config.NUM_STARS; i++) { | |
| this.canvasCtx.drawImage(Runner.imageSprite, | |
| starSourceX, this.stars[i].sourceY, starSize, starSize, | |
| Math.round(this.stars[i].x), this.stars[i].y, | |
| NightMode.config.STAR_SIZE, NightMode.config.STAR_SIZE); | |
| } | |
| } | |
| // Moon. | |
| this.canvasCtx.drawImage(Runner.imageSprite, moonSourceX, | |
| this.spritePos.y, moonSourceWidth, moonSourceHeight, | |
| Math.round(this.xPos), this.yPos, | |
| moonOutputWidth, NightMode.config.HEIGHT); | |
| this.canvasCtx.globalAlpha = 1; | |
| this.canvasCtx.restore(); | |
| }, | |
| // Do star placement. | |
| placeStars: function() { | |
| var segmentSize = Math.round(this.containerWidth / | |
| NightMode.config.NUM_STARS); | |
| for (var i = 0; i < NightMode.config.NUM_STARS; i++) { | |
| this.stars[i] = {}; | |
| this.stars[i].x = getRandomNum(segmentSize * i, segmentSize * (i + 1)); | |
| this.stars[i].y = getRandomNum(0, NightMode.config.STAR_MAX_Y); | |
| if (IS_HIDPI) { | |
| this.stars[i].sourceY = Runner.spriteDefinition.HDPI.STAR.y + | |
| NightMode.config.STAR_SIZE * 2 * i; | |
| } else { | |
| this.stars[i].sourceY = Runner.spriteDefinition.LDPI.STAR.y + | |
| NightMode.config.STAR_SIZE * i; | |
| } | |
| } | |
| }, | |
| reset: function() { | |
| this.currentPhase = 0; | |
| this.opacity = 0; | |
| this.update(false); | |
| } | |
| }; | |
| //****************************************************************************** | |
| /** | |
| * Horizon Line. | |
| * Consists of two connecting lines. Randomly assigns a flat / bumpy horizon. | |
| * @param {HTMLCanvasElement} canvas | |
| * @param {Object} spritePos Horizon position in sprite. | |
| * @constructor | |
| */ | |
| function HorizonLine(canvas, spritePos) { | |
| this.spritePos = spritePos; | |
| this.canvas = canvas; | |
| this.canvasCtx = canvas.getContext('2d'); | |
| this.sourceDimensions = {}; | |
| this.dimensions = HorizonLine.dimensions; | |
| this.sourceXPos = [this.spritePos.x, this.spritePos.x + | |
| this.dimensions.WIDTH]; | |
| this.xPos = []; | |
| this.yPos = 0; | |
| this.bumpThreshold = 0.5; | |
| this.setSourceDimensions(); | |
| this.draw(); | |
| }; | |
| /** | |
| * Horizon line dimensions. | |
| * @enum {number} | |
| */ | |
| HorizonLine.dimensions = { | |
| WIDTH: 600, | |
| HEIGHT: 12, | |
| YPOS: 127 | |
| }; | |
| HorizonLine.prototype = { | |
| /** | |
| * Set the source dimensions of the horizon line. | |
| */ | |
| setSourceDimensions: function() { | |
| for (var dimension in HorizonLine.dimensions) { | |
| if (IS_HIDPI) { | |
| if (dimension != 'YPOS') { | |
| this.sourceDimensions[dimension] = | |
| HorizonLine.dimensions[dimension] * 2; | |
| } | |
| } else { | |
| this.sourceDimensions[dimension] = | |
| HorizonLine.dimensions[dimension]; | |
| } | |
| this.dimensions[dimension] = HorizonLine.dimensions[dimension]; | |
| } | |
| this.xPos = [0, HorizonLine.dimensions.WIDTH]; | |
| this.yPos = HorizonLine.dimensions.YPOS; | |
| }, | |
| /** | |
| * Return the crop x position of a type. | |
| */ | |
| getRandomType: function() { | |
| return Math.random() > this.bumpThreshold ? this.dimensions.WIDTH : 0; | |
| }, | |
| /** | |
| * Draw the horizon line. | |
| */ | |
| draw: function() { | |
| this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[0], | |
| this.spritePos.y, | |
| this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT, | |
| this.xPos[0], this.yPos, | |
| this.dimensions.WIDTH, this.dimensions.HEIGHT); | |
| this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[1], | |
| this.spritePos.y, | |
| this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT, | |
| this.xPos[1], this.yPos, | |
| this.dimensions.WIDTH, this.dimensions.HEIGHT); | |
| }, | |
| /** | |
| * Update the x position of an indivdual piece of the line. | |
| * @param {number} pos Line position. | |
| * @param {number} increment | |
| */ | |
| updateXPos: function(pos, increment) { | |
| var line1 = pos; | |
| var line2 = pos == 0 ? 1 : 0; | |
| this.xPos[line1] -= increment; | |
| this.xPos[line2] = this.xPos[line1] + this.dimensions.WIDTH; | |
| if (this.xPos[line1] <= -this.dimensions.WIDTH) { | |
| this.xPos[line1] += this.dimensions.WIDTH * 2; | |
| this.xPos[line2] = this.xPos[line1] - this.dimensions.WIDTH; | |
| this.sourceXPos[line1] = this.getRandomType() + this.spritePos.x; | |
| } | |
| }, | |
| /** | |
| * Update the horizon line. | |
| * @param {number} deltaTime | |
| * @param {number} speed | |
| */ | |
| update: function(deltaTime, speed) { | |
| var increment = Math.floor(speed * (FPS / 1000) * deltaTime); | |
| if (this.xPos[0] <= 0) { | |
| this.updateXPos(0, increment); | |
| } else { | |
| this.updateXPos(1, increment); | |
| } | |
| this.draw(); | |
| }, | |
| /** | |
| * Reset horizon to the starting position. | |
| */ | |
| reset: function() { | |
| this.xPos[0] = 0; | |
| this.xPos[1] = HorizonLine.dimensions.WIDTH; | |
| } | |
| }; | |
| //****************************************************************************** | |
| /** | |
| * Horizon background class. | |
| * @param {HTMLCanvasElement} canvas | |
| * @param {Object} spritePos Sprite positioning. | |
| * @param {Object} dimensions Canvas dimensions. | |
| * @param {number} gapCoefficient | |
| * @constructor | |
| */ | |
| function Horizon(canvas, spritePos, dimensions, gapCoefficient) { | |
| this.canvas = canvas; | |
| this.canvasCtx = this.canvas.getContext('2d'); | |
| this.config = Horizon.config; | |
| this.dimensions = dimensions; | |
| this.gapCoefficient = gapCoefficient; | |
| this.obstacles = []; | |
| this.obstacleHistory = []; | |
| this.horizonOffsets = [0, 0]; | |
| this.cloudFrequency = this.config.CLOUD_FREQUENCY; | |
| this.spritePos = spritePos; | |
| this.nightMode = null; | |
| // Cloud | |
| this.clouds = []; | |
| this.cloudSpeed = this.config.BG_CLOUD_SPEED; | |
| // Horizon | |
| this.horizonLine = null; | |
| this.init(); | |
| }; | |
| /** | |
| * Horizon config. | |
| * @enum {number} | |
| */ | |
| Horizon.config = { | |
| BG_CLOUD_SPEED: 0.2, | |
| BUMPY_THRESHOLD: .3, | |
| CLOUD_FREQUENCY: .5, | |
| HORIZON_HEIGHT: 16, | |
| MAX_CLOUDS: 6 | |
| }; | |
| Horizon.prototype = { | |
| /** | |
| * Initialise the horizon. Just add the line and a cloud. No obstacles. | |
| */ | |
| init: function() { | |
| this.addCloud(); | |
| this.horizonLine = new HorizonLine(this.canvas, this.spritePos.HORIZON); | |
| this.nightMode = new NightMode(this.canvas, this.spritePos.MOON, | |
| this.dimensions.WIDTH); | |
| }, | |
| /** | |
| * @param {number} deltaTime | |
| * @param {number} currentSpeed | |
| * @param {boolean} updateObstacles Used as an override to prevent | |
| * the obstacles from being updated / added. This happens in the | |
| * ease in section. | |
| * @param {boolean} showNightMode Night mode activated. | |
| */ | |
| update: function(deltaTime, currentSpeed, updateObstacles, showNightMode) { | |
| this.runningTime += deltaTime; | |
| this.horizonLine.update(deltaTime, currentSpeed); | |
| this.nightMode.update(showNightMode); | |
| this.updateClouds(deltaTime, currentSpeed); | |
| if (updateObstacles) { | |
| this.updateObstacles(deltaTime, currentSpeed); | |
| } | |
| }, | |
| /** | |
| * Update the cloud positions. | |
| * @param {number} deltaTime | |
| * @param {number} currentSpeed | |
| */ | |
| updateClouds: function(deltaTime, speed) { | |
| var cloudSpeed = this.cloudSpeed / 1000 * deltaTime * speed; | |
| var numClouds = this.clouds.length; | |
| if (numClouds) { | |
| for (var i = numClouds - 1; i >= 0; i--) { | |
| this.clouds[i].update(cloudSpeed); | |
| } | |
| var lastCloud = this.clouds[numClouds - 1]; | |
| // Check for adding a new cloud. | |
| if (numClouds < this.config.MAX_CLOUDS && | |
| (this.dimensions.WIDTH - lastCloud.xPos) > lastCloud.cloudGap && | |
| this.cloudFrequency > Math.random()) { | |
| this.addCloud(); | |
| } | |
| // Remove expired clouds. | |
| this.clouds = this.clouds.filter(function(obj) { | |
| return !obj.remove; | |
| }); | |
| } else { | |
| this.addCloud(); | |
| } | |
| }, | |
| /** | |
| * Update the obstacle positions. | |
| * @param {number} deltaTime | |
| * @param {number} currentSpeed | |
| */ | |
| updateObstacles: function(deltaTime, currentSpeed) { | |
| // Obstacles, move to Horizon layer. | |
| var updatedObstacles = this.obstacles.slice(0); | |
| for (var i = 0; i < this.obstacles.length; i++) { | |
| var obstacle = this.obstacles[i]; | |
| obstacle.update(deltaTime, currentSpeed); | |
| // Clean up existing obstacles. | |
| if (obstacle.remove) { | |
| updatedObstacles.shift(); | |
| } | |
| } | |
| this.obstacles = updatedObstacles; | |
| if (this.obstacles.length > 0) { | |
| var lastObstacle = this.obstacles[this.obstacles.length - 1]; | |
| if (lastObstacle && !lastObstacle.followingObstacleCreated && | |
| lastObstacle.isVisible() && | |
| (lastObstacle.xPos + lastObstacle.width + lastObstacle.gap) < | |
| this.dimensions.WIDTH) { | |
| this.addNewObstacle(currentSpeed); | |
| lastObstacle.followingObstacleCreated = true; | |
| } | |
| } else { | |
| // Create new obstacles. | |
| this.addNewObstacle(currentSpeed); | |
| } | |
| }, | |
| removeFirstObstacle: function() { | |
| this.obstacles.shift(); | |
| }, | |
| /** | |
| * Add a new obstacle. | |
| * @param {number} currentSpeed | |
| */ | |
| addNewObstacle: function(currentSpeed) { | |
| var obstacleTypeIndex = getRandomNum(0, Obstacle.types.length - 1); | |
| var obstacleType = Obstacle.types[obstacleTypeIndex]; | |
| // Check for multiples of the same type of obstacle. | |
| // Also check obstacle is available at current speed. | |
| if (this.duplicateObstacleCheck(obstacleType.type) || | |
| currentSpeed < obstacleType.minSpeed) { | |
| this.addNewObstacle(currentSpeed); | |
| } else { | |
| var obstacleSpritePos = this.spritePos[obstacleType.type]; | |
| this.obstacles.push(new Obstacle(this.canvasCtx, obstacleType, | |
| obstacleSpritePos, this.dimensions, | |
| this.gapCoefficient, currentSpeed, obstacleType.width)); | |
| this.obstacleHistory.unshift(obstacleType.type); | |
| if (this.obstacleHistory.length > 1) { | |
| this.obstacleHistory.splice(Runner.config.MAX_OBSTACLE_DUPLICATION); | |
| } | |
| } | |
| }, | |
| /** | |
| * Returns whether the previous two obstacles are the same as the next one. | |
| * Maximum duplication is set in config value MAX_OBSTACLE_DUPLICATION. | |
| * @return {boolean} | |
| */ | |
| duplicateObstacleCheck: function(nextObstacleType) { | |
| var duplicateCount = 0; | |
| for (var i = 0; i < this.obstacleHistory.length; i++) { | |
| duplicateCount = this.obstacleHistory[i] == nextObstacleType ? | |
| duplicateCount + 1 : 0; | |
| } | |
| return duplicateCount >= Runner.config.MAX_OBSTACLE_DUPLICATION; | |
| }, | |
| /** | |
| * Reset the horizon layer. | |
| * Remove existing obstacles and reposition the horizon line. | |
| */ | |
| reset: function() { | |
| this.obstacles = []; | |
| this.horizonLine.reset(); | |
| this.nightMode.reset(); | |
| }, | |
| /** | |
| * Update the canvas width and scaling. | |
| * @param {number} width Canvas width. | |
| * @param {number} height Canvas height. | |
| */ | |
| resize: function(width, height) { | |
| this.canvas.width = width; | |
| this.canvas.height = height; | |
| }, | |
| /** | |
| * Add a new cloud to the horizon. | |
| */ | |
| addCloud: function() { | |
| this.clouds.push(new Cloud(this.canvas, this.spritePos.CLOUD, | |
| this.dimensions.WIDTH)); | |
| } | |
| }; | |
| })(); |
Skip to content
Search…
All gists
Back to GitHub
@noeff53
Please verify your email address to access all of GitHub’s features.
An email containing verification instructions was sent to [email protected].
@washingtonsoares
washingtonsoares/google_dinosaur.js
Created 2 years ago • Report abuse
0
Code
Revisions 1
Google Dinosaur Game
google_dinosaur.js
// Copyright (c) 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
(function() {
'use strict';
/**
- T-Rex runner.
- @param {string} outerContainerId Outer containing element id.
- @param {Object} opt_config
- @constructor
- @export
*/
function Runner(outerContainerId, opt_config) {
// Singleton
if (Runner.instance_) {
return Runner.instance_;
}
Runner.instance_ = this;
this.outerContainerEl = document.querySelector(outerContainerId);
this.containerEl = null;
this.snackbarEl = null;
// A div to intercept touch events. Only set while (playing && useTouch).
this.touchController = null;
this.config = opt_config || Runner.config;
// Logical dimensions of the container.
this.dimensions = Runner.defaultDimensions;
this.canvas = null;
this.canvasCtx = null;
this.tRex = null;
this.distanceMeter = null;
this.distanceRan = 0;
this.highestScore = 0;
this.syncHighestScore = false;
this.time = 0;
this.runningTime = 0;
this.msPerFrame = 1000 / FPS;
this.currentSpeed = this.config.SPEED;
this.obstacles = [];
this.activated = false; // Whether the easter egg has been activated.
this.playing = false; // Whether the game is currently in play state.
this.crashed = false;
this.paused = false;
this.inverted = false;
this.invertTimer = 0;
this.resizeTimerId_ = null;
this.playCount = 0;
// Sound FX.
this.audioBuffer = null;
this.soundFx = {};
// Global web audio context for playing sounds.
this.audioContext = null;
// Images.
this.images = {};
this.imagesLoaded = 0;
if (this.isDisabled()) {
this.setupDisabledRunner();
} else {
this.loadImages();
window['initializeEasterEggHighScore'] =
this.initializeHighScore.bind(this);
}
}
window['Runner'] = Runner;
/**
- Default game width.
- @const
*/
var DEFAULT_WIDTH = 600;
/**
- Frames per second.
- @const
*/
var FPS = 60;
/** @const */
var IS_HIDPI = window.devicePixelRatio > 1;
/** @const */
// iPads are returning "MacIntel" for iOS 13 (devices & simulators).
// Chrome on macOS also returns "MacIntel" for navigator.platform,
// but navigator.userAgent includes /Safari/.
// TODO(crbug.com/998999): Fix navigator.userAgent such that it reliably
// returns an agent string containing "CriOS".
var IS_IOS = /iPad|iPhone|iPod|MacIntel/.test(window.navigator.platform) &&
!(/Safari/.test(window.navigator.userAgent));
/** @const */
var IS_MOBILE = /Android/.test(window.navigator.userAgent) || IS_IOS;
/** @const */
var ARCADE_MODE_URL = 'chrome://dino/';
/**
- Default game configuration.
- @enum {number}
*/
Runner.config = {
ACCELERATION: 0.001,
BG_CLOUD_SPEED: 0.2,
BOTTOM_PAD: 10,
// Scroll Y threshold at which the game can be activated.
CANVAS_IN_VIEW_OFFSET: -10,
CLEAR_TIME: 3000,
CLOUD_FREQUENCY: 0.5,
GAMEOVER_CLEAR_TIME: 750,
GAP_COEFFICIENT: 0.6,
GRAVITY: 0.6,
INITIAL_JUMP_VELOCITY: 12,
INVERT_FADE_DURATION: 12000,
INVERT_DISTANCE: 700,
MAX_BLINK_COUNT: 3,
MAX_CLOUDS: 6,
MAX_OBSTACLE_LENGTH: 3,
MAX_OBSTACLE_DUPLICATION: 2,
MAX_SPEED: 13,
MIN_JUMP_HEIGHT: 35,
MOBILE_SPEED_COEFFICIENT: 1.2,
RESOURCE_TEMPLATE_ID: 'audio-resources',
SPEED: 6,
SPEED_DROP_COEFFICIENT: 3,
ARCADE_MODE_INITIAL_TOP_POSITION: 35,
ARCADE_MODE_TOP_POSITION_PERCENT: 0.1
};
/**
- Default dimensions.
- @enum {string}
*/
Runner.defaultDimensions = {
WIDTH: DEFAULT_WIDTH,
HEIGHT: 150
};
/**
- CSS class names.
- @enum {string}
*/
Runner.classes = {
ARCADE_MODE: 'arcade-mode',
CANVAS: 'runner-canvas',
CONTAINER: 'runner-container',
CRASHED: 'crashed',
ICON: 'icon-offline',
INVERTED: 'inverted',
SNACKBAR: 'snackbar',
SNACKBAR_SHOW: 'snackbar-show',
TOUCH_CONTROLLER: 'controller'
};
/**
- Sprite definition layout of the spritesheet.
- @enum {Object}
*/
Runner.spriteDefinition = {
LDPI: {
CACTUS_LARGE: {x: 332, y: 2},
CACTUS_SMALL: {x: 228, y: 2},
CLOUD: {x: 86, y: 2},
HORIZON: {x: 2, y: 54},
MOON: {x: 484, y: 2},
PTERODACTYL: {x: 134, y: 2},
RESTART: {x: 2, y: 2},
TEXT_SPRITE: {x: 655, y: 2},
TREX: {x: 848, y: 2},
STAR: {x: 645, y: 2}
},
HDPI: {
CACTUS_LARGE: {x: 652, y: 2},
CACTUS_SMALL: {x: 446, y: 2},
CLOUD: {x: 166, y: 2},
HORIZON: {x: 2, y: 104},
MOON: {x: 954, y: 2},
PTERODACTYL: {x: 260, y: 2},
RESTART: {x: 2, y: 2},
TEXT_SPRITE: {x: 1294, y: 2},
TREX: {x: 1678, y: 2},
STAR: {x: 1276, y: 2}
}
};
/**
- Sound FX. Reference to the ID of the audio tag on interstitial page.
- @enum {string}
*/
Runner.sounds = {
BUTTON_PRESS: 'offline-sound-press',
HIT: 'offline-sound-hit',
SCORE: 'offline-sound-reached'
};
/**
- Key code mapping.
- @enum {Object}
*/
Runner.keycodes = {
JUMP: {'38': 1, '32': 1}, // Up, spacebar
DUCK: {'40': 1}, // Down
RESTART: {'13': 1} // Enter
};
/**
- Runner event names.
- @enum {string}
*/
Runner.events = {
ANIM_END: 'webkitAnimationEnd',
CLICK: 'click',
KEYDOWN: 'keydown',
KEYUP: 'keyup',
POINTERDOWN: 'pointerdown',
POINTERUP: 'pointerup',
RESIZE: 'resize',
TOUCHEND: 'touchend',
TOUCHSTART: 'touchstart',
VISIBILITY: 'visibilitychange',
BLUR: 'blur',
FOCUS: 'focus',
LOAD: 'load'
};
Runner.prototype = {
/**
- Whether the easter egg has been disabled. CrOS enterprise enrolled devices.
- @return {boolean}
*/
isDisabled: function() {
return loadTimeData && loadTimeData.valueExists('disabledEasterEgg');
},
/**
- For disabled instances, set up a snackbar with the disabled message.
*/
setupDisabledRunner: function() {
this.containerEl = document.createElement('div');
this.containerEl.className = Runner.classes.SNACKBAR;
this.containerEl.textContent = loadTimeData.getValue('disabledEasterEgg');
this.outerContainerEl.appendChild(this.containerEl);
// Show notification when the activation key is pressed.
document.addEventListener(Runner.events.KEYDOWN, function(e) {
if (Runner.keycodes.JUMP[e.keyCode]) {
this.containerEl.classList.add(Runner.classes.SNACKBAR_SHOW);
document.querySelector('.icon').classList.add('icon-disabled');
}
}.bind(this));
},
/**
-
Setting individual settings for debugging.
-
@param {string} setting
-
@param {*} value
*/
updateConfigSetting: function(setting, value) {
if (setting in this.config && value != undefined) {
this.config[setting] = value;switch (setting) {
case 'GRAVITY':
case 'MIN_JUMP_HEIGHT':
case 'SPEED_DROP_COEFFICIENT':
this.tRex.config[setting] = value;
break;
case 'INITIAL_JUMP_VELOCITY':
this.tRex.setJumpVelocity(value);
break;
case 'SPEED':
this.setSpeed(value);
break;
}
}
},
/**
- Cache the appropriate image sprite from the page and get the sprite sheet
- definition.
*/
loadImages: function() {
if (IS_HIDPI) {
Runner.imageSprite = document.getElementById('offline-resources-2x');
this.spriteDef = Runner.spriteDefinition.HDPI;
} else {
Runner.imageSprite = document.getElementById('offline-resources-1x');
this.spriteDef = Runner.spriteDefinition.LDPI;
}
if (Runner.imageSprite.complete) {
this.init();
} else {
// If the images are not yet loaded, add a listener.
Runner.imageSprite.addEventListener(Runner.events.LOAD,
this.init.bind(this));
}
},
/**
-
Load and decode base 64 encoded sounds.
*/
loadSounds: function() {
if (!IS_IOS) {
this.audioContext = new AudioContext();var resourceTemplate =
document.getElementById(this.config.RESOURCE_TEMPLATE_ID).content;for (var sound in Runner.sounds) {
var soundSrc =
resourceTemplate.getElementById(Runner.sounds[sound]).src;
soundSrc = soundSrc.substr(soundSrc.indexOf(',') + 1);
var buffer = decodeBase64ToArrayBuffer(soundSrc);// Async, so no guarantee of order in array.
this.audioContext.decodeAudioData(buffer, function(index, audioData) {
this.soundFx[index] = audioData;
}.bind(this, sound));
}
}
},
/**
- Sets the game speed. Adjust the speed accordingly if on a smaller screen.
- @param {number} opt_speed
*/
setSpeed: function(opt_speed) {
var speed = opt_speed || this.currentSpeed;
// Reduce the speed on smaller mobile screens.
if (this.dimensions.WIDTH < DEFAULT_WIDTH) {
var mobileSpeed = speed * this.dimensions.WIDTH / DEFAULT_WIDTH *
this.config.MOBILE_SPEED_COEFFICIENT;
this.currentSpeed = mobileSpeed > speed ? speed : mobileSpeed;
} else if (opt_speed) {
this.currentSpeed = opt_speed;
}
},
/**
- Game initialiser.
*/
init: function() {
// Hide the static icon.
document.querySelector('.' + Runner.classes.ICON).style.visibility =
'hidden';
this.adjustDimensions();
this.setSpeed();
this.containerEl = document.createElement('div');
this.containerEl.className = Runner.classes.CONTAINER;
// Player canvas container.
this.canvas = createCanvas(this.containerEl, this.dimensions.WIDTH,
this.dimensions.HEIGHT, Runner.classes.PLAYER);
this.canvasCtx = this.canvas.getContext('2d');
this.canvasCtx.fillStyle = '#f7f7f7';
this.canvasCtx.fill();
Runner.updateCanvasScaling(this.canvas);
// Horizon contains clouds, obstacles and the ground.
this.horizon = new Horizon(this.canvas, this.spriteDef, this.dimensions,
this.config.GAP_COEFFICIENT);
// Distance meter
this.distanceMeter = new DistanceMeter(this.canvas,
this.spriteDef.TEXT_SPRITE, this.dimensions.WIDTH);
// Draw t-rex
this.tRex = new Trex(this.canvas, this.spriteDef.TREX);
this.outerContainerEl.appendChild(this.containerEl);
//
this.startListening();
this.update();
window.addEventListener(Runner.events.RESIZE,
this.debounceResize.bind(this));
},
/**
- Create the touch controller. A div that covers whole screen.
*/
createTouchController: function() {
this.touchController = document.createElement('div');
this.touchController.className = Runner.classes.TOUCH_CONTROLLER;
this.touchController.addEventListener(Runner.events.TOUCHSTART, this);
this.touchController.addEventListener(Runner.events.TOUCHEND, this);
this.outerContainerEl.appendChild(this.touchController);
},
/**
- Debounce the resize event.
*/
debounceResize: function() {
if (!this.resizeTimerId_) {
this.resizeTimerId_ =
setInterval(this.adjustDimensions.bind(this), 250);
}
},
/**
- Adjust game space dimensions on resize.
*/
adjustDimensions: function() {
clearInterval(this.resizeTimerId_);
this.resizeTimerId_ = null;
var boxStyles = window.getComputedStyle(this.outerContainerEl);
var padding = Number(boxStyles.paddingLeft.substr(0,
boxStyles.paddingLeft.length - 2));
this.dimensions.WIDTH = this.outerContainerEl.offsetWidth - padding * 2;
if (this.isArcadeMode()) {
this.dimensions.WIDTH = Math.min(DEFAULT_WIDTH, this.dimensions.WIDTH);
if (this.activated) {
this.setArcadeModeContainerScale();
}
}
// Redraw the elements back onto the canvas.
if (this.canvas) {
this.canvas.width = this.dimensions.WIDTH;
this.canvas.height = this.dimensions.HEIGHT;
Runner.updateCanvasScaling(this.canvas);
this.distanceMeter.calcXPos(this.dimensions.WIDTH);
this.clearCanvas();
this.horizon.update(0, 0, true);
this.tRex.update(0);
// Outer container and distance meter.
if (this.playing || this.crashed || this.paused) {
this.containerEl.style.width = this.dimensions.WIDTH + 'px';
this.containerEl.style.height = this.dimensions.HEIGHT + 'px';
this.distanceMeter.update(0, Math.ceil(this.distanceRan));
this.stop();
} else {
this.tRex.draw(0, 0);
}
// Game over panel.
if (this.crashed && this.gameOverPanel) {
this.gameOverPanel.updateDimensions(this.dimensions.WIDTH);
this.gameOverPanel.draw();
}
}
},
/**
- Play the game intro.
- Canvas container width expands out to the full width.
*/
playIntro: function() {
if (!this.activated && !this.crashed) {
this.playingIntro = true;
this.tRex.playingIntro = true;
//
// CSS animation definition.
var keyframes = '@-webkit-keyframes intro { ' +
'from { width:' + Trex.config.WIDTH + 'px }' +
'to { width: ' + this.dimensions.WIDTH + 'px }' +
'}';
document.styleSheets[0].insertRule(keyframes, 0);
this.containerEl.addEventListener(Runner.events.ANIM_END,
this.startGame.bind(this));
this.containerEl.style.webkitAnimation = 'intro .4s ease-out 1 both';
this.containerEl.style.width = this.dimensions.WIDTH + 'px';
this.setPlayStatus(true);
this.activated = true;
} else if (this.crashed) {
this.restart();
}
},
/**
- Update the game status to started.
*/
startGame: function() {
if (this.isArcadeMode()) {
this.setArcadeMode();
}
this.runningTime = 0;
this.playingIntro = false;
this.tRex.playingIntro = false;
this.containerEl.style.webkitAnimation = '';
this.playCount++;
// Handle tabbing off the page. Pause the current game.
document.addEventListener(Runner.events.VISIBILITY,
this.onVisibilityChange.bind(this));
window.addEventListener(Runner.events.BLUR,
this.onVisibilityChange.bind(this));
window.addEventListener(Runner.events.FOCUS,
this.onVisibilityChange.bind(this));
},
clearCanvas: function() {
this.canvasCtx.clearRect(0, 0, this.dimensions.WIDTH,
this.dimensions.HEIGHT);
},
/**
- Checks whether the canvas area is in the viewport of the browser
- through the current scroll position.
- @return boolean.
*/
isCanvasInView: function() {
return this.containerEl.getBoundingClientRect().top >
Runner.config.CANVAS_IN_VIEW_OFFSET;
},
/**
- Update the game frame and schedules the next one.
*/
update: function() {
this.updatePending = false;
var now = getTimeStamp();
var deltaTime = now - (this.time || now);
this.time = now;
if (this.playing) {
this.clearCanvas();
if (this.tRex.jumping) {
this.tRex.updateJump(deltaTime);
}
this.runningTime += deltaTime;
var hasObstacles = this.runningTime > this.config.CLEAR_TIME;
// First jump triggers the intro.
if (this.tRex.jumpCount == 1 && !this.playingIntro) {
this.playIntro();
}
// The horizon doesn't move until the intro is over.
if (this.playingIntro) {
this.horizon.update(0, this.currentSpeed, hasObstacles);
} else {
deltaTime = !this.activated ? 0 : deltaTime;
this.horizon.update(deltaTime, this.currentSpeed, hasObstacles,
this.inverted);
}
// Check for collisions.
var collision = hasObstacles &&
checkForCollision(this.horizon.obstacles[0], this.tRex);
if (!collision) {
this.distanceRan += this.currentSpeed * deltaTime / this.msPerFrame;
if (this.currentSpeed < this.config.MAX_SPEED) {
this.currentSpeed += this.config.ACCELERATION;
}
} else {
this.gameOver();
}
var playAchievementSound = this.distanceMeter.update(deltaTime,
Math.ceil(this.distanceRan));
if (playAchievementSound) {
this.playSound(this.soundFx.SCORE);
}
// Night mode.
if (this.invertTimer > this.config.INVERT_FADE_DURATION) {
this.invertTimer = 0;
this.invertTrigger = false;
this.invert();
} else if (this.invertTimer) {
this.invertTimer += deltaTime;
} else {
var actualDistance =
this.distanceMeter.getActualDistance(Math.ceil(this.distanceRan));
if (actualDistance > 0) {
this.invertTrigger = !(actualDistance %
this.config.INVERT_DISTANCE);
if (this.invertTrigger && this.invertTimer === 0) {
this.invertTimer += deltaTime;
this.invert();
}
}
}
}
if (this.playing || (!this.activated &&
this.tRex.blinkCount < Runner.config.MAX_BLINK_COUNT)) {
this.tRex.update(deltaTime);
this.scheduleNextUpdate();
}
},
/**
- Event handler.
*/
handleEvent: function(e) {
return (function(evtType, events) {
switch (evtType) {
case events.KEYDOWN:
case events.TOUCHSTART:
case events.POINTERDOWN:
this.onKeyDown(e);
break;
case events.KEYUP:
case events.TOUCHEND:
case events.POINTERUP:
this.onKeyUp(e);
break;
}
}.bind(this))(e.type, Runner.events);
},
/**
- Bind relevant key / mouse / touch listeners.
*/
startListening: function() {
// Keys.
document.addEventListener(Runner.events.KEYDOWN, this);
document.addEventListener(Runner.events.KEYUP, this);
// Touch / pointer.
this.containerEl.addEventListener(Runner.events.TOUCHSTART, this);
document.addEventListener(Runner.events.POINTERDOWN, this);
document.addEventListener(Runner.events.POINTERUP, this);
},
/**
- Remove all listeners.
*/
stopListening: function() {
document.removeEventListener(Runner.events.KEYDOWN, this);
document.removeEventListener(Runner.events.KEYUP, this);
if (this.touchController) {
this.touchController.removeEventListener(Runner.events.TOUCHSTART, this);
this.touchController.removeEventListener(Runner.events.TOUCHEND, this);
}
this.containerEl.removeEventListener(Runner.events.TOUCHSTART, this);
document.removeEventListener(Runner.events.POINTERDOWN, this);
document.removeEventListener(Runner.events.POINTERUP, this);
},
/**
- Process keydown.
- @param {Event} e
*/
onKeyDown: function(e) {
// Prevent native page scrolling whilst tapping on mobile.
if (IS_MOBILE && this.playing) {
e.preventDefault();
}
if (this.isCanvasInView()) {
if (!this.crashed && !this.paused) {
if (Runner.keycodes.JUMP[e.keyCode] ||
e.type == Runner.events.TOUCHSTART) {
e.preventDefault();
// Starting the game for the first time.
if (!this.playing) {
// Started by touch so create a touch controller.
if (!this.touchController && e.type == Runner.events.TOUCHSTART) {
this.createTouchController();
}
this.loadSounds();
this.setPlayStatus(true);
this.update();
if (window.errorPageController) {
errorPageController.trackEasterEgg();
}
}
// Start jump.
if (!this.tRex.jumping && !this.tRex.ducking) {
this.playSound(this.soundFx.BUTTON_PRESS);
this.tRex.startJump(this.currentSpeed);
}
} else if (this.playing && Runner.keycodes.DUCK[e.keyCode]) {
e.preventDefault();
if (this.tRex.jumping) {
// Speed drop, activated only when jump key is not pressed.
this.tRex.setSpeedDrop();
} else if (!this.tRex.jumping && !this.tRex.ducking) {
// Duck.
this.tRex.setDuck(true);
}
}
// iOS only triggers touchstart and no pointer events.
} else if (IS_IOS && this.crashed && e.type == Runner.events.TOUCHSTART &&
e.currentTarget == this.containerEl) {
this.handleGameOverClicks(e);
}
}
},
/**
- Process key up.
- @param {Event} e
*/
onKeyUp: function(e) {
var keyCode = String(e.keyCode);
var isjumpKey = Runner.keycodes.JUMP[keyCode] ||
e.type == Runner.events.TOUCHEND ||
e.type == Runner.events.POINTERUP;
if (this.isRunning() && isjumpKey) {
this.tRex.endJump();
} else if (Runner.keycodes.DUCK[keyCode]) {
this.tRex.speedDrop = false;
this.tRex.setDuck(false);
} else if (this.crashed) {
// Check that enough time has elapsed before allowing jump key to restart.
var deltaTime = getTimeStamp() - this.time;
if (this.isCanvasInView() &&
(Runner.keycodes.RESTART[keyCode] || this.isLeftClickOnCanvas(e) ||
(deltaTime >= this.config.GAMEOVER_CLEAR_TIME &&
Runner.keycodes.JUMP[keyCode]))) {
this.handleGameOverClicks(e);
}
} else if (this.paused && isjumpKey) {
// Reset the jump state
this.tRex.reset();
this.play();
}
},
/**
- Handle interactions on the game over screen state.
- A user is able to tap the high score twice to reset it.
- @param {Event} e
*/
handleGameOverClicks: function(e) {
e.preventDefault();
if (this.distanceMeter.hasClickedOnHighScore(e) && this.highestScore) {
if (this.distanceMeter.isHighScoreFlashing()) {
// Subsequent click, reset the high score.
this.saveHighScore(0, true);
this.distanceMeter.resetHighScore();
} else {
// First click, flash the high score.
this.distanceMeter.startHighScoreFlashing();
}
} else {
this.distanceMeter.cancelHighScoreFlashing();
this.restart();
}
},
/**
- Returns whether the event was a left click on canvas.
- On Windows right click is registered as a click.
- @param {Event} e
- @return {boolean}
*/
isLeftClickOnCanvas: function(e) {
return e.button != null && e.button < 2 &&
e.type == Runner.events.POINTERUP && e.target == this.canvas;
},
/**
- RequestAnimationFrame wrapper.
*/
scheduleNextUpdate: function() {
if (!this.updatePending) {
this.updatePending = true;
this.raqId = requestAnimationFrame(this.update.bind(this));
}
},
/**
- Whether the game is running.
- @return {boolean}
*/
isRunning: function() {
return !!this.raqId;
},
/**
- Set the initial high score as stored in the user's profile.
- @param {integer} highScore
*/
initializeHighScore: function(highScore) {
this.syncHighestScore = true;
highScore = Math.ceil(highScore);
if (highScore < this.highestScore) {
if (window.errorPageController) {
errorPageController.updateEasterEggHighScore(this.highestScore);
}
return;
}
this.highestScore = highScore;
this.distanceMeter.setHighScore(this.highestScore);
},
/**
- Sets the current high score and saves to the profile if available.
- @param {number} distanceRan Total distance ran.
- @param {boolean} opt_resetScore Whether to reset the score.
*/
saveHighScore: function(distanceRan, opt_resetScore) {
this.highestScore = Math.ceil(distanceRan);
this.distanceMeter.setHighScore(this.highestScore);
// Store the new high score in the profile.
if (this.syncHighestScore && window.errorPageController) {
if (opt_resetScore) {
errorPageController.resetEasterEggHighScore();
} else {
errorPageController.updateEasterEggHighScore(this.highestScore);
}
}
},
/**
- Game over state.
*/
gameOver: function() {
this.playSound(this.soundFx.HIT);
vibrate(200);
this.stop();
this.crashed = true;
this.distanceMeter.achievement = false;
this.tRex.update(100, Trex.status.CRASHED);
// Game over panel.
if (!this.gameOverPanel) {
this.gameOverPanel = new GameOverPanel(this.canvas,
this.spriteDef.TEXT_SPRITE, this.spriteDef.RESTART,
this.dimensions);
} else {
this.gameOverPanel.draw();
}
// Update the high score.
if (this.distanceRan > this.highestScore) {
this.saveHighScore(this.distanceRan);
}
// Reset the time clock.
this.time = getTimeStamp();
},
stop: function() {
this.setPlayStatus(false);
this.paused = true;
cancelAnimationFrame(this.raqId);
this.raqId = 0;
},
play: function() {
if (!this.crashed) {
this.setPlayStatus(true);
this.paused = false;
this.tRex.update(0, Trex.status.RUNNING);
this.time = getTimeStamp();
this.update();
}
},
restart: function() {
if (!this.raqId) {
this.playCount++;
this.runningTime = 0;
this.setPlayStatus(true);
this.paused = false;
this.crashed = false;
this.distanceRan = 0;
this.setSpeed(this.config.SPEED);
this.time = getTimeStamp();
this.containerEl.classList.remove(Runner.classes.CRASHED);
this.clearCanvas();
this.distanceMeter.reset(this.highestScore);
this.horizon.reset();
this.tRex.reset();
this.playSound(this.soundFx.BUTTON_PRESS);
this.invert(true);
this.bdayFlashTimer = null;
this.update();
}
},
setPlayStatus: function(isPlaying) {
if (this.touchController)
this.touchController.classList.toggle(HIDDEN_CLASS, !isPlaying);
this.playing = isPlaying;
},
/**
- Whether the game should go into arcade mode.
- @return {boolean}
*/
isArcadeMode: function() {
return document.title == ARCADE_MODE_URL;
},
/**
- Hides offline messaging for a fullscreen game only experience.
*/
setArcadeMode: function() {
document.body.classList.add(Runner.classes.ARCADE_MODE);
this.setArcadeModeContainerScale();
},
/**
- Sets the scaling for arcade mode.
*/
setArcadeModeContainerScale: function() {
var windowHeight = window.innerHeight;
var scaleHeight = windowHeight / this.dimensions.HEIGHT;
var scaleWidth = window.innerWidth / this.dimensions.WIDTH;
var scale = Math.max(1, Math.min(scaleHeight, scaleWidth));
var scaledCanvasHeight = this.dimensions.HEIGHT * scale;
// Positions the game container at 10% of the available vertical window
// height minus the game container height.
var translateY = Math.ceil(Math.max(0, (windowHeight - scaledCanvasHeight -
Runner.config.ARCADE_MODE_INITIAL_TOP_POSITION) *
Runner.config.ARCADE_MODE_TOP_POSITION_PERCENT)) *
window.devicePixelRatio;
//
this.containerEl.style.transform = 'scale(' + scale + ') translateY(' +
translateY + 'px)';
},
/**
- Pause the game if the tab is not in focus.
*/
onVisibilityChange: function(e) {
if (document.hidden || document.webkitHidden || e.type == 'blur' ||
document.visibilityState != 'visible') {
this.stop();
} else if (!this.crashed) {
this.tRex.reset();
this.play();
}
},
/**
- Play a sound.
- @param {SoundBuffer} soundBuffer
*/
playSound: function(soundBuffer) {
if (soundBuffer) {
var sourceNode = this.audioContext.createBufferSource();
sourceNode.buffer = soundBuffer;
sourceNode.connect(this.audioContext.destination);
sourceNode.start(0);
}
},
/**
- Inverts the current page / canvas colors.
- @param {boolean} Whether to reset colors.
*/
invert: function(reset) {
let htmlEl = document.firstElementChild;
if (reset) {
htmlEl.classList.toggle(Runner.classes.INVERTED,
false);
this.invertTimer = 0;
this.inverted = false;
} else {
this.inverted = htmlEl.classList.toggle(
Runner.classes.INVERTED, this.invertTrigger);
}
}
};
/**
- Updates the canvas size taking into
- account the backing store pixel ratio and
- the device pixel ratio.
- See article by Paul Lewis:
- http://www.html5rocks.com/en/tutorials/canvas/hidpi/
- @param {HTMLCanvasElement} canvas
- @param {number} opt_width
- @param {number} opt_height
- @return {boolean} Whether the canvas was scaled.
*/
Runner.updateCanvasScaling = function(canvas, opt_width, opt_height) {
var context = canvas.getContext('2d');
// Query the various pixel ratios
var devicePixelRatio = Math.floor(window.devicePixelRatio) || 1;
var backingStoreRatio = Math.floor(context.webkitBackingStorePixelRatio) || 1;
var ratio = devicePixelRatio / backingStoreRatio;
// Upscale the canvas if the two ratios don't match
if (devicePixelRatio !== backingStoreRatio) {
var oldWidth = opt_width || canvas.width;
var oldHeight = opt_height || canvas.height;
canvas.width = oldWidth * ratio;
canvas.height = oldHeight * ratio;
canvas.style.width = oldWidth + 'px';
canvas.style.height = oldHeight + 'px';
// Scale the context to counter the fact that we've manually scaled
// our canvas element.
context.scale(ratio, ratio);
return true;
} else if (devicePixelRatio == 1) {
// Reset the canvas width / height. Fixes scaling bug when the page is
// zoomed and the devicePixelRatio changes accordingly.
canvas.style.width = canvas.width + 'px';
canvas.style.height = canvas.height + 'px';
}
return false;
};
/**
- Get random number.
- @param {number} min
- @param {number} max
- @param {number}
*/
function getRandomNum(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
/**
- Vibrate on mobile devices.
- @param {number} duration Duration of the vibration in milliseconds.
*/
function vibrate(duration) {
if (IS_MOBILE && window.navigator.vibrate) {
window.navigator.vibrate(duration);
}
}
/**
- Create canvas element.
- @param {HTMLElement} container Element to append canvas to.
- @param {number} width
- @param {number} height
- @param {string} opt_classname
- @return {HTMLCanvasElement}
*/
function createCanvas(container, width, height, opt_classname) {
var canvas = document.createElement('canvas');
canvas.className = opt_classname ? Runner.classes.CANVAS + ' ' +
opt_classname : Runner.classes.CANVAS;
canvas.width = width;
canvas.height = height;
container.appendChild(canvas);
return canvas;
}
/**
- Decodes the base 64 audio to ArrayBuffer used by Web Audio.
- @param {string} base64String
*/
function decodeBase64ToArrayBuffer(base64String) {
var len = (base64String.length / 4) * 3;
var str = atob(base64String);
var arrayBuffer = new ArrayBuffer(len);
var bytes = new Uint8Array(arrayBuffer);
for (var i = 0; i < len; i++) {
bytes[i] = str.charCodeAt(i);
}
return bytes.buffer;
}
/**
- Return the current timestamp.
- @return {number}
*/
function getTimeStamp() {
return IS_IOS ? new Date().getTime() : performance.now();
}
//******************************************************************************
/**
- Game over panel.
- @param {!HTMLCanvasElement} canvas
- @param {Object} textImgPos
- @param {Object} restartImgPos
- @param {!Object} dimensions Canvas dimensions.
- @constructor
*/
function GameOverPanel(canvas, textImgPos, restartImgPos, dimensions) {
this.canvas = canvas;
this.canvasCtx = canvas.getContext('2d');
this.canvasDimensions = dimensions;
this.textImgPos = textImgPos;
this.restartImgPos = restartImgPos;
this.draw();
};
/**
- Dimensions used in the panel.
- @enum {number}
*/
GameOverPanel.dimensions = {
TEXT_X: 0,
TEXT_Y: 13,
TEXT_WIDTH: 191,
TEXT_HEIGHT: 11,
RESTART_WIDTH: 36,
RESTART_HEIGHT: 32
};
GameOverPanel.prototype = {
/**
- Update the panel dimensions.
- @param {number} width New canvas width.
- @param {number} opt_height Optional new canvas height.
*/
updateDimensions: function(width, opt_height) {
this.canvasDimensions.WIDTH = width;
if (opt_height) {
this.canvasDimensions.HEIGHT = opt_height;
}
},
/**
- Draw the panel.
*/
draw: function() {
var dimensions = GameOverPanel.dimensions;
var centerX = this.canvasDimensions.WIDTH / 2;
// Game over text.
var textSourceX = dimensions.TEXT_X;
var textSourceY = dimensions.TEXT_Y;
var textSourceWidth = dimensions.TEXT_WIDTH;
var textSourceHeight = dimensions.TEXT_HEIGHT;
var textTargetX = Math.round(centerX - (dimensions.TEXT_WIDTH / 2));
var textTargetY = Math.round((this.canvasDimensions.HEIGHT - 25) / 3);
var textTargetWidth = dimensions.TEXT_WIDTH;
var textTargetHeight = dimensions.TEXT_HEIGHT;
var restartSourceWidth = dimensions.RESTART_WIDTH;
var restartSourceHeight = dimensions.RESTART_HEIGHT;
var restartTargetX = centerX - (dimensions.RESTART_WIDTH / 2);
var restartTargetY = this.canvasDimensions.HEIGHT / 2;
if (IS_HIDPI) {
textSourceY *= 2;
textSourceX *= 2;
textSourceWidth *= 2;
textSourceHeight *= 2;
restartSourceWidth *= 2;
restartSourceHeight *= 2;
}
textSourceX += this.textImgPos.x;
textSourceY += this.textImgPos.y;
// Game over text from sprite.
this.canvasCtx.drawImage(Runner.imageSprite,
textSourceX, textSourceY, textSourceWidth, textSourceHeight,
textTargetX, textTargetY, textTargetWidth, textTargetHeight);
// Restart button.
this.canvasCtx.drawImage(Runner.imageSprite,
this.restartImgPos.x, this.restartImgPos.y,
restartSourceWidth, restartSourceHeight,
restartTargetX, restartTargetY, dimensions.RESTART_WIDTH,
dimensions.RESTART_HEIGHT);
}
};
//******************************************************************************
/**
- Check for a collision.
- @param {!Obstacle} obstacle
- @param {!Trex} tRex T-rex object.
- @param {HTMLCanvasContext} opt_canvasCtx Optional canvas context for drawing
- collision boxes.
- @return {Array}
*/
function checkForCollision(obstacle, tRex, opt_canvasCtx) {
var obstacleBoxXPos = Runner.defaultDimensions.WIDTH + obstacle.xPos;
// Adjustments are made to the bounding box as there is a 1 pixel white
// border around the t-rex and obstacles.
var tRexBox = new CollisionBox(
tRex.xPos + 1,
tRex.yPos + 1,
tRex.config.WIDTH - 2,
tRex.config.HEIGHT - 2);
var obstacleBox = new CollisionBox(
obstacle.xPos + 1,
obstacle.yPos + 1,
obstacle.typeConfig.width * obstacle.size - 2,
obstacle.typeConfig.height - 2);
// Debug outer box
if (opt_canvasCtx) {
drawCollisionBoxes(opt_canvasCtx, tRexBox, obstacleBox);
}
// Simple outer bounds check.
if (boxCompare(tRexBox, obstacleBox)) {
var collisionBoxes = obstacle.collisionBoxes;
var tRexCollisionBoxes = tRex.ducking ?
Trex.collisionBoxes.DUCKING : Trex.collisionBoxes.RUNNING;
// Detailed axis aligned box check.
for (var t = 0; t < tRexCollisionBoxes.length; t++) {
for (var i = 0; i < collisionBoxes.length; i++) {
// Adjust the box to actual positions.
var adjTrexBox =
createAdjustedCollisionBox(tRexCollisionBoxes[t], tRexBox);
var adjObstacleBox =
createAdjustedCollisionBox(collisionBoxes[i], obstacleBox);
var crashed = boxCompare(adjTrexBox, adjObstacleBox);
// Draw boxes for debug.
if (opt_canvasCtx) {
drawCollisionBoxes(opt_canvasCtx, adjTrexBox, adjObstacleBox);
}
if (crashed) {
return [adjTrexBox, adjObstacleBox];
}
}
}
}
return false;
};
/**
- Adjust the collision box.
- @param {!CollisionBox} box The original box.
- @param {!CollisionBox} adjustment Adjustment box.
- @return {CollisionBox} The adjusted collision box object.
*/
function createAdjustedCollisionBox(box, adjustment) {
return new CollisionBox(
box.x + adjustment.x,
box.y + adjustment.y,
box.width,
box.height);
};
/**
- Draw the collision boxes for debug.
*/
function drawCollisionBoxes(canvasCtx, tRexBox, obstacleBox) {
canvasCtx.save();
canvasCtx.strokeStyle = '#f00';
canvasCtx.strokeRect(tRexBox.x, tRexBox.y, tRexBox.width, tRexBox.height);
canvasCtx.strokeStyle = '#0f0';
canvasCtx.strokeRect(obstacleBox.x, obstacleBox.y,
obstacleBox.width, obstacleBox.height);
canvasCtx.restore();
};
/**
- Compare two collision boxes for a collision.
- @param {CollisionBox} tRexBox
- @param {CollisionBox} obstacleBox
- @return {boolean} Whether the boxes intersected.
*/
function boxCompare(tRexBox, obstacleBox) {
var crashed = false;
var tRexBoxX = tRexBox.x;
var tRexBoxY = tRexBox.y;
var obstacleBoxX = obstacleBox.x;
var obstacleBoxY = obstacleBox.y;
// Axis-Aligned Bounding Box method.
if (tRexBox.x < obstacleBoxX + obstacleBox.width &&
tRexBox.x + tRexBox.width > obstacleBoxX &&
tRexBox.y < obstacleBox.y + obstacleBox.height &&
tRexBox.height + tRexBox.y > obstacleBox.y) {
crashed = true;
}
return crashed;
};
//******************************************************************************
/**
- Collision box object.
- @param {number} x X position.
- @param {number} y Y Position.
- @param {number} w Width.
- @param {number} h Height.
*/
function CollisionBox(x, y, w, h) {
this.x = x;
this.y = y;
this.width = w;
this.height = h;
};
//******************************************************************************
/**
- Obstacle.
- @param {HTMLCanvasCtx} canvasCtx
- @param {Obstacle.type} type
- @param {Object} spritePos Obstacle position in sprite.
- @param {Object} dimensions
- @param {number} gapCoefficient Mutipler in determining the gap.
- @param {number} speed
- @param {number} opt_xOffset
*/
function Obstacle(canvasCtx, type, spriteImgPos, dimensions,
gapCoefficient, speed, opt_xOffset) {
this.canvasCtx = canvasCtx;
this.spritePos = spriteImgPos;
this.typeConfig = type;
this.gapCoefficient = gapCoefficient;
this.size = getRandomNum(1, Obstacle.MAX_OBSTACLE_LENGTH);
this.dimensions = dimensions;
this.remove = false;
this.xPos = dimensions.WIDTH + (opt_xOffset || 0);
this.yPos = 0;
this.width = 0;
this.collisionBoxes = [];
this.gap = 0;
this.speedOffset = 0;
// For animated obstacles.
this.currentFrame = 0;
this.timer = 0;
this.init(speed);
};
/**
- Coefficient for calculating the maximum gap.
- @const
*/
Obstacle.MAX_GAP_COEFFICIENT = 1.5;
/**
- Maximum obstacle grouping count.
- @const
*/
Obstacle.MAX_OBSTACLE_LENGTH = 3,
Obstacle.prototype = {
/**
- Initialise the DOM for the obstacle.
- @param {number} speed
*/
init: function(speed) {
this.cloneCollisionBoxes();
// Only allow sizing if we're at the right speed.
if (this.size > 1 && this.typeConfig.multipleSpeed > speed) {
this.size = 1;
}
this.width = this.typeConfig.width * this.size;
// Check if obstacle can be positioned at various heights.
if (Array.isArray(this.typeConfig.yPos)) {
var yPosConfig = IS_MOBILE ? this.typeConfig.yPosMobile :
this.typeConfig.yPos;
this.yPos = yPosConfig[getRandomNum(0, yPosConfig.length - 1)];
} else {
this.yPos = this.typeConfig.yPos;
}
this.draw();
// Make collision box adjustments,
// Central box is adjusted to the size as one box.
// ____ ______ ________
// _| |-| _| |-| _| |-|
// | |<->| | | |<--->| | | |<----->| |
// | | 1 | | | | 2 | | | | 3 | |
// |_|___|_| |_|_____|_| |_|_______|_|
//
if (this.size > 1) {
this.collisionBoxes[1].width = this.width - this.collisionBoxes[0].width -
this.collisionBoxes[2].width;
this.collisionBoxes[2].x = this.width - this.collisionBoxes[2].width;
}
// For obstacles that go at a different speed from the horizon.
if (this.typeConfig.speedOffset) {
this.speedOffset = Math.random() > 0.5 ? this.typeConfig.speedOffset :
-this.typeConfig.speedOffset;
}
this.gap = this.getGap(this.gapCoefficient, speed);
},
/**
- Draw and crop based on size.
*/
draw: function() {
var sourceWidth = this.typeConfig.width;
var sourceHeight = this.typeConfig.height;
if (IS_HIDPI) {
sourceWidth = sourceWidth * 2;
sourceHeight = sourceHeight * 2;
}
// X position in sprite.
var sourceX = (sourceWidth * this.size) * (0.5 * (this.size - 1)) +
this.spritePos.x;
// Animation frames.
if (this.currentFrame > 0) {
sourceX += sourceWidth * this.currentFrame;
}
this.canvasCtx.drawImage(Runner.imageSprite,
sourceX, this.spritePos.y,
sourceWidth * this.size, sourceHeight,
this.xPos, this.yPos,
this.typeConfig.width * this.size, this.typeConfig.height);
},
/**
-
Obstacle frame update.
-
@param {number} deltaTime
-
@param {number} speed
*/
update: function(deltaTime, speed) {
if (!this.remove) {
if (this.typeConfig.speedOffset) {
speed += this.speedOffset;
}
this.xPos -= Math.floor((speed * FPS / 1000) * deltaTime);// Update frame
if (this.typeConfig.numFrames) {
this.timer += deltaTime;
if (this.timer >= this.typeConfig.frameRate) {
this.currentFrame =
this.currentFrame == this.typeConfig.numFrames - 1 ?
0 : this.currentFrame + 1;
this.timer = 0;
}
}
this.draw();if (!this.isVisible()) {
this.remove = true;
}
}
},
/**
- Calculate a random gap size.
-
- Minimum gap gets wider as speed increses
- @param {number} gapCoefficient
- @param {number} speed
- @return {number} The gap size.
*/
getGap: function(gapCoefficient, speed) {
var minGap = Math.round(this.width * speed +
this.typeConfig.minGap * gapCoefficient);
var maxGap = Math.round(minGap * Obstacle.MAX_GAP_COEFFICIENT);
return getRandomNum(minGap, maxGap);
},
/**
- Check if obstacle is visible.
- @return {boolean} Whether the obstacle is in the game area.
*/
isVisible: function() {
return this.xPos + this.width > 0;
},
/**
- Make a copy of the collision boxes, since these will change based on
- obstacle type and size.
*/
cloneCollisionBoxes: function() {
var collisionBoxes = this.typeConfig.collisionBoxes;
for (var i = collisionBoxes.length - 1; i >= 0; i--) {
this.collisionBoxes[i] = new CollisionBox(collisionBoxes[i].x,
collisionBoxes[i].y, collisionBoxes[i].width,
collisionBoxes[i].height);
}
}
};
/**
- Obstacle definitions.
- minGap: minimum pixel space betweeen obstacles.
- multipleSpeed: Speed at which multiples are allowed.
- speedOffset: speed faster / slower than the horizon.
- minSpeed: Minimum speed which the obstacle can make an appearance.
*/
Obstacle.types = [
{
type: 'CACTUS_SMALL',
width: 17,
height: 35,
yPos: 105,
multipleSpeed: 4,
minGap: 120,
minSpeed: 0,
collisionBoxes: [
new CollisionBox(0, 7, 5, 27),
new CollisionBox(4, 0, 6, 34),
new CollisionBox(10, 4, 7, 14)
]
},
{
type: 'CACTUS_LARGE',
width: 25,
height: 50,
yPos: 90,
multipleSpeed: 7,
minGap: 120,
minSpeed: 0,
collisionBoxes: [
new CollisionBox(0, 12, 7, 38),
new CollisionBox(8, 0, 7, 49),
new CollisionBox(13, 10, 10, 38)
]
},
{
type: 'PTERODACTYL',
width: 46,
height: 40,
yPos: [ 100, 75, 50 ], // Variable height.
yPosMobile: [ 100, 50 ], // Variable height mobile.
multipleSpeed: 999,
minSpeed: 8.5,
minGap: 150,
collisionBoxes: [
new CollisionBox(15, 15, 16, 5),
new CollisionBox(18, 21, 24, 6),
new CollisionBox(2, 14, 4, 3),
new CollisionBox(6, 10, 4, 7),
new CollisionBox(10, 8, 6, 9)
],
numFrames: 2,
frameRate: 1000/6,
speedOffset: .8
}
];
//******************************************************************************
/**
- T-rex game character.
- @param {HTMLCanvas} canvas
- @param {Object} spritePos Positioning within image sprite.
- @constructor
*/
function Trex(canvas, spritePos) {
this.canvas = canvas;
this.canvasCtx = canvas.getContext('2d');
this.spritePos = spritePos;
this.xPos = 0;
this.yPos = 0;
// Position when on the ground.
this.groundYPos = 0;
this.currentFrame = 0;
this.currentAnimFrames = [];
this.blinkDelay = 0;
this.blinkCount = 0;
this.animStartTime = 0;
this.timer = 0;
this.msPerFrame = 1000 / FPS;
this.config = Trex.config;
// Current status.
this.status = Trex.status.WAITING;
this.jumping = false;
this.ducking = false;
this.jumpVelocity = 0;
this.reachedMinHeight = false;
this.speedDrop = false;
this.jumpCount = 0;
this.jumpspotX = 0;
this.init();
};
/**
- T-rex player config.
- @enum {number}
*/
Trex.config = {
DROP_VELOCITY: -5,
GRAVITY: 0.6,
HEIGHT: 47,
HEIGHT_DUCK: 25,
INIITAL_JUMP_VELOCITY: -10,
INTRO_DURATION: 1500,
MAX_JUMP_HEIGHT: 30,
MIN_JUMP_HEIGHT: 30,
SPEED_DROP_COEFFICIENT: 3,
SPRITE_WIDTH: 262,
START_X_POS: 50,
WIDTH: 44,
WIDTH_DUCK: 59
};
/**
- Used in collision detection.
- @type {Array}
*/
Trex.collisionBoxes = {
DUCKING: [
new CollisionBox(1, 18, 55, 25)
],
RUNNING: [
new CollisionBox(22, 0, 17, 16),
new CollisionBox(1, 18, 30, 9),
new CollisionBox(10, 35, 14, 8),
new CollisionBox(1, 24, 29, 5),
new CollisionBox(5, 30, 21, 4),
new CollisionBox(9, 34, 15, 4)
]
};
/**
- Animation states.
- @enum {string}
*/
Trex.status = {
CRASHED: 'CRASHED',
DUCKING: 'DUCKING',
JUMPING: 'JUMPING',
RUNNING: 'RUNNING',
WAITING: 'WAITING'
};
/**
- Blinking coefficient.
- @const
*/
Trex.BLINK_TIMING = 7000;
/**
- Animation config for different states.
- @enum {Object}
*/
Trex.animFrames = {
WAITING: {
frames: [44, 0],
msPerFrame: 1000 / 3
},
RUNNING: {
frames: [88, 132],
msPerFrame: 1000 / 12
},
CRASHED: {
frames: [220],
msPerFrame: 1000 / 60
},
JUMPING: {
frames: [0],
msPerFrame: 1000 / 60
},
DUCKING: {
frames: [264, 323],
msPerFrame: 1000 / 8
}
};
Trex.prototype = {
/**
- T-rex player initaliser.
- Sets the t-rex to blink at random intervals.
*/
init: function() {
this.groundYPos = Runner.defaultDimensions.HEIGHT - this.config.HEIGHT -
Runner.config.BOTTOM_PAD;
this.yPos = this.groundYPos;
this.minJumpHeight = this.groundYPos - this.config.MIN_JUMP_HEIGHT;
this.draw(0, 0);
this.update(0, Trex.status.WAITING);
},
/**
- Setter for the jump velocity.
- The approriate drop velocity is also set.
*/
setJumpVelocity: function(setting) {
this.config.INIITAL_JUMP_VELOCITY = -setting;
this.config.DROP_VELOCITY = -setting / 2;
},
/**
- Set the animation status.
- @param {!number} deltaTime
- @param {Trex.status} status Optional status to switch to.
*/
update: function(deltaTime, opt_status) {
this.timer += deltaTime;
// Update the status.
if (opt_status) {
this.status = opt_status;
this.currentFrame = 0;
this.msPerFrame = Trex.animFrames[opt_status].msPerFrame;
this.currentAnimFrames = Trex.animFrames[opt_status].frames;
if (opt_status == Trex.status.WAITING) {
this.animStartTime = getTimeStamp();
this.setBlinkDelay();
}
}
// Game intro animation, T-rex moves in from the left.
if (this.playingIntro && this.xPos < this.config.START_X_POS) {
this.xPos += Math.round((this.config.START_X_POS /
this.config.INTRO_DURATION) * deltaTime);
}
if (this.status == Trex.status.WAITING) {
this.blink(getTimeStamp());
} else {
this.draw(this.currentAnimFrames[this.currentFrame], 0);
}
// Update the frame position.
if (this.timer >= this.msPerFrame) {
this.currentFrame = this.currentFrame ==
this.currentAnimFrames.length - 1 ? 0 : this.currentFrame + 1;
this.timer = 0;
}
// Speed drop becomes duck if the down key is still being pressed.
if (this.speedDrop && this.yPos == this.groundYPos) {
this.speedDrop = false;
this.setDuck(true);
}
},
/**
- Draw the t-rex to a particular position.
- @param {number} x
- @param {number} y
*/
draw: function(x, y) {
var sourceX = x;
var sourceY = y;
var sourceWidth = this.ducking && this.status != Trex.status.CRASHED ?
this.config.WIDTH_DUCK : this.config.WIDTH;
var sourceHeight = this.config.HEIGHT;
var outputHeight = sourceHeight;
if (IS_HIDPI) {
sourceX *= 2;
sourceY *= 2;
sourceWidth *= 2;
sourceHeight *= 2;
}
// Adjustments for sprite sheet position.
sourceX += this.spritePos.x;
sourceY += this.spritePos.y;
// Ducking.
if (this.ducking && this.status != Trex.status.CRASHED) {
this.canvasCtx.drawImage(Runner.imageSprite, sourceX, sourceY,
sourceWidth, sourceHeight,
this.xPos, this.yPos,
this.config.WIDTH_DUCK, outputHeight);
} else {
// Crashed whilst ducking. Trex is standing up so needs adjustment.
if (this.ducking && this.status == Trex.status.CRASHED) {
this.xPos++;
}
// Standing / running
this.canvasCtx.drawImage(Runner.imageSprite, sourceX, sourceY,
sourceWidth, sourceHeight,
this.xPos, this.yPos,
this.config.WIDTH, outputHeight);
}
this.canvasCtx.globalAlpha = 1;
},
/**
- Sets a random time for the blink to happen.
*/
setBlinkDelay: function() {
this.blinkDelay = Math.ceil(Math.random() * Trex.BLINK_TIMING);
},
/**
- Make t-rex blink at random intervals.
- @param {number} time Current time in milliseconds.
*/
blink: function(time) {
var deltaTime = time - this.animStartTime;
if (deltaTime >= this.blinkDelay) {
this.draw(this.currentAnimFrames[this.currentFrame], 0);
if (this.currentFrame == 1) {
// Set new random delay to blink.
this.setBlinkDelay();
this.animStartTime = time;
this.blinkCount++;
}
}
},
/**
- Initialise a jump.
- @param {number} speed
*/
startJump: function(speed) {
if (!this.jumping) {
this.update(0, Trex.status.JUMPING);
// Tweak the jump velocity based on the speed.
this.jumpVelocity = this.config.INIITAL_JUMP_VELOCITY - (speed / 10);
this.jumping = true;
this.reachedMinHeight = false;
this.speedDrop = false;
}
},
/**
- Jump is complete, falling down.
*/
endJump: function() {
if (this.reachedMinHeight &&
this.jumpVelocity < this.config.DROP_VELOCITY) {
this.jumpVelocity = this.config.DROP_VELOCITY;
}
},
/**
- Update frame for a jump.
- @param {number} deltaTime
- @param {number} speed
*/
updateJump: function(deltaTime, speed) {
var msPerFrame = Trex.animFrames[this.status].msPerFrame;
var framesElapsed = deltaTime / msPerFrame;
// Speed drop makes Trex fall faster.
if (this.speedDrop) {
this.yPos += Math.round(this.jumpVelocity *
this.config.SPEED_DROP_COEFFICIENT * framesElapsed);
} else {
this.yPos += Math.round(this.jumpVelocity * framesElapsed);
}
this.jumpVelocity += this.config.GRAVITY * framesElapsed;
// Minimum height has been reached.
if (this.yPos < this.minJumpHeight || this.speedDrop) {
this.reachedMinHeight = true;
}
// Reached max height
if (this.yPos < this.config.MAX_JUMP_HEIGHT || this.speedDrop) {
this.endJump();
}
// Back down at ground level. Jump completed.
if (this.yPos > this.groundYPos) {
this.reset();
this.jumpCount++;
}
},
/**
- Set the speed drop. Immediately cancels the current jump.
*/
setSpeedDrop: function() {
this.speedDrop = true;
this.jumpVelocity = 1;
},
/**
- @param {boolean} isDucking.
*/
setDuck: function(isDucking) {
if (isDucking && this.status != Trex.status.DUCKING) {
this.update(0, Trex.status.DUCKING);
this.ducking = true;
} else if (this.status == Trex.status.DUCKING) {
this.update(0, Trex.status.RUNNING);
this.ducking = false;
}
},
/**
- Reset the t-rex to running at start of game.
*/
reset: function() {
this.yPos = this.groundYPos;
this.jumpVelocity = 0;
this.jumping = false;
this.ducking = false;
this.update(0, Trex.status.RUNNING);
this.midair = false;
this.speedDrop = false;
this.jumpCount = 0;
}
};
//******************************************************************************
/**
- Handles displaying the distance meter.
- @param {!HTMLCanvasElement} canvas
- @param {Object} spritePos Image position in sprite.
- @param {number} canvasWidth
- @constructor
*/
function DistanceMeter(canvas, spritePos, canvasWidth) {
this.canvas = canvas;
this.canvasCtx = canvas.getContext('2d');
this.image = Runner.imageSprite;
this.spritePos = spritePos;
this.x = 0;
this.y = 5;
this.currentDistance = 0;
this.maxScore = 0;
this.highScore = 0;
this.container = null;
this.digits = [];
this.achievement = false;
this.defaultString = '';
this.flashTimer = 0;
this.flashIterations = 0;
this.invertTrigger = false;
this.flashingRafId = null;
this.highScoreBounds = {};
this.highScoreFlashing = false;
this.config = DistanceMeter.config;
this.maxScoreUnits = this.config.MAX_DISTANCE_UNITS;
this.init(canvasWidth);
};
/**
- @enum {number}
*/
DistanceMeter.dimensions = {
WIDTH: 10,
HEIGHT: 13,
DEST_WIDTH: 11
};
/**
- Y positioning of the digits in the sprite sheet.
- X position is always 0.
- @type {Array}
*/
DistanceMeter.yPos = [0, 13, 27, 40, 53, 67, 80, 93, 107, 120];
/**
- Distance meter config.
- @enum {number}
*/
DistanceMeter.config = {
// Number of digits.
MAX_DISTANCE_UNITS: 5,
// Distance that causes achievement animation.
ACHIEVEMENT_DISTANCE: 100,
// Used for conversion from pixel distance to a scaled unit.
COEFFICIENT: 0.025,
// Flash duration in milliseconds.
FLASH_DURATION: 1000 / 4,
// Flash iterations for achievement animation.
FLASH_ITERATIONS: 3,
// Padding around the high score hit area.
HIGH_SCORE_HIT_AREA_PADDING: 4
};
DistanceMeter.prototype = {
/**
- Initialise the distance meter to '00000'.
- @param {number} width Canvas width in px.
*/
init: function(width) {
var maxDistanceStr = '';
this.calcXPos(width);
this.maxScore = this.maxScoreUnits;
for (var i = 0; i < this.maxScoreUnits; i++) {
this.draw(i, 0);
this.defaultString += '0';
maxDistanceStr += '9';
}
this.maxScore = parseInt(maxDistanceStr);
},
/**
- Calculate the xPos in the canvas.
- @param {number} canvasWidth
*/
calcXPos: function(canvasWidth) {
this.x = canvasWidth - (DistanceMeter.dimensions.DEST_WIDTH *
(this.maxScoreUnits + 1));
},
/**
- Draw a digit to canvas.
- @param {number} digitPos Position of the digit.
- @param {number} value Digit value 0-9.
- @param {boolean} opt_highScore Whether drawing the high score.
*/
draw: function(digitPos, value, opt_highScore) {
var sourceWidth = DistanceMeter.dimensions.WIDTH;
var sourceHeight = DistanceMeter.dimensions.HEIGHT;
var sourceX = DistanceMeter.dimensions.WIDTH * value;
var sourceY = 0;
var targetX = digitPos * DistanceMeter.dimensions.DEST_WIDTH;
var targetY = this.y;
var targetWidth = DistanceMeter.dimensions.WIDTH;
var targetHeight = DistanceMeter.dimensions.HEIGHT;
// For high DPI we 2x source values.
if (IS_HIDPI) {
sourceWidth *= 2;
sourceHeight *= 2;
sourceX *= 2;
}
sourceX += this.spritePos.x;
sourceY += this.spritePos.y;
this.canvasCtx.save();
if (opt_highScore) {
// Left of the current score.
var highScoreX = this.x - (this.maxScoreUnits * 2) *
DistanceMeter.dimensions.WIDTH;
this.canvasCtx.translate(highScoreX, this.y);
} else {
this.canvasCtx.translate(this.x, this.y);
}
this.canvasCtx.drawImage(this.image, sourceX, sourceY,
sourceWidth, sourceHeight,
targetX, targetY,
targetWidth, targetHeight
);
this.canvasCtx.restore();
},
/**
- Covert pixel distance to a 'real' distance.
- @param {number} distance Pixel distance ran.
- @return {number} The 'real' distance ran.
*/
getActualDistance: function(distance) {
return distance ? Math.round(distance * this.config.COEFFICIENT) : 0;
},
/**
- Update the distance meter.
- @param {number} distance
- @param {number} deltaTime
- @return {boolean} Whether the acheivement sound fx should be played.
*/
update: function(deltaTime, distance) {
var paint = true;
var playSound = false;
if (!this.achievement) {
distance = this.getActualDistance(distance);
// Score has gone beyond the initial digit count.
if (distance > this.maxScore && this.maxScoreUnits ==
this.config.MAX_DISTANCE_UNITS) {
this.maxScoreUnits++;
this.maxScore = parseInt(this.maxScore + '9');
} else {
this.distance = 0;
}
if (distance > 0) {
// Acheivement unlocked
if (distance % this.config.ACHIEVEMENT_DISTANCE == 0) {
// Flash score and play sound.
this.achievement = true;
this.flashTimer = 0;
playSound = true;
}
// Create a string representation of the distance with leading 0.
var distanceStr = (this.defaultString +
distance).substr(-this.maxScoreUnits);
this.digits = distanceStr.split('');
} else {
this.digits = this.defaultString.split('');
}
} else {
// Control flashing of the score on reaching acheivement.
if (this.flashIterations <= this.config.FLASH_ITERATIONS) {
this.flashTimer += deltaTime;
if (this.flashTimer < this.config.FLASH_DURATION) {
paint = false;
} else if (this.flashTimer >
this.config.FLASH_DURATION * 2) {
this.flashTimer = 0;
this.flashIterations++;
}
} else {
this.achievement = false;
this.flashIterations = 0;
this.flashTimer = 0;
}
}
// Draw the digits if not flashing.
if (paint) {
for (var i = this.digits.length - 1; i >= 0; i--) {
this.draw(i, parseInt(this.digits[i]));
}
}
this.drawHighScore();
return playSound;
},
/**
- Draw the high score.
*/
drawHighScore: function() {
this.canvasCtx.save();
this.canvasCtx.globalAlpha = .8;
for (var i = this.highScore.length - 1; i >= 0; i--) {
this.draw(i, parseInt(this.highScore[i], 10), true);
}
this.canvasCtx.restore();
},
/**
- Set the highscore as a array string.
- Position of char in the sprite: H - 10, I - 11.
- @param {number} distance Distance ran in pixels.
*/
setHighScore: function(distance) {
distance = this.getActualDistance(distance);
var highScoreStr = (this.defaultString +
distance).substr(-this.maxScoreUnits);
this.highScore = ['10', '11', ''].concat(highScoreStr.split(''));
},
/**
- Whether a clicked is in the high score area.
- @param {TouchEvent|ClickEvent} e Event object.
- @return {boolean} Whether the click was in the high score bounds.
*/
hasClickedOnHighScore: function(e) {
var x = 0;
var y = 0;
if (e.touches) {
// Bounds for touch differ from pointer.
var canvasBounds = this.canvas.getBoundingClientRect();
x = e.touches[0].clientX - canvasBounds.left;
y = e.touches[0].clientY - canvasBounds.top;
} else {
x = e.offsetX;
y = e.offsetY;
}
this.highScoreBounds = this.getHighScoreBounds();
return x >= this.highScoreBounds.x && x <=
this.highScoreBounds.x + this.highScoreBounds.width &&
y >= this.highScoreBounds.y && y <=
this.highScoreBounds.y + this.highScoreBounds.height;
},
/**
- Get the bounding box for the high score.
- @return {Object} Object with x, y, width and height properties.
*/
getHighScoreBounds: function() {
return {
x: (this.x - (this.maxScoreUnits * 2) *
DistanceMeter.dimensions.WIDTH) -
DistanceMeter.config.HIGH_SCORE_HIT_AREA_PADDING,
y: this.y,
width: DistanceMeter.dimensions.WIDTH * (this.highScore.length + 1) +
DistanceMeter.config.HIGH_SCORE_HIT_AREA_PADDING,
height: DistanceMeter.dimensions.HEIGHT +
(DistanceMeter.config.HIGH_SCORE_HIT_AREA_PADDING * 2)
};
},
/**
- Animate flashing the high score to indicate ready for resetting.
- The flashing stops following this.config.FLASH_ITERATIONS x 2 flashes.
*/
flashHighScore: function() {
var now = getTimeStamp();
var deltaTime = now - (this.frameTimeStamp || now);
var paint = true;
this.frameTimeStamp = now;
// Reached the max number of flashes.
if (this.flashIterations > this.config.FLASH_ITERATIONS * 2) {
this.cancelHighScoreFlashing();
return;
}
this.flashTimer += deltaTime;
if (this.flashTimer < this.config.FLASH_DURATION) {
paint = false;
} else if (this.flashTimer > this.config.FLASH_DURATION * 2) {
this.flashTimer = 0;
this.flashIterations++;
}
if (paint) {
this.drawHighScore();
} else {
this.clearHighScoreBounds();
}
// Frame update.
this.flashingRafId =
requestAnimationFrame(this.flashHighScore.bind(this));
},
/**
- Draw empty rectangle over high score.
*/
clearHighScoreBounds: function() {
this.canvasCtx.save();
this.canvasCtx.fillStyle = '#fff';
this.canvasCtx.rect(this.highScoreBounds.x, this.highScoreBounds.y,
this.highScoreBounds.width, this.highScoreBounds.height);
this.canvasCtx.fill();
this.canvasCtx.restore();
},
/**
- Starts the flashing of the high score.
*/
startHighScoreFlashing() {
this.highScoreFlashing = true;
this.flashHighScore();
},
/**
- Whether high score is flashing.
- @return {boolean}
*/
isHighScoreFlashing() {
return this.highScoreFlashing;
},
/**
- Stop flashing the high score.
*/
cancelHighScoreFlashing: function() {
cancelAnimationFrame(this.flashingRafId);
this.flashIterations = 0;
this.flashTimer = 0;
this.highScoreFlashing = false;
this.clearHighScoreBounds();
this.drawHighScore();
},
/**
- Clear the high score.
*/
resetHighScore: function() {
this.setHighScore(0);
this.cancelHighScoreFlashing();
},
/**
- Reset the distance meter back to '00000'.
*/
reset: function() {
this.update(0);
this.achievement = false;
}
};
//******************************************************************************
/**
- Cloud background item.
- Similar to an obstacle object but without collision boxes.
- @param {HTMLCanvasElement} canvas Canvas element.
- @param {Object} spritePos Position of image in sprite.
- @param {number} containerWidth
*/
function Cloud(canvas, spritePos, containerWidth) {
this.canvas = canvas;
this.canvasCtx = this.canvas.getContext('2d');
this.spritePos = spritePos;
this.containerWidth = containerWidth;
this.xPos = containerWidth;
this.yPos = 0;
this.remove = false;
this.cloudGap = getRandomNum(Cloud.config.MIN_CLOUD_GAP,
Cloud.config.MAX_CLOUD_GAP);
this.init();
};
/**
- Cloud object config.
- @enum {number}
*/
Cloud.config = {
HEIGHT: 14,
MAX_CLOUD_GAP: 400,
MAX_SKY_LEVEL: 30,
MIN_CLOUD_GAP: 100,
MIN_SKY_LEVEL: 71,
WIDTH: 46
};
Cloud.prototype = {
/**
- Initialise the cloud. Sets the Cloud height.
*/
init: function() {
this.yPos = getRandomNum(Cloud.config.MAX_SKY_LEVEL,
Cloud.config.MIN_SKY_LEVEL);
this.draw();
},
/**
- Draw the cloud.
*/
draw: function() {
this.canvasCtx.save();
var sourceWidth = Cloud.config.WIDTH;
var sourceHeight = Cloud.config.HEIGHT;
var outputWidth = sourceWidth;
var outputHeight = sourceHeight;
if (IS_HIDPI) {
sourceWidth = sourceWidth * 2;
sourceHeight = sourceHeight * 2;
}
this.canvasCtx.drawImage(Runner.imageSprite, this.spritePos.x,
this.spritePos.y,
sourceWidth, sourceHeight,
this.xPos, this.yPos,
outputWidth, outputHeight);
this.canvasCtx.restore();
},
/**
-
Update the cloud position.
-
@param {number} speed
*/
update: function(speed) {
if (!this.remove) {
this.xPos -= Math.ceil(speed);
this.draw();// Mark as removeable if no longer in the canvas.
if (!this.isVisible()) {
this.remove = true;
}
}
},
/**
- Check if the cloud is visible on the stage.
- @return {boolean}
*/
isVisible: function() {
return this.xPos + Cloud.config.WIDTH > 0;
}
};
//******************************************************************************
/**
- Nightmode shows a moon and stars on the horizon.
*/
function NightMode(canvas, spritePos, containerWidth) {
this.spritePos = spritePos;
this.canvas = canvas;
this.canvasCtx = canvas.getContext('2d');
this.xPos = containerWidth - 50;
this.yPos = 30;
this.currentPhase = 0;
this.opacity = 0;
this.containerWidth = containerWidth;
this.stars = [];
this.drawStars = false;
this.placeStars();
};
/**
- @enum {number}
*/
NightMode.config = {
FADE_SPEED: 0.035,
HEIGHT: 40,
MOON_SPEED: 0.25,
NUM_STARS: 2,
STAR_SIZE: 9,
STAR_SPEED: 0.3,
STAR_MAX_Y: 70,
WIDTH: 20
};
NightMode.phases = [140, 120, 100, 60, 40, 20, 0];
NightMode.prototype = {
/**
-
Update moving moon, changing phases.
-
@param {boolean} activated Whether night mode is activated.
-
@param {number} delta
*/
update: function(activated, delta) {
// Moon phase.
if (activated && this.opacity == 0) {
this.currentPhase++;if (this.currentPhase >= NightMode.phases.length) {
this.currentPhase = 0;
}
}
// Fade in / out.
if (activated && (this.opacity < 1 || this.opacity == 0)) {
this.opacity += NightMode.config.FADE_SPEED;
} else if (this.opacity > 0) {
this.opacity -= NightMode.config.FADE_SPEED;
}
// Set moon positioning.
if (this.opacity > 0) {
this.xPos = this.updateXPos(this.xPos, NightMode.config.MOON_SPEED);
// Update stars.
if (this.drawStars) {
for (var i = 0; i < NightMode.config.NUM_STARS; i++) {
this.stars[i].x = this.updateXPos(this.stars[i].x,
NightMode.config.STAR_SPEED);
}
}
this.draw();
} else {
this.opacity = 0;
this.placeStars();
}
this.drawStars = true;
},
updateXPos: function(currentPos, speed) {
if (currentPos < -NightMode.config.WIDTH) {
currentPos = this.containerWidth;
} else {
currentPos -= speed;
}
return currentPos;
},
draw: function() {
var moonSourceWidth = this.currentPhase == 3 ? NightMode.config.WIDTH * 2 :
NightMode.config.WIDTH;
var moonSourceHeight = NightMode.config.HEIGHT;
var moonSourceX = this.spritePos.x + NightMode.phases[this.currentPhase];
var moonOutputWidth = moonSourceWidth;
var starSize = NightMode.config.STAR_SIZE;
var starSourceX = Runner.spriteDefinition.LDPI.STAR.x;
if (IS_HIDPI) {
moonSourceWidth *= 2;
moonSourceHeight *= 2;
moonSourceX = this.spritePos.x +
(NightMode.phases[this.currentPhase] * 2);
starSize *= 2;
starSourceX = Runner.spriteDefinition.HDPI.STAR.x;
}
this.canvasCtx.save();
this.canvasCtx.globalAlpha = this.opacity;
// Stars.
if (this.drawStars) {
for (var i = 0; i < NightMode.config.NUM_STARS; i++) {
this.canvasCtx.drawImage(Runner.imageSprite,
starSourceX, this.stars[i].sourceY, starSize, starSize,
Math.round(this.stars[i].x), this.stars[i].y,
NightMode.config.STAR_SIZE, NightMode.config.STAR_SIZE);
}
}
// Moon.
this.canvasCtx.drawImage(Runner.imageSprite, moonSourceX,
this.spritePos.y, moonSourceWidth, moonSourceHeight,
Math.round(this.xPos), this.yPos,
moonOutputWidth, NightMode.config.HEIGHT);
this.canvasCtx.globalAlpha = 1;
this.canvasCtx.restore();
},
// Do star placement.
placeStars: function() {
var segmentSize = Math.round(this.containerWidth /
NightMode.config.NUM_STARS);
for (var i = 0; i < NightMode.config.NUM_STARS; i++) {
this.stars[i] = {};
this.stars[i].x = getRandomNum(segmentSize * i, segmentSize * (i + 1));
this.stars[i].y = getRandomNum(0, NightMode.config.STAR_MAX_Y);
if (IS_HIDPI) {
this.stars[i].sourceY = Runner.spriteDefinition.HDPI.STAR.y +
NightMode.config.STAR_SIZE * 2 * i;
} else {
this.stars[i].sourceY = Runner.spriteDefinition.LDPI.STAR.y +
NightMode.config.STAR_SIZE * i;
}
}
},
reset: function() {
this.currentPhase = 0;
this.opacity = 0;
this.update(false);
}
};
//******************************************************************************
/**
- Horizon Line.
- Consists of two connecting lines. Randomly assigns a flat / bumpy horizon.
- @param {HTMLCanvasElement} canvas
- @param {Object} spritePos Horizon position in sprite.
- @constructor
*/
function HorizonLine(canvas, spritePos) {
this.spritePos = spritePos;
this.canvas = canvas;
this.canvasCtx = canvas.getContext('2d');
this.sourceDimensions = {};
this.dimensions = HorizonLine.dimensions;
this.sourceXPos = [this.spritePos.x, this.spritePos.x +
this.dimensions.WIDTH];
this.xPos = [];
this.yPos = 0;
this.bumpThreshold = 0.5;
this.setSourceDimensions();
this.draw();
};
/**
- Horizon line dimensions.
- @enum {number}
*/
HorizonLine.dimensions = {
WIDTH: 600,
HEIGHT: 12,
YPOS: 127
};
HorizonLine.prototype = {
/**
- Set the source dimensions of the horizon line.
*/
setSourceDimensions: function() {
for (var dimension in HorizonLine.dimensions) {
if (IS_HIDPI) {
if (dimension != 'YPOS') {
this.sourceDimensions[dimension] =
HorizonLine.dimensions[dimension] * 2;
}
} else {
this.sourceDimensions[dimension] =
HorizonLine.dimensions[dimension];
}
this.dimensions[dimension] = HorizonLine.dimensions[dimension];
}
this.xPos = [0, HorizonLine.dimensions.WIDTH];
this.yPos = HorizonLine.dimensions.YPOS;
},
/**
- Return the crop x position of a type.
*/
getRandomType: function() {
return Math.random() > this.bumpThreshold ? this.dimensions.WIDTH : 0;
},
/**
- Draw the horizon line.
*/
draw: function() {
this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[0],
this.spritePos.y,
this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
this.xPos[0], this.yPos,
this.dimensions.WIDTH, this.dimensions.HEIGHT);
this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[1],
this.spritePos.y,
this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
this.xPos[1], this.yPos,
this.dimensions.WIDTH, this.dimensions.HEIGHT);
},
/**
- Update the x position of an indivdual piece of the line.
- @param {number} pos Line position.
- @param {number} increment
*/
updateXPos: function(pos, increment) {
var line1 = pos;
var line2 = pos == 0 ? 1 : 0;
this.xPos[line1] -= increment;
this.xPos[line2] = this.xPos[line1] + this.dimensions.WIDTH;
if (this.xPos[line1] <= -this.dimensions.WIDTH) {
this.xPos[line1] += this.dimensions.WIDTH * 2;
this.xPos[line2] = this.xPos[line1] - this.dimensions.WIDTH;
this.sourceXPos[line1] = this.getRandomType() + this.spritePos.x;
}
},
/**
- Update the horizon line.
- @param {number} deltaTime
- @param {number} speed
*/
update: function(deltaTime, speed) {
var increment = Math.floor(speed * (FPS / 1000) * deltaTime);
if (this.xPos[0] <= 0) {
this.updateXPos(0, increment);
} else {
this.updateXPos(1, increment);
}
this.draw();
},
/**
- Reset horizon to the starting position.
*/
reset: function() {
this.xPos[0] = 0;
this.xPos[1] = HorizonLine.dimensions.WIDTH;
}
};
//******************************************************************************
/**
- Horizon background class.
- @param {HTMLCanvasElement} canvas
- @param {Object} spritePos Sprite positioning.
- @param {Object} dimensions Canvas dimensions.
- @param {number} gapCoefficient
- @constructor
*/
function Horizon(canvas, spritePos, dimensions, gapCoefficient) {
this.canvas = canvas;
this.canvasCtx = this.canvas.getContext('2d');
this.config = Horizon.config;
this.dimensions = dimensions;
this.gapCoefficient = gapCoefficient;
this.obstacles = [];
this.obstacleHistory = [];
this.horizonOffsets = [0, 0];
this.cloudFrequency = this.config.CLOUD_FREQUENCY;
this.spritePos = spritePos;
this.nightMode = null;
// Cloud
this.clouds = [];
this.cloudSpeed = this.config.BG_CLOUD_SPEED;
// Horizon
this.horizonLine = null;
this.init();
};
/**
- Horizon config.
- @enum {number}
*/
Horizon.config = {
BG_CLOUD_SPEED: 0.2,
BUMPY_THRESHOLD: .3,
CLOUD_FREQUENCY: .5,
HORIZON_HEIGHT: 16,
MAX_CLOUDS: 6
};
Horizon.prototype = {
/**
- Initialise the horizon. Just add the line and a cloud. No obstacles.
*/
init: function() {
this.addCloud();
this.horizonLine = new HorizonLine(this.canvas, this.spritePos.HORIZON);
this.nightMode = new NightMode(this.canvas, this.spritePos.MOON,
this.dimensions.WIDTH);
},
/**
- @param {number} deltaTime
- @param {number} currentSpeed
- @param {boolean} updateObstacles Used as an override to prevent
-
the obstacles from being updated / added. This happens in the -
ease in section. - @param {boolean} showNightMode Night mode activated.
*/
update: function(deltaTime, currentSpeed, updateObstacles, showNightMode) {
this.runningTime += deltaTime;
this.horizonLine.update(deltaTime, currentSpeed);
this.nightMode.update(showNightMode);
this.updateClouds(deltaTime, currentSpeed);
if (updateObstacles) {
this.updateObstacles(deltaTime, currentSpeed);
}
},
/**
- Update the cloud positions.
- @param {number} deltaTime
- @param {number} currentSpeed
*/
updateClouds: function(deltaTime, speed) {
var cloudSpeed = this.cloudSpeed / 1000 * deltaTime * speed;
var numClouds = this.clouds.length;
if (numClouds) {
for (var i = numClouds - 1; i >= 0; i--) {
this.clouds[i].update(cloudSpeed);
}
var lastCloud = this.clouds[numClouds - 1];
// Check for adding a new cloud.
if (numClouds < this.config.MAX_CLOUDS &&
(this.dimensions.WIDTH - lastCloud.xPos) > lastCloud.cloudGap &&
this.cloudFrequency > Math.random()) {
this.addCloud();
}
// Remove expired clouds.
this.clouds = this.clouds.filter(function(obj) {
return !obj.remove;
});
} else {
this.addCloud();
}
},
/**
- Update the obstacle positions.
- @param {number} deltaTime
- @param {number} currentSpeed
*/
updateObstacles: function(deltaTime, currentSpeed) {
// Obstacles, move to Horizon layer.
var updatedObstacles = this.obstacles.slice(0);
for (var i = 0; i < this.obstacles.length; i++) {
var obstacle = this.obstacles[i];
obstacle.update(deltaTime, currentSpeed);
// Clean up existing obstacles.
if (obstacle.remove) {
updatedObstacles.shift();
}
}
this.obstacles = updatedObstacles;
if (this.obstacles.length > 0) {
var lastObstacle = this.obstacles[this.obstacles.length - 1];
if (lastObstacle && !lastObstacle.followingObstacleCreated &&
lastObstacle.isVisible() &&
(lastObstacle.xPos + lastObstacle.width + lastObstacle.gap) <
this.dimensions.WIDTH) {
this.addNewObstacle(currentSpeed);
lastObstacle.followingObstacleCreated = true;
}
} else {
// Create new obstacles.
this.addNewObstacle(currentSpeed);
}
},
removeFirstObstacle: function() {
this.obstacles.shift();
},
/**
- Add a new obstacle.
- @param {number} currentSpeed
*/
addNewObstacle: function(currentSpeed) {
var obstacleTypeIndex = getRandomNum(0, Obstacle.types.length - 1);
var obstacleType = Obstacle.types[obstacleTypeIndex];
// Check for multiples of the same type of obstacle.
// Also check obstacle is available at current speed.
if (this.duplicateObstacleCheck(obstacleType.type) ||
currentSpeed < obstacleType.minSpeed) {
this.addNewObstacle(currentSpeed);
} else {
var obstacleSpritePos = this.spritePos[obstacleType.type];
this.obstacles.push(new Obstacle(this.canvasCtx, obstacleType,
obstacleSpritePos, this.dimensions,
this.gapCoefficient, currentSpeed, obstacleType.width));
this.obstacleHistory.unshift(obstacleType.type);
if (this.obstacleHistory.length > 1) {
this.obstacleHistory.splice(Runner.config.MAX_OBSTACLE_DUPLICATION);
}
}
},
/**
- Returns whether the previous two obstacles are the same as the next one.
- Maximum duplication is set in config value MAX_OBSTACLE_DUPLICATION.
- @return {boolean}
*/
duplicateObstacleCheck: function(nextObstacleType) {
var duplicateCount = 0;
for (var i = 0; i < this.obstacleHistory.length; i++) {
duplicateCount = this.obstacleHistory[i] == nextObstacleType ?
duplicateCount + 1 : 0;
}
return duplicateCount >= Runner.config.MAX_OBSTACLE_DUPLICATION;
},
/**
- Reset the horizon layer.
- Remove existing obstacles and reposition the horizon line.
*/
reset: function() {
this.obstacles = [];
this.horizonLine.reset();
this.nightMode.reset();
},
/**
- Update the canvas width and scaling.
- @param {number} width Canvas width.
- @param {number} height Canvas height.
*/
resize: function(width, height) {
this.canvas.width = width;
this.canvas.height = height;
},
/**
- Add a new cloud to the horizon.
*/
addCloud: function() {
this.clouds.push(new Cloud(this.canvas, this.spritePos.CLOUD,
this.dimensions.WIDTH));
}
};
})();
@noeff53
You can't comment at this time — your comment can't be blank.
Leave a comment
No file chosen
Attach files by dragging & dropping, selecting or pasting them.
© 2021 GitHub, Inc.
Terms
Privacy
Security
Status
Docs
Contact GitHub
Pricing
API
Training
Blog
About
Skip to content
Search…
All gists
Back to GitHub
@noeff53
Please verify your email address to access all of GitHub’s features.
An email containing verification instructions was sent to [email protected].
@washingtonsoares
washingtonsoares/google_dinosaur.js
Created 2 years ago • Report abuse
0
Code
Revisions 1
Google Dinosaur Game
google_dinosaur.js
// Copyright (c) 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
(function() {
'use strict';
/**
- T-Rex runner.
- @param {string} outerContainerId Outer containing element id.
- @param {Object} opt_config
- @constructor
- @export
*/
function Runner(outerContainerId, opt_config) {
// Singleton
if (Runner.instance_) {
return Runner.instance_;
}
Runner.instance_ = this;
this.outerContainerEl = document.querySelector(outerContainerId);
this.containerEl = null;
this.snackbarEl = null;
// A div to intercept touch events. Only set while (playing && useTouch).
this.touchController = null;
this.config = opt_config || Runner.config;
// Logical dimensions of the container.
this.dimensions = Runner.defaultDimensions;
this.canvas = null;
this.canvasCtx = null;
this.tRex = null;
this.distanceMeter = null;
this.distanceRan = 0;
this.highestScore = 0;
this.syncHighestScore = false;
this.time = 0;
this.runningTime = 0;
this.msPerFrame = 1000 / FPS;
this.currentSpeed = this.config.SPEED;
this.obstacles = [];
this.activated = false; // Whether the easter egg has been activated.
this.playing = false; // Whether the game is currently in play state.
this.crashed = false;
this.paused = false;
this.inverted = false;
this.invertTimer = 0;
this.resizeTimerId_ = null;
this.playCount = 0;
// Sound FX.
this.audioBuffer = null;
this.soundFx = {};
// Global web audio context for playing sounds.
this.audioContext = null;
// Images.
this.images = {};
this.imagesLoaded = 0;
if (this.isDisabled()) {
this.setupDisabledRunner();
} else {
this.loadImages();
window['initializeEasterEggHighScore'] =
this.initializeHighScore.bind(this);
}
}
window['Runner'] = Runner;
/**
- Default game width.
- @const
*/
var DEFAULT_WIDTH = 600;
/**
- Frames per second.
- @const
*/
var FPS = 60;
/** @const */
var IS_HIDPI = window.devicePixelRatio > 1;
/** @const */
// iPads are returning "MacIntel" for iOS 13 (devices & simulators).
// Chrome on macOS also returns "MacIntel" for navigator.platform,
// but navigator.userAgent includes /Safari/.
// TODO(crbug.com/998999): Fix navigator.userAgent such that it reliably
// returns an agent string containing "CriOS".
var IS_IOS = /iPad|iPhone|iPod|MacIntel/.test(window.navigator.platform) &&
!(/Safari/.test(window.navigator.userAgent));
/** @const */
var IS_MOBILE = /Android/.test(window.navigator.userAgent) || IS_IOS;
/** @const */
var ARCADE_MODE_URL = 'chrome://dino/';
/**
- Default game configuration.
- @enum {number}
*/
Runner.config = {
ACCELERATION: 0.001,
BG_CLOUD_SPEED: 0.2,
BOTTOM_PAD: 10,
// Scroll Y threshold at which the game can be activated.
CANVAS_IN_VIEW_OFFSET: -10,
CLEAR_TIME: 3000,
CLOUD_FREQUENCY: 0.5,
GAMEOVER_CLEAR_TIME: 750,
GAP_COEFFICIENT: 0.6,
GRAVITY: 0.6,
INITIAL_JUMP_VELOCITY: 12,
INVERT_FADE_DURATION: 12000,
INVERT_DISTANCE: 700,
MAX_BLINK_COUNT: 3,
MAX_CLOUDS: 6,
MAX_OBSTACLE_LENGTH: 3,
MAX_OBSTACLE_DUPLICATION: 2,
MAX_SPEED: 13,
MIN_JUMP_HEIGHT: 35,
MOBILE_SPEED_COEFFICIENT: 1.2,
RESOURCE_TEMPLATE_ID: 'audio-resources',
SPEED: 6,
SPEED_DROP_COEFFICIENT: 3,
ARCADE_MODE_INITIAL_TOP_POSITION: 35,
ARCADE_MODE_TOP_POSITION_PERCENT: 0.1
};
/**
- Default dimensions.
- @enum {string}
*/
Runner.defaultDimensions = {
WIDTH: DEFAULT_WIDTH,
HEIGHT: 150
};
/**
- CSS class names.
- @enum {string}
*/
Runner.classes = {
ARCADE_MODE: 'arcade-mode',
CANVAS: 'runner-canvas',
CONTAINER: 'runner-container',
CRASHED: 'crashed',
ICON: 'icon-offline',
INVERTED: 'inverted',
SNACKBAR: 'snackbar',
SNACKBAR_SHOW: 'snackbar-show',
TOUCH_CONTROLLER: 'controller'
};
/**
- Sprite definition layout of the spritesheet.
- @enum {Object}
*/
Runner.spriteDefinition = {
LDPI: {
CACTUS_LARGE: {x: 332, y: 2},
CACTUS_SMALL: {x: 228, y: 2},
CLOUD: {x: 86, y: 2},
HORIZON: {x: 2, y: 54},
MOON: {x: 484, y: 2},
PTERODACTYL: {x: 134, y: 2},
RESTART: {x: 2, y: 2},
TEXT_SPRITE: {x: 655, y: 2},
TREX: {x: 848, y: 2},
STAR: {x: 645, y: 2}
},
HDPI: {
CACTUS_LARGE: {x: 652, y: 2},
CACTUS_SMALL: {x: 446, y: 2},
CLOUD: {x: 166, y: 2},
HORIZON: {x: 2, y: 104},
MOON: {x: 954, y: 2},
PTERODACTYL: {x: 260, y: 2},
RESTART: {x: 2, y: 2},
TEXT_SPRITE: {x: 1294, y: 2},
TREX: {x: 1678, y: 2},
STAR: {x: 1276, y: 2}
}
};
/**
- Sound FX. Reference to the ID of the audio tag on interstitial page.
- @enum {string}
*/
Runner.sounds = {
BUTTON_PRESS: 'offline-sound-press',
HIT: 'offline-sound-hit',
SCORE: 'offline-sound-reached'
};
/**
- Key code mapping.
- @enum {Object}
*/
Runner.keycodes = {
JUMP: {'38': 1, '32': 1}, // Up, spacebar
DUCK: {'40': 1}, // Down
RESTART: {'13': 1} // Enter
};
/**
- Runner event names.
- @enum {string}
*/
Runner.events = {
ANIM_END: 'webkitAnimationEnd',
CLICK: 'click',
KEYDOWN: 'keydown',
KEYUP: 'keyup',
POINTERDOWN: 'pointerdown',
POINTERUP: 'pointerup',
RESIZE: 'resize',
TOUCHEND: 'touchend',
TOUCHSTART: 'touchstart',
VISIBILITY: 'visibilitychange',
BLUR: 'blur',
FOCUS: 'focus',
LOAD: 'load'
};
Runner.prototype = {
/**
- Whether the easter egg has been disabled. CrOS enterprise enrolled devices.
- @return {boolean}
*/
isDisabled: function() {
return loadTimeData && loadTimeData.valueExists('disabledEasterEgg');
},
/**
- For disabled instances, set up a snackbar with the disabled message.
*/
setupDisabledRunner: function() {
this.containerEl = document.createElement('div');
this.containerEl.className = Runner.classes.SNACKBAR;
this.containerEl.textContent = loadTimeData.getValue('disabledEasterEgg');
this.outerContainerEl.appendChild(this.containerEl);
// Show notification when the activation key is pressed.
document.addEventListener(Runner.events.KEYDOWN, function(e) {
if (Runner.keycodes.JUMP[e.keyCode]) {
this.containerEl.classList.add(Runner.classes.SNACKBAR_SHOW);
document.querySelector('.icon').classList.add('icon-disabled');
}
}.bind(this));
},
/**
-
Setting individual settings for debugging.
-
@param {string} setting
-
@param {*} value
*/
updateConfigSetting: function(setting, value) {
if (setting in this.config && value != undefined) {
this.config[setting] = value;switch (setting) {
case 'GRAVITY':
case 'MIN_JUMP_HEIGHT':
case 'SPEED_DROP_COEFFICIENT':
this.tRex.config[setting] = value;
break;
case 'INITIAL_JUMP_VELOCITY':
this.tRex.setJumpVelocity(value);
break;
case 'SPEED':
this.setSpeed(value);
break;
}
}
},
/**
- Cache the appropriate image sprite from the page and get the sprite sheet
- definition.
*/
loadImages: function() {
if (IS_HIDPI) {
Runner.imageSprite = document.getElementById('offline-resources-2x');
this.spriteDef = Runner.spriteDefinition.HDPI;
} else {
Runner.imageSprite = document.getElementById('offline-resources-1x');
this.spriteDef = Runner.spriteDefinition.LDPI;
}
if (Runner.imageSprite.complete) {
this.init();
} else {
// If the images are not yet loaded, add a listener.
Runner.imageSprite.addEventListener(Runner.events.LOAD,
this.init.bind(this));
}
},
/**
-
Load and decode base 64 encoded sounds.
*/
loadSounds: function() {
if (!IS_IOS) {
this.audioContext = new AudioContext();var resourceTemplate =
document.getElementById(this.config.RESOURCE_TEMPLATE_ID).content;for (var sound in Runner.sounds) {
var soundSrc =
resourceTemplate.getElementById(Runner.sounds[sound]).src;
soundSrc = soundSrc.substr(soundSrc.indexOf(',') + 1);
var buffer = decodeBase64ToArrayBuffer(soundSrc);// Async, so no guarantee of order in array.
this.audioContext.decodeAudioData(buffer, function(index, audioData) {
this.soundFx[index] = audioData;
}.bind(this, sound));
}
}
},
/**
- Sets the game speed. Adjust the speed accordingly if on a smaller screen.
- @param {number} opt_speed
*/
setSpeed: function(opt_speed) {
var speed = opt_speed || this.currentSpeed;
// Reduce the speed on smaller mobile screens.
if (this.dimensions.WIDTH < DEFAULT_WIDTH) {
var mobileSpeed = speed * this.dimensions.WIDTH / DEFAULT_WIDTH *
this.config.MOBILE_SPEED_COEFFICIENT;
this.currentSpeed = mobileSpeed > speed ? speed : mobileSpeed;
} else if (opt_speed) {
this.currentSpeed = opt_speed;
}
},
/**
- Game initialiser.
*/
init: function() {
// Hide the static icon.
document.querySelector('.' + Runner.classes.ICON).style.visibility =
'hidden';
this.adjustDimensions();
this.setSpeed();
this.containerEl = document.createElement('div');
this.containerEl.className = Runner.classes.CONTAINER;
// Player canvas container.
this.canvas = createCanvas(this.containerEl, this.dimensions.WIDTH,
this.dimensions.HEIGHT, Runner.classes.PLAYER);
this.canvasCtx = this.canvas.getContext('2d');
this.canvasCtx.fillStyle = '#f7f7f7';
this.canvasCtx.fill();
Runner.updateCanvasScaling(this.canvas);
// Horizon contains clouds, obstacles and the ground.
this.horizon = new Horizon(this.canvas, this.spriteDef, this.dimensions,
this.config.GAP_COEFFICIENT);
// Distance meter
this.distanceMeter = new DistanceMeter(this.canvas,
this.spriteDef.TEXT_SPRITE, this.dimensions.WIDTH);
// Draw t-rex
this.tRex = new Trex(this.canvas, this.spriteDef.TREX);
this.outerContainerEl.appendChild(this.containerEl);
//
this.startListening();
this.update();
window.addEventListener(Runner.events.RESIZE,
this.debounceResize.bind(this));
},
/**
- Create the touch controller. A div that covers whole screen.
*/
createTouchController: function() {
this.touchController = document.createElement('div');
this.touchController.className = Runner.classes.TOUCH_CONTROLLER;
this.touchController.addEventListener(Runner.events.TOUCHSTART, this);
this.touchController.addEventListener(Runner.events.TOUCHEND, this);
this.outerContainerEl.appendChild(this.touchController);
},
/**
- Debounce the resize event.
*/
debounceResize: function() {
if (!this.resizeTimerId_) {
this.resizeTimerId_ =
setInterval(this.adjustDimensions.bind(this), 250);
}
},
/**
- Adjust game space dimensions on resize.
*/
adjustDimensions: function() {
clearInterval(this.resizeTimerId_);
this.resizeTimerId_ = null;
var boxStyles = window.getComputedStyle(this.outerContainerEl);
var padding = Number(boxStyles.paddingLeft.substr(0,
boxStyles.paddingLeft.length - 2));
this.dimensions.WIDTH = this.outerContainerEl.offsetWidth - padding * 2;
if (this.isArcadeMode()) {
this.dimensions.WIDTH = Math.min(DEFAULT_WIDTH, this.dimensions.WIDTH);
if (this.activated) {
this.setArcadeModeContainerScale();
}
}
// Redraw the elements back onto the canvas.
if (this.canvas) {
this.canvas.width = this.dimensions.WIDTH;
this.canvas.height = this.dimensions.HEIGHT;
Runner.updateCanvasScaling(this.canvas);
this.distanceMeter.calcXPos(this.dimensions.WIDTH);
this.clearCanvas();
this.horizon.update(0, 0, true);
this.tRex.update(0);
// Outer container and distance meter.
if (this.playing || this.crashed || this.paused) {
this.containerEl.style.width = this.dimensions.WIDTH + 'px';
this.containerEl.style.height = this.dimensions.HEIGHT + 'px';
this.distanceMeter.update(0, Math.ceil(this.distanceRan));
this.stop();
} else {
this.tRex.draw(0, 0);
}
// Game over panel.
if (this.crashed && this.gameOverPanel) {
this.gameOverPanel.updateDimensions(this.dimensions.WIDTH);
this.gameOverPanel.draw();
}
}
},
/**
- Play the game intro.
- Canvas container width expands out to the full width.
*/
playIntro: function() {
if (!this.activated && !this.crashed) {
this.playingIntro = true;
this.tRex.playingIntro = true;
//
// CSS animation definition.
var keyframes = '@-webkit-keyframes intro { ' +
'from { width:' + Trex.config.WIDTH + 'px }' +
'to { width: ' + this.dimensions.WIDTH + 'px }' +
'}';
document.styleSheets[0].insertRule(keyframes, 0);
this.containerEl.addEventListener(Runner.events.ANIM_END,
this.startGame.bind(this));
this.containerEl.style.webkitAnimation = 'intro .4s ease-out 1 both';
this.containerEl.style.width = this.dimensions.WIDTH + 'px';
this.setPlayStatus(true);
this.activated = true;
} else if (this.crashed) {
this.restart();
}
},
/**
- Update the game status to started.
*/
startGame: function() {
if (this.isArcadeMode()) {
this.setArcadeMode();
}
this.runningTime = 0;
this.playingIntro = false;
this.tRex.playingIntro = false;
this.containerEl.style.webkitAnimation = '';
this.playCount++;
// Handle tabbing off the page. Pause the current game.
document.addEventListener(Runner.events.VISIBILITY,
this.onVisibilityChange.bind(this));
window.addEventListener(Runner.events.BLUR,
this.onVisibilityChange.bind(this));
window.addEventListener(Runner.events.FOCUS,
this.onVisibilityChange.bind(this));
},
clearCanvas: function() {
this.canvasCtx.clearRect(0, 0, this.dimensions.WIDTH,
this.dimensions.HEIGHT);
},
/**
- Checks whether the canvas area is in the viewport of the browser
- through the current scroll position.
- @return boolean.
*/
isCanvasInView: function() {
return this.containerEl.getBoundingClientRect().top >
Runner.config.CANVAS_IN_VIEW_OFFSET;
},
/**
- Update the game frame and schedules the next one.
*/
update: function() {
this.updatePending = false;
var now = getTimeStamp();
var deltaTime = now - (this.time || now);
this.time = now;
if (this.playing) {
this.clearCanvas();
if (this.tRex.jumping) {
this.tRex.updateJump(deltaTime);
}
this.runningTime += deltaTime;
var hasObstacles = this.runningTime > this.config.CLEAR_TIME;
// First jump triggers the intro.
if (this.tRex.jumpCount == 1 && !this.playingIntro) {
this.playIntro();
}
// The horizon doesn't move until the intro is over.
if (this.playingIntro) {
this.horizon.update(0, this.currentSpeed, hasObstacles);
} else {
deltaTime = !this.activated ? 0 : deltaTime;
this.horizon.update(deltaTime, this.currentSpeed, hasObstacles,
this.inverted);
}
// Check for collisions.
var collision = hasObstacles &&
checkForCollision(this.horizon.obstacles[0], this.tRex);
if (!collision) {
this.distanceRan += this.currentSpeed * deltaTime / this.msPerFrame;
if (this.currentSpeed < this.config.MAX_SPEED) {
this.currentSpeed += this.config.ACCELERATION;
}
} else {
this.gameOver();
}
var playAchievementSound = this.distanceMeter.update(deltaTime,
Math.ceil(this.distanceRan));
if (playAchievementSound) {
this.playSound(this.soundFx.SCORE);
}
// Night mode.
if (this.invertTimer > this.config.INVERT_FADE_DURATION) {
this.invertTimer = 0;
this.invertTrigger = false;
this.invert();
} else if (this.invertTimer) {
this.invertTimer += deltaTime;
} else {
var actualDistance =
this.distanceMeter.getActualDistance(Math.ceil(this.distanceRan));
if (actualDistance > 0) {
this.invertTrigger = !(actualDistance %
this.config.INVERT_DISTANCE);
if (this.invertTrigger && this.invertTimer === 0) {
this.invertTimer += deltaTime;
this.invert();
}
}
}
}
if (this.playing || (!this.activated &&
this.tRex.blinkCount < Runner.config.MAX_BLINK_COUNT)) {
this.tRex.update(deltaTime);
this.scheduleNextUpdate();
}
},
/**
- Event handler.
*/
handleEvent: function(e) {
return (function(evtType, events) {
switch (evtType) {
case events.KEYDOWN:
case events.TOUCHSTART:
case events.POINTERDOWN:
this.onKeyDown(e);
break;
case events.KEYUP:
case events.TOUCHEND:
case events.POINTERUP:
this.onKeyUp(e);
break;
}
}.bind(this))(e.type, Runner.events);
},
/**
- Bind relevant key / mouse / touch listeners.
*/
startListening: function() {
// Keys.
document.addEventListener(Runner.events.KEYDOWN, this);
document.addEventListener(Runner.events.KEYUP, this);
// Touch / pointer.
this.containerEl.addEventListener(Runner.events.TOUCHSTART, this);
document.addEventListener(Runner.events.POINTERDOWN, this);
document.addEventListener(Runner.events.POINTERUP, this);
},
/**
- Remove all listeners.
*/
stopListening: function() {
document.removeEventListener(Runner.events.KEYDOWN, this);
document.removeEventListener(Runner.events.KEYUP, this);
if (this.touchController) {
this.touchController.removeEventListener(Runner.events.TOUCHSTART, this);
this.touchController.removeEventListener(Runner.events.TOUCHEND, this);
}
this.containerEl.removeEventListener(Runner.events.TOUCHSTART, this);
document.removeEventListener(Runner.events.POINTERDOWN, this);
document.removeEventListener(Runner.events.POINTERUP, this);
},
/**
- Process keydown.
- @param {Event} e
*/
onKeyDown: function(e) {
// Prevent native page scrolling whilst tapping on mobile.
if (IS_MOBILE && this.playing) {
e.preventDefault();
}
if (this.isCanvasInView()) {
if (!this.crashed && !this.paused) {
if (Runner.keycodes.JUMP[e.keyCode] ||
e.type == Runner.events.TOUCHSTART) {
e.preventDefault();
// Starting the game for the first time.
if (!this.playing) {
// Started by touch so create a touch controller.
if (!this.touchController && e.type == Runner.events.TOUCHSTART) {
this.createTouchController();
}
this.loadSounds();
this.setPlayStatus(true);
this.update();
if (window.errorPageController) {
errorPageController.trackEasterEgg();
}
}
// Start jump.
if (!this.tRex.jumping && !this.tRex.ducking) {
this.playSound(this.soundFx.BUTTON_PRESS);
this.tRex.startJump(this.currentSpeed);
}
} else if (this.playing && Runner.keycodes.DUCK[e.keyCode]) {
e.preventDefault();
if (this.tRex.jumping) {
// Speed drop, activated only when jump key is not pressed.
this.tRex.setSpeedDrop();
} else if (!this.tRex.jumping && !this.tRex.ducking) {
// Duck.
this.tRex.setDuck(true);
}
}
// iOS only triggers touchstart and no pointer events.
} else if (IS_IOS && this.crashed && e.type == Runner.events.TOUCHSTART &&
e.currentTarget == this.containerEl) {
this.handleGameOverClicks(e);
}
}
},
/**
- Process key up.
- @param {Event} e
*/
onKeyUp: function(e) {
var keyCode = String(e.keyCode);
var isjumpKey = Runner.keycodes.JUMP[keyCode] ||
e.type == Runner.events.TOUCHEND ||
e.type == Runner.events.POINTERUP;
if (this.isRunning() && isjumpKey) {
this.tRex.endJump();
} else if (Runner.keycodes.DUCK[keyCode]) {
this.tRex.speedDrop = false;
this.tRex.setDuck(false);
} else if (this.crashed) {
// Check that enough time has elapsed before allowing jump key to restart.
var deltaTime = getTimeStamp() - this.time;
if (this.isCanvasInView() &&
(Runner.keycodes.RESTART[keyCode] || this.isLeftClickOnCanvas(e) ||
(deltaTime >= this.config.GAMEOVER_CLEAR_TIME &&
Runner.keycodes.JUMP[keyCode]))) {
this.handleGameOverClicks(e);
}
} else if (this.paused && isjumpKey) {
// Reset the jump state
this.tRex.reset();
this.play();
}
},
/**
- Handle interactions on the game over screen state.
- A user is able to tap the high score twice to reset it.
- @param {Event} e
*/
handleGameOverClicks: function(e) {
e.preventDefault();
if (this.distanceMeter.hasClickedOnHighScore(e) && this.highestScore) {
if (this.distanceMeter.isHighScoreFlashing()) {
// Subsequent click, reset the high score.
this.saveHighScore(0, true);
this.distanceMeter.resetHighScore();
} else {
// First click, flash the high score.
this.distanceMeter.startHighScoreFlashing();
}
} else {
this.distanceMeter.cancelHighScoreFlashing();
this.restart();
}
},
/**
- Returns whether the event was a left click on canvas.
- On Windows right click is registered as a click.
- @param {Event} e
- @return {boolean}
*/
isLeftClickOnCanvas: function(e) {
return e.button != null && e.button < 2 &&
e.type == Runner.events.POINTERUP && e.target == this.canvas;
},
/**
- RequestAnimationFrame wrapper.
*/
scheduleNextUpdate: function() {
if (!this.updatePending) {
this.updatePending = true;
this.raqId = requestAnimationFrame(this.update.bind(this));
}
},
/**
- Whether the game is running.
- @return {boolean}
*/
isRunning: function() {
return !!this.raqId;
},
/**
- Set the initial high score as stored in the user's profile.
- @param {integer} highScore
*/
initializeHighScore: function(highScore) {
this.syncHighestScore = true;
highScore = Math.ceil(highScore);
if (highScore < this.highestScore) {
if (window.errorPageController) {
errorPageController.updateEasterEggHighScore(this.highestScore);
}
return;
}
this.highestScore = highScore;
this.distanceMeter.setHighScore(this.highestScore);
},
/**
- Sets the current high score and saves to the profile if available.
- @param {number} distanceRan Total distance ran.
- @param {boolean} opt_resetScore Whether to reset the score.
*/
saveHighScore: function(distanceRan, opt_resetScore) {
this.highestScore = Math.ceil(distanceRan);
this.distanceMeter.setHighScore(this.highestScore);
// Store the new high score in the profile.
if (this.syncHighestScore && window.errorPageController) {
if (opt_resetScore) {
errorPageController.resetEasterEggHighScore();
} else {
errorPageController.updateEasterEggHighScore(this.highestScore);
}
}
},
/**
- Game over state.
*/
gameOver: function() {
this.playSound(this.soundFx.HIT);
vibrate(200);
this.stop();
this.crashed = true;
this.distanceMeter.achievement = false;
this.tRex.update(100, Trex.status.CRASHED);
// Game over panel.
if (!this.gameOverPanel) {
this.gameOverPanel = new GameOverPanel(this.canvas,
this.spriteDef.TEXT_SPRITE, this.spriteDef.RESTART,
this.dimensions);
} else {
this.gameOverPanel.draw();
}
// Update the high score.
if (this.distanceRan > this.highestScore) {
this.saveHighScore(this.distanceRan);
}
// Reset the time clock.
this.time = getTimeStamp();
},
stop: function() {
this.setPlayStatus(false);
this.paused = true;
cancelAnimationFrame(this.raqId);
this.raqId = 0;
},
play: function() {
if (!this.crashed) {
this.setPlayStatus(true);
this.paused = false;
this.tRex.update(0, Trex.status.RUNNING);
this.time = getTimeStamp();
this.update();
}
},
restart: function() {
if (!this.raqId) {
this.playCount++;
this.runningTime = 0;
this.setPlayStatus(true);
this.paused = false;
this.crashed = false;
this.distanceRan = 0;
this.setSpeed(this.config.SPEED);
this.time = getTimeStamp();
this.containerEl.classList.remove(Runner.classes.CRASHED);
this.clearCanvas();
this.distanceMeter.reset(this.highestScore);
this.horizon.reset();
this.tRex.reset();
this.playSound(this.soundFx.BUTTON_PRESS);
this.invert(true);
this.bdayFlashTimer = null;
this.update();
}
},
setPlayStatus: function(isPlaying) {
if (this.touchController)
this.touchController.classList.toggle(HIDDEN_CLASS, !isPlaying);
this.playing = isPlaying;
},
/**
- Whether the game should go into arcade mode.
- @return {boolean}
*/
isArcadeMode: function() {
return document.title == ARCADE_MODE_URL;
},
/**
- Hides offline messaging for a fullscreen game only experience.
*/
setArcadeMode: function() {
document.body.classList.add(Runner.classes.ARCADE_MODE);
this.setArcadeModeContainerScale();
},
/**
- Sets the scaling for arcade mode.
*/
setArcadeModeContainerScale: function() {
var windowHeight = window.innerHeight;
var scaleHeight = windowHeight / this.dimensions.HEIGHT;
var scaleWidth = window.innerWidth / this.dimensions.WIDTH;
var scale = Math.max(1, Math.min(scaleHeight, scaleWidth));
var scaledCanvasHeight = this.dimensions.HEIGHT * scale;
// Positions the game container at 10% of the available vertical window
// height minus the game container height.
var translateY = Math.ceil(Math.max(0, (windowHeight - scaledCanvasHeight -
Runner.config.ARCADE_MODE_INITIAL_TOP_POSITION) *
Runner.config.ARCADE_MODE_TOP_POSITION_PERCENT)) *
window.devicePixelRatio;
//
this.containerEl.style.transform = 'scale(' + scale + ') translateY(' +
translateY + 'px)';
},
/**
- Pause the game if the tab is not in focus.
*/
onVisibilityChange: function(e) {
if (document.hidden || document.webkitHidden || e.type == 'blur' ||
document.visibilityState != 'visible') {
this.stop();
} else if (!this.crashed) {
this.tRex.reset();
this.play();
}
},
/**
- Play a sound.
- @param {SoundBuffer} soundBuffer
*/
playSound: function(soundBuffer) {
if (soundBuffer) {
var sourceNode = this.audioContext.createBufferSource();
sourceNode.buffer = soundBuffer;
sourceNode.connect(this.audioContext.destination);
sourceNode.start(0);
}
},
/**
- Inverts the current page / canvas colors.
- @param {boolean} Whether to reset colors.
*/
invert: function(reset) {
let htmlEl = document.firstElementChild;
if (reset) {
htmlEl.classList.toggle(Runner.classes.INVERTED,
false);
this.invertTimer = 0;
this.inverted = false;
} else {
this.inverted = htmlEl.classList.toggle(
Runner.classes.INVERTED, this.invertTrigger);
}
}
};
/**
- Updates the canvas size taking into
- account the backing store pixel ratio and
- the device pixel ratio.
- See article by Paul Lewis:
- http://www.html5rocks.com/en/tutorials/canvas/hidpi/
- @param {HTMLCanvasElement} canvas
- @param {number} opt_width
- @param {number} opt_height
- @return {boolean} Whether the canvas was scaled.
*/
Runner.updateCanvasScaling = function(canvas, opt_width, opt_height) {
var context = canvas.getContext('2d');
// Query the various pixel ratios
var devicePixelRatio = Math.floor(window.devicePixelRatio) || 1;
var backingStoreRatio = Math.floor(context.webkitBackingStorePixelRatio) || 1;
var ratio = devicePixelRatio / backingStoreRatio;
// Upscale the canvas if the two ratios don't match
if (devicePixelRatio !== backingStoreRatio) {
var oldWidth = opt_width || canvas.width;
var oldHeight = opt_height || canvas.height;
canvas.width = oldWidth * ratio;
canvas.height = oldHeight * ratio;
canvas.style.width = oldWidth + 'px';
canvas.style.height = oldHeight + 'px';
// Scale the context to counter the fact that we've manually scaled
// our canvas element.
context.scale(ratio, ratio);
return true;
} else if (devicePixelRatio == 1) {
// Reset the canvas width / height. Fixes scaling bug when the page is
// zoomed and the devicePixelRatio changes accordingly.
canvas.style.width = canvas.width + 'px';
canvas.style.height = canvas.height + 'px';
}
return false;
};
/**
- Get random number.
- @param {number} min
- @param {number} max
- @param {number}
*/
function getRandomNum(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
/**
- Vibrate on mobile devices.
- @param {number} duration Duration of the vibration in milliseconds.
*/
function vibrate(duration) {
if (IS_MOBILE && window.navigator.vibrate) {
window.navigator.vibrate(duration);
}
}
/**
- Create canvas element.
- @param {HTMLElement} container Element to append canvas to.
- @param {number} width
- @param {number} height
- @param {string} opt_classname
- @return {HTMLCanvasElement}
*/
function createCanvas(container, width, height, opt_classname) {
var canvas = document.createElement('canvas');
canvas.className = opt_classname ? Runner.classes.CANVAS + ' ' +
opt_classname : Runner.classes.CANVAS;
canvas.width = width;
canvas.height = height;
container.appendChild(canvas);
return canvas;
}
/**
- Decodes the base 64 audio to ArrayBuffer used by Web Audio.
- @param {string} base64String
*/
function decodeBase64ToArrayBuffer(base64String) {
var len = (base64String.length / 4) * 3;
var str = atob(base64String);
var arrayBuffer = new ArrayBuffer(len);
var bytes = new Uint8Array(arrayBuffer);
for (var i = 0; i < len; i++) {
bytes[i] = str.charCodeAt(i);
}
return bytes.buffer;
}
/**
- Return the current timestamp.
- @return {number}
*/
function getTimeStamp() {
return IS_IOS ? new Date().getTime() : performance.now();
}
//******************************************************************************
/**
- Game over panel.
- @param {!HTMLCanvasElement} canvas
- @param {Object} textImgPos
- @param {Object} restartImgPos
- @param {!Object} dimensions Canvas dimensions.
- @constructor
*/
function GameOverPanel(canvas, textImgPos, restartImgPos, dimensions) {
this.canvas = canvas;
this.canvasCtx = canvas.getContext('2d');
this.canvasDimensions = dimensions;
this.textImgPos = textImgPos;
this.restartImgPos = restartImgPos;
this.draw();
};
/**
- Dimensions used in the panel.
- @enum {number}
*/
GameOverPanel.dimensions = {
TEXT_X: 0,
TEXT_Y: 13,
TEXT_WIDTH: 191,
TEXT_HEIGHT: 11,
RESTART_WIDTH: 36,
RESTART_HEIGHT: 32
};
GameOverPanel.prototype = {
/**
- Update the panel dimensions.
- @param {number} width New canvas width.
- @param {number} opt_height Optional new canvas height.
*/
updateDimensions: function(width, opt_height) {
this.canvasDimensions.WIDTH = width;
if (opt_height) {
this.canvasDimensions.HEIGHT = opt_height;
}
},
/**
- Draw the panel.
*/
draw: function() {
var dimensions = GameOverPanel.dimensions;
var centerX = this.canvasDimensions.WIDTH / 2;
// Game over text.
var textSourceX = dimensions.TEXT_X;
var textSourceY = dimensions.TEXT_Y;
var textSourceWidth = dimensions.TEXT_WIDTH;
var textSourceHeight = dimensions.TEXT_HEIGHT;
var textTargetX = Math.round(centerX - (dimensions.TEXT_WIDTH / 2));
var textTargetY = Math.round((this.canvasDimensions.HEIGHT - 25) / 3);
var textTargetWidth = dimensions.TEXT_WIDTH;
var textTargetHeight = dimensions.TEXT_HEIGHT;
var restartSourceWidth = dimensions.RESTART_WIDTH;
var restartSourceHeight = dimensions.RESTART_HEIGHT;
var restartTargetX = centerX - (dimensions.RESTART_WIDTH / 2);
var restartTargetY = this.canvasDimensions.HEIGHT / 2;
if (IS_HIDPI) {
textSourceY *= 2;
textSourceX *= 2;
textSourceWidth *= 2;
textSourceHeight *= 2;
restartSourceWidth *= 2;
restartSourceHeight *= 2;
}
textSourceX += this.textImgPos.x;
textSourceY += this.textImgPos.y;
// Game over text from sprite.
this.canvasCtx.drawImage(Runner.imageSprite,
textSourceX, textSourceY, textSourceWidth, textSourceHeight,
textTargetX, textTargetY, textTargetWidth, textTargetHeight);
// Restart button.
this.canvasCtx.drawImage(Runner.imageSprite,
this.restartImgPos.x, this.restartImgPos.y,
restartSourceWidth, restartSourceHeight,
restartTargetX, restartTargetY, dimensions.RESTART_WIDTH,
dimensions.RESTART_HEIGHT);
}
};
//******************************************************************************
/**
- Check for a collision.
- @param {!Obstacle} obstacle
- @param {!Trex} tRex T-rex object.
- @param {HTMLCanvasContext} opt_canvasCtx Optional canvas context for drawing
- collision boxes.
- @return {Array}
*/
function checkForCollision(obstacle, tRex, opt_canvasCtx) {
var obstacleBoxXPos = Runner.defaultDimensions.WIDTH + obstacle.xPos;
// Adjustments are made to the bounding box as there is a 1 pixel white
// border around the t-rex and obstacles.
var tRexBox = new CollisionBox(
tRex.xPos + 1,
tRex.yPos + 1,
tRex.config.WIDTH - 2,
tRex.config.HEIGHT - 2);
var obstacleBox = new CollisionBox(
obstacle.xPos + 1,
obstacle.yPos + 1,
obstacle.typeConfig.width * obstacle.size - 2,
obstacle.typeConfig.height - 2);
// Debug outer box
if (opt_canvasCtx) {
drawCollisionBoxes(opt_canvasCtx, tRexBox, obstacleBox);
}
// Simple outer bounds check.
if (boxCompare(tRexBox, obstacleBox)) {
var collisionBoxes = obstacle.collisionBoxes;
var tRexCollisionBoxes = tRex.ducking ?
Trex.collisionBoxes.DUCKING : Trex.collisionBoxes.RUNNING;
// Detailed axis aligned box check.
for (var t = 0; t < tRexCollisionBoxes.length; t++) {
for (var i = 0; i < collisionBoxes.length; i++) {
// Adjust the box to actual positions.
var adjTrexBox =
createAdjustedCollisionBox(tRexCollisionBoxes[t], tRexBox);
var adjObstacleBox =
createAdjustedCollisionBox(collisionBoxes[i], obstacleBox);
var crashed = boxCompare(adjTrexBox, adjObstacleBox);
// Draw boxes for debug.
if (opt_canvasCtx) {
drawCollisionBoxes(opt_canvasCtx, adjTrexBox, adjObstacleBox);
}
if (crashed) {
return [adjTrexBox, adjObstacleBox];
}
}
}
}
return false;
};
/**
- Adjust the collision box.
- @param {!CollisionBox} box The original box.
- @param {!CollisionBox} adjustment Adjustment box.
- @return {CollisionBox} The adjusted collision box object.
*/
function createAdjustedCollisionBox(box, adjustment) {
return new CollisionBox(
box.x + adjustment.x,
box.y + adjustment.y,
box.width,
box.height);
};
/**
- Draw the collision boxes for debug.
*/
function drawCollisionBoxes(canvasCtx, tRexBox, obstacleBox) {
canvasCtx.save();
canvasCtx.strokeStyle = '#f00';
canvasCtx.strokeRect(tRexBox.x, tRexBox.y, tRexBox.width, tRexBox.height);
canvasCtx.strokeStyle = '#0f0';
canvasCtx.strokeRect(obstacleBox.x, obstacleBox.y,
obstacleBox.width, obstacleBox.height);
canvasCtx.restore();
};
/**
- Compare two collision boxes for a collision.
- @param {CollisionBox} tRexBox
- @param {CollisionBox} obstacleBox
- @return {boolean} Whether the boxes intersected.
*/
function boxCompare(tRexBox, obstacleBox) {
var crashed = false;
var tRexBoxX = tRexBox.x;
var tRexBoxY = tRexBox.y;
var obstacleBoxX = obstacleBox.x;
var obstacleBoxY = obstacleBox.y;
// Axis-Aligned Bounding Box method.
if (tRexBox.x < obstacleBoxX + obstacleBox.width &&
tRexBox.x + tRexBox.width > obstacleBoxX &&
tRexBox.y < obstacleBox.y + obstacleBox.height &&
tRexBox.height + tRexBox.y > obstacleBox.y) {
crashed = true;
}
return crashed;
};
//******************************************************************************
/**
- Collision box object.
- @param {number} x X position.
- @param {number} y Y Position.
- @param {number} w Width.
- @param {number} h Height.
*/
function CollisionBox(x, y, w, h) {
this.x = x;
this.y = y;
this.width = w;
this.height = h;
};
//******************************************************************************
/**
- Obstacle.
- @param {HTMLCanvasCtx} canvasCtx
- @param {Obstacle.type} type
- @param {Object} spritePos Obstacle position in sprite.
- @param {Object} dimensions
- @param {number} gapCoefficient Mutipler in determining the gap.
- @param {number} speed
- @param {number} opt_xOffset
*/
function Obstacle(canvasCtx, type, spriteImgPos, dimensions,
gapCoefficient, speed, opt_xOffset) {
this.canvasCtx = canvasCtx;
this.spritePos = spriteImgPos;
this.typeConfig = type;
this.gapCoefficient = gapCoefficient;
this.size = getRandomNum(1, Obstacle.MAX_OBSTACLE_LENGTH);
this.dimensions = dimensions;
this.remove = false;
this.xPos = dimensions.WIDTH + (opt_xOffset || 0);
this.yPos = 0;
this.width = 0;
this.collisionBoxes = [];
this.gap = 0;
this.speedOffset = 0;
// For animated obstacles.
this.currentFrame = 0;
this.timer = 0;
this.init(speed);
};
/**
- Coefficient for calculating the maximum gap.
- @const
*/
Obstacle.MAX_GAP_COEFFICIENT = 1.5;
/**
- Maximum obstacle grouping count.
- @const
*/
Obstacle.MAX_OBSTACLE_LENGTH = 3,
Obstacle.prototype = {
/**
- Initialise the DOM for the obstacle.
- @param {number} speed
*/
init: function(speed) {
this.cloneCollisionBoxes();
// Only allow sizing if we're at the right speed.
if (this.size > 1 && this.typeConfig.multipleSpeed > speed) {
this.size = 1;
}
this.width = this.typeConfig.width * this.size;
// Check if obstacle can be positioned at various heights.
if (Array.isArray(this.typeConfig.yPos)) {
var yPosConfig = IS_MOBILE ? this.typeConfig.yPosMobile :
this.typeConfig.yPos;
this.yPos = yPosConfig[getRandomNum(0, yPosConfig.length - 1)];
} else {
this.yPos = this.typeConfig.yPos;
}
this.draw();
// Make collision box adjustments,
// Central box is adjusted to the size as one box.
// ____ ______ ________
// _| |-| _| |-| _| |-|
// | |<->| | | |<--->| | | |<----->| |
// | | 1 | | | | 2 | | | | 3 | |
// |_|___|_| |_|_____|_| |_|_______|_|
//
if (this.size > 1) {
this.collisionBoxes[1].width = this.width - this.collisionBoxes[0].width -
this.collisionBoxes[2].width;
this.collisionBoxes[2].x = this.width - this.collisionBoxes[2].width;
}
// For obstacles that go at a different speed from the horizon.
if (this.typeConfig.speedOffset) {
this.speedOffset = Math.random() > 0.5 ? this.typeConfig.speedOffset :
-this.typeConfig.speedOffset;
}
this.gap = this.getGap(this.gapCoefficient, speed);
},
/**
- Draw and crop based on size.
*/
draw: function() {
var sourceWidth = this.typeConfig.width;
var sourceHeight = this.typeConfig.height;
if (IS_HIDPI) {
sourceWidth = sourceWidth * 2;
sourceHeight = sourceHeight * 2;
}
// X position in sprite.
var sourceX = (sourceWidth * this.size) * (0.5 * (this.size - 1)) +
this.spritePos.x;
// Animation frames.
if (this.currentFrame > 0) {
sourceX += sourceWidth * this.currentFrame;
}
this.canvasCtx.drawImage(Runner.imageSprite,
sourceX, this.spritePos.y,
sourceWidth * this.size, sourceHeight,
this.xPos, this.yPos,
this.typeConfig.width * this.size, this.typeConfig.height);
},
/**
-
Obstacle frame update.
-
@param {number} deltaTime
-
@param {number} speed
*/
update: function(deltaTime, speed) {
if (!this.remove) {
if (this.typeConfig.speedOffset) {
speed += this.speedOffset;
}
this.xPos -= Math.floor((speed * FPS / 1000) * deltaTime);// Update frame
if (this.typeConfig.numFrames) {
this.timer += deltaTime;
if (this.timer >= this.typeConfig.frameRate) {
this.currentFrame =
this.currentFrame == this.typeConfig.numFrames - 1 ?
0 : this.currentFrame + 1;
this.timer = 0;
}
}
this.draw();if (!this.isVisible()) {
this.remove = true;
}
}
},
/**
- Calculate a random gap size.
-
- Minimum gap gets wider as speed increses
- @param {number} gapCoefficient
- @param {number} speed
- @return {number} The gap size.
*/
getGap: function(gapCoefficient, speed) {
var minGap = Math.round(this.width * speed +
this.typeConfig.minGap * gapCoefficient);
var maxGap = Math.round(minGap * Obstacle.MAX_GAP_COEFFICIENT);
return getRandomNum(minGap, maxGap);
},
/**
- Check if obstacle is visible.
- @return {boolean} Whether the obstacle is in the game area.
*/
isVisible: function() {
return this.xPos + this.width > 0;
},
/**
- Make a copy of the collision boxes, since these will change based on
- obstacle type and size.
*/
cloneCollisionBoxes: function() {
var collisionBoxes = this.typeConfig.collisionBoxes;
for (var i = collisionBoxes.length - 1; i >= 0; i--) {
this.collisionBoxes[i] = new CollisionBox(collisionBoxes[i].x,
collisionBoxes[i].y, collisionBoxes[i].width,
collisionBoxes[i].height);
}
}
};
/**
- Obstacle definitions.
- minGap: minimum pixel space betweeen obstacles.
- multipleSpeed: Speed at which multiples are allowed.
- speedOffset: speed faster / slower than the horizon.
- minSpeed: Minimum speed which the obstacle can make an appearance.
*/
Obstacle.types = [
{
type: 'CACTUS_SMALL',
width: 17,
height: 35,
yPos: 105,
multipleSpeed: 4,
minGap: 120,
minSpeed: 0,
collisionBoxes: [
new CollisionBox(0, 7, 5, 27),
new CollisionBox(4, 0, 6, 34),
new CollisionBox(10, 4, 7, 14)
]
},
{
type: 'CACTUS_LARGE',
width: 25,
height: 50,
yPos: 90,
multipleSpeed: 7,
minGap: 120,
minSpeed: 0,
collisionBoxes: [
new CollisionBox(0, 12, 7, 38),
new CollisionBox(8, 0, 7, 49),
new CollisionBox(13, 10, 10, 38)
]
},
{
type: 'PTERODACTYL',
width: 46,
height: 40,
yPos: [ 100, 75, 50 ], // Variable height.
yPosMobile: [ 100, 50 ], // Variable height mobile.
multipleSpeed: 999,
minSpeed: 8.5,
minGap: 150,
collisionBoxes: [
new CollisionBox(15, 15, 16, 5),
new CollisionBox(18, 21, 24, 6),
new CollisionBox(2, 14, 4, 3),
new CollisionBox(6, 10, 4, 7),
new CollisionBox(10, 8, 6, 9)
],
numFrames: 2,
frameRate: 1000/6,
speedOffset: .8
}
];
//******************************************************************************
/**
- T-rex game character.
- @param {HTMLCanvas} canvas
- @param {Object} spritePos Positioning within image sprite.
- @constructor
*/
function Trex(canvas, spritePos) {
this.canvas = canvas;
this.canvasCtx = canvas.getContext('2d');
this.spritePos = spritePos;
this.xPos = 0;
this.yPos = 0;
// Position when on the ground.
this.groundYPos = 0;
this.currentFrame = 0;
this.currentAnimFrames = [];
this.blinkDelay = 0;
this.blinkCount = 0;
this.animStartTime = 0;
this.timer = 0;
this.msPerFrame = 1000 / FPS;
this.config = Trex.config;
// Current status.
this.status = Trex.status.WAITING;
this.jumping = false;
this.ducking = false;
this.jumpVelocity = 0;
this.reachedMinHeight = false;
this.speedDrop = false;
this.jumpCount = 0;
this.jumpspotX = 0;
this.init();
};
/**
- T-rex player config.
- @enum {number}
*/
Trex.config = {
DROP_VELOCITY: -5,
GRAVITY: 0.6,
HEIGHT: 47,
HEIGHT_DUCK: 25,
INIITAL_JUMP_VELOCITY: -10,
INTRO_DURATION: 1500,
MAX_JUMP_HEIGHT: 30,
MIN_JUMP_HEIGHT: 30,
SPEED_DROP_COEFFICIENT: 3,
SPRITE_WIDTH: 262,
START_X_POS: 50,
WIDTH: 44,
WIDTH_DUCK: 59
};
/**
- Used in collision detection.
- @type {Array}
*/
Trex.collisionBoxes = {
DUCKING: [
new CollisionBox(1, 18, 55, 25)
],
RUNNING: [
new CollisionBox(22, 0, 17, 16),
new CollisionBox(1, 18, 30, 9),
new CollisionBox(10, 35, 14, 8),
new CollisionBox(1, 24, 29, 5),
new CollisionBox(5, 30, 21, 4),
new CollisionBox(9, 34, 15, 4)
]
};
/**
- Animation states.
- @enum {string}
*/
Trex.status = {
CRASHED: 'CRASHED',
DUCKING: 'DUCKING',
JUMPING: 'JUMPING',
RUNNING: 'RUNNING',
WAITING: 'WAITING'
};
/**
- Blinking coefficient.
- @const
*/
Trex.BLINK_TIMING = 7000;
/**
- Animation config for different states.
- @enum {Object}
*/
Trex.animFrames = {
WAITING: {
frames: [44, 0],
msPerFrame: 1000 / 3
},
RUNNING: {
frames: [88, 132],
msPerFrame: 1000 / 12
},
CRASHED: {
frames: [220],
msPerFrame: 1000 / 60
},
JUMPING: {
frames: [0],
msPerFrame: 1000 / 60
},
DUCKING: {
frames: [264, 323],
msPerFrame: 1000 / 8
}
};
Trex.prototype = {
/**
- T-rex player initaliser.
- Sets the t-rex to blink at random intervals.
*/
init: function() {
this.groundYPos = Runner.defaultDimensions.HEIGHT - this.config.HEIGHT -
Runner.config.BOTTOM_PAD;
this.yPos = this.groundYPos;
this.minJumpHeight = this.groundYPos - this.config.MIN_JUMP_HEIGHT;
this.draw(0, 0);
this.update(0, Trex.status.WAITING);
},
/**
- Setter for the jump velocity.
- The approriate drop velocity is also set.
*/
setJumpVelocity: function(setting) {
this.config.INIITAL_JUMP_VELOCITY = -setting;
this.config.DROP_VELOCITY = -setting / 2;
},
/**
- Set the animation status.
- @param {!number} deltaTime
- @param {Trex.status} status Optional status to switch to.
*/
update: function(deltaTime, opt_status) {
this.timer += deltaTime;
// Update the status.
if (opt_status) {
this.status = opt_status;
this.currentFrame = 0;
this.msPerFrame = Trex.animFrames[opt_status].msPerFrame;
this.currentAnimFrames = Trex.animFrames[opt_status].frames;
if (opt_status == Trex.status.WAITING) {
this.animStartTime = getTimeStamp();
this.setBlinkDelay();
}
}
// Game intro animation, T-rex moves in from the left.
if (this.playingIntro && this.xPos < this.config.START_X_POS) {
this.xPos += Math.round((this.config.START_X_POS /
this.config.INTRO_DURATION) * deltaTime);
}
if (this.status == Trex.status.WAITING) {
this.blink(getTimeStamp());
} else {
this.draw(this.currentAnimFrames[this.currentFrame], 0);
}
// Update the frame position.
if (this.timer >= this.msPerFrame) {
this.currentFrame = this.currentFrame ==
this.currentAnimFrames.length - 1 ? 0 : this.currentFrame + 1;
this.timer = 0;
}
// Speed drop becomes duck if the down key is still being pressed.
if (this.speedDrop && this.yPos == this.groundYPos) {
this.speedDrop = false;
this.setDuck(true);
}
},
/**
- Draw the t-rex to a particular position.
- @param {number} x
- @param {number} y
*/
draw: function(x, y) {
var sourceX = x;
var sourceY = y;
var sourceWidth = this.ducking && this.status != Trex.status.CRASHED ?
this.config.WIDTH_DUCK : this.config.WIDTH;
var sourceHeight = this.config.HEIGHT;
var outputHeight = sourceHeight;
if (IS_HIDPI) {
sourceX *= 2;
sourceY *= 2;
sourceWidth *= 2;
sourceHeight *= 2;
}
// Adjustments for sprite sheet position.
sourceX += this.spritePos.x;
sourceY += this.spritePos.y;
// Ducking.
if (this.ducking && this.status != Trex.status.CRASHED) {
this.canvasCtx.drawImage(Runner.imageSprite, sourceX, sourceY,
sourceWidth, sourceHeight,
this.xPos, this.yPos,
this.config.WIDTH_DUCK, outputHeight);
} else {
// Crashed whilst ducking. Trex is standing up so needs adjustment.
if (this.ducking && this.status == Trex.status.CRASHED) {
this.xPos++;
}
// Standing / running
this.canvasCtx.drawImage(Runner.imageSprite, sourceX, sourceY,
sourceWidth, sourceHeight,
this.xPos, this.yPos,
this.config.WIDTH, outputHeight);
}
this.canvasCtx.globalAlpha = 1;
},
/**
- Sets a random time for the blink to happen.
*/
setBlinkDelay: function() {
this.blinkDelay = Math.ceil(Math.random() * Trex.BLINK_TIMING);
},
/**
- Make t-rex blink at random intervals.
- @param {number} time Current time in milliseconds.
*/
blink: function(time) {
var deltaTime = time - this.animStartTime;
if (deltaTime >= this.blinkDelay) {
this.draw(this.currentAnimFrames[this.currentFrame], 0);
if (this.currentFrame == 1) {
// Set new random delay to blink.
this.setBlinkDelay();
this.animStartTime = time;
this.blinkCount++;
}
}
},
/**
- Initialise a jump.
- @param {number} speed
*/
startJump: function(speed) {
if (!this.jumping) {
this.update(0, Trex.status.JUMPING);
// Tweak the jump velocity based on the speed.
this.jumpVelocity = this.config.INIITAL_JUMP_VELOCITY - (speed / 10);
this.jumping = true;
this.reachedMinHeight = false;
this.speedDrop = false;
}
},
/**
- Jump is complete, falling down.
*/
endJump: function() {
if (this.reachedMinHeight &&
this.jumpVelocity < this.config.DROP_VELOCITY) {
this.jumpVelocity = this.config.DROP_VELOCITY;
}
},
/**
- Update frame for a jump.
- @param {number} deltaTime
- @param {number} speed
*/
updateJump: function(deltaTime, speed) {
var msPerFrame = Trex.animFrames[this.status].msPerFrame;
var framesElapsed = deltaTime / msPerFrame;
// Speed drop makes Trex fall faster.
if (this.speedDrop) {
this.yPos += Math.round(this.jumpVelocity *
this.config.SPEED_DROP_COEFFICIENT * framesElapsed);
} else {
this.yPos += Math.round(this.jumpVelocity * framesElapsed);
}
this.jumpVelocity += this.config.GRAVITY * framesElapsed;
// Minimum height has been reached.
if (this.yPos < this.minJumpHeight || this.speedDrop) {
this.reachedMinHeight = true;
}
// Reached max height
if (this.yPos < this.config.MAX_JUMP_HEIGHT || this.speedDrop) {
this.endJump();
}
// Back down at ground level. Jump completed.
if (this.yPos > this.groundYPos) {
this.reset();
this.jumpCount++;
}
},
/**
- Set the speed drop. Immediately cancels the current jump.
*/
setSpeedDrop: function() {
this.speedDrop = true;
this.jumpVelocity = 1;
},
/**
- @param {boolean} isDucking.
*/
setDuck: function(isDucking) {
if (isDucking && this.status != Trex.status.DUCKING) {
this.update(0, Trex.status.DUCKING);
this.ducking = true;
} else if (this.status == Trex.status.DUCKING) {
this.update(0, Trex.status.RUNNING);
this.ducking = false;
}
},
/**
- Reset the t-rex to running at start of game.
*/
reset: function() {
this.yPos = this.groundYPos;
this.jumpVelocity = 0;
this.jumping = false;
this.ducking = false;
this.update(0, Trex.status.RUNNING);
this.midair = false;
this.speedDrop = false;
this.jumpCount = 0;
}
};
//******************************************************************************
/**
- Handles displaying the distance meter.
- @param {!HTMLCanvasElement} canvas
- @param {Object} spritePos Image position in sprite.
- @param {number} canvasWidth
- @constructor
*/
function DistanceMeter(canvas, spritePos, canvasWidth) {
this.canvas = canvas;
this.canvasCtx = canvas.getContext('2d');
this.image = Runner.imageSprite;
this.spritePos = spritePos;
this.x = 0;
this.y = 5;
this.currentDistance = 0;
this.maxScore = 0;
this.highScore = 0;
this.container = null;
this.digits = [];
this.achievement = false;
this.defaultString = '';
this.flashTimer = 0;
this.flashIterations = 0;
this.invertTrigger = false;
this.flashingRafId = null;
this.highScoreBounds = {};
this.highScoreFlashing = false;
this.config = DistanceMeter.config;
this.maxScoreUnits = this.config.MAX_DISTANCE_UNITS;
this.init(canvasWidth);
};
/**
- @enum {number}
*/
DistanceMeter.dimensions = {
WIDTH: 10,
HEIGHT: 13,
DEST_WIDTH: 11
};
/**
- Y positioning of the digits in the sprite sheet.
- X position is always 0.
- @type {Array}
*/
DistanceMeter.yPos = [0, 13, 27, 40, 53, 67, 80, 93, 107, 120];
/**
- Distance meter config.
- @enum {number}
*/
DistanceMeter.config = {
// Number of digits.
MAX_DISTANCE_UNITS: 5,
// Distance that causes achievement animation.
ACHIEVEMENT_DISTANCE: 100,
// Used for conversion from pixel distance to a scaled unit.
COEFFICIENT: 0.025,
// Flash duration in milliseconds.
FLASH_DURATION: 1000 / 4,
// Flash iterations for achievement animation.
FLASH_ITERATIONS: 3,
// Padding around the high score hit area.
HIGH_SCORE_HIT_AREA_PADDING: 4
};
DistanceMeter.prototype = {
/**
- Initialise the distance meter to '00000'.
- @param {number} width Canvas width in px.
*/
init: function(width) {
var maxDistanceStr = '';
this.calcXPos(width);
this.maxScore = this.maxScoreUnits;
for (var i = 0; i < this.maxScoreUnits; i++) {
this.draw(i, 0);
this.defaultString += '0';
maxDistanceStr += '9';
}
this.maxScore = parseInt(maxDistanceStr);
},
/**
- Calculate the xPos in the canvas.
- @param {number} canvasWidth
*/
calcXPos: function(canvasWidth) {
this.x = canvasWidth - (DistanceMeter.dimensions.DEST_WIDTH *
(this.maxScoreUnits + 1));
},
/**
- Draw a digit to canvas.
- @param {number} digitPos Position of the digit.
- @param {number} value Digit value 0-9.
- @param {boolean} opt_highScore Whether drawing the high score.
*/
draw: function(digitPos, value, opt_highScore) {
var sourceWidth = DistanceMeter.dimensions.WIDTH;
var sourceHeight = DistanceMeter.dimensions.HEIGHT;
var sourceX = DistanceMeter.dimensions.WIDTH * value;
var sourceY = 0;
var targetX = digitPos * DistanceMeter.dimensions.DEST_WIDTH;
var targetY = this.y;
var targetWidth = DistanceMeter.dimensions.WIDTH;
var targetHeight = DistanceMeter.dimensions.HEIGHT;
// For high DPI we 2x source values.
if (IS_HIDPI) {
sourceWidth *= 2;
sourceHeight *= 2;
sourceX *= 2;
}
sourceX += this.spritePos.x;
sourceY += this.spritePos.y;
this.canvasCtx.save();
if (opt_highScore) {
// Left of the current score.
var highScoreX = this.x - (this.maxScoreUnits * 2) *
DistanceMeter.dimensions.WIDTH;
this.canvasCtx.translate(highScoreX, this.y);
} else {
this.canvasCtx.translate(this.x, this.y);
}
this.canvasCtx.drawImage(this.image, sourceX, sourceY,
sourceWidth, sourceHeight,
targetX, targetY,
targetWidth, targetHeight
);
this.canvasCtx.restore();
},
/**
- Covert pixel distance to a 'real' distance.
- @param {number} distance Pixel distance ran.
- @return {number} The 'real' distance ran.
*/
getActualDistance: function(distance) {
return distance ? Math.round(distance * this.config.COEFFICIENT) : 0;
},
/**
- Update the distance meter.
- @param {number} distance
- @param {number} deltaTime
- @return {boolean} Whether the acheivement sound fx should be played.
*/
update: function(deltaTime, distance) {
var paint = true;
var playSound = false;
if (!this.achievement) {
distance = this.getActualDistance(distance);
// Score has gone beyond the initial digit count.
if (distance > this.maxScore && this.maxScoreUnits ==
this.config.MAX_DISTANCE_UNITS) {
this.maxScoreUnits++;
this.maxScore = parseInt(this.maxScore + '9');
} else {
this.distance = 0;
}
if (distance > 0) {
// Acheivement unlocked
if (distance % this.config.ACHIEVEMENT_DISTANCE == 0) {
// Flash score and play sound.
this.achievement = true;
this.flashTimer = 0;
playSound = true;
}
// Create a string representation of the distance with leading 0.
var distanceStr = (this.defaultString +
distance).substr(-this.maxScoreUnits);
this.digits = distanceStr.split('');
} else {
this.digits = this.defaultString.split('');
}
} else {
// Control flashing of the score on reaching acheivement.
if (this.flashIterations <= this.config.FLASH_ITERATIONS) {
this.flashTimer += deltaTime;
if (this.flashTimer < this.config.FLASH_DURATION) {
paint = false;
} else if (this.flashTimer >
this.config.FLASH_DURATION * 2) {
this.flashTimer = 0;
this.flashIterations++;
}
} else {
this.achievement = false;
this.flashIterations = 0;
this.flashTimer = 0;
}
}
// Draw the digits if not flashing.
if (paint) {
for (var i = this.digits.length - 1; i >= 0; i--) {
this.draw(i, parseInt(this.digits[i]));
}
}
this.drawHighScore();
return playSound;
},
/**
- Draw the high score.
*/
drawHighScore: function() {
this.canvasCtx.save();
this.canvasCtx.globalAlpha = .8;
for (var i = this.highScore.length - 1; i >= 0; i--) {
this.draw(i, parseInt(this.highScore[i], 10), true);
}
this.canvasCtx.restore();
},
/**
- Set the highscore as a array string.
- Position of char in the sprite: H - 10, I - 11.
- @param {number} distance Distance ran in pixels.
*/
setHighScore: function(distance) {
distance = this.getActualDistance(distance);
var highScoreStr = (this.defaultString +
distance).substr(-this.maxScoreUnits);
this.highScore = ['10', '11', ''].concat(highScoreStr.split(''));
},
/**
- Whether a clicked is in the high score area.
- @param {TouchEvent|ClickEvent} e Event object.
- @return {boolean} Whether the click was in the high score bounds.
*/
hasClickedOnHighScore: function(e) {
var x = 0;
var y = 0;
if (e.touches) {
// Bounds for touch differ from pointer.
var canvasBounds = this.canvas.getBoundingClientRect();
x = e.touches[0].clientX - canvasBounds.left;
y = e.touches[0].clientY - canvasBounds.top;
} else {
x = e.offsetX;
y = e.offsetY;
}
this.highScoreBounds = this.getHighScoreBounds();
return x >= this.highScoreBounds.x && x <=
this.highScoreBounds.x + this.highScoreBounds.width &&
y >= this.highScoreBounds.y && y <=
this.highScoreBounds.y + this.highScoreBounds.height;
},
/**
- Get the bounding box for the high score.
- @return {Object} Object with x, y, width and height properties.
*/
getHighScoreBounds: function() {
return {
x: (this.x - (this.maxScoreUnits * 2) *
DistanceMeter.dimensions.WIDTH) -
DistanceMeter.config.HIGH_SCORE_HIT_AREA_PADDING,
y: this.y,
width: DistanceMeter.dimensions.WIDTH * (this.highScore.length + 1) +
DistanceMeter.config.HIGH_SCORE_HIT_AREA_PADDING,
height: DistanceMeter.dimensions.HEIGHT +
(DistanceMeter.config.HIGH_SCORE_HIT_AREA_PADDING * 2)
};
},
/**
- Animate flashing the high score to indicate ready for resetting.
- The flashing stops following this.config.FLASH_ITERATIONS x 2 flashes.
*/
flashHighScore: function() {
var now = getTimeStamp();
var deltaTime = now - (this.frameTimeStamp || now);
var paint = true;
this.frameTimeStamp = now;
// Reached the max number of flashes.
if (this.flashIterations > this.config.FLASH_ITERATIONS * 2) {
this.cancelHighScoreFlashing();
return;
}
this.flashTimer += deltaTime;
if (this.flashTimer < this.config.FLASH_DURATION) {
paint = false;
} else if (this.flashTimer > this.config.FLASH_DURATION * 2) {
this.flashTimer = 0;
this.flashIterations++;
}
if (paint) {
this.drawHighScore();
} else {
this.clearHighScoreBounds();
}
// Frame update.
this.flashingRafId =
requestAnimationFrame(this.flashHighScore.bind(this));
},
/**
- Draw empty rectangle over high score.
*/
clearHighScoreBounds: function() {
this.canvasCtx.save();
this.canvasCtx.fillStyle = '#fff';
this.canvasCtx.rect(this.highScoreBounds.x, this.highScoreBounds.y,
this.highScoreBounds.width, this.highScoreBounds.height);
this.canvasCtx.fill();
this.canvasCtx.restore();
},
/**
- Starts the flashing of the high score.
*/
startHighScoreFlashing() {
this.highScoreFlashing = true;
this.flashHighScore();
},
/**
- Whether high score is flashing.
- @return {boolean}
*/
isHighScoreFlashing() {
return this.highScoreFlashing;
},
/**
- Stop flashing the high score.
*/
cancelHighScoreFlashing: function() {
cancelAnimationFrame(this.flashingRafId);
this.flashIterations = 0;
this.flashTimer = 0;
this.highScoreFlashing = false;
this.clearHighScoreBounds();
this.drawHighScore();
},
/**
- Clear the high score.
*/
resetHighScore: function() {
this.setHighScore(0);
this.cancelHighScoreFlashing();
},
/**
- Reset the distance meter back to '00000'.
*/
reset: function() {
this.update(0);
this.achievement = false;
}
};
//******************************************************************************
/**
- Cloud background item.
- Similar to an obstacle object but without collision boxes.
- @param {HTMLCanvasElement} canvas Canvas element.
- @param {Object} spritePos Position of image in sprite.
- @param {number} containerWidth
*/
function Cloud(canvas, spritePos, containerWidth) {
this.canvas = canvas;
this.canvasCtx = this.canvas.getContext('2d');
this.spritePos = spritePos;
this.containerWidth = containerWidth;
this.xPos = containerWidth;
this.yPos = 0;
this.remove = false;
this.cloudGap = getRandomNum(Cloud.config.MIN_CLOUD_GAP,
Cloud.config.MAX_CLOUD_GAP);
this.init();
};
/**
- Cloud object config.
- @enum {number}
*/
Cloud.config = {
HEIGHT: 14,
MAX_CLOUD_GAP: 400,
MAX_SKY_LEVEL: 30,
MIN_CLOUD_GAP: 100,
MIN_SKY_LEVEL: 71,
WIDTH: 46
};
Cloud.prototype = {
/**
- Initialise the cloud. Sets the Cloud height.
*/
init: function() {
this.yPos = getRandomNum(Cloud.config.MAX_SKY_LEVEL,
Cloud.config.MIN_SKY_LEVEL);
this.draw();
},
/**
- Draw the cloud.
*/
draw: function() {
this.canvasCtx.save();
var sourceWidth = Cloud.config.WIDTH;
var sourceHeight = Cloud.config.HEIGHT;
var outputWidth = sourceWidth;
var outputHeight = sourceHeight;
if (IS_HIDPI) {
sourceWidth = sourceWidth * 2;
sourceHeight = sourceHeight * 2;
}
this.canvasCtx.drawImage(Runner.imageSprite, this.spritePos.x,
this.spritePos.y,
sourceWidth, sourceHeight,
this.xPos, this.yPos,
outputWidth, outputHeight);
this.canvasCtx.restore();
},
/**
-
Update the cloud position.
-
@param {number} speed
*/
update: function(speed) {
if (!this.remove) {
this.xPos -= Math.ceil(speed);
this.draw();// Mark as removeable if no longer in the canvas.
if (!this.isVisible()) {
this.remove = true;
}
}
},
/**
- Check if the cloud is visible on the stage.
- @return {boolean}
*/
isVisible: function() {
return this.xPos + Cloud.config.WIDTH > 0;
}
};
//******************************************************************************
/**
- Nightmode shows a moon and stars on the horizon.
*/
function NightMode(canvas, spritePos, containerWidth) {
this.spritePos = spritePos;
this.canvas = canvas;
this.canvasCtx = canvas.getContext('2d');
this.xPos = containerWidth - 50;
this.yPos = 30;
this.currentPhase = 0;
this.opacity = 0;
this.containerWidth = containerWidth;
this.stars = [];
this.drawStars = false;
this.placeStars();
};
/**
- @enum {number}
*/
NightMode.config = {
FADE_SPEED: 0.035,
HEIGHT: 40,
MOON_SPEED: 0.25,
NUM_STARS: 2,
STAR_SIZE: 9,
STAR_SPEED: 0.3,
STAR_MAX_Y: 70,
WIDTH: 20
};
NightMode.phases = [140, 120, 100, 60, 40, 20, 0];
NightMode.prototype = {
/**
-
Update moving moon, changing phases.
-
@param {boolean} activated Whether night mode is activated.
-
@param {number} delta
*/
update: function(activated, delta) {
// Moon phase.
if (activated && this.opacity == 0) {
this.currentPhase++;if (this.currentPhase >= NightMode.phases.length) {
this.currentPhase = 0;
}
}
// Fade in / out.
if (activated && (this.opacity < 1 || this.opacity == 0)) {
this.opacity += NightMode.config.FADE_SPEED;
} else if (this.opacity > 0) {
this.opacity -= NightMode.config.FADE_SPEED;
}
// Set moon positioning.
if (this.opacity > 0) {
this.xPos = this.updateXPos(this.xPos, NightMode.config.MOON_SPEED);
// Update stars.
if (this.drawStars) {
for (var i = 0; i < NightMode.config.NUM_STARS; i++) {
this.stars[i].x = this.updateXPos(this.stars[i].x,
NightMode.config.STAR_SPEED);
}
}
this.draw();
} else {
this.opacity = 0;
this.placeStars();
}
this.drawStars = true;
},
updateXPos: function(currentPos, speed) {
if (currentPos < -NightMode.config.WIDTH) {
currentPos = this.containerWidth;
} else {
currentPos -= speed;
}
return currentPos;
},
draw: function() {
var moonSourceWidth = this.currentPhase == 3 ? NightMode.config.WIDTH * 2 :
NightMode.config.WIDTH;
var moonSourceHeight = NightMode.config.HEIGHT;
var moonSourceX = this.spritePos.x + NightMode.phases[this.currentPhase];
var moonOutputWidth = moonSourceWidth;
var starSize = NightMode.config.STAR_SIZE;
var starSourceX = Runner.spriteDefinition.LDPI.STAR.x;
if (IS_HIDPI) {
moonSourceWidth *= 2;
moonSourceHeight *= 2;
moonSourceX = this.spritePos.x +
(NightMode.phases[this.currentPhase] * 2);
starSize *= 2;
starSourceX = Runner.spriteDefinition.HDPI.STAR.x;
}
this.canvasCtx.save();
this.canvasCtx.globalAlpha = this.opacity;
// Stars.
if (this.drawStars) {
for (var i = 0; i < NightMode.config.NUM_STARS; i++) {
this.canvasCtx.drawImage(Runner.imageSprite,
starSourceX, this.stars[i].sourceY, starSize, starSize,
Math.round(this.stars[i].x), this.stars[i].y,
NightMode.config.STAR_SIZE, NightMode.config.STAR_SIZE);
}
}
// Moon.
this.canvasCtx.drawImage(Runner.imageSprite, moonSourceX,
this.spritePos.y, moonSourceWidth, moonSourceHeight,
Math.round(this.xPos), this.yPos,
moonOutputWidth, NightMode.config.HEIGHT);
this.canvasCtx.globalAlpha = 1;
this.canvasCtx.restore();
},
// Do star placement.
placeStars: function() {
var segmentSize = Math.round(this.containerWidth /
NightMode.config.NUM_STARS);
for (var i = 0; i < NightMode.config.NUM_STARS; i++) {
this.stars[i] = {};
this.stars[i].x = getRandomNum(segmentSize * i, segmentSize * (i + 1));
this.stars[i].y = getRandomNum(0, NightMode.config.STAR_MAX_Y);
if (IS_HIDPI) {
this.stars[i].sourceY = Runner.spriteDefinition.HDPI.STAR.y +
NightMode.config.STAR_SIZE * 2 * i;
} else {
this.stars[i].sourceY = Runner.spriteDefinition.LDPI.STAR.y +
NightMode.config.STAR_SIZE * i;
}
}
},
reset: function() {
this.currentPhase = 0;
this.opacity = 0;
this.update(false);
}
};
//******************************************************************************
/**
- Horizon Line.
- Consists of two connecting lines. Randomly assigns a flat / bumpy horizon.
- @param {HTMLCanvasElement} canvas
- @param {Object} spritePos Horizon position in sprite.
- @constructor
*/
function HorizonLine(canvas, spritePos) {
this.spritePos = spritePos;
this.canvas = canvas;
this.canvasCtx = canvas.getContext('2d');
this.sourceDimensions = {};
this.dimensions = HorizonLine.dimensions;
this.sourceXPos = [this.spritePos.x, this.spritePos.x +
this.dimensions.WIDTH];
this.xPos = [];
this.yPos = 0;
this.bumpThreshold = 0.5;
this.setSourceDimensions();
this.draw();
};
/**
- Horizon line dimensions.
- @enum {number}
*/
HorizonLine.dimensions = {
WIDTH: 600,
HEIGHT: 12,
YPOS: 127
};
HorizonLine.prototype = {
/**
- Set the source dimensions of the horizon line.
*/
setSourceDimensions: function() {
for (var dimension in HorizonLine.dimensions) {
if (IS_HIDPI) {
if (dimension != 'YPOS') {
this.sourceDimensions[dimension] =
HorizonLine.dimensions[dimension] * 2;
}
} else {
this.sourceDimensions[dimension] =
HorizonLine.dimensions[dimension];
}
this.dimensions[dimension] = HorizonLine.dimensions[dimension];
}
this.xPos = [0, HorizonLine.dimensions.WIDTH];
this.yPos = HorizonLine.dimensions.YPOS;
},
/**
- Return the crop x position of a type.
*/
getRandomType: function() {
return Math.random() > this.bumpThreshold ? this.dimensions.WIDTH : 0;
},
/**
- Draw the horizon line.
*/
draw: function() {
this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[0],
this.spritePos.y,
this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
this.xPos[0], this.yPos,
this.dimensions.WIDTH, this.dimensions.HEIGHT);
this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[1],
this.spritePos.y,
this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
this.xPos[1], this.yPos,
this.dimensions.WIDTH, this.dimensions.HEIGHT);
},
/**
- Update the x position of an indivdual piece of the line.
- @param {number} pos Line position.
- @param {number} increment
*/
updateXPos: function(pos, increment) {
var line1 = pos;
var line2 = pos == 0 ? 1 : 0;
this.xPos[line1] -= increment;
this.xPos[line2] = this.xPos[line1] + this.dimensions.WIDTH;
if (this.xPos[line1] <= -this.dimensions.WIDTH) {
this.xPos[line1] += this.dimensions.WIDTH * 2;
this.xPos[line2] = this.xPos[line1] - this.dimensions.WIDTH;
this.sourceXPos[line1] = this.getRandomType() + this.spritePos.x;
}
},
/**
- Update the horizon line.
- @param {number} deltaTime
- @param {number} speed
*/
update: function(deltaTime, speed) {
var increment = Math.floor(speed * (FPS / 1000) * deltaTime);
if (this.xPos[0] <= 0) {
this.updateXPos(0, increment);
} else {
this.updateXPos(1, increment);
}
this.draw();
},
/**
- Reset horizon to the starting position.
*/
reset: function() {
this.xPos[0] = 0;
this.xPos[1] = HorizonLine.dimensions.WIDTH;
}
};
//******************************************************************************
/**
- Horizon background class.
- @param {HTMLCanvasElement} canvas
- @param {Object} spritePos Sprite positioning.
- @param {Object} dimensions Canvas dimensions.
- @param {number} gapCoefficient
- @constructor
*/
function Horizon(canvas, spritePos, dimensions, gapCoefficient) {
this.canvas = canvas;
this.canvasCtx = this.canvas.getContext('2d');
this.config = Horizon.config;
this.dimensions = dimensions;
this.gapCoefficient = gapCoefficient;
this.obstacles = [];
this.obstacleHistory = [];
this.horizonOffsets = [0, 0];
this.cloudFrequency = this.config.CLOUD_FREQUENCY;
this.spritePos = spritePos;
this.nightMode = null;
// Cloud
this.clouds = [];
this.cloudSpeed = this.config.BG_CLOUD_SPEED;
// Horizon
this.horizonLine = null;
this.init();
};
/**
- Horizon config.
- @enum {number}
*/
Horizon.config = {
BG_CLOUD_SPEED: 0.2,
BUMPY_THRESHOLD: .3,
CLOUD_FREQUENCY: .5,
HORIZON_HEIGHT: 16,
MAX_CLOUDS: 6
};
Horizon.prototype = {
/**
- Initialise the horizon. Just add the line and a cloud. No obstacles.
*/
init: function() {
this.addCloud();
this.horizonLine = new HorizonLine(this.canvas, this.spritePos.HORIZON);
this.nightMode = new NightMode(this.canvas, this.spritePos.MOON,
this.dimensions.WIDTH);
},
/**
- @param {number} deltaTime
- @param {number} currentSpeed
- @param {boolean} updateObstacles Used as an override to prevent
-
the obstacles from being updated / added. This happens in the -
ease in section. - @param {boolean} showNightMode Night mode activated.
*/
update: function(deltaTime, currentSpeed, updateObstacles, showNightMode) {
this.runningTime += deltaTime;
this.horizonLine.update(deltaTime, currentSpeed);
this.nightMode.update(showNightMode);
this.updateClouds(deltaTime, currentSpeed);
if (updateObstacles) {
this.updateObstacles(deltaTime, currentSpeed);
}
},
/**
- Update the cloud positions.
- @param {number} deltaTime
- @param {number} currentSpeed
*/
updateClouds: function(deltaTime, speed) {
var cloudSpeed = this.cloudSpeed / 1000 * deltaTime * speed;
var numClouds = this.clouds.length;
if (numClouds) {
for (var i = numClouds - 1; i >= 0; i--) {
this.clouds[i].update(cloudSpeed);
}
var lastCloud = this.clouds[numClouds - 1];
// Check for adding a new cloud.
if (numClouds < this.config.MAX_CLOUDS &&
(this.dimensions.WIDTH - lastCloud.xPos) > lastCloud.cloudGap &&
this.cloudFrequency > Math.random()) {
this.addCloud();
}
// Remove expired clouds.
this.clouds = this.clouds.filter(function(obj) {
return !obj.remove;
});
} else {
this.addCloud();
}
},
/**
- Update the obstacle positions.
- @param {number} deltaTime
- @param {number} currentSpeed
*/
updateObstacles: function(deltaTime, currentSpeed) {
// Obstacles, move to Horizon layer.
var updatedObstacles = this.obstacles.slice(0);
for (var i = 0; i < this.obstacles.length; i++) {
var obstacle = this.obstacles[i];
obstacle.update(deltaTime, currentSpeed);
// Clean up existing obstacles.
if (obstacle.remove) {
updatedObstacles.shift();
}
}
this.obstacles = updatedObstacles;
if (this.obstacles.length > 0) {
var lastObstacle = this.obstacles[this.obstacles.length - 1];
if (lastObstacle && !lastObstacle.followingObstacleCreated &&
lastObstacle.isVisible() &&
(lastObstacle.xPos + lastObstacle.width + lastObstacle.gap) <
this.dimensions.WIDTH) {
this.addNewObstacle(currentSpeed);
lastObstacle.followingObstacleCreated = true;
}
} else {
// Create new obstacles.
this.addNewObstacle(currentSpeed);
}
},
removeFirstObstacle: function() {
this.obstacles.shift();
},
/**
- Add a new obstacle.
- @param {number} currentSpeed
*/
addNewObstacle: function(currentSpeed) {
var obstacleTypeIndex = getRandomNum(0, Obstacle.types.length - 1);
var obstacleType = Obstacle.types[obstacleTypeIndex];
// Check for multiples of the same type of obstacle.
// Also check obstacle is available at current speed.
if (this.duplicateObstacleCheck(obstacleType.type) ||
currentSpeed < obstacleType.minSpeed) {
this.addNewObstacle(currentSpeed);
} else {
var obstacleSpritePos = this.spritePos[obstacleType.type];
this.obstacles.push(new Obstacle(this.canvasCtx, obstacleType,
obstacleSpritePos, this.dimensions,
this.gapCoefficient, currentSpeed, obstacleType.width));
this.obstacleHistory.unshift(obstacleType.type);
if (this.obstacleHistory.length > 1) {
this.obstacleHistory.splice(Runner.config.MAX_OBSTACLE_DUPLICATION);
}
}
},
/**
- Returns whether the previous two obstacles are the same as the next one.
- Maximum duplication is set in config value MAX_OBSTACLE_DUPLICATION.
- @return {boolean}
*/
duplicateObstacleCheck: function(nextObstacleType) {
var duplicateCount = 0;
for (var i = 0; i < this.obstacleHistory.length; i++) {
duplicateCount = this.obstacleHistory[i] == nextObstacleType ?
duplicateCount + 1 : 0;
}
return duplicateCount >= Runner.config.MAX_OBSTACLE_DUPLICATION;
},
/**
- Reset the horizon layer.
- Remove existing obstacles and reposition the horizon line.
*/
reset: function() {
this.obstacles = [];
this.horizonLine.reset();
this.nightMode.reset();
},
/**
- Update the canvas width and scaling.
- @param {number} width Canvas width.
- @param {number} height Canvas height.
*/
resize: function(width, height) {
this.canvas.width = width;
this.canvas.height = height;
},
/**
- Add a new cloud to the horizon.
*/
addCloud: function() {
this.clouds.push(new Cloud(this.canvas, this.spritePos.CLOUD,
this.dimensions.WIDTH));
}
};
})();
@noeff53
You can't comment at this time — your comment can't be blank.
Leave a comment
No file chosen
Attach files by dragging & dropping, selecting or pasting them.
© 2021 GitHub, Inc.
Terms
Privacy
Security
Status
Docs
Contact GitHub
Pricing
API
Training
Blog
About
Skip to content
Search…
<script src="https://gist.github.com/washingtonsoares/05c3d112182de7d5742eb572c90d197b.js"></script>All gists
Back to GitHub
@noeff53
Please verify your email address to access all of GitHub’s features.
An email containing verification instructions was sent to [email protected].
@washingtonsoares
washingtonsoares/google_dinosaur.js
Created 2 years ago • Report abuse
0
Code
Revisions 1
Google Dinosaur Game
google_dinosaur.js
// Copyright (c) 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
(function() {
'use strict';
/**
*/
function Runner(outerContainerId, opt_config) {
// Singleton
if (Runner.instance_) {
return Runner.instance_;
}
Runner.instance_ = this;
this.outerContainerEl = document.querySelector(outerContainerId);
this.containerEl = null;
this.snackbarEl = null;
// A div to intercept touch events. Only set while (playing && useTouch).
this.touchController = null;
this.config = opt_config || Runner.config;
// Logical dimensions of the container.
this.dimensions = Runner.defaultDimensions;
this.canvas = null;
this.canvasCtx = null;
this.tRex = null;
this.distanceMeter = null;
this.distanceRan = 0;
this.highestScore = 0;
this.syncHighestScore = false;
this.time = 0;
this.runningTime = 0;
this.msPerFrame = 1000 / FPS;
this.currentSpeed = this.config.SPEED;
this.obstacles = [];
this.activated = false; // Whether the easter egg has been activated.
this.playing = false; // Whether the game is currently in play state.
this.crashed = false;
this.paused = false;
this.inverted = false;
this.invertTimer = 0;
this.resizeTimerId_ = null;
this.playCount = 0;
// Sound FX.
this.audioBuffer = null;
this.soundFx = {};
// Global web audio context for playing sounds.
this.audioContext = null;
// Images.
this.images = {};
this.imagesLoaded = 0;
if (this.isDisabled()) {
this.setupDisabledRunner();
} else {
this.loadImages();
}
}
window['Runner'] = Runner;
/**
*/
var DEFAULT_WIDTH = 600;
/**
*/
var FPS = 60;
/** @const */
var IS_HIDPI = window.devicePixelRatio > 1;
/** @const */
// iPads are returning "MacIntel" for iOS 13 (devices & simulators).
// Chrome on macOS also returns "MacIntel" for navigator.platform,
// but navigator.userAgent includes /Safari/.
// TODO(crbug.com/998999): Fix navigator.userAgent such that it reliably
// returns an agent string containing "CriOS".
var IS_IOS = /iPad|iPhone|iPod|MacIntel/.test(window.navigator.platform) &&
!(/Safari/.test(window.navigator.userAgent));
/** @const */
var IS_MOBILE = /Android/.test(window.navigator.userAgent) || IS_IOS;
/** @const */
var ARCADE_MODE_URL = 'chrome://dino/';
/**
*/
Runner.config = {
ACCELERATION: 0.001,
BG_CLOUD_SPEED: 0.2,
BOTTOM_PAD: 10,
// Scroll Y threshold at which the game can be activated.
CANVAS_IN_VIEW_OFFSET: -10,
CLEAR_TIME: 3000,
CLOUD_FREQUENCY: 0.5,
GAMEOVER_CLEAR_TIME: 750,
GAP_COEFFICIENT: 0.6,
GRAVITY: 0.6,
INITIAL_JUMP_VELOCITY: 12,
INVERT_FADE_DURATION: 12000,
INVERT_DISTANCE: 700,
MAX_BLINK_COUNT: 3,
MAX_CLOUDS: 6,
MAX_OBSTACLE_LENGTH: 3,
MAX_OBSTACLE_DUPLICATION: 2,
MAX_SPEED: 13,
MIN_JUMP_HEIGHT: 35,
MOBILE_SPEED_COEFFICIENT: 1.2,
RESOURCE_TEMPLATE_ID: 'audio-resources',
SPEED: 6,
SPEED_DROP_COEFFICIENT: 3,
ARCADE_MODE_INITIAL_TOP_POSITION: 35,
ARCADE_MODE_TOP_POSITION_PERCENT: 0.1
};
/**
*/
Runner.defaultDimensions = {
WIDTH: DEFAULT_WIDTH,
HEIGHT: 150
};
/**
*/
Runner.classes = {
ARCADE_MODE: 'arcade-mode',
CANVAS: 'runner-canvas',
CONTAINER: 'runner-container',
CRASHED: 'crashed',
ICON: 'icon-offline',
INVERTED: 'inverted',
SNACKBAR: 'snackbar',
SNACKBAR_SHOW: 'snackbar-show',
TOUCH_CONTROLLER: 'controller'
};
/**
*/
Runner.spriteDefinition = {
LDPI: {
CACTUS_LARGE: {x: 332, y: 2},
CACTUS_SMALL: {x: 228, y: 2},
CLOUD: {x: 86, y: 2},
HORIZON: {x: 2, y: 54},
MOON: {x: 484, y: 2},
PTERODACTYL: {x: 134, y: 2},
RESTART: {x: 2, y: 2},
TEXT_SPRITE: {x: 655, y: 2},
TREX: {x: 848, y: 2},
STAR: {x: 645, y: 2}
},
HDPI: {
CACTUS_LARGE: {x: 652, y: 2},
CACTUS_SMALL: {x: 446, y: 2},
CLOUD: {x: 166, y: 2},
HORIZON: {x: 2, y: 104},
MOON: {x: 954, y: 2},
PTERODACTYL: {x: 260, y: 2},
RESTART: {x: 2, y: 2},
TEXT_SPRITE: {x: 1294, y: 2},
TREX: {x: 1678, y: 2},
STAR: {x: 1276, y: 2}
}
};
/**
*/
Runner.sounds = {
BUTTON_PRESS: 'offline-sound-press',
HIT: 'offline-sound-hit',
SCORE: 'offline-sound-reached'
};
/**
*/
Runner.keycodes = {
JUMP: {'38': 1, '32': 1}, // Up, spacebar
DUCK: {'40': 1}, // Down
RESTART: {'13': 1} // Enter
};
/**
*/
Runner.events = {
ANIM_END: 'webkitAnimationEnd',
CLICK: 'click',
KEYDOWN: 'keydown',
KEYUP: 'keyup',
POINTERDOWN: 'pointerdown',
POINTERUP: 'pointerup',
RESIZE: 'resize',
TOUCHEND: 'touchend',
TOUCHSTART: 'touchstart',
VISIBILITY: 'visibilitychange',
BLUR: 'blur',
FOCUS: 'focus',
LOAD: 'load'
};
Runner.prototype = {
/**
*/
isDisabled: function() {
return loadTimeData && loadTimeData.valueExists('disabledEasterEgg');
},
/**
*/
setupDisabledRunner: function() {
this.containerEl = document.createElement('div');
this.containerEl.className = Runner.classes.SNACKBAR;
this.containerEl.textContent = loadTimeData.getValue('disabledEasterEgg');
this.outerContainerEl.appendChild(this.containerEl);
},
/**
Setting individual settings for debugging.
@param {string} setting
@param {*} value
*/
updateConfigSetting: function(setting, value) {
if (setting in this.config && value != undefined) {
this.config[setting] = value;
switch (setting) {
case 'GRAVITY':
case 'MIN_JUMP_HEIGHT':
case 'SPEED_DROP_COEFFICIENT':
this.tRex.config[setting] = value;
break;
case 'INITIAL_JUMP_VELOCITY':
this.tRex.setJumpVelocity(value);
break;
case 'SPEED':
this.setSpeed(value);
break;
}
}
},
/**
*/
loadImages: function() {
if (IS_HIDPI) {
Runner.imageSprite = document.getElementById('offline-resources-2x');
this.spriteDef = Runner.spriteDefinition.HDPI;
} else {
Runner.imageSprite = document.getElementById('offline-resources-1x');
this.spriteDef = Runner.spriteDefinition.LDPI;
}
},
/**
Load and decode base 64 encoded sounds.
*/
loadSounds: function() {
if (!IS_IOS) {
this.audioContext = new AudioContext();
var resourceTemplate =
document.getElementById(this.config.RESOURCE_TEMPLATE_ID).content;
for (var sound in Runner.sounds) {
var soundSrc =
resourceTemplate.getElementById(Runner.sounds[sound]).src;
soundSrc = soundSrc.substr(soundSrc.indexOf(',') + 1);
var buffer = decodeBase64ToArrayBuffer(soundSrc);
// Async, so no guarantee of order in array.
this.audioContext.decodeAudioData(buffer, function(index, audioData) {
this.soundFx[index] = audioData;
}.bind(this, sound));
}
}
},
/**
*/
setSpeed: function(opt_speed) {
var speed = opt_speed || this.currentSpeed;
},
/**
*/
init: function() {
// Hide the static icon.
document.querySelector('.' + Runner.classes.ICON).style.visibility =
'hidden';
//
},
/**
*/
createTouchController: function() {
this.touchController = document.createElement('div');
this.touchController.className = Runner.classes.TOUCH_CONTROLLER;
this.touchController.addEventListener(Runner.events.TOUCHSTART, this);
this.touchController.addEventListener(Runner.events.TOUCHEND, this);
this.outerContainerEl.appendChild(this.touchController);
},
/**
*/
debounceResize: function() {
if (!this.resizeTimerId_) {
this.resizeTimerId_ =
setInterval(this.adjustDimensions.bind(this), 250);
}
},
/**
*/
adjustDimensions: function() {
clearInterval(this.resizeTimerId_);
this.resizeTimerId_ = null;
},
/**
*/
playIntro: function() {
if (!this.activated && !this.crashed) {
this.playingIntro = true;
this.tRex.playingIntro = true;
//
// CSS animation definition.
var keyframes = '@-webkit-keyframes intro { ' +
'from { width:' + Trex.config.WIDTH + 'px }' +
'to { width: ' + this.dimensions.WIDTH + 'px }' +
'}';
document.styleSheets[0].insertRule(keyframes, 0);
},
/**
*/
startGame: function() {
if (this.isArcadeMode()) {
this.setArcadeMode();
}
this.runningTime = 0;
this.playingIntro = false;
this.tRex.playingIntro = false;
this.containerEl.style.webkitAnimation = '';
this.playCount++;
},
clearCanvas: function() {
this.canvasCtx.clearRect(0, 0, this.dimensions.WIDTH,
this.dimensions.HEIGHT);
},
/**
*/
isCanvasInView: function() {
return this.containerEl.getBoundingClientRect().top >
Runner.config.CANVAS_IN_VIEW_OFFSET;
},
/**
*/
update: function() {
this.updatePending = false;
},
/**
*/
handleEvent: function(e) {
return (function(evtType, events) {
switch (evtType) {
case events.KEYDOWN:
case events.TOUCHSTART:
case events.POINTERDOWN:
this.onKeyDown(e);
break;
case events.KEYUP:
case events.TOUCHEND:
case events.POINTERUP:
this.onKeyUp(e);
break;
}
}.bind(this))(e.type, Runner.events);
},
/**
*/
startListening: function() {
// Keys.
document.addEventListener(Runner.events.KEYDOWN, this);
document.addEventListener(Runner.events.KEYUP, this);
},
/**
*/
stopListening: function() {
document.removeEventListener(Runner.events.KEYDOWN, this);
document.removeEventListener(Runner.events.KEYUP, this);
},
/**
*/
onKeyDown: function(e) {
// Prevent native page scrolling whilst tapping on mobile.
if (IS_MOBILE && this.playing) {
e.preventDefault();
}
},
/**
*/
onKeyUp: function(e) {
var keyCode = String(e.keyCode);
var isjumpKey = Runner.keycodes.JUMP[keyCode] ||
e.type == Runner.events.TOUCHEND ||
e.type == Runner.events.POINTERUP;
},
/**
*/
handleGameOverClicks: function(e) {
e.preventDefault();
if (this.distanceMeter.hasClickedOnHighScore(e) && this.highestScore) {
if (this.distanceMeter.isHighScoreFlashing()) {
// Subsequent click, reset the high score.
this.saveHighScore(0, true);
this.distanceMeter.resetHighScore();
} else {
// First click, flash the high score.
this.distanceMeter.startHighScoreFlashing();
}
} else {
this.distanceMeter.cancelHighScoreFlashing();
this.restart();
}
},
/**
*/
isLeftClickOnCanvas: function(e) {
return e.button != null && e.button < 2 &&
e.type == Runner.events.POINTERUP && e.target == this.canvas;
},
/**
*/
scheduleNextUpdate: function() {
if (!this.updatePending) {
this.updatePending = true;
this.raqId = requestAnimationFrame(this.update.bind(this));
}
},
/**
*/
isRunning: function() {
return !!this.raqId;
},
/**
*/
initializeHighScore: function(highScore) {
this.syncHighestScore = true;
highScore = Math.ceil(highScore);
if (highScore < this.highestScore) {
if (window.errorPageController) {
errorPageController.updateEasterEggHighScore(this.highestScore);
}
return;
}
this.highestScore = highScore;
this.distanceMeter.setHighScore(this.highestScore);
},
/**
*/
saveHighScore: function(distanceRan, opt_resetScore) {
this.highestScore = Math.ceil(distanceRan);
this.distanceMeter.setHighScore(this.highestScore);
},
/**
*/
gameOver: function() {
this.playSound(this.soundFx.HIT);
vibrate(200);
},
stop: function() {
this.setPlayStatus(false);
this.paused = true;
cancelAnimationFrame(this.raqId);
this.raqId = 0;
},
play: function() {
if (!this.crashed) {
this.setPlayStatus(true);
this.paused = false;
this.tRex.update(0, Trex.status.RUNNING);
this.time = getTimeStamp();
this.update();
}
},
restart: function() {
if (!this.raqId) {
this.playCount++;
this.runningTime = 0;
this.setPlayStatus(true);
this.paused = false;
this.crashed = false;
this.distanceRan = 0;
this.setSpeed(this.config.SPEED);
this.time = getTimeStamp();
this.containerEl.classList.remove(Runner.classes.CRASHED);
this.clearCanvas();
this.distanceMeter.reset(this.highestScore);
this.horizon.reset();
this.tRex.reset();
this.playSound(this.soundFx.BUTTON_PRESS);
this.invert(true);
this.bdayFlashTimer = null;
this.update();
}
},
setPlayStatus: function(isPlaying) {
if (this.touchController)
this.touchController.classList.toggle(HIDDEN_CLASS, !isPlaying);
this.playing = isPlaying;
},
/**
*/
isArcadeMode: function() {
return document.title == ARCADE_MODE_URL;
},
/**
*/
setArcadeMode: function() {
document.body.classList.add(Runner.classes.ARCADE_MODE);
this.setArcadeModeContainerScale();
},
/**
*/
setArcadeModeContainerScale: function() {
var windowHeight = window.innerHeight;
var scaleHeight = windowHeight / this.dimensions.HEIGHT;
var scaleWidth = window.innerWidth / this.dimensions.WIDTH;
var scale = Math.max(1, Math.min(scaleHeight, scaleWidth));
var scaledCanvasHeight = this.dimensions.HEIGHT * scale;
// Positions the game container at 10% of the available vertical window
// height minus the game container height.
var translateY = Math.ceil(Math.max(0, (windowHeight - scaledCanvasHeight -
Runner.config.ARCADE_MODE_INITIAL_TOP_POSITION) *
Runner.config.ARCADE_MODE_TOP_POSITION_PERCENT)) *
window.devicePixelRatio;
//
this.containerEl.style.transform = 'scale(' + scale + ') translateY(' +
translateY + 'px)';
},
/**
*/
onVisibilityChange: function(e) {
if (document.hidden || document.webkitHidden || e.type == 'blur' ||
document.visibilityState != 'visible') {
this.stop();
} else if (!this.crashed) {
this.tRex.reset();
this.play();
}
},
/**
*/
playSound: function(soundBuffer) {
if (soundBuffer) {
var sourceNode = this.audioContext.createBufferSource();
sourceNode.buffer = soundBuffer;
sourceNode.connect(this.audioContext.destination);
sourceNode.start(0);
}
},
/**
*/
invert: function(reset) {
let htmlEl = document.firstElementChild;
}
};
/**
*/
Runner.updateCanvasScaling = function(canvas, opt_width, opt_height) {
var context = canvas.getContext('2d');
// Query the various pixel ratios
var devicePixelRatio = Math.floor(window.devicePixelRatio) || 1;
var backingStoreRatio = Math.floor(context.webkitBackingStorePixelRatio) || 1;
var ratio = devicePixelRatio / backingStoreRatio;
// Upscale the canvas if the two ratios don't match
if (devicePixelRatio !== backingStoreRatio) {
var oldWidth = opt_width || canvas.width;
var oldHeight = opt_height || canvas.height;
} else if (devicePixelRatio == 1) {
// Reset the canvas width / height. Fixes scaling bug when the page is
// zoomed and the devicePixelRatio changes accordingly.
canvas.style.width = canvas.width + 'px';
canvas.style.height = canvas.height + 'px';
}
return false;
};
/**
*/
function getRandomNum(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
/**
*/
function vibrate(duration) {
if (IS_MOBILE && window.navigator.vibrate) {
window.navigator.vibrate(duration);
}
}
/**
*/
function createCanvas(container, width, height, opt_classname) {
var canvas = document.createElement('canvas');
canvas.className = opt_classname ? Runner.classes.CANVAS + ' ' +
opt_classname : Runner.classes.CANVAS;
canvas.width = width;
canvas.height = height;
container.appendChild(canvas);
return canvas;
}
/**
*/
function decodeBase64ToArrayBuffer(base64String) {
var len = (base64String.length / 4) * 3;
var str = atob(base64String);
var arrayBuffer = new ArrayBuffer(len);
var bytes = new Uint8Array(arrayBuffer);
for (var i = 0; i < len; i++) {
bytes[i] = str.charCodeAt(i);
}
return bytes.buffer;
}
/**
*/
function getTimeStamp() {
return IS_IOS ? new Date().getTime() : performance.now();
}
//******************************************************************************
/**
*/
function GameOverPanel(canvas, textImgPos, restartImgPos, dimensions) {
this.canvas = canvas;
this.canvasCtx = canvas.getContext('2d');
this.canvasDimensions = dimensions;
this.textImgPos = textImgPos;
this.restartImgPos = restartImgPos;
this.draw();
};
/**
*/
GameOverPanel.dimensions = {
TEXT_X: 0,
TEXT_Y: 13,
TEXT_WIDTH: 191,
TEXT_HEIGHT: 11,
RESTART_WIDTH: 36,
RESTART_HEIGHT: 32
};
GameOverPanel.prototype = {
/**
*/
updateDimensions: function(width, opt_height) {
this.canvasDimensions.WIDTH = width;
if (opt_height) {
this.canvasDimensions.HEIGHT = opt_height;
}
},
/**
*/
draw: function() {
var dimensions = GameOverPanel.dimensions;
}
};
//******************************************************************************
/**
*/
function checkForCollision(obstacle, tRex, opt_canvasCtx) {
var obstacleBoxXPos = Runner.defaultDimensions.WIDTH + obstacle.xPos;
// Adjustments are made to the bounding box as there is a 1 pixel white
// border around the t-rex and obstacles.
var tRexBox = new CollisionBox(
tRex.xPos + 1,
tRex.yPos + 1,
tRex.config.WIDTH - 2,
tRex.config.HEIGHT - 2);
var obstacleBox = new CollisionBox(
obstacle.xPos + 1,
obstacle.yPos + 1,
obstacle.typeConfig.width * obstacle.size - 2,
obstacle.typeConfig.height - 2);
// Debug outer box
if (opt_canvasCtx) {
drawCollisionBoxes(opt_canvasCtx, tRexBox, obstacleBox);
}
// Simple outer bounds check.
if (boxCompare(tRexBox, obstacleBox)) {
var collisionBoxes = obstacle.collisionBoxes;
var tRexCollisionBoxes = tRex.ducking ?
Trex.collisionBoxes.DUCKING : Trex.collisionBoxes.RUNNING;
}
return false;
};
/**
*/
function createAdjustedCollisionBox(box, adjustment) {
return new CollisionBox(
box.x + adjustment.x,
box.y + adjustment.y,
box.width,
box.height);
};
/**
*/
function drawCollisionBoxes(canvasCtx, tRexBox, obstacleBox) {
canvasCtx.save();
canvasCtx.strokeStyle = '#f00';
canvasCtx.strokeRect(tRexBox.x, tRexBox.y, tRexBox.width, tRexBox.height);
canvasCtx.strokeStyle = '#0f0';
canvasCtx.strokeRect(obstacleBox.x, obstacleBox.y,
obstacleBox.width, obstacleBox.height);
canvasCtx.restore();
};
/**
*/
function boxCompare(tRexBox, obstacleBox) {
var crashed = false;
var tRexBoxX = tRexBox.x;
var tRexBoxY = tRexBox.y;
var obstacleBoxX = obstacleBox.x;
var obstacleBoxY = obstacleBox.y;
// Axis-Aligned Bounding Box method.
if (tRexBox.x < obstacleBoxX + obstacleBox.width &&
tRexBox.x + tRexBox.width > obstacleBoxX &&
tRexBox.y < obstacleBox.y + obstacleBox.height &&
tRexBox.height + tRexBox.y > obstacleBox.y) {
crashed = true;
}
return crashed;
};
//******************************************************************************
/**
*/
function CollisionBox(x, y, w, h) {
this.x = x;
this.y = y;
this.width = w;
this.height = h;
};
//******************************************************************************
/**
*/
function Obstacle(canvasCtx, type, spriteImgPos, dimensions,
gapCoefficient, speed, opt_xOffset) {
this.canvasCtx = canvasCtx;
this.spritePos = spriteImgPos;
this.typeConfig = type;
this.gapCoefficient = gapCoefficient;
this.size = getRandomNum(1, Obstacle.MAX_OBSTACLE_LENGTH);
this.dimensions = dimensions;
this.remove = false;
this.xPos = dimensions.WIDTH + (opt_xOffset || 0);
this.yPos = 0;
this.width = 0;
this.collisionBoxes = [];
this.gap = 0;
this.speedOffset = 0;
// For animated obstacles.
this.currentFrame = 0;
this.timer = 0;
this.init(speed);
};
/**
*/
Obstacle.MAX_GAP_COEFFICIENT = 1.5;
/**
*/
Obstacle.MAX_OBSTACLE_LENGTH = 3,
Obstacle.prototype = {
/**
*/
init: function(speed) {
this.cloneCollisionBoxes();
},
/**
*/
draw: function() {
var sourceWidth = this.typeConfig.width;
var sourceHeight = this.typeConfig.height;
},
/**
Obstacle frame update.
@param {number} deltaTime
@param {number} speed
*/
update: function(deltaTime, speed) {
if (!this.remove) {
if (this.typeConfig.speedOffset) {
speed += this.speedOffset;
}
this.xPos -= Math.floor((speed * FPS / 1000) * deltaTime);
// Update frame
if (this.typeConfig.numFrames) {
this.timer += deltaTime;
if (this.timer >= this.typeConfig.frameRate) {
this.currentFrame =
this.currentFrame == this.typeConfig.numFrames - 1 ?
0 : this.currentFrame + 1;
this.timer = 0;
}
}
this.draw();
if (!this.isVisible()) {
this.remove = true;
}
}
},
/**
*/
getGap: function(gapCoefficient, speed) {
var minGap = Math.round(this.width * speed +
this.typeConfig.minGap * gapCoefficient);
var maxGap = Math.round(minGap * Obstacle.MAX_GAP_COEFFICIENT);
return getRandomNum(minGap, maxGap);
},
/**
*/
isVisible: function() {
return this.xPos + this.width > 0;
},
/**
*/
cloneCollisionBoxes: function() {
var collisionBoxes = this.typeConfig.collisionBoxes;
}
};
/**
*/
Obstacle.types = [
{
type: 'CACTUS_SMALL',
width: 17,
height: 35,
yPos: 105,
multipleSpeed: 4,
minGap: 120,
minSpeed: 0,
collisionBoxes: [
new CollisionBox(0, 7, 5, 27),
new CollisionBox(4, 0, 6, 34),
new CollisionBox(10, 4, 7, 14)
]
},
{
type: 'CACTUS_LARGE',
width: 25,
height: 50,
yPos: 90,
multipleSpeed: 7,
minGap: 120,
minSpeed: 0,
collisionBoxes: [
new CollisionBox(0, 12, 7, 38),
new CollisionBox(8, 0, 7, 49),
new CollisionBox(13, 10, 10, 38)
]
},
{
type: 'PTERODACTYL',
width: 46,
height: 40,
yPos: [ 100, 75, 50 ], // Variable height.
yPosMobile: [ 100, 50 ], // Variable height mobile.
multipleSpeed: 999,
minSpeed: 8.5,
minGap: 150,
collisionBoxes: [
new CollisionBox(15, 15, 16, 5),
new CollisionBox(18, 21, 24, 6),
new CollisionBox(2, 14, 4, 3),
new CollisionBox(6, 10, 4, 7),
new CollisionBox(10, 8, 6, 9)
],
numFrames: 2,
frameRate: 1000/6,
speedOffset: .8
}
];
//******************************************************************************
/**
*/
function Trex(canvas, spritePos) {
this.canvas = canvas;
this.canvasCtx = canvas.getContext('2d');
this.spritePos = spritePos;
this.xPos = 0;
this.yPos = 0;
// Position when on the ground.
this.groundYPos = 0;
this.currentFrame = 0;
this.currentAnimFrames = [];
this.blinkDelay = 0;
this.blinkCount = 0;
this.animStartTime = 0;
this.timer = 0;
this.msPerFrame = 1000 / FPS;
this.config = Trex.config;
// Current status.
this.status = Trex.status.WAITING;
this.jumping = false;
this.ducking = false;
this.jumpVelocity = 0;
this.reachedMinHeight = false;
this.speedDrop = false;
this.jumpCount = 0;
this.jumpspotX = 0;
this.init();
};
/**
*/
Trex.config = {
DROP_VELOCITY: -5,
GRAVITY: 0.6,
HEIGHT: 47,
HEIGHT_DUCK: 25,
INIITAL_JUMP_VELOCITY: -10,
INTRO_DURATION: 1500,
MAX_JUMP_HEIGHT: 30,
MIN_JUMP_HEIGHT: 30,
SPEED_DROP_COEFFICIENT: 3,
SPRITE_WIDTH: 262,
START_X_POS: 50,
WIDTH: 44,
WIDTH_DUCK: 59
};
/**
*/
Trex.collisionBoxes = {
DUCKING: [
new CollisionBox(1, 18, 55, 25)
],
RUNNING: [
new CollisionBox(22, 0, 17, 16),
new CollisionBox(1, 18, 30, 9),
new CollisionBox(10, 35, 14, 8),
new CollisionBox(1, 24, 29, 5),
new CollisionBox(5, 30, 21, 4),
new CollisionBox(9, 34, 15, 4)
]
};
/**
*/
Trex.status = {
CRASHED: 'CRASHED',
DUCKING: 'DUCKING',
JUMPING: 'JUMPING',
RUNNING: 'RUNNING',
WAITING: 'WAITING'
};
/**
*/
Trex.BLINK_TIMING = 7000;
/**
*/
Trex.animFrames = {
WAITING: {
frames: [44, 0],
msPerFrame: 1000 / 3
},
RUNNING: {
frames: [88, 132],
msPerFrame: 1000 / 12
},
CRASHED: {
frames: [220],
msPerFrame: 1000 / 60
},
JUMPING: {
frames: [0],
msPerFrame: 1000 / 60
},
DUCKING: {
frames: [264, 323],
msPerFrame: 1000 / 8
}
};
Trex.prototype = {
/**
*/
init: function() {
this.groundYPos = Runner.defaultDimensions.HEIGHT - this.config.HEIGHT -
Runner.config.BOTTOM_PAD;
this.yPos = this.groundYPos;
this.minJumpHeight = this.groundYPos - this.config.MIN_JUMP_HEIGHT;
},
/**
*/
setJumpVelocity: function(setting) {
this.config.INIITAL_JUMP_VELOCITY = -setting;
this.config.DROP_VELOCITY = -setting / 2;
},
/**
*/
update: function(deltaTime, opt_status) {
this.timer += deltaTime;
},
/**
*/
draw: function(x, y) {
var sourceX = x;
var sourceY = y;
var sourceWidth = this.ducking && this.status != Trex.status.CRASHED ?
this.config.WIDTH_DUCK : this.config.WIDTH;
var sourceHeight = this.config.HEIGHT;
var outputHeight = sourceHeight;
},
/**
*/
setBlinkDelay: function() {
this.blinkDelay = Math.ceil(Math.random() * Trex.BLINK_TIMING);
},
/**
*/
blink: function(time) {
var deltaTime = time - this.animStartTime;
},
/**
*/
startJump: function(speed) {
if (!this.jumping) {
this.update(0, Trex.status.JUMPING);
// Tweak the jump velocity based on the speed.
this.jumpVelocity = this.config.INIITAL_JUMP_VELOCITY - (speed / 10);
this.jumping = true;
this.reachedMinHeight = false;
this.speedDrop = false;
}
},
/**
*/
endJump: function() {
if (this.reachedMinHeight &&
this.jumpVelocity < this.config.DROP_VELOCITY) {
this.jumpVelocity = this.config.DROP_VELOCITY;
}
},
/**
*/
updateJump: function(deltaTime, speed) {
var msPerFrame = Trex.animFrames[this.status].msPerFrame;
var framesElapsed = deltaTime / msPerFrame;
},
/**
*/
setSpeedDrop: function() {
this.speedDrop = true;
this.jumpVelocity = 1;
},
/**
*/
setDuck: function(isDucking) {
if (isDucking && this.status != Trex.status.DUCKING) {
this.update(0, Trex.status.DUCKING);
this.ducking = true;
} else if (this.status == Trex.status.DUCKING) {
this.update(0, Trex.status.RUNNING);
this.ducking = false;
}
},
/**
*/
reset: function() {
this.yPos = this.groundYPos;
this.jumpVelocity = 0;
this.jumping = false;
this.ducking = false;
this.update(0, Trex.status.RUNNING);
this.midair = false;
this.speedDrop = false;
this.jumpCount = 0;
}
};
//******************************************************************************
/**
*/
function DistanceMeter(canvas, spritePos, canvasWidth) {
this.canvas = canvas;
this.canvasCtx = canvas.getContext('2d');
this.image = Runner.imageSprite;
this.spritePos = spritePos;
this.x = 0;
this.y = 5;
this.currentDistance = 0;
this.maxScore = 0;
this.highScore = 0;
this.container = null;
this.digits = [];
this.achievement = false;
this.defaultString = '';
this.flashTimer = 0;
this.flashIterations = 0;
this.invertTrigger = false;
this.flashingRafId = null;
this.highScoreBounds = {};
this.highScoreFlashing = false;
this.config = DistanceMeter.config;
this.maxScoreUnits = this.config.MAX_DISTANCE_UNITS;
this.init(canvasWidth);
};
/**
*/
DistanceMeter.dimensions = {
WIDTH: 10,
HEIGHT: 13,
DEST_WIDTH: 11
};
/**
*/
DistanceMeter.yPos = [0, 13, 27, 40, 53, 67, 80, 93, 107, 120];
/**
*/
DistanceMeter.config = {
// Number of digits.
MAX_DISTANCE_UNITS: 5,
// Distance that causes achievement animation.
ACHIEVEMENT_DISTANCE: 100,
// Used for conversion from pixel distance to a scaled unit.
COEFFICIENT: 0.025,
// Flash duration in milliseconds.
FLASH_DURATION: 1000 / 4,
// Flash iterations for achievement animation.
FLASH_ITERATIONS: 3,
// Padding around the high score hit area.
HIGH_SCORE_HIT_AREA_PADDING: 4
};
DistanceMeter.prototype = {
/**
*/
init: function(width) {
var maxDistanceStr = '';
},
/**
*/
calcXPos: function(canvasWidth) {
this.x = canvasWidth - (DistanceMeter.dimensions.DEST_WIDTH *
(this.maxScoreUnits + 1));
},
/**
*/
draw: function(digitPos, value, opt_highScore) {
var sourceWidth = DistanceMeter.dimensions.WIDTH;
var sourceHeight = DistanceMeter.dimensions.HEIGHT;
var sourceX = DistanceMeter.dimensions.WIDTH * value;
var sourceY = 0;
},
/**
*/
getActualDistance: function(distance) {
return distance ? Math.round(distance * this.config.COEFFICIENT) : 0;
},
/**
*/
update: function(deltaTime, distance) {
var paint = true;
var playSound = false;
},
/**
*/
drawHighScore: function() {
this.canvasCtx.save();
this.canvasCtx.globalAlpha = .8;
for (var i = this.highScore.length - 1; i >= 0; i--) {
this.draw(i, parseInt(this.highScore[i], 10), true);
}
this.canvasCtx.restore();
},
/**
*/
setHighScore: function(distance) {
distance = this.getActualDistance(distance);
var highScoreStr = (this.defaultString +
distance).substr(-this.maxScoreUnits);
},
/**
*/
hasClickedOnHighScore: function(e) {
var x = 0;
var y = 0;
},
/**
*/
getHighScoreBounds: function() {
return {
x: (this.x - (this.maxScoreUnits * 2) *
DistanceMeter.dimensions.WIDTH) -
DistanceMeter.config.HIGH_SCORE_HIT_AREA_PADDING,
y: this.y,
width: DistanceMeter.dimensions.WIDTH * (this.highScore.length + 1) +
DistanceMeter.config.HIGH_SCORE_HIT_AREA_PADDING,
height: DistanceMeter.dimensions.HEIGHT +
(DistanceMeter.config.HIGH_SCORE_HIT_AREA_PADDING * 2)
};
},
/**
*/
flashHighScore: function() {
var now = getTimeStamp();
var deltaTime = now - (this.frameTimeStamp || now);
var paint = true;
this.frameTimeStamp = now;
},
/**
*/
clearHighScoreBounds: function() {
this.canvasCtx.save();
this.canvasCtx.fillStyle = '#fff';
this.canvasCtx.rect(this.highScoreBounds.x, this.highScoreBounds.y,
this.highScoreBounds.width, this.highScoreBounds.height);
this.canvasCtx.fill();
this.canvasCtx.restore();
},
/**
*/
startHighScoreFlashing() {
this.highScoreFlashing = true;
this.flashHighScore();
},
/**
*/
isHighScoreFlashing() {
return this.highScoreFlashing;
},
/**
*/
cancelHighScoreFlashing: function() {
cancelAnimationFrame(this.flashingRafId);
this.flashIterations = 0;
this.flashTimer = 0;
this.highScoreFlashing = false;
this.clearHighScoreBounds();
this.drawHighScore();
},
/**
*/
resetHighScore: function() {
this.setHighScore(0);
this.cancelHighScoreFlashing();
},
/**
*/
reset: function() {
this.update(0);
this.achievement = false;
}
};
//******************************************************************************
/**
*/
function Cloud(canvas, spritePos, containerWidth) {
this.canvas = canvas;
this.canvasCtx = this.canvas.getContext('2d');
this.spritePos = spritePos;
this.containerWidth = containerWidth;
this.xPos = containerWidth;
this.yPos = 0;
this.remove = false;
this.cloudGap = getRandomNum(Cloud.config.MIN_CLOUD_GAP,
Cloud.config.MAX_CLOUD_GAP);
this.init();
};
/**
*/
Cloud.config = {
HEIGHT: 14,
MAX_CLOUD_GAP: 400,
MAX_SKY_LEVEL: 30,
MIN_CLOUD_GAP: 100,
MIN_SKY_LEVEL: 71,
WIDTH: 46
};
Cloud.prototype = {
/**
*/
init: function() {
this.yPos = getRandomNum(Cloud.config.MAX_SKY_LEVEL,
Cloud.config.MIN_SKY_LEVEL);
this.draw();
},
/**
*/
draw: function() {
this.canvasCtx.save();
var sourceWidth = Cloud.config.WIDTH;
var sourceHeight = Cloud.config.HEIGHT;
var outputWidth = sourceWidth;
var outputHeight = sourceHeight;
if (IS_HIDPI) {
sourceWidth = sourceWidth * 2;
sourceHeight = sourceHeight * 2;
}
},
/**
Update the cloud position.
@param {number} speed
*/
update: function(speed) {
if (!this.remove) {
this.xPos -= Math.ceil(speed);
this.draw();
// Mark as removeable if no longer in the canvas.
if (!this.isVisible()) {
this.remove = true;
}
}
},
/**
*/
isVisible: function() {
return this.xPos + Cloud.config.WIDTH > 0;
}
};
//******************************************************************************
/**
*/
function NightMode(canvas, spritePos, containerWidth) {
this.spritePos = spritePos;
this.canvas = canvas;
this.canvasCtx = canvas.getContext('2d');
this.xPos = containerWidth - 50;
this.yPos = 30;
this.currentPhase = 0;
this.opacity = 0;
this.containerWidth = containerWidth;
this.stars = [];
this.drawStars = false;
this.placeStars();
};
/**
*/
NightMode.config = {
FADE_SPEED: 0.035,
HEIGHT: 40,
MOON_SPEED: 0.25,
NUM_STARS: 2,
STAR_SIZE: 9,
STAR_SPEED: 0.3,
STAR_MAX_Y: 70,
WIDTH: 20
};
NightMode.phases = [140, 120, 100, 60, 40, 20, 0];
NightMode.prototype = {
/**
Update moving moon, changing phases.
@param {boolean} activated Whether night mode is activated.
@param {number} delta
*/
update: function(activated, delta) {
// Moon phase.
if (activated && this.opacity == 0) {
this.currentPhase++;
if (this.currentPhase >= NightMode.phases.length) {
this.currentPhase = 0;
}
}
},
updateXPos: function(currentPos, speed) {
if (currentPos < -NightMode.config.WIDTH) {
currentPos = this.containerWidth;
} else {
currentPos -= speed;
}
return currentPos;
},
draw: function() {
var moonSourceWidth = this.currentPhase == 3 ? NightMode.config.WIDTH * 2 :
NightMode.config.WIDTH;
var moonSourceHeight = NightMode.config.HEIGHT;
var moonSourceX = this.spritePos.x + NightMode.phases[this.currentPhase];
var moonOutputWidth = moonSourceWidth;
var starSize = NightMode.config.STAR_SIZE;
var starSourceX = Runner.spriteDefinition.LDPI.STAR.x;
},
// Do star placement.
placeStars: function() {
var segmentSize = Math.round(this.containerWidth /
NightMode.config.NUM_STARS);
},
reset: function() {
this.currentPhase = 0;
this.opacity = 0;
this.update(false);
}
};
//******************************************************************************
/**
*/
function HorizonLine(canvas, spritePos) {
this.spritePos = spritePos;
this.canvas = canvas;
this.canvasCtx = canvas.getContext('2d');
this.sourceDimensions = {};
this.dimensions = HorizonLine.dimensions;
this.sourceXPos = [this.spritePos.x, this.spritePos.x +
this.dimensions.WIDTH];
this.xPos = [];
this.yPos = 0;
this.bumpThreshold = 0.5;
this.setSourceDimensions();
this.draw();
};
/**
*/
HorizonLine.dimensions = {
WIDTH: 600,
HEIGHT: 12,
YPOS: 127
};
HorizonLine.prototype = {
/**
*/
setSourceDimensions: function() {
},
/**
*/
getRandomType: function() {
return Math.random() > this.bumpThreshold ? this.dimensions.WIDTH : 0;
},
/**
*/
draw: function() {
this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[0],
this.spritePos.y,
this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
this.xPos[0], this.yPos,
this.dimensions.WIDTH, this.dimensions.HEIGHT);
},
/**
*/
updateXPos: function(pos, increment) {
var line1 = pos;
var line2 = pos == 0 ? 1 : 0;
},
/**
*/
update: function(deltaTime, speed) {
var increment = Math.floor(speed * (FPS / 1000) * deltaTime);
},
/**
*/
reset: function() {
this.xPos[0] = 0;
this.xPos[1] = HorizonLine.dimensions.WIDTH;
}
};
//******************************************************************************
/**
*/
function Horizon(canvas, spritePos, dimensions, gapCoefficient) {
this.canvas = canvas;
this.canvasCtx = this.canvas.getContext('2d');
this.config = Horizon.config;
this.dimensions = dimensions;
this.gapCoefficient = gapCoefficient;
this.obstacles = [];
this.obstacleHistory = [];
this.horizonOffsets = [0, 0];
this.cloudFrequency = this.config.CLOUD_FREQUENCY;
this.spritePos = spritePos;
this.nightMode = null;
// Cloud
this.clouds = [];
this.cloudSpeed = this.config.BG_CLOUD_SPEED;
// Horizon
this.horizonLine = null;
this.init();
};
/**
*/
Horizon.config = {
BG_CLOUD_SPEED: 0.2,
BUMPY_THRESHOLD: .3,
CLOUD_FREQUENCY: .5,
HORIZON_HEIGHT: 16,
MAX_CLOUDS: 6
};
Horizon.prototype = {
/**
*/
init: function() {
this.addCloud();
this.horizonLine = new HorizonLine(this.canvas, this.spritePos.HORIZON);
this.nightMode = new NightMode(this.canvas, this.spritePos.MOON,
this.dimensions.WIDTH);
},
/**
*/
update: function(deltaTime, currentSpeed, updateObstacles, showNightMode) {
this.runningTime += deltaTime;
this.horizonLine.update(deltaTime, currentSpeed);
this.nightMode.update(showNightMode);
this.updateClouds(deltaTime, currentSpeed);
},
/**
*/
updateClouds: function(deltaTime, speed) {
var cloudSpeed = this.cloudSpeed / 1000 * deltaTime * speed;
var numClouds = this.clouds.length;
},
/**
*/
updateObstacles: function(deltaTime, currentSpeed) {
// Obstacles, move to Horizon layer.
var updatedObstacles = this.obstacles.slice(0);
},
removeFirstObstacle: function() {
this.obstacles.shift();
},
/**
*/
addNewObstacle: function(currentSpeed) {
var obstacleTypeIndex = getRandomNum(0, Obstacle.types.length - 1);
var obstacleType = Obstacle.types[obstacleTypeIndex];
},
/**
*/
duplicateObstacleCheck: function(nextObstacleType) {
var duplicateCount = 0;
},
/**
*/
reset: function() {
this.obstacles = [];
this.horizonLine.reset();
this.nightMode.reset();
},
/**
*/
resize: function(width, height) {
this.canvas.width = width;
this.canvas.height = height;
},
/**
*/
addCloud: function() {
this.clouds.push(new Cloud(this.canvas, this.spritePos.CLOUD,
this.dimensions.WIDTH));
}
};
})();
@noeff53
You can't comment at this time — your comment can't be blank.
Leave a comment
No file chosen
Attach files by dragging & dropping, selecting or pasting them.
© 2021 GitHub, Inc.
Terms
Privacy
Security
Status
Docs
Contact GitHub
Pricing
API
Training
Blog
About