├── .gitignore
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── config
├── eslint.config.mjs
└── vite.config.js
├── docs
├── logo.png
├── planning
│ ├── map-layout.png
│ ├── movement.png
│ ├── state-machine.png
│ ├── state_machine.excalidraw
│ ├── zelda_map_layout.excalidraw
│ └── zelda_movement.excalidraw
├── screenshot1.png
├── screenshot2.png
└── screenshot3.png
├── index.html
├── package.json
├── pnpm-lock.yaml
├── project-task.todo
├── public
└── assets
│ ├── data
│ └── assets.json
│ ├── fonts
│ └── Press_Start_2P
│ │ ├── OFL.txt
│ │ └── PressStart2P-Regular.ttf
│ └── images
│ ├── enemies
│ ├── drow.json
│ ├── drow.png
│ ├── enemy_death.png
│ ├── spider.json
│ ├── spider.png
│ ├── wisp.json
│ └── wisp.png
│ ├── hud
│ ├── heart_and_hud_numbers.json
│ └── heart_and_hud_numbers.png
│ ├── levels
│ ├── common
│ │ ├── collision.png
│ │ ├── dungeon_objects.json
│ │ ├── dungeon_objects.png
│ │ ├── pot.png
│ │ └── pot_break.png
│ ├── dungeon_1
│ │ ├── dungeon_1.json
│ │ ├── dungeon_1_background.png
│ │ └── dungeon_1_foreground.png
│ └── world
│ │ ├── world.json
│ │ ├── world_background.png
│ │ └── world_foreground.png
│ ├── player
│ ├── dagger.png
│ ├── main_green.json
│ └── main_green.png
│ └── ui
│ ├── cursor_white.png
│ ├── dialog_ui.png
│ └── icons.png
├── src
├── common
│ ├── assets.ts
│ ├── common.ts
│ ├── config.ts
│ ├── data-manager.ts
│ ├── event-bus.ts
│ ├── juice-utils.ts
│ ├── tiled
│ │ ├── common.ts
│ │ ├── tiled-utils.ts
│ │ └── types.ts
│ ├── types.ts
│ └── utils.ts
├── components
│ ├── game-object
│ │ ├── animation-component.ts
│ │ ├── base-game-object-component.ts
│ │ ├── colliding-objects-component.ts
│ │ ├── controls-component.ts
│ │ ├── direction-component.ts
│ │ ├── held-game-object-component.ts
│ │ ├── interactive-object-component.ts
│ │ ├── invulnerable-component.ts
│ │ ├── life-component.ts
│ │ ├── speed-component.ts
│ │ ├── throwable-object-component.ts
│ │ └── weapon-component.ts
│ ├── input
│ │ ├── input-component.ts
│ │ └── keyboard-component.ts
│ ├── inventory
│ │ └── inventory-manager.ts
│ └── state-machine
│ │ ├── state-machine.ts
│ │ └── states
│ │ └── character
│ │ ├── attack-state.ts
│ │ ├── base-character-state.ts
│ │ ├── base-move-state.ts
│ │ ├── boss
│ │ └── drow
│ │ │ ├── boss-drow-hidden-state.ts
│ │ │ ├── boss-drow-idle-state.ts
│ │ │ ├── boss-drow-prepare-attack-state.ts
│ │ │ └── boss-drow-teleport-state.ts
│ │ ├── bounce-move-state.ts
│ │ ├── character-states.ts
│ │ ├── death-state.ts
│ │ ├── hurt-state.ts
│ │ ├── idle-holding-state.ts
│ │ ├── idle-state.ts
│ │ ├── lift-state.ts
│ │ ├── move-holding-state.ts
│ │ ├── move-state.ts
│ │ ├── open-chest-state.ts
│ │ └── throw-state.ts
├── game-objects
│ ├── common
│ │ └── character-game-object.ts
│ ├── enemies
│ │ ├── boss
│ │ │ └── drow.ts
│ │ ├── spider.ts
│ │ └── wisp.ts
│ ├── objects
│ │ ├── button.ts
│ │ ├── chest.ts
│ │ ├── door.ts
│ │ └── pot.ts
│ ├── player
│ │ └── player.ts
│ └── weapons
│ │ ├── base-weapon.ts
│ │ ├── dagger.ts
│ │ └── sword.ts
├── main.ts
└── scenes
│ ├── game-over-scene.ts
│ ├── game-scene.ts
│ ├── preload-scene.ts
│ ├── scene-keys.ts
│ └── ui-scene.ts
├── tsconfig.json
└── vite.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | project-task-tracker.todo
5 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "eslint.format.enable": true,
3 | "eslint.options": {
4 | "overrideConfigFile": "config/eslint.config.mjs"
5 | },
6 | "editor.defaultFormatter": "dbaeumer.vscode-eslint",
7 | "editor.codeActionsOnSave": {
8 | "source.fixAll.eslint": "explicit"
9 | },
10 | "cSpell.words": [
11 | "anims",
12 | "Aseprite",
13 | "devshareacademy",
14 | "DROW",
15 | "firstgid",
16 | "Interactable",
17 | "respawn",
18 | "spritesheet",
19 | "Tilemap",
20 | "Tilemaps",
21 | "tileset",
22 | "tweens"
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Dev Share Academy
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 | # Legend of the Wispguard
2 |
3 | 
4 |
5 | 
6 |
7 | Legend of the Wispguard - Zelda-like Tutorial with [Phaser 3](https://github.com/photonstorm/phaser)!
8 |
9 | This repo is the official code repository for the Legend of the Wispguard: Build a Zelda-Like Game in Phaser 3 Course that is available on YouTube.
10 |
11 | ## Demo
12 |
13 | You can find a playable demo of the game on Itch.io here: [Legend of the Wispguard](https://galemius.itch.io/legend-of-the-wispguard)
14 |
15 | 
16 | 
17 | 
18 |
19 | ## How To Play
20 |
21 | Currently, the only supported way to play the game is with a Keyboard.
22 |
23 | ### Controls
24 |
25 | | Keys | Description |
26 | | -------------------------------------- | ----------------------------------------------------------------------------------------------------- |
27 | | Arrow Keys (Up, Down, Left, and Right) | Moves the player. Navigate menu. |
28 | | Z | Attack |
29 | | X | Lift/Throw |
30 | | Enter | Select menu option. |
31 |
32 |
33 | ## Local Development
34 |
35 | ### Requirements
36 |
37 | Node.js and pnpm are required to install dependencies and run scripts via `pnpm`.
38 |
39 | **Note:** You can also use `npm` to install the required project dependencies. To do this, replace the commands listed below with the relevant `npm` command, such as `npm install` or `npm run start`.
40 |
41 | Vite is required to bundle and serve the web application. This is included as part of the projects dev dependencies.
42 |
43 | ### Available Commands
44 |
45 | | Command | Description |
46 | |---------|-------------|
47 | | `pnpm install --frozen-lockfile` | Install project dependencies |
48 | | `pnpm start` | Build project and open web server running project |
49 | | `pnpm build` | Builds code bundle for production |
50 | | `pnpm lint` | Uses ESLint to lint code |
51 |
52 | ### Writing Code
53 |
54 | After cloning the repo, run `pnpm install --frozen-lockfile` from your project directory. Then, you can start the local development
55 | server by running `pnpm start`.
56 |
57 | After starting the development server with `pnpm start`, you can edit any files in the `src` folder
58 | and parcel will automatically recompile and reload your server (available at `http://localhost:8080`
59 | by default).
60 |
61 | ### Deploying Code
62 |
63 | After you run the `pnpm build` command, your code will be built into a single bundle located at
64 | `dist/*` along with any other assets you project depended.
65 |
66 | If you put the contents of the `dist` folder in a publicly-accessible location (say something like `http://myserver.com`),
67 | you should be able to open `http://myserver.com/index.html` and play your game.
68 |
69 | ### Static Assets
70 |
71 | Any static assets like images or audio files should be placed in the `public` folder. It'll then be served at `http://localhost:8080/path-to-file-your-file/file-name.file-type`.
72 |
73 | ## Credits
74 |
75 | This project would have not been possible without the use of some awesome assets created by some amazing artists! This project would not have been possible without the following people/resources:
76 |
77 | | Asset | Author | Link |
78 | | --------------------------- | ---------------- | ---------------------------------------------------------------------- |
79 | | Press Start 2P Font | CodeMan38 | [Google Fonts](https://fonts.google.com/specimen/Press+Start+2P) |
80 | | Player | Foozle | [Legend Main Character](https://foozlecc.itch.io/legend-main-character)|
81 | | Enemies | Foozle | [Legend Enemy Pack 1](https://foozlecc.itch.io/legend-enemy-pack-1) |
82 | | Dungeon Pack | Foozle | [Legend Spider Dungeon](https://foozlecc.itch.io/legend-spider-dungeon)|
83 | | UI Icons | Foozle | [Legend UI Icons](https://foozlecc.itch.io/legend-ui-icons) |
84 |
85 | ## Issues
86 |
87 | For any issues you encounter, please open a new [GitHub Issue](https://github.com/devshareacademy/phaser-zelda-like-tutorial/issues) on this project.
88 |
--------------------------------------------------------------------------------
/config/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import path from "node:path";
2 | import { fileURLToPath } from "node:url";
3 | import js from "@eslint/js";
4 | import { FlatCompat } from "@eslint/eslintrc";
5 |
6 | const __filename = fileURLToPath(import.meta.url);
7 | const __dirname = path.dirname(__filename);
8 | const compat = new FlatCompat({
9 | baseDirectory: __dirname,
10 | recommendedConfig: js.configs.recommended,
11 | allConfig: js.configs.all
12 | });
13 |
14 | export default [{
15 | ignores: ["**/node_modules", "**/dist"],
16 | }, ...compat.extends("@devshareacademy/eslint-config"), {
17 | languageOptions: {
18 | ecmaVersion: 5,
19 | sourceType: "script",
20 |
21 | parserOptions: {
22 | project: "./tsconfig.json",
23 | },
24 | },
25 |
26 | rules: {
27 | "@typescript-eslint/unbound-method": "off",
28 | },
29 |
30 | files: ["**/*.ts", "**/*.tsx"]
31 | }];
32 |
--------------------------------------------------------------------------------
/config/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 |
3 | export default defineConfig({
4 | base: "./",
5 | build: {
6 | chunkSizeWarningLimit: 1600,
7 | rollupOptions: {
8 | output: {
9 | entryFileNames: 'assets/js/[name]-[hash].js',
10 | },
11 | },
12 | },
13 | server: {
14 | port: 3000,
15 | },
16 | });
17 |
--------------------------------------------------------------------------------
/docs/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devshareacademy/phaser-zelda-like-tutorial/63b62db25fb9e6ffdff6f54e5230ed304ddfe9cc/docs/logo.png
--------------------------------------------------------------------------------
/docs/planning/map-layout.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devshareacademy/phaser-zelda-like-tutorial/63b62db25fb9e6ffdff6f54e5230ed304ddfe9cc/docs/planning/map-layout.png
--------------------------------------------------------------------------------
/docs/planning/movement.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devshareacademy/phaser-zelda-like-tutorial/63b62db25fb9e6ffdff6f54e5230ed304ddfe9cc/docs/planning/movement.png
--------------------------------------------------------------------------------
/docs/planning/state-machine.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devshareacademy/phaser-zelda-like-tutorial/63b62db25fb9e6ffdff6f54e5230ed304ddfe9cc/docs/planning/state-machine.png
--------------------------------------------------------------------------------
/docs/screenshot1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devshareacademy/phaser-zelda-like-tutorial/63b62db25fb9e6ffdff6f54e5230ed304ddfe9cc/docs/screenshot1.png
--------------------------------------------------------------------------------
/docs/screenshot2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devshareacademy/phaser-zelda-like-tutorial/63b62db25fb9e6ffdff6f54e5230ed304ddfe9cc/docs/screenshot2.png
--------------------------------------------------------------------------------
/docs/screenshot3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devshareacademy/phaser-zelda-like-tutorial/63b62db25fb9e6ffdff6f54e5230ed304ddfe9cc/docs/screenshot3.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Phaser 3 - zelda_legend
6 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@devshareacademy/phaser-3-zelda_legend",
3 | "version": "1.0.0",
4 | "description": "A Phaser 3 implementation of zelda_legend.",
5 | "scripts": {
6 | "start": "vite --config config/vite.config.js",
7 | "build": "tsc && vite build --config config/vite.config.js",
8 | "serve": "vite preview --config config/vite.config.js",
9 | "lint": "eslint ./src -c ./config/eslint.config.mjs"
10 | },
11 | "author": "scottwestover",
12 | "license": "MIT",
13 | "repository": {
14 | "type": "git",
15 | "url": "https://github.com/devshareacademy/phaser-zelda-like-tutorial.git"
16 | },
17 | "homepage": "https://github.com/devshareacademy/phaser-zelda-like-tutorial",
18 | "devDependencies": {
19 | "@devshareacademy/eslint-config": "0.0.19",
20 | "@devshareacademy/prettier-config": "0.0.6",
21 | "@devshareacademy/tsconfig": "0.0.3",
22 | "@typescript-eslint/eslint-plugin": "8.20.0",
23 | "@typescript-eslint/parser": "8.20.0",
24 | "eslint": "9.18.0",
25 | "eslint-config-prettier": "10.0.1",
26 | "eslint-plugin-prettier": "5.2.3",
27 | "prettier": "3.4.2",
28 | "typescript": "5.7.3",
29 | "vite": "6.0.7"
30 | },
31 | "dependencies": {
32 | "phaser": "3.87.0"
33 | },
34 | "resolutions": {},
35 | "prettier": "@devshareacademy/prettier-config",
36 | "volta": {
37 | "node": "20.11.0",
38 | "yarn": "1.22.11",
39 | "pnpm": "8.14.3"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/project-task.todo:
--------------------------------------------------------------------------------
1 | Intro:
2 | ☐ demo game that will be built
3 | ☐ project requirements
4 | ☐ review project task list
5 | ☐ credit for assets used in project
6 |
7 | Project Setup:
8 | ☐ download basic project files
9 | ☐ download tiled for level editing
10 | ☐ install project dependencies
11 | ☐ running project on local web server
12 | ☐ adding assets to project
13 |
14 | Player:
15 | ☐ create player game object
16 | ☐ handle player input
17 | ☐ player movement
18 | ☐ character state machine
19 | ☐ components for animation, speed, and direction
20 |
21 | Enemies:
22 | ☐ create character base class
23 | ☐ update spider and player class to use
24 | ☐ create spider
25 | ☐ state machine
26 | ☐ components for animation, speed, and direction
27 | ☐ basic random enemy movement
28 | ☐ create wisp
29 | ☐ state machine
30 | ☐ add common components
31 | ☐ invulnerable component
32 | ☐ collisions between player and enemies
33 | ☐ life component and taking damage
34 |
35 | Interactable Items:
36 | ☐ create breakable pot
37 | ☐ create chest
38 | ☐ add collision between player and items
39 | ☐ add player states for interacting with items
40 | ☐ open chests
41 | ☐ pickup and throw pots
42 | ☐ drop pots when hurt
43 |
44 | Level Design & Creation:
45 | ☐ review dungeon and over world level designs
46 | ☐ review data we will need for each level
47 | ☐ review how levels are connected together
48 | ☐ tiled
49 | ☐ create dungeon
50 | ☐ add background image for a guide
51 | ☐ build dungeon rooms and outline
52 | ☐ add collision layers
53 | ☐ import custom types
54 | ☐ add game objects for doors, chests, enemies, traps, etc
55 | ☐ create foreground layer
56 | ☐ add room layers
57 | ☐ export json data and images
58 | ☐ create over world level (same steps as creating dungeon)
59 |
60 | Levels:
61 | ☐ create background and foreground images
62 | ☐ loading tiled map data
63 | ☐ add collision layers
64 | ☐ camera follow player
65 | ☐ bind camera to room
66 | ☐ room transitions
67 | ☐ level transitions
68 |
69 | Level Objects:
70 | ☐ create pots
71 | ☐ create chests
72 | ☐ create enemies
73 | ☐ create doors
74 | ☐ create buttons
75 |
76 | Dungeon Mechanics:
77 | ☐ traps
78 | ☐ button press
79 | ☐ enemies defeated
80 | ☐ inventory manager
81 | ☐ chest items
82 | ☐ locked doors
83 | ☐ small key door
84 | ☐ boss key door
85 | ☐ respawn select objects
86 | ☐ show/hide objects dynamically
87 |
88 | Attacking:
89 | ☐ attack state
90 | ☐ weapon component
91 | ☐ base weapon class
92 | ☐ sword weapon
93 | ☐ weapon collisions
94 |
95 | Data Manager:
96 | ☐ player details
97 | ☐ level details
98 | ☐ game scene updates
99 |
100 | UI:
101 | ☐ player health
102 | ☐ game over
103 | ☐ dialog ui
104 |
105 | Boss:
106 | ☐ create boss character
107 | ☐ add boss states
108 | ☐ dagger weapon
109 | ☐ boss defeated
110 |
111 |
112 |
113 |
114 | -------------------------------
115 | Premium Content:
116 | ☐ asset pack splitting and dynamic asset loading
117 | ☐ saving and loading game state
118 | ☐ tile scene
119 | ☐ saved data scene
120 | ☐ additional weapons & items
121 | ☐ bombs
122 | ☐ bow
123 | ☐ 2nd dungeon
124 | ☐ flying tile enemies
125 | ☐ additional boss
126 | ☐ inventory screen
127 | ☐ map screen
128 | ☐ hud enhancements - items, equipped item, etc.
129 | ☐ new mechanics
130 | ☐ bombable entrances
131 | ☐ falling floor
132 | ☐ level 2 tier items
133 | ☐ deal more damage
134 | ☐ receive less damage
135 |
136 | -------------------------------
137 |
138 | -------------------------------
139 | ☐ ✔ ✘
140 | -------------------------------
141 |
--------------------------------------------------------------------------------
/public/assets/data/assets.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "path": "assets/fonts",
4 | "files": [
5 | {
6 | "type": "font",
7 | "key": "FONT_PRESS_START_2P",
8 | "url": "Press_Start_2P/PressStart2P-Regular.ttf",
9 | "format": "truetype"
10 | }
11 | ]
12 | },
13 | {
14 | "path": "assets/images/hud",
15 | "files": [
16 | {
17 | "type": "aseprite",
18 | "key": "HUD_NUMBERS",
19 | "textureURL": "heart_and_hud_numbers.png",
20 | "atlasURL": "heart_and_hud_numbers.json"
21 | }
22 | ]
23 | },
24 | {
25 | "path": "assets/images/ui",
26 | "files": [
27 | {
28 | "type": "image",
29 | "key": "UI_DIALOG",
30 | "url": "dialog_ui.png"
31 | },
32 | {
33 | "type": "image",
34 | "key": "UI_CURSOR",
35 | "url": "cursor_white.png"
36 | },
37 | {
38 | "type": "spritesheet",
39 | "key": "UI_ICONS",
40 | "url": "icons.png",
41 | "frameConfig": { "frameWidth": 16, "frameHeight": 16 }
42 | }
43 | ]
44 | },
45 | {
46 | "path": "assets/images/player",
47 | "files": [
48 | {
49 | "type": "aseprite",
50 | "key": "PLAYER",
51 | "textureURL": "main_green.png",
52 | "atlasURL": "main_green.json"
53 | },
54 | {
55 | "type": "spritesheet",
56 | "key": "DAGGER",
57 | "url": "dagger.png",
58 | "frameConfig": { "frameWidth": 16, "frameHeight": 16 }
59 | }
60 | ]
61 | },
62 | {
63 | "path": "assets/images/levels/common",
64 | "files": [
65 | {
66 | "type": "image",
67 | "key": "COLLISION",
68 | "url": "collision.png"
69 | },
70 | {
71 | "type": "atlas",
72 | "key": "DUNGEON_OBJECTS",
73 | "textureURL": "dungeon_objects.png",
74 | "atlasURL": "dungeon_objects.json"
75 | },
76 | {
77 | "type": "image",
78 | "key": "POT",
79 | "url": "pot.png"
80 | },
81 | {
82 | "type": "spritesheet",
83 | "key": "POT_BREAK",
84 | "url": "pot_break.png",
85 | "frameConfig": { "frameWidth": 32, "frameHeight": 32 }
86 | }
87 | ]
88 | },
89 | {
90 | "path": "assets/images/enemies",
91 | "files": [
92 | {
93 | "type": "aseprite",
94 | "key": "SPIDER",
95 | "textureURL": "spider.png",
96 | "atlasURL": "spider.json"
97 | },
98 | {
99 | "type": "aseprite",
100 | "key": "WISP",
101 | "textureURL": "wisp.png",
102 | "atlasURL": "wisp.json"
103 | },
104 | {
105 | "type": "spritesheet",
106 | "key": "ENEMY_DEATH",
107 | "url": "enemy_death.png",
108 | "frameConfig": { "frameWidth": 16, "frameHeight": 16 }
109 | },
110 | {
111 | "type": "aseprite",
112 | "key": "DROW",
113 | "textureURL": "drow.png",
114 | "atlasURL": "drow.json"
115 | }
116 | ]
117 | },
118 | {
119 | "path": "assets/images/levels/dungeon_1",
120 | "files": [
121 | {
122 | "type": "image",
123 | "key": "DUNGEON_1_BACKGROUND",
124 | "url": "dungeon_1_background.png"
125 | },
126 | {
127 | "type": "image",
128 | "key": "DUNGEON_1_FOREGROUND",
129 | "url": "dungeon_1_foreground.png"
130 | },
131 | {
132 | "type": "tilemapTiledJSON",
133 | "key": "DUNGEON_1_LEVEL",
134 | "url": "dungeon_1.json"
135 | }
136 | ]
137 | },
138 | {
139 | "path": "assets/images/levels/world",
140 | "files": [
141 | {
142 | "type": "image",
143 | "key": "WORLD_BACKGROUND",
144 | "url": "world_background.png"
145 | },
146 | {
147 | "type": "image",
148 | "key": "WORLD_FOREGROUND",
149 | "url": "world_foreground.png"
150 | },
151 | {
152 | "type": "tilemapTiledJSON",
153 | "key": "WORLD_LEVEL",
154 | "url": "world.json"
155 | }
156 | ]
157 | }
158 | ]
159 |
--------------------------------------------------------------------------------
/public/assets/fonts/Press_Start_2P/OFL.txt:
--------------------------------------------------------------------------------
1 | Copyright 2012 The Press Start 2P Project Authors (cody@zone38.net), with Reserved Font Name "Press Start 2P".
2 |
3 | This Font Software is licensed under the SIL Open Font License, Version 1.1.
4 | This license is copied below, and is also available with a FAQ at:
5 | https://openfontlicense.org
6 |
7 |
8 | -----------------------------------------------------------
9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10 | -----------------------------------------------------------
11 |
12 | PREAMBLE
13 | The goals of the Open Font License (OFL) are to stimulate worldwide
14 | development of collaborative font projects, to support the font creation
15 | efforts of academic and linguistic communities, and to provide a free and
16 | open framework in which fonts may be shared and improved in partnership
17 | with others.
18 |
19 | The OFL allows the licensed fonts to be used, studied, modified and
20 | redistributed freely as long as they are not sold by themselves. The
21 | fonts, including any derivative works, can be bundled, embedded,
22 | redistributed and/or sold with any software provided that any reserved
23 | names are not used by derivative works. The fonts and derivatives,
24 | however, cannot be released under any other type of license. The
25 | requirement for fonts to remain under this license does not apply
26 | to any document created using the fonts or their derivatives.
27 |
28 | DEFINITIONS
29 | "Font Software" refers to the set of files released by the Copyright
30 | Holder(s) under this license and clearly marked as such. This may
31 | include source files, build scripts and documentation.
32 |
33 | "Reserved Font Name" refers to any names specified as such after the
34 | copyright statement(s).
35 |
36 | "Original Version" refers to the collection of Font Software components as
37 | distributed by the Copyright Holder(s).
38 |
39 | "Modified Version" refers to any derivative made by adding to, deleting,
40 | or substituting -- in part or in whole -- any of the components of the
41 | Original Version, by changing formats or by porting the Font Software to a
42 | new environment.
43 |
44 | "Author" refers to any designer, engineer, programmer, technical
45 | writer or other person who contributed to the Font Software.
46 |
47 | PERMISSION & CONDITIONS
48 | Permission is hereby granted, free of charge, to any person obtaining
49 | a copy of the Font Software, to use, study, copy, merge, embed, modify,
50 | redistribute, and sell modified and unmodified copies of the Font
51 | Software, subject to the following conditions:
52 |
53 | 1) Neither the Font Software nor any of its individual components,
54 | in Original or Modified Versions, may be sold by itself.
55 |
56 | 2) Original or Modified Versions of the Font Software may be bundled,
57 | redistributed and/or sold with any software, provided that each copy
58 | contains the above copyright notice and this license. These can be
59 | included either as stand-alone text files, human-readable headers or
60 | in the appropriate machine-readable metadata fields within text or
61 | binary files as long as those fields can be easily viewed by the user.
62 |
63 | 3) No Modified Version of the Font Software may use the Reserved Font
64 | Name(s) unless explicit written permission is granted by the corresponding
65 | Copyright Holder. This restriction only applies to the primary font name as
66 | presented to the users.
67 |
68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69 | Software shall not be used to promote, endorse or advertise any
70 | Modified Version, except to acknowledge the contribution(s) of the
71 | Copyright Holder(s) and the Author(s) or with their explicit written
72 | permission.
73 |
74 | 5) The Font Software, modified or unmodified, in part or in whole,
75 | must be distributed entirely under this license, and must not be
76 | distributed under any other license. The requirement for fonts to
77 | remain under this license does not apply to any document created
78 | using the Font Software.
79 |
80 | TERMINATION
81 | This license becomes null and void if any of the above conditions are
82 | not met.
83 |
84 | DISCLAIMER
85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93 | OTHER DEALINGS IN THE FONT SOFTWARE.
94 |
--------------------------------------------------------------------------------
/public/assets/fonts/Press_Start_2P/PressStart2P-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devshareacademy/phaser-zelda-like-tutorial/63b62db25fb9e6ffdff6f54e5230ed304ddfe9cc/public/assets/fonts/Press_Start_2P/PressStart2P-Regular.ttf
--------------------------------------------------------------------------------
/public/assets/images/enemies/drow.json:
--------------------------------------------------------------------------------
1 | { "frames": {
2 | "0": {
3 | "frame": { "x": 81, "y": 28, "w": 16, "h": 26 },
4 | "rotated": false,
5 | "trimmed": true,
6 | "spriteSourceSize": { "x": 8, "y": 6, "w": 16, "h": 26 },
7 | "sourceSize": { "w": 32, "h": 32 },
8 | "duration": 200
9 | },
10 | "1": {
11 | "frame": { "x": 56, "y": 55, "w": 16, "h": 25 },
12 | "rotated": false,
13 | "trimmed": true,
14 | "spriteSourceSize": { "x": 8, "y": 7, "w": 16, "h": 25 },
15 | "sourceSize": { "w": 32, "h": 32 },
16 | "duration": 200
17 | },
18 | "2": {
19 | "frame": { "x": 81, "y": 28, "w": 16, "h": 26 },
20 | "rotated": false,
21 | "trimmed": true,
22 | "spriteSourceSize": { "x": 8, "y": 6, "w": 16, "h": 26 },
23 | "sourceSize": { "w": 32, "h": 32 },
24 | "duration": 200
25 | },
26 | "3": {
27 | "frame": { "x": 73, "y": 55, "w": 16, "h": 25 },
28 | "rotated": false,
29 | "trimmed": true,
30 | "spriteSourceSize": { "x": 8, "y": 7, "w": 16, "h": 25 },
31 | "sourceSize": { "w": 32, "h": 32 },
32 | "duration": 200
33 | },
34 | "4": {
35 | "frame": { "x": 99, "y": 81, "w": 14, "h": 25 },
36 | "rotated": false,
37 | "trimmed": true,
38 | "spriteSourceSize": { "x": 9, "y": 7, "w": 14, "h": 25 },
39 | "sourceSize": { "w": 32, "h": 32 },
40 | "duration": 200
41 | },
42 | "5": {
43 | "frame": { "x": 35, "y": 81, "w": 15, "h": 26 },
44 | "rotated": false,
45 | "trimmed": true,
46 | "spriteSourceSize": { "x": 8, "y": 6, "w": 15, "h": 26 },
47 | "sourceSize": { "w": 32, "h": 32 },
48 | "duration": 200
49 | },
50 | "6": {
51 | "frame": { "x": 83, "y": 81, "w": 15, "h": 25 },
52 | "rotated": false,
53 | "trimmed": true,
54 | "spriteSourceSize": { "x": 8, "y": 7, "w": 15, "h": 25 },
55 | "sourceSize": { "w": 32, "h": 32 },
56 | "duration": 200
57 | },
58 | "7": {
59 | "frame": { "x": 51, "y": 81, "w": 15, "h": 26 },
60 | "rotated": false,
61 | "trimmed": true,
62 | "spriteSourceSize": { "x": 8, "y": 6, "w": 15, "h": 26 },
63 | "sourceSize": { "w": 32, "h": 32 },
64 | "duration": 200
65 | },
66 | "8": {
67 | "frame": { "x": 98, "y": 28, "w": 16, "h": 26 },
68 | "rotated": false,
69 | "trimmed": true,
70 | "spriteSourceSize": { "x": 8, "y": 6, "w": 16, "h": 26 },
71 | "sourceSize": { "w": 32, "h": 32 },
72 | "duration": 200
73 | },
74 | "9": {
75 | "frame": { "x": 64, "y": 27, "w": 16, "h": 27 },
76 | "rotated": false,
77 | "trimmed": true,
78 | "spriteSourceSize": { "x": 8, "y": 5, "w": 16, "h": 27 },
79 | "sourceSize": { "w": 32, "h": 32 },
80 | "duration": 200
81 | },
82 | "10": {
83 | "frame": { "x": 98, "y": 28, "w": 16, "h": 26 },
84 | "rotated": false,
85 | "trimmed": true,
86 | "spriteSourceSize": { "x": 8, "y": 6, "w": 16, "h": 26 },
87 | "sourceSize": { "w": 32, "h": 32 },
88 | "duration": 200
89 | },
90 | "11": {
91 | "frame": { "x": 22, "y": 28, "w": 16, "h": 27 },
92 | "rotated": false,
93 | "trimmed": true,
94 | "spriteSourceSize": { "x": 8, "y": 5, "w": 16, "h": 27 },
95 | "sourceSize": { "w": 32, "h": 32 },
96 | "duration": 200
97 | },
98 | "12": {
99 | "frame": { "x": 81, "y": 28, "w": 16, "h": 26 },
100 | "rotated": false,
101 | "trimmed": true,
102 | "spriteSourceSize": { "x": 8, "y": 6, "w": 16, "h": 26 },
103 | "sourceSize": { "w": 32, "h": 32 },
104 | "duration": 200
105 | },
106 | "13": {
107 | "frame": { "x": 90, "y": 55, "w": 16, "h": 25 },
108 | "rotated": false,
109 | "trimmed": true,
110 | "spriteSourceSize": { "x": 8, "y": 6, "w": 16, "h": 25 },
111 | "sourceSize": { "w": 32, "h": 32 },
112 | "duration": 200
113 | },
114 | "14": {
115 | "frame": { "x": 18, "y": 56, "w": 16, "h": 25 },
116 | "rotated": false,
117 | "trimmed": true,
118 | "spriteSourceSize": { "x": 8, "y": 7, "w": 16, "h": 25 },
119 | "sourceSize": { "w": 32, "h": 32 },
120 | "duration": 200
121 | },
122 | "15": {
123 | "frame": { "x": 1, "y": 33, "w": 16, "h": 26 },
124 | "rotated": false,
125 | "trimmed": true,
126 | "spriteSourceSize": { "x": 8, "y": 6, "w": 16, "h": 26 },
127 | "sourceSize": { "w": 32, "h": 32 },
128 | "duration": 200
129 | },
130 | "16": {
131 | "frame": { "x": 18, "y": 82, "w": 14, "h": 25 },
132 | "rotated": false,
133 | "trimmed": true,
134 | "spriteSourceSize": { "x": 9, "y": 7, "w": 14, "h": 25 },
135 | "sourceSize": { "w": 32, "h": 32 },
136 | "duration": 200
137 | },
138 | "17": {
139 | "frame": { "x": 67, "y": 81, "w": 15, "h": 26 },
140 | "rotated": false,
141 | "trimmed": true,
142 | "spriteSourceSize": { "x": 8, "y": 6, "w": 15, "h": 26 },
143 | "sourceSize": { "w": 32, "h": 32 },
144 | "duration": 200
145 | },
146 | "18": {
147 | "frame": { "x": 70, "y": 1, "w": 22, "h": 25 },
148 | "rotated": false,
149 | "trimmed": true,
150 | "spriteSourceSize": { "x": 8, "y": 7, "w": 22, "h": 25 },
151 | "sourceSize": { "w": 32, "h": 32 },
152 | "duration": 200
153 | },
154 | "19": {
155 | "frame": { "x": 46, "y": 1, "w": 23, "h": 25 },
156 | "rotated": false,
157 | "trimmed": true,
158 | "spriteSourceSize": { "x": 9, "y": 7, "w": 23, "h": 25 },
159 | "sourceSize": { "w": 32, "h": 32 },
160 | "duration": 200
161 | },
162 | "20": {
163 | "frame": { "x": 98, "y": 28, "w": 16, "h": 26 },
164 | "rotated": false,
165 | "trimmed": true,
166 | "spriteSourceSize": { "x": 8, "y": 6, "w": 16, "h": 26 },
167 | "sourceSize": { "w": 32, "h": 32 },
168 | "duration": 200
169 | },
170 | "21": {
171 | "frame": { "x": 1, "y": 60, "w": 16, "h": 25 },
172 | "rotated": false,
173 | "trimmed": true,
174 | "spriteSourceSize": { "x": 8, "y": 7, "w": 16, "h": 25 },
175 | "sourceSize": { "w": 32, "h": 32 },
176 | "duration": 200
177 | },
178 | "22": {
179 | "frame": { "x": 22, "y": 1, "w": 23, "h": 26 },
180 | "rotated": false,
181 | "trimmed": true,
182 | "spriteSourceSize": { "x": 7, "y": 6, "w": 23, "h": 26 },
183 | "sourceSize": { "w": 32, "h": 32 },
184 | "duration": 200
185 | },
186 | "23": {
187 | "frame": { "x": 1, "y": 1, "w": 20, "h": 31 },
188 | "rotated": false,
189 | "trimmed": true,
190 | "spriteSourceSize": { "x": 7, "y": 1, "w": 20, "h": 31 },
191 | "sourceSize": { "w": 32, "h": 32 },
192 | "duration": 200
193 | },
194 | "24": {
195 | "frame": { "x": 93, "y": 1, "w": 21, "h": 26 },
196 | "rotated": false,
197 | "trimmed": true,
198 | "spriteSourceSize": { "x": 6, "y": 5, "w": 21, "h": 26 },
199 | "sourceSize": { "w": 32, "h": 32 },
200 | "duration": 200
201 | },
202 | "25": {
203 | "frame": { "x": 46, "y": 27, "w": 17, "h": 26 },
204 | "rotated": false,
205 | "trimmed": true,
206 | "spriteSourceSize": { "x": 8, "y": 5, "w": 17, "h": 26 },
207 | "sourceSize": { "w": 32, "h": 32 },
208 | "duration": 200
209 | },
210 | "26": {
211 | "frame": { "x": 39, "y": 54, "w": 16, "h": 26 },
212 | "rotated": false,
213 | "trimmed": true,
214 | "spriteSourceSize": { "x": 8, "y": 5, "w": 16, "h": 26 },
215 | "sourceSize": { "w": 32, "h": 32 },
216 | "duration": 200
217 | },
218 | "27": {
219 | "frame": { "x": 81, "y": 28, "w": 16, "h": 26 },
220 | "rotated": false,
221 | "trimmed": true,
222 | "spriteSourceSize": { "x": 8, "y": 6, "w": 16, "h": 26 },
223 | "sourceSize": { "w": 32, "h": 32 },
224 | "duration": 200
225 | }
226 | },
227 | "meta": {
228 | "app": "https://www.aseprite.org/",
229 | "version": "1.3.7-x64",
230 | "image": "drow.png",
231 | "format": "RGBA8888",
232 | "size": { "w": 115, "h": 108 },
233 | "scale": "1",
234 | "frameTags": [
235 | { "name": "drow_walk_down", "from": 0, "to": 3, "direction": "forward", "color": "#000000ff" },
236 | { "name": "drow_idle_down", "from": 0, "to": 0, "direction": "forward", "color": "#000000ff" },
237 | { "name": "drow_walk_right", "from": 4, "to": 7, "direction": "forward", "color": "#000000ff" },
238 | { "name": "drow_idle_right", "from": 4, "to": 4, "direction": "forward", "color": "#000000ff" },
239 | { "name": "drow_walk_up", "from": 8, "to": 11, "direction": "forward", "color": "#000000ff" },
240 | { "name": "drow_idle_up", "from": 8, "to": 8, "direction": "forward", "color": "#000000ff" },
241 | { "name": "drow_atk_down", "from": 12, "to": 15, "direction": "forward", "color": "#000000ff" },
242 | { "name": "drow_atk_right", "from": 16, "to": 19, "direction": "forward", "color": "#000000ff" },
243 | { "name": "drow_atk_up", "from": 20, "to": 23, "direction": "forward", "color": "#000000ff" },
244 | { "name": "drow_hit", "from": 24, "to": 27, "direction": "forward", "color": "#000000ff" }
245 | ],
246 | "layers": [
247 | { "name": "Layer 2", "opacity": 255, "blendMode": "normal" },
248 | { "name": "left arm", "opacity": 255, "blendMode": "normal" },
249 | { "name": "body", "opacity": 255, "blendMode": "normal" },
250 | { "name": "right arm", "opacity": 255, "blendMode": "normal" },
251 | { "name": "Head", "opacity": 255, "blendMode": "normal" },
252 | { "name": "Hair", "opacity": 255, "blendMode": "normal" },
253 | { "name": "Layer 3", "opacity": 255, "blendMode": "normal" },
254 | { "name": "Layer 5", "opacity": 255, "blendMode": "normal" },
255 | { "name": "Layer 4", "opacity": 255, "blendMode": "normal" }
256 | ],
257 | "slices": [
258 | ]
259 | }
260 | }
261 |
--------------------------------------------------------------------------------
/public/assets/images/enemies/drow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devshareacademy/phaser-zelda-like-tutorial/63b62db25fb9e6ffdff6f54e5230ed304ddfe9cc/public/assets/images/enemies/drow.png
--------------------------------------------------------------------------------
/public/assets/images/enemies/enemy_death.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devshareacademy/phaser-zelda-like-tutorial/63b62db25fb9e6ffdff6f54e5230ed304ddfe9cc/public/assets/images/enemies/enemy_death.png
--------------------------------------------------------------------------------
/public/assets/images/enemies/spider.json:
--------------------------------------------------------------------------------
1 | { "frames": {
2 | "0": {
3 | "frame": { "x": 1, "y": 1, "w": 18, "h": 18 },
4 | "rotated": false,
5 | "trimmed": false,
6 | "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 16 },
7 | "sourceSize": { "w": 16, "h": 16 },
8 | "duration": 200
9 | },
10 | "1": {
11 | "frame": { "x": 20, "y": 1, "w": 18, "h": 18 },
12 | "rotated": false,
13 | "trimmed": false,
14 | "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 16 },
15 | "sourceSize": { "w": 16, "h": 16 },
16 | "duration": 200
17 | },
18 | "2": {
19 | "frame": { "x": 39, "y": 1, "w": 18, "h": 18 },
20 | "rotated": false,
21 | "trimmed": false,
22 | "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 16 },
23 | "sourceSize": { "w": 16, "h": 16 },
24 | "duration": 200
25 | },
26 | "3": {
27 | "frame": { "x": 1, "y": 20, "w": 18, "h": 18 },
28 | "rotated": false,
29 | "trimmed": false,
30 | "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 16 },
31 | "sourceSize": { "w": 16, "h": 16 },
32 | "duration": 200
33 | },
34 | "4": {
35 | "frame": { "x": 39, "y": 20, "w": 18, "h": 17 },
36 | "rotated": false,
37 | "trimmed": true,
38 | "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 15 },
39 | "sourceSize": { "w": 16, "h": 16 },
40 | "duration": 200
41 | },
42 | "5": {
43 | "frame": { "x": 39, "y": 38, "w": 18, "h": 17 },
44 | "rotated": false,
45 | "trimmed": true,
46 | "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 15 },
47 | "sourceSize": { "w": 16, "h": 16 },
48 | "duration": 200
49 | },
50 | "6": {
51 | "frame": { "x": 20, "y": 20, "w": 18, "h": 18 },
52 | "rotated": false,
53 | "trimmed": false,
54 | "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 16 },
55 | "sourceSize": { "w": 16, "h": 16 },
56 | "duration": 200
57 | },
58 | "7": {
59 | "frame": { "x": 1, "y": 1, "w": 18, "h": 18 },
60 | "rotated": false,
61 | "trimmed": false,
62 | "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 16 },
63 | "sourceSize": { "w": 16, "h": 16 },
64 | "duration": 200
65 | }
66 | },
67 | "meta": {
68 | "app": "https://www.aseprite.org/",
69 | "version": "1.3.7-x64",
70 | "image": "spider.png",
71 | "format": "RGBA8888",
72 | "size": { "w": 58, "h": 56 },
73 | "scale": "1",
74 | "frameTags": [
75 | { "name": "spider_walk", "from": 0, "to": 3, "direction": "forward", "color": "#000000ff" },
76 | { "name": "spider_hit", "from": 4, "to": 7, "direction": "forward", "color": "#000000ff" }
77 | ],
78 | "layers": [
79 | { "name": "Layer 1", "opacity": 255, "blendMode": "normal" },
80 | { "name": "Layer 2", "opacity": 255, "blendMode": "normal" }
81 | ],
82 | "slices": [
83 | ]
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/public/assets/images/enemies/spider.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devshareacademy/phaser-zelda-like-tutorial/63b62db25fb9e6ffdff6f54e5230ed304ddfe9cc/public/assets/images/enemies/spider.png
--------------------------------------------------------------------------------
/public/assets/images/enemies/wisp.json:
--------------------------------------------------------------------------------
1 | { "frames": {
2 | "0": {
3 | "frame": { "x": 1, "y": 1, "w": 18, "h": 18 },
4 | "rotated": false,
5 | "trimmed": false,
6 | "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 16 },
7 | "sourceSize": { "w": 16, "h": 16 },
8 | "duration": 50
9 | },
10 | "1": {
11 | "frame": { "x": 20, "y": 1, "w": 18, "h": 18 },
12 | "rotated": false,
13 | "trimmed": false,
14 | "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 16 },
15 | "sourceSize": { "w": 16, "h": 16 },
16 | "duration": 100
17 | },
18 | "2": {
19 | "frame": { "x": 39, "y": 1, "w": 18, "h": 18 },
20 | "rotated": false,
21 | "trimmed": false,
22 | "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 16 },
23 | "sourceSize": { "w": 16, "h": 16 },
24 | "duration": 100
25 | },
26 | "3": {
27 | "frame": { "x": 1, "y": 20, "w": 18, "h": 18 },
28 | "rotated": false,
29 | "trimmed": false,
30 | "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 16 },
31 | "sourceSize": { "w": 16, "h": 16 },
32 | "duration": 100
33 | },
34 | "4": {
35 | "frame": { "x": 20, "y": 20, "w": 18, "h": 18 },
36 | "rotated": false,
37 | "trimmed": false,
38 | "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 16 },
39 | "sourceSize": { "w": 16, "h": 16 },
40 | "duration": 100
41 | },
42 | "5": {
43 | "frame": { "x": 39, "y": 20, "w": 18, "h": 18 },
44 | "rotated": false,
45 | "trimmed": false,
46 | "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 16 },
47 | "sourceSize": { "w": 16, "h": 16 },
48 | "duration": 50
49 | }
50 | },
51 | "meta": {
52 | "app": "https://www.aseprite.org/",
53 | "version": "1.3.7-x64",
54 | "image": "wisp.png",
55 | "format": "RGBA8888",
56 | "size": { "w": 58, "h": 39 },
57 | "scale": "1",
58 | "frameTags": [
59 | { "name": "wisp_idle", "from": 0, "to": 5, "direction": "forward", "color": "#000000ff" }
60 | ],
61 | "layers": [
62 | { "name": "reference", "opacity": 255, "blendMode": "normal" },
63 | { "name": "wisp", "opacity": 255, "blendMode": "normal" }
64 | ],
65 | "slices": [
66 | ]
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/public/assets/images/enemies/wisp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devshareacademy/phaser-zelda-like-tutorial/63b62db25fb9e6ffdff6f54e5230ed304ddfe9cc/public/assets/images/enemies/wisp.png
--------------------------------------------------------------------------------
/public/assets/images/hud/heart_and_hud_numbers.json:
--------------------------------------------------------------------------------
1 | { "frames": {
2 | "0": {
3 | "frame": { "x": 40, "y": 1, "w": 6, "h": 9 },
4 | "rotated": false,
5 | "trimmed": true,
6 | "spriteSourceSize": { "x": 1, "y": 0, "w": 4, "h": 7 },
7 | "sourceSize": { "w": 7, "h": 7 },
8 | "duration": 100
9 | },
10 | "1": {
11 | "frame": { "x": 10, "y": 21, "w": 7, "h": 9 },
12 | "rotated": false,
13 | "trimmed": true,
14 | "spriteSourceSize": { "x": 1, "y": 0, "w": 5, "h": 7 },
15 | "sourceSize": { "w": 7, "h": 7 },
16 | "duration": 100
17 | },
18 | "2": {
19 | "frame": { "x": 31, "y": 1, "w": 8, "h": 9 },
20 | "rotated": false,
21 | "trimmed": true,
22 | "spriteSourceSize": { "x": 1, "y": 0, "w": 6, "h": 7 },
23 | "sourceSize": { "w": 7, "h": 7 },
24 | "duration": 100
25 | },
26 | "3": {
27 | "frame": { "x": 18, "y": 21, "w": 7, "h": 9 },
28 | "rotated": false,
29 | "trimmed": true,
30 | "spriteSourceSize": { "x": 1, "y": 0, "w": 5, "h": 7 },
31 | "sourceSize": { "w": 7, "h": 7 },
32 | "duration": 100
33 | },
34 | "4": {
35 | "frame": { "x": 1, "y": 11, "w": 8, "h": 9 },
36 | "rotated": false,
37 | "trimmed": true,
38 | "spriteSourceSize": { "x": 0, "y": 0, "w": 6, "h": 7 },
39 | "sourceSize": { "w": 7, "h": 7 },
40 | "duration": 100
41 | },
42 | "5": {
43 | "frame": { "x": 10, "y": 11, "w": 8, "h": 9 },
44 | "rotated": false,
45 | "trimmed": true,
46 | "spriteSourceSize": { "x": 1, "y": 0, "w": 6, "h": 7 },
47 | "sourceSize": { "w": 7, "h": 7 },
48 | "duration": 100
49 | },
50 | "6": {
51 | "frame": { "x": 19, "y": 11, "w": 8, "h": 9 },
52 | "rotated": false,
53 | "trimmed": true,
54 | "spriteSourceSize": { "x": 1, "y": 0, "w": 6, "h": 7 },
55 | "sourceSize": { "w": 7, "h": 7 },
56 | "duration": 100
57 | },
58 | "7": {
59 | "frame": { "x": 28, "y": 11, "w": 8, "h": 9 },
60 | "rotated": false,
61 | "trimmed": true,
62 | "spriteSourceSize": { "x": 1, "y": 0, "w": 6, "h": 7 },
63 | "sourceSize": { "w": 7, "h": 7 },
64 | "duration": 100
65 | },
66 | "8": {
67 | "frame": { "x": 37, "y": 11, "w": 8, "h": 9 },
68 | "rotated": false,
69 | "trimmed": true,
70 | "spriteSourceSize": { "x": 0, "y": 0, "w": 6, "h": 7 },
71 | "sourceSize": { "w": 7, "h": 7 },
72 | "duration": 100
73 | },
74 | "9": {
75 | "frame": { "x": 1, "y": 21, "w": 8, "h": 9 },
76 | "rotated": false,
77 | "trimmed": true,
78 | "spriteSourceSize": { "x": 1, "y": 0, "w": 6, "h": 7 },
79 | "sourceSize": { "w": 7, "h": 7 },
80 | "duration": 100
81 | },
82 | "10": {
83 | "frame": { "x": 1, "y": 1, "w": 9, "h": 9 },
84 | "rotated": false,
85 | "trimmed": false,
86 | "spriteSourceSize": { "x": 0, "y": 0, "w": 7, "h": 7 },
87 | "sourceSize": { "w": 7, "h": 7 },
88 | "duration": 100
89 | },
90 | "11": {
91 | "frame": { "x": 34, "y": 21, "w": 5, "h": 9 },
92 | "rotated": false,
93 | "trimmed": true,
94 | "spriteSourceSize": { "x": 2, "y": 0, "w": 3, "h": 7 },
95 | "sourceSize": { "w": 7, "h": 7 },
96 | "duration": 100
97 | },
98 | "12": {
99 | "frame": { "x": 11, "y": 1, "w": 9, "h": 9 },
100 | "rotated": false,
101 | "trimmed": false,
102 | "spriteSourceSize": { "x": 0, "y": 0, "w": 7, "h": 7 },
103 | "sourceSize": { "w": 7, "h": 7 },
104 | "duration": 100
105 | },
106 | "13": {
107 | "frame": { "x": 26, "y": 21, "w": 7, "h": 9 },
108 | "rotated": false,
109 | "trimmed": true,
110 | "spriteSourceSize": { "x": 1, "y": 0, "w": 5, "h": 7 },
111 | "sourceSize": { "w": 7, "h": 7 },
112 | "duration": 100
113 | },
114 | "14": {
115 | "frame": { "x": 21, "y": 1, "w": 9, "h": 9 },
116 | "rotated": false,
117 | "trimmed": false,
118 | "spriteSourceSize": { "x": 0, "y": 0, "w": 7, "h": 7 },
119 | "sourceSize": { "w": 7, "h": 7 },
120 | "duration": 100
121 | },
122 | "15": {
123 | "frame": { "x": 40, "y": 21, "w": 3, "h": 3 },
124 | "rotated": false,
125 | "trimmed": true,
126 | "spriteSourceSize": { "x": 0, "y": 0, "w": 1, "h": 1 },
127 | "sourceSize": { "w": 7, "h": 7 },
128 | "duration": 100
129 | }
130 | },
131 | "meta": {
132 | "app": "https://www.aseprite.org/",
133 | "version": "1.3.7-x64",
134 | "image": "heart_and_hud_numbers.png",
135 | "format": "RGBA8888",
136 | "size": { "w": 47, "h": 31 },
137 | "scale": "1",
138 | "frameTags": [
139 | { "name": "0", "from": 0, "to": 0, "direction": "forward", "color": "#000000ff" },
140 | { "name": "1", "from": 1, "to": 1, "direction": "forward", "color": "#000000ff" },
141 | { "name": "2", "from": 2, "to": 2, "direction": "forward", "color": "#000000ff" },
142 | { "name": "3", "from": 3, "to": 3, "direction": "forward", "color": "#000000ff" },
143 | { "name": "4", "from": 4, "to": 4, "direction": "forward", "color": "#000000ff" },
144 | { "name": "5", "from": 5, "to": 5, "direction": "forward", "color": "#000000ff" },
145 | { "name": "6", "from": 6, "to": 6, "direction": "forward", "color": "#000000ff" },
146 | { "name": "7", "from": 7, "to": 7, "direction": "forward", "color": "#000000ff" },
147 | { "name": "8", "from": 8, "to": 8, "direction": "forward", "color": "#000000ff" },
148 | { "name": "9", "from": 9, "to": 9, "direction": "forward", "color": "#000000ff" },
149 | { "name": "heart_lose_full", "from": 10, "to": 14, "direction": "forward", "color": "#000000ff" },
150 | { "name": "heart_lost_first_half", "from": 10, "to": 12, "direction": "forward", "color": "#000000ff" },
151 | { "name": "heart_full", "from": 10, "to": 10, "direction": "forward", "color": "#000000ff" },
152 | { "name": "heart_lose_last_half", "from": 12, "to": 14, "direction": "forward", "color": "#000000ff" },
153 | { "name": "heart_half", "from": 12, "to": 12, "direction": "forward", "color": "#000000ff" },
154 | { "name": "heart_empty", "from": 14, "to": 14, "direction": "forward", "color": "#000000ff" },
155 | { "name": "heart_none", "from": 15, "to": 15, "direction": "forward", "color": "#000000ff" }
156 | ],
157 | "layers": [
158 | { "name": "hud_numbers_and_hearts", "opacity": 255, "blendMode": "normal" }
159 | ],
160 | "slices": [
161 | ]
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/public/assets/images/hud/heart_and_hud_numbers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devshareacademy/phaser-zelda-like-tutorial/63b62db25fb9e6ffdff6f54e5230ed304ddfe9cc/public/assets/images/hud/heart_and_hud_numbers.png
--------------------------------------------------------------------------------
/public/assets/images/levels/common/collision.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devshareacademy/phaser-zelda-like-tutorial/63b62db25fb9e6ffdff6f54e5230ed304ddfe9cc/public/assets/images/levels/common/collision.png
--------------------------------------------------------------------------------
/public/assets/images/levels/common/dungeon_objects.json:
--------------------------------------------------------------------------------
1 | {
2 | "frames": {
3 | "big_chest_closed.png": {
4 | "frame": {
5 | "x": 0,
6 | "y": 0,
7 | "w": 32,
8 | "h": 32
9 | },
10 | "rotated": false,
11 | "trimmed": false,
12 | "spriteSourceSize": {
13 | "x": 0,
14 | "y": 0,
15 | "w": 32,
16 | "h": 32
17 | },
18 | "sourceSize": {
19 | "w": 32,
20 | "h": 32
21 | }
22 | },
23 | "big_chest_open.png": {
24 | "frame": {
25 | "x": 32,
26 | "y": 0,
27 | "w": 32,
28 | "h": 32
29 | },
30 | "rotated": false,
31 | "trimmed": false,
32 | "spriteSourceSize": {
33 | "x": 0,
34 | "y": 0,
35 | "w": 32,
36 | "h": 32
37 | },
38 | "sourceSize": {
39 | "w": 32,
40 | "h": 32
41 | }
42 | },
43 | "boss_down.png": {
44 | "frame": {
45 | "x": 64,
46 | "y": 0,
47 | "w": 32,
48 | "h": 32
49 | },
50 | "rotated": false,
51 | "trimmed": false,
52 | "spriteSourceSize": {
53 | "x": 0,
54 | "y": 0,
55 | "w": 32,
56 | "h": 32
57 | },
58 | "sourceSize": {
59 | "w": 32,
60 | "h": 32
61 | }
62 | },
63 | "boss_left.png": {
64 | "frame": {
65 | "x": 96,
66 | "y": 0,
67 | "w": 32,
68 | "h": 32
69 | },
70 | "rotated": false,
71 | "trimmed": false,
72 | "spriteSourceSize": {
73 | "x": 0,
74 | "y": 0,
75 | "w": 32,
76 | "h": 32
77 | },
78 | "sourceSize": {
79 | "w": 32,
80 | "h": 32
81 | }
82 | },
83 | "boss_right.png": {
84 | "frame": {
85 | "x": 128,
86 | "y": 0,
87 | "w": 32,
88 | "h": 32
89 | },
90 | "rotated": false,
91 | "trimmed": false,
92 | "spriteSourceSize": {
93 | "x": 0,
94 | "y": 0,
95 | "w": 32,
96 | "h": 32
97 | },
98 | "sourceSize": {
99 | "w": 32,
100 | "h": 32
101 | }
102 | },
103 | "boss_up.png": {
104 | "frame": {
105 | "x": 0,
106 | "y": 32,
107 | "w": 32,
108 | "h": 32
109 | },
110 | "rotated": false,
111 | "trimmed": false,
112 | "spriteSourceSize": {
113 | "x": 0,
114 | "y": 0,
115 | "w": 32,
116 | "h": 32
117 | },
118 | "sourceSize": {
119 | "w": 32,
120 | "h": 32
121 | }
122 | },
123 | "chest_closed.png": {
124 | "frame": {
125 | "x": 160,
126 | "y": 0,
127 | "w": 16,
128 | "h": 16
129 | },
130 | "rotated": false,
131 | "trimmed": false,
132 | "spriteSourceSize": {
133 | "x": 0,
134 | "y": 0,
135 | "w": 16,
136 | "h": 16
137 | },
138 | "sourceSize": {
139 | "w": 16,
140 | "h": 16
141 | }
142 | },
143 | "chest_open.png": {
144 | "frame": {
145 | "x": 160,
146 | "y": 16,
147 | "w": 16,
148 | "h": 16
149 | },
150 | "rotated": false,
151 | "trimmed": false,
152 | "spriteSourceSize": {
153 | "x": 0,
154 | "y": 0,
155 | "w": 16,
156 | "h": 16
157 | },
158 | "sourceSize": {
159 | "w": 16,
160 | "h": 16
161 | }
162 | },
163 | "floor_switch.png": {
164 | "frame": {
165 | "x": 32,
166 | "y": 32,
167 | "w": 16,
168 | "h": 16
169 | },
170 | "rotated": false,
171 | "trimmed": false,
172 | "spriteSourceSize": {
173 | "x": 0,
174 | "y": 0,
175 | "w": 16,
176 | "h": 16
177 | },
178 | "sourceSize": {
179 | "w": 16,
180 | "h": 16
181 | }
182 | },
183 | "lock_down.png": {
184 | "frame": {
185 | "x": 48,
186 | "y": 32,
187 | "w": 32,
188 | "h": 32
189 | },
190 | "rotated": false,
191 | "trimmed": false,
192 | "spriteSourceSize": {
193 | "x": 0,
194 | "y": 0,
195 | "w": 32,
196 | "h": 32
197 | },
198 | "sourceSize": {
199 | "w": 32,
200 | "h": 32
201 | }
202 | },
203 | "lock_left.png": {
204 | "frame": {
205 | "x": 80,
206 | "y": 32,
207 | "w": 32,
208 | "h": 32
209 | },
210 | "rotated": false,
211 | "trimmed": false,
212 | "spriteSourceSize": {
213 | "x": 0,
214 | "y": 0,
215 | "w": 32,
216 | "h": 32
217 | },
218 | "sourceSize": {
219 | "w": 32,
220 | "h": 32
221 | }
222 | },
223 | "lock_right.png": {
224 | "frame": {
225 | "x": 112,
226 | "y": 32,
227 | "w": 32,
228 | "h": 32
229 | },
230 | "rotated": false,
231 | "trimmed": false,
232 | "spriteSourceSize": {
233 | "x": 0,
234 | "y": 0,
235 | "w": 32,
236 | "h": 32
237 | },
238 | "sourceSize": {
239 | "w": 32,
240 | "h": 32
241 | }
242 | },
243 | "lock_up.png": {
244 | "frame": {
245 | "x": 144,
246 | "y": 32,
247 | "w": 32,
248 | "h": 32
249 | },
250 | "rotated": false,
251 | "trimmed": false,
252 | "spriteSourceSize": {
253 | "x": 0,
254 | "y": 0,
255 | "w": 32,
256 | "h": 32
257 | },
258 | "sourceSize": {
259 | "w": 32,
260 | "h": 32
261 | }
262 | },
263 | "open_down.png": {
264 | "frame": {
265 | "x": 0,
266 | "y": 64,
267 | "w": 32,
268 | "h": 32
269 | },
270 | "rotated": false,
271 | "trimmed": false,
272 | "spriteSourceSize": {
273 | "x": 0,
274 | "y": 0,
275 | "w": 32,
276 | "h": 32
277 | },
278 | "sourceSize": {
279 | "w": 32,
280 | "h": 32
281 | }
282 | },
283 | "open_left.png": {
284 | "frame": {
285 | "x": 32,
286 | "y": 64,
287 | "w": 32,
288 | "h": 32
289 | },
290 | "rotated": false,
291 | "trimmed": false,
292 | "spriteSourceSize": {
293 | "x": 0,
294 | "y": 0,
295 | "w": 32,
296 | "h": 32
297 | },
298 | "sourceSize": {
299 | "w": 32,
300 | "h": 32
301 | }
302 | },
303 | "open_right.png": {
304 | "frame": {
305 | "x": 64,
306 | "y": 64,
307 | "w": 32,
308 | "h": 32
309 | },
310 | "rotated": false,
311 | "trimmed": false,
312 | "spriteSourceSize": {
313 | "x": 0,
314 | "y": 0,
315 | "w": 32,
316 | "h": 32
317 | },
318 | "sourceSize": {
319 | "w": 32,
320 | "h": 32
321 | }
322 | },
323 | "open_up.png": {
324 | "frame": {
325 | "x": 96,
326 | "y": 64,
327 | "w": 32,
328 | "h": 32
329 | },
330 | "rotated": false,
331 | "trimmed": false,
332 | "spriteSourceSize": {
333 | "x": 0,
334 | "y": 0,
335 | "w": 32,
336 | "h": 32
337 | },
338 | "sourceSize": {
339 | "w": 32,
340 | "h": 32
341 | }
342 | },
343 | "plate_switch.png": {
344 | "frame": {
345 | "x": 128,
346 | "y": 64,
347 | "w": 16,
348 | "h": 16
349 | },
350 | "rotated": false,
351 | "trimmed": false,
352 | "spriteSourceSize": {
353 | "x": 0,
354 | "y": 0,
355 | "w": 16,
356 | "h": 16
357 | },
358 | "sourceSize": {
359 | "w": 16,
360 | "h": 16
361 | }
362 | },
363 | "plate_swtich_pressed.png": {
364 | "frame": {
365 | "x": 144,
366 | "y": 64,
367 | "w": 16,
368 | "h": 16
369 | },
370 | "rotated": false,
371 | "trimmed": false,
372 | "spriteSourceSize": {
373 | "x": 0,
374 | "y": 0,
375 | "w": 16,
376 | "h": 16
377 | },
378 | "sourceSize": {
379 | "w": 16,
380 | "h": 16
381 | }
382 | },
383 | "stair_down_down.png": {
384 | "frame": {
385 | "x": 128,
386 | "y": 80,
387 | "w": 32,
388 | "h": 32
389 | },
390 | "rotated": false,
391 | "trimmed": false,
392 | "spriteSourceSize": {
393 | "x": 0,
394 | "y": 0,
395 | "w": 32,
396 | "h": 32
397 | },
398 | "sourceSize": {
399 | "w": 32,
400 | "h": 32
401 | }
402 | },
403 | "stair_down_left.png": {
404 | "frame": {
405 | "x": 0,
406 | "y": 112,
407 | "w": 32,
408 | "h": 32
409 | },
410 | "rotated": false,
411 | "trimmed": false,
412 | "spriteSourceSize": {
413 | "x": 0,
414 | "y": 0,
415 | "w": 32,
416 | "h": 32
417 | },
418 | "sourceSize": {
419 | "w": 32,
420 | "h": 32
421 | }
422 | },
423 | "stair_down_right.png": {
424 | "frame": {
425 | "x": 32,
426 | "y": 112,
427 | "w": 32,
428 | "h": 32
429 | },
430 | "rotated": false,
431 | "trimmed": false,
432 | "spriteSourceSize": {
433 | "x": 0,
434 | "y": 0,
435 | "w": 32,
436 | "h": 32
437 | },
438 | "sourceSize": {
439 | "w": 32,
440 | "h": 32
441 | }
442 | },
443 | "stair_down_up.png": {
444 | "frame": {
445 | "x": 64,
446 | "y": 112,
447 | "w": 32,
448 | "h": 32
449 | },
450 | "rotated": false,
451 | "trimmed": false,
452 | "spriteSourceSize": {
453 | "x": 0,
454 | "y": 0,
455 | "w": 32,
456 | "h": 32
457 | },
458 | "sourceSize": {
459 | "w": 32,
460 | "h": 32
461 | }
462 | },
463 | "stair_downleft_down.png": {
464 | "frame": {
465 | "x": 96,
466 | "y": 112,
467 | "w": 32,
468 | "h": 32
469 | },
470 | "rotated": false,
471 | "trimmed": false,
472 | "spriteSourceSize": {
473 | "x": 0,
474 | "y": 0,
475 | "w": 32,
476 | "h": 32
477 | },
478 | "sourceSize": {
479 | "w": 32,
480 | "h": 32
481 | }
482 | },
483 | "stair_downleft_left.png": {
484 | "frame": {
485 | "x": 128,
486 | "y": 112,
487 | "w": 32,
488 | "h": 32
489 | },
490 | "rotated": false,
491 | "trimmed": false,
492 | "spriteSourceSize": {
493 | "x": 0,
494 | "y": 0,
495 | "w": 32,
496 | "h": 32
497 | },
498 | "sourceSize": {
499 | "w": 32,
500 | "h": 32
501 | }
502 | },
503 | "stair_downleft_right.png": {
504 | "frame": {
505 | "x": 0,
506 | "y": 144,
507 | "w": 32,
508 | "h": 32
509 | },
510 | "rotated": false,
511 | "trimmed": false,
512 | "spriteSourceSize": {
513 | "x": 0,
514 | "y": 0,
515 | "w": 32,
516 | "h": 32
517 | },
518 | "sourceSize": {
519 | "w": 32,
520 | "h": 32
521 | }
522 | },
523 | "stair_downleft_up.png": {
524 | "frame": {
525 | "x": 32,
526 | "y": 144,
527 | "w": 32,
528 | "h": 32
529 | },
530 | "rotated": false,
531 | "trimmed": false,
532 | "spriteSourceSize": {
533 | "x": 0,
534 | "y": 0,
535 | "w": 32,
536 | "h": 32
537 | },
538 | "sourceSize": {
539 | "w": 32,
540 | "h": 32
541 | }
542 | },
543 | "stair_upright_down.png": {
544 | "frame": {
545 | "x": 64,
546 | "y": 144,
547 | "w": 32,
548 | "h": 32
549 | },
550 | "rotated": false,
551 | "trimmed": false,
552 | "spriteSourceSize": {
553 | "x": 0,
554 | "y": 0,
555 | "w": 32,
556 | "h": 32
557 | },
558 | "sourceSize": {
559 | "w": 32,
560 | "h": 32
561 | }
562 | },
563 | "stair_upright_left.png": {
564 | "frame": {
565 | "x": 96,
566 | "y": 144,
567 | "w": 32,
568 | "h": 32
569 | },
570 | "rotated": false,
571 | "trimmed": false,
572 | "spriteSourceSize": {
573 | "x": 0,
574 | "y": 0,
575 | "w": 32,
576 | "h": 32
577 | },
578 | "sourceSize": {
579 | "w": 32,
580 | "h": 32
581 | }
582 | },
583 | "stair_upright_right.png": {
584 | "frame": {
585 | "x": 128,
586 | "y": 144,
587 | "w": 32,
588 | "h": 32
589 | },
590 | "rotated": false,
591 | "trimmed": false,
592 | "spriteSourceSize": {
593 | "x": 0,
594 | "y": 0,
595 | "w": 32,
596 | "h": 32
597 | },
598 | "sourceSize": {
599 | "w": 32,
600 | "h": 32
601 | }
602 | },
603 | "stair_upright_up.png": {
604 | "frame": {
605 | "x": 176,
606 | "y": 0,
607 | "w": 32,
608 | "h": 32
609 | },
610 | "rotated": false,
611 | "trimmed": false,
612 | "spriteSourceSize": {
613 | "x": 0,
614 | "y": 0,
615 | "w": 32,
616 | "h": 32
617 | },
618 | "sourceSize": {
619 | "w": 32,
620 | "h": 32
621 | }
622 | },
623 | "trap_down.png": {
624 | "frame": {
625 | "x": 176,
626 | "y": 32,
627 | "w": 32,
628 | "h": 32
629 | },
630 | "rotated": false,
631 | "trimmed": false,
632 | "spriteSourceSize": {
633 | "x": 0,
634 | "y": 0,
635 | "w": 32,
636 | "h": 32
637 | },
638 | "sourceSize": {
639 | "w": 32,
640 | "h": 32
641 | }
642 | },
643 | "trap_left.png": {
644 | "frame": {
645 | "x": 160,
646 | "y": 64,
647 | "w": 32,
648 | "h": 32
649 | },
650 | "rotated": false,
651 | "trimmed": false,
652 | "spriteSourceSize": {
653 | "x": 0,
654 | "y": 0,
655 | "w": 32,
656 | "h": 32
657 | },
658 | "sourceSize": {
659 | "w": 32,
660 | "h": 32
661 | }
662 | },
663 | "trap_right.png": {
664 | "frame": {
665 | "x": 160,
666 | "y": 96,
667 | "w": 32,
668 | "h": 32
669 | },
670 | "rotated": false,
671 | "trimmed": false,
672 | "spriteSourceSize": {
673 | "x": 0,
674 | "y": 0,
675 | "w": 32,
676 | "h": 32
677 | },
678 | "sourceSize": {
679 | "w": 32,
680 | "h": 32
681 | }
682 | },
683 | "trap_up.png": {
684 | "frame": {
685 | "x": 160,
686 | "y": 128,
687 | "w": 32,
688 | "h": 32
689 | },
690 | "rotated": false,
691 | "trimmed": false,
692 | "spriteSourceSize": {
693 | "x": 0,
694 | "y": 0,
695 | "w": 32,
696 | "h": 32
697 | },
698 | "sourceSize": {
699 | "w": 32,
700 | "h": 32
701 | }
702 | }
703 | },
704 | "meta": {
705 | "app": "http://www.codeandweb.com/texturepacker",
706 | "version": "1.0",
707 | "image": "spritesheet.png",
708 | "format": "RGBA8888",
709 | "size": {
710 | "w": 208,
711 | "h": 176
712 | },
713 | "scale": "1"
714 | }
715 | }
--------------------------------------------------------------------------------
/public/assets/images/levels/common/dungeon_objects.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devshareacademy/phaser-zelda-like-tutorial/63b62db25fb9e6ffdff6f54e5230ed304ddfe9cc/public/assets/images/levels/common/dungeon_objects.png
--------------------------------------------------------------------------------
/public/assets/images/levels/common/pot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devshareacademy/phaser-zelda-like-tutorial/63b62db25fb9e6ffdff6f54e5230ed304ddfe9cc/public/assets/images/levels/common/pot.png
--------------------------------------------------------------------------------
/public/assets/images/levels/common/pot_break.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devshareacademy/phaser-zelda-like-tutorial/63b62db25fb9e6ffdff6f54e5230ed304ddfe9cc/public/assets/images/levels/common/pot_break.png
--------------------------------------------------------------------------------
/public/assets/images/levels/dungeon_1/dungeon_1_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devshareacademy/phaser-zelda-like-tutorial/63b62db25fb9e6ffdff6f54e5230ed304ddfe9cc/public/assets/images/levels/dungeon_1/dungeon_1_background.png
--------------------------------------------------------------------------------
/public/assets/images/levels/dungeon_1/dungeon_1_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devshareacademy/phaser-zelda-like-tutorial/63b62db25fb9e6ffdff6f54e5230ed304ddfe9cc/public/assets/images/levels/dungeon_1/dungeon_1_foreground.png
--------------------------------------------------------------------------------
/public/assets/images/levels/world/world_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devshareacademy/phaser-zelda-like-tutorial/63b62db25fb9e6ffdff6f54e5230ed304ddfe9cc/public/assets/images/levels/world/world_background.png
--------------------------------------------------------------------------------
/public/assets/images/levels/world/world_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devshareacademy/phaser-zelda-like-tutorial/63b62db25fb9e6ffdff6f54e5230ed304ddfe9cc/public/assets/images/levels/world/world_foreground.png
--------------------------------------------------------------------------------
/public/assets/images/player/dagger.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devshareacademy/phaser-zelda-like-tutorial/63b62db25fb9e6ffdff6f54e5230ed304ddfe9cc/public/assets/images/player/dagger.png
--------------------------------------------------------------------------------
/public/assets/images/player/main_green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devshareacademy/phaser-zelda-like-tutorial/63b62db25fb9e6ffdff6f54e5230ed304ddfe9cc/public/assets/images/player/main_green.png
--------------------------------------------------------------------------------
/public/assets/images/ui/cursor_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devshareacademy/phaser-zelda-like-tutorial/63b62db25fb9e6ffdff6f54e5230ed304ddfe9cc/public/assets/images/ui/cursor_white.png
--------------------------------------------------------------------------------
/public/assets/images/ui/dialog_ui.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devshareacademy/phaser-zelda-like-tutorial/63b62db25fb9e6ffdff6f54e5230ed304ddfe9cc/public/assets/images/ui/dialog_ui.png
--------------------------------------------------------------------------------
/public/assets/images/ui/icons.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devshareacademy/phaser-zelda-like-tutorial/63b62db25fb9e6ffdff6f54e5230ed304ddfe9cc/public/assets/images/ui/icons.png
--------------------------------------------------------------------------------
/src/common/assets.ts:
--------------------------------------------------------------------------------
1 | export const ASSET_PACK_KEYS = {
2 | MAIN: 'MAIN',
3 | } as const;
4 |
5 | export const ASSET_KEYS = {
6 | PLAYER: 'PLAYER',
7 | POT: 'POT',
8 | POT_BREAK: 'POT_BREAK',
9 | SPIDER: 'SPIDER',
10 | WISP: 'WISP',
11 | DROW: 'DROW',
12 | DAGGER: 'DAGGER',
13 | DUNGEON_1_BACKGROUND: 'DUNGEON_1_BACKGROUND',
14 | DUNGEON_1_FOREGROUND: 'DUNGEON_1_FOREGROUND',
15 | DUNGEON_1_LEVEL: 'DUNGEON_1_LEVEL',
16 | COLLISION: 'COLLISION',
17 | DUNGEON_OBJECTS: 'DUNGEON_OBJECTS',
18 | ENEMY_DEATH: 'ENEMY_DEATH',
19 | UI_DIALOG: 'UI_DIALOG',
20 | UI_ICONS: 'UI_ICONS',
21 | UI_CURSOR: 'UI_CURSOR',
22 | WORLD_BACKGROUND: 'WORLD_BACKGROUND',
23 | WORLD_FOREGROUND: 'WORLD_FOREGROUND',
24 | WORLD_LEVEL: 'WORLD_LEVEL',
25 | HUD_NUMBERS: 'HUD_NUMBERS',
26 | FONT_PRESS_START_2P: 'FONT_PRESS_START_2P',
27 | } as const;
28 |
29 | export const PLAYER_ANIMATION_KEYS = {
30 | WALK_DOWN: 'player_walk_down',
31 | WALK_UP: 'player_walk_up',
32 | WALK_SIDE: 'player_walk_side',
33 | IDLE_DOWN: 'player_idle_down',
34 | IDLE_UP: 'player_idle_up',
35 | IDLE_SIDE: 'player_idle_side',
36 | IDLE_HOLD_DOWN: 'player_hand_in_air_down',
37 | IDLE_HOLD_UP: 'player_hand_in_air_up',
38 | IDLE_HOLD_SIDE: 'player_hand_in_air_side',
39 | WALK_HOLD_DOWN: 'player_walk_hand_in_air_down',
40 | WALK_HOLD_UP: 'player_walk_hand_in_air_up',
41 | WALK_HOLD_SIDE: 'player_walk_hand_in_air_side',
42 | LIFT_DOWN: 'player_open_chest_down',
43 | LIFT_UP: 'player_open_chest_up',
44 | LIFT_SIDE: 'player_open_chest_side',
45 | HURT_DOWN: 'player_hit_down',
46 | HURT_UP: 'player_hit_up',
47 | HURT_SIDE: 'player_hit_side',
48 | DIE_DOWN: 'player_die_down',
49 | DIE_UP: 'player_die_up',
50 | DIE_SIDE: 'player_die_side',
51 | SWORD_1_ATTACK_DOWN: 'player_atk_1_down',
52 | SWORD_1_ATTACK_UP: 'player_atk_1_up',
53 | SWORD_1_ATTACK_SIDE: 'player_atk_1_side',
54 | } as const;
55 |
56 | export const SPIDER_ANIMATION_KEYS = {
57 | WALK: 'spider_walk',
58 | HIT: 'spider_hit',
59 | DEATH: ASSET_KEYS.ENEMY_DEATH,
60 | } as const;
61 |
62 | export const DROW_ANIMATION_KEYS = {
63 | WALK_DOWN: 'drow_walk_down',
64 | WALK_UP: 'drow_walk_up',
65 | WALK_LEFT: 'drow_walk_left',
66 | WALK_RIGHT: 'drow_walk_right',
67 | IDLE_DOWN: 'drow_idle_down',
68 | IDLE_UP: 'drow_idle_up',
69 | IDLE_SIDE: 'drow_idle_right',
70 | HIT: 'drow_hit',
71 | ATTACK_DOWN: 'drow_atk_down',
72 | ATTACK_UP: 'drow_atk_up',
73 | ATTACK_SIDE: 'drow_atk_right',
74 | } as const;
75 |
76 | export const WISP_ANIMATION_KEYS = {
77 | IDLE: 'wisp_idle',
78 | } as const;
79 |
80 | export const CHARACTER_ANIMATIONS = {
81 | IDLE_DOWN: 'IDLE_DOWN',
82 | IDLE_UP: 'IDLE_UP',
83 | IDLE_LEFT: 'IDLE_LEFT',
84 | IDLE_RIGHT: 'IDLE_RIGHT',
85 | WALK_DOWN: 'WALK_DOWN',
86 | WALK_UP: 'WALK_UP',
87 | WALK_LEFT: 'WALK_LEFT',
88 | WALK_RIGHT: 'WALK_RIGHT',
89 | IDLE_HOLD_DOWN: 'IDLE_HOLD_DOWN',
90 | IDLE_HOLD_UP: 'IDLE_HOLD_UP',
91 | IDLE_HOLD_LEFT: 'IDLE_HOLD_LEFT',
92 | IDLE_HOLD_RIGHT: 'IDLE_HOLD_RIGHT',
93 | WALK_HOLD_DOWN: 'WALK_HOLD_DOWN',
94 | WALK_HOLD_UP: 'WALK_HOLD_UP',
95 | WALK_HOLD_LEFT: 'WALK_HOLD_LEFT',
96 | WALK_HOLD_RIGHT: 'WALK_HOLD_RIGHT',
97 | LIFT_DOWN: 'LIFT_DOWN',
98 | LIFT_UP: 'LIFT_UP',
99 | LIFT_LEFT: 'LIFT_LEFT',
100 | LIFT_RIGHT: 'LIFT_RIGHT',
101 | HURT_DOWN: 'HURT_DOWN',
102 | HURT_UP: 'HURT_UP',
103 | HURT_LEFT: 'HURT_LEFT',
104 | HURT_RIGHT: 'HURT_RIGHT',
105 | DIE_DOWN: 'DIE_DOWN',
106 | DIE_UP: 'DIE_UP',
107 | DIE_LEFT: 'DIE_LEFT',
108 | DIE_RIGHT: 'DIE_RIGHT',
109 | } as const;
110 |
111 | export const CHEST_FRAME_KEYS = {
112 | BIG_CHEST_CLOSED: 'big_chest_closed.png',
113 | SMALL_CHEST_CLOSED: 'chest_closed.png',
114 | BIG_CHEST_OPEN: 'big_chest_open.png',
115 | SMALL_CHEST_OPEN: 'chest_open.png',
116 | } as const;
117 |
118 | export const DOOR_FRAME_KEYS = {
119 | TRAP_LEFT: 'trap_left.png',
120 | TRAP_RIGHT: 'trap_right.png',
121 | TRAP_UP: 'trap_up.png',
122 | TRAP_DOWN: 'trap_down.png',
123 | BOSS_LEFT: 'boss_left.png',
124 | BOSS_RIGHT: 'boss_right.png',
125 | BOSS_UP: 'boss_up.png',
126 | BOSS_DOWN: 'boss_down.png',
127 | LOCK_LEFT: 'lock_left.png',
128 | LOCK_RIGHT: 'lock_right.png',
129 | LOCK_UP: 'lock_up.png',
130 | LOCK_DOWN: 'lock_down.png',
131 | } as const;
132 |
133 | export const BUTTON_FRAME_KEYS = {
134 | FLOOR_SWITCH: 'floor_switch.png',
135 | PLATE_SWITCH: 'plate_switch.png',
136 | } as const;
137 |
138 | export const CHEST_REWARD_TO_TEXTURE_FRAME = {
139 | SMALL_KEY: 119,
140 | BOSS_KEY: 121,
141 | MAP: 117,
142 | COMPASS: 118,
143 | NOTHING: 126,
144 | } as const;
145 |
146 | export const HEART_ANIMATIONS = {
147 | LOSE_LAST_HALF: 'heart_lose_last_half',
148 | LOSE_FIRST_HALF: 'heart_lost_first_half',
149 | };
150 |
151 | export const HEART_TEXTURE_FRAME = {
152 | NONE: '15',
153 | FULL: '10',
154 | EMPTY: '14',
155 | HALF: '12',
156 | } as const;
157 |
--------------------------------------------------------------------------------
/src/common/common.ts:
--------------------------------------------------------------------------------
1 | import * as Phaser from 'phaser';
2 | import { ASSET_KEYS } from './assets';
3 |
4 | export const DIRECTION = {
5 | UP: 'UP',
6 | DOWN: 'DOWN',
7 | LEFT: 'LEFT',
8 | RIGHT: 'RIGHT',
9 | } as const;
10 |
11 | export const CHEST_STATE = {
12 | HIDDEN: 'HIDDEN',
13 | REVEALED: 'REVEALED',
14 | OPEN: 'OPEN',
15 | } as const;
16 |
17 | export const INTERACTIVE_OBJECT_TYPE = {
18 | AUTO: 'AUTO',
19 | PICKUP: 'PICKUP',
20 | OPEN: 'OPEN',
21 | } as const;
22 |
23 | export const LEVEL_NAME = {
24 | WORLD: 'WORLD',
25 | DUNGEON_1: 'DUNGEON_1',
26 | } as const;
27 |
28 | export const DUNGEON_ITEM = {
29 | SMALL_KEY: 'SMALL_KEY',
30 | BOSS_KEY: 'BOSS_KEY',
31 | MAP: 'MAP',
32 | COMPASS: 'COMPASS',
33 | } as const;
34 |
35 | export const DEFAULT_UI_TEXT_STYLE: Phaser.Types.GameObjects.Text.TextStyle = {
36 | align: 'center',
37 | fontFamily: ASSET_KEYS.FONT_PRESS_START_2P,
38 | fontSize: 8,
39 | wordWrap: { width: 170 },
40 | color: '#FFFFFF',
41 | };
42 |
43 | export const CHEST_REWARD_TO_DIALOG_MAP = {
44 | SMALL_KEY: 'You found a small key! You can use this to open locked doors.',
45 | BOSS_KEY:
46 | 'You got the Big Key! This is the master key of the dungeon. It can open many locks that small keys cannot.',
47 | MAP: 'You got the Map! You can use it to see your current position and the rest of the dungeon (Press X).',
48 | COMPASS: "You fond the Compass! Now you can pinpoint the lair of the dungeon's evil master!",
49 | NOTHING: '...The chest was empty!',
50 | } as const;
51 |
--------------------------------------------------------------------------------
/src/common/config.ts:
--------------------------------------------------------------------------------
1 | export const ENABLE_LOGGING = false;
2 | export const ENABLE_DEBUG_ZONE_AREA = false;
3 | export const DEBUG_COLLISION_ALPHA = 0;
4 |
5 | export const PLAYER_SPEED = 80;
6 | export const PLAYER_INVULNERABLE_AFTER_HIT_DURATION = 1000;
7 | export const PLAYER_HURT_PUSH_BACK_SPEED = 50;
8 | export const PLAYER_START_MAX_HEALTH = 6;
9 | export const PLAYER_ATTACK_DAMAGE = 1;
10 |
11 | export const ENEMY_SPIDER_SPEED = 80;
12 | export const ENEMY_SPIDER_CHANGE_DIRECTION_DELAY_MIN = 500;
13 | export const ENEMY_SPIDER_CHANGE_DIRECTION_DELAY_MAX = 1500;
14 | export const ENEMY_SPIDER_CHANGE_DIRECTION_DELAY_WAIT = 200;
15 | export const ENEMY_SPIDER_HURT_PUSH_BACK_SPEED = 50;
16 | export const ENEMY_SPIDER_MAX_HEALTH = 2;
17 |
18 | export const ENEMY_WISP_SPEED = 50;
19 | export const ENEMY_WISP_PULSE_ANIMATION_SCALE_X = 1.2;
20 | export const ENEMY_WISP_PULSE_ANIMATION_SCALE_Y = 1.2;
21 | export const ENEMY_WISP_PULSE_ANIMATION_DURATION = 500;
22 | export const ENEMY_WISP_MAX_HEALTH = 1;
23 |
24 | export const ENEMY_BOSS_DROW_SPEED = 80;
25 | export const ENEMY_BOSS_DROW_MAX_HEALTH = 6;
26 | export const ENEMY_BOSS_DROW_DEATH_ANIMATION_DURATION = 3000;
27 | export const ENEMY_BOSS_IDLE_STATE_DURATION = 3000;
28 | export const ENEMY_BOSS_HIDDEN_STATE_DURATION = 1000;
29 | export const ENEMY_BOSS_TELEPORT_STATE_INITIAL_DELAY = 150;
30 | export const ENEMY_BOSS_TELEPORT_STATE_FINISHED_DELAY = 500;
31 | export const ENEMY_BOSS_PREPARE_ATTACK_STATE_FINISHED_DELAY = 500;
32 | export const ENEMY_BOSS_ATTACK_DAMAGE = 1;
33 | export const ENEMY_BOSS_ATTACK_SPEED = 160;
34 | export const ENEMY_BOSS_START_INITIAL_DELAY = 1000;
35 |
36 | export const HURT_PUSH_BACK_DELAY = 200;
37 | export const BOSS_HURT_PUSH_BACK_DELAY = 50;
38 |
39 | export const THROW_ITEM_SPEED = 300;
40 | export const THROW_ITEM_DELAY_BEFORE_CALLBACK = 200;
41 |
42 | export const LIFT_ITEM_ANIMATION_DELAY = 0;
43 | export const LIFT_ITEM_ANIMATION_DURATION = 250;
44 | export const LIFT_ITEM_ANIMATION_ENABLE_DEBUGGING = false;
45 |
46 | export const ROOM_TRANSITION_PLAYER_INTO_HALL_DURATION = 750;
47 | export const ROOM_TRANSITION_PLAYER_INTO_HALL_DELAY = 250;
48 | export const ROOM_TRANSITION_PLAYER_INTO_NEXT_ROOM_DURATION = 1000;
49 | export const ROOM_TRANSITION_PLAYER_INTO_NEXT_ROOM_DELAY = 1200;
50 | export const ROOM_TRANSITION_CAMERA_ANIMATION_DURATION = 1000;
51 | export const ROOM_TRANSITION_CAMERA_ANIMATION_DELAY = 500;
52 |
--------------------------------------------------------------------------------
/src/common/data-manager.ts:
--------------------------------------------------------------------------------
1 | import { LEVEL_NAME } from './common';
2 | import { PLAYER_START_MAX_HEALTH } from './config';
3 | import {
4 | CUSTOM_EVENTS,
5 | EVENT_BUS,
6 | PLAYER_HEALTH_UPDATE_TYPE,
7 | PlayerHealthUpdated,
8 | PlayerHealthUpdateType,
9 | } from './event-bus';
10 | import { LevelName } from './types';
11 |
12 | export type PlayerData = {
13 | currentHealth: number;
14 | maxHealth: number;
15 | currentArea: {
16 | name: LevelName;
17 | startRoomId: number;
18 | startDoorId: number;
19 | };
20 | areaDetails: {
21 | [key in LevelName]: {
22 | [key: number]: {
23 | chests: {
24 | [key: string]: {
25 | revealed: boolean;
26 | opened: boolean;
27 | };
28 | };
29 | doors: {
30 | [key: string]: {
31 | unlocked: boolean;
32 | };
33 | };
34 | };
35 | bossDefeated?: boolean;
36 | };
37 | };
38 | };
39 |
40 | export class DataManager {
41 | static #instance: DataManager;
42 |
43 | #data: PlayerData;
44 |
45 | private constructor() {
46 | this.#data = {
47 | currentHealth: PLAYER_START_MAX_HEALTH,
48 | maxHealth: PLAYER_START_MAX_HEALTH,
49 | currentArea: {
50 | name: LEVEL_NAME.DUNGEON_1,
51 | startRoomId: 3,
52 | startDoorId: 3,
53 | },
54 | areaDetails: {
55 | DUNGEON_1: {
56 | bossDefeated: false,
57 | },
58 | WORLD: {},
59 | },
60 | };
61 | }
62 |
63 | public static get instance(): DataManager {
64 | if (!DataManager.#instance) {
65 | DataManager.#instance = new DataManager();
66 | }
67 | return DataManager.#instance;
68 | }
69 |
70 | get data(): PlayerData {
71 | return { ...this.#data };
72 | }
73 |
74 | set data(data: PlayerData) {
75 | this.#data = { ...data };
76 | }
77 |
78 | public updateAreaData(area: LevelName, startRoomId: number, startDoorId: number): void {
79 | this.#data.currentArea = {
80 | name: area,
81 | startDoorId,
82 | startRoomId,
83 | };
84 | }
85 |
86 | public updateChestData(roomId: number, chestId: number, revealed: boolean, opened: boolean): void {
87 | this.#populateDefaultRoomData(roomId);
88 | this.#data.areaDetails[this.#data.currentArea.name][roomId].chests[chestId] = {
89 | revealed,
90 | opened,
91 | };
92 | }
93 |
94 | public updateDoorData(roomId: number, doorId: number, unlocked: boolean): void {
95 | this.#populateDefaultRoomData(roomId);
96 | this.#data.areaDetails[this.#data.currentArea.name][roomId].doors[doorId] = {
97 | unlocked,
98 | };
99 | }
100 |
101 | public resetPlayerHealthToMin(): void {
102 | this.#data.currentHealth = PLAYER_START_MAX_HEALTH;
103 | }
104 |
105 | public updatePlayerCurrentHealth(health: number): void {
106 | if (health === this.#data.currentHealth) {
107 | return;
108 | }
109 | let healthUpdateType: PlayerHealthUpdateType = PLAYER_HEALTH_UPDATE_TYPE.DECREASE;
110 | if (health > this.#data.currentHealth) {
111 | healthUpdateType = PLAYER_HEALTH_UPDATE_TYPE.INCREASE;
112 | }
113 | const dataToPass: PlayerHealthUpdated = {
114 | previousHealth: this.#data.currentHealth,
115 | currentHealth: health,
116 | type: healthUpdateType,
117 | };
118 | EVENT_BUS.emit(CUSTOM_EVENTS.PLAYER_HEALTH_UPDATED, dataToPass);
119 | this.#data.currentHealth = health;
120 | }
121 |
122 | public defeatedCurrentAreaBoss(): void {
123 | this.#data.areaDetails[this.#data.currentArea.name].bossDefeated = true;
124 | }
125 |
126 | #populateDefaultRoomData(roomId: number): void {
127 | if (this.#data.areaDetails[this.#data.currentArea.name][roomId] === undefined) {
128 | this.#data.areaDetails[this.#data.currentArea.name][roomId] = {
129 | chests: {},
130 | doors: {},
131 | };
132 | }
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/src/common/event-bus.ts:
--------------------------------------------------------------------------------
1 | import * as Phaser from 'phaser';
2 |
3 | export const EVENT_BUS = new Phaser.Events.EventEmitter();
4 |
5 | export const CUSTOM_EVENTS = {
6 | OPENED_CHEST: 'OPENED_CHEST',
7 | ENEMY_DESTROYED: 'ENEMY_DESTROYED',
8 | PLAYER_DEFEATED: 'PLAYER_DEFEATED',
9 | PLAYER_HEALTH_UPDATED: 'PLAYER_HEALTH_UPDATED',
10 | SHOW_DIALOG: 'SHOW_DIALOG',
11 | DIALOG_CLOSED: 'DIALOG_CLOSED',
12 | BOSS_DEFEATED: 'BOSS_DEFEATED',
13 | } as const;
14 |
15 | export const PLAYER_HEALTH_UPDATE_TYPE = {
16 | INCREASE: 'INCREASE',
17 | DECREASE: 'DECREASE',
18 | } as const;
19 |
20 | export type PlayerHealthUpdateType = keyof typeof PLAYER_HEALTH_UPDATE_TYPE;
21 |
22 | export type PlayerHealthUpdated = {
23 | currentHealth: number;
24 | previousHealth: number;
25 | type: PlayerHealthUpdateType;
26 | };
27 |
--------------------------------------------------------------------------------
/src/common/juice-utils.ts:
--------------------------------------------------------------------------------
1 | import * as Phaser from 'phaser';
2 |
3 | /**
4 | * Creates a flash animation effect by using the built in Phaser 3 Timer Events. The provided game object
5 | * will be the target of the effect that is created.
6 | * @param {Phaser.GameObjects.Image | Phaser.GameObjects.Sprite} target The target game object that the effect will be applied to.
7 | * @param {() => void} [callback] The callback that will be invoked when the tween is finished
8 | * @returns {void}
9 | */
10 | export function flash(target: Phaser.GameObjects.Image | Phaser.GameObjects.Sprite, callback?: () => void): void {
11 | const timeEvent = target.scene.time.addEvent({
12 | delay: 250,
13 | callback: () => {
14 | target.setTintFill(0xffffff);
15 | target.setAlpha(0.7);
16 |
17 | target.scene.time.addEvent({
18 | delay: 150,
19 | callback: () => {
20 | target.setTint(0xffffff);
21 | target.setAlpha(1);
22 | if (timeEvent.getOverallProgress() === 1 && callback) {
23 | callback();
24 | }
25 | },
26 | });
27 | },
28 | startAt: 150,
29 | repeat: 3,
30 | });
31 | }
32 |
--------------------------------------------------------------------------------
/src/common/tiled/common.ts:
--------------------------------------------------------------------------------
1 | export const TILED_ROOM_OBJECT_PROPERTY = {
2 | ID: 'id',
3 | } as const;
4 |
5 | export const TILED_LAYER_NAMES = {
6 | ROOMS: 'rooms',
7 | SWITCHES: 'switches',
8 | POTS: 'pots',
9 | DOORS: 'doors',
10 | CHESTS: 'chests',
11 | ENEMIES: 'enemies',
12 | COLLISION: 'collision',
13 | ENEMY_COLLISION: 'enemy_collision',
14 | } as const;
15 |
16 | export const TILED_TILESET_NAMES = {
17 | COLLISION: 'collision',
18 | } as const;
19 |
20 | export const DOOR_TYPE = {
21 | OPEN: 'OPEN',
22 | LOCK: 'LOCK',
23 | TRAP: 'TRAP',
24 | BOSS: 'BOSS',
25 | OPEN_ENTRANCE: 'OPEN_ENTRANCE',
26 | } as const;
27 |
28 | export const TRAP_TYPE = {
29 | NONE: 'NONE',
30 | ENEMIES_DEFEATED: 'ENEMIES_DEFEATED',
31 | SWITCH: 'SWITCH',
32 | BOSS_DEFEATED: 'BOSS_DEFEATED',
33 | } as const;
34 |
35 | export const TILED_DOOR_OBJECT_PROPERTY = {
36 | TARGET_DOOR_ID: 'targetDoorId',
37 | TARGET_ROOM_ID: 'targetRoomId',
38 | TARGET_LEVEL: 'targetLevel',
39 | ID: 'id',
40 | DIRECTION: 'direction',
41 | DOOR_TYPE: 'doorType',
42 | TRAP_DOOR_TRIGGER: 'trapDoorTrigger',
43 | IS_LEVEL_TRANSITION: 'isLevelTransition',
44 | } as const;
45 |
46 | export const CHEST_REWARD = {
47 | SMALL_KEY: 'SMALL_KEY',
48 | BOSS_KEY: 'BOSS_KEY',
49 | MAP: 'MAP',
50 | COMPASS: 'COMPASS',
51 | NOTHING: 'NOTHING',
52 | } as const;
53 |
54 | export const TILED_CHEST_OBJECT_PROPERTY = {
55 | CONTENTS: 'contents',
56 | ID: 'id',
57 | REVEAL_CHEST_TRIGGER: 'revealChestTrigger',
58 | REQUIRES_BOSS_KEY: 'requiresBossKey',
59 | } as const;
60 |
61 | export const TILED_SWITCH_OBJECT_PROPERTY = {
62 | TARGET_IDS: 'targetIds',
63 | ACTION: 'action',
64 | TEXTURE: 'texture',
65 | } as const;
66 |
67 | export const SWITCH_TEXTURE = {
68 | PLATE: 'PLATE',
69 | FLOOR: 'FLOOR',
70 | } as const;
71 |
72 | export const SWITCH_ACTION = {
73 | NOTHING: 'NOTHING',
74 | OPEN_DOOR: 'OPEN_DOOR',
75 | REVEAL_CHEST: 'REVEAL_CHEST',
76 | REVEAL_KEY: 'REVEAL_KEY',
77 | } as const;
78 |
--------------------------------------------------------------------------------
/src/common/tiled/tiled-utils.ts:
--------------------------------------------------------------------------------
1 | import * as Phaser from 'phaser';
2 | import {
3 | ChestReward,
4 | DoorType,
5 | SwitchAction,
6 | SwitchTexture,
7 | TILED_ENEMY_OBJECT_PROPERTY,
8 | TiledChestObject,
9 | TiledDoorObject,
10 | TiledEnemyObject,
11 | TiledObjectProperty,
12 | TiledObjectWithProperties,
13 | TiledPotObject,
14 | TiledRoomObject,
15 | TiledSwitchObject,
16 | TrapType,
17 | } from './types';
18 | import {
19 | CHEST_REWARD,
20 | DOOR_TYPE,
21 | SWITCH_ACTION,
22 | SWITCH_TEXTURE,
23 | TILED_CHEST_OBJECT_PROPERTY,
24 | TILED_DOOR_OBJECT_PROPERTY,
25 | TILED_ROOM_OBJECT_PROPERTY,
26 | TILED_SWITCH_OBJECT_PROPERTY,
27 | TRAP_TYPE,
28 | } from './common';
29 | import { isDirection } from '../utils';
30 | import { LevelName } from '../types';
31 |
32 | /**
33 | * Validates that the provided property is of the type TiledObjectProperty.
34 | */
35 | export function isTiledObjectProperty(property: unknown): property is TiledObjectProperty {
36 | if (typeof property !== 'object' || property === null || property === undefined) {
37 | return false;
38 | }
39 | return property['name'] !== undefined && property['type'] !== undefined && property['value'] !== undefined;
40 | }
41 |
42 | /**
43 | * Returns an array of validated TiledObjectProperty objects from the provided Phaser Tiled Object properties.
44 | */
45 | export function getTiledProperties(properties: unknown): TiledObjectProperty[] {
46 | const validProperties: TiledObjectProperty[] = [];
47 | if (typeof properties !== 'object' || properties === null || properties === undefined || !Array.isArray(properties)) {
48 | return validProperties;
49 | }
50 | properties.forEach((property) => {
51 | if (!isTiledObjectProperty(property)) {
52 | return;
53 | }
54 | validProperties.push(property);
55 | });
56 | return validProperties;
57 | }
58 |
59 | /**
60 | * Returns the value of the given Tiled property name on an object. In Tiled the object properties are
61 | * stored on an array, and we need to loop through the Array to find the property we are looking for.
62 | */
63 | export function getTiledPropertyByName(properties: TiledObjectProperty[], propertyName: string): T | undefined {
64 | const tiledProperty = properties.find((prop) => {
65 | return prop.name === propertyName;
66 | });
67 | if (tiledProperty === undefined) {
68 | return undefined;
69 | }
70 | return tiledProperty.value as T;
71 | }
72 |
73 | /**
74 | * Finds all of the Tiled Objects for a given layer of a Tilemap, and filters to only objects that include
75 | * the basic properties for an objects position, width, and height.
76 | */
77 | export function getTiledObjectsFromLayer(map: Phaser.Tilemaps.Tilemap, layerName: string): TiledObjectWithProperties[] {
78 | const validTiledObjects: TiledObjectWithProperties[] = [];
79 | // get the Tiled object layer by its name
80 | const tiledObjectLayer = map.getObjectLayer(layerName);
81 | if (!tiledObjectLayer) {
82 | return validTiledObjects;
83 | }
84 |
85 | // loop through each object and validate object has basic properties for position, width, height, etc
86 | const tiledObjects = tiledObjectLayer.objects;
87 | tiledObjects.forEach((tiledObject) => {
88 | if (
89 | tiledObject.x === undefined ||
90 | tiledObject.y === undefined ||
91 | tiledObject.width === undefined ||
92 | tiledObject.height === undefined
93 | ) {
94 | return;
95 | }
96 | validTiledObjects.push({
97 | x: tiledObject.x,
98 | y: tiledObject.y,
99 | width: tiledObject.width,
100 | height: tiledObject.height,
101 | properties: getTiledProperties(tiledObject.properties),
102 | });
103 | });
104 |
105 | return validTiledObjects;
106 | }
107 |
108 | /**
109 | * Finds all of the valid 'Room' Tiled Objects on a given layer of a Tilemap.
110 | */
111 | export function getTiledRoomObjectsFromMap(map: Phaser.Tilemaps.Tilemap, layerName: string): TiledRoomObject[] {
112 | const roomObjects: TiledRoomObject[] = [];
113 |
114 | // loop through each object and validate object has properties for the object we are planning to build
115 | const tiledObjects = getTiledObjectsFromLayer(map, layerName);
116 | tiledObjects.forEach((tiledObject) => {
117 | const id = getTiledPropertyByName(tiledObject.properties, TILED_ROOM_OBJECT_PROPERTY.ID);
118 | if (id === undefined) {
119 | return;
120 | }
121 |
122 | roomObjects.push({
123 | x: tiledObject.x,
124 | y: tiledObject.y,
125 | width: tiledObject.width,
126 | height: tiledObject.height,
127 | id,
128 | });
129 | });
130 |
131 | return roomObjects;
132 | }
133 |
134 | /**
135 | * Parses the provided Phaser Tilemap and returns all Object layer names with the provided prefix.
136 | * This function expects the layer names to be in a format like: rooms/1/enemies.
137 | */
138 | export function getAllLayerNamesWithPrefix(map: Phaser.Tilemaps.Tilemap, prefix: string): string[] {
139 | return map
140 | .getObjectLayerNames()
141 | .filter((layerName) => layerName.startsWith(`${prefix}/`))
142 | .filter((layerName) => {
143 | const layerData = layerName.split('/');
144 | if (layerData.length !== 3) {
145 | return false;
146 | }
147 | return true;
148 | });
149 | }
150 |
151 | /**
152 | * Finds all of the valid 'Door' Tiled Objects on a given layer of a Tilemap.
153 | */
154 | export function getTiledDoorObjectsFromMap(map: Phaser.Tilemaps.Tilemap, layerName: string): TiledDoorObject[] {
155 | const doorObjects: TiledDoorObject[] = [];
156 |
157 | // loop through each object and validate object has properties for the object we are planning to build
158 | const tiledObjects = getTiledObjectsFromLayer(map, layerName);
159 | tiledObjects.forEach((tiledObject) => {
160 | const doorId = getTiledPropertyByName(tiledObject.properties, TILED_DOOR_OBJECT_PROPERTY.ID);
161 | const targetDoorId = getTiledPropertyByName(
162 | tiledObject.properties,
163 | TILED_DOOR_OBJECT_PROPERTY.TARGET_DOOR_ID,
164 | );
165 | const doorDirection = getTiledPropertyByName(tiledObject.properties, TILED_DOOR_OBJECT_PROPERTY.DIRECTION);
166 | const doorType = getTiledPropertyByName(tiledObject.properties, TILED_DOOR_OBJECT_PROPERTY.DOOR_TYPE);
167 | const trapDoorType = getTiledPropertyByName(
168 | tiledObject.properties,
169 | TILED_DOOR_OBJECT_PROPERTY.TRAP_DOOR_TRIGGER,
170 | );
171 | const isLevelTransition = getTiledPropertyByName(
172 | tiledObject.properties,
173 | TILED_DOOR_OBJECT_PROPERTY.IS_LEVEL_TRANSITION,
174 | );
175 | const targetLevel = getTiledPropertyByName(
176 | tiledObject.properties,
177 | TILED_DOOR_OBJECT_PROPERTY.TARGET_LEVEL,
178 | );
179 | const targetRoomId = getTiledPropertyByName(
180 | tiledObject.properties,
181 | TILED_DOOR_OBJECT_PROPERTY.TARGET_ROOM_ID,
182 | );
183 | if (
184 | doorId === undefined ||
185 | targetDoorId === undefined ||
186 | doorDirection === undefined ||
187 | doorType === undefined ||
188 | trapDoorType === undefined ||
189 | isLevelTransition === undefined ||
190 | targetLevel === undefined ||
191 | targetRoomId === undefined ||
192 | !isDirection(doorDirection) ||
193 | !isDoorType(doorType) ||
194 | !isTrapType(trapDoorType)
195 | ) {
196 | return;
197 | }
198 |
199 | doorObjects.push({
200 | x: tiledObject.x,
201 | y: tiledObject.y,
202 | width: tiledObject.width,
203 | height: tiledObject.height,
204 | id: doorId,
205 | targetDoorId: targetDoorId,
206 | direction: doorDirection,
207 | doorType: doorType,
208 | trapDoorTrigger: trapDoorType,
209 | isLevelTransition,
210 | targetLevel,
211 | targetRoomId,
212 | isUnlocked: false,
213 | });
214 | });
215 |
216 | return doorObjects;
217 | }
218 |
219 | export function isDoorType(doorType: string): doorType is DoorType {
220 | return DOOR_TYPE[doorType] !== undefined;
221 | }
222 |
223 | export function isTrapType(trapType: string): trapType is TrapType {
224 | return TRAP_TYPE[trapType] !== undefined;
225 | }
226 |
227 | /**
228 | * Finds all of the valid 'Pot' Tiled Objects on a given layer of a Tilemap.
229 | */
230 | export function getTiledPotObjectsFromMap(map: Phaser.Tilemaps.Tilemap, layerName: string): TiledPotObject[] {
231 | const potObjects: TiledPotObject[] = [];
232 |
233 | // loop through each object and validate object has properties for the object we are planning to build
234 | const tiledObjects = getTiledObjectsFromLayer(map, layerName);
235 | tiledObjects.forEach((tiledObject) => {
236 | potObjects.push({
237 | x: tiledObject.x,
238 | y: tiledObject.y,
239 | width: tiledObject.width,
240 | height: tiledObject.height,
241 | });
242 | });
243 |
244 | return potObjects;
245 | }
246 |
247 | /**
248 | * Finds all of the valid 'Chest' Tiled Objects on a given layer of a Tilemap.
249 | */
250 | export function getTiledChestObjectsFromMap(map: Phaser.Tilemaps.Tilemap, layerName: string): TiledChestObject[] {
251 | const chestObjects: TiledChestObject[] = [];
252 |
253 | // loop through each object and validate object has properties for the object we are planning to build
254 | const tiledObjects = getTiledObjectsFromLayer(map, layerName);
255 | tiledObjects.forEach((tiledObject) => {
256 | const contents = getTiledPropertyByName(tiledObject.properties, TILED_CHEST_OBJECT_PROPERTY.CONTENTS);
257 | const id = getTiledPropertyByName(tiledObject.properties, TILED_CHEST_OBJECT_PROPERTY.ID);
258 | const revealChestTrigger = getTiledPropertyByName(
259 | tiledObject.properties,
260 | TILED_CHEST_OBJECT_PROPERTY.REVEAL_CHEST_TRIGGER,
261 | );
262 | const requiresBossKey = getTiledPropertyByName(
263 | tiledObject.properties,
264 | TILED_CHEST_OBJECT_PROPERTY.REQUIRES_BOSS_KEY,
265 | );
266 | if (
267 | contents === undefined ||
268 | id === undefined ||
269 | revealChestTrigger === undefined ||
270 | requiresBossKey === undefined ||
271 | !isTrapType(revealChestTrigger) ||
272 | !isChestReward(contents)
273 | ) {
274 | return;
275 | }
276 |
277 | chestObjects.push({
278 | x: tiledObject.x,
279 | y: tiledObject.y,
280 | width: tiledObject.width,
281 | height: tiledObject.height,
282 | id,
283 | revealChestTrigger,
284 | contents,
285 | requiresBossKey,
286 | });
287 | });
288 |
289 | return chestObjects;
290 | }
291 |
292 | export function isChestReward(reward: string): reward is ChestReward {
293 | return CHEST_REWARD[reward] !== undefined;
294 | }
295 |
296 | /**
297 | * Finds all of the valid 'Enemy' Tiled Objects on a given layer of a Tilemap.
298 | */
299 | export function getTiledEnemyObjectsFromMap(map: Phaser.Tilemaps.Tilemap, layerName: string): TiledEnemyObject[] {
300 | const enemyObjects: TiledEnemyObject[] = [];
301 |
302 | // loop through each object and validate object has properties for the object we are planning to build
303 | const tiledObjects = getTiledObjectsFromLayer(map, layerName);
304 | tiledObjects.forEach((tiledObject) => {
305 | const enemyType = getTiledPropertyByName(tiledObject.properties, TILED_ENEMY_OBJECT_PROPERTY.TYPE);
306 | if (enemyType === undefined) {
307 | return;
308 | }
309 |
310 | enemyObjects.push({
311 | x: tiledObject.x,
312 | y: tiledObject.y,
313 | width: tiledObject.width,
314 | height: tiledObject.height,
315 | type: enemyType,
316 | });
317 | });
318 |
319 | return enemyObjects;
320 | }
321 |
322 | /**
323 | * Finds all of the valid 'Switch' Tiled Objects on a given layer of a Tilemap.
324 | */
325 | export function getTiledSwitchObjectsFromMap(map: Phaser.Tilemaps.Tilemap, layerName: string): TiledSwitchObject[] {
326 | const switchObjects: TiledSwitchObject[] = [];
327 |
328 | // loop through each object and validate object has properties for the object we are planning to build
329 | const tiledObjects = getTiledObjectsFromLayer(map, layerName);
330 | tiledObjects.forEach((tiledObject) => {
331 | const action = getTiledPropertyByName(tiledObject.properties, TILED_SWITCH_OBJECT_PROPERTY.ACTION);
332 | const targetIds = getTiledPropertyByName(tiledObject.properties, TILED_SWITCH_OBJECT_PROPERTY.TARGET_IDS);
333 | const texture = getTiledPropertyByName(tiledObject.properties, TILED_SWITCH_OBJECT_PROPERTY.TEXTURE);
334 |
335 | if (
336 | action === undefined ||
337 | targetIds === undefined ||
338 | texture === undefined ||
339 | !isSwitchAction(action) ||
340 | !isSwitchTexture(texture)
341 | ) {
342 | return;
343 | }
344 |
345 | switchObjects.push({
346 | x: tiledObject.x,
347 | y: tiledObject.y,
348 | width: tiledObject.width,
349 | height: tiledObject.height,
350 | action,
351 | targetIds: targetIds.split(',').map((value) => parseInt(value, 10)),
352 | texture,
353 | });
354 | });
355 |
356 | return switchObjects;
357 | }
358 |
359 | export function isSwitchTexture(switchTexture: string): switchTexture is SwitchTexture {
360 | return SWITCH_TEXTURE[switchTexture] !== undefined;
361 | }
362 |
363 | export function isSwitchAction(switchAction: string): switchAction is SwitchAction {
364 | return SWITCH_ACTION[switchAction] !== undefined;
365 | }
366 |
--------------------------------------------------------------------------------
/src/common/tiled/types.ts:
--------------------------------------------------------------------------------
1 | import { Direction } from '../types';
2 | import { CHEST_REWARD, DOOR_TYPE, SWITCH_ACTION, SWITCH_TEXTURE, TRAP_TYPE } from './common';
3 |
4 | export type TiledObject = {
5 | x: number;
6 | y: number;
7 | width: number;
8 | height: number;
9 | };
10 |
11 | export type TiledObjectProperty = {
12 | name: string;
13 | type: string;
14 | value: string | number | boolean;
15 | };
16 |
17 | export type TiledObjectWithProperties = {
18 | properties: TiledObjectProperty[];
19 | } & TiledObject;
20 |
21 | export type TiledRoomObject = {
22 | id: number;
23 | } & TiledObject;
24 |
25 | export type TiledDoorObject = {
26 | id: number;
27 | targetDoorId: number;
28 | isUnlocked: boolean;
29 | doorType: DoorType;
30 | direction: Direction;
31 | trapDoorTrigger: TrapType;
32 | isLevelTransition: boolean;
33 | targetLevel: string;
34 | targetRoomId: number;
35 | } & TiledObject;
36 |
37 | export type DoorType = keyof typeof DOOR_TYPE;
38 |
39 | export type TrapType = keyof typeof TRAP_TYPE;
40 |
41 | export type TiledPotObject = TiledObject;
42 |
43 | export type TiledChestObject = {
44 | contents: ChestReward;
45 | id: number;
46 | revealChestTrigger: TrapType;
47 | requiresBossKey: boolean;
48 | } & TiledObject;
49 |
50 | export type ChestReward = keyof typeof CHEST_REWARD;
51 |
52 | export type TiledEnemyObject = {
53 | type: number;
54 | } & TiledObject;
55 |
56 | export const TILED_ENEMY_OBJECT_PROPERTY = {
57 | TYPE: 'type',
58 | } as const;
59 |
60 | export type TiledSwitchObject = {
61 | targetIds: number[];
62 | action: SwitchAction;
63 | texture: SwitchTexture;
64 | } & TiledObject;
65 |
66 | export type SwitchTexture = keyof typeof SWITCH_TEXTURE;
67 |
68 | export type SwitchAction = keyof typeof SWITCH_ACTION;
69 |
--------------------------------------------------------------------------------
/src/common/types.ts:
--------------------------------------------------------------------------------
1 | import * as Phaser from 'phaser';
2 | import { CHARACTER_ANIMATIONS } from './assets';
3 | import { CHEST_STATE, DIRECTION, DUNGEON_ITEM, INTERACTIVE_OBJECT_TYPE, LEVEL_NAME } from './common';
4 |
5 | export type CharacterAnimation = keyof typeof CHARACTER_ANIMATIONS;
6 |
7 | export type Position = {
8 | x: number;
9 | y: number;
10 | };
11 |
12 | export type GameObject = Phaser.GameObjects.Sprite | Phaser.GameObjects.Image;
13 |
14 | export type Direction = keyof typeof DIRECTION;
15 |
16 | export type ChestState = keyof typeof CHEST_STATE;
17 |
18 | export type InteractiveObjectType = keyof typeof INTERACTIVE_OBJECT_TYPE;
19 |
20 | export interface CustomGameObject {
21 | enableObject(): void;
22 | disableObject(): void;
23 | }
24 |
25 | export type LevelName = keyof typeof LEVEL_NAME;
26 |
27 | export type LevelData = {
28 | level: LevelName;
29 | doorId: number;
30 | roomId: number;
31 | };
32 |
33 | export type DungeonItem = keyof typeof DUNGEON_ITEM;
34 |
--------------------------------------------------------------------------------
/src/common/utils.ts:
--------------------------------------------------------------------------------
1 | import * as Phaser from 'phaser';
2 | import { DIRECTION, LEVEL_NAME } from './common';
3 | import { CustomGameObject, Direction, GameObject, LevelName, Position } from './types';
4 |
5 | /**
6 | * Utility function to ensure we handle the full possible range of types when checking a variable for a possible
7 | * type in a union.
8 | *
9 | * A good example of this is when we check for all of the possible values in a `switch` statement, and we want
10 | * to ensure we check for all possible values in an enum type object.
11 | */
12 | export function exhaustiveGuard(_value: never): never {
13 | throw new Error(`Error! Reached forbidden guard function with unexpected value: ${JSON.stringify(_value)}`);
14 | }
15 |
16 | export function isArcadePhysicsBody(
17 | body: Phaser.Physics.Arcade.Body | Phaser.Physics.Arcade.StaticBody | MatterJS.BodyType | null,
18 | ): body is Phaser.Physics.Arcade.Body {
19 | if (body === undefined || body === null) {
20 | return false;
21 | }
22 | return body instanceof Phaser.Physics.Arcade.Body;
23 | }
24 |
25 | export function isDirection(direction: string): direction is Direction {
26 | return DIRECTION[direction] !== undefined;
27 | }
28 |
29 | export function isCustomGameObject(gameObject: GameObject): gameObject is GameObject & CustomGameObject {
30 | return gameObject['disableObject'] !== undefined && gameObject['enableObject'] !== undefined;
31 | }
32 |
33 | export function getDirectionOfObjectFromAnotherObject(object: Position, targetObject: Position): Direction {
34 | if (object.y < targetObject.y) {
35 | return DIRECTION.DOWN;
36 | }
37 | if (object.y > targetObject.y) {
38 | return DIRECTION.UP;
39 | }
40 | if (object.x < targetObject.x) {
41 | return DIRECTION.RIGHT;
42 | }
43 | return DIRECTION.LEFT;
44 | }
45 |
46 | export function isLevelName(levelName: string): levelName is LevelName {
47 | return LEVEL_NAME[levelName] !== undefined;
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/game-object/animation-component.ts:
--------------------------------------------------------------------------------
1 | import { CharacterAnimation, GameObject } from '../../common/types';
2 | import { BaseGameObjectComponent } from './base-game-object-component';
3 |
4 | export type AnimationConfig = {
5 | [key in CharacterAnimation]?: { key: string; repeat: number; ignoreIfPlaying: boolean };
6 | };
7 |
8 | export class AnimationComponent extends BaseGameObjectComponent {
9 | declare protected gameObject: Phaser.GameObjects.Sprite;
10 |
11 | #config: AnimationConfig;
12 |
13 | constructor(gameObject: GameObject, config: AnimationConfig) {
14 | super(gameObject);
15 | this.#config = config;
16 | }
17 |
18 | public getAnimationKey(characterAnimationKey: CharacterAnimation): string | undefined {
19 | if (this.#config[characterAnimationKey] === undefined) {
20 | return undefined;
21 | }
22 | return this.#config[characterAnimationKey].key;
23 | }
24 |
25 | public playAnimation(characterAnimationKey: CharacterAnimation, callback?: () => void): void {
26 | if (this.#config[characterAnimationKey] === undefined) {
27 | if (callback) {
28 | callback();
29 | }
30 | return;
31 | }
32 | const animationConfig: Phaser.Types.Animations.PlayAnimationConfig = {
33 | key: this.#config[characterAnimationKey].key,
34 | repeat: this.#config[characterAnimationKey].repeat,
35 | timeScale: 1,
36 | };
37 | if (callback) {
38 | const animationKey = Phaser.Animations.Events.ANIMATION_COMPLETE_KEY + this.#config[characterAnimationKey].key;
39 | this.gameObject.once(animationKey, () => {
40 | callback();
41 | });
42 | }
43 | this.gameObject.play(animationConfig, this.#config[characterAnimationKey].ignoreIfPlaying);
44 | }
45 |
46 | public playAnimationInReverse(characterAnimationKey: CharacterAnimation, callback?: () => void): void {
47 | if (this.#config[characterAnimationKey] === undefined) {
48 | if (callback) {
49 | callback();
50 | }
51 | return;
52 | }
53 | const animationConfig: Phaser.Types.Animations.PlayAnimationConfig = {
54 | key: this.#config[characterAnimationKey].key,
55 | repeat: this.#config[characterAnimationKey].repeat,
56 | timeScale: 1.75,
57 | };
58 | if (callback) {
59 | const animationKey = Phaser.Animations.Events.ANIMATION_COMPLETE_KEY + this.#config[characterAnimationKey].key;
60 | this.gameObject.once(animationKey, () => {
61 | callback();
62 | });
63 | }
64 | this.gameObject.playReverse(animationConfig, this.#config[characterAnimationKey].ignoreIfPlaying);
65 | }
66 |
67 | public isAnimationPlaying(): boolean {
68 | return this.gameObject.anims.isPlaying;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/components/game-object/base-game-object-component.ts:
--------------------------------------------------------------------------------
1 | import * as Phaser from 'phaser';
2 | import { GameObject } from '../../common/types';
3 |
4 | export class BaseGameObjectComponent {
5 | protected scene: Phaser.Scene;
6 | protected gameObject: GameObject;
7 |
8 | constructor(gameObject: GameObject) {
9 | this.scene = gameObject.scene;
10 | this.gameObject = gameObject;
11 | this.assignComponentToObject(gameObject);
12 | }
13 |
14 | static getComponent(gameObject: GameObject): T {
15 | return gameObject[`_${this.name}`] as T;
16 | }
17 |
18 | static removeComponent(gameObject: GameObject): void {
19 | delete gameObject[`_${this.name}`];
20 | }
21 |
22 | protected assignComponentToObject(object: GameObject | Phaser.Physics.Arcade.Body): void {
23 | object[`_${this.constructor.name}`] = this;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/game-object/colliding-objects-component.ts:
--------------------------------------------------------------------------------
1 | import { GameObject } from '../../common/types';
2 | import { BaseGameObjectComponent } from './base-game-object-component';
3 |
4 | export class CollidingObjectsComponent extends BaseGameObjectComponent {
5 | #objects: GameObject[];
6 |
7 | constructor(gameObject: GameObject) {
8 | super(gameObject);
9 | this.#objects = [];
10 | }
11 |
12 | get objects(): GameObject[] {
13 | return this.#objects;
14 | }
15 |
16 | public add(gameObject: GameObject): void {
17 | this.#objects.push(gameObject);
18 | }
19 |
20 | public reset(): void {
21 | this.#objects = [];
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/game-object/controls-component.ts:
--------------------------------------------------------------------------------
1 | import { GameObject } from '../../common/types';
2 | import { InputComponent } from '../input/input-component';
3 | import { BaseGameObjectComponent } from './base-game-object-component';
4 |
5 | export class ControlsComponent extends BaseGameObjectComponent {
6 | #inputComponent: InputComponent;
7 |
8 | constructor(gameObject: GameObject, inputComponent: InputComponent) {
9 | super(gameObject);
10 | this.#inputComponent = inputComponent;
11 | }
12 |
13 | get controls(): InputComponent {
14 | return this.#inputComponent;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/game-object/direction-component.ts:
--------------------------------------------------------------------------------
1 | import { DIRECTION } from '../../common/common';
2 | import { Direction, GameObject } from '../../common/types';
3 | import { BaseGameObjectComponent } from './base-game-object-component';
4 |
5 | export class DirectionComponent extends BaseGameObjectComponent {
6 | #direction: Direction;
7 | #callback: (direction: Direction) => void;
8 |
9 | constructor(gameObject: GameObject, onDirectionCallback = () => undefined) {
10 | super(gameObject);
11 | this.#direction = DIRECTION.DOWN;
12 | this.#callback = onDirectionCallback;
13 | }
14 |
15 | get direction(): Direction {
16 | return this.#direction;
17 | }
18 |
19 | set direction(direction: Direction) {
20 | this.#direction = direction;
21 | this.#callback(this.#direction);
22 | }
23 |
24 | set callback(callback: (direction: Direction) => void) {
25 | this.#callback = callback;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/game-object/held-game-object-component.ts:
--------------------------------------------------------------------------------
1 | import { GameObject } from '../../common/types';
2 | import { BaseGameObjectComponent } from './base-game-object-component';
3 |
4 | export class HeldGameObjectComponent extends BaseGameObjectComponent {
5 | #object: GameObject | undefined;
6 |
7 | constructor(gameObject: GameObject) {
8 | super(gameObject);
9 | }
10 |
11 | get object(): GameObject | undefined {
12 | return this.#object;
13 | }
14 |
15 | set object(object: GameObject) {
16 | this.#object = object;
17 | }
18 |
19 | public drop(): void {
20 | this.#object = undefined;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/game-object/interactive-object-component.ts:
--------------------------------------------------------------------------------
1 | import { GameObject, InteractiveObjectType } from '../../common/types';
2 | import { BaseGameObjectComponent } from './base-game-object-component';
3 |
4 | export class InteractiveObjectComponent extends BaseGameObjectComponent {
5 | #objectType: InteractiveObjectType;
6 | #callback: () => void;
7 | #canInteractCheck: () => boolean;
8 |
9 | constructor(
10 | gameObject: GameObject,
11 | objectType: InteractiveObjectType,
12 | canInteractCheck = () => true,
13 | callback = () => undefined,
14 | ) {
15 | super(gameObject);
16 | this.#objectType = objectType;
17 | this.#callback = callback;
18 | this.#canInteractCheck = canInteractCheck;
19 | }
20 |
21 | get objectType(): InteractiveObjectType {
22 | return this.#objectType;
23 | }
24 |
25 | public interact(): void {
26 | this.#callback();
27 | }
28 |
29 | public canInteractWith(): boolean {
30 | return this.#canInteractCheck();
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/game-object/invulnerable-component.ts:
--------------------------------------------------------------------------------
1 | import { GameObject } from '../../common/types';
2 | import { BaseGameObjectComponent } from './base-game-object-component';
3 |
4 | export class InvulnerableComponent extends BaseGameObjectComponent {
5 | #invulnerable: boolean;
6 | #invulnerableAfterHitAnimationDuration: number;
7 |
8 | constructor(gameObject: GameObject, invulnerable = false, invulnerableAfterHitAnimationDuration = 0) {
9 | super(gameObject);
10 | this.#invulnerable = invulnerable;
11 | this.#invulnerableAfterHitAnimationDuration = invulnerableAfterHitAnimationDuration;
12 | }
13 |
14 | get invulnerable(): boolean {
15 | return this.#invulnerable;
16 | }
17 |
18 | set invulnerable(val: boolean) {
19 | this.#invulnerable = val;
20 | }
21 |
22 | get invulnerableAfterHitAnimationDuration(): number {
23 | return this.#invulnerableAfterHitAnimationDuration;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/game-object/life-component.ts:
--------------------------------------------------------------------------------
1 | import { GameObject } from '../../common/types';
2 | import { BaseGameObjectComponent } from './base-game-object-component';
3 |
4 | export class LifeComponent extends BaseGameObjectComponent {
5 | #maxLife: number;
6 | #currentLife: number;
7 |
8 | constructor(gameObject: GameObject, maxLife: number, currentLife = maxLife) {
9 | super(gameObject);
10 | this.#maxLife = maxLife;
11 | this.#currentLife = currentLife;
12 | }
13 |
14 | get life(): number {
15 | return this.#currentLife;
16 | }
17 |
18 | get maxLife(): number {
19 | return this.#maxLife;
20 | }
21 |
22 | public takeDamage(damage: number): void {
23 | if (this.#currentLife === 0) {
24 | return;
25 | }
26 | this.#currentLife -= damage;
27 | if (this.#currentLife < 0) {
28 | this.#currentLife = 0;
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/game-object/speed-component.ts:
--------------------------------------------------------------------------------
1 | import { GameObject } from '../../common/types';
2 | import { BaseGameObjectComponent } from './base-game-object-component';
3 |
4 | export class SpeedComponent extends BaseGameObjectComponent {
5 | #speed: number;
6 |
7 | constructor(gameObject: GameObject, speed: number) {
8 | super(gameObject);
9 | this.#speed = speed;
10 | }
11 |
12 | get speed(): number {
13 | return this.#speed;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/game-object/throwable-object-component.ts:
--------------------------------------------------------------------------------
1 | import { DIRECTION } from '../../common/common';
2 | import { THROW_ITEM_DELAY_BEFORE_CALLBACK, THROW_ITEM_SPEED } from '../../common/config';
3 | import { Direction, GameObject } from '../../common/types';
4 | import { exhaustiveGuard, isArcadePhysicsBody, isCustomGameObject } from '../../common/utils';
5 | import { BaseGameObjectComponent } from './base-game-object-component';
6 |
7 | export class ThrowableObjectComponent extends BaseGameObjectComponent {
8 | #callback: () => void;
9 |
10 | constructor(gameObject: GameObject, callback = () => undefined) {
11 | super(gameObject);
12 | this.#callback = callback;
13 | }
14 |
15 | public drop(): void {
16 | this.#callback();
17 | }
18 |
19 | public throw(direction: Direction): void {
20 | if (!isArcadePhysicsBody(this.gameObject.body) || !isCustomGameObject(this.gameObject)) {
21 | this.#callback();
22 | return;
23 | }
24 |
25 | const body = this.gameObject.body;
26 | body.velocity.x = 0;
27 | body.velocity.y = 0;
28 |
29 | const throwSpeed = THROW_ITEM_SPEED;
30 | switch (direction) {
31 | case DIRECTION.DOWN:
32 | this.gameObject.y += 20;
33 | body.velocity.y = throwSpeed;
34 | break;
35 | case DIRECTION.UP:
36 | body.velocity.y = throwSpeed * -1;
37 | break;
38 | case DIRECTION.LEFT:
39 | body.velocity.x = throwSpeed * -1;
40 | break;
41 | case DIRECTION.RIGHT:
42 | body.velocity.x = throwSpeed;
43 | break;
44 | default:
45 | exhaustiveGuard(direction);
46 | }
47 |
48 | this.gameObject.enableObject();
49 | this.gameObject.scene.time.delayedCall(THROW_ITEM_DELAY_BEFORE_CALLBACK, () => {
50 | body.velocity.x = 0;
51 | body.velocity.y = 0;
52 | this.#callback();
53 | });
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/game-object/weapon-component.ts:
--------------------------------------------------------------------------------
1 | import { GameObject } from '../../common/types';
2 | import { Weapon } from '../../game-objects/weapons/base-weapon';
3 | import { BaseGameObjectComponent } from './base-game-object-component';
4 |
5 | export class WeaponComponent extends BaseGameObjectComponent {
6 | #weapon: Weapon | undefined;
7 | #weaponPhysicsBody: Phaser.Physics.Arcade.Body;
8 |
9 | constructor(gameObject: GameObject) {
10 | super(gameObject);
11 | this.#weaponPhysicsBody = gameObject.scene.physics.add.body(gameObject.x, gameObject.y, 1, 1);
12 | this.#weaponPhysicsBody.enable = false;
13 | this.assignComponentToObject(this.#weaponPhysicsBody);
14 | }
15 |
16 | get weapon(): Weapon | undefined {
17 | return this.#weapon;
18 | }
19 |
20 | set weapon(weapon: Weapon | undefined) {
21 | this.#weapon = weapon;
22 | }
23 |
24 | get body(): Phaser.Physics.Arcade.Body {
25 | return this.#weaponPhysicsBody;
26 | }
27 |
28 | get weaponDamage(): number {
29 | if (this.#weapon === undefined) {
30 | return 0;
31 | }
32 | return this.#weapon.baseDamage;
33 | }
34 |
35 | public update(): void {
36 | if (this.#weapon === undefined) {
37 | return;
38 | }
39 | this.#weapon.update();
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/input/input-component.ts:
--------------------------------------------------------------------------------
1 | export class InputComponent {
2 | #up: boolean;
3 | #down: boolean;
4 | #left: boolean;
5 | #right: boolean;
6 | #actionKey: boolean;
7 | #attackKey: boolean;
8 | #selectKey: boolean;
9 | #enterKey: boolean;
10 | #isMovementLocked: boolean;
11 |
12 | constructor() {
13 | this.#up = false;
14 | this.#left = false;
15 | this.#right = false;
16 | this.#down = false;
17 | this.#actionKey = false;
18 | this.#attackKey = false;
19 | this.#selectKey = false;
20 | this.#enterKey = false;
21 | this.#isMovementLocked = false;
22 | }
23 |
24 | get isMovementLocked(): boolean {
25 | return this.#isMovementLocked;
26 | }
27 |
28 | set isMovementLocked(val: boolean) {
29 | this.#isMovementLocked = val;
30 | }
31 |
32 | get isUpDown(): boolean {
33 | return this.#up;
34 | }
35 |
36 | get isUpJustDown(): boolean {
37 | return this.#up;
38 | }
39 |
40 | set isUpDown(val: boolean) {
41 | this.#up = val;
42 | }
43 |
44 | get isDownDown(): boolean {
45 | return this.#down;
46 | }
47 |
48 | get isDownJustDown(): boolean {
49 | return this.#down;
50 | }
51 |
52 | set isDownDown(val: boolean) {
53 | this.#down = val;
54 | }
55 |
56 | get isLeftDown(): boolean {
57 | return this.#left;
58 | }
59 |
60 | set isLeftDown(val: boolean) {
61 | this.#left = val;
62 | }
63 |
64 | get isRightDown(): boolean {
65 | return this.#right;
66 | }
67 |
68 | set isRightDown(val: boolean) {
69 | this.#right = val;
70 | }
71 |
72 | get isActionKeyJustDown(): boolean {
73 | return this.#actionKey;
74 | }
75 |
76 | set isActionKeyJustDown(val: boolean) {
77 | this.#actionKey = val;
78 | }
79 |
80 | get isAttackKeyJustDown(): boolean {
81 | return this.#attackKey;
82 | }
83 |
84 | set isAttackKeyJustDown(val: boolean) {
85 | this.#attackKey = val;
86 | }
87 |
88 | get isSelectKeyJustDown(): boolean {
89 | return this.#selectKey;
90 | }
91 |
92 | set isSelectKeyJustDown(val: boolean) {
93 | this.#selectKey = val;
94 | }
95 |
96 | get isEnterKeyJustDown(): boolean {
97 | return this.#enterKey;
98 | }
99 |
100 | set isEnterKeyJustDown(val: boolean) {
101 | this.#enterKey = val;
102 | }
103 |
104 | public reset(): void {
105 | this.#down = false;
106 | this.#up = false;
107 | this.#left = false;
108 | this.#right = false;
109 | this.#attackKey = false;
110 | this.#actionKey = false;
111 | this.#selectKey = false;
112 | this.#enterKey = false;
113 | this.#isMovementLocked = false;
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/components/input/keyboard-component.ts:
--------------------------------------------------------------------------------
1 | import * as Phaser from 'phaser';
2 | import { InputComponent } from './input-component';
3 |
4 | export class KeyboardComponent extends InputComponent {
5 | #cursorKeys: Phaser.Types.Input.Keyboard.CursorKeys;
6 | #attackKey: Phaser.Input.Keyboard.Key;
7 | #actionKey: Phaser.Input.Keyboard.Key;
8 | #enterKey: Phaser.Input.Keyboard.Key;
9 |
10 | constructor(keyboardPlugin: Phaser.Input.Keyboard.KeyboardPlugin) {
11 | super();
12 | this.#cursorKeys = keyboardPlugin.createCursorKeys();
13 | this.#attackKey = keyboardPlugin.addKey(Phaser.Input.Keyboard.KeyCodes.Z);
14 | this.#actionKey = keyboardPlugin.addKey(Phaser.Input.Keyboard.KeyCodes.X);
15 | this.#enterKey = keyboardPlugin.addKey(Phaser.Input.Keyboard.KeyCodes.ENTER);
16 |
17 | // z = B, Attack
18 | // x = A, Talk, Run, Lift/Throw, Push/Pull
19 | // shift = Select, Open Save Menu
20 | // return/enter = Start, Open Inventory
21 | }
22 |
23 | get isUpDown(): boolean {
24 | return this.#cursorKeys.up.isDown;
25 | }
26 |
27 | get isUpJustDown(): boolean {
28 | return Phaser.Input.Keyboard.JustDown(this.#cursorKeys.up);
29 | }
30 |
31 | get isDownDown(): boolean {
32 | return this.#cursorKeys.down.isDown;
33 | }
34 |
35 | get isDownJustDown(): boolean {
36 | return Phaser.Input.Keyboard.JustDown(this.#cursorKeys.down);
37 | }
38 |
39 | get isLeftDown(): boolean {
40 | return this.#cursorKeys.left.isDown;
41 | }
42 |
43 | get isRightDown(): boolean {
44 | return this.#cursorKeys.right.isDown;
45 | }
46 |
47 | get isActionKeyJustDown(): boolean {
48 | return Phaser.Input.Keyboard.JustDown(this.#actionKey);
49 | }
50 |
51 | get isAttackKeyJustDown(): boolean {
52 | return Phaser.Input.Keyboard.JustDown(this.#attackKey);
53 | }
54 |
55 | get isSelectKeyJustDown(): boolean {
56 | return Phaser.Input.Keyboard.JustDown(this.#cursorKeys.shift);
57 | }
58 |
59 | get isEnterKeyJustDown(): boolean {
60 | return Phaser.Input.Keyboard.JustDown(this.#enterKey);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/components/inventory/inventory-manager.ts:
--------------------------------------------------------------------------------
1 | import { DUNGEON_ITEM } from '../../common/common';
2 | import { DungeonItem, LevelName } from '../../common/types';
3 | import { exhaustiveGuard } from '../../common/utils';
4 |
5 | type AreaInventory = {
6 | map: boolean;
7 | compass: boolean;
8 | bossKey: boolean;
9 | keys: number;
10 | };
11 |
12 | type ItemInventory = {
13 | sword: boolean;
14 | };
15 |
16 | export type InventoryData = {
17 | general: ItemInventory;
18 | area: { [key in LevelName]: AreaInventory };
19 | };
20 |
21 | export class InventoryManager {
22 | static #instance: InventoryManager;
23 |
24 | #generalInventory: ItemInventory;
25 | #areaInventory: { [key in LevelName]: AreaInventory };
26 |
27 | private constructor() {
28 | this.#generalInventory = {
29 | sword: true,
30 | };
31 | this.#areaInventory = {
32 | DUNGEON_1: {
33 | map: false,
34 | bossKey: false,
35 | compass: false,
36 | keys: 0,
37 | },
38 | WORLD: {
39 | map: false,
40 | bossKey: false,
41 | compass: false,
42 | keys: 0,
43 | },
44 | };
45 | }
46 |
47 | public static get instance(): InventoryManager {
48 | if (!InventoryManager.#instance) {
49 | InventoryManager.#instance = new InventoryManager();
50 | }
51 | return InventoryManager.#instance;
52 | }
53 |
54 | get data(): InventoryData {
55 | return {
56 | general: { ...this.#generalInventory },
57 | area: { ...this.#areaInventory },
58 | };
59 | }
60 |
61 | set data(data: InventoryData) {
62 | this.#areaInventory = { ...data.area };
63 | this.#generalInventory = { ...data.general };
64 | }
65 |
66 | public addDungeonItem(area: LevelName, dungeonItem: DungeonItem): void {
67 | switch (dungeonItem) {
68 | case DUNGEON_ITEM.MAP:
69 | this.#areaInventory[area].map = true;
70 | return;
71 | case DUNGEON_ITEM.COMPASS:
72 | this.#areaInventory[area].compass = true;
73 | return;
74 | case DUNGEON_ITEM.BOSS_KEY:
75 | this.#areaInventory[area].bossKey = true;
76 | return;
77 | case DUNGEON_ITEM.SMALL_KEY:
78 | this.#areaInventory[area].keys += 1;
79 | return;
80 | default:
81 | exhaustiveGuard(dungeonItem);
82 | }
83 | }
84 |
85 | public getAreaInventory(area: LevelName): AreaInventory {
86 | return { ...this.#areaInventory[area] };
87 | }
88 |
89 | public useAreaSmallKey(area: LevelName): void {
90 | if (this.#areaInventory[area].keys > 0) {
91 | this.#areaInventory[area].keys -= 1;
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/components/state-machine/state-machine.ts:
--------------------------------------------------------------------------------
1 | import { ENABLE_LOGGING } from '../../common/config';
2 |
3 | export interface State {
4 | stateMachine: StateMachine;
5 | name: string;
6 | onEnter?: (args: unknown[]) => void;
7 | onUpdate?: () => void;
8 | }
9 |
10 | export class StateMachine {
11 | #id: string;
12 | #states: Map;
13 | #currentState: State | undefined;
14 | #isChangingState: boolean;
15 | #changingStateQueue: { state: string; args: unknown[] }[];
16 |
17 | /**
18 | * @param {string} [id] the unique identifier for this state machine instance.
19 | */
20 | constructor(id?: string) {
21 | if (id === undefined) {
22 | this.#id = Phaser.Math.RND.uuid();
23 | } else {
24 | this.#id = id;
25 | }
26 | this.#isChangingState = false;
27 | this.#changingStateQueue = [];
28 | this.#currentState = undefined;
29 | this.#states = new Map();
30 | }
31 |
32 | /** @type {string | undefined} */
33 | get currentStateName() {
34 | return this.#currentState?.name;
35 | }
36 |
37 | /**
38 | * Used for processing any queued states and is meant to be called during every step of our game loop.
39 | * @returns {void}
40 | */
41 | public update(): void {
42 | const queuedState = this.#changingStateQueue.shift();
43 | if (queuedState !== undefined) {
44 | this.setState(queuedState.state, queuedState.args);
45 | }
46 |
47 | if (this.#currentState && this.#currentState.onUpdate) {
48 | this.#currentState.onUpdate();
49 | }
50 | }
51 |
52 | /**
53 | * Updates the current state of the state machine to the provided state name. If the state machine
54 | * is already transitioning states, or if there is a queue of states, the new state will be added
55 | * to that queue and processed after the queue is processed.
56 | * @param {string} name
57 | * @returns {void}
58 | */
59 | public setState(name: string, ...args: unknown[]): void {
60 | const methodName = 'setState';
61 |
62 | if (!this.#states.has(name)) {
63 | console.warn(`[${StateMachine.name}-${this.#id}:${methodName}] tried to change to unknown state: ${name}`);
64 | return;
65 | }
66 |
67 | if (this.#isCurrentState(name)) {
68 | return;
69 | }
70 |
71 | if (this.#isChangingState) {
72 | this.#changingStateQueue.push({ state: name, args });
73 | return;
74 | }
75 |
76 | this.#isChangingState = true;
77 | this.#log(methodName, `change from ${this.#currentState?.name ?? 'none'} to ${name}`);
78 |
79 | this.#currentState = this.#states.get(name) as State;
80 |
81 | if (this.#currentState.onEnter) {
82 | this.#log(methodName, `${this.#currentState.name} on enter invoked`);
83 | this.#currentState.onEnter(args);
84 | }
85 |
86 | this.#isChangingState = false;
87 | }
88 |
89 | /**
90 | * Adds a new state to the current state machine instance. If a state already exists with the given name
91 | * that previous state will be replaced with the new state that was provided.
92 | * @param {State} state
93 | * @returns {void}
94 | */
95 | public addState(state: State): void {
96 | state.stateMachine = this;
97 | this.#states.set(state.name, state);
98 | }
99 |
100 | /**
101 | * Checks to see if the provided state name is the state that is currently being handled by the state machine instance.
102 | * @param {string} name
103 | * @returns {boolean}
104 | */
105 | #isCurrentState(name: string): boolean {
106 | if (!this.#currentState) {
107 | return false;
108 | }
109 | return this.#currentState.name === name;
110 | }
111 |
112 | #log(methodName: string, message: string) {
113 | if (!ENABLE_LOGGING) {
114 | return;
115 | }
116 | console.log(`[${StateMachine.name}-${this.#id}:${methodName}] ${message}`);
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/components/state-machine/states/character/attack-state.ts:
--------------------------------------------------------------------------------
1 | import { DIRECTION } from '../../../../common/common';
2 | import { exhaustiveGuard } from '../../../../common/utils';
3 | import { CharacterGameObject } from '../../../../game-objects/common/character-game-object';
4 | import { WeaponComponent } from '../../../game-object/weapon-component';
5 | import { BaseCharacterState } from './base-character-state';
6 | import { CHARACTER_STATES } from './character-states';
7 |
8 | export class AttackState extends BaseCharacterState {
9 | constructor(gameObject: CharacterGameObject) {
10 | super(CHARACTER_STATES.ATTACK_STATE, gameObject);
11 | }
12 |
13 | public onEnter(): void {
14 | // reset game object velocity
15 | this._resetObjectVelocity();
16 |
17 | const weaponComponent = WeaponComponent.getComponent(this._gameObject);
18 | if (weaponComponent === undefined || weaponComponent.weapon === undefined) {
19 | this._stateMachine.setState(CHARACTER_STATES.IDLE_STATE);
20 | return;
21 | }
22 |
23 | const weapon = weaponComponent.weapon;
24 | switch (this._gameObject.direction) {
25 | case DIRECTION.UP:
26 | return weapon.attackUp();
27 | case DIRECTION.DOWN:
28 | return weapon.attackDown();
29 | case DIRECTION.LEFT:
30 | return weapon.attackLeft();
31 | case DIRECTION.RIGHT:
32 | return weapon.attackRight();
33 | default:
34 | exhaustiveGuard(this._gameObject.direction);
35 | }
36 | }
37 |
38 | public onUpdate(): void {
39 | const weaponComponent = WeaponComponent.getComponent(this._gameObject);
40 | if (weaponComponent === undefined || weaponComponent.weapon === undefined) {
41 | this._stateMachine.setState(CHARACTER_STATES.IDLE_STATE);
42 | return;
43 | }
44 | // wait until weapon animation is done for attacking
45 | const weapon = weaponComponent.weapon;
46 | if (weapon.isAttacking) {
47 | return;
48 | }
49 | this._stateMachine.setState(CHARACTER_STATES.IDLE_STATE);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/state-machine/states/character/base-character-state.ts:
--------------------------------------------------------------------------------
1 | import { isArcadePhysicsBody } from '../../../../common/utils';
2 | import { CharacterGameObject } from '../../../../game-objects/common/character-game-object';
3 | import { State, StateMachine } from '../../state-machine';
4 |
5 | export abstract class BaseCharacterState implements State {
6 | protected _gameObject: CharacterGameObject;
7 | protected _stateMachine!: StateMachine;
8 | #name: string;
9 |
10 | constructor(name: string, gameObject: CharacterGameObject) {
11 | this._gameObject = gameObject;
12 | this.#name = name;
13 | }
14 |
15 | get name(): string {
16 | return this.#name;
17 | }
18 |
19 | set stateMachine(stateMachine: StateMachine) {
20 | this._stateMachine = stateMachine;
21 | }
22 |
23 | protected _resetObjectVelocity(): void {
24 | if (!isArcadePhysicsBody(this._gameObject.body)) {
25 | return;
26 | }
27 | this._gameObject.body.velocity.x = 0;
28 | this._gameObject.body.velocity.y = 0;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/state-machine/states/character/base-move-state.ts:
--------------------------------------------------------------------------------
1 | import { DIRECTION } from '../../../../common/common';
2 | import { Direction } from '../../../../common/types';
3 | import { isArcadePhysicsBody } from '../../../../common/utils';
4 | import { CharacterGameObject } from '../../../../game-objects/common/character-game-object';
5 | import { InputComponent } from '../../../input/input-component';
6 | import { BaseCharacterState } from './base-character-state';
7 |
8 | export abstract class BaseMoveState extends BaseCharacterState {
9 | protected _moveAnimationPrefix: 'WALK' | 'WALK_HOLD';
10 |
11 | constructor(stateName: string, gameObject: CharacterGameObject, moveAnimationPrefix: 'WALK' | 'WALK_HOLD') {
12 | super(stateName, gameObject);
13 | this._moveAnimationPrefix = moveAnimationPrefix;
14 | }
15 |
16 | protected isNoInputMovement(controls: InputComponent): boolean {
17 | return (
18 | (!controls.isDownDown && !controls.isUpDown && !controls.isLeftDown && !controls.isRightDown) ||
19 | controls.isMovementLocked
20 | );
21 | }
22 |
23 | protected handleCharacterMovement(): void {
24 | const controls = this._gameObject.controls;
25 |
26 | // vertical movement
27 | if (controls.isUpDown) {
28 | this.updateVelocity(false, -1);
29 | this.updateDirection(DIRECTION.UP);
30 | } else if (controls.isDownDown) {
31 | this.updateVelocity(false, 1);
32 | this.updateDirection(DIRECTION.DOWN);
33 | } else {
34 | this.updateVelocity(false, 0);
35 | }
36 |
37 | const isMovingVertically = controls.isDownDown || controls.isUpDown;
38 | // horizontal movement
39 | if (controls.isLeftDown) {
40 | this.flip(true);
41 | this.updateVelocity(true, -1);
42 | if (!isMovingVertically) {
43 | this.updateDirection(DIRECTION.LEFT);
44 | }
45 | } else if (controls.isRightDown) {
46 | this.flip(false);
47 | this.updateVelocity(true, 1);
48 | if (!isMovingVertically) {
49 | this.updateDirection(DIRECTION.RIGHT);
50 | }
51 | } else {
52 | this.updateVelocity(true, 0);
53 | }
54 |
55 | this.normalizeVelocity();
56 | }
57 |
58 | protected normalizeVelocity(): void {
59 | // if the player is moving diagonally, the resultant vector will have a magnitude greater than the defined speed.
60 | // if we normalize the vector, this will make sure the magnitude matches defined speed
61 | if (!isArcadePhysicsBody(this._gameObject.body)) {
62 | return;
63 | }
64 | this._gameObject.body.velocity.normalize().scale(this._gameObject.speed);
65 | }
66 |
67 | protected flip(value: boolean): void {
68 | this._gameObject.setFlipX(value);
69 | }
70 |
71 | protected updateVelocity(isX: boolean, value: number): void {
72 | if (!isArcadePhysicsBody(this._gameObject.body)) {
73 | return;
74 | }
75 | if (isX) {
76 | this._gameObject.body.velocity.x = value;
77 | return;
78 | }
79 | this._gameObject.body.velocity.y = value;
80 | }
81 |
82 | protected updateDirection(direction: Direction): void {
83 | this._gameObject.direction = direction;
84 | this._gameObject.animationComponent.playAnimation(`${this._moveAnimationPrefix}_${this._gameObject.direction}`);
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/components/state-machine/states/character/boss/drow/boss-drow-hidden-state.ts:
--------------------------------------------------------------------------------
1 | import { CharacterGameObject } from '../../../../../../game-objects/common/character-game-object';
2 | import { BaseCharacterState } from '../../base-character-state';
3 | import { CHARACTER_STATES } from '../../character-states';
4 | import { ENEMY_BOSS_HIDDEN_STATE_DURATION } from '../../../../../../common/config';
5 |
6 | export class BossDrowHiddenState extends BaseCharacterState {
7 | constructor(gameObject: CharacterGameObject) {
8 | super(CHARACTER_STATES.HIDDEN_STATE, gameObject);
9 | }
10 |
11 | public onEnter(): void {
12 | this._gameObject.disableObject();
13 |
14 | // wait a brief period of time before showing game object and then transition to teleport
15 | this._gameObject.scene.time.delayedCall(ENEMY_BOSS_HIDDEN_STATE_DURATION, () => {
16 | this._gameObject.enableObject();
17 | this._stateMachine.setState(CHARACTER_STATES.TELEPORT_STATE);
18 | });
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/state-machine/states/character/boss/drow/boss-drow-idle-state.ts:
--------------------------------------------------------------------------------
1 | import { CharacterGameObject } from '../../../../../../game-objects/common/character-game-object';
2 | import { BaseCharacterState } from '../../base-character-state';
3 | import { CHARACTER_STATES } from '../../character-states';
4 | import { ENEMY_BOSS_IDLE_STATE_DURATION } from '../../../../../../common/config';
5 |
6 | export class BossDrowIdleState extends BaseCharacterState {
7 | constructor(gameObject: CharacterGameObject) {
8 | super(CHARACTER_STATES.IDLE_STATE, gameObject);
9 | }
10 |
11 | public onEnter(): void {
12 | // play idle animation based on game object direction
13 | this._gameObject.animationComponent.playAnimation(`IDLE_${this._gameObject.direction}`);
14 |
15 | // wait a brief period of time before showing game object and then transition to teleport
16 | this._gameObject.scene.time.delayedCall(ENEMY_BOSS_IDLE_STATE_DURATION, () => {
17 | if (this._stateMachine.currentStateName === CHARACTER_STATES.IDLE_STATE) {
18 | this._stateMachine.setState(CHARACTER_STATES.TELEPORT_STATE);
19 | }
20 | });
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/state-machine/states/character/boss/drow/boss-drow-prepare-attack-state.ts:
--------------------------------------------------------------------------------
1 | import * as Phaser from 'phaser';
2 | import { DIRECTION } from '../../../../../../common/common';
3 | import { CharacterGameObject } from '../../../../../../game-objects/common/character-game-object';
4 | import { GameScene } from '../../../../../../scenes/game-scene';
5 | import { BaseCharacterState } from '../../base-character-state';
6 | import { CHARACTER_STATES } from '../../character-states';
7 | import { ENEMY_BOSS_PREPARE_ATTACK_STATE_FINISHED_DELAY } from '../../../../../../common/config';
8 |
9 | export class BossDrowPrepareAttackState extends BaseCharacterState {
10 | constructor(gameObject: CharacterGameObject) {
11 | super(CHARACTER_STATES.PREPARE_ATTACK_STATE, gameObject);
12 | }
13 |
14 | public onEnter(): void {
15 | const targetEnemy = (this._gameObject.scene as GameScene).player;
16 |
17 | const vec = new Phaser.Math.Vector2(targetEnemy.x - this._gameObject.x, targetEnemy.y - this._gameObject.y);
18 | const radians = vec.angle();
19 | const degrees = Phaser.Math.RadToDeg(radians);
20 |
21 | this._gameObject.setFlipX(false);
22 | if (degrees >= 45 && degrees < 135) {
23 | this._gameObject.direction = DIRECTION.DOWN;
24 | } else if (degrees >= 135 && degrees < 225) {
25 | this._gameObject.setFlipX(true);
26 | this._gameObject.direction = DIRECTION.LEFT;
27 | } else if (degrees >= 225 && degrees < 315) {
28 | this._gameObject.direction = DIRECTION.UP;
29 | } else {
30 | this._gameObject.direction = DIRECTION.RIGHT;
31 | }
32 |
33 | if (this._gameObject.direction === DIRECTION.DOWN || this._gameObject.direction === DIRECTION.UP) {
34 | this._gameObject.setX(targetEnemy.x);
35 | } else {
36 | this._gameObject.setY(targetEnemy.y);
37 | }
38 |
39 | this._gameObject.animationComponent.playAnimation(`IDLE_${this._gameObject.direction}`);
40 | this._gameObject.scene.time.delayedCall(ENEMY_BOSS_PREPARE_ATTACK_STATE_FINISHED_DELAY, () => {
41 | this._stateMachine.setState(CHARACTER_STATES.ATTACK_STATE);
42 | });
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/state-machine/states/character/boss/drow/boss-drow-teleport-state.ts:
--------------------------------------------------------------------------------
1 | import * as Phaser from 'phaser';
2 | import { CharacterGameObject } from '../../../../../../game-objects/common/character-game-object';
3 | import { BaseCharacterState } from '../../base-character-state';
4 | import { CHARACTER_STATES } from '../../character-states';
5 | import {
6 | ENEMY_BOSS_TELEPORT_STATE_FINISHED_DELAY,
7 | ENEMY_BOSS_TELEPORT_STATE_INITIAL_DELAY,
8 | } from '../../../../../../common/config';
9 | import { DIRECTION } from '../../../../../../common/common';
10 |
11 | export class BossDrowTeleportState extends BaseCharacterState {
12 | #possibleTeleportLocations: Phaser.Math.Vector2[];
13 |
14 | constructor(gameObject: CharacterGameObject, possibleTeleportLocations: Phaser.Math.Vector2[]) {
15 | super(CHARACTER_STATES.TELEPORT_STATE, gameObject);
16 | this.#possibleTeleportLocations = possibleTeleportLocations;
17 | }
18 |
19 | public onEnter(): void {
20 | this._gameObject.invulnerableComponent.invulnerable = true;
21 |
22 | const timeEvent = this._gameObject.scene.time.addEvent({
23 | delay: ENEMY_BOSS_TELEPORT_STATE_INITIAL_DELAY,
24 | callback: () => {
25 | if (timeEvent.getOverallProgress() === 1) {
26 | this.#handleTeleportFinished();
27 | return;
28 | }
29 | this._gameObject.direction = DIRECTION.DOWN;
30 | this._gameObject.animationComponent.playAnimation(`IDLE_${this._gameObject.direction}`);
31 | const location =
32 | this.#possibleTeleportLocations[timeEvent.repeatCount % this.#possibleTeleportLocations.length];
33 | this._gameObject.setPosition(location.x, location.y);
34 | },
35 | callbackScope: this,
36 | repeat: this.#possibleTeleportLocations.length * 3 - 1,
37 | });
38 | }
39 |
40 | #handleTeleportFinished(): void {
41 | this._gameObject.visible = false;
42 | this._gameObject.scene.time.delayedCall(ENEMY_BOSS_TELEPORT_STATE_FINISHED_DELAY, () => {
43 | const randomLocation = Phaser.Utils.Array.GetRandom(this.#possibleTeleportLocations);
44 | this._gameObject.setPosition(randomLocation.x, randomLocation.y);
45 | this._gameObject.visible = true;
46 | this._gameObject.invulnerableComponent.invulnerable = false;
47 | this._stateMachine.setState(CHARACTER_STATES.PREPARE_ATTACK_STATE);
48 | });
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/state-machine/states/character/bounce-move-state.ts:
--------------------------------------------------------------------------------
1 | import * as Phaser from 'phaser';
2 | import { BaseCharacterState } from './base-character-state';
3 | import { CHARACTER_STATES } from './character-states';
4 | import { CharacterGameObject } from '../../../../game-objects/common/character-game-object';
5 |
6 | export class BounceMoveState extends BaseCharacterState {
7 | constructor(gameObject: CharacterGameObject) {
8 | super(CHARACTER_STATES.BOUNCE_MOVE_STATE, gameObject);
9 | }
10 |
11 | public onEnter(): void {
12 | // play idle animation based on game object direction
13 | this._gameObject.animationComponent.playAnimation(`IDLE_${this._gameObject.direction}`);
14 |
15 | // pick a random direction to start moving towards
16 | const speed = this._gameObject.speed;
17 | const randomDirection = Phaser.Math.Between(0, 3);
18 | if (randomDirection === 0) {
19 | this._gameObject.setVelocity(speed, speed * -1);
20 | } else if (randomDirection === 1) {
21 | this._gameObject.setVelocity(speed, speed);
22 | } else if (randomDirection === 2) {
23 | this._gameObject.setVelocity(speed * -1, speed);
24 | } else {
25 | this._gameObject.setVelocity(speed * -1, speed * -1);
26 | }
27 | this._gameObject.setBounce(1);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/state-machine/states/character/character-states.ts:
--------------------------------------------------------------------------------
1 | export const CHARACTER_STATES = {
2 | IDLE_STATE: 'IDLE_STATE',
3 | MOVE_STATE: 'MOVE_STATE',
4 | BOUNCE_MOVE_STATE: 'BOUNCE_MOVE_STATE',
5 | HURT_STATE: 'HURT_STATE',
6 | DEATH_STATE: 'DEATH_STATE',
7 | LIFT_STATE: 'LIFT_STATE',
8 | OPEN_CHEST_STATE: 'OPEN_CHEST_STATE',
9 | IDLE_HOLDING_STATE: 'IDLE_HOLDING_STATE',
10 | MOVE_HOLDING_STATE: 'MOVE_HOLDING_STATE',
11 | THROW_STATE: 'THROW_STATE',
12 | ATTACK_STATE: 'ATTACK_STATE',
13 | HIDDEN_STATE: 'HIDDEN_STATE',
14 | TELEPORT_STATE: 'TELEPORT_STATE',
15 | PREPARE_ATTACK_STATE: 'PREPARE_ATTACK_STATE',
16 | } as const;
17 |
--------------------------------------------------------------------------------
/src/components/state-machine/states/character/death-state.ts:
--------------------------------------------------------------------------------
1 | import { CharacterGameObject } from '../../../../game-objects/common/character-game-object';
2 | import { CHARACTER_ANIMATIONS } from '../../../../common/assets';
3 | import { BaseCharacterState } from './base-character-state';
4 | import { CHARACTER_STATES } from './character-states';
5 | import { HeldGameObjectComponent } from '../../../game-object/held-game-object-component';
6 | import { ThrowableObjectComponent } from '../../../game-object/throwable-object-component';
7 | import { CUSTOM_EVENTS, EVENT_BUS } from '../../../../common/event-bus';
8 |
9 | export class DeathState extends BaseCharacterState {
10 | #onDieCallback: () => void;
11 |
12 | constructor(gameObject: CharacterGameObject, onDieCallback: () => void = () => undefined) {
13 | super(CHARACTER_STATES.DEATH_STATE, gameObject);
14 | this.#onDieCallback = onDieCallback;
15 | }
16 |
17 | public onEnter(): void {
18 | // reset game object velocity
19 | this._resetObjectVelocity();
20 |
21 | // drop held object
22 | const heldComponent = HeldGameObjectComponent.getComponent(this._gameObject);
23 | if (heldComponent !== undefined && heldComponent.object !== undefined) {
24 | const throwObjectComponent = ThrowableObjectComponent.getComponent(
25 | heldComponent.object,
26 | );
27 | if (throwObjectComponent !== undefined) {
28 | throwObjectComponent.drop();
29 | }
30 | heldComponent.drop();
31 | }
32 |
33 | // make character invulnerable after taking a hit
34 | this._gameObject.invulnerableComponent.invulnerable = true;
35 |
36 | // disable body on game object so we stop triggering the collision
37 | (this._gameObject.body as Phaser.Physics.Arcade.Body).enable = false;
38 |
39 | // play animation for character dying
40 | this._gameObject.animationComponent.playAnimation(CHARACTER_ANIMATIONS.DIE_DOWN, () => {
41 | this.#triggerDefeatedEvent();
42 | });
43 | }
44 |
45 | #triggerDefeatedEvent(): void {
46 | this._gameObject.disableObject();
47 | if (this._gameObject.isEnemy) {
48 | EVENT_BUS.emit(CUSTOM_EVENTS.ENEMY_DESTROYED);
49 | } else {
50 | EVENT_BUS.emit(CUSTOM_EVENTS.PLAYER_DEFEATED);
51 | }
52 | this.#onDieCallback();
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/components/state-machine/states/character/hurt-state.ts:
--------------------------------------------------------------------------------
1 | import { CharacterGameObject } from '../../../../game-objects/common/character-game-object';
2 | import { DIRECTION } from '../../../../common/common';
3 | import { BaseCharacterState } from './base-character-state';
4 | import { CHARACTER_STATES } from './character-states';
5 | import { exhaustiveGuard, isArcadePhysicsBody } from '../../../../common/utils';
6 | import { Direction } from '../../../../common/types';
7 | import { HURT_PUSH_BACK_DELAY } from '../../../../common/config';
8 | import { CHARACTER_ANIMATIONS } from '../../../../common/assets';
9 | import { HeldGameObjectComponent } from '../../../game-object/held-game-object-component';
10 | import { ThrowableObjectComponent } from '../../../game-object/throwable-object-component';
11 |
12 | export class HurtState extends BaseCharacterState {
13 | #hurtPushBackSpeed: number;
14 | #onHurtCallback: () => void;
15 | #nextState: string;
16 |
17 | constructor(
18 | gameObject: CharacterGameObject,
19 | hurtPushBackSpeed: number,
20 | onHurtCallback: () => void = () => undefined,
21 | nextState: string = CHARACTER_STATES.IDLE_STATE,
22 | ) {
23 | super(CHARACTER_STATES.HURT_STATE, gameObject);
24 | this.#hurtPushBackSpeed = hurtPushBackSpeed;
25 | this.#onHurtCallback = onHurtCallback;
26 | this.#nextState = nextState;
27 | }
28 |
29 | public onEnter(args: unknown[]): void {
30 | const attackDirection = args[0] as Direction;
31 |
32 | // reset game object velocity
33 | this._resetObjectVelocity();
34 |
35 | const heldComponent = HeldGameObjectComponent.getComponent(this._gameObject);
36 | if (heldComponent !== undefined && heldComponent.object !== undefined) {
37 | const throwObjectComponent = ThrowableObjectComponent.getComponent(
38 | heldComponent.object,
39 | );
40 | if (throwObjectComponent !== undefined) {
41 | throwObjectComponent.drop();
42 | }
43 | heldComponent.drop();
44 | }
45 |
46 | if (isArcadePhysicsBody(this._gameObject.body)) {
47 | const body = this._gameObject.body;
48 |
49 | switch (attackDirection) {
50 | case DIRECTION.DOWN:
51 | body.velocity.y = this.#hurtPushBackSpeed;
52 | break;
53 | case DIRECTION.UP:
54 | body.velocity.y = this.#hurtPushBackSpeed * -1;
55 | break;
56 | case DIRECTION.LEFT:
57 | body.velocity.x = this.#hurtPushBackSpeed * -1;
58 | break;
59 | case DIRECTION.RIGHT:
60 | body.velocity.x = this.#hurtPushBackSpeed;
61 | break;
62 | default:
63 | exhaustiveGuard(attackDirection);
64 | }
65 |
66 | // wait a certain amount of time before resetting velocity to stop the push back
67 | this._gameObject.scene.time.delayedCall(HURT_PUSH_BACK_DELAY, () => {
68 | this._resetObjectVelocity();
69 | });
70 | }
71 |
72 | // make character invulnerable after taking a hit
73 | this._gameObject.invulnerableComponent.invulnerable = true;
74 | // call callback so we can do custom animations if provided
75 | this.#onHurtCallback();
76 |
77 | // play animation for character being hurt
78 | this._gameObject.animationComponent.playAnimation(CHARACTER_ANIMATIONS.HURT_DOWN, () => {
79 | this.#transition();
80 | });
81 | }
82 |
83 | #transition(): void {
84 | // wait set amount of time before making character vulnerable again
85 | this._gameObject.scene.time.delayedCall(
86 | this._gameObject.invulnerableComponent.invulnerableAfterHitAnimationDuration,
87 | () => {
88 | this._gameObject.invulnerableComponent.invulnerable = false;
89 | },
90 | );
91 | this._stateMachine.setState(this.#nextState);
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/components/state-machine/states/character/idle-holding-state.ts:
--------------------------------------------------------------------------------
1 | import { CharacterGameObject } from '../../../../game-objects/common/character-game-object';
2 | import { BaseCharacterState } from './base-character-state';
3 | import { CHARACTER_STATES } from './character-states';
4 |
5 | export class IdleHoldingState extends BaseCharacterState {
6 | constructor(gameObject: CharacterGameObject) {
7 | super(CHARACTER_STATES.IDLE_HOLDING_STATE, gameObject);
8 | }
9 |
10 | public onEnter(): void {
11 | // play idle animation based on game object direction
12 | this._gameObject.animationComponent.playAnimation(`IDLE_HOLD_${this._gameObject.direction}`);
13 |
14 | // reset game object velocity
15 | this._resetObjectVelocity();
16 | }
17 |
18 | public onUpdate(): void {
19 | const controls = this._gameObject.controls;
20 |
21 | // if action key was pressed, throw item
22 | if (controls.isActionKeyJustDown) {
23 | this._stateMachine.setState(CHARACTER_STATES.THROW_STATE);
24 | return;
25 | }
26 |
27 | // if no other input is provided, do nothing
28 | if (!controls.isDownDown && !controls.isUpDown && !controls.isLeftDown && !controls.isRightDown) {
29 | return;
30 | }
31 |
32 | this._stateMachine.setState(CHARACTER_STATES.MOVE_HOLDING_STATE);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/state-machine/states/character/idle-state.ts:
--------------------------------------------------------------------------------
1 | import { BaseCharacterState } from './base-character-state';
2 | import { CHARACTER_STATES } from './character-states';
3 | import { CharacterGameObject } from '../../../../game-objects/common/character-game-object';
4 | import { HeldGameObjectComponent } from '../../../game-object/held-game-object-component';
5 | import { ThrowableObjectComponent } from '../../../game-object/throwable-object-component';
6 |
7 | export class IdleState extends BaseCharacterState {
8 | constructor(gameObject: CharacterGameObject) {
9 | super(CHARACTER_STATES.IDLE_STATE, gameObject);
10 | }
11 |
12 | public onEnter(): void {
13 | // play idle animation based on game object direction
14 | this._gameObject.animationComponent.playAnimation(`IDLE_${this._gameObject.direction}`);
15 |
16 | // reset game object velocity
17 | this._resetObjectVelocity();
18 |
19 | const heldComponent = HeldGameObjectComponent.getComponent(this._gameObject);
20 | if (heldComponent !== undefined && heldComponent.object !== undefined) {
21 | const throwObjectComponent = ThrowableObjectComponent.getComponent(
22 | heldComponent.object,
23 | );
24 | if (throwObjectComponent !== undefined) {
25 | throwObjectComponent.drop();
26 | }
27 | heldComponent.drop();
28 | }
29 | }
30 |
31 | public onUpdate(): void {
32 | const controls = this._gameObject.controls;
33 |
34 | if (controls.isMovementLocked) {
35 | return;
36 | }
37 |
38 | // if attack key was pressed, attack with weapon
39 | if (controls.isAttackKeyJustDown) {
40 | this._stateMachine.setState(CHARACTER_STATES.ATTACK_STATE);
41 | return;
42 | }
43 |
44 | // if no other input is provided, do nothing
45 | if (!controls.isDownDown && !controls.isUpDown && !controls.isLeftDown && !controls.isRightDown) {
46 | return;
47 | }
48 |
49 | this._stateMachine.setState(CHARACTER_STATES.MOVE_STATE);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/state-machine/states/character/lift-state.ts:
--------------------------------------------------------------------------------
1 | import * as Phaser from 'phaser';
2 | import { BaseCharacterState } from './base-character-state';
3 | import { CHARACTER_STATES } from './character-states';
4 | import { CharacterGameObject } from '../../../../game-objects/common/character-game-object';
5 | import { isArcadePhysicsBody } from '../../../../common/utils';
6 | import { HeldGameObjectComponent } from '../../../game-object/held-game-object-component';
7 | import { GameObject } from '../../../../common/types';
8 | import {
9 | LIFT_ITEM_ANIMATION_DELAY,
10 | LIFT_ITEM_ANIMATION_DURATION,
11 | LIFT_ITEM_ANIMATION_ENABLE_DEBUGGING,
12 | } from '../../../../common/config';
13 |
14 | export class LiftState extends BaseCharacterState {
15 | constructor(gameObject: CharacterGameObject) {
16 | super(CHARACTER_STATES.LIFT_STATE, gameObject);
17 | }
18 |
19 | public onEnter(args: unknown[]): void {
20 | const gameObjectBeingPickedUp = args[0] as GameObject;
21 |
22 | // reset game object velocity
23 | this._resetObjectVelocity();
24 |
25 | const heldComponent = HeldGameObjectComponent.getComponent(this._gameObject);
26 | if (heldComponent === undefined) {
27 | this._stateMachine.setState(CHARACTER_STATES.IDLE_STATE);
28 | return;
29 | }
30 |
31 | // play lift animation and then transition to hold item state
32 | this._gameObject.animationComponent.playAnimation(`LIFT_${this._gameObject.direction}`);
33 |
34 | // store a reference to the lifted up game object
35 | heldComponent.object = gameObjectBeingPickedUp;
36 |
37 | // disable body on the lifted up game object
38 | if (isArcadePhysicsBody(gameObjectBeingPickedUp.body)) {
39 | gameObjectBeingPickedUp.body.enable = false;
40 | }
41 |
42 | // have character carry the object
43 | gameObjectBeingPickedUp.setDepth(2).setOrigin(0.5, 0.5);
44 | // create curved path for ball to follow
45 | const startPoint = new Phaser.Math.Vector2(gameObjectBeingPickedUp.x + 8, gameObjectBeingPickedUp.y - 8);
46 | const controlPoint1 = new Phaser.Math.Vector2(gameObjectBeingPickedUp.x + 8, gameObjectBeingPickedUp.y - 24);
47 | const controlPoint2 = new Phaser.Math.Vector2(gameObjectBeingPickedUp.x + 8, gameObjectBeingPickedUp.y - 24);
48 | const endPoint = new Phaser.Math.Vector2(this._gameObject.x, this._gameObject.y - 8);
49 | const curve = new Phaser.Curves.CubicBezier(startPoint, controlPoint1, controlPoint2, endPoint);
50 | const curvePath = new Phaser.Curves.Path(startPoint.x, startPoint.y).add(curve);
51 |
52 | // draw curve (for debugging)
53 | let g: Phaser.GameObjects.Graphics | undefined;
54 | if (LIFT_ITEM_ANIMATION_ENABLE_DEBUGGING) {
55 | g = this._gameObject.scene.add.graphics();
56 | g.clear();
57 | g.lineStyle(4, 0x00ff00, 1);
58 | curvePath.draw(g);
59 | }
60 | gameObjectBeingPickedUp.setAlpha(0);
61 |
62 | const follower = this._gameObject.scene.add
63 | .follower(curvePath, startPoint.x, startPoint.y, gameObjectBeingPickedUp.texture)
64 | .setAlpha(1);
65 | follower.startFollow({
66 | delay: LIFT_ITEM_ANIMATION_DELAY,
67 | duration: LIFT_ITEM_ANIMATION_DURATION,
68 | onComplete: () => {
69 | follower.destroy();
70 | if (g !== undefined) {
71 | g.destroy();
72 | }
73 | gameObjectBeingPickedUp.setPosition(follower.x, follower.y).setAlpha(1);
74 | },
75 | });
76 | }
77 |
78 | public onUpdate(): void {
79 | if (this._gameObject.animationComponent.isAnimationPlaying()) {
80 | return;
81 | }
82 |
83 | this._stateMachine.setState(CHARACTER_STATES.IDLE_HOLDING_STATE);
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/components/state-machine/states/character/move-holding-state.ts:
--------------------------------------------------------------------------------
1 | import { DIRECTION } from '../../../../common/common';
2 | import { CharacterGameObject } from '../../../../game-objects/common/character-game-object';
3 | import { HeldGameObjectComponent } from '../../../game-object/held-game-object-component';
4 | import { BaseMoveState } from './base-move-state';
5 | import { CHARACTER_STATES } from './character-states';
6 |
7 | export class MoveHoldingState extends BaseMoveState {
8 | constructor(gameObject: CharacterGameObject) {
9 | super(CHARACTER_STATES.MOVE_HOLDING_STATE, gameObject, 'WALK_HOLD');
10 | }
11 |
12 | public onUpdate(): void {
13 | const controls = this._gameObject.controls;
14 |
15 | // if action key was pressed, throw item
16 | if (controls.isActionKeyJustDown) {
17 | this._stateMachine.setState(CHARACTER_STATES.THROW_STATE);
18 | return;
19 | }
20 |
21 | // if no input is provided transition back to idle state
22 | if (this.isNoInputMovement(controls)) {
23 | this._stateMachine.setState(CHARACTER_STATES.IDLE_HOLDING_STATE);
24 | return;
25 | }
26 |
27 | // handle character movement
28 | this.handleCharacterMovement();
29 |
30 | const heldComponent = HeldGameObjectComponent.getComponent(this._gameObject);
31 | if (heldComponent === undefined || heldComponent.object === undefined) {
32 | this._stateMachine.setState(CHARACTER_STATES.IDLE_STATE);
33 | return;
34 | }
35 |
36 | if (this._gameObject.direction === DIRECTION.DOWN) {
37 | heldComponent.object.setPosition(this._gameObject.x + 1, this._gameObject.y - 2);
38 | } else if (this._gameObject.direction === DIRECTION.UP) {
39 | heldComponent.object.setPosition(this._gameObject.x + 1, this._gameObject.y - 6);
40 | } else {
41 | heldComponent.object.setPosition(this._gameObject.x, this._gameObject.y - 8);
42 | }
43 | if (this._gameObject.flipX) {
44 | heldComponent.object.setX(this._gameObject.x);
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/components/state-machine/states/character/move-state.ts:
--------------------------------------------------------------------------------
1 | import { INTERACTIVE_OBJECT_TYPE } from '../../../../common/common';
2 | import { CHARACTER_STATES } from './character-states';
3 | import { exhaustiveGuard } from '../../../../common/utils';
4 | import { CharacterGameObject } from '../../../../game-objects/common/character-game-object';
5 | import { CollidingObjectsComponent } from '../../../game-object/colliding-objects-component';
6 | import { InteractiveObjectComponent } from '../../../game-object/interactive-object-component';
7 | import { InputComponent } from '../../../input/input-component';
8 | import { BaseMoveState } from './base-move-state';
9 |
10 | export class MoveState extends BaseMoveState {
11 | constructor(gameObject: CharacterGameObject) {
12 | super(CHARACTER_STATES.MOVE_STATE, gameObject, 'WALK');
13 | }
14 |
15 | public onUpdate(): void {
16 | const controls = this._gameObject.controls;
17 |
18 | // if attack key was pressed, attack with weapon
19 | if (controls.isAttackKeyJustDown) {
20 | this._stateMachine.setState(CHARACTER_STATES.ATTACK_STATE);
21 | return;
22 | }
23 |
24 | // if no input is provided transition back to idle state
25 | if (this.isNoInputMovement(controls)) {
26 | this._stateMachine.setState(CHARACTER_STATES.IDLE_STATE);
27 | return;
28 | }
29 |
30 | // if we interacted with an object and switched states, stop processing
31 | if (this.#checkIfObjectWasInteractedWith(controls)) {
32 | return;
33 | }
34 |
35 | // handle character movement
36 | this.handleCharacterMovement();
37 | }
38 |
39 | #checkIfObjectWasInteractedWith(controls: InputComponent): boolean {
40 | const collideComponent = CollidingObjectsComponent.getComponent(this._gameObject);
41 | if (collideComponent === undefined || collideComponent.objects.length === 0) {
42 | return false;
43 | }
44 |
45 | const collisionObject = collideComponent.objects[0];
46 | const interactiveObjectComponent =
47 | InteractiveObjectComponent.getComponent(collisionObject);
48 | if (interactiveObjectComponent === undefined) {
49 | return false;
50 | }
51 |
52 | if (!controls.isActionKeyJustDown) {
53 | return false;
54 | }
55 |
56 | // check if game object can be interacted with
57 | if (!interactiveObjectComponent.canInteractWith()) {
58 | return false;
59 | }
60 | interactiveObjectComponent.interact();
61 |
62 | // we can carry this item
63 | if (interactiveObjectComponent.objectType === INTERACTIVE_OBJECT_TYPE.PICKUP) {
64 | this._stateMachine.setState(CHARACTER_STATES.LIFT_STATE, collisionObject);
65 | return true;
66 | }
67 |
68 | // we can open this item
69 | if (interactiveObjectComponent.objectType === INTERACTIVE_OBJECT_TYPE.OPEN) {
70 | this._stateMachine.setState(CHARACTER_STATES.OPEN_CHEST_STATE, collisionObject);
71 | return true;
72 | }
73 |
74 | if (interactiveObjectComponent.objectType === INTERACTIVE_OBJECT_TYPE.AUTO) {
75 | return false;
76 | }
77 |
78 | // we should never hit this code block
79 | exhaustiveGuard(interactiveObjectComponent.objectType);
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/components/state-machine/states/character/open-chest-state.ts:
--------------------------------------------------------------------------------
1 | import { BaseCharacterState } from './base-character-state';
2 | import { CHARACTER_STATES } from './character-states';
3 | import { CharacterGameObject } from '../../../../game-objects/common/character-game-object';
4 | import { Chest } from '../../../../game-objects/objects/chest';
5 | import { CUSTOM_EVENTS, EVENT_BUS } from '../../../../common/event-bus';
6 |
7 | export class OpenChestState extends BaseCharacterState {
8 | constructor(gameObject: CharacterGameObject) {
9 | super(CHARACTER_STATES.OPEN_CHEST_STATE, gameObject);
10 | }
11 |
12 | onEnter(args: unknown[]): void {
13 | const chest = args[0] as Chest;
14 |
15 | // make character invulnerable so we can collect the item
16 | this._gameObject.invulnerableComponent.invulnerable = true;
17 |
18 | // reset game object velocity
19 | this._resetObjectVelocity();
20 |
21 | // play lift animation based on game object direction
22 | this._gameObject.animationComponent.playAnimation(`LIFT_${this._gameObject.direction}`, () => {
23 | // emit event data regarding chest
24 | EVENT_BUS.emit(CUSTOM_EVENTS.OPENED_CHEST, chest);
25 | // after showing message to player, transition to idle state
26 | EVENT_BUS.once(CUSTOM_EVENTS.DIALOG_CLOSED, () => {
27 | // make character vulnerable so we can take damage
28 | this._gameObject.invulnerableComponent.invulnerable = false;
29 | this._stateMachine.setState(CHARACTER_STATES.IDLE_STATE);
30 | });
31 | });
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/state-machine/states/character/throw-state.ts:
--------------------------------------------------------------------------------
1 | import { CharacterGameObject } from '../../../../game-objects/common/character-game-object';
2 | import { HeldGameObjectComponent } from '../../../game-object/held-game-object-component';
3 | import { ThrowableObjectComponent } from '../../../game-object/throwable-object-component';
4 | import { BaseCharacterState } from './base-character-state';
5 | import { CHARACTER_STATES } from './character-states';
6 |
7 | export class ThrowState extends BaseCharacterState {
8 | constructor(gameObject: CharacterGameObject) {
9 | super(CHARACTER_STATES.THROW_STATE, gameObject);
10 | }
11 |
12 | public onEnter(): void {
13 | // reset game object velocity
14 | this._resetObjectVelocity();
15 |
16 | // play lift animation to throw items
17 | this._gameObject.animationComponent.playAnimationInReverse(`LIFT_${this._gameObject.direction}`);
18 |
19 | // get item held by character and see if this is a throwable item
20 | const heldComponent = HeldGameObjectComponent.getComponent(this._gameObject);
21 | if (heldComponent === undefined || heldComponent.object === undefined) {
22 | return;
23 | }
24 | const throwObjectComponent = ThrowableObjectComponent.getComponent(heldComponent.object);
25 | if (throwObjectComponent !== undefined) {
26 | throwObjectComponent.throw(this._gameObject.direction);
27 | }
28 | heldComponent.drop();
29 | }
30 |
31 | public onUpdate(): void {
32 | if (this._gameObject.animationComponent.isAnimationPlaying()) {
33 | return;
34 | }
35 |
36 | this._stateMachine.setState(CHARACTER_STATES.IDLE_STATE);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/game-objects/common/character-game-object.ts:
--------------------------------------------------------------------------------
1 | import * as Phaser from 'phaser';
2 | import { CustomGameObject, Direction, Position } from '../../common/types';
3 | import { InputComponent } from '../../components/input/input-component';
4 | import { ControlsComponent } from '../../components/game-object/controls-component';
5 | import { StateMachine } from '../../components/state-machine/state-machine';
6 | import { SpeedComponent } from '../../components/game-object/speed-component';
7 | import { DirectionComponent } from '../../components/game-object/direction-component';
8 | import { AnimationComponent, AnimationConfig } from '../../components/game-object/animation-component';
9 | import { InvulnerableComponent } from '../../components/game-object/invulnerable-component';
10 | import { CHARACTER_STATES } from '../../components/state-machine/states/character/character-states';
11 | import { LifeComponent } from '../../components/game-object/life-component';
12 | import { DataManager } from '../../common/data-manager';
13 | import { WeaponComponent } from '../../components/game-object/weapon-component';
14 |
15 | export type CharacterConfig = {
16 | scene: Phaser.Scene;
17 | position: Position;
18 | assetKey: string;
19 | frame?: number;
20 | inputComponent: InputComponent;
21 | animationConfig: AnimationConfig;
22 | speed: number;
23 | id?: string;
24 | isPlayer: boolean;
25 | isInvulnerable?: boolean;
26 | invulnerableAfterHitAnimationDuration?: number;
27 | maxLife: number;
28 | currentLife?: number;
29 | };
30 |
31 | export abstract class CharacterGameObject extends Phaser.Physics.Arcade.Sprite implements CustomGameObject {
32 | protected _controlsComponent: ControlsComponent;
33 | protected _speedComponent: SpeedComponent;
34 | protected _directionComponent: DirectionComponent;
35 | protected _animationComponent: AnimationComponent;
36 | protected _invulnerableComponent: InvulnerableComponent;
37 | protected _lifeComponent: LifeComponent;
38 | protected _stateMachine: StateMachine;
39 | protected _isPlayer: boolean;
40 | protected _isDefeated: boolean;
41 |
42 | constructor(config: CharacterConfig) {
43 | const {
44 | scene,
45 | position,
46 | assetKey,
47 | frame,
48 | speed,
49 | animationConfig,
50 | inputComponent,
51 | id,
52 | isPlayer,
53 | invulnerableAfterHitAnimationDuration,
54 | isInvulnerable,
55 | maxLife,
56 | currentLife,
57 | } = config;
58 | const { x, y } = position;
59 | super(scene, x, y, assetKey, frame || 0);
60 |
61 | // add object to scene and enable phaser physics
62 | scene.add.existing(this);
63 | scene.physics.add.existing(this);
64 |
65 | // add shared components
66 | this._controlsComponent = new ControlsComponent(this, inputComponent);
67 | this._speedComponent = new SpeedComponent(this, speed);
68 | this._directionComponent = new DirectionComponent(this);
69 | this._animationComponent = new AnimationComponent(this, animationConfig);
70 | this._invulnerableComponent = new InvulnerableComponent(
71 | this,
72 | isInvulnerable || false,
73 | invulnerableAfterHitAnimationDuration,
74 | );
75 | this._lifeComponent = new LifeComponent(this, maxLife, currentLife);
76 |
77 | // create state machine
78 | this._stateMachine = new StateMachine(id);
79 |
80 | // general config
81 | this._isPlayer = isPlayer;
82 | this._isDefeated = false;
83 | if (!this._isPlayer) {
84 | this.disableObject();
85 | }
86 | }
87 |
88 | get isDefeated(): boolean {
89 | return this._isDefeated;
90 | }
91 |
92 | get isEnemy(): boolean {
93 | return !this._isPlayer;
94 | }
95 |
96 | get controls(): InputComponent {
97 | return this._controlsComponent.controls;
98 | }
99 |
100 | get speed(): number {
101 | return this._speedComponent.speed;
102 | }
103 |
104 | get direction(): Direction {
105 | return this._directionComponent.direction;
106 | }
107 |
108 | set direction(value: Direction) {
109 | this._directionComponent.direction = value;
110 | }
111 |
112 | get animationComponent(): AnimationComponent {
113 | return this._animationComponent;
114 | }
115 |
116 | get invulnerableComponent(): InvulnerableComponent {
117 | return this._invulnerableComponent;
118 | }
119 |
120 | get stateMachine(): StateMachine {
121 | return this._stateMachine;
122 | }
123 |
124 | public update(): void {
125 | this._stateMachine.update();
126 | }
127 |
128 | public hit(direction: Direction, damage: number): void {
129 | if (this._isDefeated) {
130 | return;
131 | }
132 |
133 | // check if character is invulnerable, if not update state to be hurt
134 | if (this._invulnerableComponent.invulnerable) {
135 | return;
136 | }
137 |
138 | // have character take damage and see if the character has died
139 | this._lifeComponent.takeDamage(damage);
140 | if (this._isPlayer) {
141 | DataManager.instance.updatePlayerCurrentHealth(this._lifeComponent.life);
142 | }
143 | if (this._lifeComponent.life === 0) {
144 | this._isDefeated = true;
145 | this._stateMachine.setState(CHARACTER_STATES.DEATH_STATE, direction);
146 | return;
147 | }
148 |
149 | this._stateMachine.setState(CHARACTER_STATES.HURT_STATE, direction);
150 | }
151 |
152 | public disableObject(): void {
153 | // disable body on game object so we stop triggering the collision
154 | (this.body as Phaser.Physics.Arcade.Body).enable = false;
155 |
156 | // make not active and not visible until player re-enters room
157 | this.active = false;
158 | if (!this._isPlayer) {
159 | this.visible = false;
160 | }
161 |
162 | const weaponComponent = WeaponComponent.getComponent(this);
163 | if (weaponComponent !== undefined && weaponComponent.weapon !== undefined && weaponComponent.weapon.isAttacking) {
164 | weaponComponent.weapon.onCollisionCallback();
165 | }
166 | }
167 |
168 | public enableObject(): void {
169 | if (this._isDefeated) {
170 | return;
171 | }
172 |
173 | (this.body as Phaser.Physics.Arcade.Body).enable = true;
174 | this.active = true;
175 | this.visible = true;
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/src/game-objects/enemies/boss/drow.ts:
--------------------------------------------------------------------------------
1 | import * as Phaser from 'phaser';
2 | import { ASSET_KEYS, DROW_ANIMATION_KEYS } from '../../../common/assets';
3 | import {
4 | ENEMY_BOSS_DROW_SPEED,
5 | ENEMY_BOSS_DROW_MAX_HEALTH,
6 | BOSS_HURT_PUSH_BACK_DELAY,
7 | ENEMY_BOSS_DROW_DEATH_ANIMATION_DURATION,
8 | ENEMY_BOSS_START_INITIAL_DELAY,
9 | ENEMY_BOSS_ATTACK_DAMAGE,
10 | ENEMY_BOSS_ATTACK_SPEED,
11 | } from '../../../common/config';
12 | import { Position } from '../../../common/types';
13 | import { AnimationConfig } from '../../../components/game-object/animation-component';
14 | import { InputComponent } from '../../../components/input/input-component';
15 | import { CHARACTER_STATES } from '../../../components/state-machine/states/character/character-states';
16 | import { CharacterGameObject } from '../../common/character-game-object';
17 | import { WeaponComponent } from '../../../components/game-object/weapon-component';
18 | import { HurtState } from '../../../components/state-machine/states/character/hurt-state';
19 | import { DeathState } from '../../../components/state-machine/states/character/death-state';
20 | import { flash } from '../../../common/juice-utils';
21 | import { BossDrowHiddenState } from '../../../components/state-machine/states/character/boss/drow/boss-drow-hidden-state';
22 | import { BossDrowPrepareAttackState } from '../../../components/state-machine/states/character/boss/drow/boss-drow-prepare-attack-state';
23 | import { BossDrowTeleportState } from '../../../components/state-machine/states/character/boss/drow/boss-drow-teleport-state';
24 | import { AttackState } from '../../../components/state-machine/states/character/attack-state';
25 | import { BossDrowIdleState } from '../../../components/state-machine/states/character/boss/drow/boss-drow-idle-state';
26 | import { Dagger } from '../../weapons/dagger';
27 | import { CUSTOM_EVENTS, EVENT_BUS } from '../../../common/event-bus';
28 |
29 | type DrowConfig = {
30 | scene: Phaser.Scene;
31 | position: Position;
32 | };
33 |
34 | export class Drow extends CharacterGameObject {
35 | #weaponComponent: WeaponComponent;
36 |
37 | constructor(config: DrowConfig) {
38 | // create animation config for component
39 | const hurtAnimConfig = { key: DROW_ANIMATION_KEYS.HIT, repeat: 0, ignoreIfPlaying: true };
40 | const animationConfig: AnimationConfig = {
41 | WALK_DOWN: { key: DROW_ANIMATION_KEYS.WALK_DOWN, repeat: -1, ignoreIfPlaying: true },
42 | WALK_UP: { key: DROW_ANIMATION_KEYS.WALK_UP, repeat: -1, ignoreIfPlaying: true },
43 | WALK_LEFT: { key: DROW_ANIMATION_KEYS.WALK_LEFT, repeat: -1, ignoreIfPlaying: true },
44 | WALK_RIGHT: { key: DROW_ANIMATION_KEYS.WALK_RIGHT, repeat: -1, ignoreIfPlaying: true },
45 | HURT_DOWN: hurtAnimConfig,
46 | HURT_UP: hurtAnimConfig,
47 | HURT_LEFT: hurtAnimConfig,
48 | HURT_RIGHT: hurtAnimConfig,
49 | IDLE_DOWN: { key: DROW_ANIMATION_KEYS.IDLE_DOWN, repeat: -1, ignoreIfPlaying: true },
50 | IDLE_UP: { key: DROW_ANIMATION_KEYS.IDLE_UP, repeat: -1, ignoreIfPlaying: true },
51 | IDLE_LEFT: { key: DROW_ANIMATION_KEYS.IDLE_SIDE, repeat: -1, ignoreIfPlaying: true },
52 | IDLE_RIGHT: { key: DROW_ANIMATION_KEYS.IDLE_SIDE, repeat: -1, ignoreIfPlaying: true },
53 | };
54 |
55 | super({
56 | scene: config.scene,
57 | position: config.position,
58 | assetKey: ASSET_KEYS.DROW,
59 | frame: 0,
60 | id: `drow-${Phaser.Math.RND.uuid()}`,
61 | isPlayer: false,
62 | animationConfig,
63 | speed: ENEMY_BOSS_DROW_SPEED,
64 | inputComponent: new InputComponent(),
65 | isInvulnerable: false,
66 | maxLife: ENEMY_BOSS_DROW_MAX_HEALTH,
67 | });
68 |
69 | this.#weaponComponent = new WeaponComponent(this);
70 | this.#weaponComponent.weapon = new Dagger(
71 | this,
72 | this.#weaponComponent,
73 | {
74 | DOWN: DROW_ANIMATION_KEYS.ATTACK_DOWN,
75 | UP: DROW_ANIMATION_KEYS.ATTACK_UP,
76 | LEFT: DROW_ANIMATION_KEYS.ATTACK_SIDE,
77 | RIGHT: DROW_ANIMATION_KEYS.ATTACK_SIDE,
78 | },
79 | ENEMY_BOSS_ATTACK_DAMAGE,
80 | ENEMY_BOSS_ATTACK_SPEED,
81 | );
82 |
83 | // add state machine
84 | this._stateMachine.addState(new BossDrowIdleState(this));
85 | this._stateMachine.addState(new BossDrowHiddenState(this));
86 | this._stateMachine.addState(new BossDrowPrepareAttackState(this));
87 | this._stateMachine.addState(
88 | new BossDrowTeleportState(this, [
89 | new Phaser.Math.Vector2(this.scene.scale.width / 2, 80),
90 | new Phaser.Math.Vector2(64, 180),
91 | new Phaser.Math.Vector2(192, 180),
92 | ]),
93 | );
94 | this._stateMachine.addState(new AttackState(this));
95 | this._stateMachine.addState(
96 | new HurtState(this, BOSS_HURT_PUSH_BACK_DELAY, undefined, CHARACTER_STATES.TELEPORT_STATE),
97 | );
98 | this._stateMachine.addState(
99 | new DeathState(this, () => {
100 | this.visible = true;
101 | flash(this, () => {
102 | const fx = this.postFX.addWipe(0.1, 0, 1);
103 | this.scene.add.tween({
104 | targets: fx,
105 | progress: 1,
106 | duration: ENEMY_BOSS_DROW_DEATH_ANIMATION_DURATION,
107 | onComplete: () => {
108 | this.visible = false;
109 | EVENT_BUS.emit(CUSTOM_EVENTS.BOSS_DEFEATED);
110 | },
111 | });
112 | });
113 | }),
114 | );
115 |
116 | this.setScale(1.25);
117 | this.physicsBody.setSize(12, 24, true).setOffset(this.displayWidth / 4, this.displayHeight / 4 - 3);
118 | }
119 |
120 | get physicsBody(): Phaser.Physics.Arcade.Body {
121 | return this.body as Phaser.Physics.Arcade.Body;
122 | }
123 |
124 | public update(): void {
125 | super.update();
126 | this.#weaponComponent.update();
127 | }
128 |
129 | public enableObject(): void {
130 | super.enableObject();
131 |
132 | if (this._isDefeated) {
133 | return;
134 | }
135 |
136 | if (this._stateMachine.currentStateName === undefined) {
137 | this.visible = false;
138 | this.scene.time.delayedCall(ENEMY_BOSS_START_INITIAL_DELAY, () => {
139 | this.visible = true;
140 | this._stateMachine.setState(CHARACTER_STATES.HIDDEN_STATE);
141 | });
142 | }
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/src/game-objects/enemies/spider.ts:
--------------------------------------------------------------------------------
1 | import * as Phaser from 'phaser';
2 | import { Direction, Position } from '../../common/types';
3 | import { InputComponent } from '../../components/input/input-component';
4 | import { IdleState } from '../../components/state-machine/states/character/idle-state';
5 | import { CHARACTER_STATES } from '../../components/state-machine/states/character/character-states';
6 | import { MoveState } from '../../components/state-machine/states/character/move-state';
7 | import {
8 | ENEMY_SPIDER_CHANGE_DIRECTION_DELAY_MAX,
9 | ENEMY_SPIDER_CHANGE_DIRECTION_DELAY_MIN,
10 | ENEMY_SPIDER_CHANGE_DIRECTION_DELAY_WAIT,
11 | ENEMY_SPIDER_HURT_PUSH_BACK_SPEED,
12 | ENEMY_SPIDER_MAX_HEALTH,
13 | ENEMY_SPIDER_SPEED,
14 | } from '../../common/config';
15 | import { AnimationConfig } from '../../components/game-object/animation-component';
16 | import { ASSET_KEYS, SPIDER_ANIMATION_KEYS } from '../../common/assets';
17 | import { CharacterGameObject } from '../common/character-game-object';
18 | import { DIRECTION } from '../../common/common';
19 | import { exhaustiveGuard } from '../../common/utils';
20 | import { HurtState } from '../../components/state-machine/states/character/hurt-state';
21 | import { DeathState } from '../../components/state-machine/states/character/death-state';
22 |
23 | export type SpiderConfig = {
24 | scene: Phaser.Scene;
25 | position: Position;
26 | };
27 |
28 | export class Spider extends CharacterGameObject {
29 | constructor(config: SpiderConfig) {
30 | // create animation config for component
31 | const animConfig = { key: SPIDER_ANIMATION_KEYS.WALK, repeat: -1, ignoreIfPlaying: true };
32 | const hurtAnimConfig = { key: SPIDER_ANIMATION_KEYS.HIT, repeat: 0, ignoreIfPlaying: true };
33 | const deathAnimConfig = { key: SPIDER_ANIMATION_KEYS.DEATH, repeat: 0, ignoreIfPlaying: true };
34 | const animationConfig: AnimationConfig = {
35 | WALK_DOWN: animConfig,
36 | WALK_UP: animConfig,
37 | WALK_LEFT: animConfig,
38 | WALK_RIGHT: animConfig,
39 | IDLE_DOWN: animConfig,
40 | IDLE_UP: animConfig,
41 | IDLE_LEFT: animConfig,
42 | IDLE_RIGHT: animConfig,
43 | HURT_DOWN: hurtAnimConfig,
44 | HURT_UP: hurtAnimConfig,
45 | HURT_LEFT: hurtAnimConfig,
46 | HURT_RIGHT: hurtAnimConfig,
47 | DIE_DOWN: deathAnimConfig,
48 | DIE_UP: deathAnimConfig,
49 | DIE_LEFT: deathAnimConfig,
50 | DIE_RIGHT: deathAnimConfig,
51 | };
52 |
53 | super({
54 | scene: config.scene,
55 | position: config.position,
56 | assetKey: ASSET_KEYS.SPIDER,
57 | frame: 0,
58 | id: `spider-${Phaser.Math.RND.uuid()}`,
59 | isPlayer: false,
60 | animationConfig,
61 | speed: ENEMY_SPIDER_SPEED,
62 | inputComponent: new InputComponent(),
63 | isInvulnerable: false,
64 | maxLife: ENEMY_SPIDER_MAX_HEALTH,
65 | });
66 |
67 | // add shared components
68 | this._directionComponent.callback = (direction: Direction) => {
69 | this.#handleDirectionChange(direction);
70 | };
71 |
72 | // add state machine
73 | this._stateMachine.addState(new IdleState(this));
74 | this._stateMachine.addState(new MoveState(this));
75 | this._stateMachine.addState(new HurtState(this, ENEMY_SPIDER_HURT_PUSH_BACK_SPEED));
76 | this._stateMachine.addState(new DeathState(this));
77 | this._stateMachine.setState(CHARACTER_STATES.IDLE_STATE);
78 | }
79 |
80 | public enableObject(): void {
81 | super.enableObject();
82 |
83 | // start simple ai movement pattern
84 | this.scene.time.addEvent({
85 | delay: Phaser.Math.Between(ENEMY_SPIDER_CHANGE_DIRECTION_DELAY_MIN, ENEMY_SPIDER_CHANGE_DIRECTION_DELAY_MAX),
86 | callback: this.#changeDirection,
87 | callbackScope: this,
88 | loop: false,
89 | });
90 | }
91 |
92 | #handleDirectionChange(direction: Direction): void {
93 | switch (direction) {
94 | case DIRECTION.DOWN:
95 | this.setAngle(0);
96 | return;
97 | case DIRECTION.UP:
98 | this.setAngle(180);
99 | return;
100 | case DIRECTION.LEFT:
101 | this.setAngle(90);
102 | return;
103 | case DIRECTION.RIGHT:
104 | this.setAngle(270);
105 | return;
106 | default:
107 | exhaustiveGuard(direction);
108 | }
109 | }
110 |
111 | #changeDirection(): void {
112 | // reset existing enemy input
113 | this.controls.reset();
114 |
115 | if (!this.active) {
116 | return;
117 | }
118 |
119 | // wait a small period of time and then choose a random direction to move
120 | this.scene.time.delayedCall(ENEMY_SPIDER_CHANGE_DIRECTION_DELAY_WAIT, () => {
121 | const randomDirection = Phaser.Math.Between(0, 3);
122 | if (randomDirection === 0) {
123 | this.controls.isUpDown = true;
124 | } else if (randomDirection === 1) {
125 | this.controls.isRightDown = true;
126 | } else if (randomDirection === 2) {
127 | this.controls.isDownDown = true;
128 | } else {
129 | this.controls.isLeftDown = true;
130 | }
131 |
132 | // set up event for next direction change
133 | this.scene.time.addEvent({
134 | delay: Phaser.Math.Between(ENEMY_SPIDER_CHANGE_DIRECTION_DELAY_MIN, ENEMY_SPIDER_CHANGE_DIRECTION_DELAY_MAX),
135 | callback: this.#changeDirection,
136 | callbackScope: this,
137 | loop: false,
138 | });
139 | });
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/src/game-objects/enemies/wisp.ts:
--------------------------------------------------------------------------------
1 | import * as Phaser from 'phaser';
2 | import { Position } from '../../common/types';
3 | import { InputComponent } from '../../components/input/input-component';
4 | import { CHARACTER_STATES } from '../../components/state-machine/states/character/character-states';
5 | import {
6 | ENEMY_WISP_MAX_HEALTH,
7 | ENEMY_WISP_PULSE_ANIMATION_DURATION,
8 | ENEMY_WISP_PULSE_ANIMATION_SCALE_X,
9 | ENEMY_WISP_PULSE_ANIMATION_SCALE_Y,
10 | ENEMY_WISP_SPEED,
11 | } from '../../common/config';
12 | import { AnimationConfig } from '../../components/game-object/animation-component';
13 | import { ASSET_KEYS, WISP_ANIMATION_KEYS } from '../../common/assets';
14 | import { CharacterGameObject } from '../common/character-game-object';
15 | import { BounceMoveState } from '../../components/state-machine/states/character/bounce-move-state';
16 |
17 | export type WispConfig = {
18 | scene: Phaser.Scene;
19 | position: Position;
20 | };
21 |
22 | export class Wisp extends CharacterGameObject {
23 | constructor(config: WispConfig) {
24 | // create animation config for component
25 | const animConfig = { key: WISP_ANIMATION_KEYS.IDLE, repeat: -1, ignoreIfPlaying: true };
26 | const animationConfig: AnimationConfig = {
27 | IDLE_DOWN: animConfig,
28 | IDLE_UP: animConfig,
29 | IDLE_LEFT: animConfig,
30 | IDLE_RIGHT: animConfig,
31 | };
32 |
33 | super({
34 | scene: config.scene,
35 | position: config.position,
36 | assetKey: ASSET_KEYS.WISP,
37 | frame: 0,
38 | id: `wisp-${Phaser.Math.RND.uuid()}`,
39 | isPlayer: false,
40 | animationConfig,
41 | speed: ENEMY_WISP_SPEED,
42 | inputComponent: new InputComponent(),
43 | isInvulnerable: true,
44 | maxLife: ENEMY_WISP_MAX_HEALTH,
45 | });
46 |
47 | // add state machine
48 | this._stateMachine.addState(new BounceMoveState(this));
49 | this._stateMachine.setState(CHARACTER_STATES.BOUNCE_MOVE_STATE);
50 |
51 | // custom animation for movement
52 | this.scene.tweens.add({
53 | targets: this,
54 | scaleX: ENEMY_WISP_PULSE_ANIMATION_SCALE_X,
55 | scaleY: ENEMY_WISP_PULSE_ANIMATION_SCALE_Y,
56 | yoyo: true,
57 | repeat: -1,
58 | duration: ENEMY_WISP_PULSE_ANIMATION_DURATION,
59 | });
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/game-objects/objects/button.ts:
--------------------------------------------------------------------------------
1 | import * as Phaser from 'phaser';
2 | import { SwitchAction, TiledSwitchObject } from '../../common/tiled/types';
3 | import { CustomGameObject } from '../../common/types';
4 | import { SWITCH_TEXTURE } from '../../common/tiled/common';
5 | import { ASSET_KEYS, BUTTON_FRAME_KEYS } from '../../common/assets';
6 |
7 | type ButtonPressedEvent = {
8 | action: SwitchAction;
9 | targetIds: number[];
10 | };
11 |
12 | export class Button extends Phaser.Physics.Arcade.Image implements CustomGameObject {
13 | #switchTargetIds: number[];
14 | #switchAction: SwitchAction;
15 |
16 | constructor(scene: Phaser.Scene, config: TiledSwitchObject) {
17 | const frame =
18 | config.texture === SWITCH_TEXTURE.FLOOR ? BUTTON_FRAME_KEYS.FLOOR_SWITCH : BUTTON_FRAME_KEYS.PLATE_SWITCH;
19 | super(scene, config.x, config.y, ASSET_KEYS.DUNGEON_OBJECTS, frame);
20 |
21 | // add object to scene and enable phaser physics
22 | scene.add.existing(this);
23 | scene.physics.add.existing(this);
24 | this.setOrigin(0, 1).setImmovable(true);
25 |
26 | this.#switchTargetIds = config.targetIds;
27 | this.#switchAction = config.action;
28 |
29 | // disable physics body and make game objects inactive/not visible
30 | this.disableObject();
31 | }
32 |
33 | public press(): ButtonPressedEvent {
34 | this.disableObject();
35 |
36 | // return data about button being pressed with metadata tied to action
37 | return {
38 | action: this.#switchAction,
39 | targetIds: this.#switchTargetIds,
40 | };
41 | }
42 |
43 | public disableObject(): void {
44 | // disable body on game object so we stop triggering the collision
45 | (this.body as Phaser.Physics.Arcade.Body).enable = false;
46 | // make not visible until player re-enters room
47 | this.active = false;
48 | this.visible = false;
49 | }
50 |
51 | public enableObject(): void {
52 | (this.body as Phaser.Physics.Arcade.Body).enable = true;
53 | this.active = true;
54 | this.visible = true;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/game-objects/objects/chest.ts:
--------------------------------------------------------------------------------
1 | import * as Phaser from 'phaser';
2 | import { ASSET_KEYS, CHEST_FRAME_KEYS } from '../../common/assets';
3 | import { CHEST_STATE, INTERACTIVE_OBJECT_TYPE, LEVEL_NAME } from '../../common/common';
4 | import { ChestState, CustomGameObject } from '../../common/types';
5 | import { InteractiveObjectComponent } from '../../components/game-object/interactive-object-component';
6 | import { ChestReward, TiledChestObject, TrapType } from '../../common/tiled/types';
7 | import { TRAP_TYPE } from '../../common/tiled/common';
8 | import { InventoryManager } from '../../components/inventory/inventory-manager';
9 | import { DataManager } from '../../common/data-manager';
10 |
11 | export class Chest extends Phaser.Physics.Arcade.Image implements CustomGameObject {
12 | #state: ChestState;
13 | #isBossKeyChest: boolean;
14 | #id: number;
15 | #revealTrigger: TrapType;
16 | #contents: ChestReward;
17 |
18 | constructor(scene: Phaser.Scene, config: TiledChestObject, chestState = CHEST_STATE.HIDDEN) {
19 | const frameKey = config.requiresBossKey ? CHEST_FRAME_KEYS.BIG_CHEST_CLOSED : CHEST_FRAME_KEYS.SMALL_CHEST_CLOSED;
20 | super(scene, config.x, config.y, ASSET_KEYS.DUNGEON_OBJECTS, frameKey);
21 |
22 | // add object to scene and enable phaser physics
23 | scene.add.existing(this);
24 | scene.physics.add.existing(this);
25 | this.setOrigin(0, 1).setImmovable(true);
26 |
27 | this.#state = chestState;
28 | this.#isBossKeyChest = config.requiresBossKey;
29 | this.#id = config.id;
30 | this.#revealTrigger = config.revealChestTrigger;
31 | this.#contents = config.contents;
32 |
33 | if (this.#isBossKeyChest) {
34 | (this.body as Phaser.Physics.Arcade.Body).setSize(32, 24).setOffset(0, 8);
35 | }
36 |
37 | // add components
38 | new InteractiveObjectComponent(
39 | this,
40 | INTERACTIVE_OBJECT_TYPE.OPEN,
41 | () => {
42 | // if this is a small chest, then the player can open
43 | if (!this.#isBossKeyChest) {
44 | return true;
45 | }
46 | // use area information from data manager
47 | if (!InventoryManager.instance.getAreaInventory(DataManager.instance.data.currentArea.name).bossKey) {
48 | return false;
49 | }
50 | return true;
51 | },
52 | () => {
53 | this.open();
54 | },
55 | );
56 |
57 | if (this.#revealTrigger === TRAP_TYPE.NONE) {
58 | if (this.#state === CHEST_STATE.HIDDEN) {
59 | this.#state = CHEST_STATE.REVEALED;
60 | }
61 | return;
62 | }
63 | // disable physics body and make game objects inactive/not visible
64 | this.disableObject();
65 | }
66 |
67 | get revealTrigger(): TrapType {
68 | return this.#revealTrigger;
69 | }
70 |
71 | get id(): number {
72 | return this.#id;
73 | }
74 |
75 | get contents(): ChestReward {
76 | return this.#contents;
77 | }
78 |
79 | public open(): void {
80 | if (this.#state !== CHEST_STATE.REVEALED) {
81 | return;
82 | }
83 |
84 | this.#state = CHEST_STATE.OPEN;
85 | const frameKey = this.#isBossKeyChest ? CHEST_FRAME_KEYS.BIG_CHEST_OPEN : CHEST_FRAME_KEYS.SMALL_CHEST_OPEN;
86 | this.setFrame(frameKey);
87 |
88 | // after we open the chest, we can no longer interact with it
89 | InteractiveObjectComponent.removeComponent(this);
90 | }
91 |
92 | public disableObject(): void {
93 | // disable body on game object so we stop triggering the collision
94 | (this.body as Phaser.Physics.Arcade.Body).enable = false;
95 | // make not visible until player re-enters room
96 | this.active = false;
97 | this.visible = false;
98 | }
99 |
100 | public enableObject(): void {
101 | if (this.#state === CHEST_STATE.HIDDEN) {
102 | return;
103 | }
104 |
105 | // enable body on game object so we trigger the collision
106 | (this.body as Phaser.Physics.Arcade.Body).enable = true;
107 | // make visible to the player
108 | this.active = true;
109 | this.visible = true;
110 | }
111 |
112 | public reveal(): void {
113 | if (this.#state !== CHEST_STATE.HIDDEN) {
114 | return;
115 | }
116 | this.#state = CHEST_STATE.REVEALED;
117 | this.enableObject();
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/game-objects/objects/door.ts:
--------------------------------------------------------------------------------
1 | import { ASSET_KEYS, DOOR_FRAME_KEYS } from '../../common/assets';
2 | import { DIRECTION } from '../../common/common';
3 | import { ENABLE_DEBUG_ZONE_AREA } from '../../common/config';
4 | import { DOOR_TYPE } from '../../common/tiled/common';
5 | import { DoorType, TiledDoorObject, TrapType } from '../../common/tiled/types';
6 | import { CustomGameObject, Direction } from '../../common/types';
7 |
8 | export class Door implements CustomGameObject {
9 | #scene: Phaser.Scene;
10 | #roomId: number;
11 | #targetDoorId: number;
12 | #targetRoomId: number;
13 | #x: number;
14 | #y: number;
15 | #targetLevel: string;
16 | #doorTransitionZone: Phaser.GameObjects.Zone;
17 | // eslint-disable-next-line no-unused-private-class-members
18 | #debugDoorTransitionZone: Phaser.GameObjects.Rectangle | undefined;
19 | #direction: Direction;
20 | #id: number;
21 | #isUnlocked: boolean;
22 | #doorObject!: Phaser.Types.Physics.Arcade.ImageWithDynamicBody | undefined;
23 | #trapDoorTrigger: TrapType;
24 | #doorType: DoorType;
25 |
26 | constructor(scene: Phaser.Scene, config: TiledDoorObject, roomId: number) {
27 | this.#scene = scene;
28 | this.#id = config.id;
29 | this.#roomId = roomId;
30 | this.#targetDoorId = config.targetDoorId;
31 | this.#targetRoomId = config.targetRoomId;
32 | this.#targetLevel = config.targetLevel;
33 | this.#x = config.x;
34 | this.#y = config.y;
35 | this.#direction = config.direction;
36 | this.#doorType = config.doorType;
37 | this.#isUnlocked = config.isUnlocked;
38 | this.#trapDoorTrigger = config.trapDoorTrigger;
39 |
40 | // create door transition
41 | this.#doorTransitionZone = this.#scene.add
42 | .zone(config.x, config.y, config.width, config.height)
43 | .setOrigin(0, 1)
44 | .setName(config.id.toString(10));
45 | this.#scene.physics.world.enable(this.#doorTransitionZone);
46 |
47 | if (ENABLE_DEBUG_ZONE_AREA) {
48 | this.#debugDoorTransitionZone = this.#scene.add
49 | .rectangle(
50 | this.#doorTransitionZone.x,
51 | this.#doorTransitionZone.y,
52 | this.#doorTransitionZone.width,
53 | this.#doorTransitionZone.height,
54 | 0xffff00,
55 | 0.6,
56 | )
57 | .setOrigin(0, 1);
58 | }
59 |
60 | // if door is not open type create sprite for the door
61 | if (this.#doorType !== DOOR_TYPE.OPEN && this.#doorType !== DOOR_TYPE.OPEN_ENTRANCE) {
62 | const frameName = DOOR_FRAME_KEYS[`${this.#doorType}_${this.#direction}`];
63 |
64 | const door = this.#scene.physics.add
65 | .image(this.#x, this.y, ASSET_KEYS.DUNGEON_OBJECTS, frameName)
66 | .setImmovable(true)
67 | .setName(config.id.toString(10));
68 |
69 | switch (this.#direction) {
70 | case DIRECTION.UP:
71 | door.setOrigin(0, 0.5);
72 | break;
73 | case DIRECTION.DOWN:
74 | door.setOrigin(0, 0.75);
75 | break;
76 | case DIRECTION.LEFT:
77 | door.setOrigin(0.25, 1);
78 | break;
79 | case DIRECTION.RIGHT:
80 | door.setOrigin(0.5, 1);
81 | break;
82 | }
83 |
84 | this.#doorObject = door;
85 | }
86 |
87 | // disable physics body and make game objects inactive/not visible
88 | this.disableObject();
89 | }
90 |
91 | get x(): number {
92 | return this.#x;
93 | }
94 |
95 | get y(): number {
96 | return this.#y;
97 | }
98 |
99 | get roomId(): number {
100 | return this.#roomId;
101 | }
102 |
103 | get targetRoomId(): number {
104 | return this.#targetRoomId;
105 | }
106 |
107 | get targetDoorId(): number {
108 | return this.#targetDoorId;
109 | }
110 |
111 | get doorTransitionZone(): Phaser.GameObjects.Zone {
112 | return this.#doorTransitionZone;
113 | }
114 |
115 | get targetLevel(): string {
116 | return this.#targetLevel;
117 | }
118 |
119 | get direction(): Direction {
120 | return this.#direction;
121 | }
122 |
123 | get doorObject(): Phaser.Types.Physics.Arcade.ImageWithDynamicBody | undefined {
124 | return this.#doorObject;
125 | }
126 |
127 | get id(): number {
128 | return this.#id;
129 | }
130 |
131 | get trapDoorTrigger(): TrapType {
132 | return this.#trapDoorTrigger;
133 | }
134 |
135 | get doorType(): DoorType {
136 | return this.#doorType;
137 | }
138 |
139 | public disableObject(disableDoorTrigger = true): void {
140 | if (disableDoorTrigger) {
141 | (this.#doorTransitionZone.body as Phaser.Physics.Arcade.Body).enable = false;
142 | this.#doorTransitionZone.active = true;
143 | }
144 |
145 | if (this.#doorObject !== undefined) {
146 | this.#doorObject.body.enable = false;
147 | this.#doorObject.active = false;
148 | this.#doorObject.visible = false;
149 | }
150 | }
151 |
152 | public enableObject(): void {
153 | (this.#doorTransitionZone.body as Phaser.Physics.Arcade.Body).enable = true;
154 | this.#doorTransitionZone.active = true;
155 |
156 | if (this.#isUnlocked) {
157 | return;
158 | }
159 |
160 | if (this.#doorObject !== undefined) {
161 | this.#doorObject.body.enable = true;
162 | this.#doorObject.active = true;
163 | this.#doorObject.visible = true;
164 | }
165 | }
166 |
167 | public open(): void {
168 | if (this.#doorType === DOOR_TYPE.LOCK || this.#doorType === DOOR_TYPE.BOSS) {
169 | this.#isUnlocked = true;
170 | }
171 |
172 | this.disableObject(false);
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/src/game-objects/objects/pot.ts:
--------------------------------------------------------------------------------
1 | import * as Phaser from 'phaser';
2 | import { ASSET_KEYS } from '../../common/assets';
3 | import { CustomGameObject, Position } from '../../common/types';
4 | import { InteractiveObjectComponent } from '../../components/game-object/interactive-object-component';
5 | import { INTERACTIVE_OBJECT_TYPE } from '../../common/common';
6 | import { ThrowableObjectComponent } from '../../components/game-object/throwable-object-component';
7 | import { TiledPotObject } from '../../common/tiled/types';
8 |
9 | export class Pot extends Phaser.Physics.Arcade.Sprite implements CustomGameObject {
10 | #position: Position;
11 |
12 | constructor(scene: Phaser.Scene, config: TiledPotObject) {
13 | super(scene, config.x, config.y, ASSET_KEYS.POT, 0);
14 |
15 | // add object to scene and enable phaser physics
16 | scene.add.existing(this);
17 | scene.physics.add.existing(this);
18 | this.setOrigin(0, 1).setImmovable(true);
19 |
20 | // keep track of original position for the pot
21 | this.#position = { x: config.x, y: config.y };
22 |
23 | // add components
24 | new InteractiveObjectComponent(this, INTERACTIVE_OBJECT_TYPE.PICKUP);
25 | new ThrowableObjectComponent(this, () => {
26 | this.break();
27 | });
28 |
29 | // disable physics body and make game objects inactive/not visible
30 | this.disableObject();
31 | }
32 |
33 | public disableObject(): void {
34 | // disable body on game object so we stop triggering the collision
35 | (this.body as Phaser.Physics.Arcade.Body).enable = false;
36 | // make not visible until player re-enters room
37 | this.active = false;
38 | this.visible = false;
39 | }
40 |
41 | public enableObject(): void {
42 | // enable body on game object so we trigger the collision
43 | (this.body as Phaser.Physics.Arcade.Body).enable = true;
44 | // make visible to the player
45 | this.active = true;
46 | this.visible = true;
47 | }
48 |
49 | public break(): void {
50 | (this.body as Phaser.Physics.Arcade.Body).enable = false;
51 | this.setTexture(ASSET_KEYS.POT_BREAK, 0).play(ASSET_KEYS.POT_BREAK);
52 | // once animation is finished, disable object and reset the initial texture
53 | this.once(Phaser.Animations.Events.ANIMATION_COMPLETE_KEY + ASSET_KEYS.POT_BREAK, () => {
54 | this.setTexture(ASSET_KEYS.POT, 0);
55 | this.disableObject();
56 | });
57 | }
58 |
59 | public resetPosition(): void {
60 | this.scene.time.delayedCall(1, () => {
61 | this.setPosition(this.#position.x, this.#position.y).setOrigin(0, 1);
62 | this.enableObject();
63 | });
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/game-objects/player/player.ts:
--------------------------------------------------------------------------------
1 | import * as Phaser from 'phaser';
2 | import { GameObject, Position } from '../../common/types';
3 | import { InputComponent } from '../../components/input/input-component';
4 | import { IdleState } from '../../components/state-machine/states/character/idle-state';
5 | import { CHARACTER_STATES } from '../../components/state-machine/states/character/character-states';
6 | import { MoveState } from '../../components/state-machine/states/character/move-state';
7 | import {
8 | PLAYER_ATTACK_DAMAGE,
9 | PLAYER_HURT_PUSH_BACK_SPEED,
10 | PLAYER_INVULNERABLE_AFTER_HIT_DURATION,
11 | PLAYER_SPEED,
12 | } from '../../common/config';
13 | import { AnimationConfig } from '../../components/game-object/animation-component';
14 | import { ASSET_KEYS, PLAYER_ANIMATION_KEYS } from '../../common/assets';
15 | import { CharacterGameObject } from '../common/character-game-object';
16 | import { HurtState } from '../../components/state-machine/states/character/hurt-state';
17 | import { flash } from '../../common/juice-utils';
18 | import { DeathState } from '../../components/state-machine/states/character/death-state';
19 | import { CollidingObjectsComponent } from '../../components/game-object/colliding-objects-component';
20 | import { LiftState } from '../../components/state-machine/states/character/lift-state';
21 | import { OpenChestState } from '../../components/state-machine/states/character/open-chest-state';
22 | import { IdleHoldingState } from '../../components/state-machine/states/character/idle-holding-state';
23 | import { MoveHoldingState } from '../../components/state-machine/states/character/move-holding-state';
24 | import { HeldGameObjectComponent } from '../../components/game-object/held-game-object-component';
25 | import { ThrowState } from '../../components/state-machine/states/character/throw-state';
26 | import { AttackState } from '../../components/state-machine/states/character/attack-state';
27 | import { WeaponComponent } from '../../components/game-object/weapon-component';
28 | import { Sword } from '../weapons/sword';
29 |
30 | export type PlayerConfig = {
31 | scene: Phaser.Scene;
32 | position: Position;
33 | controls: InputComponent;
34 | maxLife: number;
35 | currentLife: number;
36 | };
37 |
38 | export class Player extends CharacterGameObject {
39 | #collidingObjectsComponent: CollidingObjectsComponent;
40 | #weaponComponent: WeaponComponent;
41 |
42 | constructor(config: PlayerConfig) {
43 | // create animation config for component
44 | const animationConfig: AnimationConfig = {
45 | WALK_DOWN: { key: PLAYER_ANIMATION_KEYS.WALK_DOWN, repeat: -1, ignoreIfPlaying: true },
46 | WALK_UP: { key: PLAYER_ANIMATION_KEYS.WALK_UP, repeat: -1, ignoreIfPlaying: true },
47 | WALK_LEFT: { key: PLAYER_ANIMATION_KEYS.WALK_SIDE, repeat: -1, ignoreIfPlaying: true },
48 | WALK_RIGHT: { key: PLAYER_ANIMATION_KEYS.WALK_SIDE, repeat: -1, ignoreIfPlaying: true },
49 | IDLE_DOWN: { key: PLAYER_ANIMATION_KEYS.IDLE_DOWN, repeat: -1, ignoreIfPlaying: true },
50 | IDLE_UP: { key: PLAYER_ANIMATION_KEYS.IDLE_UP, repeat: -1, ignoreIfPlaying: true },
51 | IDLE_LEFT: { key: PLAYER_ANIMATION_KEYS.IDLE_SIDE, repeat: -1, ignoreIfPlaying: true },
52 | IDLE_RIGHT: { key: PLAYER_ANIMATION_KEYS.IDLE_SIDE, repeat: -1, ignoreIfPlaying: true },
53 | HURT_DOWN: { key: PLAYER_ANIMATION_KEYS.HURT_DOWN, repeat: 0, ignoreIfPlaying: true },
54 | HURT_UP: { key: PLAYER_ANIMATION_KEYS.HURT_UP, repeat: 0, ignoreIfPlaying: true },
55 | HURT_LEFT: { key: PLAYER_ANIMATION_KEYS.HURT_SIDE, repeat: 0, ignoreIfPlaying: true },
56 | HURT_RIGHT: { key: PLAYER_ANIMATION_KEYS.HURT_SIDE, repeat: 0, ignoreIfPlaying: true },
57 | DIE_DOWN: { key: PLAYER_ANIMATION_KEYS.DIE_DOWN, repeat: 0, ignoreIfPlaying: true },
58 | DIE_UP: { key: PLAYER_ANIMATION_KEYS.DIE_UP, repeat: 0, ignoreIfPlaying: true },
59 | DIE_LEFT: { key: PLAYER_ANIMATION_KEYS.DIE_SIDE, repeat: 0, ignoreIfPlaying: true },
60 | DIE_RIGHT: { key: PLAYER_ANIMATION_KEYS.DIE_SIDE, repeat: 0, ignoreIfPlaying: true },
61 | IDLE_HOLD_DOWN: { key: PLAYER_ANIMATION_KEYS.IDLE_HOLD_DOWN, repeat: -1, ignoreIfPlaying: true },
62 | IDLE_HOLD_UP: { key: PLAYER_ANIMATION_KEYS.IDLE_HOLD_UP, repeat: -1, ignoreIfPlaying: true },
63 | IDLE_HOLD_LEFT: { key: PLAYER_ANIMATION_KEYS.IDLE_HOLD_SIDE, repeat: -1, ignoreIfPlaying: true },
64 | IDLE_HOLD_RIGHT: { key: PLAYER_ANIMATION_KEYS.IDLE_HOLD_SIDE, repeat: -1, ignoreIfPlaying: true },
65 | WALK_HOLD_DOWN: { key: PLAYER_ANIMATION_KEYS.WALK_HOLD_DOWN, repeat: -1, ignoreIfPlaying: true },
66 | WALK_HOLD_UP: { key: PLAYER_ANIMATION_KEYS.WALK_HOLD_UP, repeat: -1, ignoreIfPlaying: true },
67 | WALK_HOLD_LEFT: { key: PLAYER_ANIMATION_KEYS.WALK_HOLD_SIDE, repeat: -1, ignoreIfPlaying: true },
68 | WALK_HOLD_RIGHT: { key: PLAYER_ANIMATION_KEYS.WALK_HOLD_SIDE, repeat: -1, ignoreIfPlaying: true },
69 | LIFT_DOWN: { key: PLAYER_ANIMATION_KEYS.LIFT_DOWN, repeat: 0, ignoreIfPlaying: true },
70 | LIFT_UP: { key: PLAYER_ANIMATION_KEYS.LIFT_UP, repeat: 0, ignoreIfPlaying: true },
71 | LIFT_LEFT: { key: PLAYER_ANIMATION_KEYS.LIFT_SIDE, repeat: 0, ignoreIfPlaying: true },
72 | LIFT_RIGHT: { key: PLAYER_ANIMATION_KEYS.LIFT_SIDE, repeat: 0, ignoreIfPlaying: true },
73 | };
74 |
75 | super({
76 | scene: config.scene,
77 | position: config.position,
78 | assetKey: ASSET_KEYS.PLAYER,
79 | frame: 0,
80 | id: 'player',
81 | isPlayer: true,
82 | animationConfig,
83 | speed: PLAYER_SPEED,
84 | inputComponent: config.controls,
85 | isInvulnerable: false,
86 | invulnerableAfterHitAnimationDuration: PLAYER_INVULNERABLE_AFTER_HIT_DURATION,
87 | maxLife: config.maxLife,
88 | currentLife: config.currentLife,
89 | });
90 |
91 | // add state machine
92 | this._stateMachine.addState(new IdleState(this));
93 | this._stateMachine.addState(new MoveState(this));
94 | this._stateMachine.addState(
95 | new HurtState(this, PLAYER_HURT_PUSH_BACK_SPEED, () => {
96 | flash(this);
97 | }),
98 | );
99 | this._stateMachine.addState(new DeathState(this));
100 | this._stateMachine.addState(new LiftState(this));
101 | this._stateMachine.addState(new OpenChestState(this));
102 | this._stateMachine.addState(new IdleHoldingState(this));
103 | this._stateMachine.addState(new MoveHoldingState(this));
104 | this._stateMachine.addState(new ThrowState(this));
105 | this._stateMachine.addState(new AttackState(this));
106 | this._stateMachine.setState(CHARACTER_STATES.IDLE_STATE);
107 |
108 | // add components
109 | this.#collidingObjectsComponent = new CollidingObjectsComponent(this);
110 | new HeldGameObjectComponent(this);
111 | this.#weaponComponent = new WeaponComponent(this);
112 | this.#weaponComponent.weapon = new Sword(
113 | this,
114 | this.#weaponComponent,
115 | {
116 | DOWN: PLAYER_ANIMATION_KEYS.SWORD_1_ATTACK_DOWN,
117 | UP: PLAYER_ANIMATION_KEYS.SWORD_1_ATTACK_UP,
118 | LEFT: PLAYER_ANIMATION_KEYS.SWORD_1_ATTACK_SIDE,
119 | RIGHT: PLAYER_ANIMATION_KEYS.SWORD_1_ATTACK_SIDE,
120 | },
121 | PLAYER_ATTACK_DAMAGE,
122 | );
123 |
124 | // enable auto update functionality
125 | config.scene.events.on(Phaser.Scenes.Events.UPDATE, this.update, this);
126 | config.scene.events.once(
127 | Phaser.Scenes.Events.SHUTDOWN,
128 | () => {
129 | config.scene.events.off(Phaser.Scenes.Events.UPDATE, this.update, this);
130 | },
131 | this,
132 | );
133 |
134 | // update physics body
135 | this.physicsBody.setSize(12, 16, true).setOffset(this.width / 2 - 5, this.height / 2);
136 | }
137 |
138 | get physicsBody(): Phaser.Physics.Arcade.Body {
139 | return this.body as Phaser.Physics.Arcade.Body;
140 | }
141 |
142 | get weaponComponent(): WeaponComponent {
143 | return this.#weaponComponent;
144 | }
145 |
146 | public collidedWithGameObject(gameObject: GameObject): void {
147 | this.#collidingObjectsComponent.add(gameObject);
148 | }
149 |
150 | public update(): void {
151 | super.update();
152 | this.#collidingObjectsComponent.reset();
153 | this.#weaponComponent.update();
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/src/game-objects/weapons/base-weapon.ts:
--------------------------------------------------------------------------------
1 | import { Direction } from '../../common/types';
2 | import { WeaponComponent } from '../../components/game-object/weapon-component';
3 |
4 | export interface Weapon {
5 | baseDamage: number;
6 | isAttacking: boolean;
7 | attackUp(): void;
8 | attackDown(): void;
9 | attackRight(): void;
10 | attackLeft(): void;
11 | update(): void;
12 | onCollisionCallback(): void;
13 | }
14 |
15 | export type WeaponAttackAnimationConfig = {
16 | [key in Direction]: string;
17 | };
18 |
19 | export abstract class BaseWeapon implements Weapon {
20 | protected _weaponComponent: WeaponComponent;
21 | protected _attacking: boolean;
22 | protected _sprite: Phaser.GameObjects.Sprite;
23 | protected _attackAnimationConfig: WeaponAttackAnimationConfig;
24 | protected _baseDamage: number;
25 |
26 | constructor(
27 | sprite: Phaser.GameObjects.Sprite,
28 | weaponComponent: WeaponComponent,
29 | animationConfig: WeaponAttackAnimationConfig,
30 | baseDamage: number,
31 | ) {
32 | this._sprite = sprite;
33 | this._weaponComponent = weaponComponent;
34 | this._attackAnimationConfig = animationConfig;
35 | this._baseDamage = baseDamage;
36 | this._attacking = false;
37 | }
38 |
39 | get isAttacking(): boolean {
40 | return this._attacking;
41 | }
42 |
43 | get baseDamage(): number {
44 | return this._baseDamage;
45 | }
46 |
47 | protected attack(direction: Direction): void {
48 | const attackAnimationKey = this._attackAnimationConfig[direction];
49 | this._attacking = true;
50 | this._sprite.play({ key: attackAnimationKey, repeat: 0 }, true);
51 | this._weaponComponent.body.enable = true;
52 | this._sprite.once(Phaser.Animations.Events.ANIMATION_COMPLETE_KEY + attackAnimationKey, () => {
53 | this.attackAnimationCompleteHandler();
54 | });
55 | }
56 |
57 | protected attackAnimationCompleteHandler(): void {
58 | this._attacking = false;
59 | this._weaponComponent.body.enable = false;
60 | }
61 |
62 | // following methods must be implemented by weapon implementations
63 | public abstract attackUp(): void;
64 |
65 | public abstract attackDown(): void;
66 |
67 | public abstract attackRight(): void;
68 |
69 | public abstract attackLeft(): void;
70 |
71 | // following methods to be overridden if needed by weapon implementations
72 | public update(): void {
73 | // not implemented
74 | }
75 |
76 | public onCollisionCallback(): void {
77 | // not implemented
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/game-objects/weapons/dagger.ts:
--------------------------------------------------------------------------------
1 | import { BaseWeapon, WeaponAttackAnimationConfig } from './base-weapon';
2 | import { DIRECTION } from '../../common/common';
3 | import { WeaponComponent } from '../../components/game-object/weapon-component';
4 | import { ASSET_KEYS } from '../../common/assets';
5 |
6 | export class Dagger extends BaseWeapon {
7 | #weaponSprite: Phaser.GameObjects.Sprite;
8 | #weaponSpeed: number;
9 |
10 | constructor(
11 | sprite: Phaser.GameObjects.Sprite,
12 | weaponComponent: WeaponComponent,
13 | animationConfig: WeaponAttackAnimationConfig,
14 | baseDamage: number,
15 | weaponSpeed: number,
16 | ) {
17 | super(sprite, weaponComponent, animationConfig, baseDamage);
18 |
19 | this.#weaponSprite = sprite.scene.add
20 | .sprite(0, 0, ASSET_KEYS.DAGGER, 0)
21 | .setVisible(false)
22 | .setOrigin(0, 1)
23 | .play(ASSET_KEYS.DAGGER);
24 | this.#weaponSpeed = weaponSpeed;
25 | this._weaponComponent.body.setSize(this.#weaponSprite.width, this.#weaponSprite.height);
26 | }
27 |
28 | public attackUp(): void {
29 | this._weaponComponent.body.position.set(this._sprite.x - 8, this._sprite.y - 25);
30 | this._weaponComponent.body.setVelocityY(this.#weaponSpeed * -1);
31 | this.#weaponSprite
32 | .setPosition(this._weaponComponent.body.position.x, this._weaponComponent.body.y)
33 | .setVisible(true)
34 | .setOrigin(0, 0)
35 | .setAngle(0)
36 | .setFlipY(false);
37 | this.attack(DIRECTION.UP);
38 | }
39 |
40 | public attackDown(): void {
41 | this._weaponComponent.body.position.set(this._sprite.x - 7, this._sprite.y + 20);
42 | this._weaponComponent.body.setVelocityY(this.#weaponSpeed);
43 | this.#weaponSprite
44 | .setPosition(this._weaponComponent.body.position.x, this._weaponComponent.body.y)
45 | .setVisible(true)
46 | .setOrigin(1, 1)
47 | .setAngle(180)
48 | .setFlipY(false);
49 | this.attack(DIRECTION.DOWN);
50 | }
51 |
52 | public attackRight(): void {
53 | this._weaponComponent.body.position.set(this._sprite.x + 10, this._sprite.y - 5);
54 | this._weaponComponent.body.setVelocityX(this.#weaponSpeed);
55 | this.#weaponSprite
56 | .setPosition(this._weaponComponent.body.position.x, this._weaponComponent.body.y)
57 | .setVisible(true)
58 | .setOrigin(0, 1)
59 | .setAngle(90)
60 | .setFlipY(false);
61 | this.attack(DIRECTION.RIGHT);
62 | }
63 |
64 | public attackLeft(): void {
65 | this._weaponComponent.body.position.set(this._sprite.x - 25, this._sprite.y - 5);
66 | this._weaponComponent.body.setVelocityX(this.#weaponSpeed * -1);
67 | this.#weaponSprite
68 | .setPosition(this._weaponComponent.body.position.x, this._weaponComponent.body.y)
69 | .setVisible(true)
70 | .setOrigin(0, 1)
71 | .setAngle(90)
72 | .setFlipY(true);
73 | this.attack(DIRECTION.LEFT);
74 | }
75 |
76 | public update(): void {
77 | this.#weaponSprite.setPosition(this._weaponComponent.body.position.x, this._weaponComponent.body.position.y);
78 | }
79 |
80 | protected attackAnimationCompleteHandler(): void {
81 | super.attackAnimationCompleteHandler();
82 | this.#weaponSprite.setVisible(false);
83 | this._weaponComponent.body.setVelocityX(0);
84 | this._weaponComponent.body.setVelocityY(0);
85 | }
86 |
87 | public onCollisionCallback(): void {
88 | this.attackAnimationCompleteHandler();
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/game-objects/weapons/sword.ts:
--------------------------------------------------------------------------------
1 | import { BaseWeapon } from './base-weapon';
2 | import { DIRECTION } from '../../common/common';
3 |
4 | export class Sword extends BaseWeapon {
5 | public attackUp(): void {
6 | this._weaponComponent.body.setSize(30, 18);
7 | this._weaponComponent.body.position.set(this._sprite.x - 16, this._sprite.y - 22);
8 | this.attack(DIRECTION.UP);
9 | }
10 |
11 | public attackDown(): void {
12 | this._weaponComponent.body.setSize(30, 18);
13 | if (this._sprite.flipX) {
14 | this._weaponComponent.body.position.set(this._sprite.x - 20, this._sprite.y + 10);
15 | } else {
16 | this._weaponComponent.body.position.set(this._sprite.x - 10, this._sprite.y + 10);
17 | }
18 | this.attack(DIRECTION.DOWN);
19 | }
20 |
21 | public attackRight(): void {
22 | this._weaponComponent.body.setSize(18, 30);
23 | this._weaponComponent.body.position.set(this._sprite.x + 10, this._sprite.y - 10);
24 | this.attack(DIRECTION.RIGHT);
25 | }
26 |
27 | public attackLeft(): void {
28 | this._weaponComponent.body.setSize(18, 30);
29 | this._weaponComponent.body.position.set(this._sprite.x - 30, this._sprite.y - 10);
30 | this.attack(DIRECTION.LEFT);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import * as Phaser from 'phaser';
2 | import { SCENE_KEYS } from './scenes/scene-keys';
3 | import { PreloadScene } from './scenes/preload-scene';
4 | import { GameScene } from './scenes/game-scene';
5 | import { UiScene } from './scenes/ui-scene';
6 | import { GameOverScene } from './scenes/game-over-scene';
7 |
8 | const gameConfig: Phaser.Types.Core.GameConfig = {
9 | type: Phaser.WEBGL,
10 | pixelArt: true,
11 | roundPixels: true,
12 | scale: {
13 | parent: 'game-container',
14 | width: 256,
15 | height: 224,
16 | autoCenter: Phaser.Scale.CENTER_BOTH,
17 | mode: Phaser.Scale.HEIGHT_CONTROLS_WIDTH,
18 | },
19 | backgroundColor: '#000000',
20 | physics: {
21 | default: 'arcade',
22 | arcade: {
23 | gravity: { y: 0, x: 0 },
24 | debug: false,
25 | },
26 | },
27 | };
28 |
29 | const game = new Phaser.Game(gameConfig);
30 |
31 | game.scene.add(SCENE_KEYS.PRELOAD_SCENE, PreloadScene);
32 | game.scene.add(SCENE_KEYS.GAME_SCENE, GameScene);
33 | game.scene.add(SCENE_KEYS.UI_SCENE, UiScene);
34 | game.scene.add(SCENE_KEYS.GAME_OVER_SCENE, GameOverScene);
35 | game.scene.start(SCENE_KEYS.PRELOAD_SCENE);
36 |
--------------------------------------------------------------------------------
/src/scenes/game-over-scene.ts:
--------------------------------------------------------------------------------
1 | import * as Phaser from 'phaser';
2 | import { SCENE_KEYS } from './scene-keys';
3 | import { ASSET_KEYS } from '../common/assets';
4 | import { KeyboardComponent } from '../components/input/keyboard-component';
5 | import { DataManager } from '../common/data-manager';
6 | import { DEFAULT_UI_TEXT_STYLE } from '../common/common';
7 |
8 | export class GameOverScene extends Phaser.Scene {
9 | #menuContainer!: Phaser.GameObjects.Container;
10 | #cursorGameObject!: Phaser.GameObjects.Image;
11 | #controls!: KeyboardComponent;
12 | #selectedMenuOptionIndex!: number;
13 |
14 | constructor() {
15 | super({
16 | key: SCENE_KEYS.GAME_OVER_SCENE,
17 | });
18 | }
19 |
20 | public create(): void {
21 | if (!this.input.keyboard) {
22 | return;
23 | }
24 |
25 | this.add.text(this.scale.width / 2, 100, 'Game Over', DEFAULT_UI_TEXT_STYLE).setOrigin(0.5);
26 |
27 | this.#menuContainer = this.add.container(32, 142, [
28 | this.add.image(0, 0, ASSET_KEYS.UI_DIALOG, 0).setOrigin(0),
29 | this.add.text(32, 16, 'Continue', DEFAULT_UI_TEXT_STYLE).setOrigin(0),
30 | this.add.text(32, 32, 'Quit', DEFAULT_UI_TEXT_STYLE).setOrigin(0),
31 | ]);
32 | this.#cursorGameObject = this.add.image(20, 14, ASSET_KEYS.UI_CURSOR, 0).setOrigin(0);
33 | this.#menuContainer.add(this.#cursorGameObject);
34 |
35 | this.#controls = new KeyboardComponent(this.input.keyboard);
36 | this.#selectedMenuOptionIndex = 0;
37 | DataManager.instance.resetPlayerHealthToMin();
38 | }
39 |
40 | public update(): void {
41 | if (this.#controls.isActionKeyJustDown || this.#controls.isAttackKeyJustDown || this.#controls.isEnterKeyJustDown) {
42 | if (this.#selectedMenuOptionIndex === 1) {
43 | // this option would be used to take the player back to the title screen for the game
44 | // instead of refreshing the current browser tab
45 | window.location.reload();
46 | return;
47 | }
48 |
49 | this.scene.start(SCENE_KEYS.GAME_SCENE);
50 | return;
51 | }
52 |
53 | if (this.#controls.isUpJustDown) {
54 | this.#selectedMenuOptionIndex -= 1;
55 | if (this.#selectedMenuOptionIndex < 0) {
56 | this.#selectedMenuOptionIndex = 0;
57 | }
58 | } else if (this.#controls.isDownJustDown) {
59 | this.#selectedMenuOptionIndex += 1;
60 | if (this.#selectedMenuOptionIndex > 1) {
61 | this.#selectedMenuOptionIndex = 1;
62 | }
63 | } else {
64 | return;
65 | }
66 |
67 | this.#cursorGameObject.setY(14 + this.#selectedMenuOptionIndex * 16);
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/scenes/preload-scene.ts:
--------------------------------------------------------------------------------
1 | import * as Phaser from 'phaser';
2 | import { SCENE_KEYS } from './scene-keys';
3 | import { ASSET_KEYS, ASSET_PACK_KEYS } from '../common/assets';
4 | import { LevelData } from '../common/types';
5 | import { DataManager } from '../common/data-manager';
6 |
7 | export class PreloadScene extends Phaser.Scene {
8 | constructor() {
9 | super({
10 | key: SCENE_KEYS.PRELOAD_SCENE,
11 | });
12 | }
13 |
14 | public preload(): void {
15 | // load asset pack that has assets for the rest of the game
16 | this.load.pack(ASSET_PACK_KEYS.MAIN, 'assets/data/assets.json');
17 | }
18 |
19 | public create(): void {
20 | this.#createAnimations();
21 |
22 | const sceneData: LevelData = {
23 | level: DataManager.instance.data.currentArea.name,
24 | roomId: DataManager.instance.data.currentArea.startRoomId,
25 | doorId: DataManager.instance.data.currentArea.startDoorId,
26 | };
27 | this.scene.start(SCENE_KEYS.GAME_SCENE, sceneData);
28 | }
29 |
30 | #createAnimations(): void {
31 | this.anims.createFromAseprite(ASSET_KEYS.HUD_NUMBERS);
32 | this.anims.createFromAseprite(ASSET_KEYS.PLAYER);
33 | this.anims.createFromAseprite(ASSET_KEYS.SPIDER);
34 | this.anims.createFromAseprite(ASSET_KEYS.WISP);
35 | this.anims.createFromAseprite(ASSET_KEYS.DROW);
36 | this.anims.create({
37 | key: ASSET_KEYS.ENEMY_DEATH,
38 | frames: this.anims.generateFrameNumbers(ASSET_KEYS.ENEMY_DEATH),
39 | frameRate: 6,
40 | repeat: 0,
41 | delay: 0,
42 | });
43 | this.anims.create({
44 | key: ASSET_KEYS.POT_BREAK,
45 | frames: this.anims.generateFrameNumbers(ASSET_KEYS.POT_BREAK),
46 | frameRate: 6,
47 | repeat: 0,
48 | delay: 0,
49 | hideOnComplete: true,
50 | });
51 | this.anims.create({
52 | key: ASSET_KEYS.DAGGER,
53 | frames: this.anims.generateFrameNumbers(ASSET_KEYS.DAGGER),
54 | frameRate: 16,
55 | repeat: -1,
56 | delay: 0,
57 | });
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/scenes/scene-keys.ts:
--------------------------------------------------------------------------------
1 | export const SCENE_KEYS = {
2 | PRELOAD_SCENE: 'PRELOAD_SCENE',
3 | GAME_SCENE: 'GAME_SCENE',
4 | UI_SCENE: 'UI_SCENE',
5 | GAME_OVER_SCENE: 'GAME_OVER_SCENE',
6 | } as const;
7 |
--------------------------------------------------------------------------------
/src/scenes/ui-scene.ts:
--------------------------------------------------------------------------------
1 | import * as Phaser from 'phaser';
2 | import { SCENE_KEYS } from './scene-keys';
3 | import { ASSET_KEYS, HEART_ANIMATIONS, HEART_TEXTURE_FRAME } from '../common/assets';
4 | import { DataManager } from '../common/data-manager';
5 | import { CUSTOM_EVENTS, EVENT_BUS, PLAYER_HEALTH_UPDATE_TYPE, PlayerHealthUpdated } from '../common/event-bus';
6 | import { DEFAULT_UI_TEXT_STYLE } from '../common/common';
7 |
8 | export class UiScene extends Phaser.Scene {
9 | #hudContainer!: Phaser.GameObjects.Container;
10 | #hearts!: Phaser.GameObjects.Sprite[];
11 | #dialogContainer!: Phaser.GameObjects.Container;
12 | #dialogContainerText!: Phaser.GameObjects.Text;
13 |
14 | constructor() {
15 | super({
16 | key: SCENE_KEYS.UI_SCENE,
17 | });
18 | }
19 |
20 | public create(): void {
21 | // create main hud
22 | this.#hudContainer = this.add.container(0, 0, []);
23 | this.#hearts = [];
24 |
25 | const numberOfHearts = Math.floor(DataManager.instance.data.maxHealth / 2);
26 | const numberOfFullHearts = Math.floor(DataManager.instance.data.currentHealth / 2);
27 | const hasHalfHeart = DataManager.instance.data.currentHealth % 2 === 1;
28 | for (let i = 0; i < 20; i += 1) {
29 | let x = 157 + 8 * i;
30 | let y = 25;
31 | if (i >= 10) {
32 | x = 157 + 8 * (i - 10);
33 | y = 33;
34 | }
35 | let frame: string = HEART_TEXTURE_FRAME.NONE;
36 | if (i < numberOfFullHearts) {
37 | frame = HEART_TEXTURE_FRAME.FULL;
38 | } else if (i < numberOfHearts) {
39 | frame = HEART_TEXTURE_FRAME.EMPTY;
40 | }
41 | if (hasHalfHeart && i === numberOfFullHearts) {
42 | frame = HEART_TEXTURE_FRAME.HALF;
43 | }
44 | this.#hearts.push(this.add.sprite(x, y, ASSET_KEYS.HUD_NUMBERS, frame).setOrigin(0));
45 | }
46 | this.#hudContainer.add(this.#hearts);
47 |
48 | this.#dialogContainer = this.add.container(32, 142, [this.add.image(0, 0, ASSET_KEYS.UI_DIALOG, 0).setOrigin(0)]);
49 | this.#dialogContainerText = this.add.text(14, 14, '', DEFAULT_UI_TEXT_STYLE).setOrigin(0);
50 | this.#dialogContainer.add(this.#dialogContainerText);
51 | this.#dialogContainer.visible = false;
52 |
53 | // register event listeners
54 | EVENT_BUS.on(CUSTOM_EVENTS.PLAYER_HEALTH_UPDATED, this.updateHealthInHud, this);
55 | EVENT_BUS.on(CUSTOM_EVENTS.SHOW_DIALOG, this.showDialog, this);
56 |
57 | this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => {
58 | EVENT_BUS.off(CUSTOM_EVENTS.PLAYER_HEALTH_UPDATED, this.updateHealthInHud, this);
59 | EVENT_BUS.off(CUSTOM_EVENTS.SHOW_DIALOG, this.showDialog, this);
60 | });
61 | }
62 |
63 | public async updateHealthInHud(data: PlayerHealthUpdated): Promise {
64 | if (data.type === PLAYER_HEALTH_UPDATE_TYPE.INCREASE) {
65 | // if player has increased their health, picking up hearts, new heart container, fairy, etc.,
66 | // need to update their health here
67 | return;
68 | }
69 |
70 | // play animation for losing hearts depending on the amount of health lost
71 | const healthDifference = data.previousHealth - data.currentHealth;
72 | let health = data.previousHealth;
73 | for (let i = 0; i < healthDifference; i += 1) {
74 | const heartIndex = Math.round(health / 2) - 1;
75 | const isHalfHeart = health % 2 === 1;
76 | let animationName = HEART_ANIMATIONS.LOSE_LAST_HALF;
77 | if (!isHalfHeart) {
78 | animationName = HEART_ANIMATIONS.LOSE_FIRST_HALF;
79 | }
80 | await new Promise((resolve) => {
81 | this.#hearts[heartIndex].play(animationName);
82 | this.#hearts[heartIndex].once(Phaser.Animations.Events.ANIMATION_COMPLETE_KEY + animationName, () => {
83 | resolve(undefined);
84 | });
85 | });
86 | health -= 1;
87 | }
88 | }
89 |
90 | public showDialog(message: string): void {
91 | this.#dialogContainer.visible = true;
92 | this.#dialogContainerText.setText(message);
93 |
94 | this.time.delayedCall(3000, () => {
95 | this.#dialogContainer.visible = false;
96 | EVENT_BUS.emit(CUSTOM_EVENTS.DIALOG_CLOSED);
97 | });
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@devshareacademy/tsconfig/tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "dist",
5 | "typeRoots": [
6 | "node_modules/@types"
7 | ]
8 | },
9 | "include": [
10 | "**/*.ts"
11 | ],
12 | "ts-node": {
13 | "compilerOptions": {
14 | "module": "commonjs"
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 |
3 | export default defineConfig({
4 | build: {
5 | rollupOptions: {
6 | output: {
7 | entryFileNames: 'assets/js/[name]-[hash].js',
8 | },
9 | },
10 | },
11 | });
12 |
--------------------------------------------------------------------------------