Now About Social Code
summaryrefslogtreecommitdiff
path: root/scripts/bsp_level_generator.gd
blob: 543eb1df7066f82430ec7eae259d8c1673b94bac (plain)
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
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
extends Node3D

@export_category("Level Generator")
@export var width: int = 40
@export var height: int = 40
@export var min_dim: int = 5

@export_category("Geometry Generator")
## Geometry generation config
@export var wall_thickness: float = 1

var wall_inst = preload("res://models/wall.blend")
var locker_inst = preload("res://prefabs/locker.tscn")
var level_geo: Node3D

enum Direction {LEFT, RIGHT}

signal grid_generated(grid: Array[Tile], p_grid_width: int, p_grid_height: int)

var min_room_size: int = min_dim * min_dim

var grid_width = width + 1
var grid_height = height + 1

var rooms: Array[Rect2i] = []

class BSPNode:
	var axis: int
	var min_dims: Vector2i
	var max_dims: Vector2i
	var left: BSPNode
	var right: BSPNode
	
	func _init(p_axis: int, p_min_dims: Vector2i, p_max_dims: Vector2i, p_left: BSPNode, p_right: BSPNode):
		axis = p_axis
		min_dims = p_min_dims
		max_dims = p_max_dims
		left = p_left
		right = p_right

func generate_level(axis: int, min_space: Vector2i, max_space: Vector2i, depth: int = 0) -> BSPNode:
	var dims = max_space - min_space
	var new_axis = (axis + 1) % 2
	# 10% we stop here and just create a big room
	if (depth > 2 and randi_range(0, 9) == 0) \
	or dims.x * dims.y <= min_dim * min_dim \
	or dims[new_axis] / 2 < min_dim:
		return BSPNode.new(new_axis, min_space, max_space, null, null)
		
	# Calculate min and max ranges so that a split 
	# doesn't create a room that violates min dimensions
	var min_value = min_space[axis] + min_dim
	var max_value = max_space[axis] - min_dim
	var split = randi_range(min_value, max_value)
	#print("Spliting axis ", axis, " at ", split)
	
	var left_min_space = min_space
	var left_max_space = max_space
	left_max_space[axis] = split
	var left = generate_level(new_axis, left_min_space, left_max_space, depth + 1)
	
	var right_min_space = min_space
	right_min_space[axis] = split
	var right_max_space = max_space
	var right = generate_level(new_axis, right_min_space, right_max_space, depth + 1)
	
	return BSPNode.new(axis, min_space, max_space, left, right)
	
func is_leaf(node: BSPNode) -> bool:
	return node.left == null and node.right == null
	
func check_door(grid: Array[Tile.Tile], axis: int, door_pos: Vector2i) -> bool:
	var room_left = door_pos
	room_left[axis] -= 1
	
	var room_right = door_pos
	room_right[axis] += 1
	
	if door_pos.x >= grid_width or door_pos.y >= grid_height:
		return false
	if room_left.x >= grid_width or room_left.y >= grid_height:
		return false
	if room_right.x >= grid_width or room_right.y >= grid_height:
		return false
	
	# Check if there are two spaces to connect
	# And ensure no door already exists in the space
	return grid[room_left.y * grid_width + room_left.x] == Tile.Tile.FLOOR \
	and grid[room_right.y * grid_width + room_right.x] == Tile.Tile.FLOOR \
	and grid[door_pos.y * grid_width + door_pos.x] == Tile.Tile.WALL

func generate_grid(map: BSPNode, grid: Array[Tile.Tile]) -> void:
	if is_leaf(map):
		for y in range(map.min_dims.y, map.max_dims.y - 1):
			for x in range(map.min_dims.x, map.max_dims.x - 1):
				grid[(y+1) * grid_width + (x+1)] = Tile.Tile.FLOOR
		
		# TODO double check room dimensions are correct here
		var room = Rect2i(map.min_dims + Vector2i.ONE, map.max_dims - map.min_dims)
		rooms.append(room)
	else:
		generate_grid(map.left, grid)
		generate_grid(map.right, grid)
		
		# Look for space on dividing wall to place door
		var other_axis = (map.axis + 1) % 2
		var split_axis = map.left.max_dims[map.axis]
		var have_door = false
		var tries = 0
		
		while not have_door:
			var test_door = randi_range(map.min_dims[other_axis], map.max_dims[other_axis] - 1)
			var door_pos = Vector2i.ZERO
			door_pos[map.axis] = split_axis
			door_pos[other_axis] = test_door + 1
			var door_pos2 = door_pos
			door_pos2[other_axis] += 1
			
			if check_door(grid, map.axis, door_pos) and check_door(grid, map.axis, door_pos2):
				have_door = true
				
				# Place grid for mesh
				grid[door_pos.y * grid_width + door_pos.x] = Tile.Tile.DOOR
				grid[door_pos2.y * grid_width + door_pos2.x] = Tile.Tile.DOOR
				#door_pos[map.axis] -= 1
				#grid[door_pos.y * grid_width + door_pos.x] = Tile.Tile.FLOOR

			tries += 1
			if tries > 1000:
				print("Took too many attempts to generate a door")
				get_tree().quit()
				return
				
func get_tile(grid: Array[Tile.Tile], pos: Vector2i) -> Tile.Tile:
	return grid[pos.y * grid_width + pos.x]
	
func place_lockers(grid: Array[Tile.Tile]) -> void:
	for room in rooms:
		var num_lockers = randi_range(0, 2)
		print("Generating ", num_lockers, "Lockers")
		for i in range(num_lockers):
			var found = false
			while not found:
				var pos = Vector2i(randi_range(room.position.x, room.end.x - 1), randi_range(room.position.y, room.end.y - 1))
				if grid[pos.y * grid_width + pos.x] == Tile.Tile.FLOOR \
				and (get_tile(grid, pos + Vector2i(1, 0)) == Tile.Tile.WALL \
				or get_tile(grid, pos + Vector2i(-1, 0)) == Tile.Tile.WALL \
				or get_tile(grid, pos + Vector2i(0, 1)) == Tile.Tile.WALL \
				or get_tile(grid, pos + Vector2i(0, -1)) == Tile.Tile.WALL) \
				and get_tile(grid, pos + Vector2i(1, 0)) != Tile.Tile.DOOR \
				and get_tile(grid, pos + Vector2i(-1, 0)) != Tile.Tile.DOOR \
				and get_tile(grid, pos + Vector2i(0, 1)) != Tile.Tile.DOOR \
				and get_tile(grid, pos + Vector2i(0, -1)) != Tile.Tile.DOOR:
					grid[pos.y * grid_width + pos.x] = Tile.Tile.LOCKER
					found = true
					
func place_player(grid: Array[Tile.Tile]) -> Rect2i:
	var player_room: Rect2i = rooms.pick_random()
	var player_pos = Vector2i(player_room.position.x + player_room.size.x / 2, player_room.position.y + player_room.size.y / 2)
	
	grid[player_pos.y * grid_width + player_pos.x] = Tile.Tile.PLAYER
	return player_room

func place_enemies(grid: Array[Tile.Tile], player_room: Rect2i) -> void:
	var num_enemies = randi_range(4, 10)

	for i in range(num_enemies):
		var found_pos = false
		while not found_pos:
			var room: Rect2i = rooms.pick_random()
			if room == player_room:
				continue
				
			var pos = Vector2i(randi_range(room.position.x, room.end.x - 1), randi_range(room.position.y, room.end.y - 1))
			if grid[pos.y * grid_width + pos.x] == Tile.Tile.FLOOR:
				grid[pos.y * grid_width + pos.x] = Tile.Tile.ENEMY
				found_pos = true

func place_exit(grid: Array[Tile.Tile], player_room: Rect2i) -> void:
	var found_pos = false
	
	while not found_pos:
		var room: Rect2i = rooms.pick_random()
		if room == player_room:
			continue
		
		var pos = Vector2i(randi_range(room.position.x, room.end.x - 1), randi_range(room.position.y, room.end.y - 1))
		var left = pos + Vector2i.LEFT
		var right = pos + Vector2i.RIGHT
		var up = pos + Vector2i.UP
		var down = pos + Vector2i.DOWN
		if grid[pos.y * grid_width + pos.x] == Tile.Tile.FLOOR \
		and grid[left.y * grid_width + left.x] == Tile.Tile.FLOOR \
		and grid[right.y * grid_width + right.x] == Tile.Tile.FLOOR \
		and grid[up.y * grid_width + up.x] == Tile.Tile.FLOOR \
		and grid[down.y * grid_width + down.x] == Tile.Tile.FLOOR:
			grid[pos.y * grid_width + pos.x] = Tile.Tile.PORTAL
			found_pos = true

func populate_grid(grid: Array[Tile.Tile]) -> void:
	place_lockers(grid)
	var player_room = place_player(grid)
	place_exit(grid, player_room)
	place_enemies(grid, player_room)

func generate_plane(array: Array, pos: Vector3, dim: Array[Vector3], normal: Vector3) -> void:
	var index = len(array[Mesh.ARRAY_VERTEX])
	array[Mesh.ARRAY_VERTEX].append(wall_thickness*pos)
	array[Mesh.ARRAY_VERTEX].append(wall_thickness*(pos+dim[0]))
	array[Mesh.ARRAY_VERTEX].append(wall_thickness*(pos+dim[0]+dim[1]))
	array[Mesh.ARRAY_VERTEX].append(wall_thickness*(pos+dim[1]))
	
	array[Mesh.ARRAY_NORMAL].append(normal)
	array[Mesh.ARRAY_NORMAL].append(normal)
	array[Mesh.ARRAY_NORMAL].append(normal)
	array[Mesh.ARRAY_NORMAL].append(normal)
	
	array[Mesh.ARRAY_INDEX].append(index + 0)
	array[Mesh.ARRAY_INDEX].append(index + 1)
	array[Mesh.ARRAY_INDEX].append(index + 2)
	array[Mesh.ARRAY_INDEX].append(index + 0)
	array[Mesh.ARRAY_INDEX].append(index + 2)
	array[Mesh.ARRAY_INDEX].append(index + 3)
				
func generate_geo(grid: Array[Tile.Tile], array: Array) -> void:
	for y in range(grid_height):
		for x in range(grid_width):
			var tile = grid[y * grid_width + x]
			
			if tile == Tile.Tile.WALL:
				generate_plane(array, Vector3(x, 2, y), [Vector3(0, 0, 1), Vector3(0, -2, 0)], Vector3.LEFT)
				generate_plane(array, Vector3(x, 2, y+1), [Vector3(1, 0, 0), Vector3(0, -2, 0)], Vector3.BACK)
				generate_plane(array, Vector3(x+1, 2, y+1), [Vector3(0, 0, -1), Vector3(0, -2, 0)], Vector3.RIGHT)
				generate_plane(array, Vector3(x+1, 2, y), [Vector3(-1, 0, 0), Vector3(0, -2, 0)], Vector3.FORWARD)
				var wall: Node3D = level_geo.get_node("./Wall").duplicate()
				wall.position = Vector3(x, 0, y)
				add_child(wall)
			else:
				generate_plane(array, Vector3(x, 0, y), [Vector3(1, 0, 0), Vector3(0, 0, 1)], Vector3.UP)
				var floor_tile: Node3D = level_geo.get_node("./Floor").duplicate()
				floor_tile.position = Vector3(x, 0, y)
				add_child(floor_tile)
				
				var ciel_tile: Node3D = level_geo.get_node("./Floor").duplicate()
				ciel_tile.rotate_z(deg_to_rad(180))
				ciel_tile.position = Vector3(x+1, 2, y)
				add_child(ciel_tile)
				
			if tile == Tile.Tile.LOCKER:
				var locker: Node3D = locker_inst.instantiate()
				locker.position = Vector3(x, 0, y)
				add_child(locker)

func _ready() -> void:
	var starting_axis = randi_range(0, 1)
	var min_space = Vector2i(0, 0)
	var max_space = Vector2i(width, height)
	var map = generate_level(starting_axis, min_space, max_space)
	var grid: Array[Tile.Tile] = []
	grid.resize(grid_width * grid_height)
	grid.fill(Tile.Tile.WALL)
	
	var surface_array = []
	surface_array.resize(Mesh.ARRAY_MAX)
	
	surface_array[Mesh.ARRAY_VERTEX] = PackedVector3Array()
	surface_array[Mesh.ARRAY_INDEX] = PackedInt32Array()
	surface_array[Mesh.ARRAY_NORMAL] = PackedVector3Array()
	
	generate_grid(map, grid)
	populate_grid(grid)
	grid_generated.emit(grid, grid_width, grid_height)
	level_geo = wall_inst.instantiate() 
	generate_geo(grid, surface_array)
	
	var mesh = ArrayMesh.new()
	mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, surface_array)
	var tri_mesh = mesh.create_trimesh_shape()
	$NavigationRegion3D/StaticBody3D/CollisionShape3D.shape = tri_mesh
	#$NavigationRegion3D.navigation_mesh = NavigationMesh.new()
	#$NavigationRegion3D.navigation_mesh.radius
	$NavigationRegion3D.bake_navigation_mesh(false)