├── .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 |
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 |
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 |
--------------------------------------------------------------------------------