Skip to content

A Continuation of PR #750 - Custom Stage Generation - #921

Open
Devilcasters wants to merge 8 commits into
TeamREPENTOGON:mainfrom
Devilcasters:custom-stage-generation
Open

Devilcasters wants to merge 8 commits into
TeamREPENTOGON:mainfrom
Devilcasters:custom-stage-generation

Conversation

@Devilcasters

@Devilcasters Devilcasters commented Aug 29, 2026

Copy link
Copy Markdown

A continuation and expansion of the custom dungeon generator system from PR #750 by @thicco-catto, adding crash guards, full Greed Mode support, automatic off-grid room handling, additional Lua APIs and Lua examples

Additions

Greed Mode Support

  • Added generator:SetGreedGoldRoom(room / gridIdx) for registering the Golden Treasure Room
  • Added Greed wave counter reset and wave RNG seed array initialization

High Lua Abstraction

  • Modders can now choose between automatic doors by leaving the Doors argument out or passing an explicit door bitmask for more control over adjacent rooms
  • Added table parameter support for PlaceRandomRoom() with defaults for the current stage, standard room type, and auto-doors
  • Added 1D (gridIdx) and 2D (col, row) overloads for PlaceRoom() and PlaceRandomRoom()
  • Added automatic off-grid room initialization, filling only missing rooms without overwriting custom placements

Fixes and Crash Guards

Start Room Crash Guard
ValidateFloor() now automatically places a default starting room if Grid 84 is unassigned, preventing crashes and allowing modders to omit the starting room unless they want to customize it

PlaceRoom False Failure Fix
Fixed false placement failures in PlaceRoom() by verifying internal room list growth rather than relying on the native return value

The Void and Null-Safety Crash Fix
Added null checks when resolving room configs in PlaceRoom(), TryPlaceRoom(), and PlaceOffGridRoom(), fixed an unguarded off-grid descriptor dereference in PlaceRoomsInFloor(), and removed The Void ban

Door Reciprocity
Added validation to prevent invalid doors from being generated

Stage Lookup and Logging
Added a fallback to current stage in GetRoomConfig() when the requested stage does not contain a matching room configuration
Removed the fallback to the current stage when no matching room configuration is found and added detailed validation logging showing the Stage, Type, Shape and Doors that caused a room configuration lookup to fail

The old fallback could make modders think their generation was accepted when it had actually failed validation and silently fallen back to another stage

Callback Handling
Fixed MC_PRE_GENERATE_DUNGEON handling in main_ex.lua so multiple mods can listen to the callback without triggering premature validation or generator resets

Constructor Parameter Order
Corrected the (col, row) parameter order in DungeonGeneratorRoom constructors

Considerations

  • Ascent vanilla generation can be intercepted, but I couldn't get the starting room to use the correct layout or get the Lightbeam to spawn
  • Adjacent rooms will always generate connecting doors, so revisiting PlaceRoom() may be worth considering in the future for more control over stage building

Lua Samples

Bossless Single Room bossless
local mod = RegisterMod("Bossless Single Room", 1)

-- Single isolated starting room with no boss or connected rooms

mod:AddCallback(ModCallbacks.MC_PRE_GENERATE_DUNGEON, function(_, generator, rng, dungeonType)
    local startRoom = generator:TryPlaceDefaultStartingRoom()
    if not startRoom then
        return false
    end

    -- Mark start room as the final boss room so floor validation passes
    generator:SetFinalBossRoom(startRoom)

    return true
end)
8 Boss Rooms 8boss
local mod = RegisterMod("8 Boss Rooms", 1)

-- A layout with a central 2x2 start room and 8 surrounding boss rooms

mod:AddCallback(ModCallbacks.MC_PRE_GENERATE_DUNGEON, function(_, generator, rng, dungeonType)
    local stage = Isaac.GetCurrentStageConfigId()
    -- Blue Womb STB contains 0 2x2 rooms; fallback to Basement
    if stage == StbType.BLUE_WOMB or dungeonType == GenerateDungeonType.BLUE_WOMB then
        stage = StbType.BASEMENT
    end

    local startRoom = generator:PlaceRandomRoom(84, {
        Stage = stage, 
        Type = RoomType.ROOM_DEFAULT,
        Shape = RoomShape.ROOMSHAPE_2x2,
    })
    if not startRoom then
        return false
    end

    local bossIndices = { 71, 72, 83, 86, 96, 99, 110, 111 }
    local lastBossRoom = nil

    for _, idx in ipairs(bossIndices) do
        local boss = generator:PlaceRandomRoom(idx, { 
            Stage = StbType.SPECIAL_ROOMS,
            Type = RoomType.ROOM_BOSS,
            Shape = RoomShape.ROOMSHAPE_1x1,
        })
        if boss then
            lastBossRoom = boss
        end
    end

    if lastBossRoom then
        generator:SetFinalBossRoom(lastBossRoom)
    end

    return true
end)
L-Rooms l_rooms
local mod = RegisterMod("L Rooms", 1)

-- 2x2 Start Room surrounded by 4 L-shaped rooms forming a 4x4 block surrounded by ultra secret rooms

mod:AddCallback(ModCallbacks.MC_PRE_GENERATE_DUNGEON, function(_, generator, rng, dungeonType)
    local startRoom = generator:PlaceRandomRoom(84, {
        Stage = StbType.BASEMENT,
        Type = RoomType.ROOM_DEFAULT,
        Shape = RoomShape.ROOMSHAPE_2x2,
    })
    if not startRoom then
        return false
    end

    generator:PlaceRandomRoom(70, {
        Stage = StbType.BASEMENT,
        Type = RoomType.ROOM_DEFAULT,
        Shape = RoomShape.ROOMSHAPE_LBR,
    })

    generator:PlaceRandomRoom(72, {
        Stage = StbType.BASEMENT,
        Type = RoomType.ROOM_DEFAULT,
        Shape = RoomShape.ROOMSHAPE_LBL,
    })

    generator:PlaceRandomRoom(96, {
        Stage = StbType.BASEMENT,
        Type = RoomType.ROOM_DEFAULT,
        Shape = RoomShape.ROOMSHAPE_LTR,
    })

    local bossLTL = generator:PlaceRandomRoom(98, {
        Stage = StbType.BASEMENT,
        Type = RoomType.ROOM_DEFAULT,
        Shape = RoomShape.ROOMSHAPE_LTL,
    })
    if bossLTL then
        generator:SetFinalBossRoom(bossLTL)
    end

    local ultraSecretIndices = {
        56, 57, 58, 
        59, 60, 61,
        69, 74, 82, 
        87, 95, 100,
        108, 113, 121, 
        122, 123, 126
    }

    for _, idx in ipairs(ultraSecretIndices) do
        generator:PlaceRandomRoom(idx, {
            Stage = StbType.SPECIAL_ROOMS,
            Type = RoomType.ROOM_ULTRASECRET,
            Shape = RoomShape.ROOMSHAPE_1x1,
        })
    end

    return true
end)
Maze maze
local mod = RegisterMod("Maze", 1)

-- A 3x3 maze layout demonstrating explicit door control on shared walls
-- Intentional failure on grid 97 for error handling showcase

mod:AddCallback(ModCallbacks.MC_PRE_GENERATE_DUNGEON, function(_, generator, rng, dungeonType)
    local startRoom = generator:TryPlaceDefaultStartingRoom(DoorMask.UP0)
    if not startRoom then
        return false
    end

    generator:PlaceRandomRoom(70, {
        Stage = StbType.BASEMENT,
        Type = RoomType.ROOM_DEFAULT,
        Doors = DoorMask.RIGHT0 | DoorMask.DOWN0,
    })

    generator:PlaceRandomRoom(71, {
        Stage = StbType.BASEMENT,
        Type = RoomType.ROOM_DEFAULT,
        Doors = DoorMask.DOWN0 | DoorMask.LEFT0 | DoorMask.RIGHT0,
    })

    generator:PlaceRandomRoom(72, {
        Stage = StbType.BASEMENT,
        Type = RoomType.ROOM_DEFAULT,
        Doors = DoorMask.LEFT0 | DoorMask.DOWN0,
    })

    generator:PlaceRandomRoom(83, {
        Stage = StbType.BASEMENT,
        Type = RoomType.ROOM_DEFAULT,
        Doors = DoorMask.UP0 | DoorMask.DOWN0,
    })

    generator:PlaceRandomRoom(85, {
        Stage = StbType.BASEMENT,
        Type = RoomType.ROOM_DEFAULT,
        Doors = DoorMask.UP0 | DoorMask.DOWN0,
    })

    generator:PlaceRandomRoom(96, {
        Stage = StbType.BASEMENT,
        Type = RoomType.ROOM_DEFAULT,
        Doors = DoorMask.UP0,
    })

    local bossRoom = generator:PlaceRandomRoom(97, {
        Stage = StbType.SPECIAL_ROOMS,
        Type = RoomType.ROOM_BOSS,
        Doors = DoorMask.UP0 | DoorMask.RIGHT0,
    })
    if bossRoom then
        generator:SetFinalBossRoom(bossRoom)
    end

    generator:PlaceRandomRoom(98, {
        Stage = StbType.BASEMENT,
        Type = RoomType.ROOM_DEFAULT,
        Doors = DoorMask.LEFT0 | DoorMask.UP0,
    })

    return true
end)  
Distant Islands DistantIslands
local mod = RegisterMod("Distant Islands", 1)

-- Disconnected room sections across the grid corners, requiring red key or teleportation to navigate

mod:AddCallback(ModCallbacks.MC_PRE_GENERATE_DUNGEON, function(_, generator, rng, dungeonType)
    local startRoom = generator:TryPlaceDefaultStartingRoom()
    if not startRoom then
        return false
    end

    generator:PlaceRandomRoom(0, {
        Stage = StbType.SPECIAL_ROOMS,
        Type = RoomType.ROOM_TREASURE,
    })

    generator:PlaceRandomRoom(12, {
        Stage = StbType.SPECIAL_ROOMS,
        Type = RoomType.ROOM_SHOP,
    })

    generator:PlaceRandomRoom(143, {
        Stage = StbType.SPECIAL_ROOMS,
        Type = RoomType.ROOM_SUPERSECRET,
    })

    generator:PlaceRandomRoom(144, {
        Stage = StbType.BASEMENT,
        Type = RoomType.ROOM_DEFAULT,
    })

    generator:PlaceRandomRoom(156, {
        Stage = StbType.SPECIAL_ROOMS,
        Type = RoomType.ROOM_SECRET,
    })

    generator:PlaceRandomRoom(157, {
        Stage = StbType.SPECIAL_ROOMS,
        Type = RoomType.ROOM_ULTRASECRET,
    })

    generator:PlaceRandomRoom(154, {
        Stage = StbType.BASEMENT,
        Type = RoomType.ROOM_DEFAULT,
    })

    generator:PlaceRandomRoom(155, {
        Stage = StbType.BASEMENT,
        Type = RoomType.ROOM_DEFAULT,
    })

    generator:PlaceRandomRoom(167, {
        Stage = StbType.BASEMENT,
        Type = RoomType.ROOM_DEFAULT,
    })

    local bossRoom = generator:PlaceRandomRoom(168, {
        Stage = StbType.SPECIAL_ROOMS,
        Type = RoomType.ROOM_BOSS,
    })
    if bossRoom then
        generator:SetFinalBossRoom(bossRoom)
    end

    return true
end)
Planetarium Dynamic Placement planetarium
local mod = RegisterMod("Planetarium Chance", 1)

-- Dynamic Planetarium placement based on the player's current Planetarium chance

mod:AddCallback(ModCallbacks.MC_PRE_GENERATE_DUNGEON, function(_, generator, rng, dungeonType)
    local level = Game():GetLevel()

    local startRoom = generator:TryPlaceDefaultStartingRoom()
    if not startRoom then
        return false
    end

    local planetariumChance = level:GetPlanetariumChance()
    if rng:RandomFloat() < planetariumChance then
        generator:PlaceRandomRoom(71, {
            Stage = StbType.SPECIAL_ROOMS,
            Type = RoomType.ROOM_PLANETARIUM,
        })
    end

    generator:PlaceRandomRoom(85, {
        Stage = StbType.BASEMENT,
        Type = RoomType.ROOM_DEFAULT,
    })

    local bossRoom = generator:PlaceRandomRoom(98, {
        Stage = StbType.SPECIAL_ROOMS,
        Type = RoomType.ROOM_BOSS,
    })
    if bossRoom then
        generator:SetFinalBossRoom(bossRoom)
    end

    return true
end)
Mega Satan Anywhere megasatananywhere1 megasatananywhere2
local mod = RegisterMod("Mega Satan Anywhere", 1)

-- Spawns Mega Satan directly on the floor grid instead of his off-grid room

mod:AddCallback(ModCallbacks.MC_PRE_GENERATE_DUNGEON, function(_, generator, rng, dungeonType)
    local startRoom = generator:TryPlaceDefaultStartingRoom()
    if not startRoom then
        return false
    end

    -- Variant 5000: Mega Satan's boss room
    local config = RoomConfig.GetRoomByStageTypeAndVariant(StbType.SPECIAL_ROOMS, RoomType.ROOM_BOSS, 5000)
    if config then
        local megaSatanRoom = generator:PlaceRoom(config, 58)
        if megaSatanRoom then
            generator:SetFinalBossRoom(megaSatanRoom)
        end
    end

    return true
end)
Knife Puzzle 1 knifepuzzle1
local mod = RegisterMod("Knife Puzzle 1", 1)

-- Downpour II Knife Piece 1 puzzle with White Fire and Mirror Door

mod:AddCallback(ModCallbacks.MC_PRE_GENERATE_DUNGEON, function(_, generator, rng, dungeonType)
    local level = Game():GetLevel()
    local stage = level:GetStage()
    local stageType = level:GetStageType()
    local isDownpour = (stage == LevelStage.STAGE1_2 or (stage == LevelStage.STAGE1_1 and (level:GetCurses() & LevelCurse.CURSE_OF_LABYRINTH) ~= 0))
        and (stageType == StageType.STAGETYPE_REPENTANCE or stageType == StageType.STAGETYPE_REPENTANCE_B)

    if not isDownpour then
        return nil
    end

    local stbStage = (stageType == StageType.STAGETYPE_REPENTANCE_B) and StbType.DROSS or StbType.DOWNPOUR

    local startRoom = generator:TryPlaceDefaultStartingRoom()
    if not startRoom then
        return false
    end

    -- Subtype 34: Mirror Room
    generator:PlaceRandomRoom(83, { 
        Stage = stbStage, 
        Type = RoomType.ROOM_DEFAULT, 
        Subtype = 34, 
    })

    -- Variants 5000-5018: White Fire rooms
    generator:PlaceRandomRoom(85, { 
        Stage = stbStage, 
        Type = RoomType.ROOM_DEFAULT, 
        MinVariant = 5000, 
        MaxVariant = 5018, 
    })

    -- generate_mirror_world() creates the Knife Piece 1 room in Dimension 1
    generator:PlaceRandomRoom(71, { 
        Stage = StbType.SPECIAL_ROOMS, 
        Type = RoomType.ROOM_TREASURE, 
    })

    local bossRoom = generator:PlaceRandomRoom(97, { 
        Stage = StbType.SPECIAL_ROOMS, 
        Type = RoomType.ROOM_BOSS, 
    })
    if bossRoom then
        generator:SetFinalBossRoom(bossRoom)
    end

    return true
end)
Knife Puzzle 2 knifepuzzle2-1 knifepuzzle2-2 knifepuzzle2-3 knifepuzzle2-4
local mod = RegisterMod("Knife Puzzle 2", 1)

-- Mines II Knife Piece 2 puzzle with minecart track, yellow buttons and mother chase sequence

mod:AddCallback(ModCallbacks.MC_PRE_GENERATE_DUNGEON, function(_, generator, rng, dungeonType)
    local level = Game():GetLevel()
    local stage = level:GetStage()
    local stageType = level:GetStageType()
    local isMines = (stage == LevelStage.STAGE2_2 or (stage == LevelStage.STAGE2_1 and (level:GetCurses() & LevelCurse.CURSE_OF_LABYRINTH) ~= 0))
        and (stageType == StageType.STAGETYPE_REPENTANCE or stageType == StageType.STAGETYPE_REPENTANCE_B)

    if not isMines then
        return nil
    end

    local stbStage = (stageType == StageType.STAGETYPE_REPENTANCE_B) and StbType.ASHPIT or StbType.MINES

    local startRoom = generator:TryPlaceDefaultStartingRoom()
    if not startRoom then
        return false
    end

    -- Subtype 10: minecart track room
    generator:PlaceRandomRoom(58, {
        Stage = stbStage,
        Type = RoomType.ROOM_DEFAULT,
        Shape = RoomShape.ROOMSHAPE_1x2,
        Subtype = 10,
    })

    -- Subtype 1: yellow button room
    generator:PlaceRandomRoom(85, {
        Stage = stbStage,
        Type = RoomType.ROOM_DEFAULT,
        Subtype = 1,
    })

    generator:PlaceRandomRoom(97, {
        Stage = stbStage,
        Type = RoomType.ROOM_DEFAULT,
        Subtype = 1,
    })

    local bossRoom = generator:PlaceRandomRoom(83, {
        Stage = StbType.SPECIAL_ROOMS,
        Type = RoomType.ROOM_BOSS,
    })
    if bossRoom then
        generator:SetFinalBossRoom(bossRoom)
    end

    return true
end)
Mausoleum Flesh Door mausoleumfleshdoor1 mausoleumfleshdoor2 mausoleumfleshdoor3
local mod = RegisterMod("Mausoleum Flesh Door", 1)

-- Mausoleum II with Mom's Heart and flesh door transition to Corpse

mod:AddCallback(ModCallbacks.MC_PRE_GENERATE_DUNGEON, function(_, generator, rng, dungeonType)
    local level = Game():GetLevel()
    local stage = level:GetStage()
    local stageType = level:GetStageType()
    local isMausoleum = (stage == LevelStage.STAGE3_2 or (stage == LevelStage.STAGE3_1 and (level:GetCurses() & LevelCurse.CURSE_OF_LABYRINTH) ~= 0))
        and (stageType == StageType.STAGETYPE_REPENTANCE or stageType == StageType.STAGETYPE_REPENTANCE_B)

    if not isMausoleum then
        return nil
    end

    local stbStage = (stageType == StageType.STAGETYPE_REPENTANCE_B) and StbType.GEHENNA or StbType.MAUSOLEUM

    local startRoom = generator:TryPlaceDefaultStartingRoom()
    if not startRoom then
        return false
    end

    generator:PlaceRandomRoom(71, {
        Stage = stbStage,
        Type = RoomType.ROOM_DEFAULT,
    })

    -- Subtype 89: Mausoleum Mom
    local bossRoom = generator:PlaceRandomRoom(58, {
        Stage = StbType.SPECIAL_ROOMS,
        Type = RoomType.ROOM_BOSS,
        Subtype = 89,
    })
    if bossRoom then
        generator:SetFinalBossRoom(bossRoom)
    end

    return true
end)
Corpse Mother Exit and Fight corpsemotherexit1 corpsemotherexit2
local mod = RegisterMod("Corpse Mother Exit and Fight", 1)

-- Corpse II with the entrance pit transitioning to the off-grid Mother boss room

mod:AddCallback(ModCallbacks.MC_PRE_GENERATE_DUNGEON, function(_, generator, rng, dungeonType)
    local level = Game():GetLevel()
    local stage = level:GetStage()
    local stageType = level:GetStageType()
    local isCorpse = (stage == LevelStage.STAGE4_2 or (stage == LevelStage.STAGE4_1 and (level:GetCurses() & LevelCurse.CURSE_OF_LABYRINTH) ~= 0))
        and (stageType == StageType.STAGETYPE_REPENTANCE or stageType == StageType.STAGETYPE_REPENTANCE_B)

    if not isCorpse then
        return nil
    end

    local startRoom = generator:TryPlaceDefaultStartingRoom()
    if not startRoom then
        return false
    end

    generator:PlaceRandomRoom(71, {
        Stage = StbType.CORPSE,
        Type = RoomType.ROOM_DEFAULT,
    })

    local bossRoom = generator:PlaceRandomRoom(58, {
        Stage = StbType.SPECIAL_ROOMS,
        Type = RoomType.ROOM_BOSS,
        Subtype = 88, -- Mother entrance pit
    })
    if bossRoom then
        generator:SetFinalBossRoom(bossRoom)
    end

    return true
end)
Chest / Dark Room Mega Satan chestmegasatan darkroommegasatan
local mod = RegisterMod("Chest and Dark Room Mega Satan", 1)

-- Chest / Dark Room with Golden Door to off-grid Mega Satan room

mod:AddCallback(ModCallbacks.MC_PRE_GENERATE_DUNGEON, function(_, generator, rng, dungeonType)
    local level = Game():GetLevel()
    local stage = level:GetStage()
    local stageType = level:GetStageType()
    local isStage6 = (stage == LevelStage.STAGE6)

    if not isStage6 then
        return nil
    end

    local isChest = (stageType == StageType.STAGETYPE_WOTL)
    local stbStage = isChest and StbType.CHEST or StbType.DARK_ROOM

    -- Subtype 40: ??? / Blue Baby (Chest), Subtype 54: The Lamb (Dark Room)
    local bossSubtype = isChest and 40 or 54

    -- Grid 71 (top wall) must remain empty for the Mega Satan Golden Door to spawn
    local startRoom = generator:TryPlaceDefaultStartingRoom()
    if not startRoom then
        return false
    end

    generator:PlaceRandomRoom(85, {
        Stage = stbStage,
        Type = RoomType.ROOM_DEFAULT,
    })

    local bossRoom = generator:PlaceRandomRoom(98, {
        Stage = StbType.SPECIAL_ROOMS,
        Type = RoomType.ROOM_BOSS,
        Subtype = bossSubtype,
    })
    if bossRoom then
        generator:SetFinalBossRoom(bossRoom)
    end

    return true
end)
Singular The Void Boss Room singlevoidbossroom1 singlevoidbossroom2
local mod = RegisterMod("Singular The Void Boss Room", 1)

-- Delirium boss room is connected directly to the start room

mod:AddCallback(ModCallbacks.MC_PRE_GENERATE_DUNGEON, function(_, generator, rng, dungeonType)
    local level = Game():GetLevel()
    if level:GetStage() ~= LevelStage.STAGE7 then
        return nil
    end

    local startRoom = generator:TryPlaceDefaultStartingRoom()
    if not startRoom then
        return false
    end

    local bossRoom = generator:PlaceRandomRoom(58, {
        Stage = StbType.SPECIAL_ROOMS,
        Type = RoomType.ROOM_BOSS,
        Shape = RoomShape.ROOMSHAPE_2x2,
        Subtype = 70, -- Delirium
    })
    if bossRoom then
        generator:SetFinalBossRoom(bossRoom)
    end

    return true
end)
The Ascent ascent1 ascent2
local mod = RegisterMod("The Ascent", 1)

-- Ascent floor with the correct starting boss room and light beam exit
-- It is a bit whacky but it works for this PR

mod:AddCallback(ModCallbacks.MC_PRE_GENERATE_DUNGEON, function(_, generator, rng, dungeonType)
    if dungeonType ~= GenerateDungeonType.BACKWARDS then
        return nil
    end

    local level = Game():GetLevel()
    local stage = level:GetStage()
    local stageType = level:GetStageType()
    local isMausoleum = (stage == LevelStage.STAGE3_2)
        and (stageType == StageType.STAGETYPE_REPENTANCE or stageType == StageType.STAGETYPE_REPENTANCE_B)

    -- Subtype 89: Mausoleum Mom, Subtype 6: Depths Mom
    local bossStage = isMausoleum and StbType.MAUSOLEUM or StbType.SPECIAL_ROOMS
    local bossSubtype = isMausoleum and 89 or 6
    -- Variants 1060-1064: Depths Mom layouts (Variant 1: Mausoleum Dad's Note room)
    local bossMinVar = isMausoleum and 1 or 1060
    local bossMaxVar = isMausoleum and 1 or 1064

    -- Boss room spawn
    local bossRoom = generator:PlaceRandomRoom(58, {
        Stage = bossStage,
        Type = RoomType.ROOM_BOSS,
        MinVariant = bossMinVar,
        MaxVariant = bossMaxVar,
        Subtype = bossSubtype,
    })
    if not bossRoom then
        return false
    end

    generator:PlaceRandomRoom(71, {
        Stage = StbType.ASCENT,
        Type = RoomType.ROOM_DEFAULT,
    })

    -- Variant 10000: light beam exit
    local exitRoom = generator:PlaceRandomRoom(84, {
        Stage = StbType.ASCENT,
        Type = RoomType.ROOM_DEFAULT,
        MinVariant = 10000,
        MaxVariant = 10000,
    })
    if not exitRoom then
        return false
    end

    generator:SetFinalBossRoom(bossRoom)

    return true
end)
Red Redemption redredemption
local mod = RegisterMod("Red Redemption", 1)

-- Custom generation for the Red Redemption challenge

mod:AddCallback(ModCallbacks.MC_PRE_GENERATE_DUNGEON, function(_, generator, rng, dungeonType)
    if dungeonType ~= GenerateDungeonType.RED_REDEMPTION then
        return nil
    end

    local startRoom = generator:TryPlaceDefaultStartingRoom()
    if not startRoom then
        return false
    end

    local bossRoom = generator:PlaceRandomRoom(71, {
        Stage = StbType.SPECIAL_ROOMS,
        Type = RoomType.ROOM_BOSS,
    })
    if bossRoom then
        generator:SetFinalBossRoom(bossRoom)
    end

    generator:PlaceRandomRoom(85, {
        Stage = StbType.SPECIAL_ROOMS,
        Type = RoomType.ROOM_TREASURE,
    })

    generator:PlaceRandomRoom(83, {
        Stage = StbType.SPECIAL_ROOMS,
        Type = RoomType.ROOM_SHOP,
    })

    return true
end)
Greed Mode GreedMode
local mod = RegisterMod("Greed Mode", 1)

-- Complete Greed Mode floor with wave room, shops, gold/silver treasure rooms, and exit

mod:AddCallback(ModCallbacks.MC_PRE_GENERATE_DUNGEON, function(_, generator, rng, dungeonType)
    if dungeonType ~= GenerateDungeonType.GREED then
        return nil
    end

    -- Variants 1000-1999: Greed wave room layouts
    local mainArena = generator:PlaceRandomRoom(84, {
        Stage = StbType.BASEMENT,
        Type = RoomType.ROOM_DEFAULT,
        Shape = RoomShape.ROOMSHAPE_1x2,
        MinVariant = 1000,
        MaxVariant = 1999,
    })
    if not mainArena then
        return false
    end

    local bossRoom = generator:PlaceRandomRoom(109, {
        Stage = StbType.SPECIAL_ROOMS,
        Type = RoomType.ROOM_BOSS,
        Shape = RoomShape.ROOMSHAPE_2x2,
        Mode = 0, -- 2x2 boss rooms don't exist in greed mode
    })
    if bossRoom then
        generator:SetFinalBossRoom(bossRoom)
    end

    generator:PlaceRandomRoom(136, {
        Stage = StbType.SPECIAL_ROOMS,
        Type = 23, -- Greed exit
        Shape = RoomShape.ROOMSHAPE_1x1,
    })

    generator:PlaceRandomRoom(71, {
        Stage = StbType.SPECIAL_ROOMS,
        Type = RoomType.ROOM_SHOP,
        Shape = RoomShape.ROOMSHAPE_1x1,
        Mode = 0, -- normal shops don't exist in greed mode
    })

    generator:PlaceRandomRoom(82, {
        Stage = StbType.SPECIAL_ROOMS,
        Type = RoomType.ROOM_SHOP,
        Shape = RoomShape.ROOMSHAPE_2x1,
    })

    generator:PlaceRandomRoom(69, {
        Stage = StbType.SPECIAL_ROOMS,
        Type = RoomType.ROOM_SHOP,
        Shape = RoomShape.ROOMSHAPE_2x1,
    })

    generator:PlaceRandomRoom(85, {
        Stage = StbType.SPECIAL_ROOMS,
        Type = RoomType.ROOM_TREASURE,
        Shape = RoomShape.ROOMSHAPE_1x1,
    })

    generator:PlaceRandomRoom(86, {
        Stage = StbType.BASEMENT,
        Type = RoomType.ROOM_DEFAULT,
        Shape = RoomShape.ROOMSHAPE_1x1,
        Mode = 0, -- default 1x1 rooms don't exist in greed mode
    })

    generator:PlaceRandomRoom(74, {
        Stage = StbType.BASEMENT,
        Type = RoomType.ROOM_DEFAULT,
        Shape = RoomShape.ROOMSHAPE_2x2,
        Mode = 0, -- default 2x2 rooms don't exist in greed mode
    })

    generator:PlaceRandomRoom(61, {
        Stage = StbType.SPECIAL_ROOMS,
        Type = RoomType.ROOM_PLANETARIUM,
        Shape = RoomShape.ROOMSHAPE_1x1,
        Mode = 0, -- planetariums don't exist in greed mode
    })

    generator:PlaceRandomRoom(62, {
        Stage = StbType.SPECIAL_ROOMS,
        Type = RoomType.ROOM_LIBRARY,
        Shape = RoomShape.ROOMSHAPE_1x1,
        Mode = 0, -- libraries don't exist in greed mode
    })

    -- Only one treasure room per floor can be gold (g_Game->_greedModeTreasureRoomIdx is a single scalar)
    -- Call SetGreedGoldRoom once to pick which room; every other treasure room defaults to silver
    local gTreasure76 = generator:PlaceRandomRoom(76, {
        Stage = StbType.SPECIAL_ROOMS,
        Type = RoomType.ROOM_TREASURE,
        Shape = RoomShape.ROOMSHAPE_1x1,
    })
    if gTreasure76 then
        generator:SetGreedGoldRoom(gTreasure76)
    end

    generator:PlaceRandomRoom(89, {
        Stage = StbType.SPECIAL_ROOMS,
        Type = RoomType.ROOM_TREASURE,
        Shape = RoomShape.ROOMSHAPE_1x1,
    })

    generator:PlaceRandomRoom(100, {
        Stage = StbType.SPECIAL_ROOMS,
        Type = RoomType.ROOM_TREASURE,
        Shape = RoomShape.ROOMSHAPE_1x1,
    })

    generator:PlaceRandomRoom(101, {
        Stage = StbType.SPECIAL_ROOMS,
        Type = RoomType.ROOM_TREASURE,
        Shape = RoomShape.ROOMSHAPE_1x1,
    })

    return true
end)

Comment thread repentogon/LuaInterfaces/LuaLevel.cpp Outdated
Comment thread repentogon/LuaInterfaces/LuaLevel.cpp Outdated
Comment thread repentogon/LuaInterfaces/LuaLevel.cpp Outdated
Comment thread repentogon/LuaInterfaces/Room/LuaRoomDescriptor.cpp Outdated
Comment thread repentogon/LuaInterfaces/LuaDungeonGenerator.cpp
Comment thread repentogon/LuaInterfaces/LuaDungeonGenerator.cpp Outdated
Comment thread repentogon/LuaInterfaces/LuaDungeonGenerator.cpp Outdated
Comment thread repentogon/resources/scripts/main_ex.lua
Comment thread repentogon/LuaInterfaces/LuaDungeonGenerator.cpp
Comment thread repentogon/LuaInterfaces/LuaDungeonGenerator.cpp Outdated
Comment thread libzhl/functions/Global.zhl
@Devilcasters
Devilcasters force-pushed the custom-stage-generation branch from 0221dcb to 91517e4 Compare September 1, 2026 02:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants