Facility Door Controls v6.0
Key Required

Facility Door Controls v6.0

Be an SCP or Guard [SCP-682]ContainsCatsContainsCats 104 views 20 executesUpdated Oct 3, 2026
Unrated
00
Lua Script•906 lines (31300 chars)
-- Remove any previous copy of this script (and stop its loops)
local env = (getgenv and getgenv()) or _G
if env.FacilityControlStop then pcall(env.FacilityControlStop) end
for _, g in ipairs(game:GetService("CoreGui"):GetChildren()) do
	if g.Name == "FacilityControlV5" or g.Name == "FacilityControl" then g:Destroy() end
end

local UserInputService = game:GetService("UserInputService")

-- Create ScreenGui inside CoreGui to persist across respawns
local screenGui = Instance.new("ScreenGui")
screenGui.Name = "FacilityControl"
screenGui.ResetOnSpawn = false
screenGui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling
screenGui.Parent = game:GetService("CoreGui")

-- GUI settings
local VERSION = "v6.0"
local TITLE_H = 36 -- Height of the title bar (also the height when minimized)
local MIN_W, MAX_W = 230, 520 -- Resize limits (width)
local MIN_H, MAX_H = 180, 700 -- Resize limits (height)

-- State variables
local running = true -- set to false when a newer copy of the script replaces this one
local autoCloseActive = false
local autoSprinklersActive = false
local spamActive = false
local connections = {} -- Event connections to clean up when replaced
local originalPositions = {} -- Closed-position baseline for single-part doors and chamber gates
local lastFired = {} -- Tracks when each door was last clicked (for auto-lock cooldown)
local AUTO_LOCK_COOLDOWN = 2.5 -- Seconds to wait after clicking a door before checking it again
local SPRINKLER_COOLDOWN = 3 -- Seconds to wait after clicking the sprinkler lever
local SPRINKLER_RESUME_AFTER = math.huge -- Seconds before auto sprinklers re-enable after someone turns them off (math.huge = never)
local SPAM_MIN_INTERVAL = 0.25 -- Shortest delay per detector
local SPAM_MAX_INTERVAL = 4 -- Longest delay; randomized after every click
local spamGeneration = 0 -- Invalidates workers on OFF/ON
local spamWorkers = {}
local spamRandom = Random.new()
local LIFT_COOLDOWN = 12 -- Seconds to wait after calling a lift up before checking it again
local CLOSED_TOLERANCE = 0.75 -- Studs of slack when deciding if two door panels are touching
local INVERT_096 = false -- Flips open/closed reading for the 096 gate (toggled by its button)
local INVERT_106 = false -- Flips open/closed reading for the grey box (106) gate (toggled by its button)

local function track(conn)
	table.insert(connections, conn)
	return conn
end

env.FacilityControlStop = function()
	running = false
	autoCloseActive = false
	autoSprinklersActive = false
	spamActive = false
	for _, c in ipairs(connections) do pcall(function() c:Disconnect() end) end
	pcall(function() screenGui:Destroy() end)
end


-- GUI --

local COLOR_PANEL   = Color3.fromRGB(28, 28, 33)
local COLOR_TITLE   = Color3.fromRGB(22, 22, 26)
local COLOR_CARD    = Color3.fromRGB(38, 38, 45)
local COLOR_STROKE  = Color3.fromRGB(60, 60, 70)
local COLOR_ACCENT  = Color3.fromRGB(110, 160, 255)
local COLOR_TEXT    = Color3.fromRGB(240, 240, 240)
local COLOR_MUTED   = Color3.fromRGB(150, 150, 160)
local COLOR_OFF     = Color3.fromRGB(75, 75, 80)

local expandedHeight = 450
local minimized = false

-- Main Frame Panel
local mainFrame = Instance.new("Frame")
mainFrame.Name = "MainPanel"
mainFrame.Size = UDim2.fromOffset(270, expandedHeight)
mainFrame.Position = UDim2.new(0.5, -135, 0.5, -225)
mainFrame.BackgroundColor3 = COLOR_PANEL
mainFrame.BorderSizePixel = 0
mainFrame.Active = true
mainFrame.ClipsDescendants = true
mainFrame.Parent = screenGui

local mainCorner = Instance.new("UICorner")
mainCorner.CornerRadius = UDim.new(0, 8)
mainCorner.Parent = mainFrame

local mainStroke = Instance.new("UIStroke")
mainStroke.Color = COLOR_STROKE
mainStroke.Thickness = 1
mainStroke.Parent = mainFrame

-- Title Bar (drag handle)
local titleBar = Instance.new("Frame")
titleBar.Name = "TitleBar"
titleBar.Size = UDim2.new(1, 0, 0, TITLE_H)
titleBar.BackgroundColor3 = COLOR_TITLE
titleBar.BorderSizePixel = 0
titleBar.Active = true
titleBar.Parent = mainFrame

local titleCorner = Instance.new("UICorner")
titleCorner.CornerRadius = UDim.new(0, 8)
titleCorner.Parent = titleBar

local titleLabel = Instance.new("TextLabel")
titleLabel.Size = UDim2.new(1, -80, 1, 0)
titleLabel.Position = UDim2.new(0, 12, 0, 0)
titleLabel.BackgroundTransparency = 1
titleLabel.RichText = true
titleLabel.Text = 'Facility Control | Created By ContainsCats. Follow ContainsCats on ScripBlox for more!  <font color="rgb(110,160,255)">' .. VERSION .. '</font>'
titleLabel.TextColor3 = COLOR_TEXT
titleLabel.TextSize = 15
titleLabel.Font = Enum.Font.SourceSansBold
titleLabel.TextXAlignment = Enum.TextXAlignment.Left
titleLabel.Parent = titleBar

local function createTitleButton(text, xOffset, textColor)
	local btn = Instance.new("TextButton")
	btn.Size = UDim2.new(0, 24, 0, 24)
	btn.Position = UDim2.new(1, xOffset, 0, 6)
	btn.BackgroundColor3 = Color3.fromRGB(50, 50, 55)
	btn.Text = text
	btn.TextColor3 = textColor
	btn.TextSize = 16
	btn.Font = Enum.Font.SourceSansBold
	btn.AutoButtonColor = true
	btn.Parent = titleBar
	
	local corner = Instance.new("UICorner")
	corner.CornerRadius = UDim.new(0, 4)
	corner.Parent = btn
	return btn
end

local minimizeBtn = createTitleButton("-", -58, COLOR_TEXT)
local closeGuiBtn = createTitleButton("X", -30, Color3.fromRGB(230, 80, 80))

-- Scrollable body that holds the button groups
local body = Instance.new("ScrollingFrame")
body.Name = "Body"
body.Position = UDim2.new(0, 0, 0, TITLE_H)
body.Size = UDim2.new(1, 0, 1, -TITLE_H)
body.BackgroundTransparency = 1
body.BorderSizePixel = 0
body.ScrollBarThickness = 4
body.ScrollBarImageColor3 = COLOR_STROKE
body.CanvasSize = UDim2.new(0, 0, 0, 0)
body.AutomaticCanvasSize = Enum.AutomaticSize.Y
body.Parent = mainFrame

local bodyLayout = Instance.new("UIListLayout")
bodyLayout.Padding = UDim.new(0, 8)
bodyLayout.SortOrder = Enum.SortOrder.LayoutOrder
bodyLayout.Parent = body

local bodyPadding = Instance.new("UIPadding")
bodyPadding.PaddingLeft = UDim.new(0, 8)
bodyPadding.PaddingRight = UDim.new(0, 10)
bodyPadding.PaddingTop = UDim.new(0, 8)
bodyPadding.PaddingBottom = UDim.new(0, 24) -- room for the resize grip
bodyPadding.Parent = body

-- Builds a group card with a heading and description. Buttons are parented to the returned card.
local function createGroup(title, description, order)
	local card = Instance.new("Frame")
	card.Name = title
	card.Size = UDim2.new(1, 0, 0, 0)
	card.AutomaticSize = Enum.AutomaticSize.Y
	card.BackgroundColor3 = COLOR_CARD
	card.BorderSizePixel = 0
	card.LayoutOrder = order
	card.Parent = body
	
	local corner = Instance.new("UICorner")
	corner.CornerRadius = UDim.new(0, 8)
	corner.Parent = card
	
	local stroke = Instance.new("UIStroke")
	stroke.Color = COLOR_STROKE
	stroke.Thickness = 1
	stroke.Parent = card
	
	local pad = Instance.new("UIPadding")
	pad.PaddingTop = UDim.new(0, 10)
	pad.PaddingBottom = UDim.new(0, 10)
	pad.PaddingLeft = UDim.new(0, 10)
	pad.PaddingRight = UDim.new(0, 10)
	pad.Parent = card
	
	local layout = Instance.new("UIListLayout")
	layout.Padding = UDim.new(0, 6)
	layout.SortOrder = Enum.SortOrder.LayoutOrder
	layout.Parent = card
	
	local header = Instance.new("TextLabel")
	header.Size = UDim2.new(1, 0, 0, 18)
	header.BackgroundTransparency = 1
	header.Text = string.upper(title)
	header.TextColor3 = COLOR_ACCENT
	header.TextSize = 13
	header.Font = Enum.Font.SourceSansBold
	header.TextXAlignment = Enum.TextXAlignment.Left
	header.LayoutOrder = 0
	header.Parent = card
	
	local desc = Instance.new("TextLabel")
	desc.Size = UDim2.new(1, 0, 0, 0)
	desc.AutomaticSize = Enum.AutomaticSize.Y
	desc.BackgroundTransparency = 1
	desc.Text = description
	desc.TextColor3 = COLOR_MUTED
	desc.TextSize = 13
	desc.Font = Enum.Font.SourceSans
	desc.TextWrapped = true
	desc.TextXAlignment = Enum.TextXAlignment.Left
	desc.TextYAlignment = Enum.TextYAlignment.Top
	desc.LayoutOrder = 1
	desc.Parent = card
	
	return card
end

-- Full-width button inside a group card
local function createButton(parent, text, color, order)
	local btn = Instance.new("TextButton")
	btn.Size = UDim2.new(1, 0, 0, 36)
	btn.BackgroundColor3 = color
	btn.Text = text
	btn.TextColor3 = Color3.fromRGB(255, 255, 255)
	btn.TextSize = 15
	btn.Font = Enum.Font.SourceSansBold
	btn.LayoutOrder = order
	btn.Parent = parent
	
	local corner = Instance.new("UICorner")
	corner.CornerRadius = UDim.new(0, 6)
	corner.Parent = btn
	return btn
end

-- A row holding two half-width buttons side by side
local function createSplitRow(parent, order)
	local row = Instance.new("Frame")
	row.Size = UDim2.new(1, 0, 0, 36)
	row.BackgroundTransparency = 1
	row.LayoutOrder = order
	row.Parent = parent
	return row
end

local function createHalfButton(row, text, rightSide)
	local btn = Instance.new("TextButton")
	btn.Size = UDim2.new(0.5, -3, 1, 0)
	btn.Position = rightSide and UDim2.new(0.5, 3, 0, 0) or UDim2.new(0, 0, 0, 0)
	btn.BackgroundColor3 = COLOR_OFF
	btn.Text = text
	btn.TextColor3 = Color3.fromRGB(255, 255, 255)
	btn.TextSize = 14
	btn.Font = Enum.Font.SourceSansBold
	btn.Parent = row
	
	local corner = Instance.new("UICorner")
	corner.CornerRadius = UDim.new(0, 6)
	corner.Parent = btn
	return btn
end

-- GROUP 1: Door Control
local doorGroup = createGroup("Door Control",
	"One-shot commands that close or open every door and gate in the facility.", 1)
local closeDoorsBtn = createButton(doorGroup, "Force Close All Doors", Color3.fromRGB(180, 55, 55), 2)
local openDoorsBtn  = createButton(doorGroup, "Force Open All Doors", Color3.fromRGB(55, 150, 55), 3)

-- GROUP 2: Automation
local autoGroup = createGroup("Automation",
	"Loops that run until you switch them off. Auto-Lock re-closes any door that gets opened and keeps the lifts parked on top. Auto Sprinklers turns the sprinklers on once.", 2)
local autoCloseBtn     = createButton(autoGroup, "Auto-Lock Loop: OFF", COLOR_OFF, 2)
local autoSprinklerBtn = createButton(autoGroup, "Auto Sprinklers: OFF", COLOR_OFF, 3)

-- GROUP 3: Chamber Gates
local gateGroup = createGroup("Chamber Gates",
	"If the 096 gate or the grey box (106) gate acts backwards, press its button to invert how open and closed are read.", 3)
local gateRow = createSplitRow(gateGroup, 2)
local invert096Btn = createHalfButton(gateRow, "096: Normal", false)
local invert106Btn = createHalfButton(gateRow, "106: Normal", true)

-- GROUP 4: Utilities
local utilGroup = createGroup("Utilities",
	"Spam clicks gameplay buttons independently at random intervals, skipping purchases, morphs, clothing changes and team selectors. The report prints to F9 and copies door, lift and sprinkler info.", 4)
local spamButtonsBtn = createButton(utilGroup, "Spam Gameplay Buttons: OFF", Color3.fromRGB(55, 110, 180), 2)
local debugBtn       = createButton(utilGroup, "Print & Copy Door States", Color3.fromRGB(90, 70, 140), 3)

-- Footer hint
local hint = Instance.new("TextLabel")
hint.Size = UDim2.new(1, 0, 0, 16)
hint.BackgroundTransparency = 1
hint.Text = "Press P to show / hide this panel"
hint.TextColor3 = COLOR_MUTED
hint.TextSize = 12
hint.Font = Enum.Font.SourceSans
hint.LayoutOrder = 5
hint.Parent = body

-- Resize grip (bottom-right corner)
local resizeGrip = Instance.new("TextButton")
resizeGrip.Name = "ResizeGrip"
resizeGrip.Size = UDim2.new(0, 20, 0, 20)
resizeGrip.Position = UDim2.new(1, -20, 1, -20)
resizeGrip.BackgroundTransparency = 1
resizeGrip.Text = "◢"
resizeGrip.TextColor3 = COLOR_MUTED
resizeGrip.TextSize = 16
resizeGrip.Font = Enum.Font.SourceSansBold
resizeGrip.ZIndex = 5
resizeGrip.Parent = mainFrame

-- Dragging (title bar) and resizing (grip)
local dragging, dragStart, dragStartPos = false, nil, nil
local resizing, resizeStart, resizeStartSize = false, nil, nil

local function isPointerStart(input)
	return input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch
end

track(titleBar.InputBegan:Connect(function(input)
	if isPointerStart(input) then
		dragging = true
		dragStart = input.Position
		dragStartPos = mainFrame.Position
		input.Changed:Connect(function()
			if input.UserInputState == Enum.UserInputState.End then dragging = false end
		end)
	end
end))

track(resizeGrip.InputBegan:Connect(function(input)
	if isPointerStart(input) and not minimized then
		resizing = true
		resizeStart = input.Position
		resizeStartSize = mainFrame.AbsoluteSize
		input.Changed:Connect(function()
			if input.UserInputState == Enum.UserInputState.End then resizing = false end
		end)
	end
end))

track(UserInputService.InputChanged:Connect(function(input)
	if input.UserInputType ~= Enum.UserInputType.MouseMovement and input.UserInputType ~= Enum.UserInputType.Touch then
		return
	end
	
	if dragging then
		local d = input.Position - dragStart
		mainFrame.Position = UDim2.new(
			dragStartPos.X.Scale, dragStartPos.X.Offset + d.X,
			dragStartPos.Y.Scale, dragStartPos.Y.Offset + d.Y)
	elseif resizing then
		local d = input.Position - resizeStart
		local w = math.clamp(resizeStartSize.X + d.X, MIN_W, MAX_W)
		local h = math.clamp(resizeStartSize.Y + d.Y, MIN_H, MAX_H)
		expandedHeight = h
		mainFrame.Size = UDim2.fromOffset(w, h)
	end
end))

-- Minimize / restore
minimizeBtn.MouseButton1Click:Connect(function()
	minimized = not minimized
	body.Visible = not minimized
	resizeGrip.Visible = not minimized
	minimizeBtn.Text = minimized and "+" or "-"
	local w = mainFrame.AbsoluteSize.X
	mainFrame.Size = UDim2.fromOffset(w, minimized and TITLE_H or expandedHeight)
end)

-- Toggle Visibility Hook (X Button hides, pressing 'P' shows / hides)
closeGuiBtn.MouseButton1Click:Connect(function()
	mainFrame.Visible = false
end)

track(UserInputService.InputBegan:Connect(function(input, gameProcessed)
	if not gameProcessed and input.KeyCode == Enum.KeyCode.P then
		mainFrame.Visible = not mainFrame.Visible
	end
end))


-- CORE LOGIC FOR SYSTEM FILTERING AND STATE MANAGEMENT --

-- Ancestor-name keywords that mark a button as off-limits (purchases, morphs, teams, spawns)
local BLOCKED_KEYWORDS = {
	"gamepass", "game pass", "robux", "donate", "buy", "purchase", "product",
	"vip", "shop", "team", "morph", "skin", "outfit", "costume", "cosmetic", "spawn",
	"cloth", "shirt", "pants", "trouser", "uniform", "dress", "wardrobe", "accessor", "avatar",
}

-- Filter out purchasing interfaces, dev products, gamepasses, morphs, and team selectors
local function isSafeGameplayButton(detector)
	local parent = detector.Parent
	if not parent then return false end
	
	local current = detector
	while current and current ~= workspace do
		local nameLower = string.lower(current.Name)
		for _, word in ipairs(BLOCKED_KEYWORDS) do
			if string.find(nameLower, word, 1, true) then
				return false
			end
		end
		current = current.Parent
	end
	return true
end

-- A "door" is any model named Door/Gate, inside a Gates folder, or containing a Door1 part
-- (this also catches things like Lift > BottomGate). The Lift model itself is NOT a door.
local function isDoorModel(model)
	if not model:IsA("Model") then return false end
	if model.Name == "Lift" then return false end
	if model.Name == "Gate" or model.Name == "Door" then return true end
	if model.Parent and string.lower(model.Parent.Name) == "gates" then return true end
	local d1 = model:FindFirstChild("Door1")
	if d1 and d1:IsA("BasePart") then return true end
	return false
end

-- True for the TopGate / BottomGate models that belong to a lift
local function isLiftGate(model)
	return model.Parent ~= nil and model.Parent.Name == "Lift"
		and (model.Name == "TopGate" or model.Name == "BottomGate")
end

-- Picks the part used to track whether a door has moved.
-- Returns the part and whether it's a real door panel (Door1/Door) vs a chamber's Main part.
local function getReferencePart(model)
	local p = model:FindFirstChild("Door1") or model:FindFirstChild("Door")
	if p and p:IsA("BasePart") then return p, true end
	
	p = model:FindFirstChild("Main")
	if p and p:IsA("BasePart") then return p, false end
	
	if model.PrimaryPart then return model.PrimaryPart, false end
	return nil, false
end

-- Baseline positions (assumed CLOSED at launch) for doors that only have a single moving part
local function cacheOriginalPositions()
	for _, model in ipairs(workspace:GetDescendants()) do
		if isDoorModel(model) then
			local ref = getReferencePart(model)
			if ref and not originalPositions[ref] then
				originalPositions[ref] = ref.Position
			end
		end
	end
end
cacheOriginalPositions()

-- Returns true if the door is open. Also returns the measured gap/movement (for debugging).
local function isGateOpen(model)
	local ref, isPanel = getReferencePart(model)
	if not ref then return false, 0 end
	
	-- Instant-delete style doors (only meaningful on real door panels)
	if isPanel and (ref.Transparency > 0.5 or ref.CanCollide == false) then
		return true, 0
	end
	
	-- Two-panel doors: Door1 and Door2 touch when closed and slide apart when open
	local door2 = model:FindFirstChild("Door2")
	if isPanel and door2 and door2:IsA("BasePart") then
		local rel = ref.CFrame:VectorToObjectSpace(door2.Position - ref.Position)
		local comps = {rel.X, rel.Y, rel.Z}
		local sizes1 = {ref.Size.X, ref.Size.Y, ref.Size.Z}
		local sizes2 = {door2.Size.X, door2.Size.Y, door2.Size.Z}
		
		-- The slide axis is whichever axis the panels are offset along the most
		local axis = 1
		for i = 2, 3 do
			if math.abs(comps[i]) > math.abs(comps[axis]) then axis = i end
		end
		
		local gap = math.abs(comps[axis])
		local closedGap = (sizes1[axis] + sizes2[axis]) / 2
		return gap > closedGap + CLOSED_TOLERANCE, gap - closedGap
	end
	
	-- Single-part doors and chamber gates: open = moved away from the saved position
	local initialPos = originalPositions[ref]
	if initialPos then
		local moved = (ref.Position - initialPos).Magnitude
		local open = moved > 1.5
		
		if not isPanel then
			-- Grey box (106) gate has plain "Wall" children; the 096 gate does not
			if model:FindFirstChild("Wall") then
				if INVERT_106 then open = not open end
			else
				if INVERT_096 then open = not open end
			end
		end
		
		return open, moved
	end
	return false, 0
end

-- Finds ONE safe ClickDetector for a door. Doors with two button hitboxes
-- (one per side) toggle twice if both are fired, which cancels the action out.
local function findDoorClickDetector(model)
	for _, d in ipairs(model:GetDescendants()) do
		if d:IsA("ClickDetector") and isSafeGameplayButton(d) then
			return d
		end
	end
	return nil
end

-- action: "close" or "open"
-- cooldown (optional): seconds to skip a door after it was last clicked.
-- skipLifts (optional): leave lift gates alone (used by auto-lock).
local function executeSynchronizedAction(action, cooldown, skipLifts)
	cacheOriginalPositions()

	for _, model in ipairs(workspace:GetDescendants()) do
		if isDoorModel(model) and not (skipLifts and isLiftGate(model)) then
			
			local onCooldown = cooldown and lastFired[model] and (os.clock() - lastFired[model]) < cooldown
			
			if not onCooldown then
				local isOpen = isGateOpen(model)
				
				-- Close = click doors that are open. Open = click doors that are closed.
				local shouldFire = (action == "close" and isOpen) or (action == "open" and not isOpen)
				
				if shouldFire then
					local cd = findDoorClickDetector(model)
					if cd then
						fireclickdetector(cd) -- only ONE click per door
						lastFired[model] = os.clock()
					end
				end
			end
		end
	end
end

-- Each detector has independent timing, including detectors added later.
local function startSpamWorker(cd)
	if not running or not spamActive or not cd:IsA("ClickDetector")
		or not cd:IsDescendantOf(workspace) or not isSafeGameplayButton(cd) or spamWorkers[cd] then
		return
	end
	local generation = spamGeneration
	local worker = {}
	spamWorkers[cd] = worker
	task.spawn(function()
		while running and spamActive and spamGeneration == generation
			and spamWorkers[cd] == worker and cd:IsDescendantOf(workspace) do
			-- Stagger the first click as well as every subsequent click.
			task.wait(spamRandom:NextNumber(SPAM_MIN_INTERVAL, SPAM_MAX_INTERVAL))
			if not running or not spamActive or spamGeneration ~= generation
				or spamWorkers[cd] ~= worker or not cd:IsDescendantOf(workspace) then
				break
			end
			-- Recheck in case the detector was renamed or moved into an excluded group.
			if isSafeGameplayButton(cd) then
				pcall(function() fireclickdetector(cd) end)
			end
		end
		if spamWorkers[cd] == worker then
			spamWorkers[cd] = nil
		end
	end)
end

track(workspace.DescendantAdded:Connect(startSpamWorker))
track(workspace.DescendantRemoving:Connect(function(instance)
	spamWorkers[instance] = nil
end))


-- LIFT LOGIC (keeps lifts parked on the top floor during auto-lock) --

local liftCache = {}
local liftCacheTime = -math.huge
local liftState = {} -- [lift] = {lastPos, lastCall}

local function getLifts()
	if os.clock() - liftCacheTime > 10 then
		liftCache = {}
		for _, m in ipairs(workspace:GetDescendants()) do
			if m:IsA("Model") and m.Name == "Lift"
				and m:FindFirstChild("Platform") and m:FindFirstChild("TopGate") and m:FindFirstChild("BottomGate") then
				table.insert(liftCache, m)
			end
		end
		liftCacheTime = os.clock()
	end
	return liftCache
end

-- Static reference point for a lift gate (its DoorHitbox if it has one)
local function liftGatePos(gate)
	local hb = gate:FindFirstChild("DoorHitbox")
	if hb and hb:IsA("BasePart") then return hb.Position end
	return gate:GetPivot().Position
end

-- True if the platform is closer to the top gate than the bottom gate
local function isLiftAtTop(lift)
	local pos = lift.Platform:GetPivot().Position
	local topDist = (pos - liftGatePos(lift.TopGate)).Magnitude
	local bottomDist = (pos - liftGatePos(lift.BottomGate)).Magnitude
	return topDist <= bottomDist
end

-- If a lift has stopped at the bottom, call it up using its TopGate button
local function keepLiftsOnTop()
	for _, lift in ipairs(getLifts()) do
		if lift.Parent and lift:FindFirstChild("Platform") then
			local st = liftState[lift]
			if not st then
				st = {lastPos = nil, lastCall = -math.huge}
				liftState[lift] = st
			end
			
			local pos = lift.Platform:GetPivot().Position
			local stationary = st.lastPos ~= nil and (pos - st.lastPos).Magnitude < 0.05
			st.lastPos = pos
			
			if stationary and not isLiftAtTop(lift) and (os.clock() - st.lastCall) >= LIFT_COOLDOWN then
				local cd = findDoorClickDetector(lift.TopGate)
				if cd then
					fireclickdetector(cd)
					st.lastCall = os.clock()
				end
			end
		end
	end
end


-- SPRINKLER LOGIC --

local sprinklerLeverCD = nil -- Cached lever ClickDetector
local sprinklerEmitters = {} -- Cached sprinkler particle emitters
local sprinklerSounds = {} -- Cached sprinkler audio
local lastSprinklerClick = -math.huge
local lastSprinklerSearch = -math.huge
local warnedNoParts = false
local warnedUnverified = false
local sprinklersSeenOn = false -- True once we've watched them be ON during this auto session
local sprinklerPausedAt = nil -- Time someone else turned them off (auto stays hands-off)
local sprinklerUnverified = 0 -- Clicks we made that we haven't yet seen turn the sprinklers ON
local sprinklerBlindClicked = false -- Fallback: one click made when the state can't be read

-- Finds the lever ClickDetector, sprinkler emitters and sprinkler audio
local function findSprinklerParts()
	lastSprinklerSearch = os.clock()
	sprinklerLeverCD = nil
	sprinklerEmitters = {}
	sprinklerSounds = {}
	
	for _, d in ipairs(workspace:GetDescendants()) do
		if d:IsA("ClickDetector") and not sprinklerLeverCD
			and d:FindFirstAncestor("Sprinkler System Controls") and isSafeGameplayButton(d) then
			sprinklerLeverCD = d
		elseif d:FindFirstAncestor("Sprinkler System") then
			if d:IsA("ParticleEmitter") then
				table.insert(sprinklerEmitters, d)
			elseif d:IsA("Sound") then
				table.insert(sprinklerSounds, d)
			end
		end
	end
end

-- Sprinklers count as ON if an emitter is actively emitting OR the sprinkler audio is playing
local function areSprinklersOn()
	for _, e in ipairs(sprinklerEmitters) do
		if e.Parent and e.Enabled and e.Rate > 0 then
			return true
		end
	end
	for _, s in ipairs(sprinklerSounds) do
		if s.Parent and s.IsPlaying then
			return true
		end
	end
	return false
end

local function runAutoSprinklers()
	-- Re-find parts if we don't have them (searched at most every 5 seconds)
	if (not sprinklerLeverCD or not sprinklerLeverCD.Parent or (#sprinklerEmitters + #sprinklerSounds) == 0)
		and (os.clock() - lastSprinklerSearch) > 5 then
		findSprinklerParts()
	end
	
	if not sprinklerLeverCD then
		if not warnedNoParts then
			warnedNoParts = true
			warn("[Auto Sprinklers] Couldn't find the sprinkler lever. Not clicking.")
		end
		return
	end
	
	-- Fallback: the state can't be read, so click the lever ONCE and then stay hands-off
	if (#sprinklerEmitters + #sprinklerSounds) == 0 then
		if not sprinklerBlindClicked then
			sprinklerBlindClicked = true
			fireclickdetector(sprinklerLeverCD)
			warn("[Auto Sprinklers] Can't read the sprinkler state, so the lever was clicked once. Make sure they were OFF when you enabled this.")
		end
		return
	end
	
	if areSprinklersOn() then
		sprinklersSeenOn = true
		sprinklerPausedAt = nil
		sprinklerUnverified = 0
		return
	end
	
	-- Sprinklers are OFF. If we'd seen them ON, someone turned them off, so leave them off.
	if sprinklersSeenOn and not sprinklerPausedAt then
		sprinklerPausedAt = os.clock()
		sprinklersSeenOn = false
	end
	
	if sprinklerPausedAt then
		if (os.clock() - sprinklerPausedAt) < SPRINKLER_RESUME_AFTER then
			return -- stay off
		end
		sprinklerPausedAt = nil -- resume delay passed
	end
	
	-- We already clicked once and never saw them turn ON: stop instead of flipping them back off
	if sprinklerUnverified >= 1 then
		if not warnedUnverified then
			warnedUnverified = true
			warn("[Auto Sprinklers] Clicked the lever but the sprinklers never read as ON. Press 'Print & Copy Door States' and check the SPRINKLERS section.")
		end
		return
	end
	
	if (os.clock() - lastSprinklerClick) >= SPRINKLER_COOLDOWN then
		fireclickdetector(sprinklerLeverCD)
		lastSprinklerClick = os.clock()
		sprinklerUnverified += 1
	end
end

-- Builds the report, prints it to F9, and copies it to the clipboard.
-- Returns true if the clipboard copy worked.
local function printDoorStates()
	local lines = {"---------- DOOR STATES ----------"}
	local count = 0
	for _, model in ipairs(workspace:GetDescendants()) do
		if isDoorModel(model) then
			count += 1
			local isOpen, measure = isGateOpen(model)
			local ref = getReferencePart(model)
			table.insert(lines, string.format("%s | %s | ref: %s | open-gap: %.2f",
				model:GetFullName(), isOpen and "OPEN" or "CLOSED", ref and ref.Name or "NONE", measure or 0))
		end
	end
	table.insert(lines, "Total doors found: " .. count)
	
	table.insert(lines, "---------- LIFTS ----------")
	for _, lift in ipairs(getLifts()) do
		if lift.Parent then
			table.insert(lines, string.format("%s | platform at %s", lift:GetFullName(), isLiftAtTop(lift) and "TOP" or "BOTTOM"))
		end
	end
	
	findSprinklerParts()
	table.insert(lines, "---------- SPRINKLERS ----------")
	table.insert(lines, "Lever ClickDetector: " .. (sprinklerLeverCD and sprinklerLeverCD:GetFullName() or "NOT FOUND"))
	for i, e in ipairs(sprinklerEmitters) do
		table.insert(lines, string.format("Emitter %d: Enabled=%s Rate=%s", i, tostring(e.Enabled), tostring(e.Rate)))
	end
	for i, s in ipairs(sprinklerSounds) do
		table.insert(lines, string.format("Sound %d: IsPlaying=%s Looped=%s", i, tostring(s.IsPlaying), tostring(s.Looped)))
	end
	-- List anything sprinkler-related with its class so we can see what the objects really are
	for _, d in ipairs(workspace:GetDescendants()) do
		if d:FindFirstAncestor("Sprinkler System")
			and (d.Name == "Sprinkler" or d.Name == "Emitter" or d.Name == "SprinklerAudio" or d.Name == "Sound") then
			table.insert(lines, string.format("Found: %s [%s]", d:GetFullName(), d.ClassName))
		end
	end
	table.insert(lines, "Script reads sprinklers as: " .. (areSprinklersOn() and "ON" or "OFF"))
	
	local report = table.concat(lines, "\n")
	print(report)
	
	local copyFn = setclipboard or toclipboard
	if copyFn then
		local ok = pcall(copyFn, report)
		return ok
	end
	return false
end


-- BUTTON ACTIONS & CONNECTIONS --

closeDoorsBtn.MouseButton1Click:Connect(function()
	executeSynchronizedAction("close")
end)

openDoorsBtn.MouseButton1Click:Connect(function()
	executeSynchronizedAction("open")
end)

-- Spam Gameplay Buttons toggle
spamButtonsBtn.MouseButton1Click:Connect(function()
	spamActive = not spamActive
	spamGeneration += 1
	table.clear(spamWorkers)
	if spamActive then
		spamButtonsBtn.Text = "Spam Gameplay Buttons: ON"
		spamButtonsBtn.BackgroundColor3 = Color3.fromRGB(40, 80, 150)
		for _, cd in ipairs(workspace:GetDescendants()) do
			startSpamWorker(cd)
		end
	else
		spamButtonsBtn.Text = "Spam Gameplay Buttons: OFF"
		spamButtonsBtn.BackgroundColor3 = Color3.fromRGB(55, 110, 180)
	end
end)

-- 096 / 106 invert toggles: flip how open/closed is read for that gate
invert096Btn.MouseButton1Click:Connect(function()
	INVERT_096 = not INVERT_096
	invert096Btn.Text = INVERT_096 and "096: Inverted" or "096: Normal"
	invert096Btn.BackgroundColor3 = INVERT_096 and Color3.fromRGB(170, 120, 40) or COLOR_OFF
end)

invert106Btn.MouseButton1Click:Connect(function()
	INVERT_106 = not INVERT_106
	invert106Btn.Text = INVERT_106 and "106: Inverted" or "106: Normal"
	invert106Btn.BackgroundColor3 = INVERT_106 and Color3.fromRGB(170, 120, 40) or COLOR_OFF
end)

debugBtn.MouseButton1Click:Connect(function()
	local copied = printDoorStates()
	debugBtn.Text = copied and "Copied!" or "Copy not supported"
	task.delay(1.5, function()
		debugBtn.Text = "Print & Copy Door States"
	end)
end)

-- Auto-Lock Loop implementation (also keeps the lifts parked on top)
local warnedLift = false
autoCloseBtn.MouseButton1Click:Connect(function()
	autoCloseActive = not autoCloseActive
	if autoCloseActive then
		autoCloseBtn.Text = "Auto-Lock Loop: ON"
		autoCloseBtn.BackgroundColor3 = Color3.fromRGB(150, 45, 45)
		
		task.spawn(function()
			while running and autoCloseActive do
				executeSynchronizedAction("close", AUTO_LOCK_COOLDOWN, true) -- true = leave lift gates alone
				local ok, err = pcall(keepLiftsOnTop)
				if not ok and not warnedLift then
					warnedLift = true
					warn("[Lift] " .. tostring(err))
				end
				task.wait(0.3)
			end
		end)
	else
		autoCloseBtn.Text = "Auto-Lock Loop: OFF"
		autoCloseBtn.BackgroundColor3 = COLOR_OFF
	end
end)

-- Auto Sprinklers implementation
autoSprinklerBtn.MouseButton1Click:Connect(function()
	autoSprinklersActive = not autoSprinklersActive
	if autoSprinklersActive then
		autoSprinklerBtn.Text = "Auto Sprinklers: ON"
		autoSprinklerBtn.BackgroundColor3 = Color3.fromRGB(55, 130, 170)
		warnedNoParts = false
		warnedUnverified = false
		sprinklersSeenOn = false -- fresh session: turn them on once, then respect anyone turning them off
		sprinklerPausedAt = nil
		sprinklerUnverified = 0
		sprinklerBlindClicked = false
		findSprinklerParts() -- Refresh in case the map changed
		
		task.spawn(function()
			while running and autoSprinklersActive do
				runAutoSprinklers()
				task.wait(0.5)
			end
		end)
	else
		autoSprinklerBtn.Text = "Auto Sprinklers: OFF"
		autoSprinklerBtn.BackgroundColor3 = COLOR_OFF
	end
end)

Features & Information

This Script can... - Force Close All Doors: Attempts to close every detected door and gate using its current position or state. - Force Open All Doors: Attempts to open every detected door and gate. - Auto-Lock Loop: Repeatedly closes doors that open and calls stopped lifts back to the top floor. - Auto Sprinklers: Turns sprinklers on once. If someone turns them off afterward, it leaves them off until you restart the toggle. - 096 / 106 Gate Inversion: Reverses how the script interprets each chamber gate’s open/closed state if its controls behave backward. So if the script keeps repeately opening the 096v or 106 chambers just click this button on the chamber tahts acting up to change teh driection of where this closes. Thsi happens because the script reads it as "I dont know whats close or open so I will use the location of where its at now as close" so if its open in a public server already, the script will read the chamber as close even if its open. Thats why theres an inversion button for the script. - Spam Gameplay Buttons: Clicks eligible controls independently, choosing a new random 0.25–4 second interval before each click. Includes doors, gates, lift controls, and newly added ClickDetectors. - Spam Exclusions: Skips controls identified as morphs, gamepasses, purchases, clothing, cosmetics, team selectors, or spawns. Identification uses etector and ancestor names, so unusually named controls may need extra exclusions. - Print & Copy Door States: Prints a diagnostic report to the F9 console and attempts to copy it to your clipboard. Includes door states, lift positions, and sprinkler details. (THIS IS MAINLY FOR WHEN I WAS CREATING THE SCRIPT. ALTHOUGH YOU CAN USE IT TO IMPROVE THE SCRIPT FOR YOUR OWN) Panel controls - Drag the title bar: Moves the panel. - Drag the bottom-right corner: Resizes the panel. - Minimize (−): Collapses the panel while active features keep running. - X or P: Closes the GUI and stops all loops and pending tasks started by this script. - Automatic cleanup: Also stops the script’s tasks if the GUI is removed, destroyed, hidden, or disabled. Running a new copy shuts down the previous copy first. (might be broken) Door detection depends on the map’s structure. Single-part doors and chamber gates are assumed to be closed when the script first runs. NOTE: I was banned from the game but have no idea if it was the script, me flying, or another script I was using which is unrelated. so in any case test the script on an alt account and send in a review to help others :D btw open source. pls improve exploits :p USE SCRIPT AT YOUR OWN RISK. RECOMMENDED USE ALT TO TEST BEFORE PLAY!!! USE SCRIPT AT YOUR OWN RISK. RECOMMENDED USE ALT TO TEST BEFORE PLAY!!! USE SCRIPT AT YOUR OWN RISK. RECOMMENDED USE ALT TO TEST BEFORE PLAY!!! USE SCRIPT AT YOUR OWN RISK. RECOMMENDED USE ALT TO TEST BEFORE PLAY!!! USE SCRIPT AT YOUR OWN RISK. RECOMMENDED USE ALT TO TEST BEFORE PLAY!!! USE SCRIPT AT YOUR OWN RISK. RECOMMENDED USE ALT TO TEST BEFORE PLAY!!!
#SCP#Admin#Doors#Open Source#pannel

Community Comments (0)

No comments yet for this script.

More Scripts for Be an SCP or Guard [SCP-682]