Recursed

Recursed

30 ratings
Creating custom levels
By Portponky
A quick guide from the developer on how to make your own levels.
   
Award
Favorite
Favorited
Unfavorite
Overview
Levels in Recursed are stored as simple Lua scripts. They can be created and edited with a text editor, and you do not need detailed knowledge of Lua to edit them. Cursory knowledge of scripting, text editing and files/directories is helpful.

Tip: This guide is full of spoilers!
Creating a simple level
A simple level can be made by adding a script to the game's custom missions directory.

Right click on Recursed in your Steam library, then click properties. Navigate to BROWSE LOCAL FILES... and click it to open the Recursed install folder.

BROWSE LOCAL FILES

From here, you should navigate to the custom missions directory.
  • If you are on MacOS, you need to go into the Recursed.app package and find the custom missions folder under Recursed.app / Contents / Resources / custom / missions
  • If you are on Windows or Linux, the data folder should just be under custom / missions
Inside the custom missions folder, create a text file and give it a name, such as mylevel.lua. The extension .lua is a requirement.

In the text file, copy/paste this starting script.

local wip = { o = "solid", ["-"] = "ledge", w = "water", a = "acid", b = "buoy" } function start() ApplyTiles(wip, 0, 0, [[ oooooooooooooooooooo o..................o o..................o o..................o o..................o o..................o o..................o o..................o o..................o o..................o o..................o o..................o o..................o oooooooooooooooooooo oooooooooooooooooooo ]]) Spawn("player", 6, 12) Spawn("crystal", 14, 8.5) end
Playing the level
Playing the level is easy.

If you start the game, and enter the Nexus (the zone select chest accessible from the top right of the first set of levels), a new jar will be available. In this jar you will find your custom missions.



Tip: the Nexus will always be open when custom levels are present!
Explanation of the level script
The level script above is very simple. It's just an empty room with the player and a crystal. Let's go through how it works.

local wip = { o = "solid", ["-"] = "ledge", w = "water", a = "acid", b = "buoy" }

The graphics and behavior for the levels are defined by tiles. If no tiles are specified for a level, it'll use the 'wip' (work in progress) tiles. These are declared in a file called wip.lua in the tiles directory. It declares the following tiles:
  • solid - can not be passed through.
  • ledge - the top edge of this tile will block the player.
  • water - tile of water, chests entered in this tile will be flooded.
  • acid - the acid from the ruins levels.
  • buoy - another solid tile, used to mark out floating blocks in flooded levels.
  • glitch - another solid tile, used to style paradox areas and glitch areas.
  • ledgewet - looks like an underwater ledge, but is solid so that items behave well in flooded conditions.
The first line of the script makes a table of five useful tiles. It maps them to single character names. This is so that the level can be drawn in an ascii-art style. Note that Lua requires special syntax of the form ["#"] when creating table entries with non-alphabetic names (e.g. the ledge tile is mapped to a minus character in this manner).

function start() ... end

The next part of the script is a function called start. Each room is a function, and the level always starts in a room called start.

There are three functions which can be called when creating a room.
  • ApplyTiles takes a tile mapping table (e.g. wip), and uses it to turn a grid of characters into tiles in the level. It also takes coordinates, although it's often used with coordinates 0,0.
  • Spawn is called to create objects. In our example, it is called twice to create the player and the crystal. This function takes the object type (string) and a coordinate.
  • Global is the same as Spawn, but the object will have the green enchantment on it. Not all objects will spawn correctly with Global.

Note that ApplyTiles uses an uncommon Lua syntax for creating multiline strings. Try to avoid mapping tiles to the characters [ and ] as they are the delimiters used by Lua. Make sure the [[ delimiter does not have a space character after it, as it will assume that is the first row of the level.

Tip: Any character that isn't mapped will have no effect on the level. In this example I've used periods.

Adding another room
The example level is too simple. Let's make it do something a little more complex.

Add the following line to the spawns section of the start room:

Spawn("chest", 10, 12.5, "pool")

This will add a chest that will try to access a new room called 'pool'. This level could have a pool of water in it. Here is an example if you want it, but I encourage you to try your own.

function pool() ApplyTiles(wip, 0, 0, [[ oooooooooooooooooooo o..................o o..................o o..................o o..................o o..................o o..................o o..................o o..................o ooooooooowwwwwwwwwwo ooooooooowwwwwwwwwwo ooooooooowwwwwwwwwwo ooooooooowwwwwwwwwwo oooooooooooooooooooo oooooooooooooooooooo ]]) Spawn("player", 6, 8) end
Adding a floodable room
When a room floods, it gets run in a slightly different way. A variable is passed to the function which evaluates to true if the chest was underwater. How the room is built can then be adapted to give the room a "flooded" appearance.

Tip: There is no way to determine if the chest is flooded with acid or water, as I felt it would be a little unclear to the player to have both liquids in play at the same time.

If the parameter is missing, or the room does not act on the parameter, the room will not flood when opened underwater.

Here's an example floodable room. Try adding it back into the same level to create a simple puzzle: take one chest inside the pool and flood it to swim to the crystal.

Tip: You can make multiple calls to ApplyTiles. It might be easier to make the level layout and then place the water on top of it.

function bucket(wet) if wet then ApplyTiles(wip, 0, 0, [[ oooooooooooooooooooo owwwwwwwwwwwwwwwwwwo owwwwwwwwwwwwwwwwwwo owwwwwwwwwwwwwwwwwwo owwwwwwwwwwwwwwwwwwo owwwwwwwwwwwwooowwwo owwwwwwwwwwwwwwwwwwo owwwwwwwwwwwwwwwwwwo owwwwwwwwwwwwwwwwwwo owwwwwwwwwwwwwwwwwwo owwwwwwwwwwwwwwwwwwo owwwwwwwwwwwwwwwwwwo owwwwwwwwwwwwwwwwwwo oooooooooooooooooooo oooooooooooooooooooo ]]) else ApplyTiles(wip, 0, 0, [[ oooooooooooooooooooo o..................o o..................o o..................o o..................o o............ooo...o o..................o o..................o o..................o o..................o o..................o o..................o o..................o oooooooooooooooooooo oooooooooooooooooooo ]]) end Spawn("player", 6, 8) Spawn("crystal", 14.5, 2.5) end
Objects
There are a number of different objects that can be placed. Here is a reference for them.

Object name
Description
Can be global?
Height above ground
Parameters
player
The player and, in sub-rooms, the pink glow.
No
1
None
box
The grey blocks for the player to stand on.
Yes
0.5
None
key
The standard gold key.
Yes
0.5
None
lock
Locked door.
Yes
1.5
None
chest
A chest to another room (or the same room).
Yes
0.5
Name of the room.
yield
The green glow which creates a jar.
No
1
None
crystal
The purple crystal which ends the level.
No
1.5
None
diamond
The white diamond which ends the level in a paradox zone.
No
1.5
None
record
A narration ring.
No
0.5
The path to the narration resource.
fan
The fans from the paradox zones.
Yes
0.5
None
bird
A restart bird spawn position.
No
1
A table of tables of object ids that must exist for the bird not to spawn.
crux
The energy core from the final puzzle.
No (but it is anyway)
0.5
None
generic
Oobleck.
Yes
0.5
None
Special room names
There are three special room names.

Name
Use case
start
The room the level starts in.
reject
The room the level reboots to if a paradox occurs.
glitch
The room that is pushed when a jar which doesn't lead anywhere is created via oobleck.

These rooms are still just rooms like any others. You can make a chest which recurses back to the start room, for example.
Making levels stylish
After you have designed a good level with the wip tiles, the next step is to make it look pretty and colorful. There are two steps to this.

Tiles

The wip tileset is found in data / tiles / wip.lua and wip.png. You can use other tiles from the game, or tiles you create yourself in the same manner. To use alternate tiles you need to do the following steps:
  • Select or create a tile image/script.
  • Add mappings for the tiles you want to use, assigning them to single characters. You can use multiple mappings.
  • Make sure ApplyTiles uses the right mappings.
  • At the top level of the script, the tiles variable must be set to the name of the script file. This is "tiles/wip" by default.

Background

The background can be set by adding three additional variables to the script.
  • pattern should be set to the location of the background image. The background image is a vertical list of square images, the first of which is the light/dark pattern and the rest are patterns which are tiled in the background. Only the red channel of this image is used.
  • dark gives the RGB values (in normalized form) for the dark areas of the background.
  • light gives the RGB values of the light areas.

To give an example, here is the script for the first level in the game. Many more examples can be found by examining other level scripts.

Tip: It's useful to map all the tiles, even if you don't use them all on this level.

-- First level. Basic stacking and crystal collection. local cave = { ["7"] = "box_ul", ["8"] = "box_u", ["9"] = "box_ur", ["4"] = "box_l", ["o"] = "box_c1", ["O"] = "box_c2", ["6"] = "box_r", ["1"] = "box_dl", ["2"] = "box_d", ["3"] = "box_dr", ["u"] = "stal_cap", ["|"] = "stal_c", ["v"] = "stal_d", ["x"] = "block" } function start() ApplyTiles(cave, 0, 0, [[ |v|v||.|v.v.v|v||.|| |.v.|v.v.....v.|v.v| u...v..........v...| 9..................u 6..................7 6..................4 6..................4 3..................1 9..................7 6..................4 6.....78897889.....4 3.....4oo64oO6.....4 9...794oO64Oo6..7891 37894612234Oo6xx4o67 94o64679791223791234 ]]) Spawn("player", 2.5, 9) Spawn("box", 6.5, 9) Spawn("box", 11.5, 9) Spawn("box", 11.9, 8) Spawn("crystal", 10, 2.75) Spawn("record", 15, 12.5, "sounds/voices/c1") end tiles = "tiles/cave" pattern = "backgrounds/checker" dark = { 0.17, 0.19, 0.48 } light = { 0.22, 0.24, 0.62 }
Level design hints
  • The player can jump three blocks high, two blocks when holding something.
  • The player is two blocks high, but every item can fit into a single block space.
  • If a level has a green chest (or green oobleck) it is very likely that a reject room will be needed.
  • If a level has oobleck and jars, a glitch room will be needed.
  • When adding water under an overhang, the water surface effect won't appear if the overhang is placed before the water is placed.
  • The key/lock, box and water mechanics are all very similar and can often be interchanged to fix design problems.
  • Keep the levels spacious, rather than cluttered.
  • Keep the backgrounds muted so that puzzle elements are clearly visible.
  • As the levels are scripts, standard Lua syntax such as loops and conditions can be used.
  • If the room doesn't appear and the game just shows the background, it's because the room doesn't exist or the player is stuck in a wall.
Adding the levels to the nexus
The game runs a script called nexus.lua (or nexus-xx.lua for other languages) to populate the menu. This script can call other scripts, which is how the update missions are added.

The menu script contains functions for each screen. The functions add various 'pegs' which can be chests, back buttons, books, etc. An example line from the script is:

Add(4, 7.5, Peg.Level, 0, "missions/basic1", "Entryway")

This adds a level (chest) at coordinates 4, 7.5 (of the screen size 20, 15). It needs 0 collected crystals to unlock. It runs the level "missions/basic1" and it shows up with the name "Entryway" in the game.

Each individual script tracks crystals and diamonds separately.
Conclusion
If you have any questions or comments, or you have any cool levels to show, please post them in the Recursed discussion forum.
7 Comments
CreepcrEePBOOOM Dec 29, 2021 @ 3:36pm 
And quick question: What is the license on Recursed?
CreepcrEePBOOOM Dec 27, 2021 @ 1:32pm 
Is it possible for custom levels?
CreepcrEePBOOOM Dec 27, 2021 @ 1:28pm 
How do you make an object have the Green Stuff?
Portponky  [author] Aug 14, 2020 @ 4:25pm 
Unfortunately not, that would be well beyond the time and effort I can dedicate to game development these days. Sorry!
DanielisGamer Aug 13, 2020 @ 7:36am 
is there gonna be an in-game level editor or is it always going to be used this way?
Portponky  [author] Jan 1, 2019 @ 4:51am 
Yes it is, use the item "cauldron" for a cauldron, which works just like a chest. The invalid areas start from a room called 'threadless' with the new tile type Tile.Sticky. The light and dark background colors become a map based on origin rooms. Check out any of the files named data/missions/addon/library* for examples.
gigawatts Dec 31, 2018 @ 11:32pm 
Is it possible to create custom levels with cauldrons?