├── .browserslistrc ├── .editorconfig ├── .env.example ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg └── pre-commit ├── .lintstagedrc.js ├── .prettierignore ├── .prettierrc.js ├── .stylelintignore ├── .stylelintrc.js ├── LICENSE ├── README.md ├── assets ├── fonts │ ├── _raw │ │ └── joystix_monospace │ │ │ └── joystix_monospace.littera.ltr │ ├── joystix_monospace.fnt │ └── joystix_monospace.png ├── icons │ ├── _raw │ │ ├── dino-start-icon-blank.png │ │ └── dino-start-icon.png │ └── dino-start-icon.png ├── sounds │ ├── achievement.mp3 │ ├── gameover.mp3 │ └── player-action.mp3 └── sprites │ ├── README.md │ ├── _raw │ ├── TexturePacker │ │ └── dino-atlas.tps │ ├── horizon │ │ ├── cloud │ │ │ └── cloud.png │ │ ├── ground │ │ │ └── ground.png │ │ ├── moon │ │ │ ├── moon-1.png │ │ │ ├── moon-2.png │ │ │ ├── moon-3.png │ │ │ ├── moon-4.png │ │ │ ├── moon-5.png │ │ │ ├── moon-6.png │ │ │ └── moon-7.png │ │ └── star │ │ │ ├── star-1.png │ │ │ ├── star-2.png │ │ │ └── star-3.png │ ├── obstacles │ │ ├── bird │ │ │ ├── bird-1.png │ │ │ └── bird-2.png │ │ └── cactus │ │ │ ├── big │ │ │ ├── cactus-big-1.png │ │ │ ├── cactus-big-2.png │ │ │ └── cactus-big-3.png │ │ │ └── small │ │ │ ├── cactus-small-1.png │ │ │ ├── cactus-small-2.png │ │ │ └── cactus-small-3.png │ ├── player │ │ ├── dead │ │ │ ├── dino-dead-outline.png │ │ │ └── dino-dead.png │ │ ├── duck │ │ │ ├── dino-duck-1.png │ │ │ └── dino-duck-2.png │ │ ├── idle │ │ │ ├── dino-idle-1.png │ │ │ └── dino-idle-2.png │ │ ├── run │ │ │ ├── dino-run-1.png │ │ │ └── dino-run-2.png │ │ └── start │ │ │ └── dino-start.png │ └── ui │ │ └── restart │ │ └── restart.png │ ├── dino-atlas.json │ └── dino-atlas.png ├── babel.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── src ├── Game.js ├── config │ ├── game │ │ ├── events │ │ │ └── index.js │ │ ├── index.js │ │ ├── prefabs │ │ │ ├── bird.js │ │ │ ├── cactus.js │ │ │ ├── cloud.js │ │ │ ├── ground.js │ │ │ ├── index.js │ │ │ ├── moon.js │ │ │ ├── player.js │ │ │ └── star.js │ │ └── scenes │ │ │ ├── boot.js │ │ │ ├── game.js │ │ │ └── index.js │ └── index.js ├── index.css ├── index.html ├── index.js ├── prefabs │ ├── horizon │ │ ├── Horizon.js │ │ ├── HorizonItems.js │ │ ├── NightMode.js │ │ ├── clouds │ │ │ ├── Cloud.js │ │ │ └── Clouds.js │ │ ├── ground │ │ │ └── Ground.js │ │ ├── moon │ │ │ └── Moon.js │ │ ├── obstacles │ │ │ ├── Obstacle.js │ │ │ ├── Obstacles.js │ │ │ ├── bird │ │ │ │ ├── AnimationManager.js │ │ │ │ └── Bird.js │ │ │ └── cactus │ │ │ │ └── Cactus.js │ │ └── stars │ │ │ ├── Star.js │ │ │ └── Stars.js │ ├── player │ │ ├── AnimationManager.js │ │ ├── InputManager.js │ │ ├── PhysicsManager.js │ │ └── Player.js │ └── ui │ │ ├── gameover │ │ └── GameOverPanel.js │ │ └── score │ │ ├── BaseScorePanel.js │ │ ├── CurrentScorePanel.js │ │ ├── HighScorePanel.js │ │ └── ScoreZone.js ├── scenes │ ├── boot │ │ └── BootScene.js │ └── game │ │ ├── GameScene.js │ │ ├── InputManager.js │ │ ├── Intro.js │ │ ├── ResizeManager.js │ │ ├── SoundManager.js │ │ ├── UI.js │ │ └── score │ │ ├── BaseScoreManager.js │ │ ├── LocalScoreManager.js │ │ └── TelegramScoreManager.js └── utils │ ├── fetchTimeout.js │ ├── index.js │ └── telegram │ ├── games.js │ ├── getTokenParam.js │ └── isTelegramMode.js └── webpack ├── common.js ├── dev.js └── prod.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | [development] 2 | last 1 chrome version 3 | last 1 firefox version 4 | last 1 safari version 5 | 6 | [production] 7 | >0.25% 8 | not ie 11 9 | not op_mini all 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # [OPTIONAL] API endpoint for Telegram scoring. Fill in if you intend to run this as Telegram game 2 | API_ENDPOINT="https://domain.example/score" 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | /src/utils/telegram/games.js 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | node: true, 5 | }, 6 | parser: '@babel/eslint-parser', 7 | extends: ['airbnb-base', 'plugin:prettier/recommended'], 8 | rules: { 9 | 'import/no-extraneous-dependencies': ['error', { devDependencies: true }], 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # System and IDE files 2 | Thumbs.db 3 | .DS_Store 4 | .idea 5 | *.suo 6 | *.sublime-project 7 | *.sublime-workspace 8 | 9 | # Env 10 | .env 11 | .env.local 12 | .env.development.local 13 | .env.test.local 14 | .env.production.local 15 | 16 | # Vendors 17 | node_modules/ 18 | 19 | # Build 20 | dist/ 21 | /npm-debug.log 22 | debug.log 23 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint:staged 5 | -------------------------------------------------------------------------------- /.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const ignore = require('ignore'); 4 | 5 | const getIgnore = (ignoreFile, defaultIgnorePatterns) => { 6 | const ig = ignore(); 7 | 8 | if (defaultIgnorePatterns) { 9 | ig.add(defaultIgnorePatterns); 10 | } 11 | 12 | if (fs.existsSync(ignoreFile)) { 13 | const ignoreFileContent = fs.readFileSync(ignoreFile); 14 | ig.add(ignoreFileContent.toString()); 15 | } 16 | 17 | return ig; 18 | }; 19 | const filterIgnore = (ignore, filenames) => ignore.filter(filenames); 20 | const joinMatch = match => match.join(' '); 21 | const getMatch = (filenames, ignoreFile, defaultIgnorePatterns) => 22 | joinMatch(filterIgnore(getIgnore(ignoreFile, defaultIgnorePatterns), filenames)); 23 | 24 | const addCommand = ([command, match]) => (match.length ? `${command} ${match}` : undefined); 25 | const getCommands = (...commands) => commands.map(addCommand).filter(Boolean); 26 | 27 | module.exports = { 28 | '*.{js,jsx,ts,tsx}': filenames => { 29 | const eslintMatch = getMatch(filenames, path.join('.eslintignore'), ['.*']); 30 | const prettierMatch = getMatch(filenames, path.join('.prettierignore')); 31 | return getCommands( 32 | ['prettier --write', prettierMatch], 33 | ['eslint --max-warnings=0', eslintMatch], 34 | ); 35 | }, 36 | 37 | '*.{css,scss,sass,less}': ['prettier --write', 'stylelint --fix'], 38 | 39 | '*.{json,md}': [`prettier --write`], 40 | }; 41 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | /src/utils/telegram/games.js 3 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | tabWidth: 2, 4 | useTabs: false, 5 | semi: true, 6 | singleQuote: true, 7 | quoteProps: 'as-needed', 8 | jsxSingleQuote: false, 9 | trailingComma: 'all', 10 | bracketSpacing: true, 11 | jsxBracketSameLine: false, 12 | arrowParens: 'avoid', 13 | vueIndentScriptAndStyle: true, 14 | endOfLine: 'lf', 15 | }; 16 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['stylelint-config-standard', 'stylelint-config-prettier'], 3 | }; 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Autapomorph 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dino 2 | 3 | A Chrome T-Rex Runner dino game (chrome://dino) clone built with [Phaser 3](https://phaser.io/) 4 | 5 | ## Demo 6 | 7 | Play [online](https://t-rex.vercel.app) 8 | 9 | ## Running Locally 10 | 11 | ### How to run 12 | 13 | - `npm i` 14 | - `npm start` 15 | 16 | ### How to build 17 | 18 | - `npm run build` 19 | - `npx serve dist` 20 | 21 | ## Features 22 | 23 | - [PWA](https://developers.google.com/web/progressive-web-apps) 24 | - Responsive 25 | - Portrait and landscape modes 26 | - Touch support 27 | 28 | ## Also 29 | 30 | See Chromium [source](https://cs.chromium.org/chromium/src/components/neterror/resources/offline.js) 31 | -------------------------------------------------------------------------------- /assets/fonts/joystix_monospace.fnt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /assets/fonts/joystix_monospace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/fonts/joystix_monospace.png -------------------------------------------------------------------------------- /assets/icons/_raw/dino-start-icon-blank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/icons/_raw/dino-start-icon-blank.png -------------------------------------------------------------------------------- /assets/icons/_raw/dino-start-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/icons/_raw/dino-start-icon.png -------------------------------------------------------------------------------- /assets/icons/dino-start-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/icons/dino-start-icon.png -------------------------------------------------------------------------------- /assets/sounds/achievement.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/sounds/achievement.mp3 -------------------------------------------------------------------------------- /assets/sounds/gameover.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/sounds/gameover.mp3 -------------------------------------------------------------------------------- /assets/sounds/player-action.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/sounds/player-action.mp3 -------------------------------------------------------------------------------- /assets/sprites/README.md: -------------------------------------------------------------------------------- 1 | # Bird fly animation spites adjustment 2 | 3 | Due to unequal dimensions of bird fly animation sprites you should adjust `sourceSize` and `spriteSourceSize` for `bird-1` and `bird-2` inside `dino-atlas.json` as follows: 4 | 5 | ```json 6 | { 7 | "filename": "bird-1", 8 | "rotated": false, 9 | "trimmed": true, 10 | "sourceSize": { 11 | "w": 92, 12 | "h": 68 13 | }, 14 | "spriteSourceSize": { 15 | "x": 0, 16 | "y": 12, 17 | "w": 92, 18 | "h": 68 19 | }, 20 | "frame": { 21 | "x": 1473, 22 | "y": 27, 23 | "w": 92, 24 | "h": 68 25 | } 26 | } 27 | ``` 28 | 29 | ```json 30 | { 31 | "filename": "bird-2", 32 | "rotated": false, 33 | "trimmed": true, 34 | "sourceSize": { 35 | "w": 92, 36 | "h": 68 37 | }, 38 | "spriteSourceSize": { 39 | "x": 0, 40 | "y": 0, 41 | "w": 92, 42 | "h": 60 43 | }, 44 | "frame": { 45 | "x": 1881, 46 | "y": 27, 47 | "w": 92, 48 | "h": 60 49 | } 50 | } 51 | ``` 52 | -------------------------------------------------------------------------------- /assets/sprites/_raw/TexturePacker/dino-atlas.tps: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | fileFormatVersion 5 | 4 6 | texturePackerVersion 7 | 5.1.0 8 | autoSDSettings 9 | 10 | 11 | scale 12 | 1 13 | extension 14 | 15 | spriteFilter 16 | 17 | acceptFractionalValues 18 | 19 | maxTextureSize 20 | 21 | width 22 | -1 23 | height 24 | -1 25 | 26 | 27 | 28 | allowRotation 29 | 30 | shapeDebug 31 | 32 | dpi 33 | 72 34 | dataFormat 35 | phaser 36 | textureFileName 37 | dino-atlas.png 38 | flipPVR 39 | 40 | pvrCompressionQuality 41 | PVR_QUALITY_NORMAL 42 | atfCompressData 43 | 44 | mipMapMinSize 45 | 32768 46 | etc1CompressionQuality 47 | ETC1_QUALITY_LOW_PERCEPTUAL 48 | etc2CompressionQuality 49 | ETC2_QUALITY_LOW_PERCEPTUAL 50 | dxtCompressionMode 51 | DXT_PERCEPTUAL 52 | jxrColorFormat 53 | JXR_YUV444 54 | jxrTrimFlexBits 55 | 0 56 | jxrCompressionLevel 57 | 0 58 | ditherType 59 | NearestNeighbour 60 | backgroundColor 61 | 0 62 | libGdx 63 | 64 | filtering 65 | 66 | x 67 | Linear 68 | y 69 | Linear 70 | 71 | 72 | shapePadding 73 | 0 74 | jpgQuality 75 | 80 76 | pngOptimizationLevel 77 | 1 78 | webpQualityLevel 79 | 101 80 | textureSubPath 81 | 82 | atfFormats 83 | 84 | textureFormat 85 | png 86 | borderPadding 87 | 0 88 | maxTextureSize 89 | 90 | width 91 | 2402 92 | height 93 | 2402 94 | 95 | fixedTextureSize 96 | 97 | width 98 | -1 99 | height 100 | -1 101 | 102 | algorithmSettings 103 | 104 | algorithm 105 | MaxRects 106 | freeSizeMode 107 | Best 108 | sizeConstraints 109 | AnySize 110 | forceSquared 111 | 112 | maxRects 113 | 114 | heuristic 115 | Best 116 | 117 | basic 118 | 119 | sortBy 120 | Best 121 | order 122 | Ascending 123 | 124 | polygon 125 | 126 | alignToGrid 127 | 1 128 | 129 | 130 | dataFileNames 131 | 132 | json 133 | 134 | name 135 | dino-atlas.json 136 | 137 | 138 | multiPack 139 | 140 | forceIdenticalLayout 141 | 142 | outputFormat 143 | RGBA8888 144 | alphaHandling 145 | ClearTransparentPixels 146 | contentProtection 147 | 148 | key 149 | 150 | 151 | autoAliasEnabled 152 | 153 | trimSpriteNames 154 | 155 | prependSmartFolderName 156 | 157 | autodetectAnimations 158 | 159 | globalSpriteSettings 160 | 161 | scale 162 | 1 163 | scaleMode 164 | Smooth 165 | extrude 166 | 1 167 | trimThreshold 168 | 1 169 | trimMargin 170 | 1 171 | trimMode 172 | Trim 173 | tracerTolerance 174 | 200 175 | heuristicMask 176 | 177 | defaultPivotPoint 178 | 0,0 179 | writePivotPoints 180 | 181 | 182 | individualSpriteSettings 183 | 184 | dino/assets/raw/sprites/bird-1.png 185 | 186 | pivotPoint 187 | 0,0.441176 188 | scale9Enabled 189 | 190 | scale9Borders 191 | 23,17,46,34 192 | scale9Paddings 193 | 23,17,46,34 194 | scale9FromFile 195 | 196 | 197 | dino/assets/raw/sprites/bird-2.png 198 | 199 | pivotPoint 200 | 0,0.5 201 | scale9Enabled 202 | 203 | scale9Borders 204 | 23,15,46,30 205 | scale9Paddings 206 | 23,15,46,30 207 | scale9FromFile 208 | 209 | 210 | dino/assets/raw/sprites/cactus-big-1.png 211 | 212 | pivotPoint 213 | 0,0 214 | scale9Enabled 215 | 216 | scale9Borders 217 | 13,25,25,50 218 | scale9Paddings 219 | 13,25,25,50 220 | scale9FromFile 221 | 222 | 223 | dino/assets/raw/sprites/cactus-big-2.png 224 | 225 | pivotPoint 226 | 0,0 227 | scale9Enabled 228 | 229 | scale9Borders 230 | 25,25,50,50 231 | scale9Paddings 232 | 25,25,50,50 233 | scale9FromFile 234 | 235 | 236 | dino/assets/raw/sprites/cactus-big-3.png 237 | 238 | pivotPoint 239 | 0,0 240 | scale9Enabled 241 | 242 | scale9Borders 243 | 38,25,75,50 244 | scale9Paddings 245 | 38,25,75,50 246 | scale9FromFile 247 | 248 | 249 | dino/assets/raw/sprites/cactus-small-1.png 250 | 251 | pivotPoint 252 | 0,0 253 | scale9Enabled 254 | 255 | scale9Borders 256 | 9,18,17,35 257 | scale9Paddings 258 | 9,18,17,35 259 | scale9FromFile 260 | 261 | 262 | dino/assets/raw/sprites/cactus-small-2.png 263 | 264 | pivotPoint 265 | 0,0 266 | scale9Enabled 267 | 268 | scale9Borders 269 | 17,18,34,35 270 | scale9Paddings 271 | 17,18,34,35 272 | scale9FromFile 273 | 274 | 275 | dino/assets/raw/sprites/cactus-small-3.png 276 | 277 | pivotPoint 278 | 0,0 279 | scale9Enabled 280 | 281 | scale9Borders 282 | 26,18,51,35 283 | scale9Paddings 284 | 26,18,51,35 285 | scale9FromFile 286 | 287 | 288 | dino/assets/raw/sprites/cloud.png 289 | 290 | pivotPoint 291 | 0,0 292 | scale9Enabled 293 | 294 | scale9Borders 295 | 23,7,46,13 296 | scale9Paddings 297 | 23,7,46,13 298 | scale9FromFile 299 | 300 | 301 | dino/assets/raw/sprites/dino-dead-outline.png 302 | dino/assets/raw/sprites/dino-idle-1.png 303 | dino/assets/raw/sprites/dino-idle-2.png 304 | dino/assets/raw/sprites/dino-run-1.png 305 | dino/assets/raw/sprites/dino-run-2.png 306 | 307 | pivotPoint 308 | 0,0 309 | scale9Enabled 310 | 311 | scale9Borders 312 | 22,24,44,47 313 | scale9Paddings 314 | 22,24,44,47 315 | scale9FromFile 316 | 317 | 318 | dino/assets/raw/sprites/dino-dead.png 319 | 320 | pivotPoint 321 | 0,0 322 | scale9Enabled 323 | 324 | scale9Borders 325 | 20,22,40,43 326 | scale9Paddings 327 | 20,22,40,43 328 | scale9FromFile 329 | 330 | 331 | dino/assets/raw/sprites/dino-duck-1.png 332 | dino/assets/raw/sprites/dino-duck-2.png 333 | 334 | pivotPoint 335 | 0,0 336 | scale9Enabled 337 | 338 | scale9Borders 339 | 30,15,59,30 340 | scale9Paddings 341 | 30,15,59,30 342 | scale9FromFile 343 | 344 | 345 | dino/assets/raw/sprites/dino-start.png 346 | 347 | pivotPoint 348 | 0,0 349 | scale9Enabled 350 | 351 | scale9Borders 352 | 22,23,44,45 353 | scale9Paddings 354 | 22,23,44,45 355 | scale9FromFile 356 | 357 | 358 | dino/assets/raw/sprites/ground.png 359 | 360 | pivotPoint 361 | 0,0 362 | scale9Enabled 363 | 364 | scale9Borders 365 | 600,6,1200,12 366 | scale9Paddings 367 | 600,6,1200,12 368 | scale9FromFile 369 | 370 | 371 | dino/assets/raw/sprites/moon-1.png 372 | 373 | pivotPoint 374 | 0,0 375 | scale9Enabled 376 | 377 | scale9Borders 378 | 20,20,40,40 379 | scale9Paddings 380 | 20,20,40,40 381 | scale9FromFile 382 | 383 | 384 | dino/assets/raw/sprites/moon-2.png 385 | dino/assets/raw/sprites/moon-3.png 386 | dino/assets/raw/sprites/moon-4.png 387 | dino/assets/raw/sprites/moon-5.png 388 | dino/assets/raw/sprites/moon-6.png 389 | dino/assets/raw/sprites/moon-7.png 390 | 391 | pivotPoint 392 | 0,0 393 | scale9Enabled 394 | 395 | scale9Borders 396 | 10,20,20,40 397 | scale9Paddings 398 | 10,20,20,40 399 | scale9FromFile 400 | 401 | 402 | dino/assets/raw/sprites/restart.png 403 | 404 | pivotPoint 405 | 0,0 406 | scale9Enabled 407 | 408 | scale9Borders 409 | 18,16,36,32 410 | scale9Paddings 411 | 18,16,36,32 412 | scale9FromFile 413 | 414 | 415 | dino/assets/raw/sprites/star-1.png 416 | dino/assets/raw/sprites/star-2.png 417 | dino/assets/raw/sprites/star-3.png 418 | 419 | pivotPoint 420 | 0,0 421 | scale9Enabled 422 | 423 | scale9Borders 424 | 5,5,9,9 425 | scale9Paddings 426 | 5,5,9,9 427 | scale9FromFile 428 | 429 | 430 | 431 | fileList 432 | 433 | dino/assets/raw/sprites/dino-start.png 434 | dino/assets/raw/sprites/ground.png 435 | dino/assets/raw/sprites/moon-1.png 436 | dino/assets/raw/sprites/moon-2.png 437 | dino/assets/raw/sprites/moon-3.png 438 | dino/assets/raw/sprites/moon-4.png 439 | dino/assets/raw/sprites/moon-5.png 440 | dino/assets/raw/sprites/moon-6.png 441 | dino/assets/raw/sprites/moon-7.png 442 | dino/assets/raw/sprites/restart.png 443 | dino/assets/raw/sprites/star-1.png 444 | dino/assets/raw/sprites/star-2.png 445 | dino/assets/raw/sprites/star-3.png 446 | dino/assets/raw/sprites/bird-1.png 447 | dino/assets/raw/sprites/bird-2.png 448 | dino/assets/raw/sprites/cactus-big-1.png 449 | dino/assets/raw/sprites/cactus-big-2.png 450 | dino/assets/raw/sprites/cactus-big-3.png 451 | dino/assets/raw/sprites/cactus-small-1.png 452 | dino/assets/raw/sprites/cactus-small-2.png 453 | dino/assets/raw/sprites/cactus-small-3.png 454 | dino/assets/raw/sprites/cloud.png 455 | dino/assets/raw/sprites/dino-dead.png 456 | dino/assets/raw/sprites/dino-dead-outline.png 457 | dino/assets/raw/sprites/dino-duck-1.png 458 | dino/assets/raw/sprites/dino-duck-2.png 459 | dino/assets/raw/sprites/dino-idle-1.png 460 | dino/assets/raw/sprites/dino-idle-2.png 461 | dino/assets/raw/sprites/dino-run-1.png 462 | dino/assets/raw/sprites/dino-run-2.png 463 | 464 | ignoreFileList 465 | 466 | replaceList 467 | 468 | ignoredWarnings 469 | 470 | commonDivisorX 471 | 1 472 | commonDivisorY 473 | 1 474 | packNormalMaps 475 | 476 | autodetectNormalMaps 477 | 478 | normalMapFilter 479 | 480 | normalMapSuffix 481 | 482 | normalMapSheetFileName 483 | 484 | exporterProperties 485 | 486 | 487 | 488 | -------------------------------------------------------------------------------- /assets/sprites/_raw/horizon/cloud/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/sprites/_raw/horizon/cloud/cloud.png -------------------------------------------------------------------------------- /assets/sprites/_raw/horizon/ground/ground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/sprites/_raw/horizon/ground/ground.png -------------------------------------------------------------------------------- /assets/sprites/_raw/horizon/moon/moon-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/sprites/_raw/horizon/moon/moon-1.png -------------------------------------------------------------------------------- /assets/sprites/_raw/horizon/moon/moon-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/sprites/_raw/horizon/moon/moon-2.png -------------------------------------------------------------------------------- /assets/sprites/_raw/horizon/moon/moon-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/sprites/_raw/horizon/moon/moon-3.png -------------------------------------------------------------------------------- /assets/sprites/_raw/horizon/moon/moon-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/sprites/_raw/horizon/moon/moon-4.png -------------------------------------------------------------------------------- /assets/sprites/_raw/horizon/moon/moon-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/sprites/_raw/horizon/moon/moon-5.png -------------------------------------------------------------------------------- /assets/sprites/_raw/horizon/moon/moon-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/sprites/_raw/horizon/moon/moon-6.png -------------------------------------------------------------------------------- /assets/sprites/_raw/horizon/moon/moon-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/sprites/_raw/horizon/moon/moon-7.png -------------------------------------------------------------------------------- /assets/sprites/_raw/horizon/star/star-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/sprites/_raw/horizon/star/star-1.png -------------------------------------------------------------------------------- /assets/sprites/_raw/horizon/star/star-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/sprites/_raw/horizon/star/star-2.png -------------------------------------------------------------------------------- /assets/sprites/_raw/horizon/star/star-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/sprites/_raw/horizon/star/star-3.png -------------------------------------------------------------------------------- /assets/sprites/_raw/obstacles/bird/bird-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/sprites/_raw/obstacles/bird/bird-1.png -------------------------------------------------------------------------------- /assets/sprites/_raw/obstacles/bird/bird-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/sprites/_raw/obstacles/bird/bird-2.png -------------------------------------------------------------------------------- /assets/sprites/_raw/obstacles/cactus/big/cactus-big-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/sprites/_raw/obstacles/cactus/big/cactus-big-1.png -------------------------------------------------------------------------------- /assets/sprites/_raw/obstacles/cactus/big/cactus-big-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/sprites/_raw/obstacles/cactus/big/cactus-big-2.png -------------------------------------------------------------------------------- /assets/sprites/_raw/obstacles/cactus/big/cactus-big-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/sprites/_raw/obstacles/cactus/big/cactus-big-3.png -------------------------------------------------------------------------------- /assets/sprites/_raw/obstacles/cactus/small/cactus-small-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/sprites/_raw/obstacles/cactus/small/cactus-small-1.png -------------------------------------------------------------------------------- /assets/sprites/_raw/obstacles/cactus/small/cactus-small-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/sprites/_raw/obstacles/cactus/small/cactus-small-2.png -------------------------------------------------------------------------------- /assets/sprites/_raw/obstacles/cactus/small/cactus-small-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/sprites/_raw/obstacles/cactus/small/cactus-small-3.png -------------------------------------------------------------------------------- /assets/sprites/_raw/player/dead/dino-dead-outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/sprites/_raw/player/dead/dino-dead-outline.png -------------------------------------------------------------------------------- /assets/sprites/_raw/player/dead/dino-dead.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/sprites/_raw/player/dead/dino-dead.png -------------------------------------------------------------------------------- /assets/sprites/_raw/player/duck/dino-duck-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/sprites/_raw/player/duck/dino-duck-1.png -------------------------------------------------------------------------------- /assets/sprites/_raw/player/duck/dino-duck-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/sprites/_raw/player/duck/dino-duck-2.png -------------------------------------------------------------------------------- /assets/sprites/_raw/player/idle/dino-idle-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/sprites/_raw/player/idle/dino-idle-1.png -------------------------------------------------------------------------------- /assets/sprites/_raw/player/idle/dino-idle-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/sprites/_raw/player/idle/dino-idle-2.png -------------------------------------------------------------------------------- /assets/sprites/_raw/player/run/dino-run-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/sprites/_raw/player/run/dino-run-1.png -------------------------------------------------------------------------------- /assets/sprites/_raw/player/run/dino-run-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/sprites/_raw/player/run/dino-run-2.png -------------------------------------------------------------------------------- /assets/sprites/_raw/player/start/dino-start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/sprites/_raw/player/start/dino-start.png -------------------------------------------------------------------------------- /assets/sprites/_raw/ui/restart/restart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/sprites/_raw/ui/restart/restart.png -------------------------------------------------------------------------------- /assets/sprites/dino-atlas.json: -------------------------------------------------------------------------------- 1 | { 2 | "textures": [ 3 | { 4 | "image": "dino-atlas.png", 5 | "format": "RGBA8888", 6 | "size": { 7 | "w": 2402, 8 | "h": 128 9 | }, 10 | "scale": 1, 11 | "frames": [ 12 | { 13 | "filename": "ground", 14 | "rotated": false, 15 | "trimmed": false, 16 | "sourceSize": { 17 | "w": 2400, 18 | "h": 24 19 | }, 20 | "spriteSourceSize": { 21 | "x": 0, 22 | "y": 0, 23 | "w": 2400, 24 | "h": 24 25 | }, 26 | "frame": { 27 | "x": 1, 28 | "y": 1, 29 | "w": 2400, 30 | "h": 24 31 | } 32 | }, 33 | { 34 | "filename": "cactus-large-3", 35 | "rotated": false, 36 | "trimmed": false, 37 | "sourceSize": { 38 | "w": 150, 39 | "h": 100 40 | }, 41 | "spriteSourceSize": { 42 | "x": 0, 43 | "y": 0, 44 | "w": 150, 45 | "h": 100 46 | }, 47 | "frame": { 48 | "x": 1, 49 | "y": 27, 50 | "w": 150, 51 | "h": 100 52 | } 53 | }, 54 | { 55 | "filename": "cactus-large-2", 56 | "rotated": false, 57 | "trimmed": false, 58 | "sourceSize": { 59 | "w": 100, 60 | "h": 100 61 | }, 62 | "spriteSourceSize": { 63 | "x": 0, 64 | "y": 0, 65 | "w": 100, 66 | "h": 100 67 | }, 68 | "frame": { 69 | "x": 153, 70 | "y": 27, 71 | "w": 100, 72 | "h": 100 73 | } 74 | }, 75 | { 76 | "filename": "cactus-large-1", 77 | "rotated": false, 78 | "trimmed": false, 79 | "sourceSize": { 80 | "w": 50, 81 | "h": 100 82 | }, 83 | "spriteSourceSize": { 84 | "x": 0, 85 | "y": 0, 86 | "w": 50, 87 | "h": 100 88 | }, 89 | "frame": { 90 | "x": 255, 91 | "y": 27, 92 | "w": 50, 93 | "h": 100 94 | } 95 | }, 96 | { 97 | "filename": "dino-dead-outline", 98 | "rotated": false, 99 | "trimmed": false, 100 | "sourceSize": { 101 | "w": 88, 102 | "h": 94 103 | }, 104 | "spriteSourceSize": { 105 | "x": 0, 106 | "y": 0, 107 | "w": 88, 108 | "h": 94 109 | }, 110 | "frame": { 111 | "x": 307, 112 | "y": 27, 113 | "w": 88, 114 | "h": 94 115 | } 116 | }, 117 | { 118 | "filename": "dino-idle-1", 119 | "rotated": false, 120 | "trimmed": false, 121 | "sourceSize": { 122 | "w": 88, 123 | "h": 94 124 | }, 125 | "spriteSourceSize": { 126 | "x": 0, 127 | "y": 0, 128 | "w": 88, 129 | "h": 94 130 | }, 131 | "frame": { 132 | "x": 397, 133 | "y": 27, 134 | "w": 88, 135 | "h": 94 136 | } 137 | }, 138 | { 139 | "filename": "dino-idle-2", 140 | "rotated": false, 141 | "trimmed": false, 142 | "sourceSize": { 143 | "w": 88, 144 | "h": 94 145 | }, 146 | "spriteSourceSize": { 147 | "x": 0, 148 | "y": 0, 149 | "w": 88, 150 | "h": 94 151 | }, 152 | "frame": { 153 | "x": 487, 154 | "y": 27, 155 | "w": 88, 156 | "h": 94 157 | } 158 | }, 159 | { 160 | "filename": "dino-run-1", 161 | "rotated": false, 162 | "trimmed": false, 163 | "sourceSize": { 164 | "w": 88, 165 | "h": 94 166 | }, 167 | "spriteSourceSize": { 168 | "x": 0, 169 | "y": 0, 170 | "w": 88, 171 | "h": 94 172 | }, 173 | "frame": { 174 | "x": 577, 175 | "y": 27, 176 | "w": 88, 177 | "h": 94 178 | } 179 | }, 180 | { 181 | "filename": "dino-run-2", 182 | "rotated": false, 183 | "trimmed": false, 184 | "sourceSize": { 185 | "w": 88, 186 | "h": 94 187 | }, 188 | "spriteSourceSize": { 189 | "x": 0, 190 | "y": 0, 191 | "w": 88, 192 | "h": 94 193 | }, 194 | "frame": { 195 | "x": 667, 196 | "y": 27, 197 | "w": 88, 198 | "h": 94 199 | } 200 | }, 201 | { 202 | "filename": "dino-start", 203 | "rotated": false, 204 | "trimmed": false, 205 | "sourceSize": { 206 | "w": 88, 207 | "h": 90 208 | }, 209 | "spriteSourceSize": { 210 | "x": 0, 211 | "y": 0, 212 | "w": 88, 213 | "h": 90 214 | }, 215 | "frame": { 216 | "x": 757, 217 | "y": 27, 218 | "w": 88, 219 | "h": 90 220 | } 221 | }, 222 | { 223 | "filename": "dino-dead", 224 | "rotated": false, 225 | "trimmed": false, 226 | "sourceSize": { 227 | "w": 80, 228 | "h": 86 229 | }, 230 | "spriteSourceSize": { 231 | "x": 0, 232 | "y": 0, 233 | "w": 80, 234 | "h": 86 235 | }, 236 | "frame": { 237 | "x": 847, 238 | "y": 27, 239 | "w": 80, 240 | "h": 86 241 | } 242 | }, 243 | { 244 | "filename": "moon-1", 245 | "rotated": false, 246 | "trimmed": false, 247 | "sourceSize": { 248 | "w": 80, 249 | "h": 80 250 | }, 251 | "spriteSourceSize": { 252 | "x": 0, 253 | "y": 0, 254 | "w": 80, 255 | "h": 80 256 | }, 257 | "frame": { 258 | "x": 929, 259 | "y": 27, 260 | "w": 80, 261 | "h": 80 262 | } 263 | }, 264 | { 265 | "filename": "star-1", 266 | "rotated": false, 267 | "trimmed": false, 268 | "sourceSize": { 269 | "w": 18, 270 | "h": 18 271 | }, 272 | "spriteSourceSize": { 273 | "x": 0, 274 | "y": 0, 275 | "w": 18, 276 | "h": 18 277 | }, 278 | "frame": { 279 | "x": 929, 280 | "y": 109, 281 | "w": 18, 282 | "h": 18 283 | } 284 | }, 285 | { 286 | "filename": "star-2", 287 | "rotated": false, 288 | "trimmed": false, 289 | "sourceSize": { 290 | "w": 18, 291 | "h": 18 292 | }, 293 | "spriteSourceSize": { 294 | "x": 0, 295 | "y": 0, 296 | "w": 18, 297 | "h": 18 298 | }, 299 | "frame": { 300 | "x": 949, 301 | "y": 109, 302 | "w": 18, 303 | "h": 18 304 | } 305 | }, 306 | { 307 | "filename": "star-3", 308 | "rotated": false, 309 | "trimmed": false, 310 | "sourceSize": { 311 | "w": 18, 312 | "h": 18 313 | }, 314 | "spriteSourceSize": { 315 | "x": 0, 316 | "y": 0, 317 | "w": 18, 318 | "h": 18 319 | }, 320 | "frame": { 321 | "x": 969, 322 | "y": 109, 323 | "w": 18, 324 | "h": 18 325 | } 326 | }, 327 | { 328 | "filename": "moon-2", 329 | "rotated": false, 330 | "trimmed": false, 331 | "sourceSize": { 332 | "w": 40, 333 | "h": 80 334 | }, 335 | "spriteSourceSize": { 336 | "x": 0, 337 | "y": 0, 338 | "w": 40, 339 | "h": 80 340 | }, 341 | "frame": { 342 | "x": 1011, 343 | "y": 27, 344 | "w": 40, 345 | "h": 80 346 | } 347 | }, 348 | { 349 | "filename": "moon-3", 350 | "rotated": false, 351 | "trimmed": false, 352 | "sourceSize": { 353 | "w": 40, 354 | "h": 80 355 | }, 356 | "spriteSourceSize": { 357 | "x": 0, 358 | "y": 0, 359 | "w": 40, 360 | "h": 80 361 | }, 362 | "frame": { 363 | "x": 1053, 364 | "y": 27, 365 | "w": 40, 366 | "h": 80 367 | } 368 | }, 369 | { 370 | "filename": "moon-4", 371 | "rotated": false, 372 | "trimmed": false, 373 | "sourceSize": { 374 | "w": 40, 375 | "h": 80 376 | }, 377 | "spriteSourceSize": { 378 | "x": 0, 379 | "y": 0, 380 | "w": 40, 381 | "h": 80 382 | }, 383 | "frame": { 384 | "x": 1095, 385 | "y": 27, 386 | "w": 40, 387 | "h": 80 388 | } 389 | }, 390 | { 391 | "filename": "moon-5", 392 | "rotated": false, 393 | "trimmed": false, 394 | "sourceSize": { 395 | "w": 40, 396 | "h": 80 397 | }, 398 | "spriteSourceSize": { 399 | "x": 0, 400 | "y": 0, 401 | "w": 40, 402 | "h": 80 403 | }, 404 | "frame": { 405 | "x": 1137, 406 | "y": 27, 407 | "w": 40, 408 | "h": 80 409 | } 410 | }, 411 | { 412 | "filename": "moon-6", 413 | "rotated": false, 414 | "trimmed": false, 415 | "sourceSize": { 416 | "w": 40, 417 | "h": 80 418 | }, 419 | "spriteSourceSize": { 420 | "x": 0, 421 | "y": 0, 422 | "w": 40, 423 | "h": 80 424 | }, 425 | "frame": { 426 | "x": 1179, 427 | "y": 27, 428 | "w": 40, 429 | "h": 80 430 | } 431 | }, 432 | { 433 | "filename": "moon-7", 434 | "rotated": false, 435 | "trimmed": false, 436 | "sourceSize": { 437 | "w": 40, 438 | "h": 80 439 | }, 440 | "spriteSourceSize": { 441 | "x": 0, 442 | "y": 0, 443 | "w": 40, 444 | "h": 80 445 | }, 446 | "frame": { 447 | "x": 1221, 448 | "y": 27, 449 | "w": 40, 450 | "h": 80 451 | } 452 | }, 453 | { 454 | "filename": "cactus-small-3", 455 | "rotated": false, 456 | "trimmed": false, 457 | "sourceSize": { 458 | "w": 102, 459 | "h": 70 460 | }, 461 | "spriteSourceSize": { 462 | "x": 0, 463 | "y": 0, 464 | "w": 102, 465 | "h": 70 466 | }, 467 | "frame": { 468 | "x": 1263, 469 | "y": 27, 470 | "w": 102, 471 | "h": 70 472 | } 473 | }, 474 | { 475 | "filename": "cloud", 476 | "rotated": false, 477 | "trimmed": false, 478 | "sourceSize": { 479 | "w": 92, 480 | "h": 27 481 | }, 482 | "spriteSourceSize": { 483 | "x": 0, 484 | "y": 0, 485 | "w": 92, 486 | "h": 27 487 | }, 488 | "frame": { 489 | "x": 1263, 490 | "y": 99, 491 | "w": 92, 492 | "h": 27 493 | } 494 | }, 495 | { 496 | "filename": "cactus-small-2", 497 | "rotated": false, 498 | "trimmed": false, 499 | "sourceSize": { 500 | "w": 68, 501 | "h": 70 502 | }, 503 | "spriteSourceSize": { 504 | "x": 0, 505 | "y": 0, 506 | "w": 68, 507 | "h": 70 508 | }, 509 | "frame": { 510 | "x": 1367, 511 | "y": 27, 512 | "w": 68, 513 | "h": 70 514 | } 515 | }, 516 | { 517 | "filename": "cactus-small-1", 518 | "rotated": false, 519 | "trimmed": false, 520 | "sourceSize": { 521 | "w": 34, 522 | "h": 70 523 | }, 524 | "spriteSourceSize": { 525 | "x": 0, 526 | "y": 0, 527 | "w": 34, 528 | "h": 70 529 | }, 530 | "frame": { 531 | "x": 1437, 532 | "y": 27, 533 | "w": 34, 534 | "h": 70 535 | } 536 | }, 537 | { 538 | "filename": "bird-1", 539 | "rotated": false, 540 | "trimmed": true, 541 | "sourceSize": { 542 | "w": 92, 543 | "h": 68 544 | }, 545 | "spriteSourceSize": { 546 | "x": 0, 547 | "y": 12, 548 | "w": 92, 549 | "h": 68 550 | }, 551 | "frame": { 552 | "x": 1473, 553 | "y": 27, 554 | "w": 92, 555 | "h": 68 556 | } 557 | }, 558 | { 559 | "filename": "restart", 560 | "rotated": false, 561 | "trimmed": false, 562 | "sourceSize": { 563 | "w": 72, 564 | "h": 64 565 | }, 566 | "spriteSourceSize": { 567 | "x": 0, 568 | "y": 0, 569 | "w": 72, 570 | "h": 64 571 | }, 572 | "frame": { 573 | "x": 1567, 574 | "y": 27, 575 | "w": 72, 576 | "h": 64 577 | } 578 | }, 579 | { 580 | "filename": "dino-duck-1", 581 | "rotated": false, 582 | "trimmed": false, 583 | "sourceSize": { 584 | "w": 118, 585 | "h": 60 586 | }, 587 | "spriteSourceSize": { 588 | "x": 0, 589 | "y": 0, 590 | "w": 118, 591 | "h": 60 592 | }, 593 | "frame": { 594 | "x": 1641, 595 | "y": 27, 596 | "w": 118, 597 | "h": 60 598 | } 599 | }, 600 | { 601 | "filename": "dino-duck-2", 602 | "rotated": false, 603 | "trimmed": false, 604 | "sourceSize": { 605 | "w": 118, 606 | "h": 60 607 | }, 608 | "spriteSourceSize": { 609 | "x": 0, 610 | "y": 0, 611 | "w": 118, 612 | "h": 60 613 | }, 614 | "frame": { 615 | "x": 1761, 616 | "y": 27, 617 | "w": 118, 618 | "h": 60 619 | } 620 | }, 621 | { 622 | "filename": "bird-2", 623 | "rotated": false, 624 | "trimmed": true, 625 | "sourceSize": { 626 | "w": 92, 627 | "h": 68 628 | }, 629 | "spriteSourceSize": { 630 | "x": 0, 631 | "y": 0, 632 | "w": 92, 633 | "h": 60 634 | }, 635 | "frame": { 636 | "x": 1881, 637 | "y": 27, 638 | "w": 92, 639 | "h": 60 640 | } 641 | } 642 | ] 643 | } 644 | ], 645 | "meta": { 646 | "app": "https://www.codeandweb.com/texturepacker", 647 | "version": "3.0", 648 | "smartupdate": "$TexturePacker:SmartUpdate:2a74979e1bd61beb486d62d8f4dc33b3:0b1cec3a288a2a331fef65a521c112bf:45b8b72db3c28d28378c06fc7bde0770$" 649 | } 650 | } 651 | -------------------------------------------------------------------------------- /assets/sprites/dino-atlas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autapomorph/dino/76be7c3ed770a366069d6c306f46e17f654972c6/assets/sprites/dino-atlas.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | useBuiltIns: 'usage', 7 | corejs: 3, 8 | shippedProposals: true, 9 | }, 10 | ], 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dino", 3 | "description": "A Chrome Dino game clone built with Phaser 3", 4 | "version": "1.2.1", 5 | "homepage": "https://github.com/Autapomorph/dino", 6 | "author": "Autapomorph (https://github.com/Autapomorph)", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/Autapomorph/dino" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/Autapomorph/dino/issues" 13 | }, 14 | "license": "MIT", 15 | "licenseUrl": "http://www.opensource.org/licenses/mit-license.php", 16 | "scripts": { 17 | "start": "webpack serve --config webpack/dev.js", 18 | "build": "webpack --config webpack/prod.js", 19 | "lint": "run-s -c lint:js lint:styles", 20 | "lint:js": "eslint src/**/*.js", 21 | "lint:styles": "stylelint src/**/*.css", 22 | "lint:staged": "lint-staged --relative", 23 | "cm": "cz", 24 | "prepare": "husky install && shx rm -rf .git/hooks && shx ln -s ../.husky .git/hooks" 25 | }, 26 | "dependencies": { 27 | "phaser": "^3.53.0" 28 | }, 29 | "devDependencies": { 30 | "@babel/core": "^7.13.8", 31 | "@babel/eslint-parser": "^7.13.8", 32 | "@babel/preset-env": "^7.13.9", 33 | "@commitlint/cli": "^12.0.1", 34 | "@commitlint/config-conventional": "^12.0.1", 35 | "autoprefixer": "^10.2.5", 36 | "babel-loader": "^8.2.2", 37 | "clean-webpack-plugin": "^3.0.0", 38 | "copy-webpack-plugin": "^8.0.0", 39 | "core-js": "^3.9.1", 40 | "cross-env": "^7.0.3", 41 | "css-loader": "^5.1.1", 42 | "css-minimizer-webpack-plugin": "^1.2.0", 43 | "cz-conventional-changelog": "^3.3.0", 44 | "dotenv-webpack": "^7.0.1", 45 | "eslint": "^7.21.0", 46 | "eslint-config-airbnb-base": "^14.2.1", 47 | "eslint-config-prettier": "^8.1.0", 48 | "eslint-plugin-import": "^2.22.1", 49 | "eslint-plugin-prettier": "^3.3.1", 50 | "eslint-webpack-plugin": "^2.5.2", 51 | "favicons": "^6.2.1", 52 | "favicons-webpack-plugin": "^5.0.2", 53 | "friendly-errors-webpack-plugin": "^1.7.0", 54 | "html-webpack-plugin": "^5.3.0", 55 | "husky": "^5.1.3", 56 | "ignore": "^5.1.8", 57 | "lint-staged": "^10.5.4", 58 | "mini-css-extract-plugin": "^1.3.9", 59 | "npm-run-all": "^4.1.5", 60 | "postcss": "^8.2.7", 61 | "postcss-loader": "^5.1.0", 62 | "prettier": "^2.2.1", 63 | "shx": "^0.3.3", 64 | "style-loader": "^2.0.0", 65 | "stylelint": "^13.12.0", 66 | "stylelint-config-prettier": "^8.0.2", 67 | "stylelint-config-standard": "^21.0.0", 68 | "terser-webpack-plugin": "^5.1.1", 69 | "webpack": "^5.24.4", 70 | "webpack-cli": "^4.5.0", 71 | "webpack-dev-server": "^4.0.0-beta.0", 72 | "webpack-merge": "^5.7.3", 73 | "webpackbar": "^5.0.0-3", 74 | "workbox-webpack-plugin": "^6.1.1" 75 | }, 76 | "config": { 77 | "commitizen": { 78 | "path": "cz-conventional-changelog" 79 | } 80 | }, 81 | "commitlint": { 82 | "extends": [ 83 | "@commitlint/config-conventional" 84 | ] 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const autoprefixer = require('autoprefixer'); 2 | 3 | module.exports = { 4 | plugins: [autoprefixer], 5 | }; 6 | -------------------------------------------------------------------------------- /src/Game.js: -------------------------------------------------------------------------------- 1 | import Phaser from 'phaser'; 2 | 3 | import CONFIG from './config/game'; 4 | import BootScene from './scenes/boot/BootScene'; 5 | import GameScene from './scenes/game/GameScene'; 6 | 7 | /** 8 | * Game 9 | * @class Game 10 | * @extends {Phaser.Game} 11 | */ 12 | class Game extends Phaser.Game { 13 | static CONFIG = CONFIG.GAME; 14 | 15 | /** 16 | * Creates an instance of Game 17 | * @param {Phaser.Types.Core.GameConfig} config - Game config 18 | */ 19 | constructor(config) { 20 | super(config); 21 | 22 | this.registerResizeHandler(); 23 | this.addScenes(); 24 | this.startScene(); 25 | } 26 | 27 | /** 28 | * Register reize handler 29 | */ 30 | registerResizeHandler() { 31 | this.scale.on('resize', () => { 32 | const { parentSize } = this.scale; 33 | const { width, height } = parentSize; 34 | 35 | if (width === this.prevParentWidth && height === this.prevParentHeight) { 36 | return; 37 | } 38 | 39 | this.prevParentWidth = width; 40 | this.prevParentHeight = height; 41 | 42 | this.resize(parentSize); 43 | }); 44 | } 45 | 46 | /** 47 | * Add scenes to game 48 | */ 49 | addScenes() { 50 | this.scene.add(BootScene.CONFIG.NAME, BootScene); 51 | this.scene.add(GameScene.CONFIG.NAME, GameScene); 52 | } 53 | 54 | /** 55 | * Start scene 56 | */ 57 | startScene() { 58 | this.scene.start(BootScene.CONFIG.NAME); 59 | } 60 | 61 | /** 62 | * Resize game 63 | * @param {Phaser.Structs.Size} parentSize - Current parent size 64 | */ 65 | resize(parentSize) { 66 | const { width: parentWidth, height: parentHeight } = parentSize; 67 | const gameWidth = 68 | parentWidth < parentHeight ? Game.CONFIG.WIDTH.PORTRAIT : Game.CONFIG.WIDTH.LANDSCAPE; 69 | const gameHeight = Game.CONFIG.HEIGHT; 70 | 71 | this.canvas.style.width = `${parentWidth}px`; 72 | this.canvas.style.height = `${gameHeight * (parentWidth / gameWidth)}px`; 73 | 74 | this.scale.resize(gameWidth, gameHeight); 75 | } 76 | } 77 | 78 | export default Game; 79 | -------------------------------------------------------------------------------- /src/config/game/events/index.js: -------------------------------------------------------------------------------- 1 | const EVENTS = { 2 | GAME_START: 'GAME_START', 3 | GAME_OVER: 'GAME_OVER', 4 | GAME_RESTART: 'GAME_RESTART', 5 | 6 | GAME_INTRO_START: 'GAME_INTRO_START', 7 | GAME_INTRO_COMPLETE: 'GAME_INTRO_COMPLETE', 8 | 9 | PLAYER_ACTION: 'PLAYER_ACTION', 10 | 11 | ACHIEVEMENT: 'ACHIEVEMENT', 12 | HIGH_SCORE_UPDATE: 'HIGH_SCORE_UPDATE', 13 | 14 | // Async score management 15 | HIGH_SCORE_FETCH: { 16 | REQUEST: 'HIGH_SCORE_FETCH_REQUEST', 17 | SUCCESS: 'HIGH_SCORE_FETCH_SUCCESS', 18 | FAIL: 'HIGH_SCORE_FETCH_FAIL', 19 | }, 20 | HIGH_SCORE_SAVE: { 21 | REQUEST: 'HIGH_SCORE_SAVE_REQUEST', 22 | SUCCESS: 'HIGH_SCORE_SAVE_SUCCESS', 23 | FAIL: 'HIGH_SCORE_SAVE_FAIL', 24 | }, 25 | }; 26 | 27 | export default EVENTS; 28 | -------------------------------------------------------------------------------- /src/config/game/index.js: -------------------------------------------------------------------------------- 1 | import SCENES from './scenes'; 2 | import PREFABS from './prefabs'; 3 | import EVENTS from './events'; 4 | 5 | const CONFIG = { 6 | GAME: { 7 | WIDTH: { 8 | PORTRAIT: 600, 9 | LANDSCAPE: 1200, 10 | }, 11 | HEIGHT: 350, 12 | }, 13 | SCENES, 14 | PREFABS, 15 | EVENTS, 16 | }; 17 | 18 | export default CONFIG; 19 | -------------------------------------------------------------------------------- /src/config/game/prefabs/bird.js: -------------------------------------------------------------------------------- 1 | const BIRD = { 2 | FRAMES: { 3 | INITIAL: 'bird-1', 4 | FLYING: ['bird-1', 'bird-2'], 5 | }, 6 | POS: { 7 | Y: [225, 250, 275], 8 | Y_MOBILE: [225, 275], 9 | }, 10 | GAP: { 11 | MIN: 120, 12 | }, 13 | SPEED: { 14 | OFFSET: [0.8, -0.8], 15 | }, 16 | }; 17 | 18 | export default BIRD; 19 | -------------------------------------------------------------------------------- /src/config/game/prefabs/cactus.js: -------------------------------------------------------------------------------- 1 | const CACTUS = { 2 | TYPES: ['small', 'large'], 3 | SIZES: [1, 2, 3], 4 | POS: { 5 | Y: 327, 6 | }, 7 | GAP: { 8 | MIN: 100, 9 | }, 10 | }; 11 | 12 | export default CACTUS; 13 | -------------------------------------------------------------------------------- /src/config/game/prefabs/cloud.js: -------------------------------------------------------------------------------- 1 | const CLOUD = { 2 | SPEED: 2, 3 | POS: { 4 | Y: { 5 | MIN: 100, 6 | MAX: 200, 7 | }, 8 | }, 9 | GAP: { 10 | MIN: 200, 11 | MAX: 500, 12 | }, 13 | }; 14 | 15 | export default CLOUD; 16 | -------------------------------------------------------------------------------- /src/config/game/prefabs/ground.js: -------------------------------------------------------------------------------- 1 | const GROUND = { 2 | POS: { 3 | X: 0, 4 | Y: 350, 5 | }, 6 | BODY: { 7 | OFFSET: { 8 | Y: 25, 9 | }, 10 | }, 11 | }; 12 | 13 | export default GROUND; 14 | -------------------------------------------------------------------------------- /src/config/game/prefabs/index.js: -------------------------------------------------------------------------------- 1 | import PLAYER from './player'; 2 | import BIRD from './bird'; 3 | import CACTUS from './cactus'; 4 | import GROUND from './ground'; 5 | import CLOUD from './cloud'; 6 | import STAR from './star'; 7 | import MOON from './moon'; 8 | 9 | const PREFABS = { 10 | PLAYER, 11 | OBSTACLES: { 12 | BIRD, 13 | CACTUS, 14 | }, 15 | GROUND, 16 | CLOUD, 17 | STAR, 18 | MOON, 19 | }; 20 | 21 | export default PREFABS; 22 | -------------------------------------------------------------------------------- /src/config/game/prefabs/moon.js: -------------------------------------------------------------------------------- 1 | const MOON = { 2 | FRAMES: ['moon-7', 'moon-6', 'moon-5', 'moon-1', 'moon-4', 'moon-3', 'moon-2'], 3 | SPEED: 1, 4 | POS: { 5 | Y: 50, 6 | }, 7 | }; 8 | 9 | export default MOON; 10 | -------------------------------------------------------------------------------- /src/config/game/prefabs/player.js: -------------------------------------------------------------------------------- 1 | const PLAYER = { 2 | FRAMES: { 3 | INITIAL: 'dino-idle-1', 4 | IDLING: ['dino-idle-1', 'dino-idle-2'], 5 | RUNNING: ['dino-run-1', 'dino-run-2'], 6 | DUCKING: ['dino-duck-1', 'dino-duck-2'], 7 | JUMPING: 'dino-idle-1', 8 | DEAD: 'dino-dead', 9 | }, 10 | POS: { 11 | INITIAL_X: 0, 12 | X: 50, 13 | Y: 325, 14 | }, 15 | GRAVITY: { 16 | Y: 2500, 17 | }, 18 | JUMP: { 19 | VELOCITY: { 20 | MAX: 1500, 21 | START: 1500 * 0.9 * -1, 22 | SPEED_FALL: 1500 * 0.75, 23 | INCREASE_INCREMENT: 75 * -1, 24 | INCREASE_THRESHOLD: 200 * -1, 25 | }, 26 | ACCELERATION: 3200, 27 | }, 28 | BLINK: { 29 | DURATION: 100, 30 | DELAY: 7000, 31 | }, 32 | STATES: { 33 | IDLING: 'IDLING', 34 | RUNNING: 'RUNNING', 35 | DUCKING: 'DUCKING', 36 | JUMPING: 'JUMPING', 37 | DEAD: 'DEAD', 38 | }, 39 | }; 40 | 41 | export default PLAYER; 42 | -------------------------------------------------------------------------------- /src/config/game/prefabs/star.js: -------------------------------------------------------------------------------- 1 | const STAR = { 2 | FRAMES: ['star-1', 'star-2', 'star-3'], 3 | SPEED: 1.2, 4 | POS: { 5 | Y: { 6 | MIN: 0, 7 | MAX: 150, 8 | }, 9 | }, 10 | }; 11 | 12 | export default STAR; 13 | -------------------------------------------------------------------------------- /src/config/game/scenes/boot.js: -------------------------------------------------------------------------------- 1 | const BOOT = { 2 | NAME: 'SCENE_BOOT', 3 | }; 4 | 5 | export default BOOT; 6 | -------------------------------------------------------------------------------- /src/config/game/scenes/game.js: -------------------------------------------------------------------------------- 1 | const GAME = { 2 | NAME: 'SCENE_GAME', 3 | GAME: { 4 | SCORE: { 5 | MAX_LENGTH: 5, 6 | COEFFICIENT: 0.02, 7 | POS: { 8 | X: { 9 | OFFSET: 0.95, 10 | }, 11 | Y: 10, 12 | }, 13 | ACHIEVEMENT: { 14 | DISTANCE: 100, 15 | FLASH: { 16 | DURATION: 250, 17 | ITERATIONS: 3, 18 | }, 19 | }, 20 | }, 21 | HIGH_SCORE: { 22 | STORAGE_KEY: 'highScore', 23 | POS: { 24 | X: { 25 | OFFSET: 15, 26 | }, 27 | Y: 10, 28 | }, 29 | GAMEOVER: { 30 | FLASH: { 31 | DURATION: 250, 32 | ITERATIONS: 6, 33 | }, 34 | }, 35 | }, 36 | OBSTACLES: { 37 | TYPES: { 38 | BIRD: { 39 | FRAME: 'bird', 40 | SPAWN: { 41 | SPEED: { 42 | MIN: 12, 43 | MIN_MOBILE: 10, 44 | }, 45 | }, 46 | }, 47 | CACTUS: { 48 | SMALL: { 49 | FRAME: 'cactus-small', 50 | SIZING: { 51 | SPEED: { 52 | MIN: 10, 53 | MIN_MOBILE: 8, 54 | }, 55 | }, 56 | }, 57 | LARGE: { 58 | FRAME: 'cactus-large', 59 | SIZING: { 60 | SPEED: { 61 | MIN: 11, 62 | MIN_MOBILE: 9, 63 | }, 64 | }, 65 | }, 66 | }, 67 | }, 68 | ACCELERATION: 0.001, 69 | SPEED: { 70 | INITIAL: 10, 71 | MAX: 17, 72 | MOBILE_COEFFICIENT: 1.2, 73 | }, 74 | GAP: { 75 | MAX_MULIPLIER: 1.5, 76 | }, 77 | SPAWN: { 78 | DELAY: 3000, 79 | MAX_DUPLICATION: 2, 80 | }, 81 | }, 82 | }, 83 | GAMEOVER: { 84 | VIBRATION: 200, 85 | }, 86 | RESTART: { 87 | COOLDOWN: 750, 88 | }, 89 | INTRO: { 90 | STATES: { 91 | WAITING: 'WAITING', 92 | ONGOING: 'ONGOING', 93 | COMPLETE: 'COMPLETE', 94 | }, 95 | DURATION: 400, 96 | CAMERA: { 97 | WIDTH: 90, 98 | }, 99 | }, 100 | NIGHTMODE: { 101 | DISTANCE: 700, 102 | DURATION: 12000, 103 | FADE_DURATION: 250, 104 | STARS: { 105 | MAX_COUNT: 2, 106 | }, 107 | }, 108 | STYLES: { 109 | TRANSITION: 'width 250ms linear, height 250ms linear', 110 | }, 111 | }; 112 | 113 | export default GAME; 114 | -------------------------------------------------------------------------------- /src/config/game/scenes/index.js: -------------------------------------------------------------------------------- 1 | import BOOT from './boot'; 2 | import GAME from './game'; 3 | 4 | const SCENES = { 5 | BOOT, 6 | GAME, 7 | }; 8 | 9 | export default SCENES; 10 | -------------------------------------------------------------------------------- /src/config/index.js: -------------------------------------------------------------------------------- 1 | import Phaser from 'phaser'; 2 | 3 | import { isProd } from '../utils'; 4 | 5 | const config = { 6 | title: 'Dino', 7 | url: 'https://github.com/Autapomorph/dino', 8 | version: '1.2.1', 9 | type: Phaser.AUTO, 10 | backgroundColor: '#fff', 11 | banner: !isProd, 12 | render: { 13 | antialias: false, 14 | }, 15 | scale: { 16 | width: 1200, 17 | height: 350, 18 | mode: Phaser.Scale.ScaleModes.NONE, 19 | }, 20 | physics: { 21 | default: 'arcade', 22 | arcade: { 23 | gravity: { y: 0 }, 24 | debug: !isProd, 25 | }, 26 | }, 27 | }; 28 | 29 | export default config; 30 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::before, 3 | *::after { 4 | box-sizing: border-box; 5 | } 6 | 7 | html { 8 | height: 100%; 9 | } 10 | 11 | body { 12 | display: flex; 13 | justify-content: center; 14 | align-items: center; 15 | height: 100%; 16 | margin: auto; 17 | padding: 0; 18 | background-color: #fff; 19 | overflow: hidden; 20 | } 21 | 22 | canvas { 23 | display: block; 24 | } 25 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dino 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Game from './Game'; 2 | import config from './config'; 3 | import './utils/telegram/games'; 4 | import { isProd } from './utils'; 5 | 6 | import './index.css'; 7 | 8 | // eslint-disable-next-line no-new 9 | new Game(config); 10 | 11 | if (isProd) { 12 | if ('serviceWorker' in navigator) { 13 | window.addEventListener('load', () => { 14 | navigator.serviceWorker.register('/service-worker.js'); 15 | }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/prefabs/horizon/Horizon.js: -------------------------------------------------------------------------------- 1 | import CONFIG from '../../config/game'; 2 | 3 | import Ground from './ground/Ground'; 4 | import Obstacles from './obstacles/Obstacles'; 5 | import Clouds from './clouds/Clouds'; 6 | import NightMode from './NightMode'; 7 | 8 | /** 9 | * Horizon 10 | * @class Horizon 11 | */ 12 | class Horizon { 13 | static CONFIG = { 14 | OBSTACLES: CONFIG.SCENES.GAME.GAME.OBSTACLES, 15 | CLOUDS: CONFIG.PREFABS.CLOUD, 16 | }; 17 | 18 | /** 19 | * Creates an instance of Horizon 20 | * @param {Phaser.Scene} scene - The Scene to which this Horizon belongs 21 | */ 22 | constructor(scene) { 23 | this.scene = scene; 24 | this.ground = new Ground(scene); 25 | this.obstacles = new Obstacles(scene); 26 | this.clouds = new Clouds(scene); 27 | this.nightMode = new NightMode(scene); 28 | 29 | // Register event handlers 30 | this.scene.events.on(CONFIG.EVENTS.GAME_INTRO_COMPLETE, this.start, this); 31 | this.scene.events.on(CONFIG.EVENTS.GAME_RESTART, this.reset, this); 32 | } 33 | 34 | /** 35 | * Set first horizon state 36 | */ 37 | start() { 38 | const { speed } = this.scene; 39 | this.spawnInitialCloud(speed); 40 | this.spawnInitialObstacle(speed); 41 | } 42 | 43 | /** 44 | * Spawn 1st cloud 45 | * @param {number} speed - Current game speed 46 | */ 47 | spawnInitialCloud(speed) { 48 | this.clouds.spawnItem(speed); 49 | } 50 | 51 | /** 52 | * Spawn 1st obstacle 53 | * @param {number} speed - Current game speed 54 | * @param {boolean} isMobile - Whether game is running in mobile mode 55 | */ 56 | spawnInitialObstacle(speed, isMobile) { 57 | const obstacleSpawnDelay = Horizon.CONFIG.OBSTACLES.SPAWN.DELAY; 58 | this.scene.time.delayedCall(obstacleSpawnDelay, () => 59 | this.obstacles.spawnItem(speed, isMobile), 60 | ); 61 | } 62 | 63 | /** 64 | * Update horizon 65 | * @param {number} speed - Current game speed 66 | * @param {boolean} isMobile - Whether game is running in mobile mode 67 | */ 68 | update(speed, isMobile) { 69 | this.ground.update(speed); 70 | this.obstacles.update(speed, isMobile); 71 | this.clouds.update(Horizon.CONFIG.CLOUDS.SPEED); 72 | this.nightMode.update(); 73 | } 74 | 75 | /** 76 | * Reset horizon 77 | */ 78 | reset() { 79 | const { speed } = this.scene; 80 | this.ground.reset(); 81 | this.obstacles.reset(); 82 | this.nightMode.reset(); 83 | this.spawnInitialObstacle(speed); 84 | } 85 | } 86 | 87 | export default Horizon; 88 | -------------------------------------------------------------------------------- /src/prefabs/horizon/HorizonItems.js: -------------------------------------------------------------------------------- 1 | import Phaser from 'phaser'; 2 | 3 | /** 4 | * HorizonItems Group 5 | * @class HorizonItems 6 | * @extends {Phaser.Physics.Arcade.Group} 7 | */ 8 | class HorizonItems extends Phaser.Physics.Arcade.Group { 9 | /** 10 | * Creates an instance of HorizonItems 11 | * @param {Phaser.Scene} scene - The Scene to which this HorizonItems group belongs 12 | */ 13 | constructor(scene) { 14 | super(scene.physics.world, scene); 15 | } 16 | 17 | /** 18 | * Update horizon items group 19 | * @param {number} speed - Current game speed 20 | * @param {boolean} isMobile - Whether game is running in mobile mode 21 | */ 22 | update(speed, isMobile) { 23 | const { width } = this.scene.scale.gameSize; 24 | 25 | this.children.each(item => item.update(speed)); 26 | 27 | const lastItem = this.getLast(true); 28 | if (lastItem && lastItem.x + lastItem.width + lastItem.gap < width) { 29 | this.clearItems(); 30 | this.spawnItem(speed, isMobile); 31 | } 32 | } 33 | 34 | /** 35 | * Spawn horizon item 36 | * @param {number} speed - Current game speed 37 | * @param {boolean} isMobile - Whether game is running in mobile mode 38 | * @abstract 39 | */ 40 | // eslint-disable-next-line class-methods-use-this, no-unused-vars 41 | spawnItem(speed, isMobile) { 42 | throw new Error('Method must be implemented by subclass'); 43 | } 44 | 45 | /** 46 | * Clear horizon items group 47 | */ 48 | clearItems() { 49 | // clear every invisible item 50 | this.children.each(item => { 51 | if (!item.visible) { 52 | item.die(); 53 | } 54 | }); 55 | } 56 | 57 | /** 58 | * Reset horizon items group 59 | */ 60 | reset() { 61 | this.children.each(item => item.reset()); 62 | this.clear(true, true); 63 | } 64 | } 65 | 66 | export default HorizonItems; 67 | -------------------------------------------------------------------------------- /src/prefabs/horizon/NightMode.js: -------------------------------------------------------------------------------- 1 | import CONFIG from '../../config/game'; 2 | import Stars from './stars/Stars'; 3 | import Moon from './moon/Moon'; 4 | 5 | /** 6 | * NightMode 7 | * @class NightMode 8 | */ 9 | class NightMode { 10 | static CONFIG = { 11 | NIGHTMODE: CONFIG.SCENES.GAME.NIGHTMODE, 12 | MOON: CONFIG.PREFABS.MOON, 13 | STARS: CONFIG.PREFABS.STAR, 14 | }; 15 | 16 | /** 17 | * Creates an instance of NightMode 18 | * @param {Phaser.Scene} scene - The Scene to which this NightMode belongs 19 | */ 20 | constructor(scene) { 21 | const { FADE_DURATION } = NightMode.CONFIG.NIGHTMODE; 22 | const gameWidth = scene.scale.gameSize.width; 23 | 24 | this.scene = scene; 25 | this.isEnabled = false; 26 | 27 | // Moon 28 | this.moon = new Moon(scene, gameWidth - 50, NightMode.CONFIG.MOON.POS.Y); 29 | this.moon.setAlpha(0); 30 | 31 | // Stars 32 | this.stars = new Stars(scene); 33 | this.stars.spawnItems(); 34 | this.stars.children.each(star => star.setAlpha(0)); 35 | 36 | // DOM canvas and parent elements 37 | this.canvas = scene.game.canvas; 38 | this.parent = scene.game.scale.parent; 39 | 40 | // Set DOM elements transitions 41 | this.canvas.style.transition = `filter ${FADE_DURATION}ms linear`; 42 | this.parent.style.transition = `background-color ${FADE_DURATION}ms linear`; 43 | } 44 | 45 | /** 46 | * Update nightmode 47 | */ 48 | update() { 49 | const { width } = this.scene.scale.gameSize; 50 | const { moon, stars } = this; 51 | 52 | if (this.isEnabled) { 53 | moon.update(NightMode.CONFIG.MOON.SPEED); 54 | stars.update(NightMode.CONFIG.STARS.SPEED); 55 | 56 | if (moon.x + moon.width < 0) { 57 | moon.setX(width); 58 | } 59 | } 60 | } 61 | 62 | /** 63 | * Check if nightmode is enabled 64 | * @returns {boolean} 65 | */ 66 | get isEnabled() { 67 | return this.enabled; 68 | } 69 | 70 | /** 71 | * Set nightmode enabled property 72 | * @param {boolean} isEnabled - Enable or disable NightMode 73 | * @returns {boolean} 74 | */ 75 | set isEnabled(isEnabled) { 76 | this.enabled = isEnabled; 77 | } 78 | 79 | /** 80 | * Enable nightmode 81 | */ 82 | enable() { 83 | this.stars.shuffleItems(); 84 | 85 | this.scene.tweens.add({ 86 | targets: [this.moon, ...this.stars.getChildren()], 87 | duration: NightMode.CONFIG.NIGHTMODE.FADE_DURATION, 88 | alpha: 1, 89 | onStart: () => { 90 | this.isEnabled = true; 91 | this.canvas.style.filter = 'invert(1)'; 92 | this.parent.style.backgroundColor = '#000'; 93 | }, 94 | }); 95 | } 96 | 97 | /** 98 | * Disable nightmode 99 | * @param {function} [onComplete=() => {}] - A function to be executed after disabling nightmode 100 | */ 101 | disable(onComplete = () => {}) { 102 | this.scene.tweens.add({ 103 | targets: [this.moon, ...this.stars.getChildren()], 104 | duration: NightMode.CONFIG.NIGHTMODE.FADE_DURATION, 105 | alpha: 0, 106 | onStart: () => { 107 | this.canvas.style.filter = 'invert(0)'; 108 | this.parent.style.backgroundColor = '#fff'; 109 | }, 110 | onComplete: () => { 111 | this.isEnabled = false; 112 | this.moon.nextPhase(); 113 | onComplete(); 114 | }, 115 | }); 116 | } 117 | 118 | /** 119 | * Reset nightmode 120 | */ 121 | reset() { 122 | this.disable(() => this.moon.reset()); 123 | } 124 | } 125 | 126 | export default NightMode; 127 | -------------------------------------------------------------------------------- /src/prefabs/horizon/clouds/Cloud.js: -------------------------------------------------------------------------------- 1 | import Phaser from 'phaser'; 2 | 3 | /** 4 | * Cloud 5 | * @class Cloud 6 | * @extends {Phaser.GameObjects.Image} 7 | */ 8 | class Cloud extends Phaser.GameObjects.Image { 9 | /** 10 | * Creates an instance of Cloud 11 | * @param {Phaser.Scene} scene - The Scene to which this Cloud belongs 12 | * @param {number} x - The horizontal position of this Cloud in the world 13 | * @param {number} y - The vertical position of this Cloud in the world 14 | */ 15 | constructor(scene, x, y) { 16 | super(scene, x, y, 'dino', 'cloud'); 17 | 18 | // Gap before next Cloud 19 | this.gap = 0; 20 | 21 | // Init image 22 | this.setOrigin(0, 0); 23 | this.setDepth(300); 24 | 25 | this.scene.add.existing(this); 26 | } 27 | 28 | /** 29 | * Update cloud 30 | * @param {number} speed - Current game speed 31 | */ 32 | update(speed) { 33 | this.move(speed); 34 | 35 | if (this.x + this.width < 0) { 36 | this.setVisible(false); 37 | } 38 | } 39 | 40 | /** 41 | * Move cloud 42 | * @param {number} speed - Current game speed 43 | */ 44 | move(speed) { 45 | this.x -= speed; 46 | } 47 | 48 | /** 49 | * Set cloud gap 50 | */ 51 | setGap(gap) { 52 | this.gap = gap; 53 | } 54 | 55 | /** 56 | * Kill cloud 57 | */ 58 | die() { 59 | this.destroy(); 60 | } 61 | } 62 | 63 | export default Cloud; 64 | -------------------------------------------------------------------------------- /src/prefabs/horizon/clouds/Clouds.js: -------------------------------------------------------------------------------- 1 | import Phaser from 'phaser'; 2 | 3 | import CONFIG from '../../../config/game'; 4 | import HorizonItems from '../HorizonItems'; 5 | import Cloud from './Cloud'; 6 | 7 | /** 8 | * Clouds group 9 | * @class Clouds 10 | * @extends {HorizonItems} 11 | */ 12 | class Clouds extends HorizonItems { 13 | static CONFIG = CONFIG.PREFABS.CLOUD; 14 | 15 | /** 16 | * Spawn cloud 17 | * @override 18 | */ 19 | spawnItem() { 20 | const { width } = this.scene.scale.gameSize; 21 | const CLOUD = Clouds.CONFIG; 22 | 23 | const y = Phaser.Math.RND.between(CLOUD.POS.Y.MIN, CLOUD.POS.Y.MAX); 24 | const gap = Phaser.Math.RND.between(CLOUD.GAP.MIN, CLOUD.GAP.MAX); 25 | 26 | const newObstacle = new Cloud(this.scene, width, y); 27 | newObstacle.setGap(gap); 28 | this.add(newObstacle); 29 | } 30 | } 31 | 32 | export default Clouds; 33 | -------------------------------------------------------------------------------- /src/prefabs/horizon/ground/Ground.js: -------------------------------------------------------------------------------- 1 | import Phaser from 'phaser'; 2 | 3 | import CONFIG from '../../../config/game'; 4 | 5 | /** 6 | * Ground 7 | * @class Ground 8 | * @extends {Phaser.GameObjects.TileSprite} 9 | */ 10 | class Ground extends Phaser.GameObjects.TileSprite { 11 | static CONFIG = CONFIG.PREFABS.GROUND; 12 | 13 | /** 14 | * Creates an instance of Ground 15 | * @param {Phaser.Scene} scene - The Scene to which this Ground belongs 16 | * @param {number} [x=Ground.CONFIG.POS.X] - The horizontal position of this Ground in the world 17 | * @param {number} [y=Ground.CONFIG.POS.Y] - The vertical position of this Ground in the world 18 | */ 19 | constructor(scene, x = Ground.CONFIG.POS.X, y = Ground.CONFIG.POS.Y) { 20 | super(scene, x, y, 0, 0, 'dino', 'ground'); 21 | 22 | // Init image 23 | const gameSize = this.scene.scale; 24 | this.setY(gameSize.height); 25 | this.setOrigin(0, 2); 26 | this.setSize(gameSize.width, this.height); 27 | 28 | // Init physics 29 | this.scene.physics.world.enable(this, Phaser.Physics.Arcade.STATIC_BODY); 30 | this.body.setOffset(0, Ground.CONFIG.BODY.OFFSET.Y); 31 | 32 | this.scene.add.existing(this); 33 | } 34 | 35 | /** 36 | * Update ground 37 | * @param {number} speed - Current game speed 38 | */ 39 | update(speed) { 40 | this.move(speed); 41 | } 42 | 43 | /** 44 | * Move ground 45 | * @param {number} speed - Current game speed 46 | */ 47 | move(speed) { 48 | this.tilePositionX += speed; 49 | } 50 | 51 | /** 52 | * Resize ground 53 | * @param {Phaser.Structs.Size} gameSize - Current game size 54 | */ 55 | resize({ width }) { 56 | this.width = width; 57 | this.body.width = width; 58 | } 59 | 60 | /** 61 | * Reset ground 62 | */ 63 | reset() { 64 | this.tilePositionX = 0; 65 | } 66 | } 67 | 68 | export default Ground; 69 | -------------------------------------------------------------------------------- /src/prefabs/horizon/moon/Moon.js: -------------------------------------------------------------------------------- 1 | import Phaser from 'phaser'; 2 | 3 | import CONFIG from '../../../config/game'; 4 | 5 | /** 6 | * Moon 7 | * @class Moon 8 | * @extends {Phaser.GameObjects.Image} 9 | */ 10 | class Moon extends Phaser.GameObjects.Image { 11 | static CONFIG = CONFIG.PREFABS.MOON; 12 | 13 | /** 14 | *Creates an instance of Moon 15 | * @param {Phaser.Scene} scene - The Scene to which this Moon belongs 16 | * @param {number} x - The horizontal position of this Moon in the world 17 | * @param {number} y - The vertical position of this Moon in the world 18 | * @param {Phaser.Animations.AnimationFrame} [frame=Moon.CONFIG.FRAMES[0]] - The frame to display this Moon phase 19 | */ 20 | constructor(scene, x, y, frame = Moon.CONFIG.FRAMES[0]) { 21 | super(scene, x, y, 'dino', frame); 22 | 23 | // Current moon phase 24 | this.phase = 0; 25 | 26 | // Init image 27 | this.setOrigin(0, 0); 28 | this.setDepth(200); 29 | 30 | this.scene.add.existing(this); 31 | } 32 | 33 | /** 34 | * Update moon 35 | * @param {number} speed - Current game speed 36 | */ 37 | update(speed) { 38 | this.move(speed); 39 | } 40 | 41 | /** 42 | * Move moon 43 | * @param {number} speed - Current game speed 44 | */ 45 | move(speed) { 46 | this.x -= speed; 47 | } 48 | 49 | /** 50 | * Set moon phase 51 | * @param {number} phase - Moon phase to be set 52 | */ 53 | setPhase(phase) { 54 | if (phase >= Moon.CONFIG.FRAMES.length) { 55 | this.phase = 0; 56 | } else { 57 | this.phase = phase; 58 | } 59 | 60 | this.setFrame(Moon.CONFIG.FRAMES[this.phase]); 61 | } 62 | 63 | /** 64 | * Increment moon phase 65 | */ 66 | nextPhase() { 67 | this.setPhase(this.phase + 1); 68 | } 69 | 70 | /** 71 | * Reset moon 72 | */ 73 | reset() { 74 | this.setPhase(0); 75 | } 76 | } 77 | 78 | export default Moon; 79 | -------------------------------------------------------------------------------- /src/prefabs/horizon/obstacles/Obstacle.js: -------------------------------------------------------------------------------- 1 | import Phaser from 'phaser'; 2 | 3 | import CONFIG from '../../../config/game'; 4 | 5 | /** 6 | * Obstacle 7 | * @class Obstacle 8 | * @extends {Phaser.Physics.Arcade.Sprite} 9 | */ 10 | class Obstacle extends Phaser.Physics.Arcade.Sprite { 11 | /** 12 | * Creates an instance of Obstacle 13 | * @param {Phaser.Scene} scene - The Scene to which this Obstacle belongs 14 | * @param {number} x - The horizontal position of this Obstacle in the world 15 | * @param {number} y - The vertical position of this Obstacle in the world 16 | * @param {string} frame - The frame from the Texture this Obstacle is rendering with 17 | */ 18 | constructor(scene, x, y, frame) { 19 | super(scene, x, y, 'dino', frame); 20 | 21 | // Gap before next Obstacle 22 | this.gap = 0; 23 | 24 | // Init image 25 | this.setOrigin(0, 1); 26 | this.setDepth(900); 27 | 28 | // Register event handlers 29 | this.scene.events.on(CONFIG.EVENTS.GAME_OVER, this.freeze, this); 30 | 31 | this.scene.add.existing(this); 32 | } 33 | 34 | /** 35 | * Update obstacle 36 | * @param {number} speed - Current game speed 37 | */ 38 | update(speed) { 39 | this.move(speed); 40 | 41 | if (this.x + this.width < 0) { 42 | this.setVisible(false); 43 | } 44 | } 45 | 46 | /** 47 | * Move obstacle 48 | * @param {number} speed - Current game speed 49 | */ 50 | move(speed) { 51 | this.x -= speed; 52 | } 53 | 54 | /** 55 | * Set obstacle gap 56 | */ 57 | setGap(gap) { 58 | this.gap = gap; 59 | } 60 | 61 | /** 62 | * Freeze obstacle 63 | */ 64 | freeze() { 65 | this.scene.events.off(CONFIG.EVENTS.GAME_OVER, this.freeze); 66 | } 67 | 68 | /** 69 | * Kill obstacle 70 | */ 71 | die() { 72 | this.freeze(); 73 | this.destroy(); 74 | } 75 | 76 | /** 77 | * Reset obstacle 78 | */ 79 | reset() { 80 | this.die(); 81 | } 82 | } 83 | 84 | export default Obstacle; 85 | -------------------------------------------------------------------------------- /src/prefabs/horizon/obstacles/Obstacles.js: -------------------------------------------------------------------------------- 1 | import Phaser from 'phaser'; 2 | 3 | import CONFIG from '../../../config/game'; 4 | import HorizonItems from '../HorizonItems'; 5 | import Cactus from './cactus/Cactus'; 6 | import Bird from './bird/Bird'; 7 | 8 | /** 9 | * Obstacles group 10 | * @class Obstacles 11 | * @extends {HorizonItems} 12 | */ 13 | class Obstacles extends HorizonItems { 14 | static CONFIG = { 15 | TYPES: CONFIG.SCENES.GAME.GAME.OBSTACLES.TYPES, 16 | MAX_DUPLICATION: CONFIG.SCENES.GAME.GAME.OBSTACLES.SPAWN.MAX_DUPLICATION, 17 | GAP: CONFIG.SCENES.GAME.GAME.OBSTACLES.GAP, 18 | }; 19 | 20 | /** 21 | * Creates an instance of Obstacles 22 | * @param {Phaser.Scene} scene - The Scene to which this Obstacles group belongs 23 | * @override 24 | */ 25 | constructor(scene) { 26 | super(scene); 27 | 28 | this.obstacleHistory = []; 29 | } 30 | 31 | /** 32 | * Check if next obstacle is duplicate 33 | * @param {Obstacle} nextObstacle - next obstacle object 34 | * @param {number} maxDuplication - max duplication count 35 | * @returns {boolean} 36 | */ 37 | checkDuplication(nextObstacle, maxDuplication) { 38 | if (this.obstacleHistory.some(obstacle => obstacle.FRAME !== nextObstacle.FRAME)) { 39 | return false; 40 | } 41 | 42 | return this.obstacleHistory.length >= maxDuplication; 43 | } 44 | 45 | /** 46 | * Spawn obstacle 47 | * @param {number} speed - Current game speed 48 | * @param {boolean} isMobile - Whether game is running in mobile mode 49 | * @override 50 | */ 51 | spawnItem(speed, isMobile) { 52 | const { MAX_DUPLICATION } = Obstacles.CONFIG; 53 | const { BIRD, CACTUS } = Obstacles.CONFIG.TYPES; 54 | let obstacleType; 55 | 56 | // only allow bird spawn if we have enough speed 57 | if (speed > BIRD.SPAWN.SPEED.MIN) { 58 | obstacleType = Phaser.Math.RND.pick([BIRD, CACTUS.SMALL, CACTUS.LARGE]); 59 | } else { 60 | obstacleType = Phaser.Math.RND.pick([CACTUS.SMALL, CACTUS.LARGE]); 61 | } 62 | 63 | if (this.checkDuplication(obstacleType, MAX_DUPLICATION)) { 64 | this.spawnItem(speed, isMobile); 65 | return; 66 | } 67 | 68 | this.obstacleHistory.push(obstacleType); 69 | if (this.obstacleHistory.length > MAX_DUPLICATION) { 70 | this.obstacleHistory = this.obstacleHistory.slice(-MAX_DUPLICATION); 71 | } 72 | 73 | if (obstacleType === BIRD) { 74 | this.spawnBird(speed, isMobile); 75 | } else { 76 | let cactusSize = 1; 77 | 78 | const cactusSizingSpeed = !isMobile 79 | ? obstacleType.SIZING.SPEED.MIN 80 | : obstacleType.SIZING.SPEED.MIN_MOBILE; 81 | if (speed > cactusSizingSpeed) { 82 | cactusSize = Phaser.Math.RND.pick(CONFIG.PREFABS.OBSTACLES.CACTUS.SIZES); 83 | } 84 | 85 | this.spawnCactus(speed, isMobile, `${obstacleType.FRAME}-${cactusSize}`); 86 | } 87 | } 88 | 89 | /** 90 | * Spawn bird obstacle 91 | * @param {number} speed - Current game speed 92 | * @param {boolean} isMobile - Whether game is running in mobile mode 93 | */ 94 | spawnBird(speed, isMobile) { 95 | const { width } = this.scene.scale.gameSize; 96 | const { BIRD } = CONFIG.PREFABS.OBSTACLES; 97 | 98 | const y = !isMobile 99 | ? Phaser.Math.RND.pick(BIRD.POS.Y) 100 | : Phaser.Math.RND.pick(BIRD.POS.Y_MOBILE); 101 | 102 | const newObstacle = new Bird(this.scene, width, y); 103 | this.add(newObstacle); 104 | 105 | const gap = this.getGap(speed, BIRD.GAP.MIN, newObstacle.width); 106 | newObstacle.setGap(gap); 107 | } 108 | 109 | /** 110 | * Spawn cactus obstacle 111 | * @param {number} speed - Current game speed 112 | * @param {boolean} isMobile - Whether game is running in mobile mode 113 | * @param {Phaser.Animations.AnimationFrame} frame - Frame to display cactus obstacle 114 | */ 115 | spawnCactus(speed, isMobile, frame) { 116 | const { width } = this.scene.scale.gameSize; 117 | const { CACTUS } = CONFIG.PREFABS.OBSTACLES; 118 | 119 | const newObstacle = new Cactus(this.scene, width, CACTUS.POS.Y, frame); 120 | this.add(newObstacle); 121 | 122 | const gap = this.getGap(speed, CACTUS.GAP.MIN, newObstacle.width); 123 | newObstacle.setGap(gap); 124 | } 125 | 126 | /** 127 | * Get random gap 128 | * @param {number} speed - Current game speed 129 | * @param {number} minGap - Minimum gap 130 | * @param {number} width - Obstacle width 131 | * @returns {number} - random gap 132 | */ 133 | // eslint-disable-next-line class-methods-use-this 134 | getGap(speed, minGap, width) { 135 | const { MAX_MULIPLIER } = Obstacles.CONFIG.GAP; 136 | 137 | // min gap is based on speed 138 | const min = Math.round(width * speed + minGap); 139 | const max = Math.round(min * MAX_MULIPLIER); 140 | 141 | return Phaser.Math.RND.between(min, max); 142 | } 143 | 144 | reset() { 145 | super.reset(); 146 | this.obstacleHistory = []; 147 | } 148 | } 149 | 150 | export default Obstacles; 151 | -------------------------------------------------------------------------------- /src/prefabs/horizon/obstacles/bird/AnimationManager.js: -------------------------------------------------------------------------------- 1 | import CONFIG from '../../../../config/game'; 2 | 3 | /** 4 | * Bird animation manager 5 | * @class AnimationManager 6 | */ 7 | class AnimationManager { 8 | static CONFIG = CONFIG.PREFABS.OBSTACLES.BIRD; 9 | 10 | /** 11 | * Creates an instance of AnimationManager 12 | * @param {Bird} bird - The Bird to which this AnimationManager belongs 13 | */ 14 | constructor(bird) { 15 | this.bird = bird; 16 | 17 | // Init animation 18 | const flyFrames = bird.scene.anims.generateFrameNames('dino', { 19 | frames: AnimationManager.CONFIG.FRAMES.FLYING, 20 | }); 21 | bird.scene.anims.create({ key: 'fly', frames: flyFrames, frameRate: 6, repeat: -1 }); 22 | 23 | // Register event handlers 24 | bird.on('animationstart', this.resizeBodyOnAnim, this); 25 | bird.on('animationupdate', this.resizeBodyOnAnim, this); 26 | } 27 | 28 | /** 29 | * Update anims 30 | */ 31 | update() { 32 | this.bird.anims.play('fly', true); 33 | } 34 | 35 | /** 36 | * Stop anims 37 | */ 38 | stop() { 39 | this.bird.anims.stop(); 40 | } 41 | 42 | /** 43 | * Resize body on animation events 44 | * @param {Phaser.Animations.Animation} anim 45 | * @param {Phaser.Animations.AnimationFrame} frame 46 | */ 47 | resizeBodyOnAnim(anim, frame) { 48 | const { name } = frame.frame; 49 | 50 | const bodyZoneWidth = 76; 51 | const bodyZoneHeight = 44; 52 | const bodyZoneXOffset = 12; 53 | const bodyZoneYOffset = 12; 54 | 55 | // Resize body to match frame dimensions 56 | if (name === AnimationManager.CONFIG.FRAMES.FLYING[0]) { 57 | const frameYOffset = 12; 58 | this.bird.setBodySize(bodyZoneWidth, bodyZoneHeight); 59 | this.bird.body.setOffset(bodyZoneXOffset, frameYOffset + bodyZoneYOffset); 60 | } else if (name === AnimationManager.CONFIG.FRAMES.FLYING[1]) { 61 | this.bird.setBodySize(bodyZoneWidth, bodyZoneHeight); 62 | this.bird.body.setOffset(bodyZoneXOffset, bodyZoneYOffset); 63 | } 64 | } 65 | } 66 | 67 | export default AnimationManager; 68 | -------------------------------------------------------------------------------- /src/prefabs/horizon/obstacles/bird/Bird.js: -------------------------------------------------------------------------------- 1 | import Phaser from 'phaser'; 2 | 3 | import CONFIG from '../../../../config/game'; 4 | import Obstacle from '../Obstacle'; 5 | import AnimationManager from './AnimationManager'; 6 | 7 | /** 8 | * Bird obstacle 9 | * @class Bird 10 | * @extends {Obstacle} 11 | */ 12 | class Bird extends Obstacle { 13 | static CONFIG = CONFIG.PREFABS.OBSTACLES.BIRD; 14 | 15 | /** 16 | * Creates an instance of Bird 17 | * @param {Phaser.Scene} scene - The Scene to which this Bird belongs 18 | * @param {number} x - The horizontal position of this Bird in the world 19 | * @param {number} y - The vertical position of this Bird in the world 20 | * @override 21 | */ 22 | constructor(scene, x, y) { 23 | super(scene, x, y, Bird.CONFIG.FRAMES.INITIAL); 24 | 25 | this.animationManager = new AnimationManager(this); 26 | this.speedOffset = Phaser.Math.RND.pick(Bird.CONFIG.SPEED.OFFSET); 27 | } 28 | 29 | /** 30 | * Update bird 31 | * @param {number} speed - Current game speed 32 | * @override 33 | */ 34 | update(speed) { 35 | this.animationManager.update(); 36 | super.update(speed); 37 | } 38 | 39 | /** 40 | * Move bird 41 | * @param {number} speed - Current game speed 42 | * @override 43 | */ 44 | move(speed) { 45 | this.x -= speed + this.speedOffset; 46 | } 47 | 48 | /** 49 | * Freeze bird 50 | * @override 51 | */ 52 | freeze() { 53 | super.freeze(); 54 | this.animationManager.stop(); 55 | } 56 | } 57 | 58 | export default Bird; 59 | -------------------------------------------------------------------------------- /src/prefabs/horizon/obstacles/cactus/Cactus.js: -------------------------------------------------------------------------------- 1 | import Obstacle from '../Obstacle'; 2 | 3 | /** 4 | * Cactus obstacle 5 | * @class Cactus 6 | * @extends {Obstacle} 7 | */ 8 | class Cactus extends Obstacle { 9 | /** 10 | * Update cactus 11 | * @param {number} speed - Current game speed 12 | */ 13 | update(speed) { 14 | super.update(speed); 15 | 16 | // reduce body size by excluding borders 17 | const borderWidth = 2; 18 | this.setBodySize(this.width - borderWidth * 2, this.height - borderWidth); 19 | this.body.setOffset(borderWidth, borderWidth); 20 | } 21 | } 22 | 23 | export default Cactus; 24 | -------------------------------------------------------------------------------- /src/prefabs/horizon/stars/Star.js: -------------------------------------------------------------------------------- 1 | import Phaser from 'phaser'; 2 | 3 | import CONFIG from '../../../config/game'; 4 | 5 | /** 6 | * Star 7 | * @class Star 8 | * @extends {Phaser.GameObjects.Image} 9 | */ 10 | class Star extends Phaser.GameObjects.Image { 11 | static CONFIG = CONFIG.PREFABS.STAR; 12 | 13 | /** 14 | * Creates an instance of Star 15 | * @param {Phaser.Scene} scene - The Scene to which this Star belongs 16 | * @param {number} [x=Player.CONFIG.POS.INITIAL_X] - The horizontal position of this Star in the world 17 | * @param {number} [y=Player.CONFIG.POS.Y] - The vertical position of this Star in the world 18 | * @param {Phaser.Animations.AnimationFrame} [frame=Phaser.Math.RND.pick(Star.CONFIG.FRAMES)] - The frame to display this Star 19 | */ 20 | constructor(scene, x, y, frame = Phaser.Math.RND.pick(Star.CONFIG.FRAMES)) { 21 | super(scene, x, y, 'dino', frame); 22 | 23 | // Init image 24 | this.setOrigin(0, 0); 25 | this.setDepth(100); 26 | 27 | this.scene.add.existing(this); 28 | } 29 | 30 | /** 31 | * Update star 32 | * @param {number} speed - Current game speed 33 | */ 34 | update(speed) { 35 | this.move(speed); 36 | } 37 | 38 | /** 39 | * Move star 40 | * @param {number} speed - Current game speed 41 | */ 42 | move(speed) { 43 | this.x -= speed; 44 | } 45 | } 46 | 47 | export default Star; 48 | -------------------------------------------------------------------------------- /src/prefabs/horizon/stars/Stars.js: -------------------------------------------------------------------------------- 1 | import Phaser from 'phaser'; 2 | 3 | import CONFIG from '../../../config/game'; 4 | import Star from './Star'; 5 | 6 | /** 7 | * Stars Group 8 | * @class Stars 9 | * @extends {Phaser.GameObjects.Group} 10 | */ 11 | class Stars extends Phaser.GameObjects.Group { 12 | static CONFIG = CONFIG.PREFABS.STAR; 13 | 14 | /** 15 | * Creates an instance of Stars 16 | * @param {Phaser.Scene} scene - The Scene to which this Stars group belongs 17 | */ 18 | constructor(scene) { 19 | super(scene); 20 | 21 | this.scene.add.existing(this); 22 | } 23 | 24 | /** 25 | * Update stars group 26 | * @param {number} speed - Current game speed 27 | */ 28 | update(speed) { 29 | const { width } = this.scene.scale.gameSize; 30 | 31 | this.children.each(star => { 32 | star.update(speed); 33 | 34 | if (star.x + star.width < 0) { 35 | star.setPosition(width, this.getRandomYPos()); 36 | star.setFrame(this.getRandomFrame()); 37 | } 38 | }); 39 | } 40 | 41 | /** 42 | * Spawn stars 43 | */ 44 | spawnItems() { 45 | const { MAX_COUNT } = CONFIG.SCENES.GAME.NIGHTMODE.STARS; 46 | 47 | for (let i = this.getLength(); i < MAX_COUNT; i += 1) { 48 | this.add(new Star(this.scene)); 49 | } 50 | 51 | this.shuffleItems(); 52 | } 53 | 54 | /** 55 | * Shuffle stars 56 | */ 57 | shuffleItems() { 58 | const { width } = this.scene.scale.gameSize; 59 | const { MAX_COUNT } = CONFIG.SCENES.GAME.NIGHTMODE.STARS; 60 | const xWidth = width / MAX_COUNT; 61 | 62 | this.children.each((star, i) => { 63 | const frame = this.getRandomFrame(); 64 | const x = this.getRandomXPos(xWidth, i); 65 | const y = this.getRandomYPos(); 66 | 67 | star.setPosition(x, y); 68 | star.setFrame(frame); 69 | }); 70 | } 71 | 72 | /** 73 | * Get random star frame 74 | */ 75 | // eslint-disable-next-line class-methods-use-this 76 | getRandomFrame() { 77 | return Phaser.Math.RND.pick(Stars.CONFIG.FRAMES); 78 | } 79 | 80 | /** 81 | * Get random star x position 82 | */ 83 | // eslint-disable-next-line class-methods-use-this 84 | getRandomXPos(xWidth, i) { 85 | return Phaser.Math.RND.between(xWidth * i, xWidth * (i + 1)); 86 | } 87 | 88 | /** 89 | * Get random star y position 90 | */ 91 | // eslint-disable-next-line class-methods-use-this 92 | getRandomYPos() { 93 | const { MIN, MAX } = Stars.CONFIG.POS.Y; 94 | return Phaser.Math.RND.between(MIN, MAX); 95 | } 96 | 97 | /** 98 | * Reset stars group 99 | */ 100 | reset() { 101 | this.clear(true, true); 102 | } 103 | } 104 | 105 | export default Stars; 106 | -------------------------------------------------------------------------------- /src/prefabs/player/AnimationManager.js: -------------------------------------------------------------------------------- 1 | import Phaser from 'phaser'; 2 | 3 | import CONFIG from '../../config/game'; 4 | 5 | /** 6 | * Player animation manager 7 | * @class AnimationManager 8 | */ 9 | class AnimationManager { 10 | static CONFIG = CONFIG.PREFABS.PLAYER; 11 | 12 | /** 13 | * Creates an instance of AnimationManager 14 | * @param {Player} player - The Player to which this AnimationManager belongs 15 | */ 16 | constructor(player) { 17 | this.player = player; 18 | 19 | this.initAnimations(); 20 | 21 | // Register event handlers 22 | player.on('animationstart', this.onAnimationEvents, this); 23 | player.on('animationupdate', this.onAnimationEvents, this); 24 | player.on('animationcomplete', this.onAnimationEvents, this); 25 | player.on('animationstart-idle', this.randomizeidleAnimation, this); 26 | player.on('animationrepeat-idle', this.randomizeidleAnimation, this); 27 | } 28 | 29 | /** 30 | * Init animations 31 | */ 32 | initAnimations() { 33 | const { scene } = this.player; 34 | 35 | const idleFrames = scene.anims.generateFrameNames('dino', { 36 | frames: AnimationManager.CONFIG.FRAMES.IDLING, 37 | }); 38 | scene.anims.create({ 39 | key: 'idle', 40 | frames: idleFrames, 41 | frameRate: 1, 42 | repeat: -1, 43 | }); 44 | 45 | const runFrames = scene.anims.generateFrameNames('dino', { 46 | frames: AnimationManager.CONFIG.FRAMES.RUNNING, 47 | }); 48 | scene.anims.create({ key: 'run', frames: runFrames, frameRate: 10, repeat: -1 }); 49 | 50 | const duckFrames = scene.anims.generateFrameNames('dino', { 51 | frames: AnimationManager.CONFIG.FRAMES.DUCKING, 52 | }); 53 | scene.anims.create({ key: 'duck', frames: duckFrames, frameRate: 10, repeat: -1 }); 54 | } 55 | 56 | /** 57 | * Set animation based on player state 58 | */ 59 | update() { 60 | const { STATES, FRAMES } = AnimationManager.CONFIG; 61 | const { player } = this; 62 | 63 | switch (player.state) { 64 | case STATES.IDLING: 65 | player.anims.play('idle', true); 66 | break; 67 | case STATES.RUNNING: 68 | player.anims.play('run', true); 69 | break; 70 | case STATES.DUCKING: 71 | player.anims.play('duck', true); 72 | break; 73 | case STATES.DEAD: 74 | player.anims.stop(); 75 | player.setFrame(FRAMES.DEAD); 76 | break; 77 | case STATES.JUMPING: 78 | default: 79 | player.anims.stop(); 80 | player.setFrame(FRAMES.JUMPING); 81 | player.resizeBodyToMatchFrame(player.frame); 82 | break; 83 | } 84 | } 85 | 86 | /** 87 | * Handle animation events 88 | * @param {Phaser.Animations.Animation} animation - Animation that triggered the event 89 | * @param {Phaser.Animations.AnimationFrame} frame - Current Animation Frame 90 | */ 91 | onAnimationEvents(animation, frame) { 92 | this.player.resizeBodyToMatchFrame(frame.frame); 93 | } 94 | 95 | /** 96 | * Randomize next idle animation blink effect duration 97 | * @param {Phaser.Animations.Animation} idleAnimation - Idle animation 98 | */ 99 | randomizeidleAnimation = idleAnimation => { 100 | const { BLINK } = AnimationManager.CONFIG; 101 | const eyeFrame = idleAnimation.getFrameAt(0); 102 | const blinkFrame = idleAnimation.getFrameAt(1); 103 | eyeFrame.duration = Phaser.Math.RND.between(0, BLINK.DELAY); 104 | blinkFrame.duration = -1 * idleAnimation.msPerFrame + BLINK.DURATION; 105 | }; 106 | } 107 | 108 | export default AnimationManager; 109 | -------------------------------------------------------------------------------- /src/prefabs/player/InputManager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Player input manager 3 | * @class InputManager 4 | */ 5 | class InputManager { 6 | /** 7 | * Creates an instance of InputManager 8 | * @param {Player} player - The Player to which this InputManager belongs 9 | */ 10 | constructor(player) { 11 | this.player = player; 12 | this.scene = player.scene; 13 | this.cursors = player.scene.input.keyboard.createCursorKeys(); 14 | } 15 | 16 | /** 17 | * Update inputManager 18 | */ 19 | update() { 20 | const { player } = this; 21 | 22 | if (player.isDead) { 23 | return; 24 | } 25 | 26 | if (player.isOnFloor) { 27 | if (this.isDuckKeyPressed) { 28 | player.duck(); 29 | } else if (this.isJumpKeyPressed) { 30 | player.jump(); 31 | } else { 32 | player.run(); 33 | } 34 | return; 35 | } 36 | 37 | if (!player.isOnFloor) { 38 | if (this.isDuckKeyPressed) { 39 | player.speedFall(); 40 | } else if (this.isJumpKeyPressed) { 41 | player.jump(); 42 | } else { 43 | player.idle(); 44 | } 45 | } 46 | } 47 | 48 | /** 49 | * Check if duck key is pressed 50 | * @readonly 51 | * @returns {boolean} 52 | */ 53 | get isDuckKeyPressed() { 54 | return this.cursors.down.isDown; 55 | } 56 | 57 | /** 58 | * Check if jump key is pressed 59 | * @readonly 60 | * @returns {boolean} 61 | */ 62 | get isJumpKeyPressed() { 63 | const { activePointer } = this.scene.input; 64 | return ( 65 | this.cursors.up.isDown || 66 | this.cursors.space.isDown || 67 | (activePointer.isDown && activePointer.wasTouch) 68 | ); 69 | } 70 | } 71 | 72 | export default InputManager; 73 | -------------------------------------------------------------------------------- /src/prefabs/player/PhysicsManager.js: -------------------------------------------------------------------------------- 1 | import CONFIG from '../../config/game'; 2 | 3 | /** 4 | * Player physics manager 5 | * @class PhysicsManager 6 | */ 7 | class PhysicsManager { 8 | static CONFIG = CONFIG.PREFABS.PLAYER; 9 | 10 | /** 11 | * Creates an instance of PhysicsManager 12 | * @param {Player} player - The Player to which this PhysicsManager belongs 13 | */ 14 | constructor(player) { 15 | this.player = player; 16 | this.scene = player.scene; 17 | 18 | // Init physics 19 | this.scene.physics.world.enable(player); 20 | player.setCollideWorldBounds(true); 21 | player.setGravityY(PhysicsManager.CONFIG.GRAVITY.Y * 2); 22 | player.setAccelerationY(PhysicsManager.CONFIG.JUMP.ACCELERATION); 23 | // player.setAccelerationY(5000); 24 | player.setMaxVelocity(0, PhysicsManager.CONFIG.JUMP.VELOCITY.MAX); 25 | } 26 | 27 | /** 28 | * Check if player is on floor 29 | * @readonly 30 | * @returns {boolean} 31 | */ 32 | get isOnFloor() { 33 | return this.player.body.onFloor(); 34 | } 35 | 36 | /** 37 | * Handle player jump 38 | */ 39 | jump() { 40 | // Handle jumping while on floor 41 | if (this.isOnFloor) { 42 | // this.player.setVelocityY(PhysicsManager.CONFIG.JUMP.VELOCITY.MAX * -1); 43 | this.player.setVelocityY(PhysicsManager.CONFIG.JUMP.VELOCITY.START); 44 | return; 45 | } 46 | 47 | // Handle jumping while mid-air 48 | const { INCREASE_THRESHOLD, INCREASE_INCREMENT } = PhysicsManager.CONFIG.JUMP.VELOCITY; 49 | const velocityY = this.player.body.velocity.y; 50 | if (velocityY < INCREASE_THRESHOLD) { 51 | this.player.setVelocityY(velocityY + INCREASE_INCREMENT); 52 | } 53 | } 54 | 55 | /** 56 | * Handle speed fall 57 | */ 58 | speedFall() { 59 | this.player.setVelocityY(PhysicsManager.CONFIG.JUMP.VELOCITY.SPEED_FALL); 60 | } 61 | 62 | /** 63 | * Reset physicsManager 64 | */ 65 | reset() { 66 | this.player.setVelocityY(0); 67 | } 68 | 69 | /** 70 | * Resize body to match frame dimensions 71 | * @param {Phaser.Textures.Frame} frame - Current Player texture frame 72 | */ 73 | resizeBodyToMatchFrame(frame) { 74 | const { name, width, height } = frame; 75 | 76 | // Resize body to reduce player collisions 77 | if ( 78 | name === PhysicsManager.CONFIG.FRAMES.JUMPING || 79 | PhysicsManager.CONFIG.FRAMES.IDLING.includes(name) || 80 | PhysicsManager.CONFIG.FRAMES.RUNNING.includes(name) 81 | ) { 82 | const headZone = 15; 83 | const tailZone = 25; 84 | const topZone = 4; 85 | this.player.setBodySize(width - headZone - tailZone, height - topZone); 86 | this.player.body.setOffset(tailZone, topZone); 87 | } else if (PhysicsManager.CONFIG.FRAMES.DUCKING.includes(name)) { 88 | const headZone = 35; 89 | const tailZone = 25; 90 | const topZone = 6; 91 | this.player.setBodySize(width - headZone - tailZone, height - topZone); 92 | this.player.body.setOffset(tailZone, topZone); 93 | } 94 | } 95 | } 96 | 97 | export default PhysicsManager; 98 | -------------------------------------------------------------------------------- /src/prefabs/player/Player.js: -------------------------------------------------------------------------------- 1 | import Phaser from 'phaser'; 2 | 3 | import CONFIG from '../../config/game'; 4 | import PhysicsManager from './PhysicsManager'; 5 | import InputManager from './InputManager'; 6 | import AnimationManager from './AnimationManager'; 7 | 8 | /** 9 | * Player 10 | * @class Player 11 | * @extends {Phaser.Physics.Arcade.Sprite} 12 | */ 13 | class Player extends Phaser.Physics.Arcade.Sprite { 14 | static CONFIG = CONFIG.PREFABS.PLAYER; 15 | 16 | /** 17 | * Creates an instance of Player 18 | * @param {Phaser.Scene} scene - The Scene to which this Player belongs 19 | * @param {number} [x=Player.CONFIG.POS.INITIAL_X] - The horizontal position of this Player in the world 20 | * @param {number} [y=Player.CONFIG.POS.Y] - The vertical position of this Player in the world 21 | */ 22 | constructor(scene, x = Player.CONFIG.POS.INITIAL_X, y = Player.CONFIG.POS.Y) { 23 | super(scene, x, y, 'dino', Player.CONFIG.FRAMES.INITIAL); 24 | 25 | this.isInitialJump = true; 26 | 27 | // Init managers 28 | this.physicsManager = new PhysicsManager(this); 29 | this.inputManager = new InputManager(this); 30 | this.animationManager = new AnimationManager(this); 31 | 32 | // Init image 33 | this.setOrigin(0, 1); 34 | this.setDepth(1000); 35 | 36 | // Register event handlers 37 | this.scene.events.on(CONFIG.EVENTS.GAME_INTRO_START, this.onIntroStart, this); 38 | this.scene.events.on(CONFIG.EVENTS.GAME_RESTART, this.reset, this); 39 | this.scene.events.on(CONFIG.EVENTS.GAME_OVER, this.die, this); 40 | 41 | // Set initial state 42 | this.idle(); 43 | this.animationManager.update(); 44 | 45 | this.scene.add.existing(this); 46 | } 47 | 48 | /** 49 | * Update player 50 | */ 51 | update() { 52 | if (this.scene.intro.isWaiting && this.isOnFloor && (this.isRunning || this.isDucking)) { 53 | this.scene.intro.start(); 54 | } 55 | 56 | this.inputManager.update(); 57 | this.animationManager.update(); 58 | } 59 | 60 | /** 61 | * Handle game intro start 62 | */ 63 | onIntroStart() { 64 | this.scene.tweens.add({ 65 | targets: this, 66 | duration: CONFIG.SCENES.GAME.INTRO.DURATION, 67 | x: Player.CONFIG.POS.X, 68 | onComplete: () => { 69 | this.scene.intro.complete(); 70 | }, 71 | }); 72 | } 73 | 74 | /** 75 | * Set player idling 76 | */ 77 | idle() { 78 | this.setState(Player.CONFIG.STATES.IDLING); 79 | } 80 | 81 | /** 82 | * Set player running 83 | */ 84 | run() { 85 | this.setState(Player.CONFIG.STATES.RUNNING); 86 | } 87 | 88 | /** 89 | * Set player ducking 90 | */ 91 | duck() { 92 | this.setState(Player.CONFIG.STATES.DUCKING); 93 | } 94 | 95 | /** 96 | * Set player jumping 97 | */ 98 | jump() { 99 | // Allow intro to start after 1st landing even if holding jump key 100 | if (this.isOnFloor && !this.isInitialJump && this.scene.intro.isWaiting) { 101 | this.run(); 102 | return; 103 | } 104 | this.isInitialJump = false; 105 | 106 | this.setState(Player.CONFIG.STATES.JUMPING); 107 | this.physicsManager.jump(); 108 | if (!this.scene.intro.isWaiting && this.isOnFloor) { 109 | this.scene.events.emit(CONFIG.EVENTS.PLAYER_ACTION); 110 | } 111 | } 112 | 113 | /** 114 | * Set player speed fall 115 | */ 116 | speedFall() { 117 | this.physicsManager.speedFall(); 118 | } 119 | 120 | /** 121 | * Set player dead | Handle gameover 122 | */ 123 | die() { 124 | this.setState(Player.CONFIG.STATES.DEAD); 125 | this.animationManager.update(); 126 | this.physicsManager.reset(); 127 | 128 | // set dead sprite pos to match run|jump sprite pos (substract 4px) 129 | this.setY(this.y - 4); 130 | } 131 | 132 | /** 133 | * Reset player | Handle game restart 134 | */ 135 | reset() { 136 | this.setState(Player.CONFIG.STATES.IDLING); 137 | this.animationManager.update(); 138 | this.setY(Player.CONFIG.POS.Y); 139 | } 140 | 141 | /** 142 | * Check if player is idling 143 | * @readonly 144 | * @returns {boolean} 145 | */ 146 | get isIdling() { 147 | return this.state === Player.CONFIG.STATES.IDLING; 148 | } 149 | 150 | /** 151 | * Check if player is running 152 | * @readonly 153 | * @returns {boolean} 154 | */ 155 | get isRunning() { 156 | return this.state === Player.CONFIG.STATES.RUNNING; 157 | } 158 | 159 | /** 160 | * Check if player is ducking 161 | * @readonly 162 | * @returns {boolean} 163 | */ 164 | get isDucking() { 165 | return this.state === Player.CONFIG.STATES.DUCKING; 166 | } 167 | 168 | /** 169 | * Check if player is jumping 170 | * @readonly 171 | * @returns {boolean} 172 | */ 173 | get isJumping() { 174 | return this.state === Player.CONFIG.STATES.JUMPING; 175 | } 176 | 177 | /** 178 | * Check if player is dead 179 | * @readonly 180 | * @returns {boolean} 181 | */ 182 | get isDead() { 183 | return this.state === Player.CONFIG.STATES.DEAD; 184 | } 185 | 186 | /** 187 | * Check if player is on floor 188 | * @readonly 189 | * @returns {boolean} 190 | */ 191 | get isOnFloor() { 192 | return this.physicsManager.isOnFloor; 193 | } 194 | 195 | /** 196 | * Set player state 197 | * @param {Player.state} state 198 | * @returns {Player} 199 | * @throws Will throw an error if state argument is invalid 200 | */ 201 | setState(state) { 202 | if (Object.hasOwnProperty.call(Player.CONFIG.STATES, state)) { 203 | this.state = state; 204 | return this; 205 | } 206 | 207 | throw new Error(`Invalid Player State: ${state}`); 208 | } 209 | 210 | /** 211 | * Resize body to match frame dimensions 212 | * @param {Phaser.Textures.Frame} frame - Current Player texture frame 213 | */ 214 | resizeBodyToMatchFrame(frame) { 215 | this.physicsManager.resizeBodyToMatchFrame(frame); 216 | } 217 | } 218 | 219 | export default Player; 220 | -------------------------------------------------------------------------------- /src/prefabs/ui/gameover/GameOverPanel.js: -------------------------------------------------------------------------------- 1 | import CONFIG from '../../../config/game'; 2 | 3 | /** 4 | * Game over panel 5 | * @class GameOverPanel 6 | */ 7 | class GameOverPanel { 8 | /** 9 | * Creates an instance of GameOverPanel 10 | * @param {Phaser.Scene} scene - The Scene to which this Player belongs 11 | */ 12 | constructor(scene) { 13 | const gameWidth = scene.scale.gameSize.width; 14 | const gameHeight = scene.scale.gameSize.height; 15 | 16 | this.scene = scene; 17 | this.zone = scene.add.zone().setOrigin(0, 0); 18 | this.gameOverText = scene.add 19 | .bitmapText(gameWidth / 2, gameHeight / 2 - 75, 'joystix', 'G A M E O V E R', 32) 20 | .setOrigin(0.5, 0.5) 21 | .setDepth(9999) 22 | .setVisible(false); 23 | this.restartImg = scene.add 24 | .image(gameWidth / 2, gameHeight / 2, 'dino', 'restart') 25 | .setOrigin(0.5, 0.5) 26 | .setDepth(9999) 27 | .setVisible(false); 28 | 29 | // Register event handlers 30 | this.scene.events.on(CONFIG.EVENTS.GAME_OVER, this.show, this); 31 | this.scene.events.on(CONFIG.EVENTS.GAME_RESTART, this.hide, this); 32 | this.zone.on('pointerdown', this.onClick, this); 33 | this.zone.on('pointerup', this.onClick, this); 34 | } 35 | 36 | /** 37 | * Handle click 38 | * @param {Phaser.Input.Pointer} pointer 39 | */ 40 | onClick(pointer) { 41 | if (!pointer.wasTouch || pointer.noButtonDown()) { 42 | this.scene.events.emit(CONFIG.EVENTS.GAME_RESTART); 43 | } 44 | } 45 | 46 | /** 47 | * Show gameover panel 48 | */ 49 | show() { 50 | const { x, y, width, height } = this.gameOverText; 51 | const { bottom } = this.restartImg.getBounds(); 52 | 53 | this.gameOverText.setVisible(true); 54 | this.restartImg.setVisible(true); 55 | this.zone.setX(x - width / 2); 56 | this.zone.setY(y - height / 2); 57 | this.zone.setSize(width, bottom - (y - height / 2)); 58 | this.zone.setInteractive(); 59 | 60 | // uncomment this line to debug interactive area 61 | // this.scene.input.enableDebug(this.zone, 0x00ff00); 62 | } 63 | 64 | /** 65 | * Hide gameover panel 66 | */ 67 | hide() { 68 | this.gameOverText.setVisible(false); 69 | this.restartImg.setVisible(false); 70 | this.zone.disableInteractive(); 71 | } 72 | 73 | /** 74 | * Resize gameover panel 75 | * @param {Phaser.Structs.Size} gameSize - Current game size 76 | */ 77 | resize(gameSize) { 78 | this.gameOverText.setX(gameSize.width / 2); 79 | this.restartImg.setX(gameSize.width / 2); 80 | 81 | const { x, y, width, height } = this.gameOverText; 82 | this.zone.setPosition(x - width / 2, y - height / 2); 83 | } 84 | } 85 | 86 | export default GameOverPanel; 87 | -------------------------------------------------------------------------------- /src/prefabs/ui/score/BaseScorePanel.js: -------------------------------------------------------------------------------- 1 | import CONFIG from '../../../config/game'; 2 | 3 | /** 4 | * Base score panel 5 | * @class BaseScorePanel 6 | */ 7 | class BaseScorePanel { 8 | static CONFIG = CONFIG.SCENES.GAME.GAME.SCORE; 9 | 10 | /** 11 | * Creates an instance of BaseScorePanel 12 | * @param {Phaser.Scene} scene - The Scene to which this Player belongs 13 | */ 14 | constructor(scene) { 15 | this.scene = scene; 16 | 17 | this.isFlashing = false; 18 | this.flashTween = this.createFlashTween(); 19 | // set maxScore to 10^MAX_LENGTH - 1 e.g. MAX_LENGTH = 5 so maxScore = 10^5 - 1 = 99_999 20 | this.maxScoreLength = BaseScorePanel.CONFIG.MAX_LENGTH; 21 | this.maxScore = 10 ** (this.maxScoreLength - 1) - 1; 22 | this.defaultString = ''; 23 | 24 | // Register event handlers 25 | this.scene.events.on(CONFIG.EVENTS.GAME_START, this.onStart, this); 26 | this.scene.events.on(CONFIG.EVENTS.GAME_RESTART, this.onRestart, this); 27 | this.scene.events.on(CONFIG.EVENTS.GAME_OVER, this.onGameOver, this); 28 | 29 | this.createScoreText(); 30 | } 31 | 32 | /** 33 | * Create {Phaser.GameObjects. BitmapText} `scoreText` 34 | * @abstract 35 | * @throws Will throw an error if not implemented 36 | */ 37 | // eslint-disable-next-line class-methods-use-this 38 | createScoreText() { 39 | // create scoreText: BitmapText var here 40 | throw new Error('Method must be implemented by subclass'); 41 | } 42 | 43 | /** 44 | * Create score text flash tween 45 | * @param {number} duration - Tween duration 46 | * @param {number} iterations - Tween iterations 47 | * @param {function} [onStart=() => {}] - A function to be executed when tween starts 48 | * @param {function} [onComplete=() => {}] - A function to be executed when tween completes 49 | * @returns {Phaser.Tweens.Tween} Created Tween object 50 | */ 51 | createFlashTween(duration, iterations, onStart = () => {}, onComplete = () => {}) { 52 | return this.scene.tweens.create({ 53 | targets: this.scoreText, 54 | duration: 0, 55 | alpha: { 56 | start: 1, 57 | to: 10e-3, 58 | }, 59 | repeat: iterations, 60 | repeatDelay: duration, 61 | yoyo: true, 62 | hold: duration, 63 | completeDelay: duration, 64 | onStart: () => { 65 | this.isFlashing = true; 66 | onStart(); 67 | }, 68 | onComplete: () => { 69 | this.isFlashing = false; 70 | this.scoreText.setAlpha(1); 71 | onComplete(); 72 | }, 73 | }); 74 | } 75 | 76 | /** 77 | * Update score panel 78 | * @param {number} score - Current game score 79 | */ 80 | update(score) { 81 | if (score >= this.maxScore) { 82 | this.maxScoreLength += 1; 83 | this.maxScore = 10 ** (this.maxScoreLength - 1) - 1; 84 | } 85 | } 86 | 87 | /** 88 | * Set score 89 | * @param {number} score - Current game score 90 | */ 91 | setScore(score) { 92 | // create score string and padStart it to MAX_LENGTH with zeros 93 | // e.g. MAX_LENGTH = 5 and score = 418, so scoreString will be '00418' 94 | const scoreString = this.defaultString + score.toString().padStart(this.maxScoreLength, '0'); 95 | this.scoreText.setText(scoreString); 96 | } 97 | 98 | /** 99 | * Clear score 100 | */ 101 | clearScore() { 102 | this.setScore(0); 103 | } 104 | 105 | /** 106 | * Flash score text 107 | */ 108 | flashScore() { 109 | this.flashTween = this.createFlashTween(); 110 | this.flashTween.play(); 111 | } 112 | 113 | /** 114 | * Reset score panel 115 | */ 116 | reset() { 117 | this.flashTween.complete(); 118 | } 119 | 120 | /** 121 | * Handle game start 122 | */ 123 | // eslint-disable-next-line class-methods-use-this 124 | onStart() {} 125 | 126 | /** 127 | * Handle game restart 128 | */ 129 | onRestart() { 130 | this.isFlashing = false; 131 | this.flashTween = this.createFlashTween(); 132 | this.maxScoreLength = BaseScorePanel.CONFIG.MAX_LENGTH; 133 | this.maxScore = 10 ** (this.maxScoreLength - 1) - 1; 134 | } 135 | 136 | /** 137 | * Handle gameover 138 | */ 139 | onGameOver() { 140 | this.reset(); 141 | } 142 | } 143 | 144 | export default BaseScorePanel; 145 | -------------------------------------------------------------------------------- /src/prefabs/ui/score/CurrentScorePanel.js: -------------------------------------------------------------------------------- 1 | import CONFIG from '../../../config/game'; 2 | import BaseScorePanel from './BaseScorePanel'; 3 | 4 | /** 5 | * Current score panel 6 | * @class CurrentScorePanel 7 | * @extends {BaseScorePanel} 8 | */ 9 | class CurrentScorePanel extends BaseScorePanel { 10 | static CONFIG = CONFIG.SCENES.GAME.GAME.SCORE; 11 | 12 | /** 13 | * Create {Phaser.GameObjects. BitmapText} `scoreText` 14 | * @override 15 | */ 16 | createScoreText() { 17 | this.scoreText = this.scene.add 18 | .bitmapText(0, 0, 'joystix', '', 32) 19 | .setOrigin(1, 0) 20 | .setDepth(9999); 21 | } 22 | 23 | /** 24 | * Create score text flash tween 25 | * @returns {Phaser.Tweens.Tween} Created Tween object 26 | * @override 27 | */ 28 | createFlashTween() { 29 | const { DURATION, ITERATIONS } = CurrentScorePanel.CONFIG.ACHIEVEMENT.FLASH; 30 | return super.createFlashTween(DURATION, ITERATIONS); 31 | } 32 | 33 | /** 34 | * Update high score panel 35 | * @param {boolean} isPlaying - Whether game is running 36 | * @param {number} currentScore - Current game score 37 | * @override 38 | */ 39 | update(isPlaying, currentScore) { 40 | super.update(currentScore); 41 | 42 | if ( 43 | isPlaying && 44 | currentScore > 0 && 45 | currentScore % CurrentScorePanel.CONFIG.DISTANCE === 0 && 46 | !this.isFlashing 47 | ) { 48 | this.scene.events.emit(CONFIG.EVENTS.ACHIEVEMENT); 49 | this.flashScore(); 50 | } 51 | 52 | if (isPlaying && !this.isFlashing) { 53 | this.setScore(currentScore); 54 | } 55 | } 56 | 57 | /** 58 | * Handle gameover 59 | * @override 60 | * @param {number} currentScore - Current game score 61 | */ 62 | onGameOver(currentScore) { 63 | super.onGameOver(); 64 | this.setScore(currentScore); 65 | } 66 | } 67 | 68 | export default CurrentScorePanel; 69 | -------------------------------------------------------------------------------- /src/prefabs/ui/score/HighScorePanel.js: -------------------------------------------------------------------------------- 1 | import Phaser from 'phaser'; 2 | 3 | import CONFIG from '../../../config/game'; 4 | import BaseScorePanel from './BaseScorePanel'; 5 | 6 | /** 7 | * High score panel 8 | * @class HighScorePanel 9 | * @extends {BaseScorePanel} 10 | */ 11 | class HighScorePanel extends BaseScorePanel { 12 | static CONFIG = CONFIG.SCENES.GAME.GAME.HIGH_SCORE; 13 | 14 | /** 15 | * Creates an instance of HighScorePanel 16 | * @param {Phaser.Scene} scene - The Scene to which this InputManager belongs 17 | * @param {boolean} [disableReset=false] - Is resetting disabled 18 | * @override 19 | */ 20 | constructor(scene, disableReset = false) { 21 | super(scene); 22 | 23 | this.scene = scene; 24 | this.defaultString = 'HI '; 25 | 26 | // Register event handlers 27 | this.scene.events.on(CONFIG.EVENTS.HIGH_SCORE_FETCH.SUCCESS, this.onFetchScore, this); 28 | this.scene.events.on(CONFIG.EVENTS.HIGH_SCORE_SAVE.SUCCESS, this.onSaveScore, this); 29 | if (!disableReset) { 30 | this.scoreText.on('pointerdown', this.onClick, this); 31 | } 32 | } 33 | 34 | /** 35 | * Create {Phaser.GameObjects. BitmapText} `scoreText` 36 | * @override 37 | */ 38 | createScoreText() { 39 | this.scoreText = this.scene.add 40 | .bitmapText(0, 0, 'joystix', '', 32) 41 | .setTintFill(0x757575) 42 | .setOrigin(1, 0) 43 | .setDepth(9999); 44 | } 45 | 46 | /** 47 | * Create score text flash tween 48 | * @returns {Phaser.Tweens.Tween} Created Tween object 49 | * @override 50 | */ 51 | createFlashTween() { 52 | const { DURATION, ITERATIONS } = HighScorePanel.CONFIG.GAMEOVER.FLASH; 53 | const onStart = () => this.scoreText.clearTint(); 54 | const onComplete = () => this.scoreText.setTintFill(0x757575); 55 | 56 | return super.createFlashTween(DURATION, ITERATIONS, onStart, onComplete); 57 | } 58 | 59 | /** 60 | * Handle high score success fetch 61 | * @param {number} highScore - Fetched high score 62 | */ 63 | onFetchScore(highScore) { 64 | this.setScore(highScore); 65 | } 66 | 67 | /** 68 | * Handle high score success save 69 | * @param {number} highScore - Saved high score 70 | */ 71 | onSaveScore(highScore) { 72 | this.setScore(highScore); 73 | } 74 | 75 | /** 76 | * Handle click 77 | */ 78 | onClick() { 79 | if (!this.isFlashing) { 80 | this.flashScore(); 81 | } else { 82 | this.reset(); 83 | this.scoreText.disableInteractive(); 84 | this.scene.events.emit(CONFIG.EVENTS.HIGH_SCORE_UPDATE, 0); 85 | } 86 | } 87 | 88 | /** 89 | * Handle game restart 90 | * @override 91 | */ 92 | onRestart() { 93 | this.reset(); 94 | this.scoreText.disableInteractive(); 95 | super.onRestart(); 96 | } 97 | 98 | /** 99 | * Handle gameover 100 | * @override 101 | */ 102 | onGameOver() { 103 | super.onGameOver(); 104 | const { width, height } = this.scoreText; 105 | this.scoreText.setInteractive({ 106 | hitArea: new Phaser.Geom.Rectangle(0, 0, width, height), 107 | }); 108 | 109 | // uncomment this line to debug interactive area 110 | // this.scene.input.enableDebug(this.scoreText, 0x00ff00); 111 | } 112 | } 113 | 114 | export default HighScorePanel; 115 | -------------------------------------------------------------------------------- /src/prefabs/ui/score/ScoreZone.js: -------------------------------------------------------------------------------- 1 | import CurrentScore from './CurrentScorePanel'; 2 | import HighScore from './HighScorePanel'; 3 | 4 | /** 5 | * Score panels zone 6 | * @class ScoreZone 7 | */ 8 | class ScoreZone { 9 | /** 10 | * Creates an instance of ScoreZone 11 | * @param {CurrentScorePanel} currentScorePanel - CurrentScorePanel to be placed inside this ScoreZone 12 | * @param {HighScorePanel} highScorePanel - HighScorePanel to be placed inside this ScoreZone 13 | */ 14 | constructor(currentScorePanel, highScorePanel) { 15 | this.currentScorePanel = currentScorePanel; 16 | this.highScorePanel = highScorePanel; 17 | } 18 | 19 | /** 20 | * Adjust score zone position 21 | * @param {Phaser.Structs.Size} gameSize - Current game size 22 | */ 23 | adjustPosition({ width }) { 24 | const { scoreText: currentScoreText } = this.currentScorePanel; 25 | const { scoreText: highScoreText } = this.highScorePanel; 26 | 27 | const currentScoreX = width * CurrentScore.CONFIG.POS.X.OFFSET; 28 | const currentScoreY = CurrentScore.CONFIG.POS.Y; 29 | currentScoreText.setPosition(currentScoreX, currentScoreY); 30 | 31 | const currentScoreWidth = currentScoreText.width; 32 | const highScoreX = currentScoreX - currentScoreWidth - HighScore.CONFIG.POS.X.OFFSET; 33 | const highScoreY = HighScore.CONFIG.POS.Y; 34 | highScoreText.setPosition(highScoreX, highScoreY); 35 | } 36 | 37 | /** 38 | * Update score zone 39 | * @param {Phaser.Structs.Size} gameSize - Current game size 40 | */ 41 | update(gameSize) { 42 | this.adjustPosition(gameSize); 43 | } 44 | 45 | /** 46 | * Resize score zone 47 | * @param {Phaser.Structs.Size} gameSize - Current game size 48 | */ 49 | resize(gameSize) { 50 | this.adjustPosition(gameSize); 51 | } 52 | } 53 | 54 | export default ScoreZone; 55 | -------------------------------------------------------------------------------- /src/scenes/boot/BootScene.js: -------------------------------------------------------------------------------- 1 | import Phaser from 'phaser'; 2 | 3 | import CONFIG from '../../config/game'; 4 | import GameScene from '../game/GameScene'; 5 | 6 | /** 7 | * Boot game scene 8 | * @class BootScene 9 | * @extends {Phaser.Scene} 10 | */ 11 | class BootScene extends Phaser.Scene { 12 | static CONFIG = CONFIG.SCENES.BOOT; 13 | 14 | /** 15 | * Creates an instance of BootScene 16 | */ 17 | constructor() { 18 | super(BootScene.CONFIG.NAME); 19 | } 20 | 21 | preload() { 22 | this.load.atlas('dino', 'assets/sprites/dino-atlas.png', 'assets/sprites/dino-atlas.json'); 23 | 24 | this.load.bitmapFont( 25 | 'joystix', 26 | 'assets/fonts/joystix_monospace.png', 27 | 'assets/fonts/joystix_monospace.fnt', 28 | ); 29 | 30 | this.load.audio('player-action', 'assets/sounds/player-action.mp3'); 31 | this.load.audio('achievement', 'assets/sounds/achievement.mp3'); 32 | this.load.audio('gameover', 'assets/sounds/gameover.mp3'); 33 | } 34 | 35 | create() { 36 | this.scene.start(GameScene.CONFIG.NAME); 37 | } 38 | } 39 | 40 | export default BootScene; 41 | -------------------------------------------------------------------------------- /src/scenes/game/GameScene.js: -------------------------------------------------------------------------------- 1 | import Phaser from 'phaser'; 2 | 3 | import CONFIG from '../../config/game'; 4 | import InputManager from './InputManager'; 5 | import SoundManager from './SoundManager'; 6 | import ResizeManager from './ResizeManager'; 7 | import LocalScoreManager from './score/LocalScoreManager'; 8 | import TelegramScoreManager from './score/TelegramScoreManager'; 9 | import UI from './UI'; 10 | import Intro from './Intro'; 11 | import Player from '../../prefabs/player/Player'; 12 | import Horizon from '../../prefabs/horizon/Horizon'; 13 | import isTelegramMode from '../../utils/telegram/isTelegramMode'; 14 | 15 | /** 16 | * Main game scene 17 | * @class GameScene 18 | * @extends {Phaser.Scene} 19 | */ 20 | class GameScene extends Phaser.Scene { 21 | static CONFIG = CONFIG.SCENES.GAME; 22 | 23 | constructor() { 24 | super(GameScene.CONFIG.NAME); 25 | } 26 | 27 | init() { 28 | // Init game state vars 29 | this.isInitialStart = true; 30 | this.isPlaying = false; 31 | this.readyToRestart = false; 32 | 33 | // Init speed vars 34 | this.speed = 0; 35 | this.maxSpeed = 0; 36 | this.initSpeed(); 37 | 38 | // Init scoring vars 39 | this.distance = 0; 40 | this.highScore = 0; 41 | 42 | // Init managers 43 | this.soundManager = new SoundManager(this); 44 | this.inputManager = new InputManager(this); 45 | this.resizeManager = new ResizeManager(this, { 46 | canvas: this.onResizeCanvas.bind(this), 47 | camera: this.onResizeCamera.bind(this), 48 | gameSpeed: this.onResizeGameSpeed.bind(this), 49 | gameObjects: this.onResizeGameObjects.bind(this), 50 | }); 51 | this.scoreManager = isTelegramMode() 52 | ? new TelegramScoreManager(this.events) 53 | : new LocalScoreManager(this.events); 54 | 55 | // Register event handlers 56 | this.events.on(CONFIG.EVENTS.GAME_START, this.onGameStart, this); 57 | this.events.on(CONFIG.EVENTS.GAME_INTRO_START, this.onIntroStart, this); 58 | this.events.on(CONFIG.EVENTS.GAME_INTRO_COMPLETE, this.onIntroComplete, this); 59 | this.events.on(CONFIG.EVENTS.GAME_RESTART, this.onGameRestart, this); 60 | this.events.on(CONFIG.EVENTS.GAME_OVER, this.onGameOver, this); 61 | this.events.on(CONFIG.EVENTS.HIGH_SCORE_UPDATE, this.onHighScoreUpdate, this); 62 | } 63 | 64 | /** 65 | * Init game speed 66 | */ 67 | initSpeed() { 68 | const { width } = this.scale.gameSize; 69 | const { INITIAL, MAX, MOBILE_COEFFICIENT } = GameScene.CONFIG.GAME.OBSTACLES.SPEED; 70 | 71 | if (width === CONFIG.GAME.WIDTH.LANDSCAPE) { 72 | this.speed = INITIAL; 73 | this.maxSpeed = MAX; 74 | } else if (width === CONFIG.GAME.WIDTH.PORTRAIT) { 75 | this.speed = INITIAL / MOBILE_COEFFICIENT; 76 | this.maxSpeed = MAX / MOBILE_COEFFICIENT; 77 | } 78 | } 79 | 80 | create() { 81 | this.ui = new UI(this); 82 | this.intro = new Intro(this.events); 83 | 84 | this.player = new Player(this); 85 | 86 | this.horizon = new Horizon(this); 87 | this.ground = this.horizon.ground; 88 | this.obstacles = this.horizon.obstacles; 89 | this.nightMode = this.horizon.nightMode; 90 | 91 | this.physics.add.collider(this.player, this.ground); 92 | this.physics.add.overlap(this.player, this.obstacles, this.onPlayerHitObstacle, null, this); 93 | 94 | this.resizeManager.resize(this.scale.gameSize, this.scale.parentSize); 95 | 96 | this.scoreManager 97 | .getHighScore() 98 | .then(highScore => { 99 | this.highScore = highScore; 100 | }) 101 | .catch(() => {}); 102 | } 103 | 104 | update() { 105 | const { gameSize } = this.scale; 106 | const isMobile = gameSize.width === CONFIG.GAME.WIDTH.PORTRAIT; 107 | 108 | this.inputManager.update(); 109 | this.ui.update(this.isPlaying, gameSize, this.score); 110 | 111 | if (this.isPlaying) { 112 | this.player.update(); 113 | 114 | if (this.intro.isComplete) { 115 | const { GAME, NIGHTMODE } = GameScene.CONFIG; 116 | const { OBSTACLES } = GAME; 117 | 118 | if (this.speed < this.maxSpeed) { 119 | this.speed += OBSTACLES.ACCELERATION; 120 | } else { 121 | this.speed = this.maxSpeed; 122 | } 123 | 124 | this.distance += this.speed; 125 | 126 | if (this.shouldNightModeStart) { 127 | this.nightMode.enable(); 128 | this.time.delayedCall(NIGHTMODE.DURATION, () => { 129 | if (this.isPlaying && this.nightMode.isEnabled) { 130 | this.nightMode.disable(); 131 | } 132 | }); 133 | } 134 | 135 | this.horizon.update(this.speed, isMobile); 136 | } 137 | } 138 | } 139 | 140 | /** 141 | * Handle player collision with obstacle 142 | */ 143 | onPlayerHitObstacle() { 144 | this.events.emit(CONFIG.EVENTS.GAME_OVER, this.score, this.highScore); 145 | } 146 | 147 | /** 148 | * Handle game start 149 | */ 150 | onGameStart() { 151 | this.isPlaying = true; 152 | this.isInitialStart = false; 153 | this.ui.highScorePanel.setScore(this.highScore); 154 | } 155 | 156 | /** 157 | * Handle game intro start 158 | */ 159 | onIntroStart() { 160 | const { width } = this.scale.gameSize; 161 | this.tweens.add({ 162 | targets: this.cameras.main, 163 | duration: GameScene.CONFIG.INTRO.DURATION, 164 | width, 165 | }); 166 | } 167 | 168 | /** 169 | * Handle game intro complete 170 | */ 171 | onIntroComplete() { 172 | const { canvas, gameSize, parentSize } = this.scale; 173 | const originalTransition = canvas.style.transition; 174 | const newTransition = `${CONFIG.SCENES.GAME.STYLES.TRANSITION}, ${originalTransition}`; 175 | 176 | canvas.style.transition = newTransition; 177 | this.resizeManager.resizeCanvas(gameSize, parentSize); 178 | canvas.addEventListener('transitionend', () => { 179 | canvas.style.transition = originalTransition; 180 | this.resizeManager.resizeCanvas(gameSize, parentSize); 181 | }); 182 | } 183 | 184 | /** 185 | * Handle game restart 186 | */ 187 | onGameRestart() { 188 | this.isPlaying = true; 189 | this.readyToRestart = false; 190 | 191 | this.distance = 0; 192 | this.speed = 0; 193 | this.maxSpeed = 0; 194 | this.initSpeed(); 195 | 196 | this.physics.resume(); 197 | 198 | this.scoreManager 199 | .getHighScore() 200 | .then(highScore => { 201 | this.highScore = highScore; 202 | }) 203 | .catch(() => {}); 204 | } 205 | 206 | /** 207 | * Handle gameover 208 | */ 209 | onGameOver() { 210 | const { width: gameWidth, height: gameHeight } = this.scale.gameSize; 211 | 212 | this.isPlaying = false; 213 | this.physics.pause(); 214 | this.scale.resize(gameWidth, gameHeight); 215 | 216 | if (this.game.device.features.vibration) { 217 | navigator.vibrate(GameScene.CONFIG.GAMEOVER.VIBRATION); 218 | } 219 | 220 | if (this.score > this.highScore) { 221 | this.events.emit(CONFIG.EVENTS.HIGH_SCORE_UPDATE, this.score); 222 | } 223 | } 224 | 225 | /** 226 | * Handle high score update 227 | * @param {number} highScore - Updated high score 228 | */ 229 | onHighScoreUpdate(highScore) { 230 | this.scoreManager 231 | .saveHighScore(highScore) 232 | .then(() => { 233 | this.highScore = highScore; 234 | }) 235 | .catch(() => {}); 236 | } 237 | 238 | /** 239 | * Get current score 240 | * @readonly 241 | * @returns {number} - Current score 242 | */ 243 | get score() { 244 | return Math.ceil(this.distance * GameScene.CONFIG.GAME.SCORE.COEFFICIENT); 245 | } 246 | 247 | /** 248 | * Check if night mode should start 249 | * @readonly 250 | */ 251 | get shouldNightModeStart() { 252 | const { score, nightMode } = this; 253 | const { DISTANCE } = GameScene.CONFIG.NIGHTMODE; 254 | return score > 0 && score % DISTANCE === 0 && !nightMode.isEnabled; 255 | } 256 | 257 | /** 258 | * Handle canvas resize 259 | * @param {Phaser.Structs.Size} gameSize - Current game size 260 | */ 261 | onResizeCanvas(gameSize) { 262 | const { width, height } = gameSize; 263 | 264 | if (!this.intro.isComplete) { 265 | return { 266 | width: width * 0.8, 267 | height: height * 0.8, 268 | }; 269 | } 270 | 271 | return { 272 | width, 273 | height, 274 | }; 275 | } 276 | 277 | /** 278 | * Handle game speed resize 279 | * @param {Phaser.Structs.Size} gameSize - Current game size 280 | */ 281 | onResizeGameSpeed(gameSize) { 282 | const { MAX, MOBILE_COEFFICIENT } = GameScene.CONFIG.GAME.OBSTACLES.SPEED; 283 | 284 | if (gameSize.width === CONFIG.GAME.WIDTH.LANDSCAPE) { 285 | this.speed *= MOBILE_COEFFICIENT; 286 | this.maxSpeed = MAX; 287 | } else if (gameSize.width === CONFIG.GAME.WIDTH.PORTRAIT) { 288 | this.speed /= MOBILE_COEFFICIENT; 289 | this.maxSpeed = MAX / MOBILE_COEFFICIENT; 290 | } 291 | } 292 | 293 | /** 294 | * Handle camera resize 295 | * @param {Phaser.Structs.Size} gameSize - Current game size 296 | */ 297 | onResizeCamera(gameSize) { 298 | const { width, height } = gameSize; 299 | const { main: mainCamera } = this.cameras; 300 | 301 | mainCamera.setOrigin(0, 0.5); 302 | 303 | if (this.intro.isComplete) { 304 | mainCamera.setViewport(0, 0, width, height); 305 | } else { 306 | mainCamera.setViewport(0, 0, GameScene.CONFIG.INTRO.CAMERA.WIDTH, height); 307 | } 308 | } 309 | 310 | /** 311 | * Handle gameobjects resize 312 | * @param {Phaser.Structs.Size} gameSize - Current game size 313 | */ 314 | onResizeGameObjects(gameSize) { 315 | this.ui.resize(gameSize); 316 | this.ground.resize(gameSize); 317 | } 318 | } 319 | 320 | export default GameScene; 321 | -------------------------------------------------------------------------------- /src/scenes/game/InputManager.js: -------------------------------------------------------------------------------- 1 | import Phaser from 'phaser'; 2 | 3 | import CONFIG from '../../config/game'; 4 | 5 | /** 6 | * Game input manager 7 | * @class InputManager 8 | */ 9 | class InputManager { 10 | /** 11 | * InputManager 12 | * @static 13 | */ 14 | static CONFIG = CONFIG.SCENES.GAME; 15 | 16 | /** 17 | * Creates an instance of InputManager 18 | * @param {Phaser.Scene} scene - The Scene to which this InputManager belongs 19 | */ 20 | constructor(scene) { 21 | this.scene = scene; 22 | 23 | this.cursors = this.scene.input.keyboard.createCursorKeys(); 24 | this.enterKey = this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.ENTER); 25 | 26 | // Register event handlers 27 | scene.events.on(CONFIG.EVENTS.GAME_OVER, this.onGameOver, this); 28 | } 29 | 30 | /** 31 | * Update inputManager 32 | */ 33 | update() { 34 | const { scene } = this; 35 | 36 | if (scene.isInitialStart) { 37 | if (this.isUpKeyPressed) { 38 | scene.events.emit(CONFIG.EVENTS.GAME_START); 39 | } 40 | return; 41 | } 42 | 43 | if (!scene.isPlaying) { 44 | if (this.isEnterKeyJustUp) { 45 | scene.events.emit(CONFIG.EVENTS.GAME_RESTART); 46 | } 47 | 48 | if (scene.readyToRestart) { 49 | if (this.isUpKeyJustUp) { 50 | scene.events.emit(CONFIG.EVENTS.GAME_RESTART); 51 | } 52 | } 53 | } 54 | } 55 | 56 | /** 57 | * Handle gameover 58 | */ 59 | onGameOver() { 60 | const { scene } = this; 61 | 62 | scene.time.delayedCall(InputManager.CONFIG.RESTART.COOLDOWN, () => { 63 | scene.input.keyboard.resetKeys(); 64 | 65 | if (!scene.isPlaying) { 66 | scene.readyToRestart = true; 67 | } 68 | }); 69 | } 70 | 71 | /** 72 | * Check if up key is pressed 73 | * @readonly 74 | * @returns {boolean} 75 | */ 76 | get isUpKeyPressed() { 77 | const { activePointer } = this.scene.input; 78 | 79 | return ( 80 | this.cursors.up.isDown || 81 | this.cursors.space.isDown || 82 | (activePointer.isDown && activePointer.wasTouch) 83 | ); 84 | } 85 | 86 | /** 87 | * Check if up key is just up 88 | * @readonly 89 | * @returns {boolean} 90 | */ 91 | get isUpKeyJustUp() { 92 | return ( 93 | Phaser.Input.Keyboard.JustUp(this.cursors.up) || 94 | Phaser.Input.Keyboard.JustUp(this.cursors.space) 95 | ); 96 | } 97 | 98 | /** 99 | * Check if enter key is just up 100 | * @readonly 101 | * @returns {boolean} 102 | */ 103 | get isEnterKeyJustUp() { 104 | return Phaser.Input.Keyboard.JustUp(this.enterKey); 105 | } 106 | } 107 | 108 | export default InputManager; 109 | -------------------------------------------------------------------------------- /src/scenes/game/Intro.js: -------------------------------------------------------------------------------- 1 | import CONFIG from '../../config/game'; 2 | 3 | /** 4 | * Game intro 5 | * @class Intro 6 | */ 7 | class Intro { 8 | static CONFIG = CONFIG.SCENES.GAME.INTRO; 9 | 10 | /** 11 | * Creates an instance of Intro 12 | * @param {Phaser.Events.EventEmitter} eventEmitter - The EventEmitter which this Intro uses 13 | */ 14 | constructor(eventEmitter) { 15 | this.eventEmitter = eventEmitter; 16 | this.setState(Intro.CONFIG.STATES.WAITING); 17 | } 18 | 19 | /** 20 | * Start intro 21 | */ 22 | start() { 23 | this.setState(Intro.CONFIG.STATES.ONGOING); 24 | this.eventEmitter.emit(CONFIG.EVENTS.GAME_INTRO_START); 25 | } 26 | 27 | /** 28 | * Complete intro 29 | */ 30 | complete() { 31 | this.setState(Intro.CONFIG.STATES.COMPLETE); 32 | this.eventEmitter.emit(CONFIG.EVENTS.GAME_INTRO_COMPLETE); 33 | } 34 | 35 | /** 36 | * Set intro state 37 | * @param {string} state - State to be set 38 | * @returns {Intro} this 39 | * @throws Will throw an error if state argument is invalid 40 | */ 41 | setState(state) { 42 | if (Object.hasOwnProperty.call(Intro.CONFIG.STATES, state)) { 43 | this.state = state; 44 | return this; 45 | } 46 | 47 | throw new Error(`Invalid Intro State: ${state}`); 48 | } 49 | 50 | /** 51 | * Check if intro state is `waiting` 52 | * @readonly 53 | */ 54 | get isWaiting() { 55 | return this.state === Intro.CONFIG.STATES.WAITING; 56 | } 57 | 58 | /** 59 | * Check if intro state is `ongoing` 60 | * @readonly 61 | */ 62 | get isOngoing() { 63 | return this.state === Intro.CONFIG.STATES.ONGOING; 64 | } 65 | 66 | /** 67 | * Check if intro state is `complete` 68 | * @readonly 69 | */ 70 | get isComplete() { 71 | return this.state === Intro.CONFIG.STATES.COMPLETE; 72 | } 73 | } 74 | 75 | export default Intro; 76 | -------------------------------------------------------------------------------- /src/scenes/game/ResizeManager.js: -------------------------------------------------------------------------------- 1 | import CONFIG from '../../config/game'; 2 | 3 | /** 4 | * Resize manager 5 | * @class ResizeManager 6 | */ 7 | class ResizeManager { 8 | static CONFIG = CONFIG.SCENES.GAME; 9 | 10 | /** 11 | * Creates an instance of ResizeManager 12 | * @param {Phaser.Scene} scene - The Scene to which this ResizeManager belongs 13 | * @param {*} resizeCallbacks - Callback to be called 14 | */ 15 | constructor(scene, { canvas, camera, gameSpeed, gameObjects }) { 16 | this.game = scene.game; 17 | this.canvas = scene.game.canvas; 18 | this.scale = scene.scale; 19 | 20 | this.callbacks = { 21 | canvas, 22 | camera, 23 | gameSpeed, 24 | gameObjects, 25 | }; 26 | 27 | this.gameWidth = this.scale.gameSize.width; 28 | 29 | // Register event handlers 30 | this.scale.on('resize', () => this.resize(this.scale.gameSize, this.scale.parentSize)); 31 | } 32 | 33 | /** 34 | * Resize scene 35 | * @param {Phaser.Structs.Size} gameSize - Current game size 36 | * @param {Phaser.Structs.Size} parentSize - Current parent size 37 | */ 38 | resize(gameSize, parentSize) { 39 | this.resizeCanvas(gameSize, parentSize); 40 | this.resizeGameSpeed(gameSize); 41 | this.resizeCamera(gameSize); 42 | this.resizeGameObjects(gameSize); 43 | } 44 | 45 | /** 46 | * Resize canvas 47 | * @param {Phaser.Structs.Size} gameSize - Current game size 48 | * @param {Phaser.Structs.Size} parentSize - Current parent size 49 | */ 50 | resizeCanvas(gameSize, parentSize) { 51 | const { width, height } = this.callbacks.canvas({ 52 | width: parentSize.width, 53 | height: gameSize.height * (parentSize.width / gameSize.width), 54 | }); 55 | 56 | this.canvas.style.width = `${width}px`; 57 | this.canvas.style.height = `${height}px`; 58 | } 59 | 60 | /** 61 | * Resize game speed 62 | * @param {Phaser.Structs.Size} gameSize - Current game size 63 | * @param {Phaser.Structs.Size} parentSize - Current parent size 64 | */ 65 | resizeGameSpeed(gameSize) { 66 | if (this.gameWidth !== gameSize.width) { 67 | this.callbacks.gameSpeed(gameSize); 68 | } 69 | 70 | this.gameWidth = gameSize.width; 71 | } 72 | 73 | /** 74 | * Resize camera 75 | * @param {Phaser.Structs.Size} gameSize - Current game size 76 | * @param {Phaser.Structs.Size} parentSize - Current parent size 77 | */ 78 | resizeCamera(gameSize) { 79 | this.callbacks.camera(gameSize); 80 | } 81 | 82 | /** 83 | * Resize gameobjects 84 | * @param {Phaser.Structs.Size} gameSize - Current game size 85 | * @param {Phaser.Structs.Size} parentSize - Current parent size 86 | */ 87 | resizeGameObjects(gameSize) { 88 | this.callbacks.gameObjects(gameSize); 89 | } 90 | } 91 | 92 | export default ResizeManager; 93 | -------------------------------------------------------------------------------- /src/scenes/game/SoundManager.js: -------------------------------------------------------------------------------- 1 | import CONFIG from '../../config/game'; 2 | 3 | /** 4 | * Sound manager 5 | * @class SoundManager 6 | */ 7 | class SoundManager { 8 | /** 9 | * Creates an instance of SoundManager. 10 | * @param {Phaser.Scene} scene - The Scene to which this SoundManager belongs 11 | */ 12 | constructor(scene) { 13 | this.scene = scene; 14 | 15 | this.playerAction = scene.sound.add('player-action'); 16 | this.achievement = scene.sound.add('achievement'); 17 | this.gameover = scene.sound.add('gameover'); 18 | 19 | // Register event handlers 20 | scene.events.on(CONFIG.EVENTS.PLAYER_ACTION, this.playPlayerAction, this); 21 | scene.events.on(CONFIG.EVENTS.ACHIEVEMENT, this.playAchievement, this); 22 | scene.events.on(CONFIG.EVENTS.GAME_OVER, this.playGameOver, this); 23 | } 24 | 25 | /** 26 | * Play player action sound 27 | */ 28 | playPlayerAction() { 29 | this.playerAction.play(); 30 | } 31 | 32 | /** 33 | * Play achievement sound 34 | */ 35 | playAchievement() { 36 | this.achievement.play(); 37 | } 38 | 39 | /** 40 | * Play gameover sound 41 | */ 42 | playGameOver() { 43 | this.gameover.play(); 44 | } 45 | } 46 | 47 | export default SoundManager; 48 | -------------------------------------------------------------------------------- /src/scenes/game/UI.js: -------------------------------------------------------------------------------- 1 | import ScoreZone from '../../prefabs/ui/score/ScoreZone'; 2 | import CurrentScorePanel from '../../prefabs/ui/score/CurrentScorePanel'; 3 | import HighScorePanel from '../../prefabs/ui/score/HighScorePanel'; 4 | import GameOverPanel from '../../prefabs/ui/gameover/GameOverPanel'; 5 | import isTelegramMode from '../../utils/telegram/isTelegramMode'; 6 | 7 | /** 8 | * Game UI manager 9 | * @class UI 10 | */ 11 | class UI { 12 | /** 13 | * Creates an instance of UI 14 | * @param {Phaser.Scene} scene - The Scene to which this UI belongs 15 | */ 16 | constructor(scene) { 17 | this.scene = scene; 18 | this.gameOverPanel = new GameOverPanel(scene); 19 | this.currentScorePanel = new CurrentScorePanel(scene); 20 | this.highScorePanel = new HighScorePanel(scene, isTelegramMode()); 21 | this.scoreZone = new ScoreZone(this.currentScorePanel, this.highScorePanel); 22 | } 23 | 24 | /** 25 | * Update UI 26 | * @param {boolean} isPlaying - Whether game is running 27 | * @param {Phaser.Structs.Size} gameSize - Current game size 28 | * @param {number} score - Current game score 29 | */ 30 | update(isPlaying, gameSize, score) { 31 | this.currentScorePanel.update(isPlaying, score); 32 | this.highScorePanel.update(score); 33 | this.scoreZone.update(gameSize); 34 | } 35 | 36 | /** 37 | * Resize UI 38 | * @param {Phaser.Structs.Size} gameSize - Current game size 39 | */ 40 | resize(gameSize) { 41 | this.scoreZone.resize(gameSize); 42 | this.gameOverPanel.resize(gameSize); 43 | } 44 | } 45 | 46 | export default UI; 47 | -------------------------------------------------------------------------------- /src/scenes/game/score/BaseScoreManager.js: -------------------------------------------------------------------------------- 1 | import CONFIG from '../../../config/game'; 2 | 3 | /** 4 | * Base score manager 5 | * @class BaseScoreManager 6 | */ 7 | class BaseScoreManager { 8 | /** 9 | * Creates an instance of BaseScoreManager 10 | * @param {Phaser.Events.EventEmitter} eventEmitter - The game EventEmitter 11 | */ 12 | constructor(eventEmitter) { 13 | this.eventEmitter = eventEmitter; 14 | } 15 | 16 | /** 17 | * Get high score from localstorage 18 | * @async 19 | */ 20 | async getHighScore() { 21 | this.eventEmitter.emit(CONFIG.EVENTS.HIGH_SCORE_FETCH.REQUEST); 22 | } 23 | 24 | /** 25 | * Handle fetch high score success 26 | * @param {number} highScore - Fetched high score 27 | */ 28 | onGetSuccess(highScore) { 29 | this.eventEmitter.emit(CONFIG.EVENTS.HIGH_SCORE_FETCH.SUCCESS, highScore); 30 | } 31 | 32 | /** 33 | * Handle fetch high score fail 34 | */ 35 | onGetFail() { 36 | this.eventEmitter.emit(CONFIG.EVENTS.HIGH_SCORE_FETCH.FAIL); 37 | } 38 | 39 | /** 40 | * Save high score to localstorage 41 | * @async 42 | */ 43 | async saveHighScore() { 44 | this.eventEmitter.emit(CONFIG.EVENTS.HIGH_SCORE_SAVE.REQUEST); 45 | } 46 | 47 | /** 48 | * Handle save high score success 49 | * @param {number} highScore - Fetched high score 50 | */ 51 | onSaveSuccess(highScore) { 52 | this.eventEmitter.emit(CONFIG.EVENTS.HIGH_SCORE_SAVE.SUCCESS, highScore); 53 | } 54 | 55 | /** 56 | * Handle save high score fail 57 | */ 58 | onSaveFail() { 59 | this.eventEmitter.emit(CONFIG.EVENTS.HIGH_SCORE_SAVE.FAIL); 60 | } 61 | } 62 | 63 | export default BaseScoreManager; 64 | -------------------------------------------------------------------------------- /src/scenes/game/score/LocalScoreManager.js: -------------------------------------------------------------------------------- 1 | import CONFIG from '../../../config/game'; 2 | import BaseScoreManager from './BaseScoreManager'; 3 | 4 | /** 5 | * Local score manager. Operates with localstorage 6 | * @class LocalScoreManager 7 | * @extends {BaseScoreManager} 8 | */ 9 | class LocalScoreManager extends BaseScoreManager { 10 | storage = window.localStorage; 11 | 12 | static CONFIG = { 13 | STORAGE_KEY: CONFIG.SCENES.GAME.GAME.HIGH_SCORE.STORAGE_KEY, 14 | }; 15 | 16 | /** 17 | * Get high score from localstorage 18 | * @async 19 | * @override 20 | * @returns {Promise} 21 | */ 22 | async getHighScore() { 23 | super.getHighScore(); 24 | 25 | try { 26 | const { STORAGE_KEY } = LocalScoreManager.CONFIG; 27 | 28 | if (this.storage.getItem(STORAGE_KEY) === null) { 29 | this.resetHighScore(); 30 | } 31 | 32 | const highScore = Number.parseInt(this.storage.getItem(STORAGE_KEY), 10) || 0; 33 | this.onGetSuccess(highScore); 34 | return highScore; 35 | } catch (error) { 36 | this.onGetFail(); 37 | throw error; 38 | } 39 | } 40 | 41 | /** 42 | * Save high score to localstorage 43 | * @async 44 | * @override 45 | * @param {number} [highScore=0] - High score to save 46 | */ 47 | async saveHighScore(highScore = 0) { 48 | super.saveHighScore(); 49 | 50 | try { 51 | this.storage.setItem(LocalScoreManager.CONFIG.STORAGE_KEY, highScore); 52 | this.onSaveSuccess(highScore); 53 | return highScore; 54 | } catch (error) { 55 | this.onSaveFail(); 56 | throw error; 57 | } 58 | } 59 | } 60 | 61 | export default LocalScoreManager; 62 | -------------------------------------------------------------------------------- /src/scenes/game/score/TelegramScoreManager.js: -------------------------------------------------------------------------------- 1 | import BaseScoreManager from './BaseScoreManager'; 2 | import getTokenParam from '../../../utils/telegram/getTokenParam'; 3 | import fetchTimeout from '../../../utils/fetchTimeout'; 4 | 5 | /** 6 | * Telegram score manager 7 | * @class TelegramScoreManager 8 | * @extends {BaseScoreManager} 9 | */ 10 | class TelegramScoreManager extends BaseScoreManager { 11 | API_ENDPOINT = process.env.API_ENDPOINT; 12 | 13 | token = getTokenParam(); 14 | 15 | /** 16 | * Get high score for current user from Telegram 17 | * @async 18 | * @override 19 | * @returns {Promise} 20 | */ 21 | async getHighScore() { 22 | super.getHighScore(); 23 | 24 | try { 25 | const response = await fetchTimeout(`${this.API_ENDPOINT}`, 3000, { 26 | headers: { 27 | Authorization: `Bearer ${this.token}`, 28 | }, 29 | }); 30 | 31 | const scoreData = await response.json(); 32 | 33 | if (scoreData.ok === false) { 34 | throw new Error(scoreData.message); 35 | } 36 | 37 | const userHighScore = scoreData.userScore ? scoreData.userScore.score : 0; 38 | this.onGetSuccess(userHighScore); 39 | return userHighScore; 40 | } catch (error) { 41 | this.onGetFail(); 42 | throw error; 43 | } 44 | } 45 | 46 | /** 47 | * Submit current user high score to Telegram 48 | * @async 49 | * @override 50 | * @param {number} highScore - High score 51 | * @returns {Promise} 52 | */ 53 | async saveHighScore(score) { 54 | super.saveHighScore(); 55 | 56 | try { 57 | const response = await fetchTimeout(`${this.API_ENDPOINT}`, 3000, { 58 | method: 'POST', 59 | headers: { 60 | Authorization: `Bearer ${this.token}`, 61 | 'Content-Type': 'application/json;charset=utf-8', 62 | }, 63 | body: JSON.stringify({ 64 | score, 65 | }), 66 | }); 67 | 68 | const scoreData = await response.json(); 69 | 70 | if (scoreData.ok === false) { 71 | throw new Error(scoreData.message); 72 | } 73 | 74 | this.onSaveSuccess(scoreData.score); 75 | return scoreData.score; 76 | } catch (error) { 77 | this.onSaveFail(); 78 | throw error; 79 | } 80 | } 81 | } 82 | 83 | export default TelegramScoreManager; 84 | -------------------------------------------------------------------------------- /src/utils/fetchTimeout.js: -------------------------------------------------------------------------------- 1 | const fetchTimeout = (url, ms = 0, { signal, ...options } = {}) => { 2 | const controller = new AbortController(); 3 | const promise = fetch(url, { signal: controller.signal, ...options }); 4 | 5 | if (signal) { 6 | signal.addEventListener('abort', () => controller.abort()); 7 | } 8 | 9 | const timeoutId = setTimeout(() => controller.abort(), ms); 10 | return promise.finally(() => clearTimeout(timeoutId)); 11 | }; 12 | 13 | export default fetchTimeout; 14 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | export const { NODE_ENV } = process.env; 2 | export const isProd = NODE_ENV === 'production'; 3 | export const isDev = NODE_ENV === 'development'; 4 | export const isTest = NODE_ENV === 'test'; 5 | -------------------------------------------------------------------------------- /src/utils/telegram/games.js: -------------------------------------------------------------------------------- 1 | // https://telegram-js.azureedge.net/js/games.js 2 | (function () { 3 | var eventHandlers = {}; 4 | 5 | // Parse init params from location hash: for Android < 5.0, TDesktop 6 | var locationHash = ''; 7 | try { 8 | locationHash = location.hash.toString(); 9 | } catch (e) {} 10 | 11 | var initParams = urlParseHashParams(locationHash); 12 | 13 | var isIframe = false; 14 | try { 15 | isIframe = (window.parent != null && window != window.parent); 16 | } catch (e) {} 17 | 18 | 19 | function urlSafeDecode(urlencoded) { 20 | try { 21 | return decodeURIComponent(urlencoded); 22 | } catch (e) { 23 | return urlencoded; 24 | } 25 | } 26 | 27 | function urlParseHashParams(locationHash) { 28 | locationHash = locationHash.replace(/^#/, ''); 29 | var params = {}; 30 | if (!locationHash.length) { 31 | return params; 32 | } 33 | if (locationHash.indexOf('=') < 0 && locationHash.indexOf('?') < 0) { 34 | params._path = urlSafeDecode(locationHash); 35 | return params; 36 | } 37 | var qIndex = locationHash.indexOf('?'); 38 | if (qIndex >= 0) { 39 | var pathParam = locationHash.substr(0, qIndex); 40 | params._path = urlSafeDecode(pathParam); 41 | locationHash = locationHash.substr(qIndex + 1); 42 | } 43 | var locationHashParams = locationHash.split('&'); 44 | var i, param, paramName, paramValue; 45 | for (i = 0; i < locationHashParams.length; i++) { 46 | param = locationHashParams[i].split('='); 47 | paramName = urlSafeDecode(param[0]); 48 | paramValue = param[1] == null ? null : urlSafeDecode(param[1]); 49 | params[paramName] = paramValue; 50 | } 51 | return params; 52 | } 53 | 54 | // Telegram apps will implement this logic to add service params (e.g. tgShareScoreUrl) to game URL 55 | function urlAppendHashParams(url, addHash) { 56 | // url looks like 'https://game.com/path?query=1#hash' 57 | // addHash looks like 'tgShareScoreUrl=' + encodeURIComponent('tgb://share_game_score?hash=very_long_hash123') 58 | 59 | var ind = url.indexOf('#'); 60 | if (ind < 0) { 61 | // https://game.com/path -> https://game.com/path#tgShareScoreUrl=etc 62 | return url + '#' + addHash; 63 | } 64 | var curHash = url.substr(ind + 1); 65 | if (curHash.indexOf('=') >= 0 || curHash.indexOf('?') >= 0) { 66 | // https://game.com/#hash=1 -> https://game.com/#hash=1&tgShareScoreUrl=etc 67 | // https://game.com/#path?query -> https://game.com/#path?query&tgShareScoreUrl=etc 68 | return url + '&' + addHash; 69 | } 70 | // https://game.com/#hash -> https://game.com/#hash?tgShareScoreUrl=etc 71 | if (curHash.length > 0) { 72 | return url + '?' + addHash; 73 | } 74 | // https://game.com/# -> https://game.com/#tgShareScoreUrl=etc 75 | return url + addHash; 76 | } 77 | 78 | 79 | function postEvent (eventType, callback, eventData) { 80 | if (!callback) { 81 | callback = function () {}; 82 | } 83 | if (eventData === undefined) { 84 | eventData = ''; 85 | } 86 | 87 | if (window.TelegramWebviewProxy !== undefined) { 88 | TelegramWebviewProxy.postEvent(eventType, JSON.stringify(eventData)); 89 | callback(); 90 | } 91 | else if (window.external && 'notify' in window.external) { 92 | window.external.notify(JSON.stringify({eventType: eventType, eventData: eventData})); 93 | callback(); 94 | } 95 | else if (isIframe) { 96 | try { 97 | var trustedTarget = 'https://web.telegram.org'; 98 | // For now we don't restrict target, for testing purposes 99 | trustedTarget = '*'; 100 | window.parent.postMessage(JSON.stringify({eventType: eventType, eventData: eventData}), trustedTarget); 101 | } catch (e) { 102 | callback(e); 103 | } 104 | } 105 | else { 106 | callback({notAvailable: true}); 107 | } 108 | }; 109 | 110 | function receiveEvent(eventType, eventData) { 111 | var curEventHandlers = eventHandlers[eventType]; 112 | if (curEventHandlers === undefined || 113 | !curEventHandlers.length) { 114 | return; 115 | } 116 | for (var i = 0; i < curEventHandlers.length; i++) { 117 | try { 118 | curEventHandlers[i](eventType, eventData); 119 | } catch (e) {} 120 | } 121 | } 122 | 123 | function onEvent (eventType, callback) { 124 | if (eventHandlers[eventType] === undefined) { 125 | eventHandlers[eventType] = []; 126 | } 127 | var index = eventHandlers[eventType].indexOf(callback); 128 | if (index === -1) { 129 | eventHandlers[eventType].push(callback); 130 | } 131 | }; 132 | 133 | function offEvent (eventType, callback) { 134 | if (eventHandlers[eventType] === undefined) { 135 | return; 136 | } 137 | var index = eventHandlers[eventType].indexOf(callback); 138 | if (index === -1) { 139 | return; 140 | } 141 | eventHandlers[eventType].splice(index, 1); 142 | }; 143 | 144 | function openProtoUrl(url) { 145 | if (!url.match(/^(web\+)?tgb?:\/\/./)) { 146 | return false; 147 | } 148 | var useIframe = navigator.userAgent.match(/iOS|iPhone OS|iPhone|iPod|iPad/i) ? true : false; 149 | if (useIframe) { 150 | var iframeContEl = document.getElementById('tgme_frame_cont') || document.body; 151 | var iframeEl = document.createElement('iframe'); 152 | iframeContEl.appendChild(iframeEl); 153 | var pageHidden = false; 154 | var enableHidden = function () { 155 | pageHidden = true; 156 | }; 157 | window.addEventListener('pagehide', enableHidden, false); 158 | window.addEventListener('blur', enableHidden, false); 159 | if (iframeEl !== null) { 160 | iframeEl.src = url; 161 | } 162 | setTimeout(function() { 163 | if (!pageHidden) { 164 | window.location = url; 165 | } 166 | window.removeEventListener('pagehide', enableHidden, false); 167 | window.removeEventListener('blur', enableHidden, false); 168 | }, 2000); 169 | } 170 | else { 171 | window.location = url; 172 | } 173 | return true; 174 | } 175 | 176 | // For Windows Phone app 177 | window.TelegramGameProxy_receiveEvent = receiveEvent; 178 | 179 | window.TelegramGameProxy = { 180 | initParams: initParams, 181 | receiveEvent: receiveEvent, 182 | onEvent: onEvent, 183 | shareScore: function () { 184 | postEvent('share_score', function (error) { 185 | if (error) { 186 | var shareScoreUrl = initParams.tgShareScoreUrl; 187 | if (shareScoreUrl) { 188 | openProtoUrl(shareScoreUrl); 189 | } 190 | } 191 | }); 192 | }, 193 | paymentFormSubmit: function (formData) { 194 | if (!formData || 195 | !formData.credentials || 196 | formData.credentials.type !== 'card' || 197 | !formData.credentials.token || 198 | !formData.credentials.token.match(/^[A-Za-z0-9\/=_\-]{4,512}$/) || 199 | !formData.title) { 200 | console.error('[TgProxy] Invalid form data submitted', formData); 201 | throw Error('PaymentFormDataInvalid'); 202 | } 203 | postEvent('payment_form_submit', false, formData); 204 | } 205 | }; 206 | 207 | })(); 208 | -------------------------------------------------------------------------------- /src/utils/telegram/getTokenParam.js: -------------------------------------------------------------------------------- 1 | const getTokenParam = (tokenParamName = 'token') => { 2 | const params = new URLSearchParams(window.location.search); 3 | return params.has(tokenParamName) ? params.get(tokenParamName) : undefined; 4 | }; 5 | 6 | export default getTokenParam; 7 | -------------------------------------------------------------------------------- /src/utils/telegram/isTelegramMode.js: -------------------------------------------------------------------------------- 1 | import getTokenParam from './getTokenParam'; 2 | 3 | const isTelegramMode = () => Boolean(window.TelegramGameProxy && getTokenParam()); 4 | 5 | export default isTelegramMode; 6 | -------------------------------------------------------------------------------- /webpack/common.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { EnvironmentPlugin } = require('webpack'); 3 | const DotenvPlugin = require('dotenv-webpack'); 4 | const ESLintPlugin = require('eslint-webpack-plugin'); 5 | const HtmlPlugin = require('html-webpack-plugin'); 6 | const FaviconsPlugin = require('favicons-webpack-plugin'); 7 | const WebpackBarPlugin = require('webpackbar'); 8 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin'); 9 | 10 | module.exports = { 11 | output: { 12 | path: path.resolve(__dirname, '..', 'dist'), 13 | }, 14 | resolve: { 15 | alias: { 16 | phaser: path.resolve(__dirname, '../node_modules/phaser/dist/phaser-arcade-physics'), 17 | }, 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.js$/, 23 | exclude: /node_modules/, 24 | use: { 25 | loader: 'babel-loader', 26 | }, 27 | }, 28 | { 29 | test: /\.css$/, 30 | use: ['style-loader', 'css-loader', 'postcss-loader'], 31 | }, 32 | { 33 | test: [/\.vert$/, /\.frag$/], 34 | type: 'asset/source', 35 | }, 36 | { 37 | test: /\.(gif|png|jpe?g|svg|xml)$/i, 38 | type: 'asset/resource', 39 | }, 40 | ], 41 | }, 42 | plugins: [ 43 | new DotenvPlugin(), 44 | new EnvironmentPlugin({ 45 | CANVAS_RENDERER: true, 46 | WEBGL_RENDERER: true, 47 | API_ENDPOINT: process.env.API_ENDPOINT || null, 48 | }), 49 | new ESLintPlugin(), 50 | new HtmlPlugin({ 51 | template: './src/index.html', 52 | }), 53 | new FaviconsPlugin({ 54 | logo: './assets/icons/dino-start-icon.png', 55 | prefix: 'favicons/', 56 | favicons: { 57 | appName: 'Dino', 58 | start_url: '/', 59 | display: 'fullscreen', 60 | background: '#fff', 61 | theme_color: '#fff', 62 | icons: { 63 | coast: false, 64 | firefox: false, 65 | windows: false, 66 | yandex: false, 67 | }, 68 | }, 69 | }), 70 | new WebpackBarPlugin(), 71 | new FriendlyErrorsPlugin(), 72 | ], 73 | }; 74 | -------------------------------------------------------------------------------- /webpack/dev.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | 3 | const commonConfig = require('./common'); 4 | 5 | process.env.NODE_ENV = 'development'; 6 | 7 | module.exports = merge(commonConfig, { 8 | mode: 'development', 9 | devtool: 'eval-source-map', 10 | devServer: { 11 | host: 'localhost', 12 | open: true, 13 | }, 14 | infrastructureLogging: { 15 | level: 'none', 16 | debug: [/webpack-dev-server/], 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /webpack/prod.js: -------------------------------------------------------------------------------- 1 | const { mergeWithRules } = require('webpack-merge'); 2 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 3 | const CopyPlugin = require('copy-webpack-plugin'); 4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 5 | const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); 6 | const WorkboxPlugin = require('workbox-webpack-plugin'); 7 | 8 | const commonConfig = require('./common'); 9 | 10 | process.env.NODE_ENV = 'production'; 11 | 12 | module.exports = mergeWithRules({ 13 | module: { 14 | rules: { 15 | test: 'match', 16 | use: 'replace', 17 | }, 18 | }, 19 | })(commonConfig, { 20 | mode: 'production', 21 | output: { 22 | filename: '[name].[contenthash].js', 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.css$/, 28 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'], 29 | }, 30 | ], 31 | }, 32 | plugins: [ 33 | new CleanWebpackPlugin(), 34 | new CopyPlugin({ 35 | patterns: [ 36 | { 37 | from: 'assets', 38 | to: 'assets', 39 | globOptions: { 40 | ignore: ['**/_raw/**', '**/*.md'], 41 | }, 42 | }, 43 | ], 44 | }), 45 | new MiniCssExtractPlugin({ 46 | filename: '[name].[contenthash].css', 47 | }), 48 | new WorkboxPlugin.GenerateSW({ 49 | exclude: [/favicons[\\/].*\.(?:png|jpg|jpeg|gif|svg)$/i, /license/i], 50 | clientsClaim: true, 51 | skipWaiting: true, 52 | }), 53 | ], 54 | optimization: { 55 | runtimeChunk: 'single', 56 | splitChunks: { 57 | chunks: 'all', 58 | }, 59 | minimizer: ['...', new CssMinimizerPlugin()], 60 | }, 61 | performance: { 62 | maxEntrypointSize: 1.1 * 1024 * 1024, 63 | maxAssetSize: 1 * 1024 * 1024, 64 | }, 65 | stats: { 66 | assets: true, 67 | assetsSort: '!size', 68 | groupAssetsByEmitStatus: false, 69 | groupAssetsByInfo: false, 70 | groupAssetsByChunk: false, 71 | groupAssetsByPath: false, 72 | groupAssetsByExtension: false, 73 | excludeAssets: [/assets/, /favicons/], 74 | modules: false, 75 | errors: false, 76 | errorsCount: false, 77 | warnings: false, 78 | warningsCount: false, 79 | timings: false, 80 | version: false, 81 | }, 82 | }); 83 | --------------------------------------------------------------------------------