├── .gitignore
├── LICENSE
├── README.md
├── assets
├── Util.png
├── annaanthropy
│ ├── CasualEncounter.fnt
│ ├── CasualEncounter.png
│ ├── CasualEncounter.ttf
│ └── README.md
└── fongoose
│ ├── README.md
│ ├── RogueEnvironment16x16-extruded.png
│ ├── RogueEnvironment16x16.png
│ ├── RogueItems16x16.png
│ ├── RoguePlayer_48x48.png
│ ├── RoguePlayer_GUIDE.png
│ └── RogueSlime32x32.png
├── index.html
├── package-lock.json
├── package.json
├── src
├── assets
│ ├── Fonts.ts
│ └── Graphics.ts
├── entities
│ ├── FOVLayer.ts
│ ├── Map.ts
│ ├── Player.ts
│ ├── Slime.ts
│ └── Tile.ts
├── main.ts
└── scenes
│ ├── DungeonScene.ts
│ ├── InfoScene.ts
│ └── ReferenceScene.ts
├── tsconfig.json
└── typings
├── assets.d.ts
└── phaser-plugin-scene-watcher.d.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .cache
3 | dist
4 | .DS_Store
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Michael Pearson
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Dungeon Dash!
2 |
3 | An experiment with making a dungeon crawler with Open Source or public domain assets, using Phaser 3 & Typescript.
4 |
5 | Live demo available at https://dungeon-dash.surge.sh
6 |
7 | MIT License applies unless otherwise noted.
8 |
9 | ## Contributing
10 |
11 | Get a local instance running with `npm i` and then `npm run start`.
12 |
13 | Press `R` in game to see a tilesheet reference, press `R` again to return to the game. Press `Q` to show the debug layer.
14 |
15 | Contributions must be valid typescript & formatted with prettier.js.
16 |
17 | Otherwise, go nuts.
18 |
19 | ## TODO
20 |
21 | * use `PerformanceObserver` to get a more accurate FPS value
22 |
23 | ## Credits
24 |
25 | * Uses [mrpas](https://www.npmjs.com/package/mrpas) to determine the field of view
26 | * Uses [dungeoneer](https://www.npmjs.com/package/dungeoneer) to generate the dungeon
27 | * `Rogue*.png` files are from the [Rogue Dungeon Tileset 16x16](https://fongoose.itch.io/rogue-dungeon-tileset-16x16) by [fongoose](https://twitter.com/fongoosemike)
28 | * "CasualEncounter" font from Anna Anthropy's [World of Fonts](https://w.itch.io/world-of-fonts)
29 |
--------------------------------------------------------------------------------
/assets/Util.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mipearson/dungeondash/4d72aee8c1dbb72dfa768e3109b9f3ca585c2230/assets/Util.png
--------------------------------------------------------------------------------
/assets/annaanthropy/CasualEncounter.fnt:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
--------------------------------------------------------------------------------
/assets/annaanthropy/CasualEncounter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mipearson/dungeondash/4d72aee8c1dbb72dfa768e3109b9f3ca585c2230/assets/annaanthropy/CasualEncounter.png
--------------------------------------------------------------------------------
/assets/annaanthropy/CasualEncounter.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mipearson/dungeondash/4d72aee8c1dbb72dfa768e3109b9f3ca585c2230/assets/annaanthropy/CasualEncounter.ttf
--------------------------------------------------------------------------------
/assets/annaanthropy/README.md:
--------------------------------------------------------------------------------
1 | Credit: https://w.itch.io/world-of-fonts
2 |
--------------------------------------------------------------------------------
/assets/fongoose/README.md:
--------------------------------------------------------------------------------
1 | The files in this directory are from Fongoose's [Rogue Dungeon Tileset](https://fongoose.itch.io/rogue-dungeon-tileset-16x16) and are used with permission.
2 |
3 | If you like this tileset, please purchase a copy from the above link rather than copying them from this project.
4 |
--------------------------------------------------------------------------------
/assets/fongoose/RogueEnvironment16x16-extruded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mipearson/dungeondash/4d72aee8c1dbb72dfa768e3109b9f3ca585c2230/assets/fongoose/RogueEnvironment16x16-extruded.png
--------------------------------------------------------------------------------
/assets/fongoose/RogueEnvironment16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mipearson/dungeondash/4d72aee8c1dbb72dfa768e3109b9f3ca585c2230/assets/fongoose/RogueEnvironment16x16.png
--------------------------------------------------------------------------------
/assets/fongoose/RogueItems16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mipearson/dungeondash/4d72aee8c1dbb72dfa768e3109b9f3ca585c2230/assets/fongoose/RogueItems16x16.png
--------------------------------------------------------------------------------
/assets/fongoose/RoguePlayer_48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mipearson/dungeondash/4d72aee8c1dbb72dfa768e3109b9f3ca585c2230/assets/fongoose/RoguePlayer_48x48.png
--------------------------------------------------------------------------------
/assets/fongoose/RoguePlayer_GUIDE.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mipearson/dungeondash/4d72aee8c1dbb72dfa768e3109b9f3ca585c2230/assets/fongoose/RoguePlayer_GUIDE.png
--------------------------------------------------------------------------------
/assets/fongoose/RogueSlime32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mipearson/dungeondash/4d72aee8c1dbb72dfa768e3109b9f3ca585c2230/assets/fongoose/RogueSlime32x32.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Dungeon Dash!
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dkgame",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "parcel index.html",
8 | "lint": "tsc --noEmit",
9 | "ship": "rm -rf dist && parcel build index.html --no-source-maps && surge --domain https://dungeon-dash.surge.sh dist && rm -rf dist"
10 | },
11 | "author": "Michael Pearson & Others",
12 | "license": "MIT",
13 | "dependencies": {
14 | "dungeoneer": "github:mipearson/dungeoneer#mp-fix-ts-bindings",
15 | "mrpas": "^2.0.0",
16 | "parcel": "^1.12.4",
17 | "phaser": "^3.21.0",
18 | "phaser-plugin-scene-watcher": "^4.0.0",
19 | "typescript": "^3.7.4"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/assets/Fonts.ts:
--------------------------------------------------------------------------------
1 | import CasualEncounterPNG from "../../assets/annaanthropy/CasualEncounter.png";
2 | import CasualEncounterFNT from "../../assets/annaanthropy/CasualEncounter.fnt";
3 |
4 | export default {
5 | default: [CasualEncounterPNG, CasualEncounterFNT]
6 | };
7 |
--------------------------------------------------------------------------------
/src/assets/Graphics.ts:
--------------------------------------------------------------------------------
1 | import RogueEnvironment from "../../assets/fongoose/RogueEnvironment16x16-extruded.png";
2 | import RoguePlayer from "../../assets/fongoose/RoguePlayer_48x48.png";
3 | import RogueSlime from "../../assets/fongoose/RogueSlime32x32.png";
4 | import RogueItems from "../../assets/fongoose/RogueItems16x16.png";
5 |
6 | import Util from "../../assets/Util.png";
7 |
8 | type AnimConfig = {
9 | key: string;
10 | frames: Phaser.Types.Animations.GenerateFrameNumbers;
11 | defaultTextureKey?: string;
12 | frameRate?: integer;
13 | duration?: integer;
14 | skipMissedFrames?: boolean;
15 | delay?: integer;
16 | repeat?: integer;
17 | repeatDelay?: integer;
18 | yoyo?: boolean;
19 | showOnStart?: boolean;
20 | hideOnComplete?: boolean;
21 | };
22 |
23 | type GraphicSet = {
24 | name: string;
25 | width: number;
26 | height: number;
27 | file: string;
28 | margin?: number;
29 | spacing?: number;
30 | };
31 |
32 | type AnimSet = GraphicSet & {
33 | animations: { [k: string]: AnimConfig };
34 | };
35 |
36 | const environment = {
37 | name: "environment",
38 | width: 16,
39 | height: 16,
40 | margin: 1,
41 | spacing: 2,
42 | file: RogueEnvironment,
43 | indices: {
44 | floor: {
45 | outer: [0x05, 0x05, 0x05, 0x15, 0x07, 0x17],
46 | outerCorridor: [0x0d, 0x0d, 0x0d, 0x1d, 0x0f, 0x1f]
47 | },
48 | block: 0x17,
49 | doors: {
50 | horizontal: 0x81,
51 | vertical: 0x92,
52 | destroyed: 0x35
53 | },
54 | walls: {
55 | alone: 0x14,
56 | intersections: {
57 | e_s: 0x00,
58 | n_e_s_w: 0x01,
59 | e_w: 0x02,
60 | s_w: 0x03,
61 | n_e_s: 0x10,
62 | w: 0x11,
63 | e: 0x12,
64 | n_s_w: 0x13,
65 | n_s: 0x20,
66 | s: 0x21,
67 | e_s_w: 0x22,
68 | n_e: 0x30,
69 | n_e_w: 0x31,
70 | n: 0x32,
71 | n_w: 0x33,
72 | e_door: 0x65,
73 | w_door: 0x67
74 | }
75 | }
76 | }
77 | };
78 |
79 | const player: AnimSet = {
80 | name: "player",
81 | width: 48,
82 | height: 48,
83 | file: RoguePlayer,
84 | animations: {
85 | idle: {
86 | key: "playerIdle",
87 | frames: { start: 0x01, end: 0x07 },
88 | frameRate: 6,
89 | repeat: -1
90 | },
91 | idleBack: {
92 | key: "playerIdleBack",
93 | frames: { start: 0x0a, end: 0x11 },
94 | frameRate: 6,
95 | repeat: -1
96 | },
97 | walk: {
98 | key: "playerWalk",
99 | frames: { start: 0x14, end: 0x19 },
100 | frameRate: 10,
101 | repeat: -1
102 | },
103 | walkBack: {
104 | key: "playerWalkBack",
105 | frames: { start: 0x1e, end: 0x23 },
106 | frameRate: 10,
107 | repeat: -1
108 | },
109 | // Ideally attacks should be five frames at 30fps to
110 | // align with the attack duration of 165ms
111 | slash: {
112 | key: "playerSlash",
113 | frames: { frames: [0x1a, 0x1a, 0x1a, 0x1b, 0x1c] },
114 | frameRate: 30
115 | },
116 | slashUp: {
117 | key: "playerSlashUp",
118 | frames: { frames: [0x2e, 0x2e, 0x2e, 0x2f, 0x30] },
119 | frameRate: 30
120 | },
121 | slashDown: {
122 | key: "playerSlashDown",
123 | frames: { frames: [0x24, 0x24, 0x24, 0x25, 0x26] },
124 | frameRate: 30
125 | },
126 | stagger: {
127 | key: "playerStagger",
128 | frames: { frames: [0x38, 0x38, 0x39, 0x3a] },
129 | frameRate: 30
130 | }
131 | }
132 | };
133 |
134 | const slime: AnimSet = {
135 | name: "slime",
136 | width: 32,
137 | height: 32,
138 | file: RogueSlime,
139 | animations: {
140 | idle: {
141 | key: "slimeIdle",
142 | frames: { start: 0x00, end: 0x05 },
143 | frameRate: 6,
144 | repeat: -1
145 | },
146 | move: {
147 | key: "slimeMove",
148 | frames: { start: 0x08, end: 0x0e },
149 | frameRate: 8,
150 | repeat: -1
151 | },
152 | death: {
153 | key: "slimeDeath",
154 | frames: { start: 0x20, end: 0x26 },
155 | frameRate: 16,
156 | hideOnComplete: true
157 | }
158 | }
159 | };
160 |
161 | const items = {
162 | name: "items",
163 | width: 16,
164 | height: 16,
165 | file: RogueItems
166 | };
167 |
168 | const util = {
169 | name: "util",
170 | width: 16,
171 | height: 16,
172 | file: Util,
173 | indices: {
174 | black: 0x00
175 | }
176 | };
177 |
178 | export default {
179 | environment,
180 | player,
181 | slime,
182 | items,
183 | util
184 | };
185 |
--------------------------------------------------------------------------------
/src/entities/FOVLayer.ts:
--------------------------------------------------------------------------------
1 | import Graphics from "../assets/Graphics";
2 | import Map from "../entities/Map";
3 | import { Mrpas } from "mrpas";
4 | import Phaser from "phaser";
5 |
6 | const radius = 7;
7 | const fogAlpha = 0.8;
8 |
9 | const lightDropoff = [0.7, 0.6, 0.3, 0.1];
10 |
11 | // Alpha to transition per MS given maximum distance between desired
12 | // and actual alpha
13 | const alphaPerMs = 0.004;
14 |
15 | function updateTileAlpha(
16 | desiredAlpha: number,
17 | delta: number,
18 | tile: Phaser.Tilemaps.Tile
19 | ) {
20 | // Update faster the further away we are from the desired value,
21 | // but restrict the lower bound so we don't get it slowing
22 | // down infinitley.
23 | const distance = Math.max(Math.abs(tile.alpha - desiredAlpha), 0.05);
24 | const updateFactor = alphaPerMs * delta * distance;
25 | if (tile.alpha > desiredAlpha) {
26 | tile.setAlpha(Phaser.Math.MinSub(tile.alpha, updateFactor, desiredAlpha));
27 | } else if (tile.alpha < desiredAlpha) {
28 | tile.setAlpha(Phaser.Math.MaxAdd(tile.alpha, updateFactor, desiredAlpha));
29 | }
30 | }
31 |
32 | export default class FOVLayer {
33 | public layer: Phaser.Tilemaps.DynamicTilemapLayer;
34 | private mrpas: Mrpas | undefined;
35 | private lastPos: Phaser.Math.Vector2;
36 | private map: Map;
37 |
38 | constructor(map: Map) {
39 | const utilTiles = map.tilemap.addTilesetImage("util");
40 |
41 | this.layer = map.tilemap
42 | .createBlankDynamicLayer("Dark", utilTiles, 0, 0)
43 | .fill(Graphics.util.indices.black);
44 | this.layer.setDepth(100);
45 |
46 | this.map = map;
47 | this.recalculate();
48 |
49 | this.lastPos = new Phaser.Math.Vector2({ x: -1, y: -1 });
50 | }
51 |
52 | recalculate() {
53 | this.mrpas = new Mrpas(
54 | this.map.width,
55 | this.map.height,
56 | (x: number, y: number) => {
57 | return this.map.tiles[y] && !this.map.tiles[y][x].collides;
58 | }
59 | );
60 | }
61 |
62 | update(
63 | pos: Phaser.Math.Vector2,
64 | bounds: Phaser.Geom.Rectangle,
65 | delta: number
66 | ) {
67 | if (!this.lastPos.equals(pos)) {
68 | this.updateMRPAS(pos);
69 | this.lastPos = pos.clone();
70 | }
71 |
72 | for (let y = bounds.y; y < bounds.y + bounds.height; y++) {
73 | for (let x = bounds.x; x < bounds.x + bounds.width; x++) {
74 | if (y < 0 || y >= this.map.height || x < 0 || x >= this.map.width) {
75 | continue;
76 | }
77 | const desiredAlpha = this.map.tiles[y][x].desiredAlpha;
78 | const tile = this.layer.getTileAt(x, y);
79 | updateTileAlpha(desiredAlpha, delta, tile);
80 | }
81 | }
82 | }
83 |
84 | updateMRPAS(pos: Phaser.Math.Vector2) {
85 | // TODO: performance?
86 | for (let row of this.map.tiles) {
87 | for (let tile of row) {
88 | if (tile.seen) {
89 | tile.desiredAlpha = fogAlpha;
90 | }
91 | }
92 | }
93 |
94 | this.mrpas!.compute(
95 | pos.x,
96 | pos.y,
97 | radius,
98 | (x: number, y: number) => this.map.tiles[y][x].seen,
99 | (x: number, y: number) => {
100 | const distance = Math.floor(
101 | new Phaser.Math.Vector2(x, y).distance(
102 | new Phaser.Math.Vector2(pos.x, pos.y)
103 | )
104 | );
105 |
106 | const rolloffIdx = distance <= radius ? radius - distance : 0;
107 | const alpha =
108 | rolloffIdx < lightDropoff.length ? lightDropoff[rolloffIdx] : 0;
109 | this.map.tiles[y][x].desiredAlpha = alpha;
110 | this.map.tiles[y][x].seen = true;
111 | }
112 | );
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/src/entities/Map.ts:
--------------------------------------------------------------------------------
1 | import Dungeoneer from "dungeoneer";
2 | import Tile, { TileType } from "./Tile";
3 | import Slime from "./Slime";
4 | import Graphics from "../assets/Graphics";
5 | import DungeonScene from "../scenes/DungeonScene";
6 |
7 | export default class Map {
8 | public readonly tiles: Tile[][];
9 | public readonly width: number;
10 | public readonly height: number;
11 | public readonly tilemap: Phaser.Tilemaps.Tilemap;
12 | public readonly wallLayer: Phaser.Tilemaps.StaticTilemapLayer;
13 | public readonly doorLayer: Phaser.Tilemaps.DynamicTilemapLayer;
14 |
15 | public readonly startingX: number;
16 | public readonly startingY: number;
17 |
18 | public readonly slimes: Slime[];
19 |
20 | public readonly rooms: Dungeoneer.Room[];
21 |
22 | constructor(width: number, height: number, scene: DungeonScene) {
23 | const dungeon = Dungeoneer.build({
24 | width: width,
25 | height: height
26 | });
27 | this.rooms = dungeon.rooms;
28 |
29 | this.width = width;
30 | this.height = height;
31 |
32 | this.tiles = [];
33 | for (let y = 0; y < height; y++) {
34 | this.tiles.push([]);
35 | for (let x = 0; x < width; x++) {
36 | const tileType = Tile.tileTypeFor(dungeon.tiles[x][y].type);
37 | this.tiles[y][x] = new Tile(tileType, x, y, this);
38 | }
39 | }
40 |
41 | const toReset = [];
42 | for (let y = 0; y < height; y++) {
43 | for (let x = 0; x < width; x++) {
44 | const tile = this.tiles[y][x];
45 | if (tile.type === TileType.Wall && tile.isEnclosed()) {
46 | toReset.push({ y: y, x: x });
47 | }
48 | }
49 | }
50 |
51 | toReset.forEach(d => {
52 | this.tiles[d.y][d.x] = new Tile(TileType.None, d.x, d.y, this);
53 | });
54 |
55 | const roomNumber = Math.floor(Math.random() * dungeon.rooms.length);
56 |
57 | const firstRoom = dungeon.rooms[roomNumber];
58 | this.startingX = Math.floor(firstRoom.x + firstRoom.width / 2);
59 | this.startingY = Math.floor(firstRoom.y + firstRoom.height / 2);
60 |
61 | this.tilemap = scene.make.tilemap({
62 | tileWidth: Graphics.environment.width,
63 | tileHeight: Graphics.environment.height,
64 | width: width,
65 | height: height
66 | });
67 |
68 | const dungeonTiles = this.tilemap.addTilesetImage(
69 | Graphics.environment.name,
70 | Graphics.environment.name,
71 | Graphics.environment.width,
72 | Graphics.environment.height,
73 | Graphics.environment.margin,
74 | Graphics.environment.spacing
75 | );
76 |
77 | const groundLayer = this.tilemap
78 | .createBlankDynamicLayer("Ground", dungeonTiles, 0, 0)
79 | .randomize(
80 | 0,
81 | 0,
82 | this.width,
83 | this.height,
84 | Graphics.environment.indices.floor.outerCorridor
85 | );
86 |
87 | this.slimes = [];
88 |
89 | for (let room of dungeon.rooms) {
90 | groundLayer.randomize(
91 | room.x - 1,
92 | room.y - 1,
93 | room.width + 2,
94 | room.height + 2,
95 | Graphics.environment.indices.floor.outer
96 | );
97 |
98 | if (room.height < 4 || room.width < 4) {
99 | continue;
100 | }
101 |
102 | const roomTL = this.tilemap.tileToWorldXY(room.x + 1, room.y + 1);
103 | const roomBounds = this.tilemap.tileToWorldXY(
104 | room.x + room.width - 1,
105 | room.y + room.height - 1
106 | );
107 | const numSlimes = Phaser.Math.Between(1, 3);
108 | for (let i = 0; i < numSlimes; i++) {
109 | this.slimes.push(
110 | new Slime(
111 | Phaser.Math.Between(roomTL.x, roomBounds.x),
112 | Phaser.Math.Between(roomTL.y, roomBounds.y),
113 | scene
114 | )
115 | );
116 | }
117 | }
118 | this.tilemap.convertLayerToStatic(groundLayer).setDepth(1);
119 |
120 | const wallLayer = this.tilemap.createBlankDynamicLayer(
121 | "Wall",
122 | dungeonTiles,
123 | 0,
124 | 0
125 | );
126 |
127 | this.doorLayer = this.tilemap.createBlankDynamicLayer(
128 | "Door",
129 | dungeonTiles,
130 | 0,
131 | 0
132 | );
133 |
134 | for (let x = 0; x < width; x++) {
135 | for (let y = 0; y < height; y++) {
136 | const tile = this.tiles[y][x];
137 | if (tile.type === TileType.Wall) {
138 | wallLayer.putTileAt(tile.spriteIndex(), x, y);
139 | } else if (tile.type === TileType.Door) {
140 | this.doorLayer.putTileAt(tile.spriteIndex(), x, y);
141 | }
142 | }
143 | }
144 | wallLayer.setCollisionBetween(0, 0x7f);
145 | const collidableDoors = [
146 | Graphics.environment.indices.doors.horizontal,
147 | Graphics.environment.indices.doors.vertical
148 | ];
149 | this.doorLayer.setCollision(collidableDoors);
150 |
151 | this.doorLayer.setTileIndexCallback(
152 | collidableDoors,
153 | (_: unknown, tile: Phaser.Tilemaps.Tile) => {
154 | this.doorLayer.putTileAt(
155 | Graphics.environment.indices.doors.destroyed,
156 | tile.x,
157 | tile.y
158 | );
159 | this.tileAt(tile.x, tile.y)!.open();
160 | scene.fov!.recalculate();
161 | },
162 | this
163 | );
164 | this.doorLayer.setDepth(3);
165 |
166 | this.wallLayer = this.tilemap.convertLayerToStatic(wallLayer);
167 | this.wallLayer.setDepth(2);
168 | }
169 |
170 | tileAt(x: number, y: number): Tile | null {
171 | if (y < 0 || y >= this.height || x < 0 || x >= this.width) {
172 | return null;
173 | }
174 | return this.tiles[y][x];
175 | }
176 |
177 | withinRoom(x: number, y: number): boolean {
178 | return (
179 | this.rooms.find(r => {
180 | const { top, left, right, bottom } = r.getBoundingBox();
181 | return (
182 | y >= top - 1 && y <= bottom + 1 && x >= left - 1 && x <= right + 1
183 | );
184 | }) != undefined
185 | );
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/src/entities/Player.ts:
--------------------------------------------------------------------------------
1 | import Phaser from "phaser";
2 | import Graphics from "../assets/Graphics";
3 |
4 | const speed = 125;
5 | const attackSpeed = 500;
6 | const attackDuration = 165;
7 | const staggerDuration = 200;
8 | const staggerSpeed = 100;
9 | const attackCooldown = attackDuration * 2;
10 |
11 | interface Keys {
12 | up: Phaser.Input.Keyboard.Key;
13 | down: Phaser.Input.Keyboard.Key;
14 | left: Phaser.Input.Keyboard.Key;
15 | right: Phaser.Input.Keyboard.Key;
16 | space: Phaser.Input.Keyboard.Key;
17 | w: Phaser.Input.Keyboard.Key;
18 | a: Phaser.Input.Keyboard.Key;
19 | s: Phaser.Input.Keyboard.Key;
20 | d: Phaser.Input.Keyboard.Key;
21 | }
22 |
23 | export default class Player {
24 | public sprite: Phaser.Physics.Arcade.Sprite;
25 | private keys: Keys;
26 |
27 | private attackUntil: number;
28 | private staggerUntil: number;
29 | private attackLockedUntil: number;
30 | private emitter: Phaser.GameObjects.Particles.ParticleEmitter;
31 | private flashEmitter: Phaser.GameObjects.Particles.ParticleEmitter;
32 | private body: Phaser.Physics.Arcade.Body;
33 | private attacking: boolean;
34 | private time: number;
35 | private staggered: boolean;
36 | private scene: Phaser.Scene;
37 | private facingUp: boolean;
38 |
39 | constructor(x: number, y: number, scene: Phaser.Scene) {
40 | this.scene = scene;
41 | this.sprite = scene.physics.add.sprite(x, y, Graphics.player.name, 0);
42 | this.sprite.setSize(8, 8);
43 | this.sprite.setOffset(20, 28);
44 | this.sprite.anims.play(Graphics.player.animations.idle.key);
45 | this.facingUp = false;
46 | this.sprite.setDepth(5);
47 |
48 | this.keys = scene.input.keyboard.addKeys({
49 | up: Phaser.Input.Keyboard.KeyCodes.UP,
50 | down: Phaser.Input.Keyboard.KeyCodes.DOWN,
51 | left: Phaser.Input.Keyboard.KeyCodes.LEFT,
52 | right: Phaser.Input.Keyboard.KeyCodes.RIGHT,
53 | space: Phaser.Input.Keyboard.KeyCodes.SPACE,
54 | w: "w",
55 | a: "a",
56 | s: "s",
57 | d: "d"
58 | }) as Keys;
59 |
60 | this.attackUntil = 0;
61 | this.attackLockedUntil = 0;
62 | this.attacking = false;
63 | this.staggerUntil = 0;
64 | this.staggered = false;
65 | const particles = scene.add.particles(Graphics.player.name);
66 | particles.setDepth(6);
67 | this.emitter = particles.createEmitter({
68 | alpha: { start: 0.7, end: 0, ease: "Cubic.easeOut" },
69 | follow: this.sprite,
70 | quantity: 1,
71 | lifespan: 200,
72 | blendMode: Phaser.BlendModes.ADD,
73 | scaleX: () => (this.sprite.flipX ? -1 : 1),
74 | emitCallback: (particle: Phaser.GameObjects.Particles.Particle) => {
75 | particle.frame = this.sprite.frame;
76 | }
77 | });
78 | this.emitter.stop();
79 |
80 | this.flashEmitter = particles.createEmitter({
81 | alpha: { start: 0.5, end: 0, ease: "Cubic.easeOut" },
82 | follow: this.sprite,
83 | quantity: 1,
84 | lifespan: 100,
85 | scaleX: () => (this.sprite.flipX ? -1 : 1),
86 | emitCallback: (particle: Phaser.GameObjects.Particles.Particle) => {
87 | particle.frame = this.sprite.frame;
88 | }
89 | });
90 | this.flashEmitter.stop();
91 |
92 | this.body = this.sprite.body;
93 | this.time = 0;
94 | }
95 |
96 | isAttacking(): boolean {
97 | return this.attacking;
98 | }
99 |
100 | stagger(): void {
101 | if (this.time > this.staggerUntil) {
102 | this.staggered = true;
103 | // TODO
104 | this.scene.cameras.main.shake(150, 0.001);
105 | this.scene.cameras.main.flash(50, 100, 0, 0);
106 | }
107 | }
108 |
109 | update(time: number) {
110 | this.time = time;
111 | const keys = this.keys;
112 | let attackAnim = "";
113 | let moveAnim = "";
114 |
115 | if (this.staggered && !this.body.touching.none) {
116 | this.staggerUntil = this.time + staggerDuration;
117 | this.staggered = false;
118 |
119 | this.body.setVelocity(0);
120 | if (this.body.touching.down) {
121 | this.body.setVelocityY(-staggerSpeed);
122 | } else if (this.body.touching.up) {
123 | this.body.setVelocityY(staggerSpeed);
124 | } else if (this.body.touching.left) {
125 | this.body.setVelocityX(staggerSpeed);
126 | this.sprite.setFlipX(true);
127 | } else if (this.body.touching.right) {
128 | this.body.setVelocityX(-staggerSpeed);
129 | this.sprite.setFlipX(false);
130 | }
131 | this.sprite.anims.play(Graphics.player.animations.stagger.key);
132 |
133 | this.flashEmitter.start();
134 | // this.sprite.setBlendMode(Phaser.BlendModes.MULTIPLY);
135 | }
136 |
137 | if (time < this.attackUntil || time < this.staggerUntil) {
138 | return;
139 | }
140 |
141 | this.body.setVelocity(0);
142 |
143 | const left = keys.left.isDown || keys.a.isDown;
144 | const right = keys.right.isDown || keys.d.isDown;
145 | const up = keys.up.isDown || keys.w.isDown;
146 | const down = keys.down.isDown || keys.s.isDown;
147 |
148 | if (!this.body.blocked.left && left) {
149 | this.body.setVelocityX(-speed);
150 | this.sprite.setFlipX(true);
151 | } else if (!this.body.blocked.right && right) {
152 | this.body.setVelocityX(speed);
153 | this.sprite.setFlipX(false);
154 | }
155 |
156 | if (!this.body.blocked.up && up) {
157 | this.body.setVelocityY(-speed);
158 | } else if (!this.body.blocked.down && down) {
159 | this.body.setVelocityY(speed);
160 | }
161 |
162 | if (left || right) {
163 | moveAnim = Graphics.player.animations.walk.key;
164 | attackAnim = Graphics.player.animations.slash.key;
165 | this.facingUp = false;
166 | } else if (down) {
167 | moveAnim = Graphics.player.animations.walk.key;
168 | attackAnim = Graphics.player.animations.slashDown.key;
169 | this.facingUp = false;
170 | } else if (up) {
171 | moveAnim = Graphics.player.animations.walkBack.key;
172 | attackAnim = Graphics.player.animations.slashUp.key;
173 | this.facingUp = true;
174 | } else if (this.facingUp) {
175 | moveAnim = Graphics.player.animations.idleBack.key;
176 | } else {
177 | moveAnim = Graphics.player.animations.idle.key;
178 | }
179 |
180 | if (
181 | keys.space!.isDown &&
182 | time > this.attackLockedUntil &&
183 | this.body.velocity.length() > 0
184 | ) {
185 | this.attackUntil = time + attackDuration;
186 | this.attackLockedUntil = time + attackDuration + attackCooldown;
187 | this.body.velocity.normalize().scale(attackSpeed);
188 | this.sprite.anims.play(attackAnim, true);
189 | this.emitter.start();
190 | this.sprite.setBlendMode(Phaser.BlendModes.ADD);
191 | this.attacking = true;
192 | return;
193 | }
194 |
195 | this.attacking = false;
196 | this.sprite.anims.play(moveAnim, true);
197 | this.body.velocity.normalize().scale(speed);
198 | this.sprite.setBlendMode(Phaser.BlendModes.NORMAL);
199 | if (this.emitter.on) {
200 | this.emitter.stop();
201 | }
202 | if (this.flashEmitter.on) {
203 | this.flashEmitter.stop();
204 | }
205 | }
206 | }
207 |
--------------------------------------------------------------------------------
/src/entities/Slime.ts:
--------------------------------------------------------------------------------
1 | import Phaser from "phaser";
2 | import Graphics from "../assets/Graphics";
3 |
4 | const speed = 20;
5 |
6 | export default class Slime {
7 | public readonly sprite: Phaser.Physics.Arcade.Sprite;
8 | private readonly body: Phaser.Physics.Arcade.Body;
9 | private nextAction: number;
10 |
11 | constructor(x: number, y: number, scene: Phaser.Scene) {
12 | this.sprite = scene.physics.add.sprite(x, y, Graphics.slime.name, 0);
13 | this.sprite.setSize(12, 10);
14 | this.sprite.setOffset(10, 14);
15 | this.sprite.anims.play(Graphics.slime.animations.idle.key);
16 | this.sprite.setDepth(10);
17 |
18 | this.body = this.sprite.body;
19 | this.nextAction = 0;
20 | this.body.bounce.set(0, 0);
21 | this.body.setImmovable(true);
22 | }
23 |
24 | update(time: number) {
25 | if (time < this.nextAction) {
26 | return;
27 | }
28 |
29 | if (Phaser.Math.Between(0, 1) === 0) {
30 | this.body.setVelocity(0);
31 | this.sprite.anims.play(Graphics.slime.animations.idle.key, true);
32 | } else {
33 | this.sprite.anims.play(Graphics.slime.animations.move.key, true);
34 | const direction = Phaser.Math.Between(0, 3);
35 | this.body.setVelocity(0);
36 |
37 | if (!this.body.blocked.left && direction === 0) {
38 | this.body.setVelocityX(-speed);
39 | } else if (!this.body.blocked.right && direction <= 1) {
40 | this.body.setVelocityX(speed);
41 | } else if (!this.body.blocked.up && direction <= 2) {
42 | this.body.setVelocityY(-speed);
43 | } else if (!this.body.blocked.down && direction <= 3) {
44 | this.body.setVelocityY(speed);
45 | } else {
46 | console.log(`Couldn't find direction for slime: ${direction}`);
47 | }
48 | }
49 |
50 | this.nextAction = time + Phaser.Math.Between(1000, 3000);
51 | }
52 |
53 | kill() {
54 | this.sprite.anims.play(Graphics.slime.animations.death.key, false);
55 | this.sprite.disableBody();
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/entities/Tile.ts:
--------------------------------------------------------------------------------
1 | import Map from "./map";
2 | import Graphics from "../assets/Graphics";
3 |
4 | export enum TileType {
5 | None,
6 | Wall,
7 | Door
8 | }
9 |
10 | export default class Tile {
11 | public collides: boolean;
12 | public readonly type: TileType;
13 | public readonly map: Map;
14 | public readonly x: number;
15 | public readonly y: number;
16 | public seen: boolean;
17 | public desiredAlpha: number; // TODO: Move out of this class, specific to FOV
18 | public readonly corridor: boolean;
19 |
20 | public static tileTypeFor(type: string): TileType {
21 | if (type === "wall") {
22 | return TileType.Wall;
23 | } else if (type === "door") {
24 | return TileType.Door;
25 | } else {
26 | return TileType.None;
27 | }
28 | }
29 |
30 | constructor(type: TileType, x: number, y: number, map: Map) {
31 | this.type = type;
32 | this.collides = type !== TileType.None;
33 | this.map = map;
34 | this.x = x;
35 | this.y = y;
36 | this.seen = false;
37 | this.desiredAlpha = 1;
38 | this.corridor = !map.withinRoom(x, y);
39 | }
40 |
41 | open() {
42 | this.collides = false;
43 | }
44 |
45 | neighbours(): { [dir: string]: Tile | null } {
46 | return {
47 | n: this.map.tileAt(this.x, this.y - 1),
48 | s: this.map.tileAt(this.x, this.y + 1),
49 | w: this.map.tileAt(this.x - 1, this.y),
50 | e: this.map.tileAt(this.x + 1, this.y),
51 | nw: this.map.tileAt(this.x - 1, this.y - 1),
52 | ne: this.map.tileAt(this.x + 1, this.y - 1),
53 | sw: this.map.tileAt(this.x - 1, this.y + 1),
54 | se: this.map.tileAt(this.x + 1, this.y + 1)
55 | };
56 | }
57 |
58 | isEnclosed(): boolean {
59 | return (
60 | Object.values(this.neighbours()).filter(
61 | t => !t || (t.type === TileType.Wall && t.corridor === this.corridor)
62 | ).length === 8
63 | );
64 | }
65 |
66 | spriteIndex(): number {
67 | const modifier = this.type === TileType.Wall && this.corridor ? 8 : 0;
68 | return this.rawIndex() + modifier;
69 | }
70 |
71 | // prettier-ignore
72 | private rawIndex(): number {
73 | const neighbours = this.neighbours();
74 |
75 | const n = neighbours.n && neighbours.n.type === TileType.Wall && neighbours.n.corridor === this.corridor;
76 | const s = neighbours.s && neighbours.s.type === TileType.Wall && neighbours.s.corridor === this.corridor
77 | const w = neighbours.w && neighbours.w.type === TileType.Wall && neighbours.w.corridor === this.corridor
78 | const e = neighbours.e && neighbours.e.type === TileType.Wall && neighbours.e.corridor === this.corridor
79 |
80 | const wDoor = neighbours.w && neighbours.w.type === TileType.Door;
81 | const eDoor = neighbours.e && neighbours.e.type === TileType.Door;
82 |
83 | const i = Graphics.environment.indices.walls;
84 |
85 | if (this.type === TileType.Wall) {
86 | if (n && e && s && w) { return i.intersections.n_e_s_w; }
87 | if (n && e && s) { return i.intersections.n_e_s; }
88 | if (n && s && w) { return i.intersections.n_s_w; }
89 | if (e && s && w) { return i.intersections.e_s_w; }
90 | if (n && e && w) { return i.intersections.n_e_w; }
91 |
92 | if (e && s) { return i.intersections.e_s; }
93 | if (e && w) { return i.intersections.e_w; }
94 | if (s && w) { return i.intersections.s_w; }
95 | if (n && s) { return i.intersections.n_s; }
96 | if (n && e) { return i.intersections.n_e; }
97 | if (n && w) { return i.intersections.n_w; }
98 |
99 | if (w && eDoor) { return i.intersections.e_door; }
100 | if (e && wDoor) { return i.intersections.w_door; }
101 |
102 | if (n) { return i.intersections.n; }
103 | if (s) { return i.intersections.s; }
104 | if (e) { return i.intersections.e; }
105 | if (w) { return i.intersections.w; }
106 |
107 | return i.alone;
108 | }
109 |
110 | if (this.type === TileType.Door) {
111 | if (n || s) {
112 | return Graphics.environment.indices.doors.vertical
113 | } else {
114 | return Graphics.environment.indices.doors.horizontal;
115 | }
116 | }
117 |
118 | return 0;
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import Phaser from "phaser";
2 | import ReferenceScene from "./scenes/ReferenceScene";
3 | import DungeonScene from "./scenes/DungeonScene";
4 | import InfoScene from "./scenes/InfoScene";
5 | // import SceneWatcherPlugin from "phaser-plugin-scene-watcher";
6 |
7 | new Phaser.Game({
8 | type: Phaser.WEBGL,
9 | width: window.innerWidth,
10 | height: window.innerHeight,
11 | render: { pixelArt: true },
12 | physics: { default: "arcade", arcade: { debug: false, gravity: { y: 0 } } },
13 | scene: [DungeonScene, InfoScene, ReferenceScene],
14 | scale: {
15 | mode: Phaser.Scale.RESIZE
16 | }
17 | // plugins: {
18 | // global: [{ key: "SceneWatcher", plugin: SceneWatcherPlugin, start: true }]
19 | // }
20 | });
21 |
--------------------------------------------------------------------------------
/src/scenes/DungeonScene.ts:
--------------------------------------------------------------------------------
1 | import Phaser from "phaser";
2 | import Graphics from "../assets/Graphics";
3 | import FOVLayer from "../entities/FOVLayer";
4 | import Player from "../entities/Player";
5 | import Slime from "../entities/Slime";
6 | import Map from "../entities/Map";
7 |
8 | const worldTileHeight = 81;
9 | const worldTileWidth = 81;
10 |
11 | export default class DungeonScene extends Phaser.Scene {
12 | lastX: number;
13 | lastY: number;
14 | player: Player | null;
15 | slimes: Slime[];
16 | slimeGroup: Phaser.GameObjects.Group | null;
17 | fov: FOVLayer | null;
18 | tilemap: Phaser.Tilemaps.Tilemap | null;
19 | roomDebugGraphics?: Phaser.GameObjects.Graphics;
20 |
21 | preload(): void {
22 | this.load.image(Graphics.environment.name, Graphics.environment.file);
23 | this.load.image(Graphics.util.name, Graphics.util.file);
24 | this.load.spritesheet(Graphics.player.name, Graphics.player.file, {
25 | frameHeight: Graphics.player.height,
26 | frameWidth: Graphics.player.width
27 | });
28 | this.load.spritesheet(Graphics.slime.name, Graphics.slime.file, {
29 | frameHeight: Graphics.slime.height,
30 | frameWidth: Graphics.slime.width
31 | });
32 | }
33 |
34 | constructor() {
35 | super("DungeonScene");
36 | this.lastX = -1;
37 | this.lastY = -1;
38 | this.player = null;
39 | this.fov = null;
40 | this.tilemap = null;
41 | this.slimes = [];
42 | this.slimeGroup = null;
43 | }
44 |
45 | slimePlayerCollide(
46 | _: Phaser.GameObjects.GameObject,
47 | slimeSprite: Phaser.GameObjects.GameObject
48 | ) {
49 | const slime = this.slimes.find(s => s.sprite === slimeSprite);
50 | if (!slime) {
51 | console.log("Missing slime for sprite collision!");
52 | return;
53 | }
54 |
55 | if (this.player!.isAttacking()) {
56 | this.slimes = this.slimes.filter(s => s != slime);
57 | slime.kill();
58 | return false;
59 | } else {
60 | this.player!.stagger();
61 | return true;
62 | }
63 | }
64 |
65 | create(): void {
66 | this.events.on("wake", () => {
67 | this.scene.run("InfoScene");
68 | });
69 |
70 | Object.values(Graphics.player.animations).forEach(anim => {
71 | if (!this.anims.get(anim.key)) {
72 | this.anims.create({
73 | ...anim,
74 | frames: this.anims.generateFrameNumbers(
75 | Graphics.player.name,
76 | anim.frames
77 | )
78 | });
79 | }
80 | });
81 |
82 | // TODO
83 | Object.values(Graphics.slime.animations).forEach(anim => {
84 | if (!this.anims.get(anim.key)) {
85 | this.anims.create({
86 | ...anim,
87 | frames: this.anims.generateFrameNumbers(
88 | Graphics.slime.name,
89 | anim.frames
90 | )
91 | });
92 | }
93 | });
94 |
95 | const map = new Map(worldTileWidth, worldTileHeight, this);
96 | this.tilemap = map.tilemap;
97 |
98 | this.fov = new FOVLayer(map);
99 |
100 | this.player = new Player(
101 | this.tilemap.tileToWorldX(map.startingX),
102 | this.tilemap.tileToWorldY(map.startingY),
103 | this
104 | );
105 |
106 | this.slimes = map.slimes;
107 | this.slimeGroup = this.physics.add.group(this.slimes.map(s => s.sprite));
108 |
109 | this.cameras.main.setRoundPixels(true);
110 | this.cameras.main.setZoom(3);
111 | this.cameras.main.setBounds(
112 | 0,
113 | 0,
114 | map.width * Graphics.environment.width,
115 | map.height * Graphics.environment.height
116 | );
117 | this.cameras.main.startFollow(this.player.sprite);
118 |
119 | this.physics.add.collider(this.player.sprite, map.wallLayer);
120 | this.physics.add.collider(this.slimeGroup, map.wallLayer);
121 |
122 | this.physics.add.collider(this.player.sprite, map.doorLayer);
123 | this.physics.add.collider(this.slimeGroup, map.doorLayer);
124 |
125 | // this.physics.add.overlap(
126 | // this.player.sprite,
127 | // this.slimeGroup,
128 | // this.slimePlayerCollide,
129 | // undefined,
130 | // this
131 | // );
132 | this.physics.add.collider(
133 | this.player.sprite,
134 | this.slimeGroup,
135 | undefined,
136 | this.slimePlayerCollide,
137 | this
138 | );
139 |
140 | // for (let slime of this.slimes) {
141 | // this.physics.add.collider(slime.sprite, map.wallLayer);
142 | // }
143 |
144 | this.input.keyboard.on("keydown_R", () => {
145 | this.scene.stop("InfoScene");
146 | this.scene.run("ReferenceScene");
147 | this.scene.sleep();
148 | });
149 |
150 | this.input.keyboard.on("keydown_Q", () => {
151 | this.physics.world.drawDebug = !this.physics.world.drawDebug;
152 | if (!this.physics.world.debugGraphic) {
153 | this.physics.world.createDebugGraphic();
154 | }
155 | this.physics.world.debugGraphic.clear();
156 | this.roomDebugGraphics!.setVisible(this.physics.world.drawDebug);
157 | });
158 |
159 | this.input.keyboard.on("keydown_F", () => {
160 | this.fov!.layer.setVisible(!this.fov!.layer.visible);
161 | });
162 |
163 | this.roomDebugGraphics = this.add.graphics({ x: 0, y: 0 });
164 | this.roomDebugGraphics.setVisible(false);
165 | this.roomDebugGraphics.lineStyle(2, 0xff5500, 0.5);
166 | for (let room of map.rooms) {
167 | this.roomDebugGraphics.strokeRect(
168 | this.tilemap!.tileToWorldX(room.x),
169 | this.tilemap!.tileToWorldY(room.y),
170 | this.tilemap!.tileToWorldX(room.width),
171 | this.tilemap!.tileToWorldY(room.height)
172 | );
173 | }
174 |
175 | this.scene.run("InfoScene");
176 | }
177 |
178 | update(time: number, delta: number) {
179 | this.player!.update(time);
180 |
181 | const camera = this.cameras.main;
182 |
183 | for (let slime of this.slimes) {
184 | slime.update(time);
185 | }
186 |
187 | const player = new Phaser.Math.Vector2({
188 | x: this.tilemap!.worldToTileX(this.player!.sprite.body.x),
189 | y: this.tilemap!.worldToTileY(this.player!.sprite.body.y)
190 | });
191 |
192 | const bounds = new Phaser.Geom.Rectangle(
193 | this.tilemap!.worldToTileX(camera.worldView.x) - 1,
194 | this.tilemap!.worldToTileY(camera.worldView.y) - 1,
195 | this.tilemap!.worldToTileX(camera.worldView.width) + 2,
196 | this.tilemap!.worldToTileX(camera.worldView.height) + 2
197 | );
198 |
199 | this.fov!.update(player, bounds, delta);
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/src/scenes/InfoScene.ts:
--------------------------------------------------------------------------------
1 | import Phaser from "phaser";
2 | import Fonts from "../assets/Fonts";
3 |
4 | export default class InfoScene extends Phaser.Scene {
5 | text?: Phaser.GameObjects.DynamicBitmapText;
6 | lastUpdate?: number;
7 |
8 | constructor() {
9 | super({ key: "InfoScene" });
10 | }
11 |
12 | preload(): void {
13 | this.load.bitmapFont("default", ...Fonts.default);
14 | }
15 |
16 | create(): void {
17 | this.text = this.add.dynamicBitmapText(25, 25, "default", "", 12);
18 | this.text.setAlpha(0.7);
19 | this.lastUpdate = 0;
20 | }
21 |
22 | update(time: number, _: number): void {
23 | if (time > this.lastUpdate! + 100) {
24 | this.text!.setText([
25 | "Dungeon Dash!",
26 | "",
27 | "Use arrow keys to walk around the map!",
28 | "Press space while moving to dash-attack!",
29 | "(debug: Q, tilesets: R)",
30 | "",
31 | "Credits & more information at",
32 | "https://github.com/mipearson/dungeondash",
33 | "",
34 | "FPS: " + Math.round(this.game.loop.actualFps)
35 | ]);
36 | this.lastUpdate = time;
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/scenes/ReferenceScene.ts:
--------------------------------------------------------------------------------
1 | import Phaser from "phaser";
2 | import Graphics from "../assets/Graphics";
3 | import Fonts from "../assets/Fonts";
4 |
5 | const tilesets = Object.values(Graphics);
6 |
7 | export default class ReferenceScene extends Phaser.Scene {
8 | index: number;
9 | group: Phaser.GameObjects.Group | null;
10 | map: Phaser.Tilemaps.Tilemap | null;
11 | title: Phaser.GameObjects.DynamicBitmapText | null;
12 |
13 | constructor() {
14 | super("ReferenceScene");
15 | this.index = 0;
16 | this.group = null;
17 | this.map = null;
18 | this.title = null;
19 | }
20 |
21 | preload(): void {
22 | tilesets.forEach(t => this.load.image(t.name, t.file));
23 | this.load.bitmapFont("default", ...Fonts.default);
24 | }
25 |
26 | create(): void {
27 | this.title = this.add.dynamicBitmapText(20, 10, "default", "", 12);
28 | this.previewTileset();
29 | this.input.keyboard.on("keydown_N", () => {
30 | this.index += 1;
31 | if (this.index >= tilesets.length) {
32 | this.index = 0;
33 | }
34 | this.reset();
35 | this.previewTileset();
36 | });
37 |
38 | this.input.keyboard.on("keydown_R", () => {
39 | this.scene.wake("DungeonScene");
40 | this.scene.stop();
41 | });
42 | }
43 |
44 | reset() {
45 | this.group && this.group.clear(true, true);
46 | this.map && this.map.destroy();
47 | this.group = null;
48 | this.map = null;
49 | }
50 |
51 | previewTileset() {
52 | this.group = this.add.group();
53 | const tileset = tilesets[this.index];
54 |
55 | this.map = this.make.tilemap({
56 | tileWidth: tileset.width,
57 | tileHeight: tileset.height
58 | });
59 | const tiles = this.map.addTilesetImage(tileset.name);
60 | const layer = this.map.createBlankDynamicLayer(
61 | "preview",
62 | tiles,
63 | 30,
64 | 40,
65 | tiles.columns,
66 | tiles.rows
67 | );
68 | layer.setScale(tileset.width > 32 ? 2 : 3);
69 |
70 | const grid = this.add
71 | .grid(
72 | layer.x + layer.displayWidth / 2,
73 | layer.y + layer.displayHeight / 2,
74 | layer.displayWidth + 16,
75 | layer.displayHeight + 16,
76 | 8,
77 | 8,
78 | 0x222222
79 | )
80 | .setAltFillStyle(0x2a2a2a)
81 | .setOutlineStyle();
82 | layer.setDepth(5);
83 | this.group.add(grid);
84 |
85 | for (let y = 0; y < tiles.rows; y++) {
86 | for (let x = 0; x < tiles.columns; x++) {
87 | const idx = y * tiles.columns + x;
88 | const text = this.add.bitmapText(
89 | this.map.tileToWorldX(x),
90 | this.map.tileToWorldY(y),
91 | "default",
92 | idx.toString(16),
93 | 6
94 | );
95 | text.setDepth(10);
96 | this.group.add(text);
97 | layer.putTileAt(idx, x, y);
98 | }
99 | }
100 |
101 | this.group.add(layer);
102 |
103 | this.title!.setText(
104 | `'${tileset.name}' (${this.index + 1} of ${
105 | tilesets.length
106 | }) ['n' for next]`
107 | );
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2017",
4 | "module": "commonjs",
5 | "rootDir": "./src",
6 |
7 | "strict": true,
8 | "noImplicitAny": true,
9 | "noImplicitThis": true,
10 | "noUnusedLocals": true /* Report errors on unused locals. */,
11 | "noUnusedParameters": true,
12 | "noImplicitReturns": true,
13 | "noFallthroughCasesInSwitch": true,
14 |
15 | "esModuleInterop": true
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/typings/assets.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.png";
2 | declare module "*.fnt";
3 |
--------------------------------------------------------------------------------
/typings/phaser-plugin-scene-watcher.d.ts:
--------------------------------------------------------------------------------
1 | declare module "phaser-plugin-scene-watcher";
2 |
--------------------------------------------------------------------------------