extends Node3D class_name LevelGenerator var levelGrid: Array[Array] @export var gridSize: int = 100 @export var mapLogic: MapLogic @export var doorBlock: PackedScene # Temporary @export var doorOBJ: PackedScene #Temporary @export var tile: PackedScene # Temporary @export var tileWall: PackedScene # Temporary @export var tileCorner: PackedScene # Temporary @export var levelGenSeed: String = "default" var rng: RandomNumberGenerator = RandomNumberGenerator.new() #Get random values, usefull for random level generation huh var currentMission: Mission var doorSpawnPoints: Array[DoorPosition] #Where to put them doors var doorBlockSpawnPoints: Array[DoorPosition] #Where to put them door blockers var biomeExitPositions: Array[BiomeExit] #Points where you can leave a biome var biomeExitChecker: RoomData = RoomData.new(preload("res://Maps/MapGenerator/BiomeExitChecker.png").get_image(),"null") #Nonexisten Room used to check if a door is a valid Biome Exit var connectPathPositions: PackedVector2Array #Where to place floor tiles of connecting paths var astar: AStar2D = AStar2D.new() func _ready() -> void: initRandom() # Set seed for level generation initGrid() #Assign a new grid space object to each space of the 1x1m grid #Center the world position.x = -gridSize/2 position.z = -gridSize/2 currentMission = RescueMission.new() #Have something choose the mission type in this part later #Biomes var doorList: Array[DoorPosition] #Seccond Array contains door positions for bPrio in currentMission.biomes.size(): doorList = placeBiomes(currentMission.biomes[bPrio]) spreadBiomes() #Rooms into Biomes placeRooms(doorList) #Doors chooseBiomeExits() #print(biomeExitPositions[0].biome,biomeExitPositions[1].biome)r doorsAtBiomeExits() findValidDoors() spawnDoors() #Generate Astar setup for finding Paths between Biomes and Rooms generateAstarPoints() connectAstarPoints() weightAstarPoints() #Connect Biomes connectPathPositions = generateConnectionPath() generateFloorPositions() placeFloorPositionTiles() placeWallsAlongTiles() print("done") func initRandom() -> void: rng.set_seed(hash(levelGenSeed)) #Misc Utility func initGrid() -> void: for x in gridSize: var newRow: Array levelGrid.push_back(newRow) for y in gridSize: var newCell := GridCell.new() newCell.position = Vector2i(x,y) levelGrid[x].push_back(newCell) func addGridCells(cell1: GridCell,cell2: GridCell) -> GridCell: var returnCell: GridCell = GridCell.new() returnCell.spaceTaken = cell1.spaceTaken or cell2.spaceTaken returnCell.door = cell1.door or cell2.door if cell1.door and !cell2.door: returnCell.doorOrientation = cell1.doorOrientation else: returnCell.doorOrientation = cell2.doorOrientation returnCell.position = cell1.position returnCell.biome = cell1.biome returnCell.biomeConnection = cell1.biomeConnection return returnCell func shuffleArray(array: Array) -> Array: for i in array.size()-1: #Get Array indexes var iA: int = array.size()-1-i var iB: int = rng.randi_range(0,array.size()-2-i) #Get two elements from the array var a = array[iA] var b = array[iB] #Swap both elements array[iB] = a array[iA] = b return array func addArrays2D(array1: Array[Array], array2: Array[Array], arr2pos: Vector2i = Vector2i(0,0)) -> void: if array1.size() <= (arr2pos.x) + array2.size(): return if array1[arr2pos.y].size() <= (arr2pos.y) + array2[0].size(): return for x in array2.size(): for y in array2[x].size(): array1[x+arr2pos.x][y+arr2pos.y] = addGridCells(array1[x+arr2pos.x][y+arr2pos.y],array2[x][y]) func checkOverlapArrays2D(array1: Array[Array], array2: Array[Array], arr2pos: Vector2i = Vector2i(0,0)) -> bool: if array1.size() <= arr2pos.x + array2.size() or arr2pos.x < 0: return true if array1[arr2pos.y].size() <= arr2pos.y + array2[0].size() or arr2pos.y < 0: return true for x in array2.size(): for y in array2[x].size(): if array1[x+arr2pos.x][y+arr2pos.y].spaceTaken and array2[x][y].spaceTaken: return true return false func checkBiomeOverlap(biome: String, pos: Vector2i, length: int, height: int) -> bool: if levelGrid.size() <= pos.x + length or pos.x < 0: return true if levelGrid[pos.y].size() <= pos.y + height or pos.y < 0: return true for x in length: for y in height: if levelGrid[x+pos.x][y+pos.y].biome: return true return false func rotateArray2D(array: Array[Array], numberOfRotationsBy90Degrees: int) -> void: var size: int = array.size() var layerCount: int = size/2 for x in numberOfRotationsBy90Degrees%4: for layer in layerCount: var first: int = layer var last: int = size - first - 1 for element in range(first, last): var offset = element - first var top = array[first][element] var right = array[element][last] var bot = array[last][last-offset] var left = array[last-offset][first] array[element][last] = top array[last][last-offset] = right array[last-offset][first] = bot array[first][element] = left func getDoorPosition(cell: GridCell, GridCellPosition: Vector2i) -> DoorPosition: if !cell.door: print("Tried to get door at cell that isnt a door") return null var returnDoorPosition := DoorPosition.new() returnDoorPosition.pos = GridCellPosition returnDoorPosition.orientation = cell.doorOrientation return returnDoorPosition func addObject(AddedObject:PackedScene, Parent: Node3D, Position: Vector3, Rotation: Vector3= Vector3(0,0,0)): if !AddedObject: print("tried to add object but packed scene is null") return var obj = AddedObject.instantiate() Parent.add_child(obj) obj.position = Position obj.rotation = Rotation return obj func changeBiomeArea2D(biome: String,pos:Vector2i,length: int,height: int): for x in length: for y in height: levelGrid[pos.x+x][pos.y+y].biome = biome #Spawning Rooms func rotateRoomData(roomData: RoomData, numberOfRotationsBy90Degrees: int) -> void: rotateArray2D(roomData.roomGrid,numberOfRotationsBy90Degrees) roomData.rotations = wrapi(roomData.rotations + numberOfRotationsBy90Degrees,0,4) for door in roomData.doorPositions: door.rotatePosRight(roomData.roomGrid.size(),numberOfRotationsBy90Degrees) roomData.roomGrid[door.pos.x][door.pos.y].doorOrientation = wrapi(door.orientation-2,0,4) func spawnRoom(roomDataInput: RoomData,pos: Vector2i,numberOfRotationsBy90Degrees: int = 0, centered: bool = false) -> Array[DoorPosition]: #Pos corresponds to the upper left corner of the room immage var roomData: RoomData = roomDataInput.duplicateRoom() var roomScene: PackedScene = load(roomData.roomSceneRef) rotateRoomData(roomData,numberOfRotationsBy90Degrees) var roomGrid := roomData.roomGrid var doorPositions: Array[DoorPosition] = roomData.doorPositions if centered: pos = Vector2i(pos.x-(roomGrid.size()/2),pos.y-(roomGrid[0].size()/2)) addArrays2D(levelGrid,roomGrid,pos) addObject(roomScene,self,Vector3(pos.x+(roomGrid.size()/2),0,pos.y+(roomGrid[0].size()/2)),Vector3(0,(roomData.rotations%4)*PI/2,0)) for door in doorPositions: door.pos = pos + door.pos return doorPositions func getDoorFromRoom(roomData: RoomData,index: int) -> DoorPosition: return roomData.doorPositions[index % roomData.doorPositions.size()] func putRoomAtDoor(roomData: RoomData, door: DoorPosition,spawnDoorIndex: int) -> Array: ##Return array contains roomData, spawn Pos in that order #Init Values: var spawnDoor: DoorPosition = getDoorFromRoom(roomData,spawnDoorIndex) #Which door from the spawned room connects to the exiting room var doorOrientationDifference : int = wrapi(spawnDoor.orientation - door.orientation,0,4) #Difference in orinet. between spawned and existing room var numberOfRoomRotations: int = 0 #How many rotations are needed to make spawned rooms spawn door face the existing door var spawnPos: Vector2i var doorOffset: Vector2i #Offset by 1 space so the doors are next to eachother and not inside eachoter #Find number of rotations and then rotate the room if !doorOrientationDifference == 2: if doorOrientationDifference == 1: numberOfRoomRotations = 3 if doorOrientationDifference == 0: numberOfRoomRotations = 2 if doorOrientationDifference == 3: numberOfRoomRotations = 1 rotateRoomData(roomData,numberOfRoomRotations) #Get new values for spawnDoor and set door offset spawnDoor = getDoorFromRoom(roomData,spawnDoorIndex) match spawnDoor.orientation: 0: doorOffset = Vector2i(0,-1) 1: doorOffset = Vector2i(1,0) 2: doorOffset = Vector2i(0,1) 3: doorOffset = Vector2i(-1,0) #Set spawn pos and return values spawnPos = door.pos - spawnDoor.pos + doorOffset var returnArray: Array = [roomData,spawnPos] return returnArray func spawnRoomAtDoor(roomDataInput: Array[RoomData], door: DoorPosition, biome: String) -> Array[DoorPosition]: var returnArray: Array[DoorPosition] var roomData: RoomData = null var spawnDoorIndex: int = -1 #Choose the room to use var rooms: Array[RoomData] = roomDataInput.duplicate() shuffleArray(rooms) for room in rooms: #Randomise door checking order var doorIndices: Array[int] for x in room.doorPositions.size(): doorIndices.push_back(x) shuffleArray(doorIndices) #Check if doorIndex fits and assign roomData and spawnDoorIndex if it does for doorIndex in doorIndices: if checkIfRoomFits(room,door,doorIndex, biome): roomData = room.duplicateRoom() spawnDoorIndex = doorIndex break #Check if a room has been found if !roomData or spawnDoorIndex == -1: var checkPosition = door.pos match door.orientation: 0: checkPosition += Vector2i(-1,-2) 1: checkPosition += Vector2i(0,-1) 2: checkPosition += Vector2i(-1,0) 3: checkPosition += Vector2i(-2,-1) if !checkOverlapArrays2D(levelGrid,biomeExitChecker.roomGrid,checkPosition): biomeExitPositions.push_back(BiomeExit.new(door.pos,door.orientation,biome)) return returnArray #Get Spawn Info and spawn room var spawnInfo: Array = putRoomAtDoor(roomData,door,spawnDoorIndex) spawnRoom(spawnInfo[0],spawnInfo[1]) returnArray = spawnInfo[0].doorPositions for x in returnArray.size(): returnArray[x].pos = spawnInfo[1] + returnArray[x].pos return returnArray func checkIfBiomeFits(length: int, height: int, pos: Vector2i, biome: String) -> bool: if levelGrid.size() <= pos.x + length or pos.x < 0: return true if levelGrid[pos.y].size() <= pos.y + height or pos.y < 0: return true for x in length: for y in height: if levelGrid[x+pos.x][y+pos.y].biome != biome: return false return true func checkIfRoomFits(roomDataInput: RoomData,door: DoorPosition,spawnDoorIndex: int, biome: String) -> bool: var roomData: RoomData = roomDataInput.duplicateRoom() var spawnInfo: Array = putRoomAtDoor(roomData,door,spawnDoorIndex) if !checkOverlapArrays2D(levelGrid,spawnInfo[0].roomGrid,spawnInfo[1]) and checkIfBiomeFits(spawnInfo[0].roomGrid.size(),spawnInfo[0].roomGrid[0].size(),spawnInfo[1],biome): return true else: return false func isOutOfArray2DBounds(sizeX: int, sizeY: int, pos: Vector2i) -> bool: if pos.x >= sizeX or pos.x < 0 or pos.y >= sizeY or pos.y < 0: return true else: return false func isValidDoor(pos:Vector2i) -> bool: # Checks if the door connects to another door using its orientation, #if this function finds a valid door, it removes the door status from the other door to avoid duplicated doors match levelGrid[pos.x][pos.y].doorOrientation: 0: if isOutOfArray2DBounds(levelGrid.size(),levelGrid[0].size(),Vector2i(pos.x,pos.y-1)): return false if !levelGrid[pos.x][pos.y-1].door: return false else: levelGrid[pos.x][pos.y-1].door = false 1: if isOutOfArray2DBounds(levelGrid.size(),levelGrid[0].size(),Vector2i(pos.x+1,pos.y)): return false if !levelGrid[pos.x+1][pos.y].door: return false else: levelGrid[pos.x+1][pos.y].door = false 2: if isOutOfArray2DBounds(levelGrid.size(),levelGrid[0].size(),Vector2i(pos.x,pos.y+1)): return false if !levelGrid[pos.x][pos.y+1].door: return false else: levelGrid[pos.x][pos.y+1].door = false 3: if isOutOfArray2DBounds(levelGrid.size(),levelGrid[0].size(),Vector2i(pos.x-1,pos.y)): return false if !levelGrid[pos.x-1][pos.y].door: return false else: levelGrid[pos.x-1][pos.y].door = false return true func findValidDoors() -> void: for x in levelGrid.size(): for y in levelGrid[x].size(): if levelGrid[x][y].door: var newDoorObject := DoorPosition.new() newDoorObject.pos = Vector2i(x,y) newDoorObject.orientation = levelGrid[x][y].doorOrientation if isValidDoor(Vector2i(x,y)): doorSpawnPoints.push_back(newDoorObject) else: doorBlockSpawnPoints.push_back(newDoorObject) func spawnDoors() -> void: for doorSpawn in doorSpawnPoints: addObject(doorOBJ,self,Vector3(doorSpawn.pos.x+0.5,0,doorSpawn.pos.y+0.5),Vector3(0,doorSpawn.orientation*-PI/2,0)) #debugCubeAtPos(doorSpawn.pos) for doorBlockSpawn in doorBlockSpawnPoints: addObject(doorBlock,self,Vector3(doorBlockSpawn.pos.x+0.5,0,doorBlockSpawn.pos.y+0.5),Vector3(0,doorBlockSpawn.orientation*-PI/2,0)) func placeRooms(doorList: Array[DoorPosition]) -> void: var nextDoors: Array[DoorPosition] = [] #YES I KNOW A DO WHILE LOOP IS BETTER HERE BUT GODOT DOESNT HAVE IT for door in doorList: var doorBiome: Biome = getBiome(levelGrid[door.pos.x][door.pos.y].biome) nextDoors += spawnRoomAtDoor(doorBiome.roomList.rooms,door,doorBiome.name) doorList = nextDoors while !doorList.is_empty(): for door in doorList: var doorBiome: Biome = getBiome(levelGrid[door.pos.x][door.pos.y].biome) nextDoors += spawnRoomAtDoor(doorBiome.roomList.rooms,door,doorBiome.name) doorList = nextDoors nextDoors = [] #Generating Biomes func getBiome(biomeName: String) -> Biome: for prio in currentMission.biomes.size(): for biome in currentMission.biomes[prio]: if biome.name == biomeName: return biome return func generateBiomePos(biome: String,starterRoomSize: Vector2i) -> Vector2i: var genPos: Vector2i var possiblePos: Array[Vector2i] #Find all possible Pos without a biome for x in levelGrid.size(): for cell in levelGrid[x]: if !cell.biome: possiblePos.push_back(cell.position) #Go through all pos untill one works genPos = possiblePos[rng.randi_range(0,possiblePos.size()-1)] while checkBiomeOverlap(biome,genPos,starterRoomSize.x,starterRoomSize.y): possiblePos.erase(genPos) genPos = possiblePos[rng.randi_range(0,possiblePos.size()-1)] return genPos func placeBiomes(biomes: Array[Biome]) -> Array[DoorPosition]: var returnDoorLists: Array[DoorPosition] = [] for biome in biomes: var starterRoomSize: Vector2i = Vector2i(biome.starterRoom.roomGrid.size(),biome.starterRoom.roomGrid[0].size()) var generatePos: Vector2i = generateBiomePos(biome.name,starterRoomSize) #mapLogic.playerStartPos = Vector3(generatePos.x-gridSize/2-5,0.5,generatePos.y-gridSize/2-5) changeBiomeArea2D(biome.name,generatePos,starterRoomSize.x,starterRoomSize.y) #Spawn Starter room and add doors to list var doorList: Array[DoorPosition] doorList = spawnRoom(biome.starterRoom,generatePos) returnDoorLists += doorList return returnDoorLists func fillCell(cellBiome: String,pos: Vector2i) -> GridCell: if isOutOfArray2DBounds(levelGrid.size(),levelGrid[0].size(),pos): return if !levelGrid[pos.x][pos.y].biome: levelGrid[pos.x][pos.y].biome = cellBiome return levelGrid[pos.x][pos.y] return func spreadCell(cell: GridCell,pos: Vector2i) -> Array[GridCell]: var returnCells: Array[GridCell] = [] returnCells.push_back(fillCell(cell.biome,pos + Vector2i(0,-1))) returnCells.push_back(fillCell(cell.biome,pos + Vector2i(1,0))) returnCells.push_back(fillCell(cell.biome,pos + Vector2i(0,1))) returnCells.push_back(fillCell(cell.biome,pos + Vector2i(-1,0))) while returnCells.has(null): returnCells.erase(null) return returnCells func allCellsHaveBiome() -> bool: for x in levelGrid.size(): for cell in levelGrid[x]: if !cell.biome: return false return true func spreadBiomes() -> void: #Get all current biome grid cells -> Put them in starterCells var startCells: Array[GridCell] = [] for x in levelGrid.size(): for y in levelGrid[x].size(): if levelGrid[x][y].biome: startCells.push_back(levelGrid[x][y]) #Spread them by one -> put the new ones in a new array while !allCellsHaveBiome(): shuffleArray(startCells) var newCells: Array[GridCell] = [] for cell in startCells: if rng.randf() * getBiome(cell.biome).spread > 0.49: newCells += spreadCell(cell,cell.position) else: newCells.push_back(cell) #New cells now contains all that were newly assigned a biome and the ones that didnt spread startCells = newCells #Astar func generateAstarPoints(findAllSpaceTakenCells: bool = false) -> void: var currentID: int = 0 for x in levelGrid.size(): for y in levelGrid[x].size(): if levelGrid[x][y].spaceTaken == findAllSpaceTakenCells: astar.add_point(currentID,Vector2i(x,y)) currentID += 1 func connectAstarPoints() -> void: for point in astar.get_point_ids(): var possibleNeighbors: Array[int] = [point+1,point-1,point-gridSize,point+gridSize] for posNeigh in possibleNeighbors: if astar.has_point(posNeigh): astar.connect_points(point,posNeigh,true) func weightAstarPoints() -> void: for point in astar.get_point_ids(): if astar.get_point_connections(point).size() != 4: astar.set_point_weight_scale(point,10) func posToID(pos: Vector2i) -> int: return gridSize*pos.x + pos.y #Generating Biome Connections func chooseBiomeExits() -> void: var existingBiomes: Array[String] = [] var currentBiomeExits: Array[BiomeExit] = [] var choosenBiomeExits: Array[BiomeExit] = [] # Find What Biomes are Present for x in levelGrid.size(): for y in levelGrid[x].size(): if !existingBiomes.has(levelGrid[x][y].biome): existingBiomes.push_back(levelGrid[x][y].biome) #For each biome choose a number of exit for i in existingBiomes.size(): #Look through all exits for exits belonging to this biome for exit in biomeExitPositions: if exit.biome == existingBiomes[i]: currentBiomeExits.push_back(exit) biomeExitPositions.erase(exit) #For each other biome, choose an exit for n in existingBiomes.size()-1: # Randomly determine an exit var exit: BiomeExit = currentBiomeExits[rng.randi_range(0,currentBiomeExits.size()-1)] #Test if exit was already choosen if !choosenBiomeExits.has(exit): choosenBiomeExits.push_back(exit) currentBiomeExits.clear() #Get rid of all other exits biomeExitPositions = choosenBiomeExits func doorsAtBiomeExits() -> void: for exit in biomeExitPositions: levelGrid[exit.pos.x][exit.pos.y].door = false func generateConnectionPath() -> PackedVector2Array: var returnArray: PackedVector2Array = [] #Look through all exits and find the closest other exit for exit in biomeExitPositions: var closestPoint: Vector2i var closestPointDistance: float = gridSize*gridSize for exit1 in biomeExitPositions: if (exit1.pos - exit.pos).length() < closestPointDistance and exit.biome != exit1.biome: closestPoint = exit1.pos closestPointDistance = (exit1.pos - exit.pos).length() #Connect the exits returnArray += astar.get_point_path(posToID(exit.pos),posToID(closestPoint)) return returnArray #astar.get_point_path(posToID(biomeA.pos),posToID(biomeB.pos)) func addFloorPos(gridCell: GridCell) -> void: if !gridCell.biomeConnection and !gridCell.spaceTaken: connectPathPositions.push_back(gridCell.position) gridCell.biomeConnection = true gridCell.spaceTaken = true func generateFloorPositions(spread: int = 2) -> void: for x in spread: for y in connectPathPositions.size(): var fPos: Vector2i = connectPathPositions[y] levelGrid[fPos.x][fPos.y].spaceTaken = true levelGrid[fPos.x][fPos.y].biomeConnection = true addFloorPos(levelGrid[clampi(fPos.x+1,0,gridSize-1)][fPos.y]) addFloorPos(levelGrid[fPos.x][clampi(fPos.y+1,0,gridSize-1)]) addFloorPos(levelGrid[clampi(fPos.x-1,0,gridSize-1)][fPos.y]) addFloorPos(levelGrid[fPos.x][clampi(fPos.y-1,0,gridSize-1)]) func placeFloorPositionTiles() -> void: for tilePos in connectPathPositions: addObject(tile,self,Vector3(tilePos.x+0.5,0,tilePos.y+0.5)) func spawnWallSegment(pos: Vector2i,rot: int) -> void: addObject(tileWall,self,Vector3(pos.x+0.5,0,pos.y+0.5),Vector3(0,rot*PI/2,0)) func spawnCornerSegment(pos: Vector2i,rot: int) -> void: addObject(tileCorner,self,Vector3(pos.x+0.5,0,pos.y+0.5),Vector3(0,rot*PI/2,0)) func placeWallsAlongTiles() -> void: for x in levelGrid.size(): for y in levelGrid[x].size(): if !levelGrid[x][y].biomeConnection: continue if x+1 != levelGrid.size(): if !levelGrid[x+1][y].spaceTaken: spawnWallSegment(Vector2i(x,y),3) if x != 0: if !levelGrid[x-1][y].spaceTaken: spawnWallSegment(Vector2i(x,y),1) if y+1 != levelGrid.size(): if !levelGrid[x][y+1].spaceTaken: spawnWallSegment(Vector2i(x,y),2) if y != -1: if !levelGrid[x][y-1].spaceTaken: spawnWallSegment(Vector2i(x,y),0) if x+1 != levelGrid.size() and y+1 != levelGrid.size(): if !levelGrid[x+1][y+1].spaceTaken: spawnCornerSegment(Vector2i(x,y),3) if x != 0 and y != 0: if !levelGrid[x-1][y-1].spaceTaken: spawnCornerSegment(Vector2i(x,y),1) if x+1 != levelGrid.size() and y != 0: if !levelGrid[x+1][y-1].spaceTaken: spawnCornerSegment(Vector2i(x,y),0) if y+1 != levelGrid.size() and x != 0: if !levelGrid[x-1][y+1].spaceTaken: spawnCornerSegment(Vector2i(x,y),2) #Debug Functions func printLevelGrid() -> void: for x in levelGrid.size(): for y in levelGrid[x].size(): if levelGrid[x][y].biomeConnection: debugCubeAtPos(Vector2(x,y)) func printBiomeGrid() -> void: for x in levelGrid.size(): for y in levelGrid[x].size(): if levelGrid[x][y].biome == "test": debugCubeAtPos(Vector2(x,y)) func debugCubeAtPos(pos: Vector2): var debugCube: PackedScene = preload("res://test/debugCube.tscn") addObject(debugCube,self,Vector3(pos.x+0.5,0,pos.y+0.5)) func showAstarPoints() -> void: for point in astar.get_point_ids(): var point_pos: Vector2i = astar.get_point_position(point) debugCubeAtPos(point_pos)