Remixed Dungeon allows you to create custom levels using JSON configuration files. Custom levels are implemented via the PredesignedLevel class, which reads level data from JSON files.
For custom levels, you can define levels using JSON with integer-based tile representations.
Create levelsDesc/custom_level.json:
{
"width": 16,
"height": 16,
"map": [
36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36,
36, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 36,
36, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 36,
36, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 36,
36, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 36,
36, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 36,
36, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 36,
36, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 36,
36, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 36,
36, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 36,
36, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 36,
36, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 36,
36, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 36,
36, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 36,
36, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 36,
36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36
],
"entrance": [7, 14], // Entrance coordinates [x, y]
"exit": [7, 1], // Exit coordinates [x, y]
"mobs": [
{
"kind": "Rat",
"x": 5,
"y": 8
},
{
"kind": "Gnoll",
"x": 10,
"y": 5
}
],
"items": [
{
"kind": "HealthPotion",
"x": 8,
"y": 8
},
{
"kind": "ScrollOfIdentify",
"x": 7,
"y": 5
}
],
"customTiles": true,
"tiles": "tiles0.png",
"water": "water0.png"
}
The map array uses integers to represent different terrain types:
You can include complex features in your JSON levels:
{
"width": 32,
"height": 32,
"map": [
// Integer array representing the level layout
],
"entrance": [15, 31], // Entrance coordinates [x, y]
"exit": [15, 0], // Exit coordinates [x, y]
// For levels with multiple exits
"multiexit": [
[10, 0],
[20, 0]
],
// Custom tile layers for visual effects
"customTiles": true,
"baseTileVar": [ /* Integer array for base tile variations */ ],
"decoTileVar": [ /* Integer array for decoration tile variations */ ],
"deco2TileVar": [ /* Integer array for secondary decoration tile variations */ ],
"roofBaseTileVar": [ /* Integer array for roof base tile variations */ ],
"roofDecoTileVar": [ /* Integer array for roof decoration tile variations */ ],
// Tileset and water texture files
"tiles": "tiles0.png",
"water": "water0.png",
"tiles_base": "tiles0.png",
"tiles_deco": "tiles0.png",
"tiles_deco2": "tiles0.png",
"tiles_logic": "tiles0.png",
"tiles_mobs": "Mobs.png",
"tiles_roof_base": "tiles0.png",
"tiles_roof_deco": "tiles0.png",
"tiles_objects": "Objects.png",
// Tile descriptions for tooltips
"decoName": [ /* Array of tile names for tooltips */ ],
"decoDesc": [ /* Array of tile descriptions for tooltips */ ],
"mobs": [
{
"kind": "Rat", // Use the internal mob class name
"x": 10,
"y": 10
}
],
"items": [
{
"kind": "HealthPotion", // Use the internal item class name
"x": 15,
"y": 15
}
],
// Complex objects in the level
"objects": [
{
"class": "com.nyrds.pixeldungeon.levels.objects.SignObject",
"x": 5,
"y": 5,
"text": "Welcome to the dungeon!"
}
],
// Level-specific properties
"lighted": true, // Whether the level is fully lit
"boss_level": false, // Whether this is a boss level
"maxBrightness": 1.05, // Maximum brightness level
// Level-specific music (defined in Dungeon.json)
// Music is set in the Dungeon.json level definition, not in the level JSON
// Lua script for level behavior
"script": "scripts/my_level_script.lua"
}
You can add script functionality to your custom levels by defining a script in Dungeon.json:
{
"Levels":{
// Other levels...
"custom_1":{"kind":"PredesignedLevel", "depth":6, "file":"levelsDesc/custom_level.json", "script": "scripts/my_custom_level_script", "music":"ost_prison"}
}
}
Create scripts/my_custom_level_script.lua. This script will be executed when the level loads:
local RPD = require "scripts/lib/commonClasses" -- The script defines a ScriptedActor which can respond to various game events local M = {} -- Called when the actor is added to the level function M.onSpawn(self) RPD.glog("Welcome to my custom level!") end -- Called each turn (very frequently) function M.onTurn(self) -- Example: spawn a random mob every 50 turns if RPD.GameAction and RPD.GameLoop.currentTurn % 50 == 0 and math.random() < 0.3 then local level = RPD.Dungeon.level() local width = level:getWidth() local height = level:getHeight() -- Find a random empty cell local pos = nil for i = 1, 100 do local testX = math.random(1, width - 2) local testY = math.random(1, height - 2) local testCell = level:cell(testX, testY) if level:freeCell(testCell) and level:passable:cell(testCell) then pos = testCell break end end if pos then RPD.spawnMob("Rat", pos) RPD.glog("A rat appears from the shadows!") end end end -- Called when the player steps on a cell (x, y) function M.onCellSelected(self, x, y, user) -- Example: add an effect when player steps on certain tiles local level = RPD.Dungeon.level() local cell = level:cell(x, y) RPD.topEffect(cell, "poison") end -- Other possible functions: -- onAttack(self, attacker, target) -- onCast(self, attacker, target) -- onDie(self, cause) -- onPickup(self, item, holder) -- onUseItem(self, item, holder) return M
Note that scripts can also be associated with individual objects/traps in the level definition:
{
"objects": [
{
"kind": "Trap",
"x": 10,
"y": 10,
"trapKind": "scriptFile",
"script": "scripts/traps/MyCustomTrap",
"uses": 5
}
]
}
For complex visual level design, you can use the Tiled map editor to design your levels visually before converting them to the PredesignedLevel format.
Use special object layers in Tiled to plan game elements:
mobs layer - Place mob spawn points using point objectsitems layer - Place items using point objectsfeatures layer - Place special features like altars, fountainsexits layer - Mark exits and entrancesTiled maps need to be converted to the integer array format used by PredesignedLevel:
map, baseTileVar, decoTileVar, etc.mobs and items arrays in JSON formatCreate a level with switches that open doors:
{
"width": 16,
"height": 16,
"map": [
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4,
4, 1, 4, 1, 4, 1, 4, 1, 1, 4, 1, 4, 1, 4, 1, 4,
4, 1, 4, 1, 4, 1, 4, 1, 1, 4, 1, 4, 1, 4, 1, 4,
4, 1, 4, 1, 4, 1, 4, 1, 1, 4, 1, 4, 1, 4, 1, 4,
4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4,
4, 1, 4, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 4,
4, 1, 4, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 4,
4, 1, 4, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 4,
4, 1, 4, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 4,
4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4,
4, 1, 4, 1, 4, 1, 4, 1, 1, 4, 1, 4, 1, 4, 1, 4,
4, 1, 4, 1, 4, 1, 4, 1, 1, 4, 1, 4, 1, 4, 1, 4,
4, 1, 4, 1, 4, 1, 4, 1, 1, 4, 1, 4, 1, 4, 1, 4,
4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4
],
"entrance": [1, 1],
"exit": [1, 14],
"mobs": [
{
"kind": "Rat",
"x": 7,
"y": 7
}
]
}
Create scripts/puzzle_level.lua and reference it in Dungeon.json:
{
"Levels":{
"puzzle_1":{"kind":"PredesignedLevel", "depth":6, "file":"levelsDesc/puzzle_level.json", "script": "scripts/puzzle_level"}
}
}
local RPD = require "scripts/lib/commonClasses" local M = {} -- Called when the actor is added to the level function M.onSpawn(self) RPD.glog("Welcome to the puzzle level!") end -- Called each turn - check if player stepped on switch function M.onTurn(self) local hero = RPD.Dungeon.hero local heroPos = hero:getPos() local level = RPD.Dungeon.level() local x, y = level:cellToCoord(heroPos) -- Check if player stepped on switch positions (represented as specific floor tiles) if level:map(heroPos) == 8 and x == 3 and y == 2 then -- Specific switch tile at 3,2 -- Change door at 3,7 from door (4) to floor (1) level:set(level:cell(3, 7), 1) RPD.glog("First door opened!") RPD.topEffect(level:cell(3, 7), "poison") elseif level:map(heroPos) == 8 and x == 5 and y == 2 then -- Switch at 5,2 level:set(level:cell(5, 7), 1) -- Open corresponding door RPD.glog("Second door opened!") RPD.topEffect(level:cell(5, 7), "poison") elseif level:map(heroPos) == 8 and x == 7 and y == 2 then -- Switch at 7,2 level:set(level:cell(7, 7), 1) -- Open corresponding door RPD.glog("Center door opened!") RPD.topEffect(level:cell(7, 7), "poison") end end return M
Create a level that generates content dynamically:
{
"width": 24,
"height": 24,
"map": [
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4,
4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4,
4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4,
4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4,
4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4,
4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4,
4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4,
4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4,
4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4,
4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4,
4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4,
4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4,
4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4,
4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4,
4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4,
4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4,
4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4,
4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4,
4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4,
4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4,
4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4,
4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4,
4, 4, 4, 4, 4, 4, 4, 7, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4
],
"entrance": [11, 23],
"multiexit": [],
"mobs": [
{
"kind": "Rat",
"x": 5,
"y": 5
}
]
}
Create scripts/arena_level.lua and reference it in Dungeon.json:
{
"Levels":{
"arena_1":{"kind":"PredesignedLevel", "depth":7, "file":"levelsDesc/arena_level.json", "script": "scripts/arena_level"}
}
}
local RPD = require "scripts/lib/commonClasses" local M = {} local wave = 0 -- Called when the actor is added to the level function M.onSpawn(self) wave = 1 RPD.glog("The arena gates close behind you! Wave " .. wave .. " approaches...") M.spawnWave() end -- Called each turn function M.onTurn(self) local level = RPD.Dungeon.level() local allMobs = level:mobs() -- Count alive mobs excluding the player local alive = 0 for i = 0, allMobs:size()-1 do local mob = allMobs:get(i) if mob:isAlive() and mob ~= RPD.Dungeon.hero then alive = alive + 1 end end if alive == 0 then wave = wave + 1 if wave <= 5 then RPD.glog("Wave " .. wave .. " begins!") M.spawnWave() else RPD.glog("You have survived the arena!") -- Change exit tile to be accessible (if needed) end end end function M.spawnWave() local enemyClasses = {"Rat", "Gnoll", "Crab"} local class = enemyClasses[wave % #enemyClasses + 1] local level = RPD.Dungeon.level() local width = level:getWidth() local height = level:getHeight() -- Spawn multiple enemies based on wave for i = 1, wave * 2 do local pos = nil for j = 1, 100 do -- Try up to 100 times to find an empty cell local testX = math.random(1, width - 2) local testY = math.random(1, height - 2) local testCell = level:cell(testX, testY) if level:freeCell(testCell) and level:passable:cell(testCell) then pos = testCell break end end if pos then RPD.spawnMob(class, pos) end end RPD.glog("Enemies pour into the arena!") end return M
To make your custom levels appear in the dungeon progression, modify levelsDesc/Dungeon.json. Each level is defined with a kind property and a unique ID:
{
"Levels":{
"0":{"kind":"SewerLevel", "depth":0, "size":[0,0]},
"town_2":{"kind":"PredesignedLevel", "depth":0, "file":"levelsDesc/Town_2021_03.json", "noFogOfWar":"true", "isSafe":true, "music":"ost_town_1","script": "scripts/actors/town/Compass","maxBrightness":1.05},
"1":{"kind":"SewerLevel", "depth":1, "size":[24,24], "music":"ost_sewer"},
"2":{"kind":"SewerLevel", "depth":2, "size":[24,24], "music":"ost_sewer"},
"3":{"kind":"SewerLevel", "depth":3, "size":[24,24], "music":"ost_sewer"},
"4":{"kind":"SewerLevel", "depth":4, "size":[24,24], "music":"ost_sewer"},
"5":{"kind":"SewerBossLevel", "depth":5, "size":[32,32], "music":"ost_boss_1_ambient", "fallbackMusic":"ost_boss_ambient"},
// Add your custom level
"custom_1":{"kind":"PredesignedLevel", "depth":6, "file":"levelsDesc/custom_level.json", "music":"ost_prison"}
},
"Graph":{
"0":[["town_2"],[]],
"town_2":[["1"],["0"]],
"1":[["2"],["town_2"]],
"2":[["3"],["1"]],
"3":[["4"],["2"]],
"4":[["5"],["3"]],
"5":[["custom_1"],["4"]], // Connect to your custom level
"custom_1":[["6"],["5"]], // Connect to next level
"6":[["7"],["custom_1"]],
// ... continue the progression
},
"Entrance":"0"
}
The “Graph” section defines how levels connect to each other. Each entry has the format:
Custom levels greatly expand the possibilities for Remixed Dungeon mods. With the PredesignedLevel system, you can create unique challenges, puzzles, and experiences for players!