├── LICENSE ├── README.md ├── demo └── dungeon gen.rbxl └── src └── main.lua /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dungeon-generator 2 | A Roblox dungeon generation module 3 | -------------------------------------------------------------------------------- /demo/dungeon gen.rbxl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EgoMooseOldProjects/Dungeon-generator/2d2485d001421f0a730348a3a446c9ae74f4197f/demo/dungeon gen.rbxl -------------------------------------------------------------------------------- /src/main.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Dungeon generator 3 | Dungeon generator module for Roblox. 4 | @author EgoMoose 5 | @link N/A 6 | @date 12/02/2016 7 | --]] 8 | 9 | ------------------------------------------------------------------------------------------------------------------------------ 10 | --// Setup 11 | 12 | local cardinal = { 13 | north = Vector2.new(0, -1); 14 | east = Vector2.new(1, 0); 15 | south = Vector2.new(0, 1); 16 | west = Vector2.new(-1, 0); 17 | }; 18 | 19 | local enums = { 20 | empty = 0; 21 | hall = 1; 22 | room = 2; 23 | junction = 3; 24 | }; 25 | 26 | ------------------------------------------------------------------------------------------------------------------------------ 27 | --// Functions 28 | 29 | function contains(tab, element, keys) 30 | local focus = keys or tab; 31 | for key, val in pairs(focus) do 32 | local index = keys and val or key; 33 | if tab[index] == element then 34 | return index; 35 | end; 36 | end; 37 | end; 38 | 39 | ------------------------------------------------------------------------------------------------------------------------------ 40 | --// Classes 41 | 42 | function class_tile(pos, enum, region) 43 | local this = {}; 44 | 45 | -- properties 46 | 47 | this.position = pos; 48 | this.enum = enum; 49 | this.region = region; 50 | this.directions = { 51 | north = false; 52 | east = false; 53 | south = false; 54 | west = false; 55 | }; 56 | 57 | -- return 58 | 59 | return this; 60 | end; 61 | 62 | function class_tiles() 63 | local this = setmetatable({}, {}); 64 | 65 | -- private functions 66 | 67 | local function init() 68 | -- this table 69 | local mt = getmetatable(this); 70 | mt.__index = function(t, k) return rawget(t, tostring(k)); end; 71 | mt.__newindex = function(t, k, v) rawset(t, tostring(k), v); end; 72 | end; 73 | 74 | -- public functions 75 | 76 | function this:setTiles(origin, enum, region, width, height) 77 | local width, height = width or 0, height or 0; 78 | for x = 0, width do 79 | for y = 0, height do 80 | local pos = origin + Vector2.new(x, y); 81 | if not this[pos] then 82 | this[pos] = class_tile(pos, enum, region); 83 | else 84 | this[pos].position = pos; 85 | this[pos].enum = enum; 86 | this[pos].region = region; 87 | this[pos].directions = { 88 | north = false; 89 | east = false; 90 | south = false; 91 | west = false; 92 | }; 93 | end; 94 | end; 95 | end; 96 | end; 97 | 98 | function this:getEnum(pos) 99 | local tile = this[pos]; 100 | if tile then 101 | return tile.enum; 102 | end; 103 | end; 104 | 105 | function this:getAllTiles(enum) 106 | local tiles = {}; 107 | for pos, tile in pairs(this) do 108 | if (type(tile) == "table" and tile.enum == enum) or not enum then 109 | table.insert(tiles, tile); 110 | end; 111 | end; 112 | return tiles; 113 | end; 114 | 115 | function this:getAllTilesIgnore(enum) 116 | local tiles = {}; 117 | for pos, tile in pairs(this) do 118 | if (type(tile) == "table" and tile.enum ~= enum) then 119 | table.insert(tiles, tile); 120 | end; 121 | end; 122 | return tiles; 123 | end; 124 | 125 | -- return 126 | 127 | init(); 128 | return this; 129 | end; 130 | 131 | function class_rectangle(x, y, width, height) 132 | local this = {}; 133 | 134 | -- properties 135 | 136 | this.position = Vector2.new(x, y); 137 | this.size = Vector2.new(width, height); 138 | 139 | this.corners = { 140 | this.position; 141 | this.position + Vector2.new(width, 0); 142 | this.position + Vector2.new(0, height); 143 | this.position + this.size; 144 | }; 145 | 146 | -- private functions 147 | 148 | local function dot2d(a, b) 149 | return a.x * b.x + a.y * b.y; 150 | end; 151 | 152 | local function getAxis(c1, c2) 153 | local axis = {}; 154 | axis[1] = c1[2] - c1[1]; 155 | axis[2] = c1[2] - c1[3]; 156 | axis[3] = c2[1] - c2[4]; 157 | axis[4] = c2[1] - c2[2]; 158 | return axis; 159 | end; 160 | 161 | -- public functions 162 | 163 | function this:collidesWith(other) 164 | -- this method of collision is called separating axis theorem 165 | -- you can read about it here: http://www.dyn4j.org/2010/01/sat/ 166 | -- understanding this requires knowledge of the dot product: 167 | -- http://wiki.roblox.com/index.php?title=User:EgoMoose/The_scary_thing_known_as_the_dot_product 168 | local scalars = {}; 169 | local axis = getAxis(self.corners, other.corners); 170 | for i = 1, #axis do 171 | for i2, set in pairs({self.corners, other.corners}) do 172 | scalars[i2] = {}; 173 | for _, point in pairs(set) do 174 | local v = dot2d(point, axis[i].unit); 175 | table.insert(scalars[i2], v); 176 | end; 177 | end; 178 | local s1max, s1min = math.max(unpack(scalars[1])), math.min(unpack(scalars[1])); 179 | local s2max, s2min = math.max(unpack(scalars[2])), math.min(unpack(scalars[2])); 180 | if s2min > s1max or s2max < s1min then 181 | return false; 182 | end; 183 | end; 184 | return true; 185 | end; 186 | 187 | -- return 188 | 189 | return this; 190 | end; 191 | 192 | function class_dungeonGen() 193 | local this = {}; 194 | 195 | -- properties 196 | 197 | this.roomSize = 3; -- potential size of rooms 198 | this.enums = enums; -- don't change 199 | this.lastSeed = nil; -- don't change 200 | this.windingPercent = 0; -- curvy maze? (0 - 100) 201 | this.attemptRoomNum = 20; -- how many rooms should be attempted to fit in the bounds 202 | this.loadBuffer = math.huge; -- how many actions between every wait (use this to, y'know, not freeze ur computer) 203 | this.genMapDirections = true; -- if not needed turn this off! it saves so much script performance 204 | this.seed = function() return tick(); end; -- returns what's used in randomseed 205 | this.bounds = { -- size of map 206 | width = 100; 207 | height = 100; 208 | }; 209 | 210 | 211 | -- private properties 212 | 213 | local currentRegion = 0; 214 | local tiles = class_tiles(); 215 | 216 | -- private functions 217 | 218 | local function newRegion() 219 | currentRegion = currentRegion + 1; 220 | end; 221 | 222 | local function boundsContains(pos) 223 | if pos.x > this.bounds.width or pos.y > this.bounds.height or pos.x < 0 or pos.y < 0 then 224 | return false; 225 | end; 226 | return true; 227 | end; 228 | 229 | local function canCarve(pos, direction) 230 | if not boundsContains(pos + direction * 3) then 231 | return false; 232 | end; 233 | return tiles:getEnum(pos + direction * 2) == 0; 234 | end; 235 | 236 | local function createRooms() 237 | local rooms = {}; 238 | 239 | -- attempt as many rooms as possible 240 | for i = 1, this.attemptRoomNum do 241 | -- randomly shape the room 242 | local size = math.random(this.roomSize) * 2 + 1; 243 | local rectangularity = math.random(1 + math.floor(size/2)) * 2; 244 | local width, height = size, size; 245 | if math.random(2) == 1 then 246 | width = width + rectangularity; 247 | else 248 | height = height + rectangularity; 249 | end; 250 | 251 | -- build room as rectangle class 252 | local x = math.random(math.floor((this.bounds.width - width)/2)) * 2 + 1; 253 | local y = math.random(math.floor((this.bounds.height - height)/2)) * 2 + 1; 254 | local room = class_rectangle(x, y, width, height); 255 | 256 | -- check for collisions 257 | local overlapping = false; 258 | for _, other in pairs(rooms) do 259 | if room:collidesWith(other) then 260 | overlapping = true; 261 | break; 262 | end; 263 | end; 264 | 265 | -- if no overlap then add as a room 266 | if not overlapping then 267 | newRegion(); 268 | table.insert(rooms, room); 269 | tiles:setTiles(room.position, enums.room, currentRegion, room.size.x - 1, room.size.y - 1); -- subtract 1? 270 | end; 271 | end; 272 | end; 273 | 274 | local function growMaze(pos) 275 | tiles:setTiles(pos, enums.hall, currentRegion); 276 | local c = 0; 277 | local cells, lastDirection = {tiles[pos]}; 278 | 279 | -- spanning tree algorithm 280 | while #cells > 0 do 281 | c = c + 1; 282 | local potential = {}; 283 | local cell = cells[#cells]; 284 | 285 | -- check the 4 cardinal directions 286 | for name, direction in pairs(cardinal) do 287 | if canCarve(cell.position, direction) then 288 | table.insert(potential, direction); 289 | end; 290 | end; 291 | 292 | -- pick a direction if plausible 293 | if #potential > 0 then 294 | local direction; 295 | if contains(potential, lastDirection) and math.random(100) > this.windingPercent then 296 | direction = lastDirection 297 | else 298 | direction = potential[math.random(#potential)]; 299 | end; 300 | 301 | -- set the tile 302 | tiles:setTiles(cell.position + direction, enums.hall, currentRegion) 303 | tiles:setTiles(cell.position + direction * 2, enums.hall, currentRegion) 304 | 305 | table.insert(cells, tiles[cell.position + direction * 2]); 306 | 307 | -- prepare to repeat 308 | lastDirection = direction; 309 | else 310 | table.remove(cells, #cells); 311 | lastDirection = nil; 312 | end; 313 | -- wait buffer 314 | if c % this.loadBuffer == 0 then 315 | wait(); 316 | end; 317 | end; 318 | end; 319 | 320 | local function connectRegions() 321 | local connectors = {}; 322 | local connectedPoints = {}; 323 | 324 | -- collect regions that can be connected 325 | for _, tile in pairs(tiles:getAllTiles(enums.empty)) do 326 | -- count unqiue regions 327 | local regions, count = {}, 0; 328 | for name, direction in pairs(cardinal) do 329 | local ntile = tiles[tile.position + direction]; 330 | if ntile and ntile.enum > 0 and ntile.region then 331 | regions[ntile.region] = true; 332 | end; 333 | end; 334 | local open = {}; 335 | for region, _ in pairs(regions) do 336 | count = count + 1; 337 | table.insert(open, region); 338 | end; 339 | -- if two unique regions store as a possible outcome 340 | if count == 2 then 341 | table.insert(connectedPoints, tile); 342 | connectors[tostring(tile.position)] = open; 343 | end; 344 | end; 345 | 346 | -- place our connections/smooth 347 | while #connectedPoints > 1 do 348 | -- pick random junction 349 | local index = math.random(#connectedPoints) 350 | local tile = connectedPoints[index]; 351 | local region = connectors[tostring(tile.position)]; 352 | 353 | tiles:setTiles(tile.position, enums.junction, nil); 354 | table.remove(connectedPoints, index); 355 | 356 | -- don't remove all similar sources 357 | for i, otile in pairs(connectedPoints) do 358 | if i ~= index then 359 | local open = connectors[tostring(otile.position)]; 360 | if contains(open, region[1]) and contains(open, region[2]) then 361 | table.remove(connectedPoints, i); 362 | end; 363 | end; 364 | end; 365 | 366 | -- remove tiles that are side by side 367 | for i, otile in pairs(tiles:getAllTiles(enums.junction)) do 368 | if otile.position ~= tile.position then 369 | if (otile.position - tile.position).magnitude < 1.1 then 370 | tiles:setTiles(otile.position, enums.empty, nil); 371 | end; 372 | end 373 | end; 374 | end; 375 | end; 376 | 377 | local function removeDeadEnds() 378 | -- collect/set initial data 379 | local done, c = false, 0; 380 | local maze = tiles:getAllTiles(enums.hall); 381 | 382 | -- check as long as is needed 383 | while not done do 384 | done = true; 385 | for i, tile in pairs(maze) do 386 | c = c + 1; 387 | local exits = 0; 388 | 389 | -- check all potential spaces 390 | for name, direction in pairs(cardinal) do 391 | local ntile = tiles[tile.position + direction]; 392 | if ntile and ntile.enum ~= enums.empty then 393 | exits = exits + 1; 394 | end; 395 | end; 396 | if c % this.loadBuffer == 0 then 397 | wait(); 398 | end; 399 | 400 | -- if only single place to move then it's a dead end 401 | if exits <= 1 then 402 | table.remove(maze, i); 403 | tiles:setTiles(tile.position, enums.empty, nil); 404 | done = false; 405 | break; 406 | end; 407 | end; 408 | end; 409 | end; 410 | 411 | local function mapDirections() 412 | local c = 0; 413 | for i, tile in pairs(tiles:getAllTilesIgnore(enums.empty)) do 414 | c = c + 1; 415 | for name, direction in pairs(cardinal) do 416 | local ntile = tiles[tile.position + direction]; 417 | if ntile and ntile.enum ~= enums.empty then 418 | tile.directions[name] = true; 419 | end; 420 | end; 421 | if c % this.loadBuffer == 0 then 422 | wait(); 423 | end; 424 | end; 425 | end; 426 | 427 | -- public functions 428 | 429 | function this:generate() 430 | -- set randome seed 431 | this.lastSeed = this.seed(); 432 | math.randomseed(this.lastSeed); 433 | 434 | -- generate initial 435 | tiles:setTiles(Vector2.new(0, 0), enums.empty, currentRegion, self.bounds.width, self.bounds.height); 436 | createRooms(); 437 | newRegion(); 438 | 439 | -- start maze 440 | for _, tile in pairs(tiles:getAllTiles(enums.empty)) do 441 | -- must be odd number position 442 | local pos = tile.position; 443 | if pos.x % 2 == 1 and pos.y % 2 == 1 and tile.enum == enums.empty then 444 | growMaze(pos); 445 | --break; 446 | end; 447 | end; 448 | 449 | -- smooth and get find final 450 | connectRegions(); 451 | removeDeadEnds(); 452 | if this.genMapDirections then 453 | mapDirections(); 454 | end; 455 | 456 | -- return tile set 457 | return tiles; 458 | end; 459 | 460 | -- return 461 | 462 | return this; 463 | end; 464 | 465 | ------------------------------------------------------------------------------------------------------------------------------ 466 | --// Run 467 | 468 | return class_dungeonGen; --------------------------------------------------------------------------------