en:rpd:modding_custom_levels
Differences
This shows you the differences between two versions of the page.
| en:rpd:modding_custom_levels [2026/01/01 19:45] – namespace move Mike | en:rpd:modding_custom_levels [2026/01/01 19:47] (current) – external edit 127.0.0.1 | ||
|---|---|---|---|
| Line 1: | Line 1: | ||
| + | ====== 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 '' | ||
| + | |||
| + | <code json> | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | 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 | ||
| + | ], | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | }, | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | ], | ||
| + | " | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | }, | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | ], | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | ==== 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/ | ||
| + | * Other values correspond to specific terrain types in the tileset | ||
| + | |||
| + | ==== Advanced Level Features ==== | ||
| + | You can include complex features in your JSON levels: | ||
| + | |||
| + | <code json> | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | // Integer array representing the level layout | ||
| + | ], | ||
| + | |||
| + | " | ||
| + | " | ||
| + | |||
| + | // For levels with multiple exits | ||
| + | " | ||
| + | [10, 0], | ||
| + | [20, 0] | ||
| + | ], | ||
| + | |||
| + | // Custom tile layers for visual effects | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | |||
| + | // Tileset and water texture files | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | |||
| + | // Tile descriptions for tooltips | ||
| + | " | ||
| + | " | ||
| + | |||
| + | " | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | ], | ||
| + | " | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | ], | ||
| + | |||
| + | // Complex objects in the level | ||
| + | " | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | ], | ||
| + | |||
| + | // Level-specific properties | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | |||
| + | // 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 | ||
| + | " | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | ==== Level Scripts ==== | ||
| + | You can add script functionality to your custom levels by defining a script in Dungeon.json: | ||
| + | |||
| + | <code json> | ||
| + | { | ||
| + | " | ||
| + | // Other levels... | ||
| + | " | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | Create '' | ||
| + | |||
| + | <code lua> | ||
| + | local RPD = require " | ||
| + | |||
| + | -- 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(" | ||
| + | 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: | ||
| + | local height = level: | ||
| + | |||
| + | -- Find a random empty cell | ||
| + | local pos = nil | ||
| + | for i = 1, 100 do | ||
| + | local testX = math.random(1, | ||
| + | local testY = math.random(1, | ||
| + | local testCell = level: | ||
| + | if level: | ||
| + | pos = testCell | ||
| + | break | ||
| + | end | ||
| + | end | ||
| + | |||
| + | if pos then | ||
| + | RPD.spawnMob(" | ||
| + | RPD.glog(" | ||
| + | end | ||
| + | end | ||
| + | end | ||
| + | |||
| + | -- Called when the player steps on a cell (x, y) | ||
| + | function M.onCellSelected(self, | ||
| + | -- Example: add an effect when player steps on certain tiles | ||
| + | local level = RPD.Dungeon.level() | ||
| + | local cell = level: | ||
| + | RPD.topEffect(cell, | ||
| + | end | ||
| + | |||
| + | -- Other possible functions: | ||
| + | -- onAttack(self, | ||
| + | -- onCast(self, | ||
| + | -- onDie(self, cause) | ||
| + | -- onPickup(self, | ||
| + | -- onUseItem(self, | ||
| + | |||
| + | return M | ||
| + | </ | ||
| + | |||
| + | Note that scripts can also be associated with individual objects/ | ||
| + | |||
| + | <code json> | ||
| + | { | ||
| + | " | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | ] | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | |||
| + | ===== 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:// | ||
| + | * 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: | ||
| + | |||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | |||
| + | ==== 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' | ||
| + | * Each Tiled layer (base, deco, etc.) corresponds to arrays in the JSON: '' | ||
| + | * Mob and item positions from Tiled object layers need to be converted to the '' | ||
| + | * 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: | ||
| + | |||
| + | <code json> | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | 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 | ||
| + | ], | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | ] | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | Create '' | ||
| + | |||
| + | <code json> | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | <code lua> | ||
| + | local RPD = require " | ||
| + | |||
| + | local M = {} | ||
| + | |||
| + | -- Called when the actor is added to the level | ||
| + | function M.onSpawn(self) | ||
| + | RPD.glog(" | ||
| + | end | ||
| + | |||
| + | -- Called each turn - check if player stepped on switch | ||
| + | function M.onTurn(self) | ||
| + | local hero = RPD.Dungeon.hero | ||
| + | local heroPos = hero: | ||
| + | local level = RPD.Dungeon.level() | ||
| + | local x, y = level: | ||
| + | |||
| + | -- Check if player stepped on switch positions (represented as specific floor tiles) | ||
| + | if level: | ||
| + | -- Change door at 3,7 from door (4) to floor (1) | ||
| + | level: | ||
| + | RPD.glog(" | ||
| + | RPD.topEffect(level: | ||
| + | elseif level: | ||
| + | level: | ||
| + | RPD.glog(" | ||
| + | RPD.topEffect(level: | ||
| + | elseif level: | ||
| + | level: | ||
| + | RPD.glog(" | ||
| + | RPD.topEffect(level: | ||
| + | end | ||
| + | end | ||
| + | |||
| + | return M | ||
| + | </ | ||
| + | |||
| + | ==== Procedural Arena Level ==== | ||
| + | Create a level that generates content dynamically: | ||
| + | |||
| + | <code json> | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | 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 | ||
| + | ], | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | ] | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | Create '' | ||
| + | |||
| + | <code json> | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | <code lua> | ||
| + | local RPD = require " | ||
| + | |||
| + | local M = {} | ||
| + | local wave = 0 | ||
| + | |||
| + | -- Called when the actor is added to the level | ||
| + | function M.onSpawn(self) | ||
| + | wave = 1 | ||
| + | RPD.glog(" | ||
| + | M.spawnWave() | ||
| + | end | ||
| + | |||
| + | -- Called each turn | ||
| + | function M.onTurn(self) | ||
| + | local level = RPD.Dungeon.level() | ||
| + | local allMobs = level: | ||
| + | |||
| + | -- Count alive mobs excluding the player | ||
| + | local alive = 0 | ||
| + | for i = 0, allMobs: | ||
| + | local mob = allMobs: | ||
| + | if mob: | ||
| + | alive = alive + 1 | ||
| + | end | ||
| + | end | ||
| + | |||
| + | if alive == 0 then | ||
| + | wave = wave + 1 | ||
| + | if wave <= 5 then | ||
| + | RPD.glog(" | ||
| + | M.spawnWave() | ||
| + | else | ||
| + | RPD.glog(" | ||
| + | -- Change exit tile to be accessible (if needed) | ||
| + | end | ||
| + | end | ||
| + | end | ||
| + | |||
| + | function M.spawnWave() | ||
| + | local enemyClasses = {" | ||
| + | local class = enemyClasses[wave % # | ||
| + | |||
| + | local level = RPD.Dungeon.level() | ||
| + | local width = level: | ||
| + | local height = level: | ||
| + | |||
| + | -- 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, | ||
| + | local testY = math.random(1, | ||
| + | local testCell = level: | ||
| + | if level: | ||
| + | pos = testCell | ||
| + | break | ||
| + | end | ||
| + | end | ||
| + | |||
| + | if pos then | ||
| + | RPD.spawnMob(class, | ||
| + | end | ||
| + | end | ||
| + | |||
| + | RPD.glog(" | ||
| + | end | ||
| + | |||
| + | return M | ||
| + | </ | ||
| + | |||
| + | ===== Including Custom Levels in the Dungeon ===== | ||
| + | |||
| + | To make your custom levels appear in the dungeon progression, | ||
| + | |||
| + | <code json> | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | |||
| + | " | ||
| + | |||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | |||
| + | // Add your custom level | ||
| + | " | ||
| + | }, | ||
| + | |||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | // ... continue the progression | ||
| + | }, | ||
| + | " | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | The " | ||
| + | * 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! | ||
