
Keyless
-- 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)No comments yet for this script.