====== Creating Custom Levels with JSON ======
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.
===== JSON-Based Level Creation =====
For custom levels, you can define levels using JSON with integer-based tile representations.
==== Basic Level Structure ====
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"
}
==== Map Array Legend ====
The map array uses integers to represent different terrain types:
* 36 - Wall (Patched stone wall in default tileset)
* 1 - Empty floor (Normal floor in default tileset)
* 4 - Door (Closed door in default tileset)
* 7 - Entrance/Exit (Transition in default tileset)
* Other values correspond to specific terrain types in the tileset
==== Advanced Level Features ====
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"
}
==== Level Scripts ====
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
}
]
}
===== Tiled Map Editor Approach =====
For complex visual level design, you can use the Tiled map editor to design your levels visually before converting them to the PredesignedLevel format.
==== Setting Up Tiled ====
* Download Tiled from https://www.mapeditor.org/
* Create a new map with the same dimensions as you plan to use in-game (typically 32x32 for most levels)
* Use tilesets that match the game's art style, or create your own
==== Tileset Creation ====
* Create a tileset image (PNG) with all your terrain tiles
* Each tile should be 16x16 pixels (same as the base game)
* Organize tiles in rows: walls, floors, special features, etc.
==== Object Layers in Tiled ====
Use special object layers in Tiled to plan game elements:
* ''mobs'' layer - Place mob spawn points using point objects
* ''items'' layer - Place items using point objects
* ''features'' layer - Place special features like altars, fountains
* ''exits'' layer - Mark exits and entrances
==== Converting from Tiled to PredesignedLevel ====
Tiled maps need to be converted to the integer array format used by PredesignedLevel:
* After designing your level in Tiled, you'll need to manually convert the tile IDs to match Remixed Dungeon's terrain system
* Each Tiled layer (base, deco, etc.) corresponds to arrays in the JSON: ''map'', ''baseTileVar'', ''decoTileVar'', etc.
* Mob and item positions from Tiled object layers need to be converted to the ''mobs'' and ''items'' arrays in JSON format
* The CSV data from Tiled layers needs to be adjusted to match the expected tile indices
===== Complex Level Examples =====
==== Switch and Door Puzzle Level ====
Create 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
==== Procedural Arena Level ====
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
===== Including Custom Levels in the Dungeon =====
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:
* First array: levels that can be reached from this level
* Second array: levels that lead to this level
===== Testing and Debugging =====
==== Common Testing Steps ====
* Enable your mod in-game
* Start a new game to access your custom levels
* Verify that all level elements (mobs, items, terrain) spawn correctly
* Test all scripted behaviors
* Check for pathfinding issues (ensure mobs and player can navigate properly)
==== Debugging Tips ====
* Use the game log to trace script execution
* Verify that coordinates in JSON are within level boundaries
* Check that all referenced files (mobs, items) exist
* Ensure terrain values match valid game terrain indices
* Use integers in map arrays that correspond to actual tileset values
* Check that level dimensions match the length of your map array
Custom levels greatly expand the possibilities for Remixed Dungeon mods. With the PredesignedLevel system, you can create unique challenges, puzzles, and experiences for players!