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