Index: chrome/renderer/resources/offline.js |
diff --git a/chrome/renderer/resources/offline.js b/chrome/renderer/resources/offline.js |
index 5fabe448cf567c1a09b82eef58a5c1933fffa751..5e6c6b348615f8a02f15c4ff5dc4807bf016a6b9 100644 |
--- a/chrome/renderer/resources/offline.js |
+++ b/chrome/renderer/resources/offline.js |
@@ -6,7 +6,7 @@ |
/** |
* T-Rex runner. |
* @param {string} outerContainerId Outer containing element id. |
- * @param {object} opt_config |
+ * @param {Object} opt_config |
* @constructor |
* @export |
*/ |
@@ -19,6 +19,7 @@ function Runner(outerContainerId, opt_config) { |
this.outerContainerEl = document.querySelector(outerContainerId); |
this.containerEl = null; |
+ this.snackbarEl = null; |
this.detailsButton = this.outerContainerEl.querySelector('#details-button'); |
this.config = opt_config || Runner.config; |
@@ -87,8 +88,8 @@ var FPS = 60; |
var IS_HIDPI = window.devicePixelRatio > 1; |
/** @const */ |
-var IS_IOS = |
- window.navigator.userAgent.indexOf('UIWebViewForStaticFileContent') > -1; |
+var IS_IOS = window.navigator.userAgent.indexOf('CriOS') > -1 || |
+ window.navigator.userAgent == 'UIWebViewForStaticFileContent'; |
/** @const */ |
var IS_MOBILE = window.navigator.userAgent.indexOf('Mobi') > -1 || IS_IOS; |
@@ -112,7 +113,8 @@ Runner.config = { |
INITIAL_JUMP_VELOCITY: 12, |
MAX_CLOUDS: 6, |
MAX_OBSTACLE_LENGTH: 3, |
- MAX_SPEED: 12, |
+ MAX_OBSTACLE_DUPLICATION: 2, |
+ MAX_SPEED: 13, |
MIN_JUMP_HEIGHT: 35, |
MOBILE_SPEED_COEFFICIENT: 1.2, |
RESOURCE_TEMPLATE_ID: 'audio-resources', |
@@ -147,28 +149,30 @@ Runner.classes = { |
/** |
- * Image source urls. |
- * @enum {array<object>} |
+ * Sprite definition layout of the spritesheet. |
+ * @enum {Object} |
*/ |
-Runner.imageSources = { |
- LDPI: [ |
- {name: 'CACTUS_LARGE', id: '1x-obstacle-large'}, |
- {name: 'CACTUS_SMALL', id: '1x-obstacle-small'}, |
- {name: 'CLOUD', id: '1x-cloud'}, |
- {name: 'HORIZON', id: '1x-horizon'}, |
- {name: 'RESTART', id: '1x-restart'}, |
- {name: 'TEXT_SPRITE', id: '1x-text'}, |
- {name: 'TREX', id: '1x-trex'} |
- ], |
- HDPI: [ |
- {name: 'CACTUS_LARGE', id: '2x-obstacle-large'}, |
- {name: 'CACTUS_SMALL', id: '2x-obstacle-small'}, |
- {name: 'CLOUD', id: '2x-cloud'}, |
- {name: 'HORIZON', id: '2x-horizon'}, |
- {name: 'RESTART', id: '2x-restart'}, |
- {name: 'TEXT_SPRITE', id: '2x-text'}, |
- {name: 'TREX', id: '2x-trex'} |
- ] |
+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}, |
+ PTERODACTYL: {x: 134, y: 2}, |
+ RESTART: {x: 2, y: 2}, |
+ TEXT_SPRITE: {x: 484, y: 2}, |
+ TREX: {x: 677, 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}, |
+ PTERODACTYL: {x: 260,y: 2}, |
+ RESTART: {x: 2,y: 2}, |
+ TEXT_SPRITE: {x: 954,y: 2}, |
+ TREX: {x: 1338,y: 2} |
+ } |
}; |
@@ -185,7 +189,7 @@ Runner.sounds = { |
/** |
* Key code mapping. |
- * @enum {object} |
+ * @enum {Object} |
*/ |
Runner.keycodes = { |
JUMP: {'38': 1, '32': 1}, // Up, spacebar |
@@ -268,18 +272,18 @@ Runner.prototype = { |
}, |
/** |
- * Load and cache the image assets from the page. |
+ * Cache the appropriate image sprite from the page and get the sprite sheet |
+ * definition. |
*/ |
loadImages: function() { |
- var imageSources = IS_HIDPI ? Runner.imageSources.HDPI : |
- Runner.imageSources.LDPI; |
- |
- var numImages = imageSources.length; |
- |
- for (var i = numImages - 1; i >= 0; i--) { |
- var imgSource = imageSources[i]; |
- this.images[imgSource.name] = document.getElementById(imgSource.id); |
+ 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; |
} |
+ |
this.init(); |
}, |
@@ -289,6 +293,7 @@ Runner.prototype = { |
loadSounds: function() { |
if (!IS_IOS) { |
this.audioContext = new AudioContext(); |
+ |
var resourceTemplate = |
document.getElementById(this.config.RESOURCE_TEMPLATE_ID).content; |
@@ -347,15 +352,15 @@ Runner.prototype = { |
Runner.updateCanvasScaling(this.canvas); |
// Horizon contains clouds, obstacles and the ground. |
- this.horizon = new Horizon(this.canvas, this.images, this.dimensions, |
+ this.horizon = new Horizon(this.canvas, this.spriteDef, this.dimensions, |
this.config.GAP_COEFFICIENT); |
// Distance meter |
this.distanceMeter = new DistanceMeter(this.canvas, |
- this.images.TEXT_SPRITE, this.dimensions.WIDTH); |
+ this.spriteDef.TEXT_SPRITE, this.dimensions.WIDTH); |
// Draw t-rex |
- this.tRex = new Trex(this.canvas, this.images.TREX); |
+ this.tRex = new Trex(this.canvas, this.spriteDef.TREX); |
this.outerContainerEl.appendChild(this.containerEl); |
@@ -504,7 +509,7 @@ Runner.prototype = { |
this.clearCanvas(); |
if (this.tRex.jumping) { |
- this.tRex.updateJump(deltaTime, this.config); |
+ this.tRex.updateJump(deltaTime); |
} |
this.runningTime += deltaTime; |
@@ -537,11 +542,6 @@ Runner.prototype = { |
this.gameOver(); |
} |
- if (this.distanceMeter.getActualDistance(this.distanceRan) > |
- this.distanceMeter.maxScore) { |
- this.distanceRan = 0; |
- } |
- |
var playAcheivementSound = this.distanceMeter.update(deltaTime, |
Math.ceil(this.distanceRan)); |
@@ -618,17 +618,23 @@ Runner.prototype = { |
* @param {Event} e |
*/ |
onKeyDown: function(e) { |
+ // Prevent native page scrolling whilst tapping on mobile. |
+ if (IS_MOBILE) { |
+ e.preventDefault(); |
+ } |
+ |
if (e.target != this.detailsButton) { |
- if (!this.crashed && (Runner.keycodes.JUMP[String(e.keyCode)] || |
+ if (!this.crashed && (Runner.keycodes.JUMP[e.keyCode] || |
e.type == Runner.events.TOUCHSTART)) { |
if (!this.activated) { |
this.loadSounds(); |
this.activated = true; |
+ errorPageController.trackEasterEgg(); |
} |
- if (!this.tRex.jumping) { |
+ if (!this.tRex.jumping && !this.tRex.ducking) { |
this.playSound(this.soundFx.BUTTON_PRESS); |
- this.tRex.startJump(); |
+ this.tRex.startJump(this.currentSpeed); |
} |
} |
@@ -638,10 +644,15 @@ Runner.prototype = { |
} |
} |
- // Speed drop, activated only when jump key is not pressed. |
- if (Runner.keycodes.DUCK[e.keyCode] && this.tRex.jumping) { |
+ if (this.activated && !this.crashed && Runner.keycodes.DUCK[e.keyCode]) { |
e.preventDefault(); |
- this.tRex.setSpeedDrop(); |
+ 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); |
+ } |
} |
}, |
@@ -660,22 +671,35 @@ Runner.prototype = { |
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 (Runner.keycodes.RESTART[keyCode] || |
- (e.type == Runner.events.MOUSEUP && e.target == this.canvas) || |
- (deltaTime >= this.config.GAMEOVER_CLEAR_TIME && |
- Runner.keycodes.JUMP[keyCode])) { |
+ if (Runner.keycodes.RESTART[keyCode] || this.isLeftClickOnCanvas(e) || |
+ (deltaTime >= this.config.GAMEOVER_CLEAR_TIME && |
+ Runner.keycodes.JUMP[keyCode])) { |
this.restart(); |
} |
} else if (this.paused && isjumpKey) { |
+ // Reset the jump state |
+ this.tRex.reset(); |
this.play(); |
} |
}, |
/** |
+ * 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 && e.button < 2 && e.type == Runner.events.MOUSEUP && |
+ e.target == this.canvas; |
+ }, |
+ |
+ /** |
* RequestAnimationFrame wrapper. |
*/ |
raq: function() { |
@@ -709,7 +733,7 @@ Runner.prototype = { |
// Game over panel. |
if (!this.gameOverPanel) { |
this.gameOverPanel = new GameOverPanel(this.canvas, |
- this.images.TEXT_SPRITE, this.images.RESTART, |
+ this.spriteDef.TEXT_SPRITE, this.spriteDef.RESTART, |
this.dimensions); |
} else { |
this.gameOverPanel.draw(); |
@@ -812,7 +836,6 @@ Runner.updateCanvasScaling = function(canvas, opt_width, opt_height) { |
// 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; |
@@ -910,17 +933,17 @@ function getTimeStamp() { |
/** |
* Game over panel. |
* @param {!HTMLCanvasElement} canvas |
- * @param {!HTMLImage} textSprite |
- * @param {!HTMLImage} restartImg |
+ * @param {Object} textImgPos |
+ * @param {Object} restartImgPos |
* @param {!Object} dimensions Canvas dimensions. |
* @constructor |
*/ |
-function GameOverPanel(canvas, textSprite, restartImg, dimensions) { |
+function GameOverPanel(canvas, textImgPos, restartImgPos, dimensions) { |
this.canvas = canvas; |
this.canvasCtx = canvas.getContext('2d'); |
this.canvasDimensions = dimensions; |
- this.textSprite = textSprite; |
- this.restartImg = restartImg; |
+ this.textImgPos = textImgPos; |
+ this.restartImgPos = restartImgPos; |
this.draw(); |
}; |
@@ -985,13 +1008,17 @@ GameOverPanel.prototype = { |
restartSourceHeight *= 2; |
} |
+ textSourceX += this.textImgPos.x; |
+ textSourceY += this.textImgPos.y; |
+ |
// Game over text from sprite. |
- this.canvasCtx.drawImage(this.textSprite, |
+ this.canvasCtx.drawImage(Runner.imageSprite, |
textSourceX, textSourceY, textSourceWidth, textSourceHeight, |
textTargetX, textTargetY, textTargetWidth, textTargetHeight); |
// Restart button. |
- this.canvasCtx.drawImage(this.restartImg, 0, 0, |
+ this.canvasCtx.drawImage(Runner.imageSprite, |
+ this.restartImgPos.x, this.restartImgPos.y, |
restartSourceWidth, restartSourceHeight, |
restartTargetX, restartTargetY, dimensions.RESTART_WIDTH, |
dimensions.RESTART_HEIGHT); |
@@ -1034,7 +1061,8 @@ function checkForCollision(obstacle, tRex, opt_canvasCtx) { |
// Simple outer bounds check. |
if (boxCompare(tRexBox, obstacleBox)) { |
var collisionBoxes = obstacle.collisionBoxes; |
- var tRexCollisionBoxes = Trex.collisionBoxes; |
+ var tRexCollisionBoxes = tRex.ducking ? |
+ Trex.collisionBoxes.DUCKING : Trex.collisionBoxes.RUNNING; |
// Detailed axis aligned box check. |
for (var t = 0; t < tRexCollisionBoxes.length; t++) { |
@@ -1082,12 +1110,11 @@ function createAdjustedCollisionBox(box, adjustment) { |
function drawCollisionBoxes(canvasCtx, tRexBox, obstacleBox) { |
canvasCtx.save(); |
canvasCtx.strokeStyle = '#f00'; |
- canvasCtx.strokeRect(tRexBox.x, tRexBox.y, |
- tRexBox.width, tRexBox.height); |
+ canvasCtx.strokeRect(tRexBox.x, tRexBox.y, tRexBox.width, tRexBox.height); |
canvasCtx.strokeStyle = '#0f0'; |
canvasCtx.strokeRect(obstacleBox.x, obstacleBox.y, |
- obstacleBox.width, obstacleBox.height); |
+ obstacleBox.width, obstacleBox.height); |
canvasCtx.restore(); |
}; |
@@ -1141,26 +1168,31 @@ function CollisionBox(x, y, w, h) { |
* Obstacle. |
* @param {HTMLCanvasCtx} canvasCtx |
* @param {Obstacle.type} type |
- * @param {image} obstacleImg Image sprite. |
+ * @param {Object} spritePos Obstacle position in sprite. |
* @param {Object} dimensions |
* @param {number} gapCoefficient Mutipler in determining the gap. |
* @param {number} speed |
*/ |
-function Obstacle(canvasCtx, type, obstacleImg, dimensions, |
+function Obstacle(canvasCtx, type, spriteImgPos, dimensions, |
gapCoefficient, speed) { |
this.canvasCtx = canvasCtx; |
- this.image = obstacleImg; |
+ 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 = 0; |
- this.yPos = this.typeConfig.yPos; |
+ 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); |
}; |
@@ -1194,6 +1226,15 @@ Obstacle.prototype = { |
this.width = this.typeConfig.width * this.size; |
this.xPos = this.dimensions.WIDTH - this.width; |
+ // 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, |
@@ -1210,6 +1251,12 @@ Obstacle.prototype = { |
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); |
}, |
@@ -1225,10 +1272,17 @@ Obstacle.prototype = { |
sourceHeight = sourceHeight * 2; |
} |
- // Sprite |
- var sourceX = (sourceWidth * this.size) * (0.5 * (this.size - 1)); |
- this.canvasCtx.drawImage(this.image, |
- sourceX, 0, |
+ // 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); |
@@ -1241,7 +1295,21 @@ Obstacle.prototype = { |
*/ |
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()) { |
@@ -1292,16 +1360,18 @@ Obstacle.prototype = { |
* 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', |
- className: ' cactus cactus-small ', |
width: 17, |
height: 35, |
yPos: 105, |
- multipleSpeed: 3, |
+ multipleSpeed: 4, |
minGap: 120, |
+ minSpeed: 0, |
collisionBoxes: [ |
new CollisionBox(0, 7, 5, 27), |
new CollisionBox(4, 0, 6, 34), |
@@ -1310,17 +1380,37 @@ Obstacle.types = [ |
}, |
{ |
type: 'CACTUS_LARGE', |
- className: ' cactus cactus-large ', |
width: 25, |
height: 50, |
yPos: 90, |
- multipleSpeed: 6, |
+ 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 |
} |
]; |
@@ -1329,13 +1419,13 @@ Obstacle.types = [ |
/** |
* T-rex game character. |
* @param {HTMLCanvas} canvas |
- * @param {HTMLImage} image Character image. |
+ * @param {Object} spritePos Positioning within image sprite. |
* @constructor |
*/ |
-function Trex(canvas, image) { |
+function Trex(canvas, spritePos) { |
this.canvas = canvas; |
this.canvasCtx = canvas.getContext('2d'); |
- this.image = image; |
+ this.spritePos = spritePos; |
this.xPos = 0; |
this.yPos = 0; |
// Position when on the ground. |
@@ -1351,6 +1441,7 @@ function Trex(canvas, image) { |
this.status = Trex.status.WAITING; |
this.jumping = false; |
+ this.ducking = false; |
this.jumpVelocity = 0; |
this.reachedMinHeight = false; |
this.speedDrop = false; |
@@ -1369,6 +1460,7 @@ Trex.config = { |
DROP_VELOCITY: -5, |
GRAVITY: 0.6, |
HEIGHT: 47, |
+ HEIGHT_DUCK: 25, |
INIITAL_JUMP_VELOCITY: -10, |
INTRO_DURATION: 1500, |
MAX_JUMP_HEIGHT: 30, |
@@ -1376,7 +1468,8 @@ Trex.config = { |
SPEED_DROP_COEFFICIENT: 3, |
SPRITE_WIDTH: 262, |
START_X_POS: 50, |
- WIDTH: 44 |
+ WIDTH: 44, |
+ WIDTH_DUCK: 59 |
}; |
@@ -1384,14 +1477,19 @@ Trex.config = { |
* Used in collision detection. |
* @type {Array<CollisionBox>} |
*/ |
-Trex.collisionBoxes = [ |
- new CollisionBox(1, -1, 30, 26), |
- new CollisionBox(32, 0, 8, 16), |
- 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.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) |
+ ] |
+}; |
/** |
@@ -1400,6 +1498,7 @@ Trex.collisionBoxes = [ |
*/ |
Trex.status = { |
CRASHED: 'CRASHED', |
+ DUCKING: 'DUCKING', |
JUMPING: 'JUMPING', |
RUNNING: 'RUNNING', |
WAITING: 'WAITING' |
@@ -1414,7 +1513,7 @@ Trex.BLINK_TIMING = 7000; |
/** |
* Animation config for different states. |
- * @enum {object} |
+ * @enum {Object} |
*/ |
Trex.animFrames = { |
WAITING: { |
@@ -1432,6 +1531,10 @@ Trex.animFrames = { |
JUMPING: { |
frames: [0], |
msPerFrame: 1000 / 60 |
+ }, |
+ DUCKING: { |
+ frames: [262, 321], |
+ msPerFrame: 1000 / 8 |
} |
}; |
@@ -1500,6 +1603,12 @@ Trex.prototype = { |
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); |
+ } |
}, |
/** |
@@ -1510,7 +1619,8 @@ Trex.prototype = { |
draw: function(x, y) { |
var sourceX = x; |
var sourceY = y; |
- var sourceWidth = this.config.WIDTH; |
+ var sourceWidth = this.ducking && this.status != Trex.status.CRASHED ? |
+ this.config.WIDTH_DUCK : this.config.WIDTH; |
var sourceHeight = this.config.HEIGHT; |
if (IS_HIDPI) { |
@@ -1520,10 +1630,27 @@ Trex.prototype = { |
sourceHeight *= 2; |
} |
- this.canvasCtx.drawImage(this.image, sourceX, sourceY, |
- sourceWidth, sourceHeight, |
- this.xPos, this.yPos, |
- this.config.WIDTH, this.config.HEIGHT); |
+ // 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, this.config.HEIGHT); |
+ } 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, this.config.HEIGHT); |
+ } |
}, |
/** |
@@ -1553,11 +1680,13 @@ Trex.prototype = { |
/** |
* Initialise a jump. |
+ * @param {number} speed |
*/ |
- startJump: function() { |
+ startJump: function(speed) { |
if (!this.jumping) { |
this.update(0, Trex.status.JUMPING); |
- this.jumpVelocity = this.config.INIITAL_JUMP_VELOCITY; |
+ // 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; |
@@ -1577,8 +1706,9 @@ Trex.prototype = { |
/** |
* Update frame for a jump. |
* @param {number} deltaTime |
+ * @param {number} speed |
*/ |
- updateJump: function(deltaTime) { |
+ updateJump: function(deltaTime, speed) { |
var msPerFrame = Trex.animFrames[this.status].msPerFrame; |
var framesElapsed = deltaTime / msPerFrame; |
@@ -1620,12 +1750,26 @@ Trex.prototype = { |
}, |
/** |
+ * @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; |
@@ -1639,14 +1783,15 @@ Trex.prototype = { |
/** |
* Handles displaying the distance meter. |
* @param {!HTMLCanvasElement} canvas |
- * @param {!HTMLImage} spriteSheet Image sprite. |
+ * @param {Object} spritePos Image position in sprite. |
* @param {number} canvasWidth |
* @constructor |
*/ |
-function DistanceMeter(canvas, spriteSheet, canvasWidth) { |
+function DistanceMeter(canvas, spritePos, canvasWidth) { |
this.canvas = canvas; |
this.canvasCtx = canvas.getContext('2d'); |
- this.image = spriteSheet; |
+ this.image = Runner.imageSprite; |
+ this.spritePos = spritePos; |
this.x = 0; |
this.y = 5; |
@@ -1662,6 +1807,7 @@ function DistanceMeter(canvas, spriteSheet, canvasWidth) { |
this.flashIterations = 0; |
this.config = DistanceMeter.config; |
+ this.maxScoreUnits = this.config.MAX_DISTANCE_UNITS; |
this.init(canvasWidth); |
}; |
@@ -1679,7 +1825,7 @@ DistanceMeter.dimensions = { |
/** |
* Y positioning of the digits in the sprite sheet. |
* X position is always 0. |
- * @type {array<number>} |
+ * @type {Array<number>} |
*/ |
DistanceMeter.yPos = [0, 13, 27, 40, 53, 67, 80, 93, 107, 120]; |
@@ -1715,8 +1861,8 @@ DistanceMeter.prototype = { |
var maxDistanceStr = ''; |
this.calcXPos(width); |
- this.maxScore = this.config.MAX_DISTANCE_UNITS; |
- for (var i = 0; i < this.config.MAX_DISTANCE_UNITS; i++) { |
+ this.maxScore = this.maxScoreUnits; |
+ for (var i = 0; i < this.maxScoreUnits; i++) { |
this.draw(i, 0); |
this.defaultString += '0'; |
maxDistanceStr += '9'; |
@@ -1731,7 +1877,7 @@ DistanceMeter.prototype = { |
*/ |
calcXPos: function(canvasWidth) { |
this.x = canvasWidth - (DistanceMeter.dimensions.DEST_WIDTH * |
- (this.config.MAX_DISTANCE_UNITS + 1)); |
+ (this.maxScoreUnits + 1)); |
}, |
/** |
@@ -1744,6 +1890,7 @@ DistanceMeter.prototype = { |
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; |
@@ -1757,18 +1904,21 @@ DistanceMeter.prototype = { |
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.config.MAX_DISTANCE_UNITS * 2) * |
+ 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, 0, |
+ this.canvasCtx.drawImage(this.image, sourceX, sourceY, |
sourceWidth, sourceHeight, |
targetX, targetY, |
targetWidth, targetHeight |
@@ -1783,14 +1933,13 @@ DistanceMeter.prototype = { |
* @return {number} The 'real' distance ran. |
*/ |
getActualDistance: function(distance) { |
- return distance ? |
- Math.round(distance * this.config.COEFFICIENT) : 0; |
+ return distance ? Math.round(distance * this.config.COEFFICIENT) : 0; |
}, |
/** |
* Update the distance meter. |
- * @param {number} deltaTime |
* @param {number} distance |
+ * @param {number} deltaTime |
* @return {boolean} Whether the acheivement sound fx should be played. |
*/ |
update: function(deltaTime, distance) { |
@@ -1800,6 +1949,15 @@ DistanceMeter.prototype = { |
if (!this.acheivement) { |
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) { |
@@ -1811,7 +1969,7 @@ DistanceMeter.prototype = { |
// Create a string representation of the distance with leading 0. |
var distanceStr = (this.defaultString + |
- distance).substr(-this.config.MAX_DISTANCE_UNITS); |
+ distance).substr(-this.maxScoreUnits); |
this.digits = distanceStr.split(''); |
} else { |
this.digits = this.defaultString.split(''); |
@@ -1867,7 +2025,7 @@ DistanceMeter.prototype = { |
setHighScore: function(distance) { |
distance = this.getActualDistance(distance); |
var highScoreStr = (this.defaultString + |
- distance).substr(-this.config.MAX_DISTANCE_UNITS); |
+ distance).substr(-this.maxScoreUnits); |
this.highScore = ['10', '11', ''].concat(highScoreStr.split('')); |
}, |
@@ -1888,13 +2046,13 @@ DistanceMeter.prototype = { |
* Cloud background item. |
* Similar to an obstacle object but without collision boxes. |
* @param {HTMLCanvasElement} canvas Canvas element. |
- * @param {Image} cloudImg |
+ * @param {Object} spritePos Position of image in sprite. |
* @param {number} containerWidth |
*/ |
-function Cloud(canvas, cloudImg, containerWidth) { |
+function Cloud(canvas, spritePos, containerWidth) { |
this.canvas = canvas; |
this.canvasCtx = this.canvas.getContext('2d'); |
- this.image = cloudImg; |
+ this.spritePos = spritePos; |
this.containerWidth = containerWidth; |
this.xPos = containerWidth; |
this.yPos = 0; |
@@ -1943,7 +2101,8 @@ Cloud.prototype = { |
sourceHeight = sourceHeight * 2; |
} |
- this.canvasCtx.drawImage(this.image, 0, 0, |
+ this.canvasCtx.drawImage(Runner.imageSprite, this.spritePos.x, |
+ this.spritePos.y, |
sourceWidth, sourceHeight, |
this.xPos, this.yPos, |
Cloud.config.WIDTH, Cloud.config.HEIGHT); |
@@ -1983,16 +2142,17 @@ Cloud.prototype = { |
* Horizon Line. |
* Consists of two connecting lines. Randomly assigns a flat / bumpy horizon. |
* @param {HTMLCanvasElement} canvas |
- * @param {HTMLImage} bgImg Horizon line sprite. |
+ * @param {Object} spritePos Horizon position in sprite. |
* @constructor |
*/ |
-function HorizonLine(canvas, bgImg) { |
- this.image = bgImg; |
+function HorizonLine(canvas, spritePos) { |
+ this.spritePos = spritePos; |
this.canvas = canvas; |
this.canvasCtx = canvas.getContext('2d'); |
this.sourceDimensions = {}; |
this.dimensions = HorizonLine.dimensions; |
- this.sourceXPos = [0, this.dimensions.WIDTH]; |
+ this.sourceXPos = [this.spritePos.x, this.spritePos.x + |
+ this.dimensions.WIDTH]; |
this.xPos = []; |
this.yPos = 0; |
this.bumpThreshold = 0.5; |
@@ -2047,12 +2207,14 @@ HorizonLine.prototype = { |
* Draw the horizon line. |
*/ |
draw: function() { |
- this.canvasCtx.drawImage(this.image, this.sourceXPos[0], 0, |
+ 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(this.image, this.sourceXPos[1], 0, |
+ 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); |
@@ -2073,7 +2235,7 @@ HorizonLine.prototype = { |
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.sourceXPos[line1] = this.getRandomType() + this.spritePos.x; |
} |
}, |
@@ -2108,36 +2270,30 @@ HorizonLine.prototype = { |
/** |
* Horizon background class. |
* @param {HTMLCanvasElement} canvas |
- * @param {Array<HTMLImageElement>} images |
- * @param {object} dimensions Canvas dimensions. |
+ * @param {Object} spritePos Sprite positioning. |
+ * @param {Object} dimensions Canvas dimensions. |
* @param {number} gapCoefficient |
* @constructor |
*/ |
-function Horizon(canvas, images, dimensions, gapCoefficient) { |
+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; |
// Cloud |
this.clouds = []; |
- this.cloudImg = images.CLOUD; |
this.cloudSpeed = this.config.BG_CLOUD_SPEED; |
// Horizon |
- this.horizonImg = images.HORIZON; |
this.horizonLine = null; |
- // Obstacles |
- this.obstacleImgs = { |
- CACTUS_SMALL: images.CACTUS_SMALL, |
- CACTUS_LARGE: images.CACTUS_LARGE |
- }; |
- |
this.init(); |
}; |
@@ -2161,7 +2317,7 @@ Horizon.prototype = { |
*/ |
init: function() { |
this.addCloud(); |
- this.horizonLine = new HorizonLine(this.canvas, this.horizonImg); |
+ this.horizonLine = new HorizonLine(this.canvas, this.spritePos.HORIZON); |
}, |
/** |
@@ -2252,13 +2408,42 @@ Horizon.prototype = { |
* @param {number} currentSpeed |
*/ |
addNewObstacle: function(currentSpeed) { |
- var obstacleTypeIndex = |
- getRandomNum(0, Obstacle.types.length - 1); |
+ var obstacleTypeIndex = getRandomNum(0, Obstacle.types.length - 1); |
var obstacleType = Obstacle.types[obstacleTypeIndex]; |
- var obstacleImg = this.obstacleImgs[obstacleType.type]; |
- this.obstacles.push(new Obstacle(this.canvasCtx, obstacleType, |
- obstacleImg, this.dimensions, this.gapCoefficient, currentSpeed)); |
+ // 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)); |
+ |
+ 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; |
}, |
/** |
@@ -2284,7 +2469,7 @@ Horizon.prototype = { |
* Add a new cloud to the horizon. |
*/ |
addCloud: function() { |
- this.clouds.push(new Cloud(this.canvas, this.cloudImg, |
+ this.clouds.push(new Cloud(this.canvas, this.spritePos.CLOUD, |
this.dimensions.WIDTH)); |
} |
}; |