├── .gitattributes ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── readme └── myimage.gif ├── src ├── assets │ ├── icons │ │ ├── favicon.ico │ │ ├── icons-192.png │ │ └── icons-512.png │ ├── img │ │ ├── car-body.png │ │ ├── car-wheel.png │ │ ├── gas.png │ │ ├── grass.png │ │ ├── restart-btn.png │ │ ├── sky.png │ │ ├── tile.png │ │ └── wholes-small.png │ ├── terrain.svg │ ├── textureAtlas.json │ ├── textureAtlas.png │ └── textureAtlas.tps ├── index.html ├── pwa │ ├── manifest.json │ └── sw.js └── scripts │ ├── customCanvas.ts │ ├── game.ts │ ├── objects │ ├── bridge.ts │ ├── car.ts │ ├── gas.ts │ ├── restart.ts │ ├── terrain.ts │ └── terrainDynamic.ts │ └── scenes │ ├── backgroundScene.ts │ ├── guiScene.ts │ ├── mainScene.ts │ └── preloadScene.ts ├── tsconfig.json ├── typings ├── custom.d.ts └── phaser.d.ts └── webpack ├── webpack.common.js ├── webpack.dev.js └── webpack.prod.js /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js linguist-detectable=false 2 | *.html linguist-detectable=false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | /.cache 4 | /.vscode -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": false, 4 | "singleQuote": true, 5 | "printWidth": 120 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Yannick Deubel (https://github.com/yandeu); Project Url: https://github.com/yandeu/phaser-project-template 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 | # Phaser 3 + Matter.js example 2 | 3 | ![GitHub package.json version](https://img.shields.io/github/package-json/v/yandeu/phaser3-matter-car-on-curved-terrain.svg?style=flat-square) 4 | ![GitHub](https://img.shields.io/github/license/yandeu/phaser3-matter-car-on-curved-terrain.svg?style=flat-square) 5 | ![Code style](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square) 6 | 7 | ## Car on a curved terrain crosses a bridge 8 | 9 | Build with Phaser 3 using the [typescript phaser-project-template](https://github.com/yandeu/phaser-project-template#readme) 10 | 11 | ## Play 12 | 13 | [Play the game](https://s3.eu-central-1.amazonaws.com/phaser3-typescript/car-on-curved-tarrain/index.html) 14 | 15 | ![screenshot gif](readme/myimage.gif) 16 | 17 | ## Features 18 | 19 | - PWA 20 | - Using WebGL 2 21 | 22 | ## About 23 | 24 | To build this example I used Phaser 3 with the physics engine Matter.js. 25 | To make Matter work, it needs two additional libraries, **poly-decomp** and **pathseg**. 26 | 27 | Reading the source code of some classes (especially [Body](http://brm.io/matter-js/docs/files/src_body_Body.js.html#l436), [Bodies](http://brm.io/matter-js/docs/files/src_factory_Bodies.js.html#l102) and [Composites](http://brm.io/matter-js/docs/files/src_factory_Composites.js.html#l230)) of Matter and the [Phaser3-docs](https://photonstorm.github.io/phaser3-docs/Phaser.Physics.Matter.html) helped a lot. 28 | 29 | The mainScene takes a while to start and to restart. This is because I use [Matter.Svg.pathToVertices](http://brm.io/matter-js/docs/classes/Svg.html) to transform a SVG Path to an array of vectors at runtime. In a production game, I probably would only include the array of vectors in the game. 30 | 31 | ## Credits 32 | 33 | https://opengameart.org/content/free-off-road-racing-truck 34 | https://opengameart.org/content/monstertruck 35 | https://opengameart.org/content/sky-backdrop 36 | 37 | ## License 38 | 39 | The MIT License (MIT) 2019 - [Yannick Deubel](https://github.com/yandeu). Please have a look at the [LICENSE](LICENSE) for more details. 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phaser-project-template", 3 | "version": "3.16.2", 4 | "description": "Phaser 3 starter template with TypeScript and webpack.", 5 | "homepage": "https://github.com/yandeu/phaser-project-template#readme", 6 | "main": "index.js", 7 | "scripts": { 8 | "start": "webpack-dev-server --config webpack/webpack.dev.js", 9 | "build": "webpack --config webpack/webpack.prod.js" 10 | }, 11 | "keywords": [ 12 | "Phaser", 13 | "Phaser 3", 14 | "Phaser3", 15 | "html5 game", 16 | "TypeScript", 17 | "webpack", 18 | "starter" 19 | ], 20 | "author": { 21 | "name": "Yannick", 22 | "url": "https://github.com/yandeu" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/yandeu/phaser-project-template.git" 27 | }, 28 | "license": "MIT", 29 | "devDependencies": { 30 | "clean-webpack-plugin": "^1.0.1", 31 | "copy-webpack-plugin": "^4.6.0", 32 | "html-webpack-plugin": "^3.2.0", 33 | "ts-loader": "^5.4.5", 34 | "typescript": "^3.4.5", 35 | "webpack": "^4.30.0", 36 | "webpack-cli": "^3.3.1", 37 | "webpack-dev-server": "^3.3.1", 38 | "webpack-merge": "^4.2.1", 39 | "webpack-obfuscator": "^0.18.0", 40 | "workbox-webpack-plugin": "^3.6.3" 41 | }, 42 | "dependencies": { 43 | "pathseg": "^1.2.0", 44 | "phaser": "~3.16.1", 45 | "poly-decomp": "^0.3.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /readme/myimage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandeu/phaser3-matter-car-on-curved-terrain/530284ec4e032affa2f1fc9fae85550564fac221/readme/myimage.gif -------------------------------------------------------------------------------- /src/assets/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandeu/phaser3-matter-car-on-curved-terrain/530284ec4e032affa2f1fc9fae85550564fac221/src/assets/icons/favicon.ico -------------------------------------------------------------------------------- /src/assets/icons/icons-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandeu/phaser3-matter-car-on-curved-terrain/530284ec4e032affa2f1fc9fae85550564fac221/src/assets/icons/icons-192.png -------------------------------------------------------------------------------- /src/assets/icons/icons-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandeu/phaser3-matter-car-on-curved-terrain/530284ec4e032affa2f1fc9fae85550564fac221/src/assets/icons/icons-512.png -------------------------------------------------------------------------------- /src/assets/img/car-body.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandeu/phaser3-matter-car-on-curved-terrain/530284ec4e032affa2f1fc9fae85550564fac221/src/assets/img/car-body.png -------------------------------------------------------------------------------- /src/assets/img/car-wheel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandeu/phaser3-matter-car-on-curved-terrain/530284ec4e032affa2f1fc9fae85550564fac221/src/assets/img/car-wheel.png -------------------------------------------------------------------------------- /src/assets/img/gas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandeu/phaser3-matter-car-on-curved-terrain/530284ec4e032affa2f1fc9fae85550564fac221/src/assets/img/gas.png -------------------------------------------------------------------------------- /src/assets/img/grass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandeu/phaser3-matter-car-on-curved-terrain/530284ec4e032affa2f1fc9fae85550564fac221/src/assets/img/grass.png -------------------------------------------------------------------------------- /src/assets/img/restart-btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandeu/phaser3-matter-car-on-curved-terrain/530284ec4e032affa2f1fc9fae85550564fac221/src/assets/img/restart-btn.png -------------------------------------------------------------------------------- /src/assets/img/sky.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandeu/phaser3-matter-car-on-curved-terrain/530284ec4e032affa2f1fc9fae85550564fac221/src/assets/img/sky.png -------------------------------------------------------------------------------- /src/assets/img/tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandeu/phaser3-matter-car-on-curved-terrain/530284ec4e032affa2f1fc9fae85550564fac221/src/assets/img/tile.png -------------------------------------------------------------------------------- /src/assets/img/wholes-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandeu/phaser3-matter-car-on-curved-terrain/530284ec4e032affa2f1fc9fae85550564fac221/src/assets/img/wholes-small.png -------------------------------------------------------------------------------- /src/assets/terrain.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 45 | 47 | 50 | 54 | 58 | 59 | 70 | 71 | 73 | 74 | 76 | image/svg+xml 77 | 79 | 80 | 81 | 82 | 83 | 88 | 94 | 103 | 109 | 120 | RESTART 135 | 146 | 152 | 158 | 163 | 164 | 170 | 176 | 181 | 182 | 188 | 194 | 199 | 200 | 206 | 212 | 217 | 218 | 224 | 230 | 235 | 236 | 242 | 248 | 253 | 254 | 260 | 266 | 271 | 272 | 278 | 284 | 289 | 290 | 296 | 302 | 307 | 308 | 314 | 320 | 325 | 326 | 332 | 338 | 343 | 344 | 350 | 356 | 361 | 362 | 368 | 374 | 379 | 380 | 386 | 392 | 397 | 398 | 404 | 410 | 415 | 416 | 422 | 428 | 433 | 434 | 440 | 446 | 451 | 452 | 458 | 464 | 469 | 470 | 479 | 489 | 490 | -------------------------------------------------------------------------------- /src/assets/textureAtlas.json: -------------------------------------------------------------------------------- 1 | { 2 | "textures": [ 3 | { 4 | "image": "textureAtlas.png", 5 | "format": "RGBA8888", 6 | "size": { 7 | "w": 1092, 8 | "h": 493 9 | }, 10 | "scale": 1, 11 | "frames": [ 12 | { 13 | "filename": "sky", 14 | "rotated": false, 15 | "trimmed": false, 16 | "sourceSize": { 17 | "w": 576, 18 | "h": 360 19 | }, 20 | "spriteSourceSize": { 21 | "x": 0, 22 | "y": 0, 23 | "w": 576, 24 | "h": 360 25 | }, 26 | "frame": { 27 | "x": 1, 28 | "y": 1, 29 | "w": 576, 30 | "h": 360 31 | } 32 | }, 33 | { 34 | "filename": "wholes-small", 35 | "rotated": false, 36 | "trimmed": false, 37 | "sourceSize": { 38 | "w": 512, 39 | "h": 288 40 | }, 41 | "spriteSourceSize": { 42 | "x": 0, 43 | "y": 0, 44 | "w": 512, 45 | "h": 288 46 | }, 47 | "frame": { 48 | "x": 579, 49 | "y": 1, 50 | "w": 512, 51 | "h": 288 52 | } 53 | }, 54 | { 55 | "filename": "gas", 56 | "rotated": false, 57 | "trimmed": false, 58 | "sourceSize": { 59 | "w": 171, 60 | "h": 201 61 | }, 62 | "spriteSourceSize": { 63 | "x": 0, 64 | "y": 0, 65 | "w": 171, 66 | "h": 201 67 | }, 68 | "frame": { 69 | "x": 579, 70 | "y": 291, 71 | "w": 171, 72 | "h": 201 73 | } 74 | }, 75 | { 76 | "filename": "car-body", 77 | "rotated": false, 78 | "trimmed": false, 79 | "sourceSize": { 80 | "w": 278, 81 | "h": 100 82 | }, 83 | "spriteSourceSize": { 84 | "x": 0, 85 | "y": 0, 86 | "w": 278, 87 | "h": 100 88 | }, 89 | "frame": { 90 | "x": 1, 91 | "y": 363, 92 | "w": 278, 93 | "h": 100 94 | } 95 | }, 96 | { 97 | "filename": "tile", 98 | "rotated": false, 99 | "trimmed": false, 100 | "sourceSize": { 101 | "w": 60, 102 | "h": 20 103 | }, 104 | "spriteSourceSize": { 105 | "x": 0, 106 | "y": 0, 107 | "w": 60, 108 | "h": 20 109 | }, 110 | "frame": { 111 | "x": 1, 112 | "y": 465, 113 | "w": 60, 114 | "h": 20 115 | } 116 | }, 117 | { 118 | "filename": "car-wheel", 119 | "rotated": false, 120 | "trimmed": false, 121 | "sourceSize": { 122 | "w": 100, 123 | "h": 100 124 | }, 125 | "spriteSourceSize": { 126 | "x": 0, 127 | "y": 0, 128 | "w": 100, 129 | "h": 100 130 | }, 131 | "frame": { 132 | "x": 281, 133 | "y": 363, 134 | "w": 100, 135 | "h": 100 136 | } 137 | }, 138 | { 139 | "filename": "restart-btn", 140 | "rotated": false, 141 | "trimmed": false, 142 | "sourceSize": { 143 | "w": 189, 144 | "h": 61 145 | }, 146 | "spriteSourceSize": { 147 | "x": 0, 148 | "y": 0, 149 | "w": 189, 150 | "h": 61 151 | }, 152 | "frame": { 153 | "x": 383, 154 | "y": 363, 155 | "w": 189, 156 | "h": 61 157 | } 158 | }, 159 | { 160 | "filename": "grass", 161 | "rotated": false, 162 | "trimmed": false, 163 | "sourceSize": { 164 | "w": 49, 165 | "h": 38 166 | }, 167 | "spriteSourceSize": { 168 | "x": 0, 169 | "y": 0, 170 | "w": 49, 171 | "h": 38 172 | }, 173 | "frame": { 174 | "x": 383, 175 | "y": 426, 176 | "w": 49, 177 | "h": 38 178 | } 179 | } 180 | ] 181 | } 182 | ], 183 | "meta": { 184 | "app": "https://www.codeandweb.com/texturepacker", 185 | "version": "3.0", 186 | "smartupdate": "$TexturePacker:SmartUpdate:13c8cca7e588a9016c12c8563c96801d:3bdd4c42a9e0f7ee3ad0d0f913ab8714:2b5604bc88faefd55ddac16e9bce1532$" 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/assets/textureAtlas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandeu/phaser3-matter-car-on-curved-terrain/530284ec4e032affa2f1fc9fae85550564fac221/src/assets/textureAtlas.png -------------------------------------------------------------------------------- /src/assets/textureAtlas.tps: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | fileFormatVersion 5 | 4 6 | texturePackerVersion 7 | 4.12.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 | 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 | PngQuantLow 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 | 2048 92 | height 93 | 2048 94 | 95 | fixedTextureSize 96 | 97 | width 98 | -1 99 | height 100 | -1 101 | 102 | algorithmSettings 103 | 104 | algorithm 105 | MaxRects 106 | freeSizeMode 107 | Best 108 | sizeConstraints 109 | AnySize 110 | forceSquared 111 | 112 | maxRects 113 | 114 | heuristic 115 | Best 116 | 117 | basic 118 | 119 | sortBy 120 | Best 121 | order 122 | Ascending 123 | 124 | polygon 125 | 126 | alignToGrid 127 | 1 128 | 129 | 130 | dataFileNames 131 | 132 | json 133 | 134 | name 135 | textureAtlas.json 136 | 137 | 138 | multiPack 139 | 140 | forceIdenticalLayout 141 | 142 | outputFormat 143 | RGBA8888 144 | alphaHandling 145 | ClearTransparentPixels 146 | contentProtection 147 | 148 | key 149 | 150 | 151 | autoAliasEnabled 152 | 153 | trimSpriteNames 154 | 155 | prependSmartFolderName 156 | 157 | autodetectAnimations 158 | 159 | globalSpriteSettings 160 | 161 | scale 162 | 1 163 | scaleMode 164 | Smooth 165 | extrude 166 | 1 167 | trimThreshold 168 | 1 169 | trimMargin 170 | 1 171 | trimMode 172 | Trim 173 | tracerTolerance 174 | 200 175 | heuristicMask 176 | 177 | defaultPivotPoint 178 | 0.5,0.5 179 | writePivotPoints 180 | 181 | 182 | individualSpriteSettings 183 | 184 | img/car-body.png 185 | 186 | pivotPoint 187 | 0.5,0.5 188 | scale9Enabled 189 | 190 | scale9Borders 191 | 64,18,128,36 192 | scale9Paddings 193 | 64,18,128,36 194 | scale9FromFile 195 | 196 | 197 | img/car-wheel.png 198 | 199 | pivotPoint 200 | 0.5,0.5 201 | scale9Enabled 202 | 203 | scale9Borders 204 | 11,11,22,22 205 | scale9Paddings 206 | 11,11,22,22 207 | scale9FromFile 208 | 209 | 210 | img/gas.png 211 | 212 | pivotPoint 213 | 0.5,0.5 214 | scale9Enabled 215 | 216 | scale9Borders 217 | 43,50,85,101 218 | scale9Paddings 219 | 43,50,85,101 220 | scale9FromFile 221 | 222 | 223 | img/grass.png 224 | 225 | pivotPoint 226 | 0.5,0.5 227 | scale9Enabled 228 | 229 | scale9Borders 230 | 12,10,25,19 231 | scale9Paddings 232 | 12,10,25,19 233 | scale9FromFile 234 | 235 | 236 | img/restart-btn.png 237 | 238 | pivotPoint 239 | 0.5,0.5 240 | scale9Enabled 241 | 242 | scale9Borders 243 | 47,15,95,31 244 | scale9Paddings 245 | 47,15,95,31 246 | scale9FromFile 247 | 248 | 249 | img/sky.png 250 | 251 | pivotPoint 252 | 0.5,0.5 253 | scale9Enabled 254 | 255 | scale9Borders 256 | 144,90,288,180 257 | scale9Paddings 258 | 144,90,288,180 259 | scale9FromFile 260 | 261 | 262 | img/tile.png 263 | 264 | pivotPoint 265 | 0.5,0.5 266 | scale9Enabled 267 | 268 | scale9Borders 269 | 15,5,30,10 270 | scale9Paddings 271 | 15,5,30,10 272 | scale9FromFile 273 | 274 | 275 | img/wholes-small.png 276 | 277 | pivotPoint 278 | 0.5,0.5 279 | scale9Enabled 280 | 281 | scale9Borders 282 | 128,72,256,144 283 | scale9Paddings 284 | 128,72,256,144 285 | scale9FromFile 286 | 287 | 288 | 289 | fileList 290 | 291 | img 292 | 293 | ignoreFileList 294 | 295 | replaceList 296 | 297 | ignoredWarnings 298 | 299 | commonDivisorX 300 | 1 301 | commonDivisorY 302 | 1 303 | packNormalMaps 304 | 305 | autodetectNormalMaps 306 | 307 | normalMapFilter 308 | 309 | normalMapSuffix 310 | 311 | normalMapSheetFileName 312 | 313 | exporterProperties 314 | 315 | 316 | 317 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 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 | <%= htmlWebpackPlugin.options.gameName %> 27 | 50 | 51 | 52 | 53 |
54 |

loading...

55 |
56 | 57 | 58 | 59 | 60 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/pwa/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "2d Car", 3 | "name": "My Cool Phaser 3 Game", 4 | "icons": [ 5 | { 6 | "src": "./assets/icons/icons-192.png", 7 | "type": "image/png", 8 | "sizes": "192x192" 9 | }, 10 | { 11 | "src": "./assets/icons/icons-512.png", 12 | "type": "image/png", 13 | "sizes": "512x512" 14 | } 15 | ], 16 | "start_url": "./index.html", 17 | "background_color": "#e6e6e6", 18 | "display": "fullscreen", 19 | "orientation": "landscape", 20 | "scope": "./", 21 | "theme_color": "#000000" 22 | } 23 | -------------------------------------------------------------------------------- /src/pwa/sw.js: -------------------------------------------------------------------------------- 1 | workbox.precaching.precacheAndRoute(__precacheManifest) 2 | -------------------------------------------------------------------------------- /src/scripts/customCanvas.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Window { 3 | RENDERING_CONTEXT: 'CANVAS' | 'WEBGL' | 'WEBGL2' 4 | } 5 | } 6 | window.RENDERING_CONTEXT = 'CANVAS' 7 | 8 | export default class CustomCanvas { 9 | private _type: number 10 | private _canvas: HTMLCanvasElement | undefined 11 | private _context: CanvasRenderingContext2D | WebGLRenderingContext | null | undefined 12 | 13 | public get type() { 14 | return this._type 15 | } 16 | 17 | public get canvas() { 18 | return this._canvas 19 | } 20 | 21 | public get context() { 22 | return this._context 23 | } 24 | 25 | constructor() { 26 | let myCustomCanvas: HTMLCanvasElement = document.getElementById('myCustomCanvas') 27 | let myCustomContext: CanvasRenderingContext2D | WebGLRenderingContext | null = null 28 | 29 | console.log(typeof myCustomCanvas) 30 | if (myCustomCanvas) { 31 | const contextCreationConfig = { 32 | alpha: false, 33 | depth: false, 34 | antialias: true, 35 | premultipliedAlpha: true, 36 | stencil: true, 37 | preserveDrawingBuffer: false, 38 | failIfMajorPerformanceCaveat: false, 39 | powerPreference: 'default' 40 | } 41 | 42 | let WEBGL2Supported = 43 | // @ts-ignore 44 | typeof window.WebGL2RenderingContext !== undefined && window.WebGL2RenderingContext ? true : false 45 | let WEBGLSupported = 46 | // @ts-ignore 47 | typeof window.WebGLRenderingContext !== undefined && window.WebGLRenderingContext ? true : false 48 | 49 | // @ts-ignore 50 | if (WEBGL2Supported) myCustomContext = myCustomCanvas.getContext('webgl2', contextCreationConfig) 51 | // @ts-ignore 52 | if (!myCustomContext && WEBGLSupported) 53 | myCustomContext = myCustomCanvas.getContext('webgl', contextCreationConfig) 54 | 55 | if (myCustomContext) { 56 | // @ts-ignore 57 | if (WEBGL2Supported && myCustomContext instanceof WebGL2RenderingContext) window.RENDERING_CONTEXT = 'WEBGL2' 58 | if (WEBGLSupported && myCustomContext instanceof WebGLRenderingContext) window.RENDERING_CONTEXT = 'WEBGL' 59 | } 60 | } 61 | this._type = myCustomContext ? Phaser.WEBGL : Phaser.CANVAS 62 | this._canvas = myCustomCanvas ? myCustomCanvas : undefined 63 | // @ts-ignore 64 | this._context = myCustomContext ? myCustomContext : undefined 65 | 66 | console.log(`Rendering: ${window.RENDERING_CONTEXT}`) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/scripts/game.ts: -------------------------------------------------------------------------------- 1 | import 'phaser' 2 | 3 | // poly-decomp and pathseg are required by matterjs 4 | // @ts-ignore 5 | import decomp from 'poly-decomp' 6 | // @ts-ignore 7 | window.decomp = decomp 8 | // @ts-ignore 9 | import 'pathseg' 10 | 11 | import MainScene from './scenes/mainScene' 12 | import PreloadScene from './scenes/preloadScene' 13 | import GuiScene from './scenes/guiScene' 14 | import BackgroundScene from './scenes/backgroundScene' 15 | import CustomCanvas from './customCanvas' 16 | 17 | const ratio = Math.max(window.innerWidth / window.innerHeight, window.innerHeight / window.innerWidth) 18 | const DEFAULT_HEIGHT = 720 19 | const DEFAULT_WIDTH = ratio * DEFAULT_HEIGHT 20 | 21 | const customCanvas = new CustomCanvas() 22 | 23 | const config: GameConfig = { 24 | type: customCanvas.type, 25 | canvas: customCanvas.canvas, 26 | // @ts-ignore 27 | context: customCanvas.context, 28 | backgroundColor: '#ffffff', 29 | scale: { 30 | mode: Phaser.Scale.FIT, 31 | autoCenter: Phaser.Scale.CENTER_BOTH, 32 | width: DEFAULT_WIDTH, 33 | height: DEFAULT_HEIGHT 34 | }, 35 | scene: [PreloadScene, BackgroundScene, MainScene, GuiScene], 36 | physics: { 37 | default: 'matter', 38 | matter: { 39 | gravity: { 40 | y: 1 41 | }, 42 | debug: false, 43 | debugBodyColor: 0x000000 44 | } 45 | } 46 | } 47 | 48 | window.addEventListener('load', () => { 49 | let game = new Phaser.Game(config) 50 | }) 51 | -------------------------------------------------------------------------------- /src/scripts/objects/bridge.ts: -------------------------------------------------------------------------------- 1 | export default class Bridge { 2 | constructor(scene: Phaser.Scene, x: number = 0, y: number = 0, width: number = 1000, numberOfPlanks: number = 18) { 3 | let woodPlanksPositions = [] 4 | 5 | let group = scene.matter.world.nextGroup(true) 6 | 7 | // creates a new wood plank 8 | const newWoodPlank = (x: number, y: number) => { 9 | return scene.matter.add.image(x, y, 'atlas', 'tile').setBody( 10 | { type: 'rectangle' }, 11 | { 12 | collisionFilter: { group: group }, 13 | label: 'bridgePlank', 14 | chamfer: 5, 15 | density: 0.005, 16 | friction: 0.6 17 | //frictionAir: 0.05 18 | } 19 | ) 20 | } 21 | 22 | // calculate the positions of all wood planks 23 | for (let i = 0; i < numberOfPlanks; i++) woodPlanksPositions.push({ x: x + (i * width) / numberOfPlanks, y: y }) 24 | 25 | // make all the wood planks 26 | let woodPlanks = woodPlanksPositions.map(pos => newWoodPlank(pos.x, pos.y)) 27 | 28 | // attaching each plank to the next one 29 | for (let i = 0; i < woodPlanks.length - 1; i++) { 30 | scene.matter.add.constraint(woodPlanks[i], woodPlanks[i + 1], 10, 0.75, { 31 | pointA: { x: 20, y: 0 }, 32 | pointB: { x: -20, y: 0 } 33 | }) 34 | } 35 | 36 | // attaching the first plank to the left side 37 | scene.matter.add.worldConstraint(woodPlanks[0], 0, 1, { 38 | pointA: { x: x, y: y }, 39 | pointB: { x: -30, y: 0 } 40 | }) 41 | 42 | // attaching the last plank to the left side 43 | scene.matter.add.worldConstraint(woodPlanks[woodPlanks.length - 1], 0, 1, { 44 | pointA: { x: x + width, y: y }, 45 | pointB: { x: 30, y: 0 } 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/scripts/objects/car.ts: -------------------------------------------------------------------------------- 1 | import Restart from './restart' 2 | 3 | export default class Car { 4 | readonly MAX_SPEED = 0.75 5 | readonly MAX_SPEED_BACKWARDS = this.MAX_SPEED * 0.75 6 | readonly ACCELERATION = this.MAX_SPEED / 130 7 | readonly ACCELERATION_BACKWARDS = this.ACCELERATION * 0.75 8 | 9 | bodies: any[] = [] 10 | gas = { 11 | right: false, 12 | left: false 13 | } 14 | wheelsDown = { 15 | rear: false, 16 | front: false 17 | } 18 | private _scene: Phaser.Scene 19 | 20 | constructor( 21 | scene: Phaser.Scene, 22 | x: number, 23 | y: number, 24 | width: number = 278, 25 | height: number = 100, 26 | wheelSize: number = 50, 27 | wheelOffset: { x: number; y: number } = { x: 62, y: 62 } 28 | ) { 29 | this._scene = scene 30 | 31 | const wheelBase = wheelOffset.x, 32 | wheelAOffset = -width * 0.5 + wheelBase, 33 | wheelBOffset = width * 0.5 - wheelBase, 34 | wheelYOffset = wheelOffset.y 35 | 36 | const density = 0.001 37 | const friction = 0.9 38 | // @ts-ignore 39 | const Matter = Phaser.Physics.Matter.Matter 40 | 41 | let group = scene.matter.world.nextGroup(true) 42 | 43 | let body = scene.matter.add.image(x, y, 'atlas', 'car-body') 44 | body.setBody( 45 | { 46 | type: 'rectangle', 47 | width: width, 48 | height: height 49 | }, 50 | { 51 | label: 'carBody', 52 | collisionFilter: { 53 | group: group 54 | }, 55 | chamfer: { 56 | radius: height * 0.5 57 | }, 58 | density: 0.002 59 | } 60 | ) 61 | 62 | let wheelA = scene.matter.add.image(x + wheelAOffset, y + wheelYOffset, 'atlas', 'car-wheel') 63 | wheelA.setBody( 64 | { 65 | type: 'circle', 66 | radius: wheelSize 67 | }, 68 | { 69 | label: 'wheelRear', 70 | collisionFilter: { 71 | group: group 72 | }, 73 | friction, 74 | density 75 | } 76 | ) 77 | 78 | let wheelB = scene.matter.add.image(x + wheelBOffset, y + wheelYOffset, 'atlas', 'car-wheel') 79 | wheelB.setBody( 80 | { 81 | type: 'circle', 82 | radius: wheelSize 83 | }, 84 | { 85 | label: 'wheelFront', 86 | collisionFilter: { 87 | group: group 88 | }, 89 | friction, 90 | density 91 | } 92 | ) 93 | 94 | let axelA = scene.matter.add.constraint(body.body, wheelA.body, 0, 0.2, { 95 | pointA: { x: wheelAOffset, y: wheelYOffset } 96 | }) 97 | 98 | let axelB = scene.matter.add.constraint(body.body, wheelB.body, 0, 0.2, { 99 | pointA: { x: wheelBOffset, y: wheelYOffset } 100 | }) 101 | 102 | this.bodies = [body.body, wheelA.body, wheelB.body] 103 | } 104 | 105 | update() { 106 | // @ts-ignore 107 | const Matter = Phaser.Physics.Matter.Matter 108 | const carBody = this.bodies[0] 109 | const wheelRear = this.bodies[1] 110 | const wheelFront = this.bodies[2] 111 | 112 | // restart the game if the car falls 113 | if (carBody.position.y > 3000) Restart.restart(this._scene) 114 | 115 | let angularVelocity = 0.005 116 | 117 | if (this.gas.right) { 118 | let newSpeed = wheelRear.angularSpeed <= 0 ? this.MAX_SPEED / 10 : wheelRear.angularSpeed + this.ACCELERATION 119 | if (newSpeed > this.MAX_SPEED) newSpeed = this.MAX_SPEED 120 | Matter.Body.setAngularVelocity(wheelRear, newSpeed) 121 | Matter.Body.setAngularVelocity(wheelFront, newSpeed) 122 | // if (!this.wheelsDown.rear && !this.wheelsDown.front) Matter.Body.setAngularVelocity(carBody, -angularVelocity) 123 | } else if (this.gas.left) { 124 | let newSpeed = 125 | wheelRear.angularSpeed <= 0 126 | ? this.MAX_SPEED_BACKWARDS / 10 127 | : wheelRear.angularSpeed + this.ACCELERATION_BACKWARDS 128 | if (newSpeed > this.MAX_SPEED_BACKWARDS) newSpeed = this.MAX_SPEED_BACKWARDS 129 | 130 | Matter.Body.setAngularVelocity(wheelRear, -newSpeed) 131 | // if (!this.wheelsDown.rear && !this.wheelsDown.front) Matter.Body.setAngularVelocity(carBody, angularVelocity) 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/scripts/objects/gas.ts: -------------------------------------------------------------------------------- 1 | import Car from './car' 2 | 3 | export default class Gas { 4 | constructor(scene: Phaser.Scene, car: Car) { 5 | // left gas 6 | scene.add 7 | .image(20, scene.cameras.main.height - 20, 'atlas', 'gas') 8 | .setScrollFactor(0) 9 | .setOrigin(0, 1) 10 | .setInteractive() 11 | .on('pointerdown', () => (car.gas.left = true)) 12 | .on('pointerover', () => (car.gas.left = true)) 13 | .on('pointerup', () => (car.gas.left = false)) 14 | .on('pointerout', () => (car.gas.left = false)) 15 | 16 | // right gas 17 | scene.add 18 | .image(scene.cameras.main.width - 20, scene.cameras.main.height - 20, 'atlas', 'gas') 19 | .setScrollFactor(0) 20 | .setOrigin(1, 1) 21 | .setInteractive() 22 | .on('pointerdown', () => (car.gas.right = true)) 23 | .on('pointerover', () => (car.gas.right = true)) 24 | .on('pointerup', () => (car.gas.right = false)) 25 | .on('pointerout', () => (car.gas.right = false)) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/scripts/objects/restart.ts: -------------------------------------------------------------------------------- 1 | export default class Restart { 2 | private _scene: Phaser.Scene 3 | 4 | constructor(scene: Phaser.Scene) { 5 | this._scene = scene 6 | } 7 | 8 | addRestartButton() { 9 | // add the restart button 10 | this._scene.add 11 | .image(this._scene.cameras.main.width / 2, 15, 'atlas', 'restart-btn') 12 | .setOrigin(0.5, 0) 13 | .setScrollFactor(0) 14 | .setInteractive() 15 | .on('pointerdown', () => { 16 | Restart.restart(this._scene) 17 | }) 18 | } 19 | 20 | static restart(scene: Phaser.Scene) { 21 | let loadingScreen = document.getElementById('loading-screen') 22 | if (loadingScreen) { 23 | loadingScreen.style.display = 'flex' 24 | } 25 | scene.time.addEvent({ 26 | delay: 250, 27 | callback: () => { 28 | const mainScene = scene.scene.get('MainScene') 29 | mainScene.scene.restart() 30 | const GuiScene = scene.scene.get('GuiScene') 31 | GuiScene.scene.restart() 32 | } 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/scripts/objects/terrain.ts: -------------------------------------------------------------------------------- 1 | export default class Terrain { 2 | constructor(scene: Phaser.Scene, path: any, x: number, y: number) { 3 | // @ts-ignore 4 | const Matter = Phaser.Physics.Matter.Matter 5 | 6 | let pathElement = document.createElementNS('http://www.w3.org/2000/svg', 'path') 7 | pathElement.setAttributeNS(null, 'd', path) 8 | pathElement.setAttributeNS(null, 'id', 'path3780') 9 | 10 | var vertexSets: { x: number; y: number }[][] = [] 11 | vertexSets.push(Matter.Svg.pathToVertices(pathElement, 30)) 12 | 13 | const minMax = (items: number[]) => { 14 | return items.reduce( 15 | (acc, val) => { 16 | acc.high = isNaN(acc.high) || val > acc.high ? val : acc.high 17 | acc.low = isNaN(acc.low) || val < acc.low ? val : acc.low 18 | return acc 19 | }, 20 | { high: NaN, low: NaN } 21 | ) 22 | } 23 | 24 | let points = { 25 | x: minMax(vertexSets[0].map(bla => bla.x)), 26 | y: minMax(vertexSets[0].map(bla => bla.y)) 27 | } 28 | 29 | const normalizeVertices = (vertices: { x: number; y: number }[]) => { 30 | return vertices.map(point => { 31 | return { x: point.x - points.x.low, y: point.y - points.y.low } 32 | }) 33 | } 34 | 35 | vertexSets[0] = normalizeVertices(vertexSets[0]) 36 | 37 | let PADDING = 200 38 | let LINE_HEIGHT = 25 39 | let WIDTH = points.x.high - points.x.low 40 | let HEIGHT = points.y.high - points.y.low 41 | 42 | //vertexSets[0].unshift({ x: vertexSets[0][0].x, y: vertexSets[0][0].y + 500 }) 43 | //vertexSets[0].push({ x: vertexSets[0][vertexSets.length - 1].x, y: vertexSets[0][vertexSets.length - 1].y + 500 }) 44 | 45 | // create the terrain 46 | let terrain = scene.add.graphics({ x: x, y: y }) 47 | terrain.fillStyle(0x685339) 48 | terrain.beginPath() 49 | vertexSets[0].forEach(c => { 50 | terrain.lineTo(Math.round(c.x), Math.round(c.y)) 51 | }) 52 | terrain.closePath() 53 | terrain.fillPath() 54 | 55 | const mask = terrain.createGeometryMask() 56 | 57 | // create the wholes in the terrain 58 | let wholes = scene.add.tileSprite(x, y, WIDTH, HEIGHT, 'atlas', 'wholes-small') 59 | wholes.setOrigin(0) 60 | wholes.setScale(2, 2) 61 | wholes.setMask(mask) 62 | 63 | // create the grass layer 64 | let grass = scene.add.graphics({ x: x, y: y }) 65 | grass.lineStyle(LINE_HEIGHT, 0xadea53) 66 | grass.beginPath() 67 | vertexSets[0].forEach(c => { 68 | grass.lineTo(Math.round(c.x), Math.round(c.y)) 69 | }) 70 | grass.closePath() 71 | grass.strokePath() 72 | 73 | // plant the grass 74 | vertexSets[0].forEach(point => { 75 | if (Math.random() < 0.15) scene.add.image(point.x + x, point.y + y - 15, 'grass') 76 | }) 77 | 78 | let terrainBody: any = scene.matter.add.fromVertices( 79 | WIDTH / 2 + x, 80 | HEIGHT / 2 + y, 81 | vertexSets, 82 | { 83 | label: 'terrain', 84 | isStatic: true, 85 | friction: 0.7 86 | }, 87 | true, 88 | //@ts-ignore 89 | 0.01, 90 | 1 91 | ) 92 | 93 | // get offset of center of mass 94 | // and set the terrain to its correct position 95 | // https://github.com/liabru/matter-js/issues/211#issuecomment-184804576 96 | let centerOfMass = Matter.Vector.sub(terrainBody.bounds.min, terrainBody.position) 97 | Matter.Body.setPosition(terrainBody, { x: Math.abs(centerOfMass.x) + x, y: Math.abs(centerOfMass.y) + y }) 98 | } 99 | update() {} 100 | } 101 | -------------------------------------------------------------------------------- /src/scripts/objects/terrainDynamic.ts: -------------------------------------------------------------------------------- 1 | interface DynamicTextures { 2 | x1: number 3 | x2: number 4 | type: 'TileSprite' | 'Graphics' | 'Image' | 'Sprite' 5 | texture: 6 | | Phaser.GameObjects.TileSprite 7 | | Phaser.GameObjects.Graphics 8 | | Phaser.GameObjects.Image 9 | | Phaser.GameObjects.Sprite 10 | } 11 | 12 | export default class Terrain { 13 | private dynamicTextures: DynamicTextures[] = [] 14 | private _scene: Phaser.Scene 15 | 16 | constructor(scene: Phaser.Scene, path: any, x: number, y: number, terrainIndex?: number) { 17 | this._scene = scene 18 | // @ts-ignore 19 | const Matter = Phaser.Physics.Matter.Matter 20 | 21 | let pathElement = document.createElementNS('http://www.w3.org/2000/svg', 'path') 22 | pathElement.setAttributeNS(null, 'd', path) 23 | pathElement.setAttributeNS(null, 'id', 'path3780') 24 | 25 | var vertexSets: { x: number; y: number }[][] = [] 26 | let PATH_LENGTH = 36 27 | vertexSets.push(Matter.Svg.pathToVertices(pathElement, PATH_LENGTH)) 28 | 29 | //console.log(JSON.stringify(vertexSets)) 30 | 31 | const minMax = (items: number[]) => { 32 | return items.reduce( 33 | (acc, val) => { 34 | acc.high = isNaN(acc.high) || val > acc.high ? val : acc.high 35 | acc.low = isNaN(acc.low) || val < acc.low ? val : acc.low 36 | return acc 37 | }, 38 | { high: NaN, low: NaN } 39 | ) 40 | } 41 | 42 | let points = { 43 | x: minMax(vertexSets[0].map(point => point.x)), 44 | y: minMax(vertexSets[0].map(point => point.y)) 45 | } 46 | 47 | vertexSets[0].push({ 48 | x: vertexSets[0][vertexSets[0].length - 1].x, 49 | y: vertexSets[0][vertexSets[0].length - 1].y + points.y.high + 360 50 | }) 51 | vertexSets[0].unshift({ 52 | x: vertexSets[0][0].x, 53 | y: vertexSets[0][0].y + points.y.high + 500 54 | }) 55 | 56 | points = { 57 | x: minMax(vertexSets[0].map(point => point.x)), 58 | y: minMax(vertexSets[0].map(point => point.y)) 59 | } 60 | 61 | const normalizeVertices = (vertices: { x: number; y: number }[]) => { 62 | return vertices.map(point => { 63 | return { x: point.x - points.x.low, y: point.y - points.y.low } 64 | }) 65 | } 66 | 67 | vertexSets[0] = normalizeVertices(vertexSets[0]) 68 | 69 | let PADDING = 200 70 | let LINE_HEIGHT = 25 71 | let WIDTH = points.x.high - points.x.low 72 | let HEIGHT = points.y.high - points.y.low 73 | 74 | // create the terrain 75 | let terrain = scene.add.graphics({ x: x, y: y }) 76 | terrain.fillStyle(0x685339) 77 | terrain.beginPath() 78 | vertexSets[0].forEach(c => { 79 | terrain.lineTo(Math.round(c.x), Math.round(c.y)) 80 | }) 81 | terrain.closePath() 82 | terrain.fillPath() 83 | 84 | const mask = terrain.createGeometryMask() 85 | 86 | const V = vertexSets[0] 87 | 88 | // create the wholes in the terrain 89 | for (let i = 0; i < Math.ceil(WIDTH / 1024); i++) { 90 | let xx = x + i * 1024 91 | 92 | // get the highest value from all vertices inside the x range of the tilesprite 93 | let points = V.filter(point => { 94 | return point.x > i * 1024 && point.x < i * 1024 + 1024 95 | }) 96 | 97 | let low = minMax(points.map(p => p.y)).low 98 | let skip = Math.floor(low / (288 * 2)) 99 | let yy = y 100 | yy = skip * 288 * 2 101 | 102 | let wholes = scene.add.tileSprite(xx, yy, 512, Math.min(HEIGHT, 1024 /*288 * 2 * 2*/), 'atlas', 'wholes-small') 103 | wholes.setOrigin(0) 104 | wholes.setScale(2, 2) 105 | wholes.setMask(mask) 106 | this.dynamicTextures.push({ x1: xx, x2: xx + 1024, texture: wholes, type: 'TileSprite' }) 107 | } 108 | 109 | terrain.setVisible(false) 110 | 111 | // create the grass layer 112 | const divide = Math.round(1024 / PATH_LENGTH) 113 | //let grass: Phaser.GameObjects.Graphics 114 | for (let i = 1; i < V.length - 1; i += divide) { 115 | let x1 = V[i].x + x 116 | let x2 = V[Math.min(i + divide, V.length - 2)].x + x 117 | 118 | let grass = scene.add.graphics({ x: x1, y: y }) 119 | grass.lineStyle(LINE_HEIGHT, 0xadea53) 120 | grass.beginPath() 121 | 122 | for (let j = i === 1 ? 1 : -1; j < divide; j++) { 123 | if (i + j > V.length - 1) break 124 | grass.lineTo(Math.round(V[i + j].x - x1 + x), Math.round(V[i + j].y)) 125 | } 126 | 127 | // grass.closePath() 128 | grass.strokePath() 129 | 130 | // let debug = true 131 | // if (debug) { 132 | // let t = `${Math.random()}` 133 | // grass.generateTexture(t) 134 | // let bla = scene.add.sprite(400, 300, t) 135 | // console.log(bla.width, bla.height) 136 | // } 137 | 138 | this.dynamicTextures.push({ 139 | x1: Math.min(x1, x2), 140 | x2: Math.max(x1, x2), 141 | texture: grass, 142 | type: 'Graphics' 143 | }) 144 | //scene.physics.add.existing(grass) 145 | } 146 | 147 | // plant the grass 148 | let plants = 0 149 | vertexSets[0].forEach(point => { 150 | // for (let i = 0; i < 150; i++) { 151 | // let point = vertexSets[0][i] 152 | if (Math.random() < 0.15) { 153 | plants++ 154 | let grass = scene.add.image(point.x + x, point.y + y - 15, 'atlas', 'grass') 155 | this.dynamicTextures.push({ x1: grass.x, x2: grass.x, texture: grass, type: 'Image' }) 156 | } 157 | }) 158 | // } 159 | 160 | let terrainBody: any = scene.matter.add.fromVertices( 161 | WIDTH / 2 + x, 162 | HEIGHT / 2 + y, 163 | vertexSets, 164 | { 165 | label: 'terrain', 166 | isStatic: true, 167 | friction: 0.7 168 | }, 169 | true, 170 | //@ts-ignore 171 | 0.01, 172 | 1 173 | ) 174 | 175 | // get offset of center of mass 176 | // and set the terrain to its correct position 177 | // https://github.com/liabru/matter-js/issues/211#issuecomment-184804576 178 | let centerOfMass = Matter.Vector.sub(terrainBody.bounds.min, terrainBody.position) 179 | Matter.Body.setPosition(terrainBody, { x: Math.abs(centerOfMass.x) + x, y: Math.abs(centerOfMass.y) + y }) 180 | 181 | this.dynamicTextures.forEach(child => { 182 | // @ts-ignore 183 | //console.log(child.type, child.texture.width, child.texture.height) 184 | //if (child.type === 'Graphics') console.log(child.texture) 185 | }) 186 | } 187 | 188 | update() { 189 | let x1 = this._scene.cameras.main.worldView.x 190 | let x2 = x1 + this._scene.cameras.main.worldView.width 191 | 192 | const isVisible = (points: DynamicTextures): boolean => { 193 | const case1 = points.x1 > x1 && points.x1 < x2 194 | const case2 = points.x2 > x1 && points.x2 < x2 195 | const case3 = points.x1 < x1 && points.x2 > x2 196 | return case1 || case2 || case3 197 | } 198 | 199 | let countVisible = 0 200 | this.dynamicTextures.forEach(whole => { 201 | if (whole.texture.visible) countVisible++ 202 | if (isVisible(whole)) { 203 | if (!whole.texture.visible) { 204 | whole.texture.setVisible(true) 205 | } 206 | } else { 207 | if (whole.texture.visible) whole.texture.setVisible(false) 208 | } 209 | }) 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/scripts/scenes/backgroundScene.ts: -------------------------------------------------------------------------------- 1 | export default class GuiScene extends Phaser.Scene { 2 | constructor() { 3 | super({ key: 'BackgroundScene' }) 4 | } 5 | 6 | create() { 7 | // blue background 8 | // this.add 9 | // .graphics({ x: 0, y: 0 }) 10 | // .fillGradientStyle(0x43c1d8, 0x43c1d8, 0x91eef5, 0x91eef5) 11 | // .fillRect(0, 0, this.cameras.main.width, this.cameras.main.height) 12 | // .setScrollFactor(0) 13 | this.cameras.main.setBackgroundColor('#4ba9e4') 14 | let bg = this.add 15 | .sprite(0, 0, 'atlas', 'sky') 16 | .setScrollFactor(0) 17 | .setOrigin(0) 18 | .setScale(3) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/scripts/scenes/guiScene.ts: -------------------------------------------------------------------------------- 1 | import Restart from '../objects/restart' 2 | import Gas from '../objects/gas' 3 | import MainScene from './mainScene' 4 | 5 | export default class GuiScene extends Phaser.Scene { 6 | fpsText: FpsText 7 | 8 | constructor() { 9 | super({ key: 'GuiScene' }) 10 | } 11 | 12 | create() { 13 | // @ts-ignore 14 | const mainScene: MainScene = this.scene.get('MainScene') 15 | 16 | new Restart(this).addRestartButton() 17 | new Gas(this, mainScene.car) 18 | 19 | this.fpsText = new FpsText(this) 20 | 21 | this.add 22 | .text(this.cameras.main.width - 10, 10, `Phaser: v${Phaser.VERSION}`, { color: 'black', fontSize: 28 }) 23 | .setOrigin(1, 0) 24 | .setScrollFactor(0) 25 | 26 | this.add 27 | .text(this.cameras.main.width - 10, 10 + 28 + 8, `Render: ${window.RENDERING_CONTEXT}`, { 28 | color: 'black', 29 | fontSize: 28 30 | }) 31 | .setOrigin(1, 0) 32 | .setScrollFactor(0) 33 | } 34 | update() { 35 | this.fpsText.update(this) 36 | } 37 | } 38 | 39 | class FpsText extends Phaser.GameObjects.Text { 40 | constructor(scene: Phaser.Scene) { 41 | super(scene, 10, 10, '', { color: 'black', fontSize: 28 }) 42 | scene.add.existing(this) 43 | this.setOrigin(0) 44 | this.setScrollFactor(0) 45 | } 46 | 47 | public update(scene: Phaser.Scene) { 48 | this.setText(`fps: ${Math.round(scene.game.loop.actualFps)}`) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/scripts/scenes/mainScene.ts: -------------------------------------------------------------------------------- 1 | import Bridge from '../objects/bridge' 2 | //import Terrain from '../objects/terrain' 3 | import Terrain from '../objects/terrainDynamic' 4 | import Car from '../objects/car' 5 | import Gas from '../objects/gas' 6 | import Restart from '../objects/restart' 7 | import { Scene } from 'phaser' 8 | 9 | export default class MainScene extends Phaser.Scene { 10 | car: Car 11 | terrains: Terrain[] 12 | 13 | constructor() { 14 | super({ key: 'MainScene' }) 15 | } 16 | 17 | create() { 18 | //this.matter.add.mouseSpring({}) 19 | 20 | const path1 = 21 | 'M -10.837105,399.91531 C -10.837105,399.91531 360.95089,618.41855 633.74796,640.65452 933.27973,665.06975 1194.6484,497.3518 1498.1559,507.62844 1727.0472,515.37863 1898.3125,665.99098 2109.7652,650.73232 2479.5945,624.04507 2737.3742,278.22805 3107.1744,251.27988 3425.2674,283.59165 3926.4207,433.05255 4489.1112,512.4571 4597.6107,527.76814 4722.879,469.1338 4833.3691,482.03476 4868.8117,486.17313 4939.2658,567.09253 4974.475,571.28218 5639.6416,650.43241 6256.8855,671.72797 6588.8994,674.43736 6943.2645,673.42027 7277.3722,403.89083 7743.1187,388.41574 8023.5431,379.09821 8167.268,733.32913 8445.1661,682.98562 9142.1521,556.72082 9382.633,653.43022 9660.1193,642.80088 9803.5716,637.30582 10351.968,200.05637 10670.53,200.2844 10999.895,200.52023 11134.798,-19.470908 11345.368,0.88878878 11664.594,31.754353 12026.765,223.6594 12296.609,161.811 12838.032,37.716588 12837.788,533.42456 12847.221,532.71357' 22 | const path2 = 23 | 'M 11617.063,3528.4898 C 11621.249,3524.7808 11611.68,3447.0568 11669.83,3358.5231 11865.52,3060.5827 11575.859,3311.6521 11343.718,3201.6923 11139.351,3104.8886 10961.733,3006.1663 10729.49,2913.6747 10396.606,2781.1024 10101.548,2541.1218 9731.5518,2514.1701 9413.2902,2546.4861 8911.8712,2695.9665 8348.8826,2775.3815 8240.3255,2790.6945 8114.9908,2732.0525 8004.4422,2744.9552 7968.9808,2749.094 7713.9634,2870.0174 7678.7355,2874.2076 7013.2163,2953.3682 6587.2685,2808.9798 6247.9814,2937.3829 5829.5541,3056.3456 5559.1438,2666.801 5093.1504,2651.3239 4812.5774,2642.0051 4668.7763,2996.2823 4390.7309,2945.9322 3693.3755,2819.6509 3452.7672,2916.373 3175.1339,2905.7423 3031.6055,2900.2465 2546.7924,2588.6333 2228.0616,2588.8613 1898.5226,2589.0972 1699.6737,2243.384 1488.9922,2263.7464 1169.5965,2294.616 807.23392,2486.5461 537.2465,2424.6896 -4.4640133,2300.579 -4.2193389,2796.3516 -13.657313,2795.6406' 24 | 25 | new Bridge(this, -960 - 200, 740, 960, 18) 26 | new Bridge(this, 10003 + 2500, 542, 960, 18) 27 | const terrain1 = new Terrain(this, path1, -200, 350, 1) 28 | const terrain2 = new Terrain(this, path2, 10826 - 81 + 2500, 5 + 375, 2) 29 | this.terrains = [terrain1, terrain2] 30 | this.car = new Car(this, 490, 650) 31 | 32 | // check of the rear wheels are touching the ground 33 | this.matter.world.on('collisionactive', (collisions: any, b: any, c: any) => { 34 | this.car.wheelsDown = { rear: false, front: false } 35 | collisions.pairs.forEach((pair: any) => { 36 | let labels: string[] = [pair['bodyA'].label, pair['bodyB'].label] 37 | if (labels.includes('wheelRear')) { 38 | this.car.wheelsDown.rear = true 39 | } 40 | if (labels.includes('wheelFront')) { 41 | this.car.wheelsDown.front = true 42 | } 43 | }) 44 | }) 45 | 46 | // listen to pointerdown click events and show world coordinates 47 | // this.input.addListener('pointerdown', (pointer: Phaser.Input.Pointer) => { 48 | // console.log(pointer.worldX, pointer.worldY) 49 | // }) 50 | 51 | // hide the loading screen 52 | let loadingScreen = document.getElementById('loading-screen') 53 | if (loadingScreen) loadingScreen.style.display = 'none' 54 | } 55 | 56 | update() { 57 | //console.log(this.cameras.main.worldView.x, this.cameras.main.worldView.width) 58 | this.terrains.forEach(terrain => { 59 | terrain.update() 60 | }) 61 | 62 | // fix the camera to the car 63 | const carBody = this.car.bodies[0] 64 | this.cameras.main.centerOn(carBody.position.x + 300, carBody.position.y - 100) 65 | 66 | // set the smooth zoom 67 | const wheelRear = this.car.bodies[1] 68 | const currentZoom = this.cameras.main.zoom 69 | let zoom = 1 - wheelRear.angularVelocity / 1.65 70 | if (zoom > currentZoom + currentZoom * 0.0022) zoom = currentZoom + currentZoom * 0.0022 71 | else if (zoom < currentZoom - currentZoom * 0.0022) zoom = currentZoom - currentZoom * 0.0022 72 | if (zoom > 1) zoom = 1 73 | if (zoom < 0.6) zoom = 0.6 74 | this.cameras.main.setZoom(zoom) 75 | 76 | this.car.update() 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/scripts/scenes/preloadScene.ts: -------------------------------------------------------------------------------- 1 | export default class PreloadScene extends Phaser.Scene { 2 | constructor() { 3 | super({ key: 'PreloadScene' }) 4 | } 5 | 6 | preload() { 7 | // this.load.image('tile', 'assets/img/tile.png') 8 | // this.load.image('gas', 'assets/img/gas.png') 9 | // this.load.image('car', 'assets/img/car-small.png') 10 | // this.load.image('wheel', 'assets/img/wheel-small.png') 11 | // this.load.image('restart', 'assets/img/restart-btn.png') 12 | // this.load.image('wholes-small', 'assets/img/wholes-small.png') 13 | // this.load.image('grass', 'assets/img/grass.png') 14 | // this.load.image('background', 'assets/img/background.png') 15 | this.load.multiatlas('atlas', 'assets/textureAtlas.json', 'assets') 16 | } 17 | 18 | create() { 19 | this.scene.start('BackgroundScene') 20 | this.scene.start('MainScene') 21 | this.scene.start('GuiScene') 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "strict": true, 6 | "noImplicitAny": true, 7 | "esModuleInterop": true, 8 | "allowUnreachableCode": false, 9 | "sourceMap": true, 10 | "strictPropertyInitialization": false, 11 | "typeRoots": ["node_modules/@types", "typings"], 12 | "lib": ["dom", "es2015.promise", "es5", "esnext"] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /typings/custom.d.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandeu/phaser3-matter-car-on-curved-terrain/530284ec4e032affa2f1fc9fae85550564fac221/typings/custom.d.ts -------------------------------------------------------------------------------- /webpack/webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const CopyWebpackPlugin = require('copy-webpack-plugin') 3 | const HtmlWebpackPlugin = require('html-webpack-plugin') 4 | const { InjectManifest } = require('workbox-webpack-plugin') 5 | 6 | module.exports = { 7 | entry: './src/scripts/game.ts', 8 | output: { 9 | filename: 'game.bundle.js', 10 | path: path.resolve(__dirname, '../dist') 11 | }, 12 | resolve: { 13 | extensions: ['.ts', '.tsx', '.js'] 14 | }, 15 | module: { 16 | rules: [{ test: /\.tsx?$/, loader: 'ts-loader' }] 17 | }, 18 | optimization: { 19 | splitChunks: { 20 | cacheGroups: { 21 | commons: { 22 | test: /[\\/]node_modules[\\/]/, 23 | name: 'vendors', 24 | chunks: 'all', 25 | filename: '[name].bundle.js' 26 | } 27 | } 28 | } 29 | }, 30 | plugins: [ 31 | new HtmlWebpackPlugin({ gameName: 'My Phaser Game', template: 'src/index.html' }), 32 | new CopyWebpackPlugin([{ from: 'src/assets', to: 'assets' }, { from: 'src/pwa', to: '' }]), 33 | new InjectManifest({ 34 | swSrc: path.resolve(__dirname, '../src/pwa/sw.js') 35 | }) 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /webpack/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge') 2 | const common = require('./webpack.common') 3 | 4 | const dev = { 5 | mode: 'development', 6 | devtool: 'inline-source-map', 7 | devServer: { 8 | open: true, 9 | noInfo: true 10 | } 11 | } 12 | 13 | module.exports = merge(common, dev) 14 | -------------------------------------------------------------------------------- /webpack/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const merge = require('webpack-merge') 3 | const common = require('./webpack.common') 4 | const JavaScriptObfuscator = require('webpack-obfuscator') 5 | const CleanWebpackPlugin = require('clean-webpack-plugin') 6 | 7 | const prod = { 8 | mode: 'production', 9 | output: { 10 | filename: 'game.[contenthash].js' 11 | }, 12 | optimization: { 13 | splitChunks: { 14 | cacheGroups: { 15 | commons: { 16 | filename: '[name].[contenthash].js' 17 | } 18 | } 19 | } 20 | }, 21 | plugins: [ 22 | new CleanWebpackPlugin(['dist/*.js'], { root: path.resolve(__dirname, '../') }), 23 | new JavaScriptObfuscator( 24 | { 25 | rotateStringArray: true, 26 | stringArray: true, 27 | // stringArrayEncoding: 'base64', // disabled by default 28 | stringArrayThreshold: 0.75 29 | }, 30 | ['vendors.*.js'] 31 | ) 32 | ] 33 | } 34 | 35 | module.exports = merge(common, prod) 36 | --------------------------------------------------------------------------------