Dungeon Generator

//  Toggle this to disable the room hiding / layer scale, so you can see the extent of the map easily!
const debug = false;

// Tile index mapping to make the code more readable
const TILES = {
    TOP_LEFT_WALL: 3,
    TOP_RIGHT_WALL: 4,
    BOTTOM_RIGHT_WALL: 23,
    BOTTOM_LEFT_WALL: 22,
    TOP_WALL: [
        { index: 39, weight: 4 },
        { index: 57, weight: 1 },
        { index: 58, weight: 1 },
        { index: 59, weight: 1 }
    ],
    LEFT_WALL: [
        { index: 21, weight: 4 },
        { index: 76, weight: 1 },
        { index: 95, weight: 1 },
        { index: 114, weight: 1 }
    ],
    RIGHT_WALL: [
        { index: 19, weight: 4 },
        { index: 77, weight: 1 },
        { index: 96, weight: 1 },
        { index: 115, weight: 1 }
    ],
    BOTTOM_WALL: [
        { index: 1, weight: 4 },
        { index: 78, weight: 1 },
        { index: 79, weight: 1 },
        { index: 80, weight: 1 }
    ],
    FLOOR: [
        { index: 6, weight: 20 },
        { index: 7, weight: 1 },
        { index: 8, weight: 1 },
        { index: 26, weight: 1 },
    ]
};

class Example extends Phaser.Scene
{
    activeRoom;
    dungeon;
    map;
    player;
    cursors;
    cam;
    layer;
    lastMoveTime = 0;

    preload ()
    {
        this.load.setBaseURL('https://cdn.phaserfiles.com/v355');
        // Credits! Michele "Buch" Bucelli (tilset artist) & Abram Connelly (tileset sponser)
        // https://opengameart.org/content/top-down-dungeon-tileset
        this.load.image('tiles', 'assets/tilemaps/tiles/buch-dungeon-tileset-extruded.png');
    }

    create ()
    {
        // Note: Dungeon is not a Phaser element - it's from the custom script embedded at the bottom :)
        // It generates a simple set of connected rectangular rooms that then we can turn into a tilemap

        //  2,500 tile test
        // dungeon = new Dungeon({
        //     width: 50,
        //     height: 50,
        //     rooms: {
        //         width: { min: 7, max: 15, onlyOdd: true },
        //         height: { min: 7, max: 15, onlyOdd: true }
        //     }
        // });

        //  40,000 tile test
        this.dungeon = new Dungeon({
            width: 200,
            height: 200,
            rooms: {
                width: { min: 7, max: 20, onlyOdd: true },
                height: { min: 7, max: 20, onlyOdd: true }
            }
        });

        //  250,000 tile test!
        // dungeon = new Dungeon({
        //     width: 500,
        //     height: 500,
        //     rooms: {
        //         width: { min: 7, max: 20, onlyOdd: true },
        //         height: { min: 7, max: 20, onlyOdd: true }
        //     }
        // });

        //  1,000,000 tile test! - Warning, takes a few seconds to generate the dungeon :)
        // dungeon = new Dungeon({
        //     width: 1000,
        //     height: 1000,
        //     rooms: {
        //         width: { min: 7, max: 20, onlyOdd: true },
        //         height: { min: 7, max: 20, onlyOdd: true }
        //     }
        // });

        // Creating a blank tilemap with dimensions matching the dungeon
        this.map = this.make.tilemap({ tileWidth: 16, tileHeight: 16, width: this.dungeon.width, height: this.dungeon.height });

        // addTilesetImage: function (tilesetName, key, tileWidth, tileHeight, tileMargin, tileSpacing, gid)

        var tileset = this.map.addTilesetImage('tiles', 'tiles', 16, 16, 1, 2);

        this.layer = this.map.createBlankLayer('Layer 1', tileset);

        if (!debug)
        {
            this.layer.setScale(3);
        }

        // Fill with black tiles
        this.layer.fill(20);

        // Use the array of rooms generated to place tiles in the map
        this.dungeon.rooms.forEach(function (room) {
            var x = room.x;
            var y = room.y;
            var w = room.width;
            var h = room.height;
            var cx = Math.floor(x + w / 2);
            var cy = Math.floor(y + h / 2);
            var left = x;
            var right = x + (w - 1);
            var top = y;
            var bottom = y + (h - 1);

            // Fill the floor with mostly clean tiles, but occasionally place a dirty tile
            // See "Weighted Randomize" example for more information on how to use weightedRandomize.
            this.map.weightedRandomize(TILES.FLOOR, x, y, w, h);

            // Place the room corners tiles
            this.map.putTileAt(TILES.TOP_LEFT_WALL, left, top);
            this.map.putTileAt(TILES.TOP_RIGHT_WALL, right, top);
            this.map.putTileAt(TILES.BOTTOM_RIGHT_WALL, right, bottom);
            this.map.putTileAt(TILES.BOTTOM_LEFT_WALL, left, bottom);

            // Fill the walls with mostly clean tiles, but occasionally place a dirty tile
            this.map.weightedRandomize(TILES.TOP_WALL, left + 1, top, w - 2, 1);
            this.map.weightedRandomize(TILES.BOTTOM_WALL, left + 1, bottom, w - 2, 1);
            this.map.weightedRandomize(TILES.LEFT_WALL, left, top + 1, 1, h - 2);
            this.map.weightedRandomize(TILES.RIGHT_WALL, right, top + 1, 1, h - 2);

            // Dungeons have rooms that are connected with doors. Each door has an x & y relative to the rooms location
            var doors = room.getDoorLocations();

            for (var i = 0; i < doors.length; i++)
            {
                this.map.putTileAt(6, x + doors[i].x, y + doors[i].y);
            }

            // Place some random stuff in rooms occasionally
            var rand = Math.random();
            if (rand <= 0.25)
            {
                this.layer.putTileAt(166, cx, cy); // Chest
            }
            else if (rand <= 0.3)
            {
                this.layer.putTileAt(81, cx, cy); // Stairs
            }
            else if (rand <= 0.4)
            {
                this.layer.putTileAt(167, cx, cy); // Trap door
            }
            else if (rand <= 0.6)
            {
                if (room.height >= 9)
                {
                    // We have room for 4 towers
                    this.layer.putTilesAt([
                        [ 186 ],
                        [ 205 ]
                    ], cx - 1, cy + 1);

                    this.layer.putTilesAt([
                        [ 186 ],
                        [ 205 ]
                    ], cx + 1, cy + 1);

                    this.layer.putTilesAt([
                        [ 186 ],
                        [ 205 ]
                    ], cx - 1, cy - 2);

                    this.layer.putTilesAt([
                        [ 186 ],
                        [ 205 ]
                    ], cx + 1, cy - 2);
                }
                else
                {
                    this.layer.putTilesAt([
                        [ 186 ],
                        [ 205 ]
                    ], cx - 1, cy - 1);

                    this.layer.putTilesAt([
                        [ 186 ],
                        [ 205 ]
                    ], cx + 1, cy - 1);
                }
            }
        }, this);

        // Not exactly correct for the tileset since there are more possible floor tiles, but this will
        // do for the example.
        this.layer.setCollisionByExclusion([ 6, 7, 8, 26 ]);

        // Hide all the rooms
        if (!debug)
        {
            this.layer.forEachTile(function (tile) { tile.alpha = 0; });
        }

        // Place the player in the first room
        var playerRoom = this.dungeon.rooms[0];

        this.player = this.add.graphics({ fillStyle: { color: 0xedca40, alpha: 1 } }).fillRect(0, 0, this.map.tileWidth * this.layer.scaleX, this.map.tileHeight * this.layer.scaleY);

        this.player.x = this.map.tileToWorldX(playerRoom.x + 1);
        this.player.y = this.map.tileToWorldY(playerRoom.y + 1);

        if (!debug)
        {
            this.setRoomAlpha(playerRoom, 1); // Make the starting room visible
        }

        // Scroll to the player
        this.cam = this.cameras.main;

        this.cam.setBounds(0, 0, this.layer.width * this.layer.scaleX, this.layer.height * this.layer.scaleY);
        this.cam.scrollX = this.player.x - this.cam.width * 0.5;
        this.cam.scrollY = this.player.y - this.cam.height * 0.5;

        this.cursors = this.input.keyboard.createCursorKeys();

        var help = this.add.text(16, 16, 'Arrows keys to move', {
            fontSize: '18px',
            padding: { x: 10, y: 5 },
            backgroundColor: '#ffffff',
            fill: '#000000'
        });

        help.setScrollFactor(0);

        var gui = new dat.GUI();

        gui.addFolder('Camera');
        gui.add(this.cam, 'scrollX').listen();
        gui.add(this.cam, 'scrollY').listen();
        gui.add(this.cam, 'zoom', 0.1, 4).step(0.1);
        gui.add(this.cam, 'rotation').step(0.01);
        gui.add(this.layer, 'skipCull').listen();
        gui.add(this.layer, 'cullPaddingX').step(1);
        gui.add(this.layer, 'cullPaddingY').step(1);
        gui.add(this.layer, 'tilesDrawn').listen();
        gui.add(this.layer, 'tilesTotal').listen();
    }

    update (time, delta)
    {
        this.updatePlayerMovement(time);

        var playerTileX = this.map.worldToTileX(this.player.x);
        var playerTileY = this.map.worldToTileY(this.player.y);

        // Another helper method from the dungeon - dungeon XY (in tiles) -> room
        var room = this.dungeon.getRoomAt(playerTileX, playerTileY);

        // If the player has entered a new room, make it visible and dim the last room
        if (room && this.activeRoom && this.activeRoom !== room)
        {
            if (!debug)
            {
                this.setRoomAlpha(room, 1);
                this.setRoomAlpha(this.activeRoom, 0.5);
            }
        }

        this.activeRoom = room;

        // Smooth follow the player
        var smoothFactor = 0.9;

        this.cam.scrollX = smoothFactor * this.cam.scrollX + (1 - smoothFactor) * (this.player.x - this.cam.width * 0.5);
        this.cam.scrollY = smoothFactor * this.cam.scrollY + (1 - smoothFactor) * (this.player.y - this.cam.height * 0.5);
    }

    // Helpers functions
    setRoomAlpha(room, alpha)
    {
        this.map.forEachTile(function (tile) {
            tile.alpha = alpha;
        }, this, room.x, room.y, room.width, room.height)
    }

    isTileOpenAt (worldX, worldY)
    {
        // nonNull = true, don't return null for empty tiles. This means null will be returned only for
        // tiles outside of the bounds of the map.
        var tile = this.map.getTileAtWorldXY(worldX, worldY, true);

        if (tile && !tile.collides)
        {
            return true;
        }
        else
        {
            return false;
        }
    }

    updatePlayerMovement (time)
    {
        var tw = this.map.tileWidth * this.layer.scaleX;
        var th = this.map.tileHeight * this.layer.scaleY;
        var repeatMoveDelay = 100;

        if (time > this.lastMoveTime + repeatMoveDelay) {
            if (this.cursors.down.isDown)
            {
                if (this.isTileOpenAt(this.player.x, this.player.y + th))
                {
                    this.player.y += th;
                    this.lastMoveTime = time;
                }
            }
            else if (this.cursors.up.isDown)
            {
                if (this.isTileOpenAt(this.player.x, this.player.y - th))
                {
                    this.player.y -= th;
                    this.lastMoveTime = time;
                }
            }

            if (this.cursors.left.isDown)
            {
                if (this.isTileOpenAt(this.player.x - tw, this.player.y))
                {
                    this.player.x -= tw;
                    this.lastMoveTime = time;
                }
            }
            else if (this.cursors.right.isDown)
            {
                if (this.isTileOpenAt(this.player.x + tw, this.player.y))
                {
                    this.player.x += tw;
                    this.lastMoveTime = time;
                }
            }
        }
    }


}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: '#2a2a55',
    parent: 'phaser-example',
    pixelArt: true,
    roundPixels: false,
    scene: Example
};

const game = new Phaser.Game(config);

// Minified & modified dungeon generator at mikewesthad/dungeon (fork of nickgravelyn/dungeon)
!function(t,o){"object"==typeof exports&&"object"==typeof module?module.exports=o():"function"==typeof define&&define.amd?define([],o):"object"==typeof exports?exports.Dungeon=o():t.Dungeon=o()}("undefined"!=typeof self?self:this,function(){return function(t){function o(r){if(e[r])return e[r].exports;var i=e[r]={i:r,l:!1,exports:{}};return t[r].call(i.exports,i,i.exports,o),i.l=!0,i.exports}var e={};return o.m=t,o.c=e,o.d=function(t,e,r){o.o(t,e)||Object.defineProperty(t,e,{configurable:!1,enumerable:!0,get:r})},o.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return o.d(e,"a",e),e},o.o=function(t,o){return Object.prototype.hasOwnProperty.call(t,o)},o.p="",o(o.s=1)}([function(t,o,e){"use strict";Object.defineProperty(o,"__esModule",{value:!0});var r={EMPTY:0,WALL:1,FLOOR:2,DOOR:3};o.default=r},function(t,o,e){"use strict";t.exports=e(2).default},function(t,o,e){"use strict";function r(t){return t&&t.__esModule?t:{default:t}}function i(t,o){if(!(t instanceof o))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(o,"__esModule",{value:!0});var n=function(){function t(t,o){var e=[],r=!0,i=!1,n=void 0;try{for(var h,a=t[Symbol.iterator]();!(r=(h=a.next()).done)&&(e.push(h.value),!o||e.length!==o);r=!0);}catch(t){i=!0,n=t}finally{try{!r&&a.return&&a.return()}finally{if(i)throw n}}return e}return function(o,e){if(Array.isArray(o))return o;if(Symbol.iterator in Object(o))return t(o,e);throw new TypeError("Invalid attempt to destructure non-iterable instance")}}(),h=function(){function t(t,o){for(var e=0;e0&&void 0!==arguments[0]?arguments[0]:{};i(this,t);var e=o.rooms||{};e.width=Object.assign({},m.rooms.width,e.width),e.height=Object.assign({},m.rooms.height,e.height),e.maxArea=e.maxArea||m.rooms.maxArea,e.maxRooms=e.maxRooms||m.rooms.maxRooms,e.width.min<3&&(e.width.min=3),e.height.min<3&&(e.height.min=3),e.width.max0;)this.generateRoom(),r-=1;for(var i=0;i=this.width||o>=this.height?null:this.roomGrid[o][t][0]}},{key:"getMappedTiles",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return t=Object.assign({},{empty:0,wall:1,floor:2,door:3},t),this.tiles.map(function(o){return o.map(function(o){return o===l.default.EMPTY?t.empty:o===l.default.WALL?t.wall:o===l.default.FLOOR?t.floor:o===l.default.DOOR?t.door:void 0})})}},{key:"addRoom",value:function(t){if(!this.canFitRoom(t))return!1;this.rooms.push(t);for(var o=t.top;o<=t.bottom;o++)for(var e=t.left;e<=t.right;e++)this.roomGrid[o][e].push(t);return!0}},{key:"canFitRoom",value:function(t){if(t.x<0||t.x+t.width>this.width-1)return!1;if(t.y<0||t.y+t.height>this.height-1)return!1;for(var o=0;or.maxArea);return new u.default(t,o)}},{key:"generateRoom",value:function(){for(var t=this.createRandomRoom(),o=150;o>0;){var e=this.findRoomAttachment(t);if(t.setPosition(e.x,e.y),this.addRoom(t)){var r=this.findNewDoorLocation(t,e.target),i=n(r,2),h=i[0],a=i[1];this.addDoor(h),this.addDoor(a);break}o-=1}}},{key:"getTiles",value:function(){for(var t=Array(this.height),o=0;o0&&a0&&s2&&void 0!==arguments[2]?arguments[2]:{},r=e.onlyOdd,h=void 0!==r&&r,a=e.onlyEven,s=void 0!==a&&a;return h?n(t,o):s?i(t,o):Math.floor(Math.random()*(o-t+1)+t)}function i(t,o){t%2!=0&&tt&&o--;var e=(o-t)/2;return 2*Math.floor(Math.random()*(e+1))+t}function n(t,o){t%2==0&&t++,o%2==0&&o--;var e=(o-t)/2;return 2*Math.floor(Math.random()*(e+1))+t}function h(t){return t[r(0,t.length-1)]}Object.defineProperty(o,"__esModule",{value:!0}),o.randomInteger=r,o.randomEvenInteger=i,o.randomOddInteger=n,o.randomPick=h},function(t,o,e){"use strict";function r(t,o){if(!(t instanceof o))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(o,"__esModule",{value:!0});var i=function(){function t(t,o){for(var e=0;et.right)&&(!(this.bottomt.bottom)))}},{key:"isConnectedTo",value:function(t){for(var o=this.getDoorLocations(),e=0;et.width-1||r.y<0||r.y>t.height-1)&&t.tiles[r.y][r.x]==h.default.DOOR)return!0}return!1}}]),t}();o.default=a},function(t,o,e){"use strict";function r(t){var o=t.roomGrid.map(function(t){return t.map(function(t){return(""+t.length).padStart(2)})});console.log(o.map(function(t){return t.join(" ")}).join("\n"))}function i(t){var o,e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};e=Object.assign({},{empty:" ",emptyColor:"rgb(0, 0, 0)",wall:"#",wallColor:"rgb(255, 0, 0)",floor:"_",floorColor:"rgb(210, 210, 210)",door:".",doorColor:"rgb(0, 0, 255)",fontSize:"15px"},e);for(var r="",i=[],n=0;n