Custom Floor Generation Steps

From PMDOWiki

Sometimes, you may find that you want to add a feature to dungeon generation that just isn't possible with built in gen steps and tools. When this happens, you can script the generation step or feature in yourself by scripting via Lua by creating a FLOOR_GEN_SCRIPT in event_mapgen.lua. We'll walk through an example on how this was done for Halcyon's Illuminant Riverbed to show an example of how you can do this and some of the useful Lua functions you might need to do this.


Depending on what exactly you're trying to achieve with your custom gen step, you may want to start with the vanilla gen steps to get a base to work off of, or you may want to start with the Lua customization first. Either way, once you're ready to sit down and work with the Lua scripting, there's some basic functions/knowledge you'll need to know. There are more functions beyond these, but these are all we need for creating a river. You may find additional functions and tools to do things such as creating traps, items, or NPCs in the FLOOR_GEN_SCRIPT.Test function in event_mapgen.lua.

The function that defines your custom gen function should be given two parameters: map and args.

  • Map is the floor you're on that you're generating. It includes a number of useful functions and variables such as:
    • map.Width: Width of the map
    • map.Height: Height of the map
    • map.Rand:Next(x, y) - returns a random number, inclusive on lower and exclusive on upper, between x and y. This is important to use instead of math.random so that the dungeon's random seeding is used to generate the random number so replays and such are consistent.
    • map:GetTile(loc) - gets the tile at the specified RogueElements.Loc(x,y).
    • map:TrySetTile(loc, terrain) - attempts to set the tile at the specified location to the specified terrain type. May fail if the tile should not be changed for some reason (for example, it is the loc of the stairs, or is out of bounds)
  • Args is a list of arguments given in the Arg table in the ScriptGenStep. Using these, you can write somewhat generic generation algorithms that can be tweaked on a per floor basis by giving different arguments to the function via the genstep's arg table depending on what floor you're on. For example, if I wanted the river to be bigger on later floors, I could supply an Arg called RiverSize that the function could use by grabbing it from the Args table.


Now, you'll need to figure out a way to programmatically define your generation step. In our case, we need to figure out how we want to define what constitutes a river. For what Halcyon wanted, the following attributes were needed for the river:

  • It takes up about half the map, with its center being around the middle of the dungeon's X axis.
  • It flows vertically.
  • Its border randomly shifts left and right a few tiles every so often, but not so far as to shift too far from the center.
  • Its border never shifts 2 water tiles at a time, or else it may look jagged and strange.
  • The left and right borders work independently of each other.
  • It doesn't override ground tiles, only wall tiles. This way rooms and hallways are not changed into water.

And with that, we use this and our knowledge of the Lua functions to define an algorithm in event_mapgen.lua that will change wall tiles near the middle of the map into water tiles to simulate a river. Below is some Lua that'll do just that, but you'll need to write something different if you want to do something else:

--Halcyon custom map gen steps
--used for making the river in the Illuminant Riverbed
function FLOOR_GEN_SCRIPT.CreateRiver(map, args)
	local mapCenter = math.ceil(map.Width / 2)
	local randomOffset = map.Rand:Next(-2,3) --a random small offset added to all tiles to help randomize where the river falls a bit 
	local leftBound = math.floor(mapCenter / 2) + randomOffset --base left bound 
	local rightBound = math.ceil(mapCenter # 3 / 2) + randomOffset -- base right bound
	local leftOffset = map.Rand:Next(-1, 2)
	local rightOffset = map.Rand:Next(-1, 2)
	local leftShore = 0
	local rightShore = 0
	
	local leftOffsetRemaining = map.Rand:Next(1, 5)--how many times this specific offset can be used before being regenerated 
	local rightOffsetRemaining = map.Rand:Next(1, 5)
	
	
	
	--go row by row. Replace ground tiles towards the center of the map with water tiles to create a river flowing through the dungeon.
	--Ground tiles will remain untouched. River will ebb a bit side to side within a limit.
	
	for y = 0, map.Height-1, 1 do 
		
		--determine starting and ending positions for row of river
		--an offset will last for a few rows before trying to roll again for a new offset
		
		--roll new offsets and set new offset timer 
		--NOTE: map.Rand:Next(lower, upper) is inclusive on lower, and exclusive on upper 
		if leftOffsetRemaining <= 0 then
			if leftOffset < 0 then
				leftOffset = map.Rand:Next(-1, 1)
			elseif leftOffset > 0 then 
				leftOffset = map.Rand:Next(0, 2)
			else 
				leftOffset = map.Rand:Next(-1, 2)
			end
			leftOffsetRemaining = map.Rand:Next(1, 5)
		end
		
		if rightOffsetRemaining <= 0 then
			if rightOffset < 0 then
				rightOffset = map.Rand:Next(-1, 1)
			elseif rightOffset > 0 then 
				rightOffset = map.Rand:Next(0, 2)
			else 
				rightOffset = map.Rand:Next(-1, 2)
			end
			rightOffsetRemaining = map.Rand:Next(1, 5)
		end
		
		leftShore = leftBound + leftOffset
		rightShore = rightBound + rightOffset
				
		--set all non ground tiles to water tiles between our left and right bounds 
		for x = leftShore, rightShore, 1 do 
			local loc = RogueElements.Loc(x, y)
			if not map:GetTile(loc):TileEquivalent(map.RoomTerrain) then
				map:TrySetTile(loc, RogueEssence.Dungeon.Tile("water"))
			end
	
	
		end 
		
		leftOffsetRemaining = leftOffsetRemaining - 1
		rightOffsetRemaining = rightOffsetRemaining - 1
		
	end 
	
	
end

Now that we have our function defined, we need to actually add it to the dungeon floor! This is done by adding it to the gen steps for the floor in the dungeon setup. Where you place it in orderwise depends on what your genstep does, but in our case, it's something that should be ran after all other terrain generation has finished, so we'll put it at priority 4 here. For the Script area, we write the name of our script (in this case, CreateRiver), and define any arguments we may have (in this case, none).

Then, we just load up our floor, and voila! We have a simple but decent looking river now flowing through our dungeon floor!