local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local Workspace = game:GetService("Workspace")
local ROOM_SOURCE_FOLDER_NAME = "ComplexRooms"
local FLOOR_COUNT = 10
local FLOOR_HEIGHT = 500
local BASE_ROOM_Y = 850000
local PERMANENT_ROOM_COUNT = 50
local LOAD_RADIUS = 450
local UNLOAD_RADIUS = 700
local UPDATE_INTERVAL = 1
local MAX_ROOMS_PER_FLOOR = 400
local DOOR_TO_DOOR_GAP_STUDS = 1
local MAX_PLACEMENT_ATTEMPTS = 6
local RANDOM_COLOR_PART_NAME = "RANDOMCOLORPART"
local COMMON_WEIGHT = 70
local RARE_WEIGHT = 25
local VERYRARE_WEIGHT = 5
local IMPOSSIBLE_CHANCE = 0.01
local COOL_HUE_MIN = 0.33
local COOL_HUE_MAX = 0.68
local COOL_COLOR_CHANCE = 0.85
local COLOR_SATURATION_MIN = 0.5
local COLOR_SATURATION_MAX = 0.8
local COLOR_VALUE_MIN = 0.55
local COLOR_VALUE_MAX = 0.9
local SPECIAL_STAIRCASE_NAME = "SPECIAL_STAIRCASE"
local floors = {}
local roomsFolder = Instance.new("Folder")
roomsFolder.Name = "GeneratedRooms"
roomsFolder.Parent = Workspace
local floorFolders = {}
for f = 1, FLOOR_COUNT do
local folder = Instance.new("Folder")
folder.Name = "Floor" .. f
folder.Parent = roomsFolder
floorFolders[f] = folder
floors[f] = { rooms = {}, nextId = 1, openExits = {}, activeCount = 0 }
end
local function floorBaseY(floor)
return BASE_ROOM_Y + (floor - 1) * FLOOR_HEIGHT
end
local function randomAccentColor()
local hue
if math.random() < COOL_COLOR_CHANCE then
hue = COOL_HUE_MIN + math.random() * (COOL_HUE_MAX - COOL_HUE_MIN)
else
local warmRange = 1 - (COOL_HUE_MAX - COOL_HUE_MIN)
local roll = math.random() * warmRange
if roll < COOL_HUE_MIN then
hue = roll
else
hue = COOL_HUE_MAX + (roll - COOL_HUE_MIN)
end
end
local saturation = COLOR_SATURATION_MIN + math.random() * (COLOR_SATURATION_MAX - COLOR_SATURATION_MIN)
local value = COLOR_VALUE_MIN + math.random() * (COLOR_VALUE_MAX - COLOR_VALUE_MIN)
return Color3.fromHSV(hue, saturation, value)
end
local function fixRoomColors(model)
for _, descendant in ipairs(model:GetDescendants()) do
if descendant:IsA("BasePart") and descendant.Name == RANDOM_COLOR_PART_NAME then
descendant.Color = randomAccentColor()
end
end
end
local function getExitParts(model)
local exits = {}
for _, descendant in ipairs(model:GetDescendants()) do
if descendant:IsA("BasePart") and descendant.Name == "Exit" then
table.insert(exits, descendant)
end
end
return exits
end
local function categorizeRoomTemplates()
local sourceFolder = ServerStorage:FindFirstChild(ROOM_SOURCE_FOLDER_NAME)
if not sourceFolder then
return {}, {}, {}, {}
end
local commonRooms, rareRooms, veryRareRooms, impossibleRooms = {}, {}, {}, {}
for _, child in ipairs(sourceFolder:GetChildren()) do
if child:IsA("Model") then
local entrance = child:FindFirstChild("Entrance", true)
local exits = getExitParts(child)
if entrance and entrance:IsA("BasePart") and #exits > 0 then
if child.Name:sub(1, 9) == "VERYRARE_" then
table.insert(veryRareRooms, child)
elseif child.Name:sub(1, 11) == "IMPOSSIBLE_" then
table.insert(impossibleRooms, child)
elseif child.Name:sub(1, 5) == "RARE_" then
table.insert(rareRooms, child)
elseif child.Name:sub(1, 7) == "COMMON_" then
table.insert(commonRooms, child)
end
end
end
end
return commonRooms, rareRooms, veryRareRooms, impossibleRooms
end
local function findSpecialStaircaseTemplate()
local sourceFolder = ServerStorage:FindFirstChild(ROOM_SOURCE_FOLDER_NAME)
if not sourceFolder then
return nil
end
local child = sourceFolder:FindFirstChild(SPECIAL_STAIRCASE_NAME)
if child and child:IsA("Model") then
local entrance = child:FindFirstChild("Entrance", true)
local exits = getExitParts(child)
if entrance and entrance:IsA("BasePart") and #exits > 0 then
return child
end
end
return nil
end
local roomPool = {}
local roomPoolTotalWeight = 0
local impossibleRoomTemplates = {}
local specialStaircaseTemplate = findSpecialStaircaseTemplate()
local function buildRoomPool()
local commonRooms, rareRooms, veryRareRooms, impossibleRooms = categorizeRoomTemplates()
impossibleRoomTemplates = impossibleRooms
for _, template in ipairs(commonRooms) do
table.insert(roomPool, { template = template, weight = COMMON_WEIGHT })
end
for _, template in ipairs(rareRooms) do
table.insert(roomPool, { template = template, weight = RARE_WEIGHT })
end
for _, template in ipairs(veryRareRooms) do
table.insert(roomPool, { template = template, weight = VERYRARE_WEIGHT })
end
for _, entry in ipairs(roomPool) do
roomPoolTotalWeight += entry.weight
end
end
buildRoomPool()
local function pickRoomTemplate()
if #impossibleRoomTemplates > 0 and math.random() < IMPOSSIBLE_CHANCE then
return impossibleRoomTemplates[math.random(1, #impossibleRoomTemplates)]
end
if #roomPool == 0 then
if #impossibleRoomTemplates > 0 then
return impossibleRoomTemplates[math.random(1, #impossibleRoomTemplates)]
end
return nil
end
local roll = math.random() * roomPoolTotalWeight
local cumulative = 0
for _, entry in ipairs(roomPool) do
cumulative += entry.weight
if roll <= cumulative then
return entry.template
end
end
return roomPool[#roomPool].template
end
local function alignRoomToTarget(clone, entrancePart, targetCFrame)
local currentModelCFrame = clone:GetPivot()
local currentEntranceCFrame = entrancePart.CFrame
local entranceOffset = currentModelCFrame:ToObjectSpace(currentEntranceCFrame)
local newModelCFrame = targetCFrame * entranceOffset:Inverse()
clone:PivotTo(newModelCFrame)
end
local function addOpenExit(floorData, roomId, exitPart)
table.insert(floorData.openExits, { roomId = roomId, exitPart = exitPart })
end
local function removeOpenExit(floorData, exitPart)
for i, entry in ipairs(floorData.openExits) do
if entry.exitPart == exitPart then
table.remove(floorData.openExits, i)
return
end
end
end
local function collectSiblingModels(floorData, parentEntry)
local models = {}
if parentEntry then
if parentEntry.model then
table.insert(models, parentEntry.model)
end
for _, roomEntry in pairs(floorData.rooms) do
if roomEntry.parentRoomId == parentEntry.id and roomEntry.model then
table.insert(models, roomEntry.model)
end
end
end
return models
end
local function isSpaceBlocked(clone, ignoreModels)
local cf, size = clone:GetBoundingBox()
local overlapParams = OverlapParams.new()
local ignoreList = {clone}
if ignoreModels then
for _, m in ipairs(ignoreModels) do
table.insert(ignoreList, m)
end
end
overlapParams.FilterDescendantsInstances = ignoreList
overlapParams.FilterType = Enum.RaycastFilterType.Exclude
local checkSize = size * 0.75
local touching = Workspace:GetPartBoundsInBox(cf, checkSize, overlapParams)
for _, tPart in ipairs(touching) do
if tPart.CanCollide and tPart:IsDescendantOf(roomsFolder) then
return true
end
end
local entrance = clone:FindFirstChild("Entrance", true)
if entrance then
local entTouching = Workspace:GetPartsInPart(entrance, overlapParams)
for _, tPart in ipairs(entTouching) do
if tPart.CanCollide and tPart:IsDescendantOf(roomsFolder) then
if tPart.Name ~= "Exit" and tPart.Name ~= "Entrance" then
return true
end
end
end
end
return false
end
local function spawnRoom(floor, permanent, dockToCFrame, fallbackCFrame, parentEntry, parentExitPart, forcedTemplate)
local floorData = floors[floor]
if floorData.activeCount >= MAX_ROOMS_PER_FLOOR then
return nil, "max_rooms"
end
local template = forcedTemplate or pickRoomTemplate()
if not template then
return nil, "no_template"
end
local clone = template:Clone()
local entrancePart = clone:FindFirstChild("Entrance", true)
local exitParts = getExitParts(clone)
if dockToCFrame then
alignRoomToTarget(clone, entrancePart, dockToCFrame)
else
clone:PivotTo(fallbackCFrame)
end
if dockToCFrame and isSpaceBlocked(clone, collectSiblingModels(floorData, parentEntry)) then
clone:Destroy()
return nil, "blocked"
end
clone.Parent = floorFolders[floor]
fixRoomColors(clone)
local id = floorData.nextId
floorData.nextId += 1
local parentRoomId = parentEntry and parentEntry.id or nil
local entry = {
id = id,
model = clone,
permanent = permanent,
floor = floor,
entrance = entrancePart,
exits = exitParts,
childCount = 0,
parentRoomId = parentRoomId,
parentExitPart = parentExitPart,
}
floorData.rooms[id] = entry
floorData.activeCount += 1
for _, exitPart in ipairs(exitParts) do
addOpenExit(floorData, id, exitPart)
end
return entry, "success"
end
local function safeSpawnRoom(...)
local ok, newEntry, reason = pcall(spawnRoom, ...)
if not ok then
return nil, "blocked"
end
return newEntry, reason
end
local function sealExit(exitPart)
if exitPart and exitPart.Parent then
exitPart.Transparency = 0
exitPart.CanCollide = true
end
end
local function extendFromExit(floorData, sourceEntry, exitPart, permanent)
if not exitPart or not exitPart.Parent then
removeOpenExit(floorData, exitPart)
return nil
end
local dockCFrame = exitPart.CFrame * CFrame.new(0, 0, -DOOR_TO_DOOR_GAP_STUDS)
local newEntry, reason
for attempt = 1, MAX_PLACEMENT_ATTEMPTS do
newEntry, reason = safeSpawnRoom(sourceEntry.floor, permanent, dockCFrame, nil, sourceEntry, exitPart)
if newEntry or reason ~= "blocked" then
break
end
end
if not newEntry and reason == "blocked" and specialStaircaseTemplate then
newEntry, reason = safeSpawnRoom(sourceEntry.floor, permanent, dockCFrame, nil, sourceEntry, exitPart, specialStaircaseTemplate)
end
if newEntry then
removeOpenExit(floorData, exitPart)
sourceEntry.childCount += 1
elseif reason == "blocked" then
sealExit(exitPart)
removeOpenExit(floorData, exitPart)
end
return newEntry
end
local function destroyRoomEntry(floor, id)
local floorData = floors[floor]
local entry = floorData.rooms[id]
if not entry then
return
end
if entry.model then
entry.model:Destroy()
end
floorData.rooms[id] = nil
floorData.activeCount -= 1
for _, exitPart in ipairs(entry.exits) do
removeOpenExit(floorData, exitPart)
end
if entry.parentRoomId then
local parentEntry = floorData.rooms[entry.parentRoomId]
if parentEntry then
parentEntry.childCount -= 1
addOpenExit(floorData, entry.parentRoomId, entry.parentExitPart)
end
end
end
local function generatePermanentChain(floor)
local floorData = floors[floor]
local fallbackCFrame = CFrame.new(0, floorBaseY(floor), 0)
local currentEntry = spawnRoom(floor, true, nil, fallbackCFrame, nil, nil)
if not currentEntry then
return
end
local queue = {currentEntry}
local count = 1
while #queue > 0 and count < PERMANENT_ROOM_COUNT do
local entry = table.remove(queue, 1)
local exits = table.clone(entry.exits)
for _, exitPart in ipairs(exits) do
if count >= PERMANENT_ROOM_COUNT then
break
end
local isOpen = false
for _, oe in ipairs(floorData.openExits) do
if oe.exitPart == exitPart then
isOpen = true
break
end
end
if isOpen then
local ok, newEntry = pcall(extendFromExit, floorData, entry, exitPart, true)
if not ok then
sealExit(exitPart)
removeOpenExit(floorData, exitPart)
elseif newEntry then
table.insert(queue, newEntry)
count += 1
end
end
end
end
end
for f = 1, FLOOR_COUNT do
generatePermanentChain(f)
end
local function getPlayerFloorPositions()
local results = {}
for _, player in ipairs(Players:GetPlayers()) do
local character = player.Character
local rootPart = character and character:FindFirstChild("HumanoidRootPart")
if rootPart then
local pos = rootPart.Position
local closestFloor, closestDist = 1, math.huge
for f = 1, FLOOR_COUNT do
local dist = math.abs(pos.Y - floorBaseY(f))
if dist < closestDist then
closestFloor, closestDist = f, dist
end
end
table.insert(results, { position = pos, floor = closestFloor })
end
end
return results
end
local MAX_CASCADE_ITERATIONS_PER_PASS = 25
local function extendChains(floor, playerPositionsOnFloor)
local floorData = floors[floor]
if #playerPositionsOnFloor == 0 then
return
end
local iterations = 0
local didExtend = true
while didExtend and iterations < MAX_CASCADE_ITERATIONS_PER_PASS do
didExtend = false
iterations += 1
local exitsToCheck = table.clone(floorData.openExits)
for _, openExit in ipairs(exitsToCheck) do
local roomEntry = floorData.rooms[openExit.roomId]
if roomEntry then
local exitWorldPos = openExit.exitPart.Position
local nearPlayer = false
for _, playerPos in ipairs(playerPositionsOnFloor) do
if (exitWorldPos - playerPos).Magnitude <= LOAD_RADIUS then
nearPlayer = true
break
end
end
if nearPlayer then
local ok, newEntry = pcall(extendFromExit, floorData, roomEntry, openExit.exitPart, false)
if not ok then
sealExit(openExit.exitPart)
removeOpenExit(floorData, openExit.exitPart)
elseif newEntry then
didExtend = true
end
end
end
end
end
end
local function unloadFarRooms(floor, playerPositionsOnFloor)
local floorData = floors[floor]
local toRemove = {}
for id, entry in pairs(floorData.rooms) do
if not entry.permanent and entry.childCount == 0 then
local roomWorldPos = entry.model:GetPivot().Position
local nearAnyPlayer = false
for _, playerPos in ipairs(playerPositionsOnFloor) do
if (roomWorldPos - playerPos).Magnitude <= UNLOAD_RADIUS then
nearAnyPlayer = true
break
end
end
if not nearAnyPlayer then
table.insert(toRemove, id)
end
end
end
for _, id in ipairs(toRemove) do
destroyRoomEntry(floor, id)
end
end
local function streamingPass()
local playerFloorPositions = getPlayerFloorPositions()
if #playerFloorPositions == 0 then
return
end
local byFloor = {}
for f = 1, FLOOR_COUNT do
byFloor[f] = {}
end
for _, entry in ipairs(playerFloorPositions) do
table.insert(byFloor[entry.floor], entry.position)
end
for f = 1, FLOOR_COUNT do
extendChains(f, byFloor[f])
unloadFarRooms(f, byFloor[f])
end
end
task.spawn(function()
while true do
task.wait(UPDATE_INTERVAL)
pcall(streamingPass)
end
end)