├── .eslintrc.js ├── .gitattributes ├── .github └── workflows │ └── deploy-to-gh-pages.yml ├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── scripts ├── copy-tileset-data.js ├── generate-atlas-files.js ├── generate-bitmap-font.js └── tests.js ├── source_files ├── enemy.psd └── game_sample.gif └── src ├── Game.jsx ├── assets ├── atlases │ ├── generated │ │ ├── coin.json │ │ ├── coin.png │ │ ├── enemy.json │ │ ├── enemy.png │ │ ├── hero.json │ │ ├── hero.png │ │ ├── npc_01.json │ │ ├── npc_01.png │ │ ├── npc_02.json │ │ ├── npc_02.png │ │ ├── npc_03.json │ │ ├── npc_03.png │ │ ├── npc_04.json │ │ ├── npc_04.png │ │ ├── npc_05.json │ │ └── npc_05.png │ └── sources │ │ ├── coin │ │ ├── coin_idle_01.png │ │ └── coin_idle_02.png │ │ ├── enemy │ │ ├── walk_down_01.png │ │ ├── walk_down_02.png │ │ ├── walk_down_03.png │ │ ├── walk_left_01.png │ │ ├── walk_left_02.png │ │ ├── walk_left_03.png │ │ ├── walk_right_01.png │ │ ├── walk_right_02.png │ │ ├── walk_right_03.png │ │ ├── walk_up_01.png │ │ ├── walk_up_02.png │ │ └── walk_up_03.png │ │ ├── hero │ │ ├── walk_down_01.png │ │ ├── walk_down_02.png │ │ ├── walk_down_03.png │ │ ├── walk_left_01.png │ │ ├── walk_left_02.png │ │ ├── walk_left_03.png │ │ ├── walk_right_01.png │ │ ├── walk_right_02.png │ │ ├── walk_right_03.png │ │ ├── walk_up_01.png │ │ ├── walk_up_02.png │ │ └── walk_up_03.png │ │ ├── npc_01 │ │ ├── walk_down_01.png │ │ ├── walk_down_02.png │ │ ├── walk_down_03.png │ │ ├── walk_left_01.png │ │ ├── walk_left_02.png │ │ ├── walk_left_03.png │ │ ├── walk_right_01.png │ │ ├── walk_right_02.png │ │ ├── walk_right_03.png │ │ ├── walk_up_01.png │ │ ├── walk_up_02.png │ │ └── walk_up_03.png │ │ ├── npc_02 │ │ ├── walk_down_01.png │ │ ├── walk_down_02.png │ │ ├── walk_down_03.png │ │ ├── walk_left_01.png │ │ ├── walk_left_02.png │ │ ├── walk_left_03.png │ │ ├── walk_right_01.png │ │ ├── walk_right_02.png │ │ ├── walk_right_03.png │ │ ├── walk_up_01.png │ │ ├── walk_up_02.png │ │ └── walk_up_03.png │ │ ├── npc_03 │ │ ├── walk_down_01.png │ │ ├── walk_down_02.png │ │ ├── walk_down_03.png │ │ ├── walk_left_01.png │ │ ├── walk_left_02.png │ │ ├── walk_left_03.png │ │ ├── walk_right_01.png │ │ ├── walk_right_02.png │ │ ├── walk_right_03.png │ │ ├── walk_up_01.png │ │ ├── walk_up_02.png │ │ └── walk_up_03.png │ │ ├── npc_04 │ │ ├── walk_down_01.png │ │ ├── walk_down_02.png │ │ ├── walk_down_03.png │ │ ├── walk_left_01.png │ │ ├── walk_left_02.png │ │ ├── walk_left_03.png │ │ ├── walk_right_01.png │ │ ├── walk_right_02.png │ │ ├── walk_right_03.png │ │ ├── walk_up_01.png │ │ ├── walk_up_02.png │ │ └── walk_up_03.png │ │ └── npc_05 │ │ ├── walk_down_01.png │ │ ├── walk_down_02.png │ │ ├── walk_down_03.png │ │ ├── walk_left_01.png │ │ ├── walk_left_02.png │ │ ├── walk_left_03.png │ │ ├── walk_right_01.png │ │ ├── walk_right_02.png │ │ ├── walk_right_03.png │ │ ├── walk_up_01.png │ │ ├── walk_up_02.png │ │ └── walk_up_03.png ├── fonts │ ├── Munro-Narrow.ttf │ ├── Munro-Small.ttf │ ├── Munro.ttf │ ├── PressStart2P-Regular.ttf │ ├── press-start-medium-white.png │ ├── press-start-medium-white.xml │ ├── press-start-normal-white.png │ ├── press-start-normal-white.xml │ ├── press-start-small-white.png │ └── press-start-small-white.xml ├── images │ ├── a_button.png │ ├── b_button.png │ ├── background_desert.png │ ├── background_fall.png │ ├── background_forest.png │ ├── background_grass.png │ ├── background_winter.png │ ├── crystal.png │ ├── d_pad_button.png │ ├── enemy_01.png │ ├── enemy_02.png │ ├── enemy_03.png │ ├── heart_empty.png │ ├── heart_full.png │ ├── heart_half.png │ └── key.png ├── maps │ ├── sample_indoor.json │ └── sample_map.json └── tilesets │ ├── city.json │ ├── city.png │ ├── objects.json │ ├── objects.png │ ├── village.json │ └── village.png ├── components ├── DialogBox │ ├── DialogBox.jsx │ └── DialogBox.module.scss ├── GameMenu │ ├── GameMenu.jsx │ └── GameMenu.module.scss ├── GameText │ ├── GameText.jsx │ └── GameText.module.scss ├── Message │ ├── Message.jsx │ └── Message.module.scss ├── MessageBox.jsx ├── ReactWrapper.jsx └── VirtualGamepad │ ├── VirtualGamepad.jsx │ └── VirtualGamepad.module.scss ├── constants.js ├── game └── scenes │ ├── BootScene.js │ ├── GameScene.js │ ├── LoadAssetsScene.js │ └── MainMenuScene.js ├── hooks ├── useMutationObserver.jsx └── useRect.jsx ├── index.css ├── index.js ├── intl └── en.json ├── setupTests.js ├── utils ├── __tests__ │ └── phaser.test.js ├── phaser.js ├── sceneHelpers.js └── utils.js └── zustand ├── assets ├── selectLoadedAssets.js └── setLoadedAssets.js ├── dialog ├── selectDialog.js └── setDialog.js ├── game ├── selectGameData.js └── setGameData.js ├── hero ├── selectHeroData.js └── setHeroData.js ├── map ├── selectMapData.js └── setMapData.js ├── menu ├── selectMenu.js └── setMenu.js ├── store.js └── text ├── selectText.js └── setText.js /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js text eol=crlf 2 | *.jsx text eol=crlf 3 | *.json text eol=crlf 4 | *.md text eol=crlf 5 | 6 | *.png binary 7 | *.jpg binary 8 | *.gif binary 9 | *.mp4 binary 10 | *.webm binary 11 | -------------------------------------------------------------------------------- /.github/workflows/deploy-to-gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build-and-deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3.3.0 14 | with: 15 | persist-credentials: false 16 | 17 | - name: Install and Build 🚧 18 | run: | 19 | npm cache clean --force 20 | npm install 21 | npm run build 22 | 23 | - name: Deploy 🚀 24 | uses: JamesIves/github-pages-deploy-action@v4.4.1 25 | with: 26 | branch: gh-pages 27 | folder: build 28 | clean: true 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .idea/* 26 | .idea 27 | .idea/ 28 | .env 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Pablo 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 | # Top-Down Phaser Game with React UI Template 2 | 3 | This project is based on a Medium post: https://javascript.plainenglish.io/i-made-a-top-down-game-version-of-my-blog-with-phaser-and-react-faf5c28cf768 4 | 5 | 6 | 7 | ## Try it out: https://blopa.github.io/top-down-react-phaser-game-template/ 8 | 9 | # Key Features 10 | - Built with Create React App 11 | - Uses Phaser 3 for game engine 12 | - State management with Zustand 13 | - UI with Material UI and React 18 14 | - CSS Modules 15 | - Uses functional programming style 16 | - Arcade physics 17 | - Automatically resizes game to fit browser window 18 | - Automatically loads Tilesets and assets 19 | - Generates atlas sheets with included script 20 | - Adjustable tile sizes 21 | - Integrates Phaser and React through Zustand 22 | - Dialog system (React-based) 23 | - Game menu (React-based) 24 | - Virtual Gamepad for mobile devices (React-based) 25 | - Includes 2D assets from Kenney.nl 26 | 27 | # How to Use 28 | 29 | ## Load Scene Files 30 | The `getScenesModules` function uses Webpack's [require.context](https://webpack.js.org/guides/dependency-management/#requirecontext) to load all `.js` and `.ts` files from the `/src/assets/games/scenes` directory. Simply add your game scenes there to have them loaded into the game. 31 | 32 | The first scene loaded by Phaser JS is the one defined in the `constants.js` file, in the `BOOT_SCENE_NAME` variable. 33 | 34 | ## Functional Programming 35 | Scene code can be written in a functional style for improved readability, by exporting functions instead of using the `Phaser.Scene` class. 36 | 37 | ```javascript 38 | // Export scene using class-based approach 39 | export default class BootScene extends Scene { 40 | constructor() { 41 | super('BootScene'); 42 | } 43 | 44 | preload() { 45 | this.load.image('background', background); 46 | } 47 | 48 | create() { 49 | this.add.image(100, 100, 'background'); 50 | } 51 | } 52 | ``` 53 | 54 | ```javascript 55 | // Export scene in functional approach 56 | export const scene = {}; 57 | 58 | export const key = 'BootScene'; 59 | 60 | export function preload() { 61 | scene.load.image('background', background); 62 | } 63 | 64 | export function create() { 65 | scene.add.image(100, 100, 'background'); 66 | } 67 | ``` 68 | 69 | The exported `scene` object will have all the helper functions of `Phaser.Scene`. While it can still be accessed with `this`, the functional approach is designed to improve code readability. 70 | 71 | This "magic" is made possible by the `prepareScene` function. 72 | 73 | ## Maps 74 | To use Tiled maps, add your Tiled tilesets JSON and images to `/src/assets/tilesets` and your Tiled maps to `/src/assets/maps`. Then start the `LoadAssetsScene` like this: 75 | 76 | ```javascript 77 | this.scene.start('LoadAssetsScene', { 78 | nextScene: 'GameScene', // Scene to load after assets are loaded 79 | assets: { 80 | mapKey: 'sample_map', // Map name, e.g. sample_map.json 81 | }, 82 | }); 83 | ``` 84 | 85 | Any tilesets used in your `sample_map.json` will be automatically loaded from the `/src/assets/tilesets` directory, as long as they are located there. 86 | 87 | ## Other assets 88 | To load other assets such as images, fonts, or atlases, call the `LoadAssetsScene` with the following parameters: 89 | 90 | ```javascript 91 | this.scene.start('LoadAssetsScene', { 92 | nextScene: 'GameScene', // scene to be loaded after the assets are loaded 93 | assets: { 94 | fonts: ['"Press Start 2P"'], // fonts to be loaded 95 | atlases: ['hero'], // atlases to be loaded, must be in `/src/assets/atlases/generated/` as hero.json and hero.png 96 | images: ['background'], // images to be loaded, must be in `/src/assets/images` as background.png 97 | }, 98 | }); 99 | ``` 100 | 101 | ## The 'GameScene' 102 | The `GameScene` file is where the game map is rendered, along with all items, enemies, etc. The `create` function is split into smaller functions for easier readability, which can be found in the `sceneHelpers.js` file. 103 | 104 | ## Virtual Gamepad 105 | The virtual gamepad will be automatically loaded when the game is run on a mobile device. The virtual gamepad is a React component that simulates keyboard keys to control the game, using the `simulateKeyEvent` function found in this [GitHub Gist](https://gist.github.com/GlauberF/d8278ce3aa592389e6e3d4e758e6a0c2). 106 | 107 | ## Dialog System 108 | A dialog box will appear automatically whenever the `state.dialog.messages` variable is populated with messages. To accomplish this, you can call the `setDialogMessagesAction` Zustand setter function. 109 | 110 | ```javascript 111 | setDialogMessages(['hello world', 'hello world 2']); 112 | ``` 113 | 114 | # Assets by Kenney.nl: 115 | - https://www.kenney.nl/assets/rpg-urban-pack 116 | - https://www.kenney.nl/assets/roguelike-rpg-pack 117 | - https://www.kenney.nl/assets/pixel-platformer 118 | - https://www.kenney.nl/assets/onscreen-controls 119 | - https://www.kenney.nl/assets/background-elements-redux 120 | - https://kenney.itch.io/creature-mixer 121 | 122 | # License 123 | MIT License 124 | 125 | Copyright (c) 2023 Pablo 126 | 127 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 128 | 129 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 130 | 131 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 132 | 133 | **Free Software, Hell Yeah!** 134 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "top-down-react-phaser-game-template", 3 | "homepage": ".", 4 | "version": "1.0.0", 5 | "private": false, 6 | "devDependencies": { 7 | "@babel/eslint-parser": "^7.19.1", 8 | "@babel/eslint-plugin": "^7.19.1", 9 | "@rtpa/phaser-bitmapfont-generator": "^1.2.2", 10 | "@testing-library/jest-dom": "^5.16.5", 11 | "@testing-library/react": "^13.4.0", 12 | "@testing-library/user-event": "^14.4.3", 13 | "eslint": "^8.34.0", 14 | "eslint-config-airbnb": "^19.0.4", 15 | "eslint-config-standard": "^17.0.0", 16 | "eslint-config-standard-jsx": "^11.0.0", 17 | "eslint-import-resolver-webpack": "^0.13.2", 18 | "eslint-plugin-babel": "^5.3.1", 19 | "eslint-plugin-filenames": "^1.3.2", 20 | "eslint-plugin-flowtype": "^8.0.3", 21 | "eslint-plugin-import": "^2.27.5", 22 | "eslint-plugin-jest": "^27.2.1", 23 | "eslint-plugin-jsx-a11y": "^6.7.1", 24 | "eslint-plugin-prettier": "^4.2.1", 25 | "eslint-plugin-react": "^7.32.2", 26 | "eslint-plugin-react-hooks": "^4.6.0", 27 | "eslint-plugin-unicorn": "^45.0.2", 28 | "eslint-plugin-unused-imports": "^2.0.0", 29 | "free-tex-packer-core": "^0.3.4", 30 | "npm-check-updates": "^16.7.5" 31 | }, 32 | "dependencies": { 33 | "@emotion/react": "^11.10.6", 34 | "@emotion/styled": "^11.10.6", 35 | "@mui/icons-material": "^5.11.9", 36 | "@mui/material": "^5.11.9", 37 | "@react-spring/web": "^9.6.1", 38 | "beautiful-react-hooks": "^3.12.2", 39 | "classnames": "^2.3.2", 40 | "cross-env": "^7.0.3", 41 | "is-mobile": "^3.1.1", 42 | "jest": "^27.5.1", 43 | "phaser": "^3.55.2", 44 | "react": "^18.2.0", 45 | "react-dom": "^18.2.0", 46 | "react-helmet": "^6.1.0", 47 | "react-intl": "^6.2.8", 48 | "react-scripts": "^5.0.1", 49 | "sass": "^1.58.3", 50 | "zustand": "^4.3.3" 51 | }, 52 | "overrides": { 53 | "react-router-dom": "5.3.4", 54 | "react-error-overlay": "6.0.9" 55 | }, 56 | "scripts": { 57 | "generate-bitmap-font": "node scripts/generate-bitmap-font.js", 58 | "generate-atlases": "node scripts/generate-atlas-files.js", 59 | "copy-tileset-data": "node scripts/copy-tileset-data.js", 60 | "start": "cross-env IMAGE_INLINE_SIZE_LIMIT=1 PUBLIC_URL=/ react-scripts start", 61 | "build": "cross-env IMAGE_INLINE_SIZE_LIMIT=1 CI=false react-scripts build", 62 | "test": "react-scripts test", 63 | "eject": "react-scripts eject", 64 | "bump": "ncu -u -t minor", 65 | "clean": "rm node_modules/.cache/.eslintcache" 66 | }, 67 | "browserslist": { 68 | "production": [ 69 | ">0.2%", 70 | "not dead", 71 | "not op_mini all" 72 | ], 73 | "development": [ 74 | "last 1 chrome version", 75 | "last 1 firefox version", 76 | "last 1 safari version" 77 | ] 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 17 | 18 | 22 | 23 | 32 | Game Title 33 | 34 | 35 | 36 |
37 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /scripts/copy-tileset-data.js: -------------------------------------------------------------------------------- 1 | const { readdirSync, readFileSync, writeFileSync } = require('fs'); 2 | const path = require('path'); 3 | 4 | const TILESETS_PATH = path.resolve( 5 | __dirname, 6 | '..', 7 | 'src', 8 | 'assets', 9 | 'maps' 10 | ); 11 | 12 | async function copyTilesetData(mapName = null) { 13 | if (!mapName) { 14 | console.error('No map passed'); 15 | return; 16 | } 17 | 18 | const allFiles = []; 19 | let sourceTilesetData = []; 20 | const mapsFolders = await readdirSync(TILESETS_PATH); 21 | // eslint-disable-next-line no-restricted-syntax 22 | for (const tilesetPath of mapsFolders) { 23 | const filePath = path.resolve(TILESETS_PATH, tilesetPath); 24 | // eslint-disable-next-line no-await-in-loop 25 | const spritesFiles = await readdirSync(filePath); 26 | // eslint-disable-next-line no-restricted-syntax 27 | for (const spritesFile of spritesFiles) { 28 | if (spritesFile === `${mapName}.json`) { 29 | // eslint-disable-next-line no-await-in-loop 30 | const jsonData = JSON.parse(await readFileSync(path.resolve(filePath, spritesFile))); 31 | sourceTilesetData = jsonData.tilesets; 32 | } else { 33 | allFiles.push(path.resolve(filePath, spritesFile)); 34 | } 35 | } 36 | } 37 | 38 | // eslint-disable-next-line no-restricted-syntax 39 | for (const jsonFile of allFiles) { 40 | // eslint-disable-next-line no-await-in-loop 41 | const jsonData = JSON.parse(await readFileSync( 42 | jsonFile 43 | )); 44 | 45 | jsonData.tilesets = sourceTilesetData; 46 | 47 | // eslint-disable-next-line no-await-in-loop 48 | await writeFileSync( 49 | jsonFile, 50 | JSON.stringify(jsonData, null, 2) 51 | ); 52 | } 53 | } 54 | 55 | copyTilesetData(process.argv[2]); 56 | -------------------------------------------------------------------------------- /scripts/generate-atlas-files.js: -------------------------------------------------------------------------------- 1 | const { readdirSync, readFileSync, writeFileSync } = require('fs'); 2 | const { packAsync } = require('free-tex-packer-core'); 3 | const path = require('path'); 4 | 5 | const SOURCE_SPRITES_PATH = path.resolve( 6 | __dirname, 7 | '..', 8 | 'src', 9 | 'assets', 10 | 'atlases', 11 | 'sources' 12 | ); 13 | 14 | const SPRITES_PATH = path.resolve( 15 | __dirname, 16 | '..', 17 | 'src', 18 | 'assets', 19 | 'atlases', 20 | 'generated' 21 | ); 22 | 23 | async function generateAtlasFiles(assetName = null) { 24 | let spritesFolders; 25 | if (assetName) { 26 | spritesFolders = [assetName]; 27 | } else { 28 | spritesFolders = await readdirSync(SOURCE_SPRITES_PATH); 29 | } 30 | 31 | // eslint-disable-next-line no-restricted-syntax 32 | for (const spritesFolder of spritesFolders) { 33 | // eslint-disable-next-line no-await-in-loop 34 | const spritesFiles = await readdirSync(path.resolve(__dirname, SOURCE_SPRITES_PATH, spritesFolder)); 35 | const images = []; 36 | spritesFiles.forEach((spritesFile) => { 37 | images.push({ 38 | path: spritesFile, 39 | contents: readFileSync( 40 | path.resolve(__dirname, SOURCE_SPRITES_PATH, spritesFolder, spritesFile) 41 | ), 42 | }); 43 | }); 44 | 45 | // eslint-disable-next-line no-await-in-loop 46 | await packImages(images, spritesFolder); 47 | } 48 | } 49 | 50 | async function packImages(images, spriteName) { 51 | try { 52 | const files = await packAsync(images, { 53 | allowRotation: false, 54 | removeFileExtension: true, 55 | prependFolderName: false, 56 | exporter: 'Phaser3', 57 | allowTrim: true, 58 | detectIdentical: true, 59 | fixedSize: false, 60 | textureName: spriteName, 61 | }); 62 | // eslint-disable-next-line no-restricted-syntax 63 | for (const item of files) { 64 | const fileExt = item.name.split('.').pop(); 65 | // eslint-disable-next-line no-await-in-loop 66 | await writeFileSync( 67 | path.resolve(__dirname, SPRITES_PATH, `${spriteName}.${fileExt}`), 68 | item.buffer 69 | ); 70 | } 71 | } catch (error) { 72 | console.log(error); 73 | } 74 | } 75 | 76 | generateAtlasFiles(process.argv[2]); 77 | -------------------------------------------------------------------------------- /scripts/generate-bitmap-font.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { TextStyle2BitmapFont } = require('@rtpa/phaser-bitmapfont-generator'); 3 | 4 | const FONTS_PATH = path.resolve(__dirname, '../src/assets/fonts'); 5 | 6 | const sizes = [{ 7 | fontSize: '10px', 8 | fileName: 'press-start-small-white', 9 | }, { 10 | fontSize: '18px', 11 | fileName: 'press-start-medium-white', 12 | }, { 13 | fontSize: '20px', 14 | fileName: 'press-start-normal-white', 15 | }]; 16 | 17 | const doWork = async () => { 18 | const fontFamily = process.argv[2] || '"Press Start 2P"'; 19 | 20 | // eslint-disable-next-line no-restricted-syntax 21 | for (const sizeData of sizes) { 22 | const { fontSize, fileName } = sizeData; 23 | // eslint-disable-next-line no-await-in-loop 24 | await TextStyle2BitmapFont( 25 | { 26 | path: FONTS_PATH, 27 | fileName, 28 | compression: null, 29 | antialias: false, 30 | // Phaser.GameObjects.RetroFont.TEXT_SET1 31 | textSet: ' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZÁÂÉÍÕÚÇÃÀÊÓÔÜ[\\]^_`abcdefghijklmnopqrstuvwxyzáâéíõúçãàêóôü{|}~', 32 | textStyle: { 33 | fontFamily, 34 | fontSize, 35 | color: '#ffffff', 36 | // shadow: { 37 | // offsetX: 4, 38 | // offsetY: 4, 39 | // blur: 0, 40 | // fill: true, 41 | // stroke: true, 42 | // color: '#000000', 43 | // }, 44 | }, 45 | } 46 | ); 47 | } 48 | 49 | return process.exit(1); 50 | }; 51 | 52 | doWork(); 53 | -------------------------------------------------------------------------------- /scripts/tests.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/scripts/tests.js -------------------------------------------------------------------------------- /source_files/enemy.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/source_files/enemy.psd -------------------------------------------------------------------------------- /source_files/game_sample.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/source_files/game_sample.gif -------------------------------------------------------------------------------- /src/Game.jsx: -------------------------------------------------------------------------------- 1 | import Phaser from 'phaser'; 2 | import { useCallback, useEffect, useState } from 'react'; 3 | import { IntlProvider } from 'react-intl'; 4 | import isMobile from 'is-mobile'; 5 | 6 | // Utils 7 | import { calculateGameSize, getScenesModules } from './utils/phaser'; 8 | import { isDev } from './utils/utils'; 9 | 10 | // Components 11 | import VirtualGamepad from './components/VirtualGamepad/VirtualGamepad'; 12 | import ReactWrapper from './components/ReactWrapper'; 13 | 14 | // Selectors 15 | import { 16 | selectGameZoom, 17 | selectGameWidth, 18 | selectGameHeight, 19 | selectGameLocale, 20 | selectGameSetters, 21 | selectGameCameraSizeUpdateCallbacks, 22 | } from './zustand/game/selectGameData'; 23 | 24 | // Store 25 | import { useGameStore } from './zustand/store'; 26 | 27 | // Constants 28 | import { 29 | TILE_WIDTH, 30 | TILE_HEIGHT, 31 | DEFAULT_LOCALE, 32 | MIN_GAME_WIDTH, 33 | GAME_CONTENT_ID, 34 | MIN_GAME_HEIGHT, 35 | RESIZE_THRESHOLD, 36 | RE_RESIZE_THRESHOLD, 37 | } from './constants'; 38 | import defaultMessages from './intl/en.json'; 39 | 40 | const IS_DEV = isDev(); 41 | 42 | function Game() { 43 | const [game, setGame] = useState(null); 44 | const locale = useGameStore(selectGameLocale) || DEFAULT_LOCALE; 45 | const cameraSizeUpdateCallbacks = useGameStore(selectGameCameraSizeUpdateCallbacks); 46 | const [messages, setMessages] = useState(defaultMessages); 47 | const { 48 | setGameZoom, 49 | setGameWidth, 50 | setGameHeight, 51 | setGameCanvasElement, 52 | } = useGameStore(selectGameSetters); 53 | 54 | // Game 55 | const gameWidth = useGameStore(selectGameWidth); 56 | const gameHeight = useGameStore(selectGameHeight); 57 | const gameZoom = useGameStore(selectGameZoom); 58 | 59 | useEffect(() => { 60 | document.documentElement.style.setProperty('--game-zoom', gameZoom); 61 | document.documentElement.style.setProperty('--game-height', gameHeight); 62 | document.documentElement.style.setProperty('--game-width', gameWidth); 63 | }, [gameHeight, gameWidth, gameZoom]); 64 | 65 | useEffect(() => { 66 | async function loadMessages() { 67 | const module = await import(`./intl/${locale}.json`); 68 | setMessages(module.default); 69 | } 70 | 71 | if (locale !== DEFAULT_LOCALE) { 72 | loadMessages(); 73 | } 74 | }, [locale]); 75 | 76 | const updateGameGlobalState = useCallback(( 77 | gameWidth, 78 | gameHeight, 79 | gameZoom 80 | ) => { 81 | setGameHeight(gameHeight); 82 | setGameWidth(gameWidth); 83 | setGameZoom(gameZoom); 84 | }, [setGameHeight, setGameWidth, setGameZoom]); 85 | 86 | // Create the game inside a useEffect 87 | // to create it only once 88 | useEffect(() => { 89 | // do this otherwise development hot-reload 90 | // will create a bunch of Phaser instances 91 | if (game) { 92 | return; 93 | } 94 | 95 | const { width, height, zoom } = calculateGameSize( 96 | MIN_GAME_WIDTH, 97 | MIN_GAME_HEIGHT, 98 | TILE_WIDTH, 99 | TILE_HEIGHT 100 | ); 101 | 102 | const phaserGame = new Phaser.Game({ 103 | type: Phaser.AUTO, 104 | title: 'some-game-title', 105 | parent: GAME_CONTENT_ID, 106 | orientation: Phaser.Scale.LANDSCAPE, 107 | localStorageName: 'some-game-title', 108 | width, 109 | height, 110 | zoom, 111 | autoRound: true, 112 | pixelArt: true, 113 | scale: { 114 | autoCenter: Phaser.Scale.CENTER_BOTH, 115 | mode: Phaser.Scale.NONE, 116 | }, 117 | scene: getScenesModules(), 118 | physics: { 119 | default: 'arcade', 120 | arcade: { 121 | debug: IS_DEV, 122 | }, 123 | }, 124 | backgroundColor: '#000000', 125 | }); 126 | 127 | updateGameGlobalState(width, height, zoom); 128 | setGame(phaserGame); 129 | 130 | if (IS_DEV) { 131 | window.phaserGame = phaserGame; 132 | } 133 | }, [ 134 | game, 135 | updateGameGlobalState, 136 | ]); 137 | 138 | useEffect(() => { 139 | if (game?.canvas) { 140 | setGameCanvasElement(game.canvas); 141 | } 142 | }, [setGameCanvasElement, game?.canvas]); 143 | 144 | useEffect(() => { 145 | if (!game) { 146 | return () => {}; 147 | } 148 | 149 | // Create listener to resize the game 150 | // when the window is resized 151 | let timeOutFunctionId; 152 | const resizeDoneCallback = () => { 153 | const scaleGame = () => { 154 | const gameSize = calculateGameSize( 155 | MIN_GAME_WIDTH, 156 | MIN_GAME_HEIGHT, 157 | TILE_WIDTH, 158 | TILE_HEIGHT 159 | ); 160 | 161 | // console.log(JSON.stringify(gameSize)); 162 | game.scale.setZoom(gameSize.zoom); 163 | game.scale.resize(gameSize.width, gameSize.height); 164 | // game.scale.setGameSize(gameSize.width, gameSize.height); 165 | // game.scale.displaySize.resize(gameSize.width, gameSize.height); 166 | // game.scale.resize(gameSize.width, gameSize.height).getParentBounds(); 167 | updateGameGlobalState(gameSize.width, gameSize.height, gameSize.zoom); 168 | // game.canvas.style.width = `${gameSize.width}px`; 169 | // game.canvas.style.height = `${gameSize.height}px`; 170 | cameraSizeUpdateCallbacks.forEach((cameraSizeUpdateCallback) => { 171 | cameraSizeUpdateCallback?.(); 172 | }); 173 | }; 174 | 175 | scaleGame(); 176 | 177 | // re-run function after resize is done to re-trigger css calculations 178 | setTimeout(scaleGame, RE_RESIZE_THRESHOLD); 179 | }; 180 | 181 | const canvasResizeCallback = () => { 182 | clearTimeout(timeOutFunctionId); 183 | timeOutFunctionId = setTimeout(resizeDoneCallback, RESIZE_THRESHOLD); 184 | }; 185 | 186 | // TODO move to the ResizeObserver https://jsfiddle.net/rudiedirkx/p0ckdcnv/ 187 | window.addEventListener('resize', canvasResizeCallback); 188 | 189 | return () => { 190 | window.removeEventListener('resize', canvasResizeCallback); 191 | }; 192 | }, [ 193 | game, 194 | updateGameGlobalState, 195 | cameraSizeUpdateCallbacks, 196 | ]); 197 | 198 | return ( 199 | 204 |
208 | {/* this is where the game canvas will be rendered */} 209 |
210 | 211 | {isMobile() && ( 212 | 213 | )} 214 |
215 | ); 216 | } 217 | 218 | export default Game; 219 | -------------------------------------------------------------------------------- /src/assets/atlases/generated/coin.json: -------------------------------------------------------------------------------- 1 | { 2 | "textures": [ 3 | { 4 | "image": "coin.png", 5 | "format": "RGBA8888", 6 | "size": { 7 | "w": 12, 8 | "h": 24 9 | }, 10 | "scale": 1, 11 | "frames": [ 12 | { 13 | "filename": "coin_idle_01", 14 | "rotated": false, 15 | "trimmed": true, 16 | "sourceSize": { 17 | "w": 16, 18 | "h": 16 19 | }, 20 | "spriteSourceSize": { 21 | "x": 2, 22 | "y": 2, 23 | "w": 12, 24 | "h": 12 25 | }, 26 | "frame": { 27 | "x": 0, 28 | "y": 0, 29 | "w": 12, 30 | "h": 12 31 | } 32 | }, 33 | { 34 | "filename": "coin_idle_02", 35 | "rotated": false, 36 | "trimmed": true, 37 | "sourceSize": { 38 | "w": 16, 39 | "h": 16 40 | }, 41 | "spriteSourceSize": { 42 | "x": 4, 43 | "y": 2, 44 | "w": 8, 45 | "h": 12 46 | }, 47 | "frame": { 48 | "x": 0, 49 | "y": 12, 50 | "w": 8, 51 | "h": 12 52 | } 53 | } 54 | ] 55 | } 56 | ], 57 | "meta": { 58 | "app": "http://github.com/odrick/free-tex-packer-core", 59 | "version": "0.3.4" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/assets/atlases/generated/coin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/generated/coin.png -------------------------------------------------------------------------------- /src/assets/atlases/generated/enemy.json: -------------------------------------------------------------------------------- 1 | { 2 | "textures": [ 3 | { 4 | "image": "enemy.png", 5 | "format": "RGBA8888", 6 | "size": { 7 | "w": 14, 8 | "h": 156 9 | }, 10 | "scale": 1, 11 | "frames": [ 12 | { 13 | "filename": "walk_down_01", 14 | "rotated": false, 15 | "trimmed": true, 16 | "sourceSize": { 17 | "w": 16, 18 | "h": 16 19 | }, 20 | "spriteSourceSize": { 21 | "x": 1, 22 | "y": 0, 23 | "w": 14, 24 | "h": 16 25 | }, 26 | "frame": { 27 | "x": 0, 28 | "y": 0, 29 | "w": 14, 30 | "h": 16 31 | } 32 | }, 33 | { 34 | "filename": "walk_down_03", 35 | "rotated": false, 36 | "trimmed": true, 37 | "sourceSize": { 38 | "w": 16, 39 | "h": 16 40 | }, 41 | "spriteSourceSize": { 42 | "x": 1, 43 | "y": 0, 44 | "w": 14, 45 | "h": 16 46 | }, 47 | "frame": { 48 | "x": 0, 49 | "y": 16, 50 | "w": 14, 51 | "h": 16 52 | } 53 | }, 54 | { 55 | "filename": "walk_left_01", 56 | "rotated": false, 57 | "trimmed": true, 58 | "sourceSize": { 59 | "w": 16, 60 | "h": 16 61 | }, 62 | "spriteSourceSize": { 63 | "x": 1, 64 | "y": 0, 65 | "w": 14, 66 | "h": 16 67 | }, 68 | "frame": { 69 | "x": 0, 70 | "y": 32, 71 | "w": 14, 72 | "h": 16 73 | } 74 | }, 75 | { 76 | "filename": "walk_right_01", 77 | "rotated": false, 78 | "trimmed": true, 79 | "sourceSize": { 80 | "w": 16, 81 | "h": 16 82 | }, 83 | "spriteSourceSize": { 84 | "x": 1, 85 | "y": 0, 86 | "w": 14, 87 | "h": 16 88 | }, 89 | "frame": { 90 | "x": 0, 91 | "y": 48, 92 | "w": 14, 93 | "h": 16 94 | } 95 | }, 96 | { 97 | "filename": "walk_up_01", 98 | "rotated": false, 99 | "trimmed": true, 100 | "sourceSize": { 101 | "w": 16, 102 | "h": 16 103 | }, 104 | "spriteSourceSize": { 105 | "x": 1, 106 | "y": 0, 107 | "w": 14, 108 | "h": 16 109 | }, 110 | "frame": { 111 | "x": 0, 112 | "y": 64, 113 | "w": 14, 114 | "h": 16 115 | } 116 | }, 117 | { 118 | "filename": "walk_up_03", 119 | "rotated": false, 120 | "trimmed": true, 121 | "sourceSize": { 122 | "w": 16, 123 | "h": 16 124 | }, 125 | "spriteSourceSize": { 126 | "x": 1, 127 | "y": 0, 128 | "w": 14, 129 | "h": 16 130 | }, 131 | "frame": { 132 | "x": 0, 133 | "y": 80, 134 | "w": 14, 135 | "h": 16 136 | } 137 | }, 138 | { 139 | "filename": "walk_down_02", 140 | "rotated": false, 141 | "trimmed": true, 142 | "sourceSize": { 143 | "w": 16, 144 | "h": 16 145 | }, 146 | "spriteSourceSize": { 147 | "x": 1, 148 | "y": 1, 149 | "w": 14, 150 | "h": 15 151 | }, 152 | "frame": { 153 | "x": 0, 154 | "y": 96, 155 | "w": 14, 156 | "h": 15 157 | } 158 | }, 159 | { 160 | "filename": "walk_left_02", 161 | "rotated": false, 162 | "trimmed": true, 163 | "sourceSize": { 164 | "w": 16, 165 | "h": 16 166 | }, 167 | "spriteSourceSize": { 168 | "x": 1, 169 | "y": 1, 170 | "w": 14, 171 | "h": 15 172 | }, 173 | "frame": { 174 | "x": 0, 175 | "y": 111, 176 | "w": 14, 177 | "h": 15 178 | } 179 | }, 180 | { 181 | "filename": "walk_right_02", 182 | "rotated": false, 183 | "trimmed": true, 184 | "sourceSize": { 185 | "w": 16, 186 | "h": 16 187 | }, 188 | "spriteSourceSize": { 189 | "x": 1, 190 | "y": 1, 191 | "w": 14, 192 | "h": 15 193 | }, 194 | "frame": { 195 | "x": 0, 196 | "y": 126, 197 | "w": 14, 198 | "h": 15 199 | } 200 | }, 201 | { 202 | "filename": "walk_up_02", 203 | "rotated": false, 204 | "trimmed": true, 205 | "sourceSize": { 206 | "w": 16, 207 | "h": 16 208 | }, 209 | "spriteSourceSize": { 210 | "x": 1, 211 | "y": 1, 212 | "w": 14, 213 | "h": 15 214 | }, 215 | "frame": { 216 | "x": 0, 217 | "y": 141, 218 | "w": 14, 219 | "h": 15 220 | } 221 | }, 222 | { 223 | "filename": "walk_left_03", 224 | "rotated": false, 225 | "trimmed": true, 226 | "sourceSize": { 227 | "w": 16, 228 | "h": 16 229 | }, 230 | "spriteSourceSize": { 231 | "x": 1, 232 | "y": 0, 233 | "w": 14, 234 | "h": 16 235 | }, 236 | "frame": { 237 | "x": 0, 238 | "y": 32, 239 | "w": 14, 240 | "h": 16 241 | } 242 | }, 243 | { 244 | "filename": "walk_right_03", 245 | "rotated": false, 246 | "trimmed": true, 247 | "sourceSize": { 248 | "w": 16, 249 | "h": 16 250 | }, 251 | "spriteSourceSize": { 252 | "x": 1, 253 | "y": 0, 254 | "w": 14, 255 | "h": 16 256 | }, 257 | "frame": { 258 | "x": 0, 259 | "y": 48, 260 | "w": 14, 261 | "h": 16 262 | } 263 | } 264 | ] 265 | } 266 | ], 267 | "meta": { 268 | "app": "http://github.com/odrick/free-tex-packer-core", 269 | "version": "0.3.4" 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/assets/atlases/generated/enemy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/generated/enemy.png -------------------------------------------------------------------------------- /src/assets/atlases/generated/hero.json: -------------------------------------------------------------------------------- 1 | { 2 | "textures": [ 3 | { 4 | "image": "hero.png", 5 | "format": "RGBA8888", 6 | "size": { 7 | "w": 23, 8 | "h": 134 9 | }, 10 | "scale": 1, 11 | "frames": [ 12 | { 13 | "filename": "walk_down_01", 14 | "rotated": false, 15 | "trimmed": true, 16 | "sourceSize": { 17 | "w": 16, 18 | "h": 16 19 | }, 20 | "spriteSourceSize": { 21 | "x": 2, 22 | "y": 1, 23 | "w": 12, 24 | "h": 15 25 | }, 26 | "frame": { 27 | "x": 0, 28 | "y": 0, 29 | "w": 12, 30 | "h": 15 31 | } 32 | }, 33 | { 34 | "filename": "walk_down_03", 35 | "rotated": false, 36 | "trimmed": true, 37 | "sourceSize": { 38 | "w": 16, 39 | "h": 16 40 | }, 41 | "spriteSourceSize": { 42 | "x": 2, 43 | "y": 1, 44 | "w": 12, 45 | "h": 15 46 | }, 47 | "frame": { 48 | "x": 0, 49 | "y": 15, 50 | "w": 12, 51 | "h": 15 52 | } 53 | }, 54 | { 55 | "filename": "walk_up_01", 56 | "rotated": false, 57 | "trimmed": true, 58 | "sourceSize": { 59 | "w": 16, 60 | "h": 16 61 | }, 62 | "spriteSourceSize": { 63 | "x": 2, 64 | "y": 1, 65 | "w": 12, 66 | "h": 15 67 | }, 68 | "frame": { 69 | "x": 0, 70 | "y": 30, 71 | "w": 12, 72 | "h": 15 73 | } 74 | }, 75 | { 76 | "filename": "walk_up_03", 77 | "rotated": false, 78 | "trimmed": true, 79 | "sourceSize": { 80 | "w": 16, 81 | "h": 16 82 | }, 83 | "spriteSourceSize": { 84 | "x": 2, 85 | "y": 1, 86 | "w": 12, 87 | "h": 15 88 | }, 89 | "frame": { 90 | "x": 0, 91 | "y": 45, 92 | "w": 12, 93 | "h": 15 94 | } 95 | }, 96 | { 97 | "filename": "walk_left_01", 98 | "rotated": false, 99 | "trimmed": true, 100 | "sourceSize": { 101 | "w": 16, 102 | "h": 16 103 | }, 104 | "spriteSourceSize": { 105 | "x": 2, 106 | "y": 1, 107 | "w": 11, 108 | "h": 15 109 | }, 110 | "frame": { 111 | "x": 0, 112 | "y": 60, 113 | "w": 11, 114 | "h": 15 115 | } 116 | }, 117 | { 118 | "filename": "walk_left_03", 119 | "rotated": false, 120 | "trimmed": true, 121 | "sourceSize": { 122 | "w": 16, 123 | "h": 16 124 | }, 125 | "spriteSourceSize": { 126 | "x": 2, 127 | "y": 1, 128 | "w": 11, 129 | "h": 15 130 | }, 131 | "frame": { 132 | "x": 0, 133 | "y": 75, 134 | "w": 11, 135 | "h": 15 136 | } 137 | }, 138 | { 139 | "filename": "walk_right_01", 140 | "rotated": false, 141 | "trimmed": true, 142 | "sourceSize": { 143 | "w": 16, 144 | "h": 16 145 | }, 146 | "spriteSourceSize": { 147 | "x": 3, 148 | "y": 1, 149 | "w": 11, 150 | "h": 15 151 | }, 152 | "frame": { 153 | "x": 0, 154 | "y": 90, 155 | "w": 11, 156 | "h": 15 157 | } 158 | }, 159 | { 160 | "filename": "walk_right_03", 161 | "rotated": false, 162 | "trimmed": true, 163 | "sourceSize": { 164 | "w": 16, 165 | "h": 16 166 | }, 167 | "spriteSourceSize": { 168 | "x": 3, 169 | "y": 1, 170 | "w": 11, 171 | "h": 15 172 | }, 173 | "frame": { 174 | "x": 0, 175 | "y": 105, 176 | "w": 11, 177 | "h": 15 178 | } 179 | }, 180 | { 181 | "filename": "walk_down_02", 182 | "rotated": false, 183 | "trimmed": true, 184 | "sourceSize": { 185 | "w": 16, 186 | "h": 16 187 | }, 188 | "spriteSourceSize": { 189 | "x": 2, 190 | "y": 2, 191 | "w": 12, 192 | "h": 14 193 | }, 194 | "frame": { 195 | "x": 0, 196 | "y": 120, 197 | "w": 12, 198 | "h": 14 199 | } 200 | }, 201 | { 202 | "filename": "walk_up_02", 203 | "rotated": false, 204 | "trimmed": true, 205 | "sourceSize": { 206 | "w": 16, 207 | "h": 16 208 | }, 209 | "spriteSourceSize": { 210 | "x": 2, 211 | "y": 2, 212 | "w": 12, 213 | "h": 14 214 | }, 215 | "frame": { 216 | "x": 11, 217 | "y": 60, 218 | "w": 12, 219 | "h": 14 220 | } 221 | }, 222 | { 223 | "filename": "walk_left_02", 224 | "rotated": false, 225 | "trimmed": true, 226 | "sourceSize": { 227 | "w": 16, 228 | "h": 16 229 | }, 230 | "spriteSourceSize": { 231 | "x": 3, 232 | "y": 2, 233 | "w": 9, 234 | "h": 14 235 | }, 236 | "frame": { 237 | "x": 11, 238 | "y": 74, 239 | "w": 9, 240 | "h": 14 241 | } 242 | }, 243 | { 244 | "filename": "walk_right_02", 245 | "rotated": false, 246 | "trimmed": true, 247 | "sourceSize": { 248 | "w": 16, 249 | "h": 16 250 | }, 251 | "spriteSourceSize": { 252 | "x": 4, 253 | "y": 2, 254 | "w": 9, 255 | "h": 14 256 | }, 257 | "frame": { 258 | "x": 11, 259 | "y": 88, 260 | "w": 9, 261 | "h": 14 262 | } 263 | } 264 | ] 265 | } 266 | ], 267 | "meta": { 268 | "app": "http://github.com/odrick/free-tex-packer-core", 269 | "version": "0.3.4" 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/assets/atlases/generated/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/generated/hero.png -------------------------------------------------------------------------------- /src/assets/atlases/generated/npc_01.json: -------------------------------------------------------------------------------- 1 | { 2 | "textures": [ 3 | { 4 | "image": "npc_01.png", 5 | "format": "RGBA8888", 6 | "size": { 7 | "w": 23, 8 | "h": 134 9 | }, 10 | "scale": 1, 11 | "frames": [ 12 | { 13 | "filename": "walk_down_01", 14 | "rotated": false, 15 | "trimmed": true, 16 | "sourceSize": { 17 | "w": 16, 18 | "h": 16 19 | }, 20 | "spriteSourceSize": { 21 | "x": 2, 22 | "y": 1, 23 | "w": 12, 24 | "h": 15 25 | }, 26 | "frame": { 27 | "x": 0, 28 | "y": 0, 29 | "w": 12, 30 | "h": 15 31 | } 32 | }, 33 | { 34 | "filename": "walk_down_03", 35 | "rotated": false, 36 | "trimmed": true, 37 | "sourceSize": { 38 | "w": 16, 39 | "h": 16 40 | }, 41 | "spriteSourceSize": { 42 | "x": 2, 43 | "y": 1, 44 | "w": 12, 45 | "h": 15 46 | }, 47 | "frame": { 48 | "x": 0, 49 | "y": 15, 50 | "w": 12, 51 | "h": 15 52 | } 53 | }, 54 | { 55 | "filename": "walk_up_01", 56 | "rotated": false, 57 | "trimmed": true, 58 | "sourceSize": { 59 | "w": 16, 60 | "h": 16 61 | }, 62 | "spriteSourceSize": { 63 | "x": 2, 64 | "y": 1, 65 | "w": 12, 66 | "h": 15 67 | }, 68 | "frame": { 69 | "x": 0, 70 | "y": 30, 71 | "w": 12, 72 | "h": 15 73 | } 74 | }, 75 | { 76 | "filename": "walk_up_03", 77 | "rotated": false, 78 | "trimmed": true, 79 | "sourceSize": { 80 | "w": 16, 81 | "h": 16 82 | }, 83 | "spriteSourceSize": { 84 | "x": 2, 85 | "y": 1, 86 | "w": 12, 87 | "h": 15 88 | }, 89 | "frame": { 90 | "x": 0, 91 | "y": 45, 92 | "w": 12, 93 | "h": 15 94 | } 95 | }, 96 | { 97 | "filename": "walk_left_01", 98 | "rotated": false, 99 | "trimmed": true, 100 | "sourceSize": { 101 | "w": 16, 102 | "h": 16 103 | }, 104 | "spriteSourceSize": { 105 | "x": 2, 106 | "y": 1, 107 | "w": 11, 108 | "h": 15 109 | }, 110 | "frame": { 111 | "x": 0, 112 | "y": 60, 113 | "w": 11, 114 | "h": 15 115 | } 116 | }, 117 | { 118 | "filename": "walk_left_03", 119 | "rotated": false, 120 | "trimmed": true, 121 | "sourceSize": { 122 | "w": 16, 123 | "h": 16 124 | }, 125 | "spriteSourceSize": { 126 | "x": 2, 127 | "y": 1, 128 | "w": 11, 129 | "h": 15 130 | }, 131 | "frame": { 132 | "x": 0, 133 | "y": 75, 134 | "w": 11, 135 | "h": 15 136 | } 137 | }, 138 | { 139 | "filename": "walk_right_01", 140 | "rotated": false, 141 | "trimmed": true, 142 | "sourceSize": { 143 | "w": 16, 144 | "h": 16 145 | }, 146 | "spriteSourceSize": { 147 | "x": 3, 148 | "y": 1, 149 | "w": 11, 150 | "h": 15 151 | }, 152 | "frame": { 153 | "x": 0, 154 | "y": 90, 155 | "w": 11, 156 | "h": 15 157 | } 158 | }, 159 | { 160 | "filename": "walk_right_03", 161 | "rotated": false, 162 | "trimmed": true, 163 | "sourceSize": { 164 | "w": 16, 165 | "h": 16 166 | }, 167 | "spriteSourceSize": { 168 | "x": 3, 169 | "y": 1, 170 | "w": 11, 171 | "h": 15 172 | }, 173 | "frame": { 174 | "x": 0, 175 | "y": 105, 176 | "w": 11, 177 | "h": 15 178 | } 179 | }, 180 | { 181 | "filename": "walk_down_02", 182 | "rotated": false, 183 | "trimmed": true, 184 | "sourceSize": { 185 | "w": 16, 186 | "h": 16 187 | }, 188 | "spriteSourceSize": { 189 | "x": 2, 190 | "y": 2, 191 | "w": 12, 192 | "h": 14 193 | }, 194 | "frame": { 195 | "x": 0, 196 | "y": 120, 197 | "w": 12, 198 | "h": 14 199 | } 200 | }, 201 | { 202 | "filename": "walk_up_02", 203 | "rotated": false, 204 | "trimmed": true, 205 | "sourceSize": { 206 | "w": 16, 207 | "h": 16 208 | }, 209 | "spriteSourceSize": { 210 | "x": 2, 211 | "y": 2, 212 | "w": 12, 213 | "h": 14 214 | }, 215 | "frame": { 216 | "x": 11, 217 | "y": 60, 218 | "w": 12, 219 | "h": 14 220 | } 221 | }, 222 | { 223 | "filename": "walk_left_02", 224 | "rotated": false, 225 | "trimmed": true, 226 | "sourceSize": { 227 | "w": 16, 228 | "h": 16 229 | }, 230 | "spriteSourceSize": { 231 | "x": 3, 232 | "y": 2, 233 | "w": 9, 234 | "h": 14 235 | }, 236 | "frame": { 237 | "x": 11, 238 | "y": 74, 239 | "w": 9, 240 | "h": 14 241 | } 242 | }, 243 | { 244 | "filename": "walk_right_02", 245 | "rotated": false, 246 | "trimmed": true, 247 | "sourceSize": { 248 | "w": 16, 249 | "h": 16 250 | }, 251 | "spriteSourceSize": { 252 | "x": 4, 253 | "y": 2, 254 | "w": 9, 255 | "h": 14 256 | }, 257 | "frame": { 258 | "x": 11, 259 | "y": 88, 260 | "w": 9, 261 | "h": 14 262 | } 263 | } 264 | ] 265 | } 266 | ], 267 | "meta": { 268 | "app": "http://github.com/odrick/free-tex-packer-core", 269 | "version": "0.3.4" 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/assets/atlases/generated/npc_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/generated/npc_01.png -------------------------------------------------------------------------------- /src/assets/atlases/generated/npc_02.json: -------------------------------------------------------------------------------- 1 | { 2 | "textures": [ 3 | { 4 | "image": "npc_02.png", 5 | "format": "RGBA8888", 6 | "size": { 7 | "w": 23, 8 | "h": 125 9 | }, 10 | "scale": 1, 11 | "frames": [ 12 | { 13 | "filename": "walk_down_01", 14 | "rotated": false, 15 | "trimmed": true, 16 | "sourceSize": { 17 | "w": 16, 18 | "h": 16 19 | }, 20 | "spriteSourceSize": { 21 | "x": 2, 22 | "y": 2, 23 | "w": 12, 24 | "h": 14 25 | }, 26 | "frame": { 27 | "x": 0, 28 | "y": 0, 29 | "w": 12, 30 | "h": 14 31 | } 32 | }, 33 | { 34 | "filename": "walk_down_03", 35 | "rotated": false, 36 | "trimmed": true, 37 | "sourceSize": { 38 | "w": 16, 39 | "h": 16 40 | }, 41 | "spriteSourceSize": { 42 | "x": 2, 43 | "y": 2, 44 | "w": 12, 45 | "h": 14 46 | }, 47 | "frame": { 48 | "x": 0, 49 | "y": 14, 50 | "w": 12, 51 | "h": 14 52 | } 53 | }, 54 | { 55 | "filename": "walk_up_01", 56 | "rotated": false, 57 | "trimmed": true, 58 | "sourceSize": { 59 | "w": 16, 60 | "h": 16 61 | }, 62 | "spriteSourceSize": { 63 | "x": 2, 64 | "y": 2, 65 | "w": 12, 66 | "h": 14 67 | }, 68 | "frame": { 69 | "x": 0, 70 | "y": 28, 71 | "w": 12, 72 | "h": 14 73 | } 74 | }, 75 | { 76 | "filename": "walk_up_03", 77 | "rotated": false, 78 | "trimmed": true, 79 | "sourceSize": { 80 | "w": 16, 81 | "h": 16 82 | }, 83 | "spriteSourceSize": { 84 | "x": 2, 85 | "y": 2, 86 | "w": 12, 87 | "h": 14 88 | }, 89 | "frame": { 90 | "x": 0, 91 | "y": 42, 92 | "w": 12, 93 | "h": 14 94 | } 95 | }, 96 | { 97 | "filename": "walk_left_01", 98 | "rotated": false, 99 | "trimmed": true, 100 | "sourceSize": { 101 | "w": 16, 102 | "h": 16 103 | }, 104 | "spriteSourceSize": { 105 | "x": 2, 106 | "y": 2, 107 | "w": 11, 108 | "h": 14 109 | }, 110 | "frame": { 111 | "x": 0, 112 | "y": 56, 113 | "w": 11, 114 | "h": 14 115 | } 116 | }, 117 | { 118 | "filename": "walk_left_03", 119 | "rotated": false, 120 | "trimmed": true, 121 | "sourceSize": { 122 | "w": 16, 123 | "h": 16 124 | }, 125 | "spriteSourceSize": { 126 | "x": 2, 127 | "y": 2, 128 | "w": 11, 129 | "h": 14 130 | }, 131 | "frame": { 132 | "x": 0, 133 | "y": 70, 134 | "w": 11, 135 | "h": 14 136 | } 137 | }, 138 | { 139 | "filename": "walk_right_01", 140 | "rotated": false, 141 | "trimmed": true, 142 | "sourceSize": { 143 | "w": 16, 144 | "h": 16 145 | }, 146 | "spriteSourceSize": { 147 | "x": 3, 148 | "y": 2, 149 | "w": 11, 150 | "h": 14 151 | }, 152 | "frame": { 153 | "x": 0, 154 | "y": 84, 155 | "w": 11, 156 | "h": 14 157 | } 158 | }, 159 | { 160 | "filename": "walk_right_03", 161 | "rotated": false, 162 | "trimmed": true, 163 | "sourceSize": { 164 | "w": 16, 165 | "h": 16 166 | }, 167 | "spriteSourceSize": { 168 | "x": 3, 169 | "y": 2, 170 | "w": 11, 171 | "h": 14 172 | }, 173 | "frame": { 174 | "x": 0, 175 | "y": 98, 176 | "w": 11, 177 | "h": 14 178 | } 179 | }, 180 | { 181 | "filename": "walk_down_02", 182 | "rotated": false, 183 | "trimmed": true, 184 | "sourceSize": { 185 | "w": 16, 186 | "h": 16 187 | }, 188 | "spriteSourceSize": { 189 | "x": 2, 190 | "y": 3, 191 | "w": 12, 192 | "h": 13 193 | }, 194 | "frame": { 195 | "x": 0, 196 | "y": 112, 197 | "w": 12, 198 | "h": 13 199 | } 200 | }, 201 | { 202 | "filename": "walk_up_02", 203 | "rotated": false, 204 | "trimmed": true, 205 | "sourceSize": { 206 | "w": 16, 207 | "h": 16 208 | }, 209 | "spriteSourceSize": { 210 | "x": 2, 211 | "y": 3, 212 | "w": 12, 213 | "h": 13 214 | }, 215 | "frame": { 216 | "x": 11, 217 | "y": 56, 218 | "w": 12, 219 | "h": 13 220 | } 221 | }, 222 | { 223 | "filename": "walk_left_02", 224 | "rotated": false, 225 | "trimmed": true, 226 | "sourceSize": { 227 | "w": 16, 228 | "h": 16 229 | }, 230 | "spriteSourceSize": { 231 | "x": 3, 232 | "y": 3, 233 | "w": 10, 234 | "h": 13 235 | }, 236 | "frame": { 237 | "x": 11, 238 | "y": 69, 239 | "w": 10, 240 | "h": 13 241 | } 242 | }, 243 | { 244 | "filename": "walk_right_02", 245 | "rotated": false, 246 | "trimmed": true, 247 | "sourceSize": { 248 | "w": 16, 249 | "h": 16 250 | }, 251 | "spriteSourceSize": { 252 | "x": 3, 253 | "y": 3, 254 | "w": 10, 255 | "h": 13 256 | }, 257 | "frame": { 258 | "x": 11, 259 | "y": 82, 260 | "w": 10, 261 | "h": 13 262 | } 263 | } 264 | ] 265 | } 266 | ], 267 | "meta": { 268 | "app": "http://github.com/odrick/free-tex-packer-core", 269 | "version": "0.3.4" 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/assets/atlases/generated/npc_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/generated/npc_02.png -------------------------------------------------------------------------------- /src/assets/atlases/generated/npc_03.json: -------------------------------------------------------------------------------- 1 | { 2 | "textures": [ 3 | { 4 | "image": "npc_03.png", 5 | "format": "RGBA8888", 6 | "size": { 7 | "w": 23, 8 | "h": 143 9 | }, 10 | "scale": 1, 11 | "frames": [ 12 | { 13 | "filename": "walk_down_01", 14 | "rotated": false, 15 | "trimmed": true, 16 | "sourceSize": { 17 | "w": 16, 18 | "h": 16 19 | }, 20 | "spriteSourceSize": { 21 | "x": 2, 22 | "y": 0, 23 | "w": 12, 24 | "h": 16 25 | }, 26 | "frame": { 27 | "x": 0, 28 | "y": 0, 29 | "w": 12, 30 | "h": 16 31 | } 32 | }, 33 | { 34 | "filename": "walk_down_03", 35 | "rotated": false, 36 | "trimmed": true, 37 | "sourceSize": { 38 | "w": 16, 39 | "h": 16 40 | }, 41 | "spriteSourceSize": { 42 | "x": 2, 43 | "y": 0, 44 | "w": 12, 45 | "h": 16 46 | }, 47 | "frame": { 48 | "x": 0, 49 | "y": 16, 50 | "w": 12, 51 | "h": 16 52 | } 53 | }, 54 | { 55 | "filename": "walk_up_01", 56 | "rotated": false, 57 | "trimmed": true, 58 | "sourceSize": { 59 | "w": 16, 60 | "h": 16 61 | }, 62 | "spriteSourceSize": { 63 | "x": 2, 64 | "y": 0, 65 | "w": 12, 66 | "h": 16 67 | }, 68 | "frame": { 69 | "x": 0, 70 | "y": 32, 71 | "w": 12, 72 | "h": 16 73 | } 74 | }, 75 | { 76 | "filename": "walk_up_03", 77 | "rotated": false, 78 | "trimmed": true, 79 | "sourceSize": { 80 | "w": 16, 81 | "h": 16 82 | }, 83 | "spriteSourceSize": { 84 | "x": 2, 85 | "y": 0, 86 | "w": 12, 87 | "h": 16 88 | }, 89 | "frame": { 90 | "x": 0, 91 | "y": 48, 92 | "w": 12, 93 | "h": 16 94 | } 95 | }, 96 | { 97 | "filename": "walk_left_01", 98 | "rotated": false, 99 | "trimmed": true, 100 | "sourceSize": { 101 | "w": 16, 102 | "h": 16 103 | }, 104 | "spriteSourceSize": { 105 | "x": 2, 106 | "y": 0, 107 | "w": 11, 108 | "h": 16 109 | }, 110 | "frame": { 111 | "x": 0, 112 | "y": 64, 113 | "w": 11, 114 | "h": 16 115 | } 116 | }, 117 | { 118 | "filename": "walk_left_03", 119 | "rotated": false, 120 | "trimmed": true, 121 | "sourceSize": { 122 | "w": 16, 123 | "h": 16 124 | }, 125 | "spriteSourceSize": { 126 | "x": 2, 127 | "y": 0, 128 | "w": 11, 129 | "h": 16 130 | }, 131 | "frame": { 132 | "x": 0, 133 | "y": 80, 134 | "w": 11, 135 | "h": 16 136 | } 137 | }, 138 | { 139 | "filename": "walk_right_01", 140 | "rotated": false, 141 | "trimmed": true, 142 | "sourceSize": { 143 | "w": 16, 144 | "h": 16 145 | }, 146 | "spriteSourceSize": { 147 | "x": 3, 148 | "y": 0, 149 | "w": 11, 150 | "h": 16 151 | }, 152 | "frame": { 153 | "x": 0, 154 | "y": 96, 155 | "w": 11, 156 | "h": 16 157 | } 158 | }, 159 | { 160 | "filename": "walk_right_03", 161 | "rotated": false, 162 | "trimmed": true, 163 | "sourceSize": { 164 | "w": 16, 165 | "h": 16 166 | }, 167 | "spriteSourceSize": { 168 | "x": 3, 169 | "y": 0, 170 | "w": 11, 171 | "h": 16 172 | }, 173 | "frame": { 174 | "x": 0, 175 | "y": 112, 176 | "w": 11, 177 | "h": 16 178 | } 179 | }, 180 | { 181 | "filename": "walk_down_02", 182 | "rotated": false, 183 | "trimmed": true, 184 | "sourceSize": { 185 | "w": 16, 186 | "h": 16 187 | }, 188 | "spriteSourceSize": { 189 | "x": 2, 190 | "y": 1, 191 | "w": 12, 192 | "h": 15 193 | }, 194 | "frame": { 195 | "x": 0, 196 | "y": 128, 197 | "w": 12, 198 | "h": 15 199 | } 200 | }, 201 | { 202 | "filename": "walk_up_02", 203 | "rotated": false, 204 | "trimmed": true, 205 | "sourceSize": { 206 | "w": 16, 207 | "h": 16 208 | }, 209 | "spriteSourceSize": { 210 | "x": 2, 211 | "y": 1, 212 | "w": 12, 213 | "h": 15 214 | }, 215 | "frame": { 216 | "x": 11, 217 | "y": 64, 218 | "w": 12, 219 | "h": 15 220 | } 221 | }, 222 | { 223 | "filename": "walk_left_02", 224 | "rotated": false, 225 | "trimmed": true, 226 | "sourceSize": { 227 | "w": 16, 228 | "h": 16 229 | }, 230 | "spriteSourceSize": { 231 | "x": 2, 232 | "y": 1, 233 | "w": 11, 234 | "h": 15 235 | }, 236 | "frame": { 237 | "x": 11, 238 | "y": 79, 239 | "w": 11, 240 | "h": 15 241 | } 242 | }, 243 | { 244 | "filename": "walk_right_02", 245 | "rotated": false, 246 | "trimmed": true, 247 | "sourceSize": { 248 | "w": 16, 249 | "h": 16 250 | }, 251 | "spriteSourceSize": { 252 | "x": 3, 253 | "y": 1, 254 | "w": 11, 255 | "h": 15 256 | }, 257 | "frame": { 258 | "x": 11, 259 | "y": 94, 260 | "w": 11, 261 | "h": 15 262 | } 263 | } 264 | ] 265 | } 266 | ], 267 | "meta": { 268 | "app": "http://github.com/odrick/free-tex-packer-core", 269 | "version": "0.3.4" 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/assets/atlases/generated/npc_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/generated/npc_03.png -------------------------------------------------------------------------------- /src/assets/atlases/generated/npc_04.json: -------------------------------------------------------------------------------- 1 | { 2 | "textures": [ 3 | { 4 | "image": "npc_04.png", 5 | "format": "RGBA8888", 6 | "size": { 7 | "w": 23, 8 | "h": 125 9 | }, 10 | "scale": 1, 11 | "frames": [ 12 | { 13 | "filename": "walk_down_01", 14 | "rotated": false, 15 | "trimmed": true, 16 | "sourceSize": { 17 | "w": 16, 18 | "h": 16 19 | }, 20 | "spriteSourceSize": { 21 | "x": 2, 22 | "y": 2, 23 | "w": 12, 24 | "h": 14 25 | }, 26 | "frame": { 27 | "x": 0, 28 | "y": 0, 29 | "w": 12, 30 | "h": 14 31 | } 32 | }, 33 | { 34 | "filename": "walk_down_03", 35 | "rotated": false, 36 | "trimmed": true, 37 | "sourceSize": { 38 | "w": 16, 39 | "h": 16 40 | }, 41 | "spriteSourceSize": { 42 | "x": 2, 43 | "y": 2, 44 | "w": 12, 45 | "h": 14 46 | }, 47 | "frame": { 48 | "x": 0, 49 | "y": 14, 50 | "w": 12, 51 | "h": 14 52 | } 53 | }, 54 | { 55 | "filename": "walk_up_01", 56 | "rotated": false, 57 | "trimmed": true, 58 | "sourceSize": { 59 | "w": 16, 60 | "h": 16 61 | }, 62 | "spriteSourceSize": { 63 | "x": 2, 64 | "y": 2, 65 | "w": 12, 66 | "h": 14 67 | }, 68 | "frame": { 69 | "x": 0, 70 | "y": 28, 71 | "w": 12, 72 | "h": 14 73 | } 74 | }, 75 | { 76 | "filename": "walk_up_03", 77 | "rotated": false, 78 | "trimmed": true, 79 | "sourceSize": { 80 | "w": 16, 81 | "h": 16 82 | }, 83 | "spriteSourceSize": { 84 | "x": 2, 85 | "y": 2, 86 | "w": 12, 87 | "h": 14 88 | }, 89 | "frame": { 90 | "x": 0, 91 | "y": 42, 92 | "w": 12, 93 | "h": 14 94 | } 95 | }, 96 | { 97 | "filename": "walk_left_01", 98 | "rotated": false, 99 | "trimmed": true, 100 | "sourceSize": { 101 | "w": 16, 102 | "h": 16 103 | }, 104 | "spriteSourceSize": { 105 | "x": 2, 106 | "y": 2, 107 | "w": 11, 108 | "h": 14 109 | }, 110 | "frame": { 111 | "x": 0, 112 | "y": 56, 113 | "w": 11, 114 | "h": 14 115 | } 116 | }, 117 | { 118 | "filename": "walk_left_03", 119 | "rotated": false, 120 | "trimmed": true, 121 | "sourceSize": { 122 | "w": 16, 123 | "h": 16 124 | }, 125 | "spriteSourceSize": { 126 | "x": 2, 127 | "y": 2, 128 | "w": 11, 129 | "h": 14 130 | }, 131 | "frame": { 132 | "x": 0, 133 | "y": 70, 134 | "w": 11, 135 | "h": 14 136 | } 137 | }, 138 | { 139 | "filename": "walk_right_01", 140 | "rotated": false, 141 | "trimmed": true, 142 | "sourceSize": { 143 | "w": 16, 144 | "h": 16 145 | }, 146 | "spriteSourceSize": { 147 | "x": 3, 148 | "y": 2, 149 | "w": 11, 150 | "h": 14 151 | }, 152 | "frame": { 153 | "x": 0, 154 | "y": 84, 155 | "w": 11, 156 | "h": 14 157 | } 158 | }, 159 | { 160 | "filename": "walk_right_03", 161 | "rotated": false, 162 | "trimmed": true, 163 | "sourceSize": { 164 | "w": 16, 165 | "h": 16 166 | }, 167 | "spriteSourceSize": { 168 | "x": 3, 169 | "y": 2, 170 | "w": 11, 171 | "h": 14 172 | }, 173 | "frame": { 174 | "x": 0, 175 | "y": 98, 176 | "w": 11, 177 | "h": 14 178 | } 179 | }, 180 | { 181 | "filename": "walk_down_02", 182 | "rotated": false, 183 | "trimmed": true, 184 | "sourceSize": { 185 | "w": 16, 186 | "h": 16 187 | }, 188 | "spriteSourceSize": { 189 | "x": 2, 190 | "y": 3, 191 | "w": 12, 192 | "h": 13 193 | }, 194 | "frame": { 195 | "x": 0, 196 | "y": 112, 197 | "w": 12, 198 | "h": 13 199 | } 200 | }, 201 | { 202 | "filename": "walk_up_02", 203 | "rotated": false, 204 | "trimmed": true, 205 | "sourceSize": { 206 | "w": 16, 207 | "h": 16 208 | }, 209 | "spriteSourceSize": { 210 | "x": 2, 211 | "y": 3, 212 | "w": 12, 213 | "h": 13 214 | }, 215 | "frame": { 216 | "x": 11, 217 | "y": 56, 218 | "w": 12, 219 | "h": 13 220 | } 221 | }, 222 | { 223 | "filename": "walk_left_02", 224 | "rotated": false, 225 | "trimmed": true, 226 | "sourceSize": { 227 | "w": 16, 228 | "h": 16 229 | }, 230 | "spriteSourceSize": { 231 | "x": 3, 232 | "y": 3, 233 | "w": 9, 234 | "h": 13 235 | }, 236 | "frame": { 237 | "x": 11, 238 | "y": 69, 239 | "w": 9, 240 | "h": 13 241 | } 242 | }, 243 | { 244 | "filename": "walk_right_02", 245 | "rotated": false, 246 | "trimmed": true, 247 | "sourceSize": { 248 | "w": 16, 249 | "h": 16 250 | }, 251 | "spriteSourceSize": { 252 | "x": 4, 253 | "y": 3, 254 | "w": 9, 255 | "h": 13 256 | }, 257 | "frame": { 258 | "x": 11, 259 | "y": 82, 260 | "w": 9, 261 | "h": 13 262 | } 263 | } 264 | ] 265 | } 266 | ], 267 | "meta": { 268 | "app": "http://github.com/odrick/free-tex-packer-core", 269 | "version": "0.3.4" 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/assets/atlases/generated/npc_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/generated/npc_04.png -------------------------------------------------------------------------------- /src/assets/atlases/generated/npc_05.json: -------------------------------------------------------------------------------- 1 | { 2 | "textures": [ 3 | { 4 | "image": "npc_05.png", 5 | "format": "RGBA8888", 6 | "size": { 7 | "w": 23, 8 | "h": 134 9 | }, 10 | "scale": 1, 11 | "frames": [ 12 | { 13 | "filename": "walk_down_01", 14 | "rotated": false, 15 | "trimmed": true, 16 | "sourceSize": { 17 | "w": 16, 18 | "h": 16 19 | }, 20 | "spriteSourceSize": { 21 | "x": 2, 22 | "y": 1, 23 | "w": 12, 24 | "h": 15 25 | }, 26 | "frame": { 27 | "x": 0, 28 | "y": 0, 29 | "w": 12, 30 | "h": 15 31 | } 32 | }, 33 | { 34 | "filename": "walk_down_03", 35 | "rotated": false, 36 | "trimmed": true, 37 | "sourceSize": { 38 | "w": 16, 39 | "h": 16 40 | }, 41 | "spriteSourceSize": { 42 | "x": 2, 43 | "y": 1, 44 | "w": 12, 45 | "h": 15 46 | }, 47 | "frame": { 48 | "x": 0, 49 | "y": 15, 50 | "w": 12, 51 | "h": 15 52 | } 53 | }, 54 | { 55 | "filename": "walk_up_01", 56 | "rotated": false, 57 | "trimmed": true, 58 | "sourceSize": { 59 | "w": 16, 60 | "h": 16 61 | }, 62 | "spriteSourceSize": { 63 | "x": 2, 64 | "y": 1, 65 | "w": 12, 66 | "h": 15 67 | }, 68 | "frame": { 69 | "x": 0, 70 | "y": 30, 71 | "w": 12, 72 | "h": 15 73 | } 74 | }, 75 | { 76 | "filename": "walk_up_03", 77 | "rotated": false, 78 | "trimmed": true, 79 | "sourceSize": { 80 | "w": 16, 81 | "h": 16 82 | }, 83 | "spriteSourceSize": { 84 | "x": 2, 85 | "y": 1, 86 | "w": 12, 87 | "h": 15 88 | }, 89 | "frame": { 90 | "x": 0, 91 | "y": 45, 92 | "w": 12, 93 | "h": 15 94 | } 95 | }, 96 | { 97 | "filename": "walk_left_01", 98 | "rotated": false, 99 | "trimmed": true, 100 | "sourceSize": { 101 | "w": 16, 102 | "h": 16 103 | }, 104 | "spriteSourceSize": { 105 | "x": 2, 106 | "y": 1, 107 | "w": 11, 108 | "h": 15 109 | }, 110 | "frame": { 111 | "x": 0, 112 | "y": 60, 113 | "w": 11, 114 | "h": 15 115 | } 116 | }, 117 | { 118 | "filename": "walk_left_03", 119 | "rotated": false, 120 | "trimmed": true, 121 | "sourceSize": { 122 | "w": 16, 123 | "h": 16 124 | }, 125 | "spriteSourceSize": { 126 | "x": 2, 127 | "y": 1, 128 | "w": 11, 129 | "h": 15 130 | }, 131 | "frame": { 132 | "x": 0, 133 | "y": 75, 134 | "w": 11, 135 | "h": 15 136 | } 137 | }, 138 | { 139 | "filename": "walk_right_01", 140 | "rotated": false, 141 | "trimmed": true, 142 | "sourceSize": { 143 | "w": 16, 144 | "h": 16 145 | }, 146 | "spriteSourceSize": { 147 | "x": 3, 148 | "y": 1, 149 | "w": 11, 150 | "h": 15 151 | }, 152 | "frame": { 153 | "x": 0, 154 | "y": 90, 155 | "w": 11, 156 | "h": 15 157 | } 158 | }, 159 | { 160 | "filename": "walk_right_03", 161 | "rotated": false, 162 | "trimmed": true, 163 | "sourceSize": { 164 | "w": 16, 165 | "h": 16 166 | }, 167 | "spriteSourceSize": { 168 | "x": 3, 169 | "y": 1, 170 | "w": 11, 171 | "h": 15 172 | }, 173 | "frame": { 174 | "x": 0, 175 | "y": 105, 176 | "w": 11, 177 | "h": 15 178 | } 179 | }, 180 | { 181 | "filename": "walk_down_02", 182 | "rotated": false, 183 | "trimmed": true, 184 | "sourceSize": { 185 | "w": 16, 186 | "h": 16 187 | }, 188 | "spriteSourceSize": { 189 | "x": 2, 190 | "y": 2, 191 | "w": 12, 192 | "h": 14 193 | }, 194 | "frame": { 195 | "x": 0, 196 | "y": 120, 197 | "w": 12, 198 | "h": 14 199 | } 200 | }, 201 | { 202 | "filename": "walk_up_02", 203 | "rotated": false, 204 | "trimmed": true, 205 | "sourceSize": { 206 | "w": 16, 207 | "h": 16 208 | }, 209 | "spriteSourceSize": { 210 | "x": 2, 211 | "y": 2, 212 | "w": 12, 213 | "h": 14 214 | }, 215 | "frame": { 216 | "x": 11, 217 | "y": 60, 218 | "w": 12, 219 | "h": 14 220 | } 221 | }, 222 | { 223 | "filename": "walk_left_02", 224 | "rotated": false, 225 | "trimmed": true, 226 | "sourceSize": { 227 | "w": 16, 228 | "h": 16 229 | }, 230 | "spriteSourceSize": { 231 | "x": 3, 232 | "y": 2, 233 | "w": 10, 234 | "h": 14 235 | }, 236 | "frame": { 237 | "x": 11, 238 | "y": 74, 239 | "w": 10, 240 | "h": 14 241 | } 242 | }, 243 | { 244 | "filename": "walk_right_02", 245 | "rotated": false, 246 | "trimmed": true, 247 | "sourceSize": { 248 | "w": 16, 249 | "h": 16 250 | }, 251 | "spriteSourceSize": { 252 | "x": 3, 253 | "y": 2, 254 | "w": 10, 255 | "h": 14 256 | }, 257 | "frame": { 258 | "x": 11, 259 | "y": 88, 260 | "w": 10, 261 | "h": 14 262 | } 263 | } 264 | ] 265 | } 266 | ], 267 | "meta": { 268 | "app": "http://github.com/odrick/free-tex-packer-core", 269 | "version": "0.3.4" 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/assets/atlases/generated/npc_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/generated/npc_05.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/coin/coin_idle_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/coin/coin_idle_01.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/coin/coin_idle_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/coin/coin_idle_02.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/enemy/walk_down_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/enemy/walk_down_01.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/enemy/walk_down_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/enemy/walk_down_02.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/enemy/walk_down_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/enemy/walk_down_03.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/enemy/walk_left_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/enemy/walk_left_01.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/enemy/walk_left_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/enemy/walk_left_02.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/enemy/walk_left_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/enemy/walk_left_03.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/enemy/walk_right_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/enemy/walk_right_01.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/enemy/walk_right_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/enemy/walk_right_02.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/enemy/walk_right_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/enemy/walk_right_03.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/enemy/walk_up_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/enemy/walk_up_01.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/enemy/walk_up_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/enemy/walk_up_02.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/enemy/walk_up_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/enemy/walk_up_03.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/hero/walk_down_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/hero/walk_down_01.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/hero/walk_down_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/hero/walk_down_02.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/hero/walk_down_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/hero/walk_down_03.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/hero/walk_left_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/hero/walk_left_01.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/hero/walk_left_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/hero/walk_left_02.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/hero/walk_left_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/hero/walk_left_03.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/hero/walk_right_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/hero/walk_right_01.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/hero/walk_right_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/hero/walk_right_02.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/hero/walk_right_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/hero/walk_right_03.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/hero/walk_up_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/hero/walk_up_01.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/hero/walk_up_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/hero/walk_up_02.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/hero/walk_up_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/hero/walk_up_03.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_01/walk_down_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_01/walk_down_01.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_01/walk_down_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_01/walk_down_02.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_01/walk_down_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_01/walk_down_03.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_01/walk_left_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_01/walk_left_01.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_01/walk_left_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_01/walk_left_02.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_01/walk_left_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_01/walk_left_03.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_01/walk_right_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_01/walk_right_01.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_01/walk_right_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_01/walk_right_02.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_01/walk_right_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_01/walk_right_03.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_01/walk_up_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_01/walk_up_01.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_01/walk_up_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_01/walk_up_02.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_01/walk_up_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_01/walk_up_03.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_02/walk_down_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_02/walk_down_01.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_02/walk_down_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_02/walk_down_02.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_02/walk_down_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_02/walk_down_03.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_02/walk_left_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_02/walk_left_01.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_02/walk_left_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_02/walk_left_02.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_02/walk_left_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_02/walk_left_03.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_02/walk_right_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_02/walk_right_01.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_02/walk_right_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_02/walk_right_02.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_02/walk_right_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_02/walk_right_03.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_02/walk_up_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_02/walk_up_01.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_02/walk_up_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_02/walk_up_02.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_02/walk_up_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_02/walk_up_03.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_03/walk_down_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_03/walk_down_01.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_03/walk_down_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_03/walk_down_02.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_03/walk_down_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_03/walk_down_03.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_03/walk_left_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_03/walk_left_01.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_03/walk_left_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_03/walk_left_02.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_03/walk_left_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_03/walk_left_03.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_03/walk_right_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_03/walk_right_01.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_03/walk_right_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_03/walk_right_02.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_03/walk_right_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_03/walk_right_03.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_03/walk_up_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_03/walk_up_01.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_03/walk_up_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_03/walk_up_02.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_03/walk_up_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_03/walk_up_03.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_04/walk_down_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_04/walk_down_01.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_04/walk_down_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_04/walk_down_02.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_04/walk_down_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_04/walk_down_03.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_04/walk_left_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_04/walk_left_01.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_04/walk_left_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_04/walk_left_02.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_04/walk_left_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_04/walk_left_03.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_04/walk_right_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_04/walk_right_01.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_04/walk_right_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_04/walk_right_02.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_04/walk_right_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_04/walk_right_03.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_04/walk_up_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_04/walk_up_01.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_04/walk_up_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_04/walk_up_02.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_04/walk_up_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_04/walk_up_03.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_05/walk_down_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_05/walk_down_01.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_05/walk_down_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_05/walk_down_02.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_05/walk_down_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_05/walk_down_03.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_05/walk_left_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_05/walk_left_01.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_05/walk_left_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_05/walk_left_02.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_05/walk_left_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_05/walk_left_03.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_05/walk_right_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_05/walk_right_01.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_05/walk_right_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_05/walk_right_02.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_05/walk_right_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_05/walk_right_03.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_05/walk_up_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_05/walk_up_01.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_05/walk_up_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_05/walk_up_02.png -------------------------------------------------------------------------------- /src/assets/atlases/sources/npc_05/walk_up_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/atlases/sources/npc_05/walk_up_03.png -------------------------------------------------------------------------------- /src/assets/fonts/Munro-Narrow.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/fonts/Munro-Narrow.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Munro-Small.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/fonts/Munro-Small.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Munro.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/fonts/Munro.ttf -------------------------------------------------------------------------------- /src/assets/fonts/PressStart2P-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/fonts/PressStart2P-Regular.ttf -------------------------------------------------------------------------------- /src/assets/fonts/press-start-medium-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/fonts/press-start-medium-white.png -------------------------------------------------------------------------------- /src/assets/fonts/press-start-medium-white.xml: -------------------------------------------------------------------------------- 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 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /src/assets/fonts/press-start-normal-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/fonts/press-start-normal-white.png -------------------------------------------------------------------------------- /src/assets/fonts/press-start-small-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/fonts/press-start-small-white.png -------------------------------------------------------------------------------- /src/assets/fonts/press-start-small-white.xml: -------------------------------------------------------------------------------- 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 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /src/assets/images/a_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/images/a_button.png -------------------------------------------------------------------------------- /src/assets/images/b_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/images/b_button.png -------------------------------------------------------------------------------- /src/assets/images/background_desert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/images/background_desert.png -------------------------------------------------------------------------------- /src/assets/images/background_fall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/images/background_fall.png -------------------------------------------------------------------------------- /src/assets/images/background_forest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/images/background_forest.png -------------------------------------------------------------------------------- /src/assets/images/background_grass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/images/background_grass.png -------------------------------------------------------------------------------- /src/assets/images/background_winter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/images/background_winter.png -------------------------------------------------------------------------------- /src/assets/images/crystal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/images/crystal.png -------------------------------------------------------------------------------- /src/assets/images/d_pad_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/images/d_pad_button.png -------------------------------------------------------------------------------- /src/assets/images/enemy_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/images/enemy_01.png -------------------------------------------------------------------------------- /src/assets/images/enemy_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/images/enemy_02.png -------------------------------------------------------------------------------- /src/assets/images/enemy_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/images/enemy_03.png -------------------------------------------------------------------------------- /src/assets/images/heart_empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/images/heart_empty.png -------------------------------------------------------------------------------- /src/assets/images/heart_full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/images/heart_full.png -------------------------------------------------------------------------------- /src/assets/images/heart_half.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/images/heart_half.png -------------------------------------------------------------------------------- /src/assets/images/key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/images/key.png -------------------------------------------------------------------------------- /src/assets/tilesets/city.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/tilesets/city.png -------------------------------------------------------------------------------- /src/assets/tilesets/objects.json: -------------------------------------------------------------------------------- 1 | { 2 | "columns": 10, 3 | "image": "objects.png", 4 | "imageheight": 80, 5 | "imagewidth": 160, 6 | "margin": 0, 7 | "name": "objects", 8 | "spacing": 0, 9 | "tilecount": 50, 10 | "tiledversion": "1.8.2", 11 | "tileheight": 16, 12 | "tilewidth": 16, 13 | "type": "tileset", 14 | "version": "1.6" 15 | } 16 | -------------------------------------------------------------------------------- /src/assets/tilesets/objects.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/tilesets/objects.png -------------------------------------------------------------------------------- /src/assets/tilesets/village.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blopa/top-down-react-phaser-game-template/2fd10e2edcb974ca72128cc493033dbae16a5795/src/assets/tilesets/village.png -------------------------------------------------------------------------------- /src/components/DialogBox/DialogBox.jsx: -------------------------------------------------------------------------------- 1 | // Components 2 | import MessageBox from '../MessageBox'; 3 | 4 | // Styles 5 | import styles from './DialogBox.module.scss'; 6 | 7 | function DialogBox({ show = false }) { 8 | return ( 9 | 16 | ); 17 | } 18 | 19 | export default DialogBox; 20 | -------------------------------------------------------------------------------- /src/components/DialogBox/DialogBox.module.scss: -------------------------------------------------------------------------------- 1 | .dialog-window { 2 | image-rendering: pixelated; 3 | font-family: "Press Start 2P", serif; 4 | text-transform: uppercase; 5 | background-color: #e2b27e; 6 | border: solid; 7 | position: absolute; 8 | bottom: 5%; 9 | width: 80%; 10 | left: 50%; 11 | transform: translate(-50%, 0%); 12 | padding: calc(var(--game-zoom) * 8px); 13 | min-height: calc((var(--game-height) / 4) * var(--game-zoom) * 1px); 14 | max-height: calc((var(--game-height) / 3) * var(--game-zoom) * 1px); 15 | } 16 | 17 | .dialog-title { 18 | font-weight: bold; 19 | font-size: calc(var(--game-zoom) * 8px); 20 | margin-bottom: calc(var(--game-zoom) * 6px); 21 | } 22 | 23 | .dialog-footer { 24 | cursor: pointer; 25 | text-align: end; 26 | position: absolute; 27 | right: calc(var(--game-zoom) * 6px); 28 | bottom: calc(var(--game-zoom) * 6px); 29 | font-size: calc(var(--game-zoom) * 8px); 30 | } -------------------------------------------------------------------------------- /src/components/GameMenu/GameMenu.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import classNames from 'classnames'; 3 | import { FormattedMessage } from 'react-intl'; 4 | 5 | // Store 6 | import { useGameStore } from '../../zustand/store'; 7 | 8 | // Constants 9 | import { ARROW_DOWN_KEY, ARROW_UP_KEY, ENTER_KEY } from '../../constants'; 10 | 11 | // Selectors 12 | import { 13 | selectMenuItems, 14 | selectMenuOnSelect, 15 | selectMenuPosition, 16 | } from '../../zustand/menu/selectMenu'; 17 | 18 | // Utils 19 | import { getTranslationVariables } from '../../utils/utils'; 20 | 21 | // Styles 22 | import styles from './GameMenu.module.scss'; 23 | 24 | function GameMenu() { 25 | // Menu 26 | const position = useGameStore(selectMenuPosition); 27 | const items = useGameStore(selectMenuItems); 28 | const onSelected = useGameStore(selectMenuOnSelect); 29 | 30 | const [selectedItemIndex, setSelectedItemIndex] = useState(0); 31 | 32 | useEffect(() => { 33 | const handleKeyPressed = (e) => { 34 | switch (e.code) { 35 | case ENTER_KEY: { 36 | onSelected(items[selectedItemIndex]); 37 | break; 38 | } 39 | 40 | case ARROW_UP_KEY: { 41 | if (selectedItemIndex > 0) { 42 | setSelectedItemIndex( 43 | selectedItemIndex - 1 44 | ); 45 | } 46 | 47 | break; 48 | } 49 | 50 | case ARROW_DOWN_KEY: { 51 | if (items.length - 1 > selectedItemIndex) { 52 | setSelectedItemIndex( 53 | selectedItemIndex + 1 54 | ); 55 | } 56 | 57 | break; 58 | } 59 | 60 | default: { 61 | break; 62 | } 63 | } 64 | }; 65 | window.addEventListener('keydown', handleKeyPressed); 66 | 67 | return () => window.removeEventListener('keydown', handleKeyPressed); 68 | }, [items, onSelected, selectedItemIndex]); 69 | 70 | return ( 71 |
78 | 104 |
105 | ); 106 | } 107 | 108 | export default GameMenu; 109 | -------------------------------------------------------------------------------- /src/components/GameMenu/GameMenu.module.scss: -------------------------------------------------------------------------------- 1 | .menu-wrapper { 2 | font-family: "Press Start 2P", serif; 3 | font-size: calc(var(--game-zoom) * 10px); 4 | text-transform: uppercase; 5 | position: absolute; 6 | transform: translate(-50%, 0%); 7 | 8 | &.position-center { 9 | min-width: calc(var(--game-zoom) * 160px); 10 | left: 50%; 11 | top: calc((var(--game-height) * var(--game-zoom)) / 2 * 1px); 12 | } 13 | 14 | &.position-left { 15 | //left: calc(((var(--game-zoom) * 95) + ((100vw - (var(--game-width) * var(--game-zoom))) / 2)) * 1px); 16 | } 17 | 18 | &.position-right { 19 | // TODO 20 | } 21 | } 22 | 23 | .menu-items-wrapper { 24 | text-align: center; 25 | padding: 0; 26 | } 27 | 28 | .menu-item { 29 | cursor: pointer; 30 | list-style: none; 31 | background-color: #94785c; 32 | padding: calc(var(--game-zoom) * 5px); 33 | margin-bottom: calc(var(--game-zoom) * 5px); 34 | border: calc(var(--game-zoom) * 1px) solid #79584f; 35 | } 36 | 37 | .selected-menu-item { 38 | font-size: calc(var(--game-zoom) * 11px); 39 | border: calc(var(--game-zoom) * 1px) solid #ddd; 40 | } -------------------------------------------------------------------------------- /src/components/GameText/GameText.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useRef } from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | import classNames from 'classnames'; 4 | 5 | // Selectors 6 | import { 7 | selectGameZoom, 8 | selectGameCanvasElement, 9 | } from '../../zustand/game/selectGameData'; 10 | 11 | // Hooks 12 | import useRect from '../../hooks/useRect'; 13 | 14 | // Store 15 | import { useGameStore } from '../../zustand/store'; 16 | 17 | // Styles 18 | import styles from './GameText.module.scss'; 19 | 20 | function GameText({ 21 | translationKey, 22 | variables = {}, 23 | config = {}, 24 | component: Component = 'p', 25 | }) { 26 | // Game 27 | const gameZoom = useGameStore(selectGameZoom); 28 | const canvas = useGameStore(selectGameCanvasElement); 29 | const domRect = useRect(canvas); 30 | const textWrapperRef = useRef(null); 31 | 32 | const { 33 | color = '#FFFFFF', 34 | position = 'center', 35 | top = 0, 36 | size = 10, 37 | } = config; 38 | 39 | const textTop = useMemo( 40 | () => ((domRect?.top || 0) + (top * gameZoom)), 41 | [domRect?.top, gameZoom, top] 42 | ); 43 | 44 | const textWidth = useMemo( 45 | () => (((domRect?.left || 0) * 2) + (gameZoom * 10)), 46 | [domRect?.left, gameZoom] 47 | ); 48 | 49 | useEffect(() => { 50 | if (textWrapperRef.current) { 51 | textWrapperRef.current.style.setProperty('--game-text-align', position); 52 | textWrapperRef.current.style.setProperty('--game-text-color', color); 53 | textWrapperRef.current.style.setProperty('--game-text-size', size); 54 | textWrapperRef.current.style.setProperty('--game-text-top', textTop); 55 | textWrapperRef.current.style.setProperty('--game-text-width', textWidth); 56 | } 57 | }, [ 58 | textWidth, 59 | position, 60 | textTop, 61 | color, 62 | size, 63 | ]); 64 | 65 | return ( 66 |
72 | 73 | 77 | 78 |
79 | ); 80 | } 81 | 82 | export default GameText; 83 | -------------------------------------------------------------------------------- /src/components/GameText/GameText.module.scss: -------------------------------------------------------------------------------- 1 | .text-wrapper { 2 | user-select: none; 3 | user-drag: none; 4 | position: absolute; 5 | text-align: var(--game-text-align); 6 | transform: translate(-50%, 0%); 7 | top: calc(var(--game-text-top) * 1px); 8 | width: calc(100% - (var(--game-text-width) * 1px)); 9 | } 10 | 11 | .text-position-wrapper { 12 | &.text-center { 13 | left: 50%; 14 | } 15 | } 16 | 17 | .text { 18 | font-size: calc(var(--game-zoom) * var(--game-text-size) * 1px); 19 | font-family: "Press Start 2P", serif; 20 | color: var(--game-text-color); 21 | text-transform: uppercase; 22 | margin: 0; 23 | } -------------------------------------------------------------------------------- /src/components/Message/Message.jsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { animated, useTransition } from '@react-spring/web'; 3 | 4 | // Styles 5 | import styles from './Message.module.scss'; 6 | 7 | function Message({ 8 | message = '', 9 | trail = 35, 10 | onMessageEnded = () => {}, 11 | forceShowFullMessage = false, 12 | }) { 13 | const items = useMemo( 14 | () => [...message.trim()].map((letter, index) => ({ 15 | item: letter, 16 | key: index, 17 | })), 18 | [message] 19 | ); 20 | 21 | const transitions = useTransition(items, { 22 | trail, 23 | from: { display: 'none' }, 24 | enter: { display: '' }, 25 | onRest: (status, controller, item) => { 26 | if (item.key === items.length - 1) { 27 | onMessageEnded(); 28 | } 29 | }, 30 | }); 31 | 32 | return ( 33 |
34 | {forceShowFullMessage && ( 35 | {message} 36 | )} 37 | 38 | {!forceShowFullMessage && transitions((animatedStyles, { item, key }) => ( 39 | 40 | {item} 41 | 42 | ))} 43 |
44 | ); 45 | } 46 | 47 | export default Message; 48 | -------------------------------------------------------------------------------- /src/components/Message/Message.module.scss: -------------------------------------------------------------------------------- 1 | .dialog-message { 2 | text-transform: uppercase; 3 | font-family: "Press Start 2P", serif; 4 | font-size: calc(var(--game-zoom) * 6px); 5 | line-height: calc(var(--game-zoom) * 10px); 6 | } 7 | -------------------------------------------------------------------------------- /src/components/MessageBox.jsx: -------------------------------------------------------------------------------- 1 | import { animated, useSpring } from '@react-spring/web'; 2 | import { useCallback, useEffect, useState } from 'react'; 3 | import { useIntl } from 'react-intl'; 4 | 5 | // Components 6 | import Message from './Message/Message'; 7 | 8 | // Selectors 9 | import { 10 | selectDialogAction, 11 | selectDialogMessages, 12 | selectDialogCharacterName, 13 | } from '../zustand/dialog/selectDialog'; 14 | import { selectGameHeight, selectGameZoom } from '../zustand/game/selectGameData'; 15 | 16 | // Constants 17 | import { ENTER_KEY, ESCAPE_KEY, SPACE_KEY } from '../constants'; 18 | 19 | // Store 20 | import { useGameStore } from '../zustand/store'; 21 | 22 | function MessageBox({ 23 | showNext = false, 24 | dialogWindowClassname = null, 25 | dialogTitleClassname = null, 26 | dialogFooterClassname = null, 27 | show = false, 28 | }) { 29 | const intl = useIntl(); 30 | const gameZoom = useGameStore(selectGameZoom); 31 | const gameHeight = useGameStore(selectGameHeight); 32 | const dialogAction = useGameStore(selectDialogAction); 33 | const dialogMessages = useGameStore(selectDialogMessages); 34 | const characterName = useGameStore(selectDialogCharacterName); 35 | 36 | const [currentMessage, setCurrentMessage] = useState(0); 37 | const [messageEnded, setMessageEnded] = useState(false); 38 | const [forceShowFullMessage, setForceShowFullMessage] = useState(false); 39 | const [shouldShowMessage, setShouldShowMessage] = useState(false); 40 | const dialogDone = currentMessage === dialogMessages.length - 1 && messageEnded; 41 | 42 | const springOnRestCallback = useCallback(({ finished }) => { 43 | setShouldShowMessage(finished && show); 44 | }, [show]); 45 | 46 | const animatedStyles = useSpring({ 47 | config: { duration: 250 }, 48 | from: { transform: `translate(-50%, ${gameHeight * gameZoom}px)` }, 49 | to: { transform: show ? 'translate(-50%, 0%)' : `translate(-50%, ${gameHeight * gameZoom}px)` }, 50 | onRest: springOnRestCallback, 51 | }); 52 | 53 | useEffect(() => { 54 | if (!show) { 55 | setCurrentMessage(0); 56 | setMessageEnded(false); 57 | setShouldShowMessage(false); 58 | setForceShowFullMessage(false); 59 | } 60 | }, [show]); 61 | 62 | const handleClick = useCallback(() => { 63 | if (!shouldShowMessage) { 64 | return; 65 | } 66 | 67 | if (messageEnded) { 68 | setMessageEnded(false); 69 | setForceShowFullMessage(false); 70 | if (currentMessage < dialogMessages.length - 1) { 71 | setCurrentMessage(currentMessage + 1); 72 | } else { 73 | setCurrentMessage(0); 74 | dialogAction?.(); 75 | } 76 | } else { 77 | setMessageEnded(true); 78 | setForceShowFullMessage(true); 79 | } 80 | }, [shouldShowMessage, messageEnded, currentMessage, dialogMessages.length, dialogAction]); 81 | 82 | useEffect(() => { 83 | const handleKeyPressed = (e) => { 84 | if ([ENTER_KEY, SPACE_KEY, ESCAPE_KEY].includes(e.code)) { 85 | handleClick(); 86 | } 87 | }; 88 | window.addEventListener('keydown', handleKeyPressed); 89 | 90 | return () => window.removeEventListener('keydown', handleKeyPressed); 91 | }, [dialogMessages.length, handleClick]); 92 | 93 | return ( 94 | 95 |
96 | {characterName} 97 |
98 | {shouldShowMessage && ( 99 | { 104 | setMessageEnded(true); 105 | }} 106 | /> 107 | )} 108 | {showNext && ( 109 |
113 | {dialogDone ? intl.formatMessage({ id: 'ok' }) : intl.formatMessage({ id: 'next' })} 114 |
115 | )} 116 |
117 | ); 118 | } 119 | 120 | export default MessageBox; 121 | -------------------------------------------------------------------------------- /src/components/ReactWrapper.jsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo, useState } from 'react'; 2 | import { useResizeObserver } from 'beautiful-react-hooks'; 3 | 4 | // Store 5 | import { useGameStore } from '../zustand/store'; 6 | 7 | // Constants 8 | import { OVERLAY_DIV_RESIZE_THRESHOLD } from '../constants'; 9 | 10 | // Hooks 11 | import useMutationObserver from '../hooks/useMutationObserver'; 12 | 13 | // Components 14 | import DialogBox from './DialogBox/DialogBox'; 15 | import GameMenu from './GameMenu/GameMenu'; 16 | import GameText from './GameText/GameText'; 17 | 18 | // Selectors 19 | import { selectGameCanvasElement } from '../zustand/game/selectGameData'; 20 | import { selectDialogMessages } from '../zustand/dialog/selectDialog'; 21 | import { selectMenuItems } from '../zustand/menu/selectMenu'; 22 | import { selectTexts } from '../zustand/text/selectText'; 23 | 24 | function ReactWrapper() { 25 | const canvas = useGameStore(selectGameCanvasElement); 26 | const dialogMessages = useGameStore(selectDialogMessages); 27 | const menuItems = useGameStore(selectMenuItems); 28 | const gameTexts = useGameStore(selectTexts); 29 | // const s = useGameStore((store) => store); 30 | // console.log(s); 31 | const ref = useMemo(() => ({ current: canvas }), [canvas]); 32 | const DOMRect = useResizeObserver(ref, OVERLAY_DIV_RESIZE_THRESHOLD); 33 | 34 | const [mutatedStyles, setMutatedStyles] = useState({}); 35 | const defaultStyles = useMemo(() => ({ 36 | // backgroundColor: '#fff', 37 | position: 'absolute', 38 | overflow: 'hidden', 39 | ...DOMRect, 40 | }), [DOMRect]); 41 | 42 | const mutationObserverCallback = useCallback((mutations) => { 43 | const { target } = mutations.at(0); 44 | 45 | setMutatedStyles({ 46 | marginLeft: target?.style?.marginLeft, 47 | marginTop: target?.style?.marginTop, 48 | }); 49 | }, []); 50 | 51 | useMutationObserver(ref, mutationObserverCallback); 52 | 53 | const inlineStyles = useMemo(() => ({ 54 | marginLeft: canvas?.style?.marginLeft, 55 | marginTop: canvas?.style?.marginTop, 56 | ...defaultStyles, 57 | ...mutatedStyles, 58 | }), [canvas?.style?.marginLeft, canvas?.style?.marginTop, defaultStyles, mutatedStyles]); 59 | 60 | // const handleWrapperClicked = useCallback((event) => { 61 | // const { clientX, clientY } = event; 62 | // 63 | // canvas.dispatchEvent(new Event('click', { 64 | // clientX, 65 | // clientY, 66 | // })); 67 | // }, [canvas]); 68 | 69 | // TODO maybe this is not needed anymore 70 | // console.log(defaultStyles, mutatedStyles); 71 | // console.log(mutatedStyles); 72 | 73 | return ( 74 |
79 | 0} /> 80 | {menuItems.length > 0 && ( 81 | 82 | )} 83 | {gameTexts.length > 0 && gameTexts.map((text) => { 84 | const { key, variables, config } = text; 85 | 86 | return ( 87 | 93 | ); 94 | })} 95 |
96 | ); 97 | } 98 | 99 | export default ReactWrapper; 100 | -------------------------------------------------------------------------------- /src/components/VirtualGamepad/VirtualGamepad.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unresolved, import/no-webpack-loader-syntax */ 2 | import { useCallback, useEffect, useRef } from 'react'; 3 | import { Geom } from 'phaser'; 4 | import classNames from 'classnames'; 5 | 6 | // Images 7 | import dPadButton from '../../assets/images/d_pad_button.png'; 8 | import aButton from '../../assets/images/a_button.png'; 9 | import bButton from '../../assets/images/b_button.png'; 10 | 11 | // Utils 12 | import { simulateKeyEvent } from '../../utils/utils'; 13 | 14 | // Constants 15 | import { 16 | ENTER_KEY, 17 | SPACE_KEY, 18 | ARROW_UP_KEY, 19 | ARROW_DOWN_KEY, 20 | ARROW_LEFT_KEY, 21 | ARROW_RIGHT_KEY, 22 | } from '../../constants'; 23 | 24 | // Styles 25 | import styles from './VirtualGamepad.module.scss'; 26 | 27 | function VirtualGamepad() { 28 | // TODO redo this with that answer from stackoverflow 29 | // https://stackoverflow.com/a/70192263/4307769 30 | 31 | const dPadLeftRef = useRef(null); 32 | const dPadRightRef = useRef(null); 33 | const dPadUpRef = useRef(null); 34 | const dPadDownRef = useRef(null); 35 | const aButtonRef = useRef(null); 36 | const bButtonRef = useRef(null); 37 | const eventRef = useRef({}); 38 | 39 | const wasAButtonClicked = useCallback((x, y) => { 40 | const { width, x: elX, y: elY } = aButtonRef.current.getBoundingClientRect(); 41 | const radius = width / 2; 42 | const circle = new Geom.Circle( 43 | // this is needed because Circles have origin set to 0.5 44 | elX + radius, 45 | elY + radius, 46 | radius 47 | ); 48 | 49 | return circle.contains(x, y); 50 | }, [aButtonRef]); 51 | 52 | const wasBButtonClicked = useCallback((x, y) => { 53 | const { width, x: elX, y: elY } = bButtonRef.current.getBoundingClientRect(); 54 | const radius = width / 2; 55 | const circle = new Geom.Circle( 56 | // this is needed because Circles have origin set to 0.5 57 | elX + radius, 58 | elY + radius, 59 | radius 60 | ); 61 | 62 | return circle.contains(x, y); 63 | }, [bButtonRef]); 64 | 65 | const wasLeftButtonClicked = useCallback((x, y) => { 66 | const { left, right, top, bottom, height } = dPadLeftRef.current.getBoundingClientRect(); 67 | const polygon = new Geom.Polygon([ 68 | { x: left, y: top }, 69 | { x: right - 31, y: top }, 70 | { x: right, y: top + (height / 2) }, 71 | { x: right - 31, y: bottom }, 72 | { x: left, y: bottom }, 73 | ]); 74 | 75 | return polygon.contains(x, y); 76 | }, [dPadLeftRef]); 77 | 78 | const wasUpButtonClicked = useCallback((x, y) => { 79 | const { left, right, top, bottom, width } = dPadUpRef.current.getBoundingClientRect(); 80 | const polygon = new Geom.Polygon([ 81 | { x: left, y: top }, 82 | { x: right, y: top }, 83 | { x: right, y: bottom - 31 }, 84 | { x: left + (width / 2), y: bottom }, 85 | { x: left, y: bottom - 31 }, 86 | ]); 87 | 88 | return polygon.contains(x, y); 89 | }, [dPadUpRef]); 90 | 91 | const wasRightButtonClicked = useCallback((x, y) => { 92 | const { left, right, top, bottom, height } = dPadRightRef.current.getBoundingClientRect(); 93 | const polygon = new Geom.Polygon([ 94 | { x: left, y: top + (height / 2) }, 95 | { x: left + 31, y: top }, 96 | { x: right, y: top }, 97 | { x: right, y: bottom }, 98 | { x: left + 31, y: bottom }, 99 | ]); 100 | 101 | return polygon.contains(x, y); 102 | }, [dPadRightRef]); 103 | 104 | const wasDownButtonClicked = useCallback((x, y) => { 105 | const { left, right, top, bottom, width } = dPadDownRef.current.getBoundingClientRect(); 106 | const polygon = new Geom.Polygon([ 107 | { x: left + (width / 2), y: top }, 108 | { x: right, y: top + 31 }, 109 | { x: right, y: bottom }, 110 | { x: left, y: bottom }, 111 | { x: left, y: top + 31 }, 112 | ]); 113 | 114 | return polygon.contains(x, y); 115 | }, [dPadDownRef]); 116 | 117 | const getPressedButton = useCallback((x, y) => { 118 | if (wasLeftButtonClicked(x, y)) { 119 | return [ARROW_LEFT_KEY, dPadLeftRef]; 120 | } 121 | 122 | if (wasRightButtonClicked(x, y)) { 123 | return [ARROW_RIGHT_KEY, dPadRightRef]; 124 | } 125 | 126 | if (wasUpButtonClicked(x, y)) { 127 | return [ARROW_UP_KEY, dPadUpRef]; 128 | } 129 | 130 | if (wasDownButtonClicked(x, y)) { 131 | return [ARROW_DOWN_KEY, dPadDownRef]; 132 | } 133 | 134 | if (wasAButtonClicked(x, y)) { 135 | return [SPACE_KEY, aButtonRef]; 136 | } 137 | 138 | if (wasBButtonClicked(x, y)) { 139 | return [ENTER_KEY, bButtonRef]; 140 | } 141 | 142 | return []; 143 | }, [ 144 | wasAButtonClicked, 145 | wasBButtonClicked, 146 | wasUpButtonClicked, 147 | wasDownButtonClicked, 148 | wasLeftButtonClicked, 149 | wasRightButtonClicked, 150 | ]); 151 | 152 | const handleButtonPressed = useCallback((event, type) => { 153 | const { x, y } = event; 154 | 155 | const [pressedButton, element] = getPressedButton(x, y); 156 | if (pressedButton && element) { 157 | simulateKeyEvent(pressedButton, type); 158 | if (type === 'down') { 159 | element.current.classList.add(styles['is-touched']); 160 | } else { 161 | element.current.classList.remove(styles['is-touched']); 162 | } 163 | } 164 | }, [getPressedButton]); 165 | 166 | useEffect(() => { 167 | const handlePointerDown = (event) => { 168 | eventRef.current = event; 169 | handleButtonPressed(event, 'down'); 170 | }; 171 | document.addEventListener('pointerdown', handlePointerDown); 172 | 173 | const handlePointerUp = (event) => { 174 | handleButtonPressed(eventRef.current, 'up'); 175 | eventRef.current = {}; 176 | }; 177 | document.addEventListener('pointerup', handlePointerUp); 178 | 179 | return () => { 180 | window.removeEventListener('pointerdown', handlePointerDown); 181 | window.removeEventListener('pointerup', handlePointerUp); 182 | }; 183 | }, [handleButtonPressed]); 184 | 185 | const handleContextMenuCallback = useCallback((event) => { 186 | event.preventDefault(); 187 | event.stopPropagation(); 188 | return false; 189 | }, []); 190 | 191 | return ( 192 |
193 |
194 | test 201 | test 208 | test 215 | test 222 |
223 |
224 | test 231 | test 238 |
239 |
240 | ); 241 | } 242 | 243 | export default VirtualGamepad; 244 | -------------------------------------------------------------------------------- /src/components/VirtualGamepad/VirtualGamepad.module.scss: -------------------------------------------------------------------------------- 1 | .buttons-wrapper { 2 | margin-top: calc(((var(--game-height) * var(--game-zoom)) - (var(--game-zoom) * 100)) * 1px); 3 | padding: 0 calc(var(--game-zoom) * 15px); 4 | justify-content: space-between; 5 | position: relative; 6 | user-select: none; 7 | user-drag: none; 8 | display: flex; 9 | z-index: 10; 10 | } 11 | 12 | .button { 13 | pointer-events: none; 14 | -webkit-touch-callout: none; 15 | user-select: none; 16 | user-drag: none; 17 | &.is-touched { 18 | filter: saturate(300%) brightness(70%); 19 | } 20 | } 21 | 22 | .a-button { 23 | width: calc(var(--game-zoom) * 40px); 24 | height: calc(var(--game-zoom) * 40px); 25 | } 26 | 27 | .b-button { 28 | width: calc(var(--game-zoom) * 40px); 29 | height: calc(var(--game-zoom) * 40px); 30 | } 31 | 32 | .d-pad-wrapper { 33 | width: calc(var(--game-zoom) * 76px); 34 | height: calc(var(--game-zoom) * 76px); 35 | } 36 | 37 | .d-pad-left { 38 | width: calc(var(--game-zoom) * 38px); 39 | height: calc(var(--game-zoom) * 30.5px); 40 | margin-bottom: calc(var(--game-zoom) * -19px); 41 | } 42 | 43 | .d-pad-right { 44 | transform: rotate(180deg); 45 | width: calc(var(--game-zoom) * 38px); 46 | height: calc(var(--game-zoom) * 30.5px); 47 | margin-bottom: calc(var(--game-zoom) * -19px); 48 | } 49 | 50 | .d-pad-up { 51 | transform: rotate(90deg); 52 | width: calc(var(--game-zoom) * 38px); 53 | height: calc(var(--game-zoom) * 30.5px); 54 | margin-left: calc(var(--game-zoom) * -57px); 55 | } 56 | 57 | .d-pad-down { 58 | transform: rotate(270deg); 59 | width: calc(var(--game-zoom) * 38px); 60 | height: calc(var(--game-zoom) * 30.5px); 61 | margin-left: calc(var(--game-zoom) * -38px); 62 | margin-bottom: calc(var(--game-zoom) * -38px); 63 | } 64 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const TILE_WIDTH = 16; 2 | export const TILE_HEIGHT = 16; 3 | 4 | export const MIN_GAME_WIDTH = 25 * TILE_WIDTH; // 400 5 | export const MIN_GAME_HEIGHT = 14 * TILE_HEIGHT; // 224 6 | 7 | export const RESIZE_THRESHOLD = 500; 8 | export const RE_RESIZE_THRESHOLD = 10; 9 | export const OVERLAY_DIV_RESIZE_THRESHOLD = RE_RESIZE_THRESHOLD; 10 | 11 | export const HERO_SPRITE_NAME = 'hero'; 12 | export const ENEMY_SPRITE_NAME = 'enemy'; 13 | export const COIN_SPRITE_NAME = 'coin'; 14 | export const HEART_SPRITE_NAME = 'heart'; 15 | export const CRYSTAL_SPRITE_NAME = 'crystal'; 16 | export const KEY_SPRITE_NAME = 'key'; 17 | 18 | // Game Objects Tiled IDs 19 | export const ENEMY = 1; 20 | export const COIN = 2; 21 | export const HEART = 3; 22 | export const CRYSTAL = 4; 23 | export const KEY = 5; 24 | export const DOOR = 6; 25 | 26 | export const IDLE_FRAME = 'walk_position_02'; 27 | export const IDLE_FRAME_POSITION_KEY = 'position'; 28 | 29 | // Directions 30 | export const RIGHT_DIRECTION = 'right'; 31 | export const LEFT_DIRECTION = 'left'; 32 | export const UP_DIRECTION = 'up'; 33 | export const DOWN_DIRECTION = 'down'; 34 | 35 | export const IGNORED_TILESETS = ['objects']; 36 | 37 | // Keys 38 | export const ENTER_KEY = 'Enter'; 39 | export const SPACE_KEY = 'Space'; 40 | export const ESCAPE_KEY = 'Escape'; 41 | export const ARROW_LEFT_KEY = 'ArrowLeft'; 42 | export const ARROW_UP_KEY = 'ArrowUp'; 43 | export const ARROW_RIGHT_KEY = 'ArrowRight'; 44 | export const ARROW_DOWN_KEY = 'ArrowDown'; 45 | 46 | // DOM identifiers 47 | export const GAME_CONTENT_ID = 'game-content'; 48 | 49 | export const BOOT_SCENE_NAME = 'BootScene'; 50 | 51 | export const DEFAULT_LOCALE = 'en'; 52 | -------------------------------------------------------------------------------- /src/game/scenes/BootScene.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { Scene } from 'phaser'; 3 | 4 | // Utils 5 | import { changeScene } from '../../utils/sceneHelpers'; 6 | 7 | // TODO move this somewhere else 8 | import backgroundDesert from '../../assets/images/background_desert.png'; 9 | import backgroundFall from '../../assets/images/background_fall.png'; 10 | import backgroundForest from '../../assets/images/background_forest.png'; 11 | import backgroundGrass from '../../assets/images/background_grass.png'; 12 | import backgroundWinter from '../../assets/images/background_winter.png'; 13 | import enemy01 from '../../assets/images/enemy_01.png'; 14 | import enemy02 from '../../assets/images/enemy_02.png'; 15 | import enemy03 from '../../assets/images/enemy_03.png'; 16 | 17 | export default class BootScene extends Scene { 18 | constructor() { 19 | super('BootScene'); 20 | } 21 | 22 | preload() { 23 | // Preload assets for the splash and title screens 24 | this.load.image('background_desert', backgroundDesert); 25 | this.load.image('background_fall', backgroundFall); 26 | this.load.image('background_forest', backgroundForest); 27 | this.load.image('background_grass', backgroundGrass); 28 | this.load.image('background_winter', backgroundWinter); 29 | 30 | this.load.image('enemy_01', enemy01); 31 | this.load.image('enemy_02', enemy02); 32 | this.load.image('enemy_03', enemy03); 33 | } 34 | 35 | create() { 36 | changeScene(this, 'MainMenuScene', { 37 | fonts: ['"Press Start 2P"'], 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/game/scenes/GameScene.js: -------------------------------------------------------------------------------- 1 | // Utils 2 | import { 3 | handleCreateMap, 4 | handleCreateHero, 5 | handleObjectsLayer, 6 | handleHeroMovement, 7 | handleCreateGroups, 8 | handleCreateControls, 9 | handleConfigureCamera, 10 | handleCreateHeroAnimations, 11 | } from '../../utils/sceneHelpers'; 12 | import { getSelectorData } from '../../utils/utils'; 13 | 14 | // Selectors 15 | import { selectGameSetters } from '../../zustand/game/selectGameData'; 16 | 17 | export const key = 'GameScene'; 18 | 19 | export const scene = {}; 20 | 21 | export function create() { 22 | // scene.input.on('pointerup', (pointer) => { 23 | // console.log('clicky click'); 24 | // }); 25 | const { addGameCameraSizeUpdateCallback } = getSelectorData(selectGameSetters); 26 | 27 | // All of these functions need to be called in order 28 | 29 | // Create controls 30 | handleCreateControls(scene); 31 | 32 | // Create game groups 33 | handleCreateGroups(scene); 34 | 35 | // Create the map 36 | const customColliders = handleCreateMap(scene); 37 | 38 | // Create hero sprite 39 | handleCreateHero(scene); 40 | 41 | // Load game objects like items, enemies, etc 42 | handleObjectsLayer(scene); 43 | 44 | // Configure the main camera 45 | handleConfigureCamera(scene); 46 | addGameCameraSizeUpdateCallback(() => { 47 | handleConfigureCamera(scene); 48 | }); 49 | 50 | // Hero animations 51 | handleCreateHeroAnimations(scene); 52 | 53 | // Handle collisions 54 | scene.physics.add.collider(scene.heroSprite, scene.enemies); 55 | scene.physics.add.collider(scene.heroSprite, customColliders); 56 | } 57 | 58 | export function update(time, delta) { 59 | handleHeroMovement(scene); 60 | scene.heroSprite.update(time, delta); 61 | } 62 | -------------------------------------------------------------------------------- /src/game/scenes/MainMenuScene.js: -------------------------------------------------------------------------------- 1 | // Constants 2 | import { DOWN_DIRECTION, IDLE_FRAME, IDLE_FRAME_POSITION_KEY } from '../../constants'; 3 | 4 | // Utils 5 | import { changeScene } from '../../utils/sceneHelpers'; 6 | import { getSelectorData } from '../../utils/utils'; 7 | 8 | // Selectors 9 | import { selectHeroSetters } from '../../zustand/hero/selectHeroData'; 10 | import { selectMapSetters } from '../../zustand/map/selectMapData'; 11 | import { selectMenuSetters } from '../../zustand/menu/selectMenu'; 12 | 13 | export const scene = {}; 14 | 15 | export const key = 'MainMenuScene'; 16 | 17 | export function create() { 18 | const { setMapKey } = getSelectorData(selectMapSetters); 19 | const { setMenuItems, setMenuOnSelect } = getSelectorData(selectMenuSetters); 20 | 21 | setMenuItems(['start_game', 'exit']); 22 | setMenuOnSelect((key, item) => { 23 | if (key === 'start_game') { 24 | handleStartGameSelected(); 25 | } else { 26 | setMenuItems([]); 27 | setMenuOnSelect(null); 28 | window.location.reload(); 29 | } 30 | }); 31 | 32 | const handleStartGameSelected = () => { 33 | setMenuItems([]); 34 | setMenuOnSelect(null); 35 | setMapKey('sample_map'); 36 | const { 37 | setHeroPreviousPosition, 38 | setHeroFacingDirection, 39 | setHeroInitialPosition, 40 | setHeroInitialFrame, 41 | } = getSelectorData(selectHeroSetters); 42 | 43 | setHeroFacingDirection(DOWN_DIRECTION); 44 | setHeroInitialFrame( 45 | IDLE_FRAME.replace(IDLE_FRAME_POSITION_KEY, DOWN_DIRECTION) 46 | ); 47 | setHeroInitialPosition({ x: 30, y: 42 }); 48 | setHeroPreviousPosition({ x: 30, y: 42 }); 49 | 50 | changeScene(scene, 'GameScene', { 51 | // fonts: ['"Press Start 2P"'], 52 | atlases: ['hero'], 53 | images: [], 54 | mapKey: 'sample_map', 55 | // mapKey: 'sample_indoor', 56 | }); 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /src/hooks/useMutationObserver.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo } from 'react'; 2 | 3 | const useMutationObserver = ( 4 | ref, 5 | callback, 6 | options = null 7 | ) => { 8 | const observerOptions = useMemo(() => { 9 | if (options) { 10 | return options; 11 | } 12 | 13 | return { 14 | // characterDataOldValue: true, 15 | // attributeOldValue: true, 16 | // attributeFilter: true, 17 | // characterData: true, 18 | attributes: true, 19 | childList: true, 20 | subtree: true, 21 | }; 22 | }, [options]); 23 | 24 | useEffect(() => { 25 | if (ref.current) { 26 | const observer = new MutationObserver(callback); 27 | observer.observe(ref.current, observerOptions); 28 | return () => observer.disconnect(); 29 | } 30 | 31 | return () => {}; 32 | }, [callback, observerOptions, ref]); 33 | }; 34 | 35 | export default useMutationObserver; 36 | -------------------------------------------------------------------------------- /src/hooks/useRect.jsx: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect, useCallback, useState } from 'react'; 2 | 3 | // code by https://gist.github.com/morajabi/523d7a642d8c0a2f71fcfa0d8b3d2846 4 | const useRect = (element) => { 5 | const [rect, setRect] = useState(getRect(element || null)); 6 | 7 | const handleResize = useCallback(() => { 8 | if (!element) { 9 | return; 10 | } 11 | 12 | // Update client rect 13 | setRect(getRect(element)); 14 | }, [element]); 15 | 16 | useLayoutEffect(() => { 17 | if (!element) { 18 | return () => {}; 19 | } 20 | 21 | handleResize(); 22 | let resizeObserver = new ResizeObserver(() => handleResize()); 23 | resizeObserver.observe(element); 24 | 25 | let intersectionObserver = new IntersectionObserver(() => handleResize()); 26 | intersectionObserver.observe(element); 27 | 28 | return () => { 29 | if (!resizeObserver) { 30 | return; 31 | } 32 | 33 | intersectionObserver.disconnect(); 34 | resizeObserver.disconnect(); 35 | resizeObserver = null; 36 | intersectionObserver = null; 37 | }; 38 | }, [handleResize, element]); 39 | 40 | return rect; 41 | }; 42 | 43 | function getRect(element) { 44 | if (!element) { 45 | return { 46 | bottom: 0, 47 | height: 0, 48 | left: 0, 49 | right: 0, 50 | top: 0, 51 | width: 0, 52 | }; 53 | } 54 | 55 | return element.getBoundingClientRect(); 56 | } 57 | 58 | export default useRect; 59 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Press Start 2P'; 3 | src: url('assets/fonts/PressStart2P-Regular.ttf') format('opentype'), 4 | url('assets/fonts/PressStart2P-Regular.ttf') format('opentype'); 5 | font-weight: normal; 6 | font-style: normal; 7 | } 8 | 9 | body { 10 | margin: 0; 11 | background-color: #2d528b; 12 | } 13 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-filename-extension */ 2 | import ReactDOM from 'react-dom/client'; 3 | import { ThemeProvider, createTheme } from '@mui/material/styles'; 4 | import { Helmet } from 'react-helmet'; 5 | import { Fragment } from 'react'; 6 | 7 | // Styles 8 | import './index.css'; 9 | 10 | // Components 11 | import Game from './Game'; 12 | 13 | const theme = createTheme(); 14 | const root = ReactDOM.createRoot(document.getElementById('root')); 15 | 16 | root.render( 17 | 18 | 19 | My Phaser Game 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | 27 | -------------------------------------------------------------------------------- /src/intl/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "game_title": "Game Title", 3 | "start_game": "Start Game", 4 | "exit": "Exit", 5 | "rock": "Rock", 6 | "paper": "Paper", 7 | "scissors": "Scissors", 8 | "attack": "Attack", 9 | "defense": "Defense", 10 | "items": "Items", 11 | "next": "Next", 12 | "ok": "Ok", 13 | "return": "Return", 14 | "dice_1": "Dice 1", 15 | "dice_2": "Dice 2", 16 | "dice_3": "Dice 3", 17 | "run": "Run" 18 | } 19 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/utils/__tests__/phaser.test.js: -------------------------------------------------------------------------------- 1 | const { 2 | calculateGameSize, 3 | asyncLoader, 4 | prepareScene, 5 | getScenesModules, 6 | } = require('../phaser'); 7 | 8 | describe('calculateGameSize', () => { 9 | beforeEach(() => { 10 | window.innerWidth = 500; 11 | window.innerHeight = 500; 12 | }); 13 | 14 | it('calculates the game size correctly', () => { 15 | const result = calculateGameSize(400, 300, 10, 20); 16 | expect(result.zoom).toEqual(1); 17 | expect(result.width).toEqual(400); 18 | expect(result.height).toEqual(300); 19 | }); 20 | 21 | it('calculates the zoom correctly', () => { 22 | const result = calculateGameSize(1000, 1000, 10, 20); 23 | expect(result.zoom).toEqual(2); 24 | expect(result.width).toEqual(500); 25 | expect(result.height).toEqual(500); 26 | }); 27 | 28 | it('uses the threshold values', () => { 29 | const result = calculateGameSize(400, 300, 10, 20, 0.1, 0.2); 30 | expect(result.width).toEqual(440); 31 | expect(result.height).toEqual(360); 32 | }); 33 | }); 34 | 35 | describe('asyncLoader', () => { 36 | it('resolves with the result', (done) => { 37 | const plugin = { 38 | on: jest.fn((event, callback) => { 39 | if (event === 'filecomplete') { 40 | // eslint-disable-next-line n/no-callback-literal 41 | callback({ name: 'test', type: 'image' }); 42 | } 43 | }), 44 | start: jest.fn(), 45 | }; 46 | asyncLoader(plugin) 47 | .then((result) => { 48 | expect(result).toEqual({ name: 'test', type: 'image' }); 49 | done(); 50 | }); 51 | }); 52 | 53 | it('rejects with an error', (done) => { 54 | const plugin = { 55 | on: jest.fn((event, callback) => { 56 | if (event === 'loaderror') { 57 | callback(new Error('Load error')); 58 | } 59 | }), 60 | start: jest.fn(), 61 | }; 62 | asyncLoader(plugin) 63 | .catch((error) => { 64 | expect(error).toEqual(new Error('Load error')); 65 | done(); 66 | }); 67 | }); 68 | }); 69 | 70 | describe('prepareScene', () => { 71 | it('should return the default property of a module if it has a prototype', () => { 72 | const defaultExport = () => {}; 73 | const module = { default: defaultExport }; 74 | const result = prepareScene(module, './testModule.js'); 75 | 76 | expect(result).toBe(defaultExport); 77 | }); 78 | 79 | it('should return an object with init method and key/name properties if the module does not have a prototype', () => { 80 | const module = {}; 81 | const result = prepareScene(module, './testModule.js'); 82 | 83 | expect(result.init).toBeDefined(); 84 | expect(result.key).toBeDefined(); 85 | expect(result.name).toBeDefined(); 86 | }); 87 | 88 | it('should use the module key property as the key/name if it is defined', () => { 89 | const key = 'testKey'; 90 | const module = { key }; 91 | const result = prepareScene(module, './testModule.js'); 92 | 93 | expect(result.key).toBe(key); 94 | expect(result.name).toBe(key); 95 | }); 96 | 97 | it('should use the filename without extension as the key/name if the module key property is not defined', () => { 98 | const module = {}; 99 | const result = prepareScene(module, './testModule.js'); 100 | 101 | expect(result.key).toBe('testModule'); 102 | expect(result.name).toBe('testModule'); 103 | }); 104 | }); 105 | 106 | describe('getScenesModules', () => { 107 | jest.mock('../phaser.js', () => ({ 108 | prepareScene: jest.fn(() => ({})), 109 | })); 110 | 111 | it('should return an array of scenes modules', () => { 112 | require.context = jest.fn(() => ({ 113 | keys: jest.fn(() => ['./bootScene.js', './testScene1.js', './testScene2.js']), 114 | })); 115 | 116 | const result = getScenesModules(); 117 | 118 | expect(result).toHaveLength(3); 119 | }); 120 | 121 | it('should call prepareScene for each scene module', () => { 122 | const contextResolver = { 123 | keys: jest.fn(() => [ 124 | './bootScene.js', 125 | './testScene1.js', 126 | './testScene2.js', 127 | ]), 128 | }; 129 | 130 | require.context = jest.fn(() => contextResolver); 131 | getScenesModules(); 132 | 133 | expect(prepareScene).toHaveBeenCalledWith(contextResolver['./bootScene.js'], './bootScene.js'); 134 | expect(prepareScene).toHaveBeenCalledWith(contextResolver['./testScene1.js'], './testScene1.js'); 135 | expect(prepareScene).toHaveBeenCalledWith(contextResolver['./testScene2.js'], './testScene2.js'); 136 | }); 137 | 138 | it('should return the boot scene as the first element of the array', () => { 139 | const contextResolver = { 140 | keys: jest.fn(() => [ 141 | './bootScene.js', 142 | './testScene1.js', 143 | './testScene2.js', 144 | ]), 145 | }; 146 | 147 | require.context = jest.fn(() => contextResolver); 148 | 149 | const result = getScenesModules(); 150 | 151 | expect(result[0]).toBeDefined(); 152 | expect(prepareScene).toHaveBeenCalledWith(contextResolver['./bootScene.js'], './bootScene.js'); 153 | }); 154 | }); 155 | -------------------------------------------------------------------------------- /src/utils/phaser.js: -------------------------------------------------------------------------------- 1 | import { getFileNameWithoutExtension, isObject } from './utils'; 2 | 3 | // Constants 4 | import { BOOT_SCENE_NAME } from '../constants'; 5 | 6 | export const calculateGameSize = ( 7 | width, 8 | height, 9 | tileWidth, 10 | tileHeight, 11 | widthThreshold = 0.5, 12 | heightThreshold = 0.5 13 | ) => { 14 | const widthScale = Math.floor(window.innerWidth / width); 15 | const heightScale = Math.floor(window.innerHeight / height); 16 | const zoom = Math.min(widthScale, heightScale) || 1; 17 | 18 | const newWidth = Math.floor(window.innerWidth / tileWidth) * tileWidth / zoom; 19 | const newHeight = Math.floor(window.innerHeight / tileHeight) * tileHeight / zoom; 20 | 21 | return { 22 | zoom, 23 | width: Math.min(newWidth, Math.floor((width * (1 + widthThreshold)) / tileWidth) * tileWidth), 24 | height: Math.min(newHeight, Math.floor((height * (1 + heightThreshold)) / tileHeight) * tileHeight), 25 | }; 26 | }; 27 | 28 | // Thanks yannick @ https://phaser.discourse.group/t/loading-audio/1306/4 29 | export const asyncLoader = (loaderPlugin) => new Promise((resolve, reject) => { 30 | loaderPlugin.on('filecomplete', resolve).on('loaderror', reject); 31 | loaderPlugin.start(); 32 | }); 33 | 34 | export const prepareScene = (module, modulePath) => { 35 | if (Object.getOwnPropertyDescriptor(module.default || {}, 'prototype')) { 36 | return module.default; 37 | } 38 | 39 | function init(data) { 40 | // eslint-disable-next-line no-undefined 41 | if (isObject(module.scene)) { 42 | // when this function is called, "this" will be the scene 43 | // eslint-disable-next-line @babel/no-invalid-this 44 | Object.entries(this).forEach(([key, value]) => { 45 | // eslint-disable-next-line no-param-reassign 46 | module.scene[key] = value; 47 | }); 48 | } 49 | 50 | module.init?.(data); 51 | } 52 | 53 | // Object.keys(Scene.prototype).forEach((key) => { 54 | // if (!module[key]) { 55 | // throw new Error(`Missing "${key}" method`); 56 | // } 57 | // }); 58 | 59 | const key = module.key || getFileNameWithoutExtension(modulePath); 60 | return { 61 | ...module, 62 | name: key, 63 | key, 64 | init, 65 | }; 66 | }; 67 | 68 | export const getScenesModules = () => { 69 | // automatically import all scenes from the scenes folder 70 | const contextResolver = require.context('../game/scenes/', true, /\.(js|ts)$/); 71 | 72 | const modulePaths = contextResolver.keys(); 73 | const bootScene = modulePaths 74 | .find((modulePath) => getFileNameWithoutExtension(modulePath) === BOOT_SCENE_NAME); 75 | const otherScenes = modulePaths 76 | .filter((modulePath) => getFileNameWithoutExtension(modulePath) !== BOOT_SCENE_NAME); 77 | 78 | return [bootScene, ...otherScenes] 79 | .map((modulePath) => prepareScene(contextResolver(modulePath), modulePath)); 80 | }; 81 | -------------------------------------------------------------------------------- /src/utils/utils.js: -------------------------------------------------------------------------------- 1 | import { GameObjects } from 'phaser'; 2 | import { 3 | ARROW_RIGHT_KEY, 4 | ARROW_DOWN_KEY, 5 | ARROW_LEFT_KEY, 6 | ARROW_UP_KEY, 7 | TILE_HEIGHT, 8 | TILE_WIDTH, 9 | ENTER_KEY, 10 | SPACE_KEY, 11 | } from '../constants'; 12 | 13 | // Store 14 | import { getState } from '../zustand/store'; 15 | 16 | export const isObject = (obj) => 17 | typeof obj === 'object' && obj?.constructor === Object; 18 | 19 | export const isObjectEmpty = (obj) => 20 | isObject(obj) && Object.keys(obj).length === 0; 21 | 22 | export const isObjectNotEmpty = (obj) => 23 | isObject(obj) && Object.keys(obj).length > 0; 24 | 25 | /** 26 | * source https://gist.github.com/GlauberF/d8278ce3aa592389e6e3d4e758e6a0c2 27 | * Simulate a key event. 28 | * @param {String} code The code of the key to simulate 29 | * @param {String} type (optional) The type of event : down, up or press. The default is down 30 | */ 31 | export const simulateKeyEvent = (code, type = 'down') => { 32 | const keysMap = { 33 | [ENTER_KEY]: 13, 34 | [SPACE_KEY]: 32, 35 | [ARROW_LEFT_KEY]: 37, 36 | [ARROW_UP_KEY]: 38, 37 | [ARROW_RIGHT_KEY]: 39, 38 | [ARROW_DOWN_KEY]: 40, 39 | }; 40 | 41 | const event = document.createEvent('HTMLEvents'); 42 | event.initEvent(`key${type}`, true, false); 43 | event.code = code; 44 | event.keyCode = keysMap[code]; 45 | 46 | document.dispatchEvent(event); 47 | }; 48 | 49 | export const getTranslationVariables = (item) => { 50 | if (isObject(item)) { 51 | return [item.key, item.variables]; 52 | } 53 | 54 | return [item, {}]; 55 | }; 56 | 57 | export const getSelectorData = (selector) => selector(getState()); 58 | 59 | export const createInteractiveGameObject = ( 60 | scene, 61 | x, 62 | y, 63 | width, 64 | height, 65 | origin = { x: 0, y: 0 } 66 | ) => { 67 | const customCollider = new GameObjects.Rectangle( 68 | scene, 69 | x, 70 | y, 71 | width, 72 | height 73 | ).setOrigin(origin.x, origin.y); 74 | 75 | scene.physics.add.existing(customCollider); 76 | customCollider.body.setImmovable(true); 77 | 78 | return customCollider; 79 | }; 80 | 81 | // Functions to check if a file exists within Webpack modules 82 | // This might look dumb, but due to the way Webpack works, this is the only way to properly check 83 | // Using a full path as a variable doesn't work because: https://github.com/webpack/webpack/issues/6680#issuecomment-370800037 84 | export const isMapFileAvailable = (file) => { 85 | try { 86 | require.resolveWeak(`../assets/maps/${file}`); 87 | return true; 88 | } catch { 89 | console.error(`Error loading file ${file}`); 90 | return false; 91 | } 92 | }; 93 | 94 | export const isImageFileAvailable = (file) => { 95 | try { 96 | require.resolveWeak(`../assets/images/${file}`); 97 | return true; 98 | } catch { 99 | console.error(`Error loading file ${file}`); 100 | return false; 101 | } 102 | }; 103 | 104 | export const isTilesetFileAvailable = (file) => { 105 | try { 106 | require.resolveWeak(`../assets/tilesets/${file}`); 107 | return true; 108 | } catch { 109 | console.error(`Error loading file ${file}`); 110 | return false; 111 | } 112 | }; 113 | 114 | export const isGeneratedAtlasFileAvailable = (file) => { 115 | try { 116 | require.resolveWeak(`../assets/atlases/generated/${file}`); 117 | return true; 118 | } catch { 119 | console.error(`Error loading file ${file}`); 120 | return false; 121 | } 122 | }; 123 | 124 | export const getDegreeFromRadians = (radians) => (radians * (180 / Math.PI)); 125 | 126 | export const getRadiansFromDegree = (degree) => (degree * (Math.PI / 180)); 127 | 128 | export const rotateRectangleInsideTile = (x, y, width, height, degree) => { 129 | switch (degree) { 130 | case 90: { 131 | return [ 132 | TILE_HEIGHT - (y + height), 133 | x, 134 | height, 135 | width, 136 | ]; 137 | } 138 | 139 | case 180: { 140 | return [ 141 | TILE_WIDTH - (x + width), 142 | TILE_HEIGHT - (y + height), 143 | width, 144 | height, 145 | ]; 146 | } 147 | 148 | case 270: { 149 | return [ 150 | y, 151 | TILE_WIDTH - (x + width), 152 | height, 153 | width, 154 | ]; 155 | } 156 | 157 | default: { 158 | return [x, y, width, height]; 159 | } 160 | } 161 | }; 162 | 163 | export const isDev = () => process.env.NODE_ENV !== 'production'; 164 | 165 | export const getFileNameWithoutExtension = (filePath) => filePath.split('/').pop().split('.').shift(); 166 | -------------------------------------------------------------------------------- /src/zustand/assets/selectLoadedAssets.js: -------------------------------------------------------------------------------- 1 | export const selectLoadedFonts = (state) => state.loadedAssets.fonts; 2 | 3 | export const selectLoadedAtlases = (state) => state.loadedAssets.atlases; 4 | 5 | export const selectLoadedImages = (state) => state.loadedAssets.images; 6 | 7 | export const selectLoadedMaps = (state) => state.loadedAssets.maps; 8 | 9 | export const selectLoadedJSONs = (state) => state.loadedAssets.jsons; 10 | 11 | export const selectAssetsSetters = (state) => state.loadedAssets.setters; 12 | -------------------------------------------------------------------------------- /src/zustand/assets/setLoadedAssets.js: -------------------------------------------------------------------------------- 1 | export default (set) => ({ 2 | addLoadedFont: (font) => 3 | set((state) => ({ 4 | ...state, 5 | loadedAssets: { 6 | ...state.loadedAssets, 7 | // TODO make this a Set() 8 | fonts: [...state.loadedAssets.fonts, font], 9 | }, 10 | })), 11 | addLoadedAtlas: (atlas) => 12 | set((state) => ({ 13 | ...state, 14 | loadedAssets: { 15 | ...state.loadedAssets, 16 | // TODO make this a Set() 17 | atlases: [...state.loadedAssets.atlases, atlas], 18 | }, 19 | })), 20 | addLoadedImage: (image) => 21 | set((state) => ({ 22 | ...state, 23 | loadedAssets: { 24 | ...state.loadedAssets, 25 | // TODO make this a Set() 26 | images: [...state.loadedAssets.images, image], 27 | }, 28 | })), 29 | addLoadedMap: (map) => 30 | set((state) => ({ 31 | ...state, 32 | loadedAssets: { 33 | ...state.loadedAssets, 34 | // TODO make this a Set() 35 | maps: [...state.loadedAssets.maps, map], 36 | }, 37 | })), 38 | addLoadedJson: (json) => 39 | set((state) => ({ 40 | ...state, 41 | loadedAssets: { 42 | ...state.loadedAssets, 43 | // TODO make this a Set() 44 | jsons: [...state.loadedAssets.jsons, json], 45 | }, 46 | })), 47 | }); 48 | -------------------------------------------------------------------------------- /src/zustand/dialog/selectDialog.js: -------------------------------------------------------------------------------- 1 | export const selectDialogMessages = (state) => state.dialog.messages; 2 | 3 | export const selectDialogAction = (state) => state.dialog.action; 4 | 5 | export const selectDialogCharacterName = (state) => state.dialog.characterName; 6 | 7 | export const selectDialogSetters = (state) => state.dialog.setters; 8 | -------------------------------------------------------------------------------- /src/zustand/dialog/setDialog.js: -------------------------------------------------------------------------------- 1 | export default (set) => ({ 2 | setDialogMessages: (messages) => 3 | set((state) => ({ 4 | ...state, 5 | dialog: { 6 | ...state.dialog, 7 | messages, 8 | }, 9 | })), 10 | setDialogAction: (action) => 11 | set((state) => ({ 12 | ...state, 13 | dialog: { 14 | ...state.dialog, 15 | action, 16 | }, 17 | })), 18 | setDialogCharacterName: (characterName) => 19 | set((state) => ({ 20 | ...state, 21 | dialog: { 22 | ...state.dialog, 23 | characterName, 24 | }, 25 | })), 26 | }); 27 | -------------------------------------------------------------------------------- /src/zustand/game/selectGameData.js: -------------------------------------------------------------------------------- 1 | import { GAME_CONTENT_ID } from '../../constants'; 2 | 3 | export const selectGameWidth = (state) => state.game.width; 4 | 5 | export const selectGameHeight = (state) => state.game.height; 6 | 7 | export const selectGameZoom = (state) => state.game.zoom; 8 | 9 | export const selectGameCanvasElement = (state) => 10 | state.game.canvas || document.querySelector(`#${GAME_CONTENT_ID}`)?.firstChild; 11 | 12 | export const selectGameLocale = (state) => state.game.locale; 13 | 14 | export const selectGameCameraSizeUpdateCallbacks = (state) => state.game.cameraSizeUpdateCallbacks; 15 | 16 | export const selectGameSetters = (state) => state.game.setters; 17 | -------------------------------------------------------------------------------- /src/zustand/game/setGameData.js: -------------------------------------------------------------------------------- 1 | export default (set) => ({ 2 | setGameWidth: (width) => 3 | set((state) => ({ 4 | ...state, 5 | game: { 6 | ...state.game, 7 | width, 8 | }, 9 | })), 10 | setGameHeight: (height) => 11 | set((state) => ({ 12 | ...state, 13 | game: { 14 | ...state.game, 15 | height, 16 | }, 17 | })), 18 | setGameZoom: (zoom) => 19 | set((state) => ({ 20 | ...state, 21 | game: { 22 | ...state.game, 23 | zoom, 24 | }, 25 | })), 26 | setGameCanvasElement: (canvas) => 27 | set((state) => ({ 28 | ...state, 29 | game: { 30 | ...state.game, 31 | canvas, 32 | }, 33 | })), 34 | addGameCameraSizeUpdateCallback: (cameraSizeUpdateCallback) => { 35 | set((state) => ({ 36 | ...state, 37 | game: { 38 | ...state.game, 39 | // TODO make this a Set() 40 | cameraSizeUpdateCallbacks: [...state.game.cameraSizeUpdateCallbacks, cameraSizeUpdateCallback], 41 | }, 42 | })); 43 | 44 | return cameraSizeUpdateCallback; 45 | }, 46 | setGameLocale: (locale) => 47 | set((state) => ({ 48 | ...state, 49 | game: { 50 | ...state.game, 51 | locale, 52 | }, 53 | })), 54 | }); 55 | -------------------------------------------------------------------------------- /src/zustand/hero/selectHeroData.js: -------------------------------------------------------------------------------- 1 | export const selectHeroFacingDirection = (state) => state.heroData.facingDirection; 2 | 3 | export const selectHeroInitialPosition = (state) => state.heroData.initialPosition; 4 | 5 | export const selectHeroPreviousPosition = (state) => state.heroData.previousPosition; 6 | 7 | export const selectHeroInitialFrame = (state) => state.heroData.initialFrame; 8 | 9 | export const selectHeroInventoryDice = (state) => state.heroData.inventory.dice; 10 | 11 | export const selectHeroEquipedInventoryDice = (state) => 12 | selectHeroInventoryDice(state).filter((die) => die.equiped); 13 | 14 | export const selectHeroSetters = (state) => state.heroData.setters; 15 | -------------------------------------------------------------------------------- /src/zustand/hero/setHeroData.js: -------------------------------------------------------------------------------- 1 | export default (set) => ({ 2 | setHeroFacingDirection: (facingDirection) => 3 | set((state) => ({ 4 | ...state, 5 | heroData: { 6 | ...state.heroData, 7 | facingDirection, 8 | }, 9 | })), 10 | setHeroInitialPosition: (initialPosition) => 11 | set((state) => ({ 12 | ...state, 13 | heroData: { 14 | ...state.heroData, 15 | initialPosition, 16 | }, 17 | })), 18 | setHeroPreviousPosition: (previousPosition) => 19 | set((state) => ({ 20 | ...state, 21 | heroData: { 22 | ...state.heroData, 23 | previousPosition, 24 | }, 25 | })), 26 | setHeroInitialFrame: (initialFrame) => 27 | set((state) => ({ 28 | ...state, 29 | heroData: { 30 | ...state.heroData, 31 | initialFrame, 32 | }, 33 | })), 34 | addHeroInventoryDice: (die) => 35 | set((state) => ({ 36 | ...state, 37 | heroData: { 38 | ...state.heroData, 39 | inventory: { 40 | ...state.heroData.inventory, 41 | // TODO make this a Set() 42 | dice: [...state.heroData.inventory.dice, die], 43 | }, 44 | }, 45 | })), 46 | }); 47 | -------------------------------------------------------------------------------- /src/zustand/map/selectMapData.js: -------------------------------------------------------------------------------- 1 | export const selectMapKey = (state) => state.mapData.mapKey; 2 | 3 | export const selectTilesets = (state) => state.mapData.tilesets; 4 | 5 | export const selectMapSetters = (state) => state.mapData.setters; 6 | -------------------------------------------------------------------------------- /src/zustand/map/setMapData.js: -------------------------------------------------------------------------------- 1 | export default (set) => ({ 2 | setMapKey: (mapKey) => 3 | set((state) => ({ 4 | ...state, 5 | mapData: { 6 | ...state.mapData, 7 | mapKey, 8 | }, 9 | })), 10 | addTileset: (tilesets) => 11 | set((state) => ({ 12 | ...state, 13 | mapData: { 14 | ...state.mapData, 15 | // TODO make this a Set() 16 | tilesets: [...state.mapData.tilesets, tilesets], 17 | }, 18 | })), 19 | }); 20 | -------------------------------------------------------------------------------- /src/zustand/menu/selectMenu.js: -------------------------------------------------------------------------------- 1 | export const selectMenuItems = (state) => state.menu.items; 2 | 3 | export const selectMenuPosition = (state) => state.menu.position; 4 | 5 | export const selectMenuOnSelect = (state) => state.menu.onSelect; 6 | 7 | export const selectMenuSetters = (state) => state.menu.setters; 8 | -------------------------------------------------------------------------------- /src/zustand/menu/setMenu.js: -------------------------------------------------------------------------------- 1 | export default (set) => ({ 2 | setMenuItems: (items) => 3 | set((state) => ({ 4 | ...state, 5 | menu: { 6 | ...state.menu, 7 | items, 8 | }, 9 | })), 10 | setMenuOnSelect: (onSelect) => 11 | set((state) => ({ 12 | ...state, 13 | menu: { 14 | ...state.menu, 15 | onSelect, 16 | }, 17 | })), 18 | setMenuPosition: (position) => 19 | set((state) => ({ 20 | ...state, 21 | menu: { 22 | ...state.menu, 23 | position, 24 | }, 25 | })), 26 | addMenuItems: (items) => 27 | set((state) => ({ 28 | ...state, 29 | menu: { 30 | ...state.menu, 31 | // TODO make this a Set() 32 | items: [...state.menu.items, ...items], 33 | }, 34 | })), 35 | }); 36 | -------------------------------------------------------------------------------- /src/zustand/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, useStore } from 'zustand'; 2 | 3 | // Constants 4 | import { MIN_GAME_HEIGHT, MIN_GAME_WIDTH } from '../constants'; 5 | 6 | // Setters 7 | import setLoadedAssets from './assets/setLoadedAssets'; 8 | import setGameData from './game/setGameData'; 9 | import setHeroData from './hero/setHeroData'; 10 | import setDialog from './dialog/setDialog'; 11 | import setMapData from './map/setMapData'; 12 | import setMenu from './menu/setMenu'; 13 | import setText from './text/setText'; 14 | 15 | // define the store 16 | const store = createStore((set) => ({ 17 | loadedAssets: { 18 | fonts: [], 19 | atlases: [], 20 | images: [], 21 | maps: [], 22 | jsons: [], 23 | setters: setLoadedAssets(set), 24 | }, 25 | heroData: { 26 | facingDirection: '', 27 | initialPosition: {}, 28 | previousPosition: {}, 29 | initialFrame: '', 30 | inventory: { 31 | dice: [], 32 | }, 33 | setters: setHeroData(set), 34 | }, 35 | mapData: { 36 | mapKey: '', 37 | tilesets: [], 38 | setters: setMapData(set), 39 | }, 40 | game: { 41 | width: MIN_GAME_WIDTH, 42 | height: MIN_GAME_HEIGHT, 43 | zoom: 1, 44 | locale: 'en', 45 | cameraSizeUpdateCallbacks: [], 46 | setters: setGameData(set), 47 | }, 48 | dialog: { 49 | messages: [], 50 | action: null, 51 | characterName: '', 52 | setters: setDialog(set), 53 | }, 54 | menu: { 55 | items: [], 56 | position: 'center', 57 | onSelect: null, 58 | setters: setMenu(set), 59 | }, 60 | text: { 61 | texts: [], 62 | setters: setText(set), 63 | }, 64 | })); 65 | 66 | export const useGameStore = (selector) => useStore(store, selector); 67 | 68 | export const getState = () => store.getState(); 69 | -------------------------------------------------------------------------------- /src/zustand/text/selectText.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/prefer-default-export 2 | export const selectTexts = (state) => state.text.texts; 3 | 4 | export const selectTextSetters = (state) => state.text.setters; 5 | -------------------------------------------------------------------------------- /src/zustand/text/setText.js: -------------------------------------------------------------------------------- 1 | export default (set) => ({ 2 | setTextTexts: (texts) => 3 | set((state) => ({ 4 | ...state, 5 | text: { 6 | ...state.text, 7 | texts, 8 | }, 9 | })), 10 | addTextTexts: (texts) => 11 | set((state) => ({ 12 | ...state, 13 | text: { 14 | ...state.text, 15 | // TODO make this a Set() 16 | texts: [...state.text.texts, ...texts], 17 | }, 18 | })), 19 | updateTextTexts: (key, variables) => 20 | set((state) => ({ 21 | ...state, 22 | text: { 23 | ...state.text, 24 | texts: 25 | state.text.texts.map((text) => { 26 | if (text.key === key) { 27 | return { 28 | ...text, 29 | variables, 30 | }; 31 | } 32 | 33 | return text; 34 | }) 35 | , 36 | }, 37 | })), 38 | removeTextTexts: (key) => 39 | set((state) => ({ 40 | ...state, 41 | text: { 42 | ...state.text, 43 | texts: state.text.texts.filter((text) => text.key !== key), 44 | }, 45 | })), 46 | }); 47 | --------------------------------------------------------------------------------