├── .editorconfig ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── .vscode ├── extensions.json └── settings.json ├── assets ├── PressStart2P-Regular.ttf ├── favicon.png ├── game-over.mp3 ├── jump.mp3 ├── level-up.mp3 ├── preview.png └── sprite.png ├── index.html ├── lib ├── actors │ ├── Actor.js │ ├── Bird.js │ ├── Cactus.js │ ├── Cloud.js │ └── Dino.js ├── game │ ├── DinoGame.js │ └── GameRunner.js ├── index.js ├── sounds.js ├── sprites.js └── utils.js ├── license ├── package.json ├── readme.md └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules 3 | index.html 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'always', 3 | semi: false, 4 | singleQuote: true, 5 | tabWidth: 2, 6 | trailingComma: 'es5', 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[javascript]": { 3 | "editor.defaultFormatter": "esbenp.prettier-vscode" 4 | }, 5 | "editor.defaultFormatter": "esbenp.prettier-vscode", 6 | "editor.formatOnSave": true, 7 | "javascript.format.enable": true, 8 | } 9 | -------------------------------------------------------------------------------- /assets/PressStart2P-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisdothtml/chrome-dino/6761e29e9301c297d474309eafebb3bf2c2b7a8f/assets/PressStart2P-Regular.ttf -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisdothtml/chrome-dino/6761e29e9301c297d474309eafebb3bf2c2b7a8f/assets/favicon.png -------------------------------------------------------------------------------- /assets/game-over.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisdothtml/chrome-dino/6761e29e9301c297d474309eafebb3bf2c2b7a8f/assets/game-over.mp3 -------------------------------------------------------------------------------- /assets/jump.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisdothtml/chrome-dino/6761e29e9301c297d474309eafebb3bf2c2b7a8f/assets/jump.mp3 -------------------------------------------------------------------------------- /assets/level-up.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisdothtml/chrome-dino/6761e29e9301c297d474309eafebb3bf2c2b7a8f/assets/level-up.mp3 -------------------------------------------------------------------------------- /assets/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisdothtml/chrome-dino/6761e29e9301c297d474309eafebb3bf2c2b7a8f/assets/preview.png -------------------------------------------------------------------------------- /assets/sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisdothtml/chrome-dino/6761e29e9301c297d474309eafebb3bf2c2b7a8f/assets/sprite.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Chrome Dino 8 | 36 | 37 | 38 | 39 | 40 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /lib/actors/Actor.js: -------------------------------------------------------------------------------- 1 | import sprites from '../sprites.js' 2 | 3 | const cache = new Map() 4 | 5 | // analyze the pixels and create a map of the transparent 6 | // and non-transparent pixels for hit testing 7 | function getSpriteAlphaMap(imageData, name) { 8 | if (cache.has(name)) { 9 | return cache.get(name) 10 | } 11 | 12 | const sprite = sprites[name] 13 | const lines = [] 14 | const initIVal = imageData.width * sprite.y * 4 15 | 16 | // for each line of pixels 17 | for ( 18 | let i = initIVal; 19 | i < initIVal + sprite.h * imageData.width * 4; 20 | // (increments by 8 because it skips every other pixel due to pixel density) 21 | i += imageData.width * 8 22 | ) { 23 | const line = [] 24 | const initJVal = i + sprite.x * 4 25 | // for each pixel in the line 26 | // (increments by 8 because it skips every other pixel due to pixel density) 27 | for (let j = initJVal; j < initJVal + sprite.w * 4; j += 8) { 28 | // 0 for transparent, 1 for not 29 | line.push(imageData.data[j + 3] === 0 ? 0 : 1) 30 | } 31 | 32 | lines.push(line) 33 | } 34 | 35 | cache.set(name, lines) 36 | return lines 37 | } 38 | 39 | export default class Actor { 40 | constructor(imageData) { 41 | this._sprite = null 42 | this.height = 0 43 | this.width = 0 44 | this.x = 0 45 | this.y = 0 46 | 47 | // the spriteImage should only be passed into actors that will 48 | // use hit detection; otherwise don't waste cpu on generating 49 | // the alpha map every time the sprite is set 50 | if (imageData) { 51 | this.imageData = imageData 52 | this.alphaMap = [] 53 | } 54 | } 55 | 56 | set sprite(name) { 57 | this._sprite = name 58 | this.height = sprites[name].h / 2 59 | this.width = sprites[name].w / 2 60 | 61 | if (this.imageData) { 62 | this.alphaMap = getSpriteAlphaMap(this.imageData, name) 63 | } 64 | } 65 | 66 | get sprite() { 67 | return this._sprite 68 | } 69 | 70 | // the x value of the right side of it 71 | get rightX() { 72 | return this.width + this.x 73 | } 74 | 75 | // the y value of the bottom of it 76 | get bottomY() { 77 | return this.height + this.y 78 | } 79 | 80 | hits(actors) { 81 | return actors.some((actor) => { 82 | if (!actor) return false 83 | 84 | if (this.x >= actor.rightX || actor.x >= this.rightX) { 85 | return false 86 | } 87 | 88 | if (this.y >= actor.bottomY || actor.y >= this.bottomY) { 89 | return false 90 | } 91 | 92 | // actors' coords are intersecting, but they still might not be hitting 93 | // each other if they intersect at transparent pixels 94 | if (this.alphaMap && actor.alphaMap) { 95 | const startY = Math.round(Math.max(this.y, actor.y)) 96 | const endY = Math.round(Math.min(this.bottomY, actor.bottomY)) 97 | const startX = Math.round(Math.max(this.x, actor.x)) 98 | const endX = Math.round(Math.min(this.rightX, actor.rightX)) 99 | const thisY = Math.round(this.y) 100 | const actorY = Math.round(actor.y) 101 | const thisX = Math.round(this.x) 102 | const actorX = Math.round(actor.x) 103 | 104 | for (let y = startY; y < endY; y++) { 105 | for (let x = startX; x < endX; x++) { 106 | // doesn't hit if either are transparent at these coords 107 | if (this.alphaMap[y - thisY][x - thisX] === 0) continue 108 | if (actor.alphaMap[y - actorY][x - actorX] === 0) continue 109 | 110 | return true 111 | } 112 | } 113 | 114 | return false 115 | } 116 | 117 | return true 118 | }) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /lib/actors/Bird.js: -------------------------------------------------------------------------------- 1 | import sprites from '../sprites.js' 2 | import Actor from './Actor.js' 3 | 4 | export default class Bird extends Actor { 5 | static maxBirdHeight = Math.max(sprites.birdUp.h, sprites.birdDown.h) / 2 6 | 7 | // pixels that are added/removed to `y` when switching between wings up and wings down 8 | static wingSpriteYShift = 6 9 | 10 | constructor(imageData) { 11 | super(imageData) 12 | this.wingFrames = 0 13 | this.wingDirection = 'Up' 14 | this.sprite = `bird${this.wingDirection}` 15 | // these are dynamically set by the game 16 | this.x = null 17 | this.y = null 18 | this.speed = null 19 | this.wingsRate = null 20 | } 21 | 22 | nextFrame() { 23 | this.x -= this.speed 24 | this.determineSprite() 25 | } 26 | 27 | determineSprite() { 28 | const oldHeight = this.height 29 | 30 | if (this.wingFrames >= this.wingsRate) { 31 | this.wingDirection = this.wingDirection === 'Up' ? 'Down' : 'Up' 32 | this.wingFrames = 0 33 | } 34 | 35 | this.sprite = `bird${this.wingDirection}` 36 | this.wingFrames++ 37 | 38 | // if we're switching sprites, y needs to be 39 | // updated for the height difference 40 | if (this.height !== oldHeight) { 41 | let adjustment = Bird.wingSpriteYShift 42 | if (this.wingDirection === 'Up') { 43 | adjustment *= -1 44 | } 45 | 46 | this.y += adjustment 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/actors/Cactus.js: -------------------------------------------------------------------------------- 1 | import { randItem } from '../utils.js' 2 | import Actor from './Actor.js' 3 | 4 | const VARIANTS = ['cactus', 'cactusDouble', 'cactusDoubleB', 'cactusTriple'] 5 | 6 | export default class Cactus extends Actor { 7 | constructor(imageData) { 8 | super(imageData) 9 | this.sprite = randItem(VARIANTS) 10 | // these are dynamically set by the game 11 | this.speed = null 12 | this.x = null 13 | this.y = null 14 | } 15 | 16 | nextFrame() { 17 | this.x -= this.speed 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/actors/Cloud.js: -------------------------------------------------------------------------------- 1 | import { randInteger } from '../utils.js' 2 | import Actor from './Actor.js' 3 | 4 | export default class Cloud extends Actor { 5 | constructor(canvasWidth) { 6 | super() 7 | this.sprite = 'cloud' 8 | this.speedMod = randInteger(6, 14) / 10 9 | // these are dynamically set by the game 10 | this.speed = null 11 | this.x = null 12 | this.y = null 13 | } 14 | 15 | nextFrame() { 16 | this.x -= this.speed * this.speedMod 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/actors/Dino.js: -------------------------------------------------------------------------------- 1 | import Actor from './Actor.js' 2 | 3 | export default class Dino extends Actor { 4 | constructor(imageData) { 5 | super(imageData) 6 | this.isDucking = false 7 | this.legFrames = 0 8 | this.legShowing = 'Left' 9 | this.sprite = `dino${this.legShowing}Leg` 10 | this.vVelocity = null 11 | this.baseY = 0 12 | this.relativeY = 0 13 | // these are dynamically set by the game 14 | this.legsRate = null 15 | this.lift = null 16 | this.gravity = null 17 | } 18 | 19 | get y() { 20 | return this.baseY - this.height + this.relativeY 21 | } 22 | 23 | set y(value) { 24 | this.baseY = value 25 | } 26 | 27 | reset() { 28 | this.isDucking = false 29 | this.legFrames = 0 30 | this.legShowing = 'Left' 31 | this.sprite = `dino${this.legShowing}Leg` 32 | this.vVelocity = null 33 | this.relativeY = 0 34 | } 35 | 36 | jump() { 37 | if (this.relativeY === 0) { 38 | this.vVelocity = -this.lift 39 | return true 40 | } 41 | return false 42 | } 43 | 44 | duck(value) { 45 | this.isDucking = Boolean(value) 46 | } 47 | 48 | nextFrame() { 49 | if (this.vVelocity !== null) { 50 | // use gravity to gradually decrease vVelocity 51 | this.vVelocity += this.gravity 52 | this.relativeY += this.vVelocity 53 | } 54 | 55 | // stop falling once back down to the ground 56 | if (this.relativeY > 0) { 57 | this.vVelocity = null 58 | this.relativeY = 0 59 | } 60 | 61 | this.determineSprite() 62 | } 63 | 64 | determineSprite() { 65 | if (this.relativeY < 0) { 66 | // in the air stiff 67 | this.sprite = 'dino' 68 | } else { 69 | // on the ground running 70 | if (this.legFrames >= this.legsRate) { 71 | this.legShowing = this.legShowing === 'Left' ? 'Right' : 'Left' 72 | this.legFrames = 0 73 | } 74 | 75 | if (this.isDucking) { 76 | this.sprite = `dinoDuck${this.legShowing}Leg` 77 | } else { 78 | this.sprite = `dino${this.legShowing}Leg` 79 | } 80 | 81 | this.legFrames++ 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/game/DinoGame.js: -------------------------------------------------------------------------------- 1 | import Bird from '../actors/Bird.js' 2 | import Cactus from '../actors/Cactus.js' 3 | import Cloud from '../actors/Cloud.js' 4 | import Dino from '../actors/Dino.js' 5 | import sprites from '../sprites.js' 6 | import { playSound } from '../sounds.js' 7 | import { 8 | loadFont, 9 | loadImage, 10 | getImageData, 11 | randBoolean, 12 | randInteger, 13 | } from '../utils.js' 14 | import GameRunner from './GameRunner.js' 15 | 16 | export default class DinoGame extends GameRunner { 17 | constructor(width, height) { 18 | super() 19 | 20 | this.width = null 21 | this.height = null 22 | this.canvas = this.createCanvas(width, height) 23 | this.canvasCtx = this.canvas.getContext('2d') 24 | this.spriteImage = null 25 | this.spriteImageData = null 26 | 27 | /* 28 | * units 29 | * fpa: frames per action 30 | * ppf: pixels per frame 31 | * px: pixels 32 | */ 33 | this.defaultSettings = { 34 | bgSpeed: 8, // ppf 35 | birdSpeed: 7.2, // ppf 36 | birdSpawnRate: 240, // fpa 37 | birdWingsRate: 15, // fpa 38 | cactiSpawnRate: 50, // fpa 39 | cloudSpawnRate: 200, // fpa 40 | cloudSpeed: 2, // ppf 41 | dinoGravity: 0.5, // ppf 42 | dinoGroundOffset: 4, // px 43 | dinoLegsRate: 6, // fpa 44 | dinoLift: 10, // ppf 45 | scoreBlinkRate: 20, // fpa 46 | scoreIncreaseRate: 6, // fpa 47 | } 48 | 49 | this.state = { 50 | settings: { ...this.defaultSettings }, 51 | birds: [], 52 | cacti: [], 53 | clouds: [], 54 | dino: null, 55 | gameOver: false, 56 | groundX: 0, 57 | groundY: 0, 58 | isRunning: false, 59 | level: 0, 60 | score: { 61 | blinkFrames: 0, 62 | blinks: 0, 63 | isBlinking: false, 64 | value: 0, 65 | }, 66 | } 67 | } 68 | 69 | // ref for canvas pixel density: 70 | // https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio#correcting_resolution_in_a_%3Ccanvas%3E 71 | createCanvas(width, height) { 72 | const canvas = document.createElement('canvas') 73 | const ctx = canvas.getContext('2d') 74 | const scale = window.devicePixelRatio 75 | 76 | this.width = width 77 | this.height = height 78 | canvas.style.width = width + 'px' 79 | canvas.style.height = height + 'px' 80 | canvas.width = Math.floor(width * scale) 81 | canvas.height = Math.floor(height * scale) 82 | ctx.scale(scale, scale) 83 | 84 | document.body.appendChild(canvas) 85 | return canvas 86 | } 87 | 88 | async preload() { 89 | const { settings } = this.state 90 | const [spriteImage] = await Promise.all([ 91 | loadImage('./assets/sprite.png'), 92 | loadFont('./assets/PressStart2P-Regular.ttf', 'PressStart2P'), 93 | ]) 94 | this.spriteImage = spriteImage 95 | this.spriteImageData = getImageData(spriteImage) 96 | const dino = new Dino(this.spriteImageData) 97 | 98 | dino.legsRate = settings.dinoLegsRate 99 | dino.lift = settings.dinoLift 100 | dino.gravity = settings.dinoGravity 101 | dino.x = 25 102 | dino.baseY = this.height - settings.dinoGroundOffset 103 | this.state.dino = dino 104 | this.state.groundY = this.height - sprites.ground.h / 2 105 | } 106 | 107 | onFrame() { 108 | const { state } = this 109 | 110 | this.drawBackground() 111 | // this.drawFPS() 112 | this.drawGround() 113 | this.drawClouds() 114 | this.drawDino() 115 | this.drawScore() 116 | 117 | if (state.isRunning) { 118 | this.drawCacti() 119 | 120 | if (state.level > 3) { 121 | this.drawBirds() 122 | } 123 | 124 | if (state.dino.hits([state.cacti[0], state.birds[0]])) { 125 | playSound('game-over') 126 | state.gameOver = true 127 | } 128 | 129 | if (state.gameOver) { 130 | this.endGame() 131 | } else { 132 | this.updateScore() 133 | } 134 | } 135 | } 136 | 137 | onInput(type) { 138 | const { state } = this 139 | 140 | switch (type) { 141 | case 'jump': { 142 | if (state.isRunning) { 143 | if (state.dino.jump()) { 144 | playSound('jump') 145 | } 146 | } else { 147 | this.resetGame() 148 | state.dino.jump() 149 | playSound('jump') 150 | } 151 | break 152 | } 153 | 154 | case 'duck': { 155 | if (state.isRunning) { 156 | state.dino.duck(true) 157 | } 158 | break 159 | } 160 | 161 | case 'stop-duck': { 162 | if (state.isRunning) { 163 | state.dino.duck(false) 164 | } 165 | break 166 | } 167 | } 168 | } 169 | 170 | resetGame() { 171 | this.state.dino.reset() 172 | Object.assign(this.state, { 173 | settings: { ...this.defaultSettings }, 174 | birds: [], 175 | cacti: [], 176 | gameOver: false, 177 | isRunning: true, 178 | level: 0, 179 | score: { 180 | blinkFrames: 0, 181 | blinks: 0, 182 | isBlinking: false, 183 | value: 0, 184 | }, 185 | }) 186 | 187 | this.start() 188 | } 189 | 190 | endGame() { 191 | const iconSprite = sprites.replayIcon 192 | const padding = 15 193 | 194 | this.paintText( 195 | 'G A M E O V E R', 196 | this.width / 2, 197 | this.height / 2 - padding, 198 | { 199 | font: 'PressStart2P', 200 | size: '12px', 201 | align: 'center', 202 | baseline: 'bottom', 203 | color: '#535353', 204 | } 205 | ) 206 | 207 | this.paintSprite( 208 | 'replayIcon', 209 | this.width / 2 - iconSprite.w / 4, 210 | this.height / 2 - iconSprite.h / 4 + padding 211 | ) 212 | 213 | this.state.isRunning = false 214 | this.drawScore() 215 | this.stop() 216 | } 217 | 218 | increaseDifficulty() { 219 | const { birds, cacti, clouds, dino, settings } = this.state 220 | const { bgSpeed, cactiSpawnRate, dinoLegsRate } = settings 221 | const { level } = this.state 222 | 223 | if (level > 4 && level < 8) { 224 | settings.bgSpeed++ 225 | settings.birdSpeed = settings.bgSpeed * 0.8 226 | } else if (level > 7) { 227 | settings.bgSpeed = Math.ceil(bgSpeed * 1.1) 228 | settings.birdSpeed = settings.bgSpeed * 0.9 229 | settings.cactiSpawnRate = Math.floor(cactiSpawnRate * 0.98) 230 | 231 | if (level > 7 && level % 2 === 0 && dinoLegsRate > 3) { 232 | settings.dinoLegsRate-- 233 | } 234 | } 235 | 236 | for (const bird of birds) { 237 | bird.speed = settings.birdSpeed 238 | } 239 | 240 | for (const cactus of cacti) { 241 | cactus.speed = settings.bgSpeed 242 | } 243 | 244 | for (const cloud of clouds) { 245 | cloud.speed = settings.bgSpeed 246 | } 247 | 248 | dino.legsRate = settings.dinoLegsRate 249 | } 250 | 251 | updateScore() { 252 | const { state } = this 253 | 254 | if (this.frameCount % state.settings.scoreIncreaseRate === 0) { 255 | const oldLevel = state.level 256 | 257 | state.score.value++ 258 | state.level = Math.floor(state.score.value / 100) 259 | 260 | if (state.level !== oldLevel) { 261 | playSound('level-up') 262 | this.increaseDifficulty() 263 | state.score.isBlinking = true 264 | } 265 | } 266 | } 267 | 268 | drawFPS() { 269 | this.paintText('fps: ' + Math.round(this.frameRate), 0, 0, { 270 | font: 'PressStart2P', 271 | size: '12px', 272 | baseline: 'top', 273 | align: 'left', 274 | color: '#535353', 275 | }) 276 | } 277 | 278 | drawBackground() { 279 | this.canvasCtx.fillStyle = '#f7f7f7' 280 | this.canvasCtx.fillRect(0, 0, this.width, this.height) 281 | } 282 | 283 | drawGround() { 284 | const { state } = this 285 | const { bgSpeed } = state.settings 286 | const groundImgWidth = sprites.ground.w / 2 287 | 288 | this.paintSprite('ground', state.groundX, state.groundY) 289 | state.groundX -= bgSpeed 290 | 291 | // append second image until first is fully translated 292 | if (state.groundX <= -groundImgWidth + this.width) { 293 | this.paintSprite('ground', state.groundX + groundImgWidth, state.groundY) 294 | 295 | if (state.groundX <= -groundImgWidth) { 296 | state.groundX = -bgSpeed 297 | } 298 | } 299 | } 300 | 301 | drawClouds() { 302 | const { clouds, settings } = this.state 303 | 304 | this.progressInstances(clouds) 305 | if (this.frameCount % settings.cloudSpawnRate === 0) { 306 | const newCloud = new Cloud() 307 | newCloud.speed = settings.bgSpeed 308 | newCloud.x = this.width 309 | newCloud.y = randInteger(20, 80) 310 | clouds.push(newCloud) 311 | } 312 | this.paintInstances(clouds) 313 | } 314 | 315 | drawDino() { 316 | const { dino } = this.state 317 | 318 | dino.nextFrame() 319 | this.paintSprite(dino.sprite, dino.x, dino.y) 320 | } 321 | 322 | drawCacti() { 323 | const { state } = this 324 | const { cacti, settings } = state 325 | 326 | this.progressInstances(cacti) 327 | if (this.frameCount % settings.cactiSpawnRate === 0) { 328 | // randomly either do or don't add cactus 329 | if (!state.birds.length && randBoolean()) { 330 | const newCacti = new Cactus(this.spriteImageData) 331 | newCacti.speed = settings.bgSpeed 332 | newCacti.x = this.width 333 | newCacti.y = this.height - newCacti.height - 2 334 | cacti.push(newCacti) 335 | } 336 | } 337 | this.paintInstances(cacti) 338 | } 339 | 340 | drawBirds() { 341 | const { birds, settings } = this.state 342 | 343 | this.progressInstances(birds) 344 | if (this.frameCount % settings.birdSpawnRate === 0) { 345 | // randomly either do or don't add bird 346 | if (randBoolean()) { 347 | const newBird = new Bird(this.spriteImageData) 348 | newBird.speed = settings.birdSpeed 349 | newBird.wingsRate = settings.birdWingsRate 350 | newBird.x = this.width 351 | // ensure birds are always at least 5px higher than a ducking dino 352 | newBird.y = 353 | this.height - 354 | Bird.maxBirdHeight - 355 | Bird.wingSpriteYShift - 356 | 5 - 357 | sprites.dinoDuckLeftLeg.h / 2 - 358 | settings.dinoGroundOffset 359 | birds.push(newBird) 360 | } 361 | } 362 | this.paintInstances(birds) 363 | } 364 | 365 | drawScore() { 366 | const { canvasCtx, state } = this 367 | const { isRunning, score, settings } = state 368 | const fontSize = 12 369 | let shouldDraw = true 370 | let drawValue = score.value 371 | 372 | if (isRunning && score.isBlinking) { 373 | score.blinkFrames++ 374 | 375 | if (score.blinkFrames % settings.scoreBlinkRate === 0) { 376 | score.blinks++ 377 | } 378 | 379 | if (score.blinks > 7) { 380 | score.blinkFrames = 0 381 | score.blinks = 0 382 | score.isBlinking = false 383 | } else { 384 | if (score.blinks % 2 === 0) { 385 | drawValue = Math.floor(drawValue / 100) * 100 386 | } else { 387 | shouldDraw = false 388 | } 389 | } 390 | } 391 | 392 | if (shouldDraw) { 393 | // draw the background behind it in case this is called 394 | // at a time where the background isn't re-drawn (i.e. in `endGame`) 395 | canvasCtx.fillStyle = '#f7f7f7' 396 | canvasCtx.fillRect(this.width - fontSize * 5, 0, fontSize * 5, fontSize) 397 | 398 | this.paintText((drawValue + '').padStart(5, '0'), this.width, 0, { 399 | font: 'PressStart2P', 400 | size: `${fontSize}px`, 401 | align: 'right', 402 | baseline: 'top', 403 | color: '#535353', 404 | }) 405 | } 406 | } 407 | 408 | /** 409 | * For each instance in the provided array, calculate the next 410 | * frame and remove any that are no longer visible 411 | * @param {Actor[]} instances 412 | */ 413 | progressInstances(instances) { 414 | for (let i = instances.length - 1; i >= 0; i--) { 415 | const instance = instances[i] 416 | 417 | instance.nextFrame() 418 | if (instance.rightX <= 0) { 419 | // remove if off screen 420 | instances.splice(i, 1) 421 | } 422 | } 423 | } 424 | 425 | /** 426 | * @param {Actor[]} instances 427 | */ 428 | paintInstances(instances) { 429 | for (const instance of instances) { 430 | this.paintSprite(instance.sprite, instance.x, instance.y) 431 | } 432 | } 433 | 434 | paintSprite(spriteName, dx, dy) { 435 | const { h, w, x, y } = sprites[spriteName] 436 | this.canvasCtx.drawImage(this.spriteImage, x, y, w, h, dx, dy, w / 2, h / 2) 437 | } 438 | 439 | paintText(text, x, y, opts) { 440 | const { font = 'serif', size = '12px' } = opts 441 | const { canvasCtx } = this 442 | 443 | canvasCtx.font = `${size} ${font}` 444 | if (opts.align) canvasCtx.textAlign = opts.align 445 | if (opts.baseline) canvasCtx.textBaseline = opts.baseline 446 | if (opts.color) canvasCtx.fillStyle = opts.color 447 | canvasCtx.fillText(text, x, y) 448 | } 449 | } 450 | -------------------------------------------------------------------------------- /lib/game/GameRunner.js: -------------------------------------------------------------------------------- 1 | export default class GameRunner { 2 | constructor() { 3 | this.looping = false 4 | this.preloaded = false 5 | this.targetFrameRate = 60 6 | this.frameCount = 0 7 | this.frameRate = 0 8 | this.paused = false 9 | this.stepFrames = null 10 | this._lastFrameTime = window.performance.now() 11 | 12 | // store this bound function so we don't have to create 13 | // one every single time we call requestAnimationFrame 14 | this.__loop = this._loop.bind(this) 15 | } 16 | 17 | async start(paused = false) { 18 | if (!this.preloaded) { 19 | if (this.preload) { 20 | await this.preload() 21 | } 22 | this.preloaded = true 23 | } 24 | 25 | if (paused) { 26 | this.paused = paused 27 | } 28 | 29 | this.looping = true 30 | 31 | if (!paused) { 32 | window.requestAnimationFrame(this.__loop) 33 | } 34 | } 35 | 36 | stop() { 37 | this.looping = false 38 | } 39 | 40 | pause() { 41 | this.paused = true 42 | } 43 | 44 | unpause() { 45 | this.paused = false 46 | } 47 | 48 | step(frames = 1) { 49 | if (typeof this.stepFrames === 'number') { 50 | this.stepFrames += frames 51 | } else { 52 | this.stepFrames = frames 53 | } 54 | 55 | this.__loop(window.performance.now()) 56 | } 57 | 58 | _loop(timestamp) { 59 | const now = window.performance.now() 60 | const timeSinceLast = now - this._lastFrameTime 61 | const targetTimeBetweenFrames = 1000 / this.targetFrameRate 62 | 63 | if (timeSinceLast >= targetTimeBetweenFrames - 5) { 64 | this.onFrame() 65 | this.frameRate = 1000 / (now - this._lastFrameTime) 66 | this._lastFrameTime = now 67 | this.frameCount++ 68 | } 69 | 70 | if (this.looping) { 71 | let shouldLoop = true 72 | 73 | if (this.paused) { 74 | if (typeof this.stepFrames === 'number') { 75 | if (this.stepFrames === 0) { 76 | this.stepFrames = null 77 | shouldLoop = false 78 | } else { 79 | this.stepFrames-- 80 | } 81 | } else { 82 | shouldLoop = false 83 | } 84 | } 85 | 86 | if (shouldLoop) { 87 | window.requestAnimationFrame(this.__loop) 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | import DinoGame from './game/DinoGame.js' 2 | 3 | const game = new DinoGame(600, 150) 4 | const isTouchDevice = 5 | 'ontouchstart' in window || 6 | navigator.maxTouchPoints > 0 || 7 | navigator.msMaxTouchPoints > 0 8 | 9 | if (isTouchDevice) { 10 | document.addEventListener('touchstart', ({ touches }) => { 11 | if (touches.length === 1) { 12 | game.onInput('jump') 13 | } else if (touches.length === 2) { 14 | game.onInput('duck') 15 | } 16 | }) 17 | 18 | document.addEventListener('touchend', ({ touches }) => { 19 | game.onInput('stop-duck') 20 | }) 21 | } else { 22 | const keycodes = { 23 | // up, spacebar 24 | JUMP: { 38: 1, 32: 1 }, 25 | // down 26 | DUCK: { 40: 1 }, 27 | } 28 | 29 | document.addEventListener('keydown', ({ keyCode }) => { 30 | if (keycodes.JUMP[keyCode]) { 31 | game.onInput('jump') 32 | } else if (keycodes.DUCK[keyCode]) { 33 | game.onInput('duck') 34 | } 35 | }) 36 | 37 | document.addEventListener('keyup', ({ keyCode }) => { 38 | if (keycodes.DUCK[keyCode]) { 39 | game.onInput('stop-duck') 40 | } 41 | }) 42 | } 43 | 44 | game.start().catch(console.error) 45 | -------------------------------------------------------------------------------- /lib/sounds.js: -------------------------------------------------------------------------------- 1 | const AudioContext = window.AudioContext || window.webkitAudioContext 2 | const audioContext = new AudioContext() 3 | const soundNames = ['game-over', 'jump', 'level-up'] 4 | const soundBuffers = {} 5 | let SOUNDS_LOADED = false 6 | 7 | loadSounds().catch(console.error) 8 | export function playSound(name) { 9 | if (SOUNDS_LOADED) { 10 | audioContext.resume() 11 | playBuffer(soundBuffers[name]) 12 | } 13 | } 14 | 15 | async function loadSounds() { 16 | await Promise.all( 17 | soundNames.map(async (soundName) => { 18 | soundBuffers[soundName] = await loadBuffer(`./assets/${soundName}.mp3`) 19 | }) 20 | ) 21 | 22 | SOUNDS_LOADED = true 23 | } 24 | 25 | function loadBuffer(filepath) { 26 | return new Promise((resolve, reject) => { 27 | const request = new XMLHttpRequest() 28 | 29 | request.open('GET', filepath) 30 | request.responseType = 'arraybuffer' 31 | request.onload = () => 32 | audioContext.decodeAudioData(request.response, resolve) 33 | request.onerror = reject 34 | request.send() 35 | }) 36 | } 37 | 38 | function playBuffer(buffer) { 39 | const source = audioContext.createBufferSource() 40 | 41 | source.buffer = buffer 42 | source.connect(audioContext.destination) 43 | source.start() 44 | } 45 | -------------------------------------------------------------------------------- /lib/sprites.js: -------------------------------------------------------------------------------- 1 | export default { 2 | birdUp: { h: 52, w: 84, x: 708, y: 31 }, 3 | birdDown: { h: 60, w: 84, x: 708, y: 85 }, 4 | cactus: { h: 92, w: 46, x: 70, y: 31 }, 5 | cactusDouble: { h: 66, w: 64, x: 118, y: 31 }, 6 | cactusDoubleB: { h: 92, w: 80, x: 184, y: 31 }, 7 | cactusTriple: { h: 66, w: 82, x: 266, y: 31 }, 8 | cloud: { h: 28, w: 92, x: 794, y: 31 }, 9 | dino: { h: 86, w: 80, x: 350, y: 31 }, 10 | dinoDuckLeftLeg: { h: 52, w: 110, x: 596, y: 31 }, 11 | dinoDuckRightLeg: { h: 52, w: 110, x: 596, y: 85 }, 12 | dinoLeftLeg: { h: 86, w: 80, x: 432, y: 31 }, 13 | dinoRightLeg: { h: 86, w: 80, x: 514, y: 31 }, 14 | ground: { h: 28, w: 2400, x: 0, y: 2 }, 15 | replayIcon: { h: 60, w: 68, x: 0, y: 31 }, 16 | } 17 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | export function getImageData(image) { 2 | const { width, height } = image 3 | const tmpCanvas = document.createElement('canvas') 4 | const ctx = tmpCanvas.getContext('2d') 5 | let result 6 | 7 | tmpCanvas.width = width 8 | tmpCanvas.height = height 9 | ctx.drawImage(image, 0, 0) 10 | 11 | result = ctx.getImageData(0, 0, width, height) 12 | tmpCanvas.remove() 13 | return result 14 | } 15 | 16 | export async function loadImage(url) { 17 | return new Promise((resolve, reject) => { 18 | const image = new Image() 19 | 20 | image.onload = () => resolve(image) 21 | image.onerror = reject 22 | image.src = url 23 | }) 24 | } 25 | 26 | function getFontName(url) { 27 | const ext = url.slice(url.lastIndexOf('.')) 28 | const pathParts = url.split('/') 29 | 30 | return pathParts[pathParts.length - 1].slice(0, -1 * ext.length) 31 | } 32 | 33 | export async function loadFont(url, fontName) { 34 | if (!fontName) fontName = getFontName(url) 35 | const styleEl = document.createElement('style') 36 | 37 | styleEl.innerHTML = ` 38 | @font-face { 39 | font-family: ${fontName}; 40 | src: url(${url}); 41 | } 42 | ` 43 | document.head.appendChild(styleEl) 44 | await document.fonts.load(`12px ${fontName}`) 45 | } 46 | 47 | export function randInteger(min, max) { 48 | return Math.floor(Math.random() * (max - min + 1)) + min 49 | } 50 | 51 | export function randBoolean() { 52 | return Boolean(randInteger(0, 1)) 53 | } 54 | 55 | export function randItem(arr) { 56 | return arr[randInteger(0, arr.length - 1)] 57 | } 58 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Chris Deacy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "lint": "prettier --check .", 5 | "lint-fix": "prettier --write .", 6 | "start": "reload" 7 | }, 8 | "devDependencies": { 9 | "prettier": "^2.6.2", 10 | "reload": "^3.2.0" 11 | }, 12 | "volta": { 13 | "node": "16.15.0", 14 | "yarn": "1.22.18" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | chrome-dino 4 |
5 |
6 |

7 | 8 | > Remaking the chrome offline dinosaur game 9 | 10 | Note that this was previously written with p5.js but has since been refactored into a dependency-free approach. You can still see the old p5.js implementation in [the p5js branch](https://github.com/chrisdothtml/chrome-dino/tree/p5js). 11 | 12 | ## Run locally (literally) 13 | 14 | I use [volta](https://volta.sh/) to automatically install/switch my node and yarn versions. Either install volta, or check the versions I'm using via the [package.json](package.json) `volta` field. 15 | 16 | ```bash 17 | # install deps 18 | $ yarn 19 | # start server 20 | $ yarn start 21 | ``` 22 | 23 | ## License 24 | 25 | [MIT](license) 26 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | cli-color@~2.0.0: 6 | version "2.0.2" 7 | resolved "https://registry.yarnpkg.com/cli-color/-/cli-color-2.0.2.tgz#e295addbae470800def0254183c648531cdf4e3f" 8 | integrity sha512-g4JYjrTW9MGtCziFNjkqp3IMpGhnJyeB0lOtRPjQkYhXzKYr6tYnXKyEVnMzITxhpbahsEW9KsxOYIDKwcsIBw== 9 | dependencies: 10 | d "^1.0.1" 11 | es5-ext "^0.10.59" 12 | es6-iterator "^2.0.3" 13 | memoizee "^0.4.15" 14 | timers-ext "^0.1.7" 15 | 16 | commander@~7.2.0: 17 | version "7.2.0" 18 | resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" 19 | integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== 20 | 21 | d@1, d@^1.0.1: 22 | version "1.0.1" 23 | resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" 24 | integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA== 25 | dependencies: 26 | es5-ext "^0.10.50" 27 | type "^1.0.1" 28 | 29 | debug@2.6.9: 30 | version "2.6.9" 31 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 32 | dependencies: 33 | ms "2.0.0" 34 | 35 | define-lazy-prop@^2.0.0: 36 | version "2.0.0" 37 | resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" 38 | integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== 39 | 40 | depd@~1.1.2: 41 | version "1.1.2" 42 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" 43 | integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= 44 | 45 | destroy@~1.0.4: 46 | version "1.0.4" 47 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" 48 | integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= 49 | 50 | ee-first@1.1.1: 51 | version "1.1.1" 52 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 53 | integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= 54 | 55 | encodeurl@~1.0.2: 56 | version "1.0.2" 57 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" 58 | integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= 59 | 60 | es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.50, es5-ext@^0.10.53, es5-ext@^0.10.59, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46: 61 | version "0.10.61" 62 | resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.61.tgz#311de37949ef86b6b0dcea894d1ffedb909d3269" 63 | integrity sha512-yFhIqQAzu2Ca2I4SE2Au3rxVfmohU9Y7wqGR+s7+H7krk26NXhIRAZDgqd6xqjCEFUomDEA3/Bo/7fKmIkW1kA== 64 | dependencies: 65 | es6-iterator "^2.0.3" 66 | es6-symbol "^3.1.3" 67 | next-tick "^1.1.0" 68 | 69 | es6-iterator@^2.0.3: 70 | version "2.0.3" 71 | resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" 72 | integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c= 73 | dependencies: 74 | d "1" 75 | es5-ext "^0.10.35" 76 | es6-symbol "^3.1.1" 77 | 78 | es6-symbol@^3.1.1, es6-symbol@^3.1.3: 79 | version "3.1.3" 80 | resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18" 81 | integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA== 82 | dependencies: 83 | d "^1.0.1" 84 | ext "^1.1.2" 85 | 86 | es6-weak-map@^2.0.3: 87 | version "2.0.3" 88 | resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.3.tgz#b6da1f16cc2cc0d9be43e6bdbfc5e7dfcdf31d53" 89 | integrity sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA== 90 | dependencies: 91 | d "1" 92 | es5-ext "^0.10.46" 93 | es6-iterator "^2.0.3" 94 | es6-symbol "^3.1.1" 95 | 96 | escape-html@~1.0.3: 97 | version "1.0.3" 98 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 99 | integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= 100 | 101 | etag@~1.8.1: 102 | version "1.8.1" 103 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" 104 | integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= 105 | 106 | event-emitter@^0.3.5: 107 | version "0.3.5" 108 | resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" 109 | integrity sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk= 110 | dependencies: 111 | d "1" 112 | es5-ext "~0.10.14" 113 | 114 | ext@^1.1.2: 115 | version "1.6.0" 116 | resolved "https://registry.yarnpkg.com/ext/-/ext-1.6.0.tgz#3871d50641e874cc172e2b53f919842d19db4c52" 117 | integrity sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg== 118 | dependencies: 119 | type "^2.5.0" 120 | 121 | finalhandler@~1.1.1: 122 | version "1.1.2" 123 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" 124 | integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== 125 | dependencies: 126 | debug "2.6.9" 127 | encodeurl "~1.0.2" 128 | escape-html "~1.0.3" 129 | on-finished "~2.3.0" 130 | parseurl "~1.3.3" 131 | statuses "~1.5.0" 132 | unpipe "~1.0.0" 133 | 134 | fresh@0.5.2: 135 | version "0.5.2" 136 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" 137 | integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= 138 | 139 | http-errors@1.8.1: 140 | version "1.8.1" 141 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c" 142 | integrity sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g== 143 | dependencies: 144 | depd "~1.1.2" 145 | inherits "2.0.4" 146 | setprototypeof "1.2.0" 147 | statuses ">= 1.5.0 < 2" 148 | toidentifier "1.0.1" 149 | 150 | inherits@2.0.4: 151 | version "2.0.4" 152 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 153 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 154 | 155 | is-docker@^2.0.0, is-docker@^2.1.1: 156 | version "2.2.1" 157 | resolved "https://unpm.uberinternal.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" 158 | integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== 159 | 160 | is-promise@^2.2.2: 161 | version "2.2.2" 162 | resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" 163 | integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== 164 | 165 | is-wsl@^2.2.0: 166 | version "2.2.0" 167 | resolved "https://unpm.uberinternal.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" 168 | integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== 169 | dependencies: 170 | is-docker "^2.0.0" 171 | 172 | lru-queue@^0.1.0: 173 | version "0.1.0" 174 | resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3" 175 | integrity sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM= 176 | dependencies: 177 | es5-ext "~0.10.2" 178 | 179 | memoizee@^0.4.15: 180 | version "0.4.15" 181 | resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.15.tgz#e6f3d2da863f318d02225391829a6c5956555b72" 182 | integrity sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ== 183 | dependencies: 184 | d "^1.0.1" 185 | es5-ext "^0.10.53" 186 | es6-weak-map "^2.0.3" 187 | event-emitter "^0.3.5" 188 | is-promise "^2.2.2" 189 | lru-queue "^0.1.0" 190 | next-tick "^1.1.0" 191 | timers-ext "^0.1.7" 192 | 193 | mime@1.6.0: 194 | version "1.6.0" 195 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" 196 | integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== 197 | 198 | minimist@~1.2.0: 199 | version "1.2.6" 200 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" 201 | integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== 202 | 203 | ms@2.0.0: 204 | version "2.0.0" 205 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 206 | 207 | ms@2.1.3: 208 | version "2.1.3" 209 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" 210 | integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== 211 | 212 | next-tick@1, next-tick@^1.1.0: 213 | version "1.1.0" 214 | resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" 215 | integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== 216 | 217 | on-finished@~2.3.0: 218 | version "2.3.0" 219 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" 220 | integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= 221 | dependencies: 222 | ee-first "1.1.1" 223 | 224 | open@^8.0.0: 225 | version "8.4.0" 226 | resolved "https://registry.yarnpkg.com/open/-/open-8.4.0.tgz#345321ae18f8138f82565a910fdc6b39e8c244f8" 227 | integrity sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q== 228 | dependencies: 229 | define-lazy-prop "^2.0.0" 230 | is-docker "^2.1.1" 231 | is-wsl "^2.2.0" 232 | 233 | parseurl@~1.3.3: 234 | version "1.3.3" 235 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" 236 | integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== 237 | 238 | prettier@^2.6.2: 239 | version "2.6.2" 240 | resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.6.2.tgz#e26d71a18a74c3d0f0597f55f01fb6c06c206032" 241 | integrity sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew== 242 | 243 | querystringify@^2.1.1: 244 | version "2.2.0" 245 | resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" 246 | integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== 247 | 248 | range-parser@~1.2.1: 249 | version "1.2.1" 250 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" 251 | integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== 252 | 253 | reload@^3.2.0: 254 | version "3.2.0" 255 | resolved "https://registry.yarnpkg.com/reload/-/reload-3.2.0.tgz#616239a2f909b8f605a53396c7ab56cae984b8e3" 256 | integrity sha512-30iJoDvFHGbfq6tT3Vag/4RV3wkpuCOqPSM3GyeuOSSo48wKfZT/iI19oeO0GCVX0XSr+44XJ6yBiRJWqOq+sw== 257 | dependencies: 258 | cli-color "~2.0.0" 259 | commander "~7.2.0" 260 | finalhandler "~1.1.1" 261 | minimist "~1.2.0" 262 | open "^8.0.0" 263 | serve-static "~1.14.0" 264 | supervisor "~0.12.0" 265 | url-parse "~1.5.0" 266 | ws "~7.4.0" 267 | 268 | requires-port@^1.0.0: 269 | version "1.0.0" 270 | resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" 271 | integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= 272 | 273 | send@0.17.2: 274 | version "0.17.2" 275 | resolved "https://registry.yarnpkg.com/send/-/send-0.17.2.tgz#926622f76601c41808012c8bf1688fe3906f7820" 276 | integrity sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww== 277 | dependencies: 278 | debug "2.6.9" 279 | depd "~1.1.2" 280 | destroy "~1.0.4" 281 | encodeurl "~1.0.2" 282 | escape-html "~1.0.3" 283 | etag "~1.8.1" 284 | fresh "0.5.2" 285 | http-errors "1.8.1" 286 | mime "1.6.0" 287 | ms "2.1.3" 288 | on-finished "~2.3.0" 289 | range-parser "~1.2.1" 290 | statuses "~1.5.0" 291 | 292 | serve-static@~1.14.0: 293 | version "1.14.2" 294 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.2.tgz#722d6294b1d62626d41b43a013ece4598d292bfa" 295 | integrity sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ== 296 | dependencies: 297 | encodeurl "~1.0.2" 298 | escape-html "~1.0.3" 299 | parseurl "~1.3.3" 300 | send "0.17.2" 301 | 302 | setprototypeof@1.2.0: 303 | version "1.2.0" 304 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" 305 | integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== 306 | 307 | "statuses@>= 1.5.0 < 2", statuses@~1.5.0: 308 | version "1.5.0" 309 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" 310 | integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= 311 | 312 | supervisor@~0.12.0: 313 | version "0.12.0" 314 | resolved "https://registry.yarnpkg.com/supervisor/-/supervisor-0.12.0.tgz#de7e6337015b291851c10f3538c4a7f04917ecc1" 315 | integrity sha1-3n5jNwFbKRhRwQ81OMSn8EkX7ME= 316 | 317 | timers-ext@^0.1.7: 318 | version "0.1.7" 319 | resolved "https://registry.yarnpkg.com/timers-ext/-/timers-ext-0.1.7.tgz#6f57ad8578e07a3fb9f91d9387d65647555e25c6" 320 | integrity sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ== 321 | dependencies: 322 | es5-ext "~0.10.46" 323 | next-tick "1" 324 | 325 | toidentifier@1.0.1: 326 | version "1.0.1" 327 | resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" 328 | integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== 329 | 330 | type@^1.0.1: 331 | version "1.2.0" 332 | resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" 333 | integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== 334 | 335 | type@^2.5.0: 336 | version "2.6.0" 337 | resolved "https://registry.yarnpkg.com/type/-/type-2.6.0.tgz#3ca6099af5981d36ca86b78442973694278a219f" 338 | integrity sha512-eiDBDOmkih5pMbo9OqsqPRGMljLodLcwd5XD5JbtNB0o89xZAwynY9EdCDsJU7LtcVCClu9DvM7/0Ep1hYX3EQ== 339 | 340 | unpipe@~1.0.0: 341 | version "1.0.0" 342 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 343 | integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= 344 | 345 | url-parse@~1.5.0: 346 | version "1.5.10" 347 | resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" 348 | integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== 349 | dependencies: 350 | querystringify "^2.1.1" 351 | requires-port "^1.0.0" 352 | 353 | ws@~7.4.0: 354 | version "7.4.6" 355 | resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" 356 | integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== 357 | --------------------------------------------------------------------------------