• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[vJASS] Dungeon Generation - problems

Status
Not open for further replies.
Level 5
Joined
Oct 14, 2010
Messages
100
So I've attempted creating a random dungeon generator that should be somewhat advanced. It's in early stages so it doesn't look good.

But I've encountered 2 problems that I can't seem to fix:

Problem 1: Destructible positioning
In the code I use a 2D virtual grid where each node can either be a room or solid wall. For example:
Code:
...##..     # = room
..###..     . = solid wall
....##.

In-game, every virtual node is 512 units apart from the next node. So the example grid would result in a 3584 x 1536 units large dungeon, in-game.

However, when creating destructibles (for walls, gates, etc.) it sometimes glitches and isn't precisely 512 units apart.
attachment.php

The purple "ceiling" in this screenshot should be more to the right so there's no hole. It works for most of the destructibles, but some are messed up.

Problem 2: Random gates
Again, a problem that leaves me clueless.
attachment.php

As you can see on the screenshot, doors should only lead to other rooms, but for some reason it randomly creates doors leading to a solid wall.

Demo map attached to this post
JASS:
library DG requires Assertion, Array, StringUtils, Stack

// ***************************************
// *
// * Configuration Settings
// *
// ***************************************

globals
    // Width & height of the map (specified in nodes)
    public constant integer MAX_WIDTH = 32
    public constant integer MAX_HEIGHT = 32
    public constant real NODE_SIZE = 512.0
    
    // Pattern symbols
    public constant string PATTERN_SYMBOL_ROOM  = "#"
    public constant string PATTERN_SYMBOL_NONE  = "."
    public constant string PATTERN_SYMBOL_LAYER = "|"
    
    // Options
    public constant real FEATURES_PER_NODE = 0.07
    public constant boolean SOFT_LOAD = true
    public constant real EPSILON = -0.15
endglobals

// ***************************************
// *
// * Layout
// *
// ***************************************

private struct LayoutHelper extends array
    readonly static integer array data[MAX_WIDTH][MAX_HEIGHT]
    
    public method operator [] takes integer y returns integer
        debug call assert(integer(this) >= 0 and integer(this) < DG_Layout.width and y >= 0 and y < DG_Layout.height, "DG::LayoutHelper::[] - coordinates not within bounds")
        return data[this][y]
    endmethod
    
    public method operator []= takes integer y, integer value returns nothing
        debug call assert(integer(this) >= 0 and integer(this) < DG_Layout.width and y >= 0 and y < DG_Layout.height, "DG::LayoutHelper::[]= - coordinates not within bounds")
        if (value == 0) then
            call DG_Layout.roomNodes.remove(DG_Position.create(this, y))
        elseif (data[this][y] == 0) then
            call DG_Layout.roomNodes.add(DG_Position.create(this, y))
        endif
        set data[this][y] = value
    endmethod
endstruct

public struct Layout extends array
    private static integer layoutWidth
    private static integer layoutHeight
    readonly static Stack roomNodes = 0
    
    public static method operator [] takes integer x returns LayoutHelper
        return x
    endmethod
    
    public static method operator width takes nothing returns integer
        return layoutWidth
    endmethod
    
    public static method operator height takes nothing returns integer
        return layoutHeight
    endmethod
    
    public static method operator width= takes integer value returns nothing
        call assert(value > 0 and value <= MAX_WIDTH, "DG::Layout::width= - invalid width")
        set layoutWidth = value
    endmethod
    
    public static method operator height= takes integer value returns nothing
        call assert(value > 0 and value <= MAX_HEIGHT, "DG::Layout::height= - invalid height")
        set layoutHeight = value
    endmethod
    
    public static method reset takes integer value returns nothing
        local integer x = 0
        local integer y
        
        // Reset LayoutGrid
        loop
            exitwhen x >= layoutWidth
            set y = 0
            loop
                exitwhen y >= layoutHeight
                set Layout[x][y] = value
                set y = y + 1
            endloop
            set x = x + 1
        endloop
        
        // Reset list of room nodes
        if (roomNodes != 0) then
            call roomNodes.destroy()
        endif
        set roomNodes = Stack.create()
    endmethod
    
    public static method getRandomWall takes nothing returns DG_Wall
        local DG_Position position = 0
        local DG_Position adjacent = 0
        local DG_Direction facing = 0
        
        loop
            set position = DG_Position(roomNodes.random())
            set facing = DG_Direction.random()
            set adjacent = position.adjacent(facing)
            if (Layout[adjacent.x][adjacent.y] == 0 and position != adjacent) then
                return DG_Wall.create(position, facing)
            endif
        endloop
        
        return 0
    endmethod
    
    public static method toString takes nothing returns string
        local string str = "|cff0000ff"
        local integer x = 0
        local integer y = 0
        local boolean lastRoom = false
        
        loop
            exitwhen x >= layoutWidth
            set y = 0
            loop
                exitwhen y >= layoutHeight
                if (Layout[x][y] > 0) then
                    if (lastRoom) then
                        set str = str + "#"
                    else
                        set str = str + "|r|cffffffff#"
                        set lastRoom = true
                    endif
                elseif (lastRoom) then
                    set str = str + "|r|cff0000ff#"
                    set lastRoom = false
                else
                    set str = str + "#"
                endif
                set y = y + 1
            endloop
            set str = str + "|r\n|cff0000ff"
            set lastRoom = false
            set x = x + 1
        endloop
        
        set str = str + "|r"
        return str
    endmethod
endstruct

// ***************************************
// *
// * Direction (NORTH, EAST, SOUTH, WEST)
// *
// ***************************************

public struct Direction extends array
    public static Direction NORTH   = 0
    public static Direction EAST    = 1
    public static Direction SOUTH   = 2
    public static Direction WEST    = 3
    
    private static method create takes nothing returns DG_Direction
        call assert(false, "DG::Direction::create - should not be called!")
        return 0
    endmethod
    
    public method rotate takes integer num90 returns DG_Direction
        local integer direction = this + num90/90
        return direction - (direction / 4) * 4
    endmethod
    
    public method angle takes nothing returns real
        return -90.0 * this
    endmethod
    
    public static method random takes nothing returns DG_Direction
        return GetRandomInt(0, 3)
    endmethod
    
    public method toString takes nothing returns string
        if (this == NORTH) then
            return "north"
        elseif (this == EAST) then
            return "east"
        elseif (this == SOUTH) then
            return "south"
        else
            return "west"
        endif
    endmethod
endstruct

// ***************************************
// *
// * Position
// *
// ***************************************

public struct Position extends array

    public static method checkBoundariesX takes integer x returns integer
        if (x < 0) then
            return 0
        elseif (x >= Layout.width) then
            return Layout.width - 1
        endif
        return x
    endmethod
    
    public static method checkBoundariesY takes integer y returns integer
        if (y < 0) then
            return 0
        elseif (y >= Layout.height) then
            return Layout.height - 1
        endif
        return y
    endmethod

    public static method create takes integer x, integer y returns Position
        return checkBoundariesX(x) * 0xFFFF + checkBoundariesY(y)
    endmethod
    
    public method operator x takes nothing returns integer
        return this / 0xFFFF
    endmethod
    
    public method operator y takes nothing returns integer
        return this - (this.x * 0xFFFF)
    endmethod
    
    public method operator x= takes integer value returns Position
        return checkBoundariesX(value) * 0xFFFF + this.y
    endmethod
    
    public method operator y= takes integer value returns Position
        return this.x * 0xFFFF + checkBoundariesY(value)
    endmethod
    
    public method operator data takes nothing returns integer
        return Layout[this.x][this.y]
    endmethod
    
    public method operator data= takes integer value returns nothing
        set Layout[this.x][this.y] = value
    endmethod
    
    public method toString takes nothing returns string
        return "(" + I2S(this.x) + "," + I2S(this.y) + ")"
    endmethod
    
    public method add takes Position other returns Position
        return Position.create(this.x + other.x, this.y + other.y)
    endmethod
    
    public method sub takes Position other returns Position
        return Position.create(this.x - other.x, this.y - other.y)
    endmethod
    
    public method operator north takes nothing returns Position
        return this.x * 0xFFFF + checkBoundariesY(this.y + 1)
    endmethod
    
    public method operator east takes nothing returns Position
        return checkBoundariesX(this.x + 1) * 0xFFFF + this.y
    endmethod
    
    public method operator south takes nothing returns Position
        return this.x * 0xFFFF + checkBoundariesY(this.y - 1)
    endmethod
    
    public method operator west takes nothing returns Position
        return checkBoundariesX(this.x - 1) * 0xFFFF + this.y
    endmethod
    
    public method adjacent takes Direction direction returns Position
        if (direction == Direction.NORTH) then
            return this.north
        elseif (direction == Direction.EAST) then
            return this.east
        elseif (direction == Direction.SOUTH) then
            return this.south
        elseif (direction == Direction.WEST) then
            return this.west
        else
            return 0
        endif
    endmethod
    
    public static method random takes integer width, integer height returns Position
        return GetRandomInt(0, width-1) * 0xFFFF + GetRandomInt(0, height-1)
    endmethod
endstruct

// ***************************************
// *
// * Wall
// *
// ***************************************

public struct Wall
    readonly Position position
    readonly Direction direction
    
    // A wall is a position and a direction
    // i.e. at node[x][y], the wall facing EAST
    // opposing wall is node[x+1][y] facing WEST
    public static method create takes Position position, Direction direction returns Wall
        local Wall this = Wall.allocate()
        set this.position = position
        set this.direction = direction
        return this
    endmethod
    
    // Returns the other side of the wall (returns 0 if the wall is a border wall)
    public method opposite takes nothing returns Wall
        local Position p = this.position.adjacent(this.direction)
        
        // Check if wall has opposite wall
        if (p == this.position) then
            return 0
        endif
        
        return Wall.create(p, this.direction.rotate(180))
    endmethod
    
    public method opposes takes Wall other returns boolean
        return this.position.adjacent(this.direction) == other.position and this.direction == other.direction.rotate(180)
    endmethod
endstruct

// ***************************************
// *
// * Pattern
// *
// ***************************************

// TO DO: pattern generator: simply create all 5x5 strings randomly consisting of . and #
public struct Pattern
    private string data
    readonly integer width
    readonly integer height
    readonly integer size
    
    public static method create takes string data returns Pattern
        local Pattern this = Pattern.allocate()
        set this.width = StringFind(data, PATTERN_SYMBOL_LAYER, 0)
        set this.height = StringCount(data, PATTERN_SYMBOL_LAYER) + 1
        set this.size = StringCount(data, PATTERN_SYMBOL_ROOM)
        set this.data = StringReplace(data, PATTERN_SYMBOL_LAYER, "")
        return this
    endmethod
    
    public method toString takes nothing returns string
        local string str = ""
        local integer x = 0
        local integer y = 0
        
        loop
            exitwhen x >= this.width
            set y = 0
            loop
                exitwhen y >= this.height
                if (this.at(Position.create(x, y)) == PATTERN_SYMBOL_ROOM) then
                    set str = str + "|cffffffff#|r"
                else
                    set str = str + "|cff0000ff#|r"
                endif
                set y = y + 1
            endloop
            set str = str + "\n"
            set x = x + 1
        endloop
        
        return str
    endmethod
    
    private method onDestroy takes nothing returns nothing
        call BJDebugMsg("DG::Pattern::onDestroy - not supposed to be destroyed")
    endmethod
    
    public method at takes Position pos returns string
        return StringCharAt(this.data, pos.x * this.height + pos.y)
    endmethod
    
    // Return the position of a node in the pattern with a wall facing in the specified direction.
    // For example: stars * are nodes with a wall facing east (random node is returned)
    // ###         -->      ##*
    // ##.  Direction EAST  #*.
    public method randomWallFacing takes Direction direction returns Position
        local Position result = 0
        local Position adjacent = 0
        
        // Implementation: keep picking random position until it's a correct node
        // Alternative: select all wall nodes, pick random. But that's not what this implementation is currently doing...
        
        loop
            set result = Position.random(this.width, this.height)
            set adjacent = result.adjacent(direction)
            
            if (this.at(result) == PATTERN_SYMBOL_ROOM) then
                
                // check if adjacent is out of boundaries (i.e. does not lie within the pattern
                exitwhen adjacent == result or adjacent.x >= this.width or adjacent.y >= this.height
                
                // check if adjacent node is SYMBOL_NONE
                exitwhen this.at(adjacent) == PATTERN_SYMBOL_NONE
            endif
            
            // else, keep picking random position
        endloop
        
        return result
    endmethod
    
    // Scans the Layout for collisions
    private method checkLayout takes Position position, integer id returns boolean
        local Position posX = position
        local Position temp = position
        local integer x = 0
        local integer y = 0
        
        loop
            exitwhen x >= this.width
            set y = 0
            set position = posX
            loop
                exitwhen y >= this.height
                
                // Check Layout[x][y]
                if (this.at(Position.create(x, y)) == PATTERN_SYMBOL_ROOM) then
                    if (id != 0) then
                        set Layout[position.x][position.y] = id
                    elseif (Layout[position.x][position.y] != 0) then
                        return false
                    endif
                endif
                
                // Check if adjacent node is within bounds
                set temp = position.adjacent(Direction.NORTH)
                if (temp == position) then
                    return false
                endif
                set position = temp
                
                set y = y + 1
            endloop
            
            // Check if adjacent node is within bounds
            set temp = posX.adjacent(Direction.EAST)
            if (temp == posX) then
                return false
            endif
            set posX = temp
            
            set x = x + 1
        endloop
        
        return true
    endmethod
    
    public method scan takes Position position returns boolean
        return this.checkLayout(position, 0)
    endmethod
    
    public method register takes Position position, integer id returns nothing
        call this.checkLayout(position, id)
    endmethod

endstruct

// ***************************************
// *
// * Room
// *
// ***************************************

public struct Room extends array
    public integer height
    public string class
    public DG_Pattern pattern
endstruct

// ***************************************
// *
// * Destructible
// *
// ***************************************

public interface Object
    public method instance takes real x, real y, real z, real angle returns nothing
endinterface

public struct Destructible extends Object
    private static integer instances = 0
    private static integer maxInstances = 100
    
    private integer id
    private real z
    private real distance
    private real angle
    private real face
    private real minScale
    private real maxScale
    private integer maxVariation
    
    private static method onInit takes nothing returns nothing
        static if (SOFT_LOAD) then
            set maxInstances = 3
        endif
    endmethod
    
    public static method create takes integer id, real x, real y, real z, real face, real minScale, real maxScale, integer maxVariation returns Destructible
        local Destructible this = Destructible.allocate()
        set this.id = id
        set this.distance = SquareRoot(x*x + y*y)
        set this.angle = Atan2(y, x)
        set this.z = z
        set this.face = face
        set this.minScale = minScale
        set this.maxScale = maxScale
        set this.maxVariation = maxVariation
        return this
    endmethod
    
    // Angle in radians
    public method instance takes real x, real y, real z, real angle returns nothing
        local real a = angle * bj_DEGTORAD + this.angle
        local real rx = x + this.distance * Cos(a) + DG_EPSILON
        local real ry = y + this.distance * Sin(a) + DG_EPSILON
        //call BJDebugMsg("Angle: " + R2S(angle))
        //call BJDebugMsg("Position: " + R2S(rx) + "," + R2S(ry))
        // TO DO: register destructible (to be removed) ?
        call CreateDestructableZ(this.id, rx, ry, this.z + z, angle + this.face, GetRandomReal(this.minScale, this.maxScale), GetRandomInt(0, this.maxVariation - 1))
        set instances = instances + 1
        if (instances == maxInstances) then
            set instances = 0
            call TriggerSleepAction(0)
        endif
    endmethod
    
endstruct

// ***************************************
// *
// * Doodad
// *
// ***************************************

public struct Doodad extends Object
    readonly DG_Setting setting
    readonly string class
    readonly Array destructibles

    public static method create takes Destructible d returns Doodad
        local Doodad this = Doodad.allocate()
        set this.setting = setting
        set this.class = class
        set this.destructibles = Array.create()
        call this.destructibles.push(d)
        return this
    endmethod
    
    public method add takes Destructible d returns Doodad
        call this.destructibles.push(d)
        return this
    endmethod
    
    public method instance takes real x, real y, real z, real angle returns nothing
        local integer i = 0
        
        loop
            exitwhen i >= this.destructibles
            call Destructible(this.destructibles[i]).instance(x, y, z, angle)
            set i = i + 1
        endloop
    endmethod
        
endstruct

// ***************************************
// *
// * Setting
// *
// ***************************************

private struct SettingDoodadHelper extends array
    // I probably want to set wall doodads under these categories as well (but not add them to classNames so randomClassName() still works)
    private Table doodads
    private Table classNames
    private integer lastClassName
    
    public static method create takes DG_Setting setting returns SettingDoodadHelper
        local SettingDoodadHelper this = setting
        set this.doodads = Table.create()
        set this.doodads[StringHash("wall")] = Array.create()
        set this.doodads[StringHash("wall-corner")] = Array.create()
        set this.doodads[StringHash("wall-floor")] = Array.create()
        set this.doodads[StringHash("wall-stairs")] = Array.create()
        set this.doodads[StringHash("wall-gate")] = Array.create()
        set this.classNames = Table.create()
        set this.lastClassName = 0
        return this
    endmethod
    
    public method operator [] takes string className returns Array
        if (this.doodads[StringHash(className)] == 0) then
            set this.classNames.string[this.lastClassName] = className
            set this.lastClassName = this.lastClassName + 1
            set this.doodads[StringHash(className)] = Array.create()
        endif
        return this.doodads[StringHash(className)]
    endmethod
    
    public method random takes string className returns Object
        local Array arr = this[className]
        if (arr != 0) then
            return arr.random()
        endif
        return 0
    endmethod
    
    public method randomClassName takes nothing returns string
        return this.classNames.string[GetRandomInt(0, this.lastClassName - 1)]
    endmethod

// [] takes string, returns Array of doodads
// random takes string class returns random doodad
// ^^ not good enough yet. We need string class + location type (i.e. library / wall decoration vs library / floor decoration)
// ^^ well, do we really need it that complicated?
endstruct

public struct Setting
    private static Table name2setting
    private static Setting lastSetting
    
    readonly string name
    readonly Array patterns
    public SettingDoodadHelper doodads
    public real height
    public integer maxHeight
    
    private static method onInit takes nothing returns nothing
        set name2setting = Table.create()
    endmethod
    
    public static method create takes string name returns Setting
        local Setting this = Setting.allocate()
        debug call assert(name2setting[StringHash(name)] == 0, "DG::Setting::create - name already taken")
        set this.name = name
        set this.patterns = Array.create()
        set this.doodads = SettingDoodadHelper.create(this)
        set name2setting[StringHash(name)] = this
        set lastSetting = this
        return this
    endmethod
    
    private method onDestroy takes nothing returns nothing
        call BJDebugMsg("DG::Setting::onDestroy - not supposed to be destroyed!")
    endmethod
    
    public static method operator [] takes string name returns Setting
        return name2setting[StringHash(name)]
    endmethod
    
    public static method random takes nothing returns Setting
        return GetRandomInt(1, lastSetting)
    endmethod
endstruct

// ***************************************
// *
// * Layout Engine
// *
// ***************************************

public function LayoutEngine_execute takes Dungeon this returns nothing
    local integer roomId = 1
    local DG_Position position = 0
    local DG_Position patternPosition = 0
    local DG_Wall wall = 0
    local DG_Pattern pattern
        
    // Reset layout
    set DG_Layout.width = this.width
    set DG_Layout.height = this.height
    call DG_Layout.reset(0)
    
    // Create first room
    // Note: you could modify this to create it at a fixed position, i.e. at dungeon entrance
    set position = DG_Position.create(this.width / 2, this.height / 2)
    set pattern = DG_Pattern(this.setting.patterns.random())
    call pattern.register(position, roomId)
    set DG_Room(roomId).height = 0
    set DG_Room(roomId).pattern = pattern
    set roomId = roomId + 1
    
    // debug call log(DG_Layout.toString())
    
    loop
        exitwhen roomId >= this.rooms
        set wall = DG_Layout.getRandomWall()
        set pattern = this.setting.patterns.random()
        //debug call log("Pattern selected: \n" + pattern.toString())
        set patternPosition = pattern.randomWallFacing(wall.direction.rotate(180))
        //debug call log("Pattern position: " + patternPosition.toString())
        
        // substract patternPosition, AND then add 1 depending on the direction (y+1 for NORTH, x-1 for WEST, etc.)
        set position = wall.position.adjacent(wall.direction).sub(patternPosition)
        if (pattern.scan(position)) then
            call pattern.register(position, roomId)
            set DG_Room(roomId).height = IMaxBJ(0, IMinBJ(this.setting.maxHeight, GetRandomInt(Room(Layout[wall.position.x][wall.position.y]).height - 1, Room(Layout[wall.position.x][wall.position.y]).height + 1)))
            set DG_Room(roomId).pattern = pattern
            
            // Register gate
            if (wall.direction == DG_Direction.NORTH) then
                set this.gatesNorth[wall.position] = 1
            elseif (wall.direction == DG_Direction.EAST) then
                set this.gatesEast[wall.position] = 1
            elseif (wall.direction == DG_Direction.SOUTH) then
                set this.gatesNorth[wall.position.adjacent(DG_Direction.SOUTH)] = 1
            elseif (wall.direction == DG_Direction.WEST) then
                set this.gatesEast[wall.position.adjacent(DG_Direction.WEST)] = 1
            endif
            
            set roomId = roomId + 1
            //debug call log(" room created")
            //debug call log(DG_Layout.toString())
            static if (not SOFT_LOAD) then
                call TriggerSleepAction(0.0)
            endif
        endif
        call wall.destroy()
        
        static if (SOFT_LOAD) then
            call TriggerSleepAction(0.0)
        endif
    endloop
endfunction

// ***************************************
// *
// * Wall Engine
// *
// ***************************************


// TO BE IMPLEMENTED: MULTI-LEVEL DUNGEONS

public function WallEngine_execute takes Dungeon this returns nothing
    local integer x = 0
    local integer y
    local real rx = this.x
    local real ry
    local DG_Position position
    local DG_Position adjacent
    local DG_Room room
    local DG_Room adjacentRoom
    
    loop
        exitwhen x >= Layout.width
        set y = 0
        set ry = this.y
        set position = DG_Position.create(x, y)
        loop
            exitwhen y >= Layout.height
            
            set room = DG_Room(Layout[x][y])
            
            // Check WEST wall
            if (x == 0) then
                call DG_Object(this.setting.doodads["wall"].random()).instance(rx, ry, 0, DG_Direction.WEST.angle())
            endif
            
            // Check SOUTH wall
            if (y == 0) then
                call DG_Object(this.setting.doodads["wall"].random()).instance(rx, ry, 0, DG_Direction.SOUTH.angle())
            endif
            
            // Check EAST wall
            set adjacent = position.adjacent(DG_Direction.EAST)
            set adjacentRoom = DG_Room(Layout[adjacent.x][adjacent.y])
            if ((adjacent == position) or room != adjacentRoom) then
                if (this.gatesEast.exists(position)) then
                    call DG_Object(this.setting.doodads["wall-gate"].random()).instance(rx, ry, 0, DG_Direction.EAST.angle())
                else
                    call DG_Object(this.setting.doodads["wall"].random()).instance(rx, ry, 0, DG_Direction.EAST.angle())
                endif
            endif
            
            // Check NORTH wall
            set adjacent = position.adjacent(DG_Direction.NORTH)
            set adjacentRoom = DG_Room(Layout[adjacent.x][adjacent.y])
            if ((adjacent == position) or room != adjacentRoom) then
                if (this.gatesNorth.exists(position)) then
                    call DG_Object(this.setting.doodads["wall-gate"].random()).instance(rx, ry, 0, DG_Direction.NORTH.angle())
                else
                    call DG_Object(this.setting.doodads["wall"].random()).instance(rx, ry, 0, DG_Direction.NORTH.angle())
                endif
            endif
            
            // Check SOLID_WALL nodes
            if (room == 0) then
                call DG_Object(this.setting.doodads["wall-floor"].random()).instance(rx, ry, this.setting.height, DG_Direction.NORTH.angle())
            endif
            
            set position = adjacent
            set y = y + 1
            set ry = ry + DG_NODE_SIZE
        endloop
        set x = x + 1
        set rx = rx + DG_NODE_SIZE
    endloop
endfunction

// ***************************************
// *
// * Dungeon
// *
// ***************************************

struct Dungeon
    private integer seed
    readonly integer width
    readonly integer height
    readonly real x
    readonly real y
    readonly Setting setting
    
    readonly Table gatesNorth
    readonly Table gatesEast
    readonly integer rooms
    
    public static method create takes integer seed, integer width, integer height returns Dungeon
        local Dungeon this = Dungeon.allocate()
        set this.seed = seed
        set this.width = width
        set this.height = height
        return this
    endmethod
    
    public method instance takes real x, real y returns nothing
        debug call log("*** Initializing Dungeon Settings")
        call SetRandomSeed(this.seed)
        set this.setting = Setting.random()
        
        set this.x = x
        set this.y = y
        set this.gatesNorth = Table.create()
        set this.gatesEast = Table.create()
        set this.rooms = R2I(FEATURES_PER_NODE * this.width * this.height + 0.5)
        
        debug call log("*** Running Layout Engine...")
        call DG_LayoutEngine_execute(this)
        
        debug call log("*** Printing layout...")
        debug call log(Layout.toString())
        
        debug call log("*** Creating walls...")
        call DG_WallEngine_execute(this)
        
        debug call log("*** Cleaning up...")
        // TO DO: clean up doors
    endmethod
        
endstruct

endlibrary

JASS:
DUNGEON GENERATOR
=================

'Overview'

The content of this document:
    1. Overview
    2. Installation
    3. Library design & API
    
'Installation'
    Copy all triggers in "Libraries" as they are dependencies
    Copy the "Dungeon Generator" triggers
    Copy any object data you wish to transfer
    
    Initialize settings & globals
    
    "Configuration options"
        MAX_WIDTH           The maximum width of a single Dungeon
        MAX_HEIGHT          The maximum height of a single dungeon
        NODE_SIZE           The in-game size of a dungeon node (best to keep as-is)
        
        PATTERN_SYMBOL_ROOM     The pattern symbol for "room node"
        PATTERN_SYMBOL_NONE     The pattern symbol for "no room node"
        PATTERN_SYMBOL_LAYER    The pattern symbol for "next row"
        
        FEATURES_PER_NODE   A dungeon of size (width, height) has exactly width * height nodes.
                            A "feature" is a room, i.e. how many rooms to create for a dungeon of a certain size
        SOFT_LOAD           If true, a dungeon is created slowly but smoothly (without lag in-game).
                            This is ideal for games where the next level of a dungeon can be created in the background
                            while players are still fighting their way through the previous dungeon
        
    
'Library Design'

    The library is designed as a (large) set of auxiliary structs, and "engine" functions.
    The "engine" functions are functions responsible for actually generating the dungeon content.
    
    "Auxiliary structs"
    
        1. DG_Layout (static)
            Layout is a 2D grid that maps positions (x, y) to room nodes.
            A grid node is either 0 (solid wall) or a room-id, a unique identifier for a room. A room consists of several adjacent gridnodes.
            
            Layout[x][y]            Retrieve/modify room node at position (x, y)
            Layout.width            Retrieve/modify the width of the grid
            Layout.height           Retrieve/modify the height of the grid
            Layout.reset(value)     Resets the grid to the specified value (only width x height nodes)
            Layout.toString()       Retrieve string representation of the grid
            Layout.getRandomWall    Retrieve a random wall (only returns nodes with a room-id, thus excluding 0-node)
            
        2. DG_Direction
            There are 4 directions: north, east, south and west.
            
            DG_Direction.NORTH      Direction pointing up
            DG_Direction.EAST       Direction pointing right
            DG_Direction.SOUTH      Direction pointing down
            DG_Direction.WEST       Direction pointing left
            DG_Direction.random()   Retrieves a random direction
            direction.rotate(90)    Rotates a direction by a multiple of 90 degrees
            direction.toString()    Retrieve string representation of the direction
            
        3. DG_Position
            A position is a set of (x, y) integers.
            All positions should - under normal circumstances - be within Layout boundaries
            A position does not have to be destroyed
            
            DG_Position.checkBoundariesX(x) Maps the x coordinate between Layout boundaries (between 0 and Layout.width - 1)
            DG_Position.checkBoundariesY(y) Maps the y coordinate between Layout boundaries (between 0 and Layout.height - 1)
            DG_Position.create(x, y)        Creates a new Position
            position.x                      Retrieve/modify X coordinate
            position.y                      Retrieve/modify Y coordinate
            position.data                   Retrieve/modify data at Layout[x][y]
            position.toString()             Retrieve string representation of the position
            position.add(second)            Addition of 2 positions
            position.sub(second)            Substraction of 2 positions
            position.north                  Retrieve position north of this position
            position.east                   Retrieve position east of this position
            position.south                  Retrieve position south of this position
            position.west                   Retrieve position west of this position
            position.adjacent(direction)    Retrieve position adjacent towards the given direction
            position.random(width, height)  Retrieve random position (0/width, 0/height)
            
        4. DG_Wall
            A wall is a combination of a position and a direction.
            Specifically, a wall is positioned at a grid node, and pointing towards the specified direction.
            Each grid node can have up to 4 walls (each facing 1 of the 4 directions).
            A grid node has a wall in a certain direction if the adjacent node (towards that direction) is either another room or solid wall (0)
            A wall must be destroyed after use!
            
            DG_Wall.create(position, direction) Creates a wall at position, facing direction
            wall.opposite                       Retrieve the opposing wall (at adjacent position, facing opposing direction)
                                                i.e. (3, 5) & NORTH opposes (3, 6) & SOUTH
            wall.opposes(other)                 Returns true if the walls are opposing each others
            
        5. DG_Pattern
            A pattern is a room pattern. A room pattern can look like
            ##.   and defines how a room looks like when placed in the grid.
            ###   A dungeon can choose between multiple patterns when generating rooms.
            ###   A pattern is defined using a data string (read below)
            
            DG_Pattern.create(data)     Creates a pattern using the data string. A data string looks like: "##.|###|###", 
                                        where "#" means "room node", "." means "no room node" and "|" means "start new row". 
                                        The example describes the pattern as seen above
            pattern.toString()          Retrieves a string representation of the pattern
            pattern.width               Retrieves the width of the pattern
            pattern.height              Retrieves the height of the pattern
            pattern.size                Retrieves the number of room nodes in the pattern
            pattern.at(position)        Retrieves character at the specified position (either "#" or ".")
            pattern.randomWallFacing(direction) Retrieves a DG_Position (not DG_Wall, as suggested by the function name).
                                        #*.     For example: randomWallFacing(Direction.EAST) would return any position containing
                                        ##*     a wall facing the EAST. In the example pattern, the possible room nodes "#" have been
                                        ##*     replaced by a "*" symbol to show which nodes could be returned.
            pattern.scan(position)      Scans a position on the Layout grid. Returns true if the pattern does not collide on the Grid
            pattern.register(position, room)    Writes the pattern on the Grid (set grid node data = room)
            
        6. DG_Room
            A room is a unique identifier for a set of properties that define the room.
            
            room.height                 The "z" height of the room
            room.pattern                The pattern of the room
            room.class                  The class name of the room (i.e. "library", "torture", etc.)
            
        7. DG_Object (interface)
            An object is a real in-game entity (i.e. doodad, destructible)
            
            object.instance(x, y, z, angle) Creates an instance of the object at position (x, y, z), facing specified angle
            
        8. DG_Destructible (DG_Object)
            A destructible is a single destructible in the object editor
            
            DG_Destructible.create(id, x, y, z, face, minScale, maxScale, maxVariation)
                                    Defines a destructible. id = object editor RAW ID
                                    x, y, z: offset position when the destructible is instantiated
                                    face: default facing angle of destructible
                                    minScale & maxScale: specifies size boundaries of destructible instances
                                    maxVariation: specifies the possible variations of the destructible (between 0 and maxVariation - 1)
        
        9. DG_Doodad (DG_Object)
            A doodad is a collection of destructibles that are relatively positioned
            
            DG_Doodad.create(destructible)  Creates a new doodad
            doodad.add(destructible)        Adds a new destructible to the doodad
            
        10. DG_Setting
            A setting is a set of options that define a specific decor/atmosphere for a dungeon.
            Each dungeon has 1 associated setting, which defines the look of the dungeon, ranging from Stone dungeons to forests and villages.
            
            DG_Setting.create(name)     Creates a setting with specified identifier
            DG_Setting[name]            Retrieves a setting with specified identifier
            DG_Setting.random()         Retrieves a random setting
            setting.name                Retrieves the name of the setting
            setting.patterns            Array* of patterns
            setting.doodads[class]      Array of Objects, i.e. setting.doodads["library"] retrieves all "library" doodads
                                        Predefined / required classes: "wall", "wall-corner", "wall-stairs", "wall-gate", "wall-floor"
            setting.doodads.random(class)   Retrieves random Doodad of class
            setting.doodads.randomClassName Retrieves random class name (i.e. "library" or "torture", ...)
            setting.maxHeight           Retrieve/modify the number of layers in the dungeon (a dungeon consists of multiple layers, specifying the height of a room)
            setting.height              The in-game height of a single layer (a room at layer 2 is at height*2)
        
            
        Array container
            a.push(object)              Adds the object to the array
            a.size                      Retrieves the number of objects in the array
            a[index]                    Retrieves the object at the index
            a.random()                  Retrieves a random object in the array

            
    "Engine Functions"
    
        1. LayoutEngine_execute(Dungeon this)
            This function is responsible for generating a dungeon layout for the specified dungeon.
            Specific responsabilities:
                - Creating a specified number of rooms (and specifying their height/pattern)
                - Filling the Layout grid with interconnecting roomnodes so the dungeon layout "looks" like a dungeon
                - Initializing the Dungeon door list (a list of all doors/stairs in the dungeon)
            
        2. WallEngine_execute(Dungeon this)
            This function is responsible for generating all the walls, floors, stairs and gates of a dungeon
            Specific responsabilities:
                - Instantiating all walls, floors, stairs and gates of the dungeon. 
            At this point we should have a physical dungeon in-game
            
        3. DecorationEngine_execute(Dungeon this)
            This function is responsible for decorating the dungeon.
                
        4. ThreatEngine_execute(Dungeon this)
            This function is responsible for creating threats in the dungeon (basically: encounters & traps)
            
            
    "Dungeon"
    
        Dungeon.create(seed, width, height)     Creates a new dungeon
        dungeon.instance(x, y)                  Instantiates the dungeon at in-game coordinates (x, y)
You can test it in-game by pressing "escape".
 

Attachments

  • DG Problem 1.jpg
    DG Problem 1.jpg
    310.3 KB · Views: 288
  • DG Problem 2.jpg
    DG Problem 2.jpg
    365.5 KB · Views: 295
  • Dungeon Generator 0.2.04.w3x
    130.4 KB · Views: 71
Last edited:
Level 15
Joined
Oct 18, 2008
Messages
1,588
Hmm an advanced map/terrain generator-I love them ^^ I just made a galaxy generator for some1 here and I love how it works :) I'm not good at JASS but this gate bug seems VERY strange... The problem is I can't open WE, so it'd be easier if you uploaded the triggers in HIDDEN tags
 
Level 5
Joined
Oct 14, 2010
Messages
100
All right, I've posted the code in the main post. Dependency libraries are quite obvious I think.

I doubt the initialization code will help, but:
JASS:
scope DGDungeonInitialization initializer onInit

private function onInit takes nothing returns nothing
    local DG_Setting setting = DG_Setting.create("Default")
    set setting.height = 335.0
    set setting.maxHeight = 2
    
    // Create patterns
    call setting.patterns.push(DG_Pattern.create("####|###."))
    call setting.patterns.push(DG_Pattern.create("##|##"))
    call setting.patterns.push(DG_Pattern.create("###|##.|###"))
    call setting.patterns.push(DG_Pattern.create("..##|####|###.|###."))
    call setting.patterns.push(DG_Pattern.create("###|###|###"))
    call setting.patterns.push(DG_Pattern.create("##|##|##"))
    call setting.patterns.push(DG_Pattern.create("###|###"))
    call setting.patterns.push(DG_Pattern.create("##|#."))
    call setting.patterns.push(DG_Pattern.create("#..|#..|###"))
    call setting.patterns.push(DG_Pattern.create(".##.|####|####|.##."))
    call setting.patterns.push(DG_Pattern.create("##"))
    call setting.patterns.push(DG_Pattern.create("#|#"))
    
    // Create wall objects
    call setting.doodads["wall"].push(DG_Destructible.create('B003', 0, DG_NODE_SIZE/2, -335, 90, 3.35, 3.35, 0))
    call setting.doodads["wall-corner"].push(DG_Destructible.create('B001', DG_NODE_SIZE/2, DG_NODE_SIZE/2, 0, 0, 3.35, 3.35, 0))
    call setting.doodads["wall-floor"].push(DG_Destructible.create('B006', -DG_NODE_SIZE/8, -DG_NODE_SIZE/8, 0, 0, 1.40, 1.40, 0))
    call setting.doodads["wall-gate"].push(DG_Destructible.create('B005', 0, DG_NODE_SIZE/2, 0, 90, 3.35, 3.35, 0))
    call setting.doodads["wall-gate"].push(DG_Destructible.create('B004', 0, DG_NODE_SIZE/2, 0, 90, 3.35, 3.35, 0))
    call setting.doodads["wall-stairs"].push(DG_Doodad.create(DG_Destructible.create('B007', 0, DG_NODE_SIZE/2, -335, 90, 3.35, 3.35, 0)).add(DG_Destructible.create('B002', DG_NODE_SIZE/4, 0, 0, 270, 2.80, 2.80, 0)))
    
    // TO DO
endfunction

endscope

A possible explanation for problem 2 would be that using 2 different keys (2 different positions) on the hashtable result in the same entry. I find that doubtful as I'd expect a hashtable to have key collision detection, but... maybe it's a hashtable bug
 
Level 15
Joined
Oct 18, 2008
Messages
1,588
Hmm those patterns are strange, and without opening the map I can't really tell what you use them for...

But your code seems a lot more complicated and long than it should be (afor a simple generator, so I can't really get together it's generation process) so tell me exactly what are it's functions, else I don't know where to start ^^ I have clues where to look but this is a lot more advanced than most ppl would make it :) I'm not so experienced with JASS (and even less with vJass) but I hope my experience with Delphi and C will help now :D

But as far as I can tell from first glance I would have used another method ^^

EDIT: Oh a documentation! GOOOOOOD :)

Hmm it's not so different from how I would've done it, but the strings confused me without the doc :) The ceiling problem could be the basic pathing-check it! :) The gate problem...

Check this line:
JASS:
            elseif (wall.direction == DG_Direction.SOUTH) then
            set this.gatesNorth[wall.position.adjacent(DG_Direction.SOUTH)] = 1
I think the second one should be NORTH as you set the adjacent room's gate ^^
 
Last edited:
Level 5
Joined
Oct 14, 2010
Messages
100
Note that I'm not going for "roguelike" dungeons with rooms that are connected through corridors, but more for a "realistic" dungeon that is compact in size. For people without the editor, see attached screenshot.

Other than that, I think all the documentation you need is in the code & the attached documentation, including what "patterns" are for.
 

Attachments

  • DG_screen1.jpg
    DG_screen1.jpg
    335.8 KB · Views: 104
Level 15
Joined
Oct 18, 2008
Messages
1,588
Here (from my last post):

The ceiling problem could be the basic pathing-check it! :) The gate problem...

Check this line:
JASS:
elseif (wall.direction == DG_Direction.SOUTH) then
set this.gatesNorth[wall.position.adjacent(DG_Direction.SOUTH)] = 1
I think the second one should be NORTH as you set the adjacent room's gate ^^

Oh, and it's way better, thanks for the screenshot :) It works like a wonder (with the 2 exceptions). I've never seen a true dungeon gen here.
Note that I'm not going for "roguelike" dungeons with rooms that are connected through corridors, but more for a "realistic" dungeon that is compact in size.
I didn't expect anything else from the start as I've already made system like this outside of WC3 (2D RPG for school competition yaaay got the second price-but I competed with programmer-classes altough I learn electronics :p)

Anyway, decoding a mass 2D array similar to:
"111122111341
111122111561
111122222221
111122222221
111111111111"
to a map and a collision map is really funny especially seeing the idiots face who competed with a hangman software... They even blamed me for using game making sofware :D

So YES I like your method :O) I begun to think about making the same in GUI (LOL)
 
Level 5
Joined
Oct 14, 2010
Messages
100
The ceiling problem isn't just ceilings by the way. It's also some of the walls, and since some ceilings work fine, and others don't, I don't see how pathing could be the problem. The problem must be in the calculations I think... But can't see where.

The code you've posted is like that on purpose.
I'm only keeping track of east & north gates. And a south gate at position A is also a NORTH gate at the position south of position A. That's what that code is supposed to do.
 
Level 15
Joined
Oct 18, 2008
Messages
1,588
The code I posted is problematic-see bellow the explanation
elseif (wall.direction == DG_Direction.SOUTH) then
set this.gatesNorth[wall.position.adjacent(DG_Direction.SOUTH)] = 1<<SOUTH of adjacent
#=Wall
+=Room
G=Gate


#####
#+++#
##G##
^^^^it's a SOUTH gate of the room here but
ˇˇˇˇit's the NORTH gate of the adjacent room
##G##
#+++#
#####

but you set it like this:
#####
#+++#
##G##
#####
#+++#
##G##

instead of
#####
#+++#
##G##
##G##
#+++#
#####

so you get one more gate than you should :)
 
Level 15
Joined
Oct 18, 2008
Messages
1,588
Oh, I got confused a bit, you're right :D
Eh then no clue then sorry :( If I made a system like that then I'd just generate a table out of it then make the dungeon out of the table, and for debug reason I could just check the table... It's hard for me to check such a long code if it's not mine :(
 
Level 5
Joined
Oct 14, 2010
Messages
100
Like I've said earlier, I'm quite sure that's not the problem, because:
A) The ceilings don't have a pathing map, yet are having the same problem as walls with pathing maps
B) I've tried creating 2 walls at the same position (with different z position) and it worked as expected. So apparently the pathing map is being ignored while using CreateDestructibleZ.

Edit: sorry, I didn't see you were talking about snapping to a grid.

It does snap to the grid (every 64 in-game units) though, but since each destructible is supposed to spawn 512 units apart, they should snap to the same grid lines. Because of an inaccuracy with ATan2, this occasionally has lead to an x coordinate of 63.999 vs 64.000, which did cause problems way more severe than the one I'm having now. I've fixed that problem by adding an epsilon value (of 0.15) to each calculated x/y coordinate, so both 64.149 and 64.150 snap to the 64.000 grid lines.
On second thought, that may have something to do with it, but leaving *out* the epsilon value only makes things worse.

Edit 2: further testing seems to indicate that the misalignment happens near the 0 axes. Will try to find a solution tomorrow.
 
Level 5
Joined
Oct 14, 2010
Messages
100
All right, I can't seem to fix problem 1 besides forcing map boundaries to be between 0 and width as opposed to -width/2 and width/2. So I guess it'll work...

edit: I've implemented a solution for problem 2 as well:

JASS:
if (this.gatesNorth.exists(position)) then
modified into
JASS:
if (this.gatesNorth.exists(position) and adjacentRoom != 0 and room != 0) then
It now checks if both nodes at each side of the gate is a room. Although I'm confident this *is* already checked before a gate is added to the gatesNorth list, apparently I must do it again.

I'm still open to suggestions for a real fix of the problems though.
 
Level 5
Joined
Oct 14, 2010
Messages
100
New problem guys, but it should be easy to fix - if I know how.

After creating a lot of destructibles, the system suddenly stops. I'm assuming it's because the op-limit has been reached, but TriggerSleepAction doesn't seem to work.

As you can see, I'm calling TSA in Destructible.instance(...), but it's not having any effect.
I've tried Destructible.instance.execute(...) but it's not helping.
Anything I can do so TSA's work?
 
Level 7
Joined
Dec 3, 2006
Messages
339
http://www.thehelper.net/forums/showthread.php/163364-Recursive-vs-loops

That's my guess. Use recursion instead of loops for some of it basically.

As for the problem with the doodads not being created at exactly 64.0 maybe the answer is to use an grid not relying on calculations really and using something similar to this for the whole map:
http://www.hiveworkshop.com/forums/graveyard-418/snippet-mapgrid-197294/
or this:
http://www.thehelper.net/forums/showthread.php/158197-Grid-system-useful-or-not-S/page3

I dunno if I help'd at all; these are just kinda suggestions.
 
Last edited:
Level 7
Joined
Dec 3, 2006
Messages
339
Has this had any success in creating multiple levels? I can't seem to figure out how to make multiple levels with it. I looked at the instructions. Tried modifying setting.maxHeight but that had no effect on the dungeon generation.

Edit:
Also here some other ideas that might help with the op limit problem try re-setting or flushing the hash table after creating a dungeon in certain places where it can be done. And maybe use this table instead: http://www.hiveworkshop.com/forums/jass-functions-413/snippet-new-table-188084/
 
Last edited:
Level 7
Joined
Dec 3, 2006
Messages
339
Sorry to necropost; but are you still working on this? Cause I definitely have some use for it in the map that I am making.
 
Status
Not open for further replies.
Top