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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263 | 1x
1x
1x
66x
22x
22x
22x
24x
24x
24x
270x
2800x
932x
1868x
1x
2x
1x
1x
1x
2x
2x
2x
2x
2x
70x
2x
2x
46x
22x
22x
22x
22x
22x
22x
22x
200x
200x
14x
14x
22x
22x
24x
2800x
24x
22x
22x
17x
22x
12x
22x
22x
22x
22x
46x
22x
24x
22x
22x
22x
22x
22x
8x
8x
8x
8x
8x
8x
14x
14x
14x
14x
14x
14x
22x
22x
8x
200x
200x
200x
200x
200x
14x
14x
2x
2x
2x
2x
| const VERTICAL = 'VERTICAL'
const HORIZONTAL = 'HORIZONTAL'
export function sum(a, b) {
return a + b
}
/**
* Returns a random number between min and max
*
* @param {number} min minimum random number to return
* @param {number} max maximum random number to return
* @returns {number}
*/
const randomIndexBetweenValues = (min, max) => (
Math.floor((Math.random() * ((max - min) + 1)) + min)
)
/**
* Return a random direction of vertical or horizontal
*
* @returns {string} 'vertical' or 'horizontal'
*/
function randomDirection(directionOptions = [VERTICAL, HORIZONTAL]) {
Iif (directionOptions.length === 0) {
return ''
}
const numOptions = directionOptions.length - 1
return directionOptions[randomIndexBetweenValues(0, numOptions)]
}
/**
* Add blocking tiles around the parameter of the 2d array
*
* @param {array} array 2d array representing a room
*/
function AddRoomBoundaries(array) {
const currHeight = array.length - 1
const currWidth = array[0].length - 1
return array.map((row, rIndex) =>
row.map((col, cIndex) => {
if (rIndex === 0 || rIndex === currHeight || cIndex === 0 || cIndex === currWidth) {
return 1
}
return col
}),
)
}
const setDefaultValues = (settings) => {
if (settings) {
return {
width: settings.width || 50,
height: settings.height || 50,
minRoomSize: settings.minRoomSize || 5,
maxRoomSize: settings.maxRoomSize || 20,
}
}
return { width: 50, height: 50, minRoomSize: 5, maxRoomSize: 20 }
}
/**
* Function used to create a new dungeon object
*
* @param {number} [width=50]
* @param {number} [height=50]
* @param {number} [minRoomSize=5]
* @param {number} [maxRoomSize=20]
* @returns dungeon object
*/
export const NewDungeon = (settings) => {
const Dungeon = {
init(width, height, minRoomSize, maxRoomSize) {
// Create an empty 2D array width x height
this.minRoomSize = 5
this.maxRoomSize = 20
this.counter = 1
// What am I trying to do here?
this.tree = {
level: Array.apply(null, { length: height }).map(() => (
Array.apply(null, { length: width })),
),
}
this.split(this.tree)
this.connectRooms(this.tree)
},
/**
* Given a 2d array (node.level), this function will split the room into two separate arrays
* and store then in node.lNode.level and node.rNode.level.
* The split is random (vertical or horizontal)
* and can be anywhere along the axis as long as it doesn't result in making the room smaller
* than this.min.
*
* @param {object} node
*/
split(node) {
if (node.level.length > this.maxRoomSize || node.level[0].length > this.maxRoomSize) {
// If this condition is true, then we need to split again
const splitDirection = randomDirection(
this.getSplitOptions(node.level.length, node.level[0].length),
)
const indexToSplit = this.getIndexToSplit(
splitDirection, node.level[0].length, node.level.length,
)
let lNode = node.leftNode = {}
let rNode = node.rightNode = {}
node.splitDirection = splitDirection
node.splitIndex = indexToSplit
/**
* Split the rooms either vertically or horizontally and store the
* new array in the left node and right nodes
*/
if (splitDirection === VERTICAL) {
lNode.level = node.level.map(row => row.slice(0, indexToSplit))
rNode.level = node.level.map(row => row.slice(indexToSplit, row.length))
} else {
lNode.level = node.level.slice(0, indexToSplit)
rNode.level = node.level.slice(indexToSplit, node.level.length)
}
/**
* Recursive call to split the rooms again if needed
*/
this.split(lNode)
this.split(rNode)
} else {
/**
* If we reach this point, then we can guarantee that the room does not
* have any children nodes. I.e. it's the smallest leaf node
* counter is used so we can visually see the different rooms when the room is rendered
*/
this.counter += 1
node.level = node.level.map(row => row.map(() => this.counter))
node.level = AddRoomBoundaries(node.level)
}
},
/**
* returns an array with the values VERTICAL if a vertical split is an option, and
* HORIZONTAL is a horizontal split is an option, or an empy array neither are options.
* Whether or not it is an option is based on if the size of the room is greater than
* the maximum allowed room size.
*
* @param {number} verticalLength
* @param {number} horizontalLength
* @returns {array}
*/
getSplitOptions(verticalLength, horizontalLength) {
const directionOptions = []
if (verticalLength > this.maxRoomSize) {
directionOptions.push(HORIZONTAL)
}
if (horizontalLength > this.maxRoomSize) {
directionOptions.push(VERTICAL)
}
return directionOptions
},
/**
* Returns a random number along the vertical or horizontal axis
*
* @param {any} verticalLength
* @param {any} horizontalLength
* @returns {number}
*/
getIndexToSplit(splitDirection, horizontalLength, verticalLength) {
const min = this.minRoomSize
const max = splitDirection === VERTICAL ? horizontalLength - min : verticalLength - min
return randomIndexBetweenValues(min, max)
},
/**
* @param {any} node
* @returns
*/
connectRooms(node) {
/**
* First, recursively loop through all the rooms so we're starting at the
* lowest nodes in the tree that have child nodes, and we work our way back up.
*/
if (node.leftNode) {
this.connectRooms(node.leftNode)
} else {
return
}
Eif (node.rightNode) {
this.connectRooms(node.rightNode)
} else {
return
}
const lNode = node.leftNode.level
const rNode = node.rightNode.level
/**
* Since this function is called after all the rooms have been created, we know that all
* the rooms already have boundaries. We just need to remove the wall at a random
* intersecting point.
*/
if (node.splitDirection === VERTICAL) {
/**
* For vertical cut, the corridor will be horizontal.
* So somewhere along the 0 -> firstRoom.length axis
* Don't put the corridor on the outer most index values (0 and length - 1) because
* that can allow the corridor to be on the map boundary
*/
const vIndex = randomIndexBetweenValues(1, lNode.length - 2)
node.corridorIndex = vIndex
lNode[vIndex][lNode[0].length - 1] = 0
lNode[vIndex][lNode[0].length - 2] = 0
rNode[vIndex][0] = 0
rNode[vIndex][1] = 0
} else {
// For horizontal cut, the corridor will be vertical.
// So somewhere along the firstRoom[row[0]] -> firstRoom[row.length] axis
const hIndex = randomIndexBetweenValues(1, lNode[0].length - 2)
node.corridorIndex = hIndex
lNode[lNode.length - 1][hIndex] = 0
lNode[lNode.length - 2][hIndex] = 0
rNode[0][hIndex] = 0
rNode[1][hIndex] = 0
}
/**
* Combine the child left node and right node rooms back together, and
* save the result in the current level node.
*/
node.level = []
if (node.splitDirection === VERTICAL) {
node.level = lNode.reduce((obj, val, index) => {
const temp = []
temp.push(...lNode[index])
temp.push(...rNode[index])
obj.push(temp)
return obj
}, [])
} else {
// If we get to this point, then the slice was horizontal
node.level.push(...lNode)
node.level.push(...rNode)
}
},
}
const dungeon = Object.create(Dungeon)
const { width, height, minRoomSize, maxRoomSize } = setDefaultValues(settings)
dungeon.init(width, height, minRoomSize, maxRoomSize)
return dungeon.tree.level
} // end NewDungeon
export default NewDungeon
|