--[[
── Robscript.com ────────────────────────────────────────────────────────────
Game: Still Water (placeId 139386728420706, group 1065149497)
Loader: Potassium, or any executor with loadstring + game:HttpGet
UI: Airflow (OuroFlow) — the library kept in the Obsidian vault
(note "Great Wall Hub" → Robly_Airflow)
Contents:
• ESP with its own settings: pick what is drawn and how it is drawn
• Listener radar, proximity alarm
• Puzzle helpers: room 9 door code, room 7 memory bridge
• One movement feature: Speed boost
Speed boost, honestly:
The game rewrites Humanoid.WalkSpeed every frame, so the boost is applied
as a LinearVelocity constraint instead. Exactly one constraint is ever
created, it ramps up over ~0.3 s rather than jumping, it only pushes on
the horizontal axes so gravity and jump height are untouched, it is off
in water, and it is capped.
A raw jump to a high velocity is what a server-side movement check reacts
to; a smooth, capped push in the direction you already walk is the least
suspicious thing this kind of feature can do.
Terminology:
Listener — the monster (Config.Run.CREATURE.STYLE is literally "Listener")
Highlight — a Roblox `Highlight` instance: the through-wall outline
Billboard — a `BillboardGui`: the floating label above a part
Tag — a Roblox attribute, e.g. `Platform`, `Safe`, `MemTile`, `State`
]]
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local SoundService = game:GetService("SoundService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TweenService = game:GetService("TweenService")
local LocalPlayer = Players.LocalPlayer
-- ─────────────────────────────────────────────────────────────────────────────
-- 0. UI library
-- ─────────────────────────────────────────────────────────────────────────────
-- Airflow has two incompatible branches; only the full one exposes Colorize
-- (and groupboxes, statuses, themes), so we detect it instead of trusting a URL.
local AIRFLOW_URLS = {
"https://raw.githubusercontent.com/bobloscript/Robly_Airflow/main/Source.luau",
"https://raw.githubusercontent.com/PookiePepelsss/Airflow-UI/refs/heads/main/Source.luau",
}
local Airflow
for _, url in ipairs(AIRFLOW_URLS) do
local ok, library = pcall(function()
return loadstring(game:HttpGet(url))()
end)
if ok and type(library) == "table" and library.CreateWindow and library.Colorize then
Airflow = library
break
end
end
if not Airflow then
error("[Robscript.com] could not load Airflow (the full Robly_Airflow branch is required)")
end
-- ─────────────────────────────────────────────────────────────────────────────
-- 1. Game data, copied from ReplicatedStorage.Config.*
-- ─────────────────────────────────────────────────────────────────────────────
local GAME = {
rooms = {
[1] = { name = "PILLAR HALL", hint = "Jump across the platforms to the exit." },
[2] = { name = "BROKEN WALKWAY", hint = "Narrow ledges with short jumps." },
[3] = { name = "SINKING PLATFORMS", hint = "Platforms sink while you stand on them." },
[4] = { name = "ROTATING BRIDGE", hint = "Ride the rotating bridges; time the jumps." },
[5] = { name = "MOVING PLATFORMS", hint = "Every platform moves." },
[6] = { name = "DRIFTING PLATFORMS", hint = "The arrows push you; do not get carried into the water." },
[7] = { name = "MEMORY BRIDGE", hint = "Remember which tiles lit up." },
[8] = { name = "CRUSHING WALLS", hint = "The walls slide in; one lane always stays open." },
[9] = { name = "LOCKED EXIT", hint = "Find the 4 digits on the pillars and photograph each one." },
},
modes = {
STARTER = { mult = 0.5, monsters = 1, roomLength = 780 },
EASY = { mult = 1.0, monsters = 3, roomLength = 1300 },
HARD = { mult = 1.5, monsters = 5, roomLength = 1690 },
},
listener = {
patrolSpeed = 16, chaseSpeed = 120, killRange = 10,
searchTime = 2.2, attackTime = 0.55,
},
movement = { walk = 22, wade = 10, jump = 56 },
rewards = { rooms = { 20, 30, 40, 50, 50, 55, 60, 65, 70 }, escape = 100, noSplash = 30, rescue = 10 },
upgrades = {
{ id = "Range", name = "Throwing Range", price = { 150, 400, 900 } },
{ id = "Skip", name = "Pebble Skipping", price = { 150, 400, 900 } },
{ id = "Battery", name = "Toy Battery", price = { 150, 400, 900 } },
{ id = "FlareRadius", name = "Flare Radius", price = { 150, 400, 900 } },
{ id = "RopeReach", name = "Rope Reach", price = { 150, 400, 900 } },
{ id = "RescueSpeed", name = "Rescue Speed", price = { 150, 400, 900 } },
},
equipment = {
{ id = "Pebble", name = "Pebbles", unlock = 0, desc = "Skips across the water and leads the Listener away." },
{ id = "Bottle", name = "Bottles", unlock = 150, desc = "One loud splash; pulls it there from far away." },
{ id = "Toy", name = "Wind-up Toy", unlock = 300, desc = "Waddles through the water, making noise." },
{ id = "Flare", name = "Flare", unlock = 450, desc = "Stuns it for 5 s, then it is immune for 8 s." },
{ id = "Rope", name = "Rescue Rope", unlock = 250, desc = "Pull a teammate onto a platform, once per room." },
},
}
local STATE_COLORS = {
Patrol = Color3.fromRGB(80, 255, 150),
Search = Color3.fromRGB(255, 210, 60),
Hunt = Color3.fromRGB(255, 140, 40),
Chase = Color3.fromRGB(255, 60, 60),
Attack = Color3.fromRGB(255, 40, 180),
Caught = Color3.fromRGB(200, 0, 255),
}
-- ─────────────────────────────────────────────────────────────────────────────
-- 2. Lookups and formatters
-- ─────────────────────────────────────────────────────────────────────────────
-- The map lives under Workspace.StillWater: the creatures, the lobby,
-- Room1..Room9, Corridor1..Corridor8 and the escape corridor.
local function stillWater()
return workspace:FindFirstChild("StillWater")
end
local function listenerFolder()
local root = stillWater()
return root and root:FindFirstChild("Creatures")
end
local function pebbleFolder()
local root = stillWater()
return root and root:FindFirstChild("PebblePickups")
end
local function character()
return LocalPlayer.Character
end
local function humanoid()
local model = character()
return model and model:FindFirstChildOfClass("Humanoid")
end
local function rootPart()
local model = character()
return model and model:FindFirstChild("HumanoidRootPart")
end
local function playerGui()
return LocalPlayer:FindFirstChildOfClass("PlayerGui")
end
-- Reads a TextLabel by GUI path, e.g. ("StillWaterUI", "ClientHUD", "Timer").
local function guiText(guiName, ...)
local gui = playerGui() and playerGui():FindFirstChild(guiName)
if not gui then return nil end
local node = gui
for _, name in ipairs({ ... }) do
node = node and node:FindFirstChild(name)
end
return node and node:IsA("TextLabel") and node.Text or nil
end
local function stateColor(state)
return STATE_COLORS[state] or Color3.fromRGB(180, 180, 200)
end
local function distanceTo(position)
local root = rootPart()
if not root or not position then return math.huge end
return (position - root.Position).Magnitude
end
local function formatStuds(distance)
if distance == math.huge then return "—" end
return string.format("%d", math.floor(distance + 0.5))
end
-- Rooms sit 830 studs apart on +Z (centres 390, 1220, 2050, ...), lobby at Z < 8.
local function currentRoomIndex()
local root = rootPart()
if not root then return 0 end
local z = root.Position.Z
if z < 8 then return 0 end
return math.clamp(math.floor((z + 8) / 830) + 1, 1, 9)
end
-- ─────────────────────────────────────────────────────────────────────────────
-- 3. Settings — every control in the UI writes here
-- ─────────────────────────────────────────────────────────────────────────────
local CFG = {
-- WHAT is drawn (one switch per kind of object)
showListener = true,
showTeammates = false,
showPickups = false,
showPlatforms = false,
platformSafeOnly = false,
showExits = false,
showCode = true,
showMemoryNumbers = false,
-- HOW it is drawn
highlights = true,
boxes = true, -- BoxHandleAdornment for bulk geometry
labels = true,
labelDistance = true,
listenerTarget = false,
maxDistance = 0, -- 0 = draw at any distance
fillOpacity = 0.55, -- Highlight.FillTransparency
boxOpacity = 0.6, -- BoxHandleAdornment.Transparency
-- danger awareness (off by default: the radar statuses are enough)
alarmEnabled = false,
alarmDistance = 60,
alarmFlash = false,
-- movement
speedEnabled = false,
speedValue = 30,
}
-- Hard limits for the movement feature. Above roughly 40 studs/s the server's
-- movement check has something to notice, and the boost stops being worth it.
local SPEED_MIN = 24
local SPEED_MAX = 40
local CONNECTIONS = {}
local STOPPED = false
local function keep(connection)
if connection then table.insert(CONNECTIONS, connection) end
return connection
end
local function stopAll()
STOPPED = true
for _, connection in ipairs(CONNECTIONS) do
pcall(function() connection:Disconnect() end)
end
CONNECTIONS = {}
end
-- Reloading shuts the previous instance down first, so no duplicate loops,
-- duplicate highlights, and above all no duplicate velocity constraints.
do
local previous = getgenv and (getgenv().Robscript or getgenv().StillWaterHub)
if previous and previous.unload then
pcall(previous.unload)
end
end
-- ─────────────────────────────────────────────────────────────────────────────
-- 4. ESP engine
-- ─────────────────────────────────────────────────────────────────────────────
-- visuals[part] = { hl = Highlight, box = BoxHandleAdornment, bb = BillboardGui,
-- label = TextLabel, marker = Part, tag = "listener" | ... }
local visuals = {}
-- The engine only renders 255 Highlight instances at once and silently drops
-- the rest, so a map with 650 platforms cannot be drawn with highlights. Bulk
-- geometry uses BoxHandleAdornment instead, and Highlights are reserved for the
-- few things that must read at a glance: the Listener, teammates, doors, code.
local HIGHLIGHT_BUDGET = 240
local highlightCount = 0
local MARKS_NAME = "RobscriptMarks"
local marksFolder
local function marks()
if marksFolder and marksFolder.Parent then return marksFolder end
local existing = workspace:FindFirstChild(MARKS_NAME)
if not existing then
existing = Instance.new("Folder")
existing.Name = MARKS_NAME
existing.Parent = workspace
end
marksFolder = existing
return marksFolder
end
-- Distance gate used by every ESP pass. 0 means "no limit".
local function inRange(position)
if CFG.maxDistance <= 0 then return true end
return distanceTo(position) <= CFG.maxDistance
end
local function makeHighlight(part, color)
if highlightCount >= HIGHLIGHT_BUDGET then return nil end
local highlight = Instance.new("Highlight")
highlight.Name = "RobscriptHighlight"
highlight.Adornee = part
highlight.DepthMode = Enum.HighlightDepthMode.AlwaysOnTop
highlight.FillColor = color
highlight.FillTransparency = CFG.fillOpacity
highlight.OutlineColor = color
highlight.OutlineTransparency = 0.1
highlight.Parent = marks()
highlightCount = highlightCount + 1
return highlight
end
local function releaseHighlight(entry)
if not entry or not entry.hl then return end
entry.hl:Destroy()
entry.hl = nil
highlightCount = math.max(0, highlightCount - 1)
end
-- BoxHandleAdornment draws an outline without consuming a Highlight slot, so
-- every platform and every pebble can stay visible at the same time.
local function makeBox(part, color, transparency)
local adornment = Instance.new("BoxHandleAdornment")
adornment.Name = "RobscriptBox"
adornment.Adornee = part
adornment.Size = part.Size + Vector3.new(0.08, 0.08, 0.08)
adornment.Color3 = color
adornment.Transparency = transparency or CFG.boxOpacity
adornment.AlwaysOnTop = true
adornment.ZIndex = 2
adornment.Parent = marks()
return adornment
end
-- `worldOffset` places the label a fixed number of studs above the adornee,
-- which keeps it clear of tall parts such as the final door.
local function makeBillboard(part, offsetY, size, worldOffset)
local billboard = Instance.new("BillboardGui")
billboard.Name = "RobscriptTag"
billboard.Size = size or UDim2.fromOffset(190, 34)
if worldOffset then
billboard.StudsOffsetWorldSpace = Vector3.new(0, offsetY or 3, 0)
else
billboard.StudsOffset = Vector3.new(0, offsetY or 3, 0)
end
billboard.AlwaysOnTop = true
billboard.MaxDistance = CFG.maxDistance > 0 and CFG.maxDistance or 4000
billboard.LightInfluence = 0
billboard.Adornee = part
billboard.Parent = marks()
local label = Instance.new("TextLabel")
label.Name = "Text"
label.BackgroundTransparency = 1
label.Size = UDim2.fromScale(1, 1)
label.Font = Enum.Font.GothamBold
label.TextScaled = true
label.TextColor3 = Color3.new(1, 1, 1)
label.TextStrokeTransparency = 0.25
label.TextStrokeColor3 = Color3.new(0, 0, 0)
label.RichText = true
label.Text = ""
label.Parent = billboard
local constraint = Instance.new("UITextSizeConstraint")
constraint.MaxTextSize = 18
constraint.Parent = label
return billboard, label
end
local function dropVisual(part)
local entry = visuals[part]
if not entry then return end
releaseHighlight(entry)
if entry.box then entry.box:Destroy() end
if entry.bb then entry.bb:Destroy() end
if entry.marker then entry.marker:Destroy() end
visuals[part] = nil
end
local function getVisual(part)
local entry = visuals[part]
if not entry then
-- `part` is stored on the entry because every style helper needs an
-- Adornee; without it a Highlight is created but never renders.
entry = { part = part }
visuals[part] = entry
else
entry.part = part
end
return entry
end
-- Drops entries whose part is gone (room regenerated, creature removed).
local function pruneVisuals()
for part, entry in pairs(visuals) do
if not part.Parent then
releaseHighlight(entry)
if entry.box then entry.box:Destroy() end
if entry.bb then entry.bb:Destroy() end
if entry.marker then entry.marker:Destroy() end
visuals[part] = nil
end
end
end
-- Removes every visual belonging to one category (used by toggles and filters).
local function dropByTag(tag)
for part, entry in pairs(visuals) do
if entry.tag == tag then dropVisual(part) end
end
end
-- Applies the current "how to show it" settings to an existing entry.
local function styleHighlight(entry, color)
if not CFG.highlights then
releaseHighlight(entry)
return
end
if not entry.hl then entry.hl = makeHighlight(entry.part, color) end
if not entry.hl then return end
entry.hl.FillColor = color
entry.hl.OutlineColor = color
entry.hl.FillTransparency = CFG.fillOpacity
end
local function styleBox(entry, color, transparency)
if not CFG.boxes then
if entry.box then entry.box:Destroy(); entry.box = nil end
return
end
if entry.box and not entry.box.Parent then entry.box = nil end
if not entry.box then
entry.box = makeBox(entry.part, color, transparency)
else
entry.box.Color3 = color
if transparency then entry.box.Transparency = transparency end
end
end
-- ── Listeners ───────────────────────────────────────────────────────────────
local function listeners()
local folder = listenerFolder()
if not folder then return {} end
local list = {}
for _, child in ipairs(folder:GetChildren()) do
if child:IsA("BasePart") then table.insert(list, child) end
end
return list
end
local function updateListenerVisuals()
if not CFG.showListener then
dropByTag("listener")
return
end
for _, part in ipairs(listeners()) do
if inRange(part.Position) then
local entry = getVisual(part)
entry.tag = "listener"
entry.part = part
-- Tags on each Listener: State, Room, Target, Home, Speed.
local state = tostring(part:GetAttribute("State") or "?")
local color = stateColor(state)
local red = math.floor(color.R * 255)
local green = math.floor(color.G * 255)
local blue = math.floor(color.B * 255)
styleHighlight(entry, color)
-- If the highlight budget is exhausted, fall back to a box so the
-- most important thing on screen never disappears.
if CFG.highlights and not entry.hl then styleBox(entry, color, 0.45) end
if CFG.labels then
if not entry.bb then
entry.bb, entry.label = makeBillboard(part, 4, UDim2.fromOffset(220, 46))
end
entry.bb.MaxDistance = CFG.maxDistance > 0 and CFG.maxDistance or 4000
local room = tostring(part:GetAttribute("Room") or "?")
local tail = CFG.labelDistance
and string.format('%s · %s studs',
red, green, blue, state, formatStuds(distanceTo(part.Position)))
or string.format('%s', red, green, blue, state)
entry.label.Text = string.format(
'LISTENER Room %s\n%s',
red, green, blue, room, tail)
elseif entry.bb then
entry.bb:Destroy(); entry.bb = nil; entry.label = nil
end
-- Marker on `Target`: where the server is steering it right now.
local target = part:GetAttribute("Target")
if CFG.listenerTarget and typeof(target) == "Vector3" then
if not entry.marker or not entry.marker.Parent then
local marker = Instance.new("Part")
marker.Name = "RobscriptTargetMarker"
marker.Shape = Enum.PartType.Ball
marker.Size = Vector3.new(2, 2, 2)
marker.Material = Enum.Material.Neon
marker.Anchored = true
marker.CanCollide = false
marker.CanQuery = false
marker.CanTouch = false
marker.Transparency = 0.35
marker.Parent = workspace
entry.marker = marker
end
entry.marker.Position = target
entry.marker.Color = color
elseif entry.marker then
entry.marker:Destroy(); entry.marker = nil
end
else
local entry = visuals[part]
if entry and entry.tag == "listener" then dropVisual(part) end
end
end
end
-- ── Jump platforms ──────────────────────────────────────────────────────────
-- Generated geometry lives in each room's `Gen` folder and every jumpable part
-- carries `Platform = true`. Room 3 adds `Safe = true`, room 7 adds `MemTile`.
-- The extra tag names are a fallback in case the game renames `Platform`.
local PLATFORM_TAGS = { "Platform", "IsPlatform", "Safe", "MemTile", "CanSink", "IsSinking", "Sinking", "Stable" }
local function platformTag(part)
for _, name in ipairs(PLATFORM_TAGS) do
if part:GetAttribute(name) == true then return name end
end
return nil
end
local function collectPlatforms()
local list = {}
local root = stillWater()
if not root then return list end
for _, room in ipairs(root:GetChildren()) do
local gen = room:FindFirstChild("Gen")
if gen then
for _, part in ipairs(gen:GetChildren()) do
if part:IsA("BasePart") and platformTag(part) then
table.insert(list, part)
end
end
end
end
return list
end
local platformCache = { at = 0, list = {} }
-- Platforms and memory tiles share one pass: the tiles are platforms too, and
-- the memory pass only adds labels on top of whatever this pass drew.
local function updatePlatformVisuals()
if not CFG.boxes then
-- Keep memory labels alive when the boxes are switched off; only the
-- box itself is removed here.
for part, entry in pairs(visuals) do
if entry.tag == "platform" then
if entry.box then entry.box:Destroy(); entry.box = nil end
if not entry.bb then visuals[part] = nil end
end
end
return
end
if not (CFG.showPlatforms or CFG.showMemoryNumbers) then
dropByTag("platform")
return
end
-- Refreshing the list touches dozens of rooms, so do it rarely.
if (os.clock() - platformCache.at) > 3 then
platformCache.list = collectPlatforms()
platformCache.at = os.clock()
end
for _, part in ipairs(platformCache.list) do
if part.Parent then
local safe = part:GetAttribute("Safe") == true
local memory = part:GetAttribute("MemTile") == true
-- The safe-only filter belongs to the platform category; memory
-- tiles are shown whenever the memory labels are requested.
local allowed = CFG.showPlatforms
and ((not CFG.platformSafeOnly) or safe or memory)
or (CFG.showMemoryNumbers and memory)
local show = allowed and inRange(part.Position)
if show then
local entry = getVisual(part)
if not entry.box or entry.tag ~= "platform" then
local color = safe and Color3.fromRGB(70, 220, 255)
or (memory and Color3.fromRGB(200, 120, 255)
or Color3.fromRGB(110, 255, 160))
-- Hundreds of boxes: set the colour once, not every tick.
styleBox(entry, color)
entry.tag = "platform"
end
else
local entry = visuals[part]
if entry and entry.tag == "platform" then dropVisual(part) end
end
end
end
end
-- ── Pebble pickups ──────────────────────────────────────────────────────────
local function updatePickupVisuals()
if not CFG.showPickups or not CFG.boxes then
dropByTag("pickup")
return
end
local folder = pebbleFolder()
if not folder then return end
for _, pile in ipairs(folder:GetChildren()) do
for _, part in ipairs(pile:GetChildren()) do
if part:IsA("BasePart") and inRange(part.Position) then
local entry = getVisual(part)
if not entry.box or entry.tag ~= "pickup" then
styleBox(entry, Color3.fromRGB(255, 245, 140), 0.35)
entry.tag = "pickup"
end
end
end
end
end
-- ── Teammates ───────────────────────────────────────────────────────────────
local function updatePlayerVisuals()
if not CFG.showTeammates or not CFG.highlights then
dropByTag("teammate")
return
end
for _, player in ipairs(Players:GetPlayers()) do
if player ~= LocalPlayer then
local model = player.Character
local part = model and model:FindFirstChild("HumanoidRootPart")
if part and inRange(part.Position) then
local entry = getVisual(part)
entry.tag = "teammate"
styleHighlight(entry, Color3.fromRGB(255, 170, 220))
if not entry.hl then styleBox(entry, Color3.fromRGB(255, 170, 220), 0.55) end
end
end
end
end
-- ── Checkpoints and the final door ──────────────────────────────────────────
local function updateExitVisuals()
if not CFG.showExits then
dropByTag("exit")
return
end
local root = stillWater()
if not root then return end
local targets = {}
local function add(part, text, color)
if part and part:IsA("BasePart") then
table.insert(targets, { part = part, text = text, color = color })
end
end
-- CheckpointN sits in CorridorN, at the end of room N.
for index = 1, 8 do
local corridor = root:FindFirstChild("Corridor" .. index)
add(corridor and corridor:FindFirstChild("Checkpoint" .. index),
"CHECKPOINT " .. index, Color3.fromRGB(120, 220, 255))
end
local room9 = root:FindFirstChild("Room9_LockedExit")
local shell9 = room9 and room9:FindFirstChild("Shell")
add(shell9 and shell9:FindFirstChild("ExitDeck"), "ROOM 9 EXIT", Color3.fromRGB(255, 220, 90))
add(room9 and room9:FindFirstChild("FinalDoor"), "FINAL DOOR", Color3.fromRGB(255, 220, 90))
add(room9 and room9:FindFirstChild("DoorLamp"), "FINAL DOOR", Color3.fromRGB(255, 220, 90))
local lobby = root:FindFirstChild("Lobby")
local back = lobby and lobby:FindFirstChild("BackToLobby")
add(back and back:FindFirstChild("DoorGlow"), "BACK TO LOBBY", Color3.fromRGB(180, 180, 255))
for _, item in ipairs(targets) do
if inRange(item.part.Position) then
local entry = getVisual(item.part)
entry.tag = "exit"
styleHighlight(entry, item.color)
if CFG.labels then
if not entry.bb then
-- Offset from the part's own half-height so the label clears
-- tall geometry such as the 20 stud door.
local offset = item.part.Size.Y * 0.5 + 2.5
entry.bb, entry.label = makeBillboard(item.part, offset, UDim2.fromOffset(170, 26), true)
end
entry.bb.MaxDistance = CFG.maxDistance > 0 and CFG.maxDistance or 4000
entry.label.Text = CFG.labelDistance
and string.format("%s · %s studs", item.text, formatStuds(distanceTo(item.part.Position)))
or item.text
entry.label.TextColor3 = item.color
elseif entry.bb then
entry.bb:Destroy(); entry.bb = nil; entry.label = nil
end
else
local entry = visuals[item.part]
if entry and entry.tag == "exit" then dropVisual(item.part) end
end
end
end
-- ─────────────────────────────────────────────────────────────────────────────
-- 5. Door code (room 9)
-- ─────────────────────────────────────────────────────────────────────────────
-- The server places four CodeNumber parts in Room9_LockedExit.Gen. Each carries
-- the tag CodeIndex (1..4) and a SurfaceGui whose TextLabel holds one digit.
local function readDoorCode()
local root = stillWater()
local room = root and root:FindFirstChild("Room9_LockedExit")
local gen = room and room:FindFirstChild("Gen")
if not gen then return nil end
local digits = {}
for _, part in ipairs(gen:GetChildren()) do
if part:IsA("BasePart") and part.Name == "CodeNumber" then
local index = part:GetAttribute("CodeIndex")
local text
local surface = part:FindFirstChildOfClass("SurfaceGui")
if surface then
for _, descendant in ipairs(surface:GetDescendants()) do
if descendant:IsA("TextLabel") and descendant.Text ~= "" and descendant.Text ~= " " then
text = descendant.Text
break
end
end
end
if index and text then digits[index] = text end
end
end
local out = {}
for index = 1, 4 do out[index] = digits[index] or "?" end
return table.concat(out, " ")
end
local function readDigitFrom(part)
local surface = part:FindFirstChildOfClass("SurfaceGui")
if not surface then return "?" end
for _, descendant in ipairs(surface:GetDescendants()) do
if descendant:IsA("TextLabel") and descendant.Text ~= "" then
return descendant.Text
end
end
return "?"
end
local function updateCodeLabels()
if not CFG.showCode then
dropByTag("code")
return
end
local root = stillWater()
local room = root and root:FindFirstChild("Room9_LockedExit")
local gen = room and room:FindFirstChild("Gen")
if not gen then return end
for _, part in ipairs(gen:GetChildren()) do
if part:IsA("BasePart") and part.Name == "CodeNumber" then
if inRange(part.Position) then
local entry = getVisual(part)
entry.tag = "code"
entry.part = part
if CFG.labels then
if not entry.bb then
local offset = part.Size.Y * 0.5 + 1.5
entry.bb, entry.label = makeBillboard(part, offset, UDim2.fromOffset(90, 90), true)
if entry.label then entry.label.TextStrokeTransparency = 0.1 end
end
entry.bb.MaxDistance = CFG.maxDistance > 0 and CFG.maxDistance or 4000
entry.label.Text = string.format(
'#%s\n%s',
tostring(part:GetAttribute("CodeIndex") or "?"), readDigitFrom(part))
elseif entry.bb then
entry.bb:Destroy(); entry.bb = nil; entry.label = nil
end
styleHighlight(entry, Color3.fromRGB(120, 255, 255))
else
local entry = visuals[part]
if entry and entry.tag == "code" then dropVisual(part) end
end
end
end
end
-- ─────────────────────────────────────────────────────────────────────────────
-- 6. Room 7 — record the order in which the memory tiles light up
-- ─────────────────────────────────────────────────────────────────────────────
-- Tiles carry MemTile = true and a stable MemId. We watch colour, material,
-- transparency and reflectance; a tile counts as lit when it becomes clearly
-- brighter or more solid. Heuristic — the numbers are a fallback either way.
local memory = {
baseline = {},
active = {},
sequence = {},
lastAt = 0,
finished = nil,
}
local function memoryTiles()
local root = stillWater()
local room = root and root:FindFirstChild("Room7_MemoryBridge")
local gen = room and room:FindFirstChild("Gen")
local list = {}
if gen then
for _, part in ipairs(gen:GetChildren()) do
if part:IsA("BasePart") and part:GetAttribute("MemTile") == true then
table.insert(list, part)
end
end
end
return list
end
local function luma(color)
return 0.299 * color.R + 0.587 * color.G + 0.114 * color.B
end
local function tileSignature(part)
return table.concat({
tostring(part.Color), tostring(part.Material),
tostring(part.Transparency), tostring(part.Reflectance),
}, "|")
end
local function memoryStep()
local tiles = memoryTiles()
if #tiles == 0 then
memory.active = {}
return
end
local now = os.clock()
for _, part in ipairs(tiles) do
local signature = tileSignature(part)
local previous = memory.baseline[part]
if previous == nil then
memory.baseline[part] = signature
elseif previous ~= signature then
local lit = (not memory.active[part]) and (
part.Transparency < 0.5
or luma(part.Color) > 0.55
or part.Material == Enum.Material.Neon
)
if lit then
memory.active[part] = true
memory.lastAt = now
local id = part:GetAttribute("MemId")
if id and not table.find(memory.sequence, id) then
table.insert(memory.sequence, id)
end
elseif memory.active[part] and part.Transparency >= 0.5 and luma(part.Color) < 0.4 then
memory.active[part] = nil
end
memory.baseline[part] = signature
end
end
-- A pause longer than 3 s means the flash is over: freeze the sequence.
if #memory.sequence > 0 and (now - memory.lastAt) > 3 then
memory.finished = table.concat(memory.sequence, " → ")
memory.sequence = {}
end
end
local function memoryText()
if #memory.sequence > 0 then
local parts = {}
for _, id in ipairs(memory.sequence) do parts[#parts + 1] = tostring(id) end
return table.concat(parts, " → ")
end
return memory.finished or "waiting for the flash…"
end
-- The platform pass already draws the box for every tile, so this pass only
-- adds the id label and tints the box green while a tile is lit. It must not
-- take the "platform" tag over, or the two passes would fight over one part.
local function updateMemoryVisuals()
for _, part in ipairs(memoryTiles()) do
local entry = visuals[part]
local showing = CFG.showMemoryNumbers and inRange(part.Position)
if showing then
entry = getVisual(part)
entry.memory = true
if CFG.labels then
if not entry.bb then
local offset = part.Size.Y * 0.5 + 1.5
entry.bb, entry.label = makeBillboard(part, offset, UDim2.fromOffset(70, 40), true)
if entry.label then entry.label.TextColor3 = Color3.fromRGB(210, 150, 255) end
end
local order = table.find(memory.sequence, part:GetAttribute("MemId"))
entry.label.Text = order
and ('' .. order .. "")
or tostring(part:GetAttribute("MemId") or "")
elseif entry.bb then
entry.bb:Destroy(); entry.bb = nil; entry.label = nil
end
if memory.active[part] and entry.box then
entry.box.Color3 = Color3.fromRGB(120, 255, 140)
end
elseif entry and entry.memory then
if entry.bb then entry.bb:Destroy(); entry.bb = nil; entry.label = nil end
if entry.box then entry.box.Color3 = Color3.fromRGB(200, 120, 255) end
entry.memory = nil
end
end
end
-- ─────────────────────────────────────────────────────────────────────────────
-- 7. Danger awareness: nearest Listener, alarm flash, beep
-- ─────────────────────────────────────────────────────────────────────────────
local alarmGui, alarmFrame
local beep
local function ensureAlarmGui()
if alarmGui and alarmGui.Parent then return end
local parent = playerGui()
if not parent then return end
alarmGui = Instance.new("ScreenGui")
alarmGui.Name = "RobscriptAlarm"
alarmGui.IgnoreGuiInset = true
alarmGui.ResetOnSpawn = false
alarmGui.DisplayOrder = 999
alarmGui.Parent = parent
alarmFrame = Instance.new("Frame")
alarmFrame.Size = UDim2.fromScale(1, 1)
alarmFrame.BackgroundColor3 = Color3.fromRGB(255, 30, 30)
alarmFrame.BackgroundTransparency = 1
alarmFrame.BorderSizePixel = 0
alarmFrame.Parent = alarmGui
end
local function flashAlarm()
if not CFG.alarmFlash then return end
ensureAlarmGui()
if not alarmFrame then return end
alarmFrame.BackgroundTransparency = 0.55
TweenService:Create(alarmFrame, TweenInfo.new(0.55, Enum.EasingStyle.Quad), {
BackgroundTransparency = 1,
}):Play()
end
-- Reuses one of the game's own sound assets, so no external asset id is needed.
local function playBeep()
if beep and beep.Parent then
beep:Play()
return
end
local ok, sounds = pcall(function()
return require(ReplicatedStorage.Config.Sounds)
end)
if not ok or type(sounds) ~= "table" then return end
local soundId
for _, key in ipairs({ "ERROR", "ROOMTONE", "CLICK" }) do
local value = sounds[key]
if type(value) == "string" then
soundId = value
elseif type(value) == "number" then
soundId = "rbxassetid://" .. tostring(value)
elseif type(value) == "table" then
local id = value.Id or value.SoundId or value.id
if type(id) == "string" then
soundId = id
elseif type(id) == "number" then
soundId = "rbxassetid://" .. tostring(id)
end
end
if soundId then break end
end
if not soundId then return end
if not soundId:match("^rbxassetid://") and not soundId:match("^rbxasset") then
soundId = "rbxassetid://" .. soundId
end
beep = Instance.new("Sound")
beep.SoundId = soundId
beep.Volume = 0.35
beep.PlaybackSpeed = 1.6
beep.Parent = SoundService
end
local nearest = { part = nil, distance = math.huge, state = nil, room = nil }
local function updateNearest()
local best, bestDistance = nil, math.huge
for _, part in ipairs(listeners()) do
local distance = distanceTo(part.Position)
if distance < bestDistance then
best, bestDistance = part, distance
end
end
nearest = {
part = best,
distance = bestDistance,
state = best and tostring(best:GetAttribute("State") or "?") or nil,
room = best and tostring(best:GetAttribute("Room") or "?") or nil,
}
return nearest
end
local lastAlarmAt = 0
local function dangerStep()
local current = updateNearest()
local aggressive = current.state ~= nil and current.state ~= "Patrol"
if CFG.alarmEnabled and current.part then
if current.distance <= CFG.alarmDistance or aggressive then
local now = os.clock()
local interval = aggressive and 0.8 or 1.6
if now - lastAlarmAt > interval then
lastAlarmAt = now
flashAlarm()
playBeep()
end
end
end
end
-- "It is hunting me": an aggressive state whose destination is close to us.
-- This fires before you can possibly see it.
local function huntingMe()
local root = rootPart()
if not root then return false end
for _, part in ipairs(listeners()) do
local state = tostring(part:GetAttribute("State") or "")
if state == "Chase" or state == "Hunt" or state == "Attack" then
local target = part:GetAttribute("Target")
if typeof(target) == "Vector3" and (target - root.Position).Magnitude < 70 then
return true
end
end
end
return false
end
-- ─────────────────────────────────────────────────────────────────────────────
-- 8. Speed boost — the only movement feature
-- ─────────────────────────────────────────────────────────────────────────────
-- Rules that keep it from tripping a movement check:
-- • exactly one constraint, always reused, cleaned on spawn and on unload
-- • ramps from the current speed instead of jumping to the target
-- • pushes on the horizontal axes only, so gravity and jumps are untouched
-- • never in water; airborne is fine, that is when you jump between platforms
-- • capped, and always horizontal (gravity keeps working normally)
local BOOST_NAME = "RobscriptBoostVelocity"
local BOOST_RAMP = 10 -- higher = snappier ramp, lower = smoother
local boost = { constraint = nil, attachment = nil, current = 0 }
local function purgeBoostParts(model)
if not model then return end
for _, descendant in ipairs(model:GetDescendants()) do
if descendant.Name == BOOST_NAME then descendant:Destroy() end
end
end
local boostFailureAt = 0
local function ensureBoostConstraint()
local root = rootPart()
if not root then return nil end
if boost.constraint and boost.constraint.Parent == root
and boost.attachment and boost.attachment.Parent == root then
return boost.constraint
end
-- Do not spin if the engine ever refuses to build the constraint.
if os.clock() - boostFailureAt < 2 then return nil end
-- Stale objects from an older character or an older run of this script.
purgeBoostParts(root)
boost.constraint = nil
boost.attachment = nil
local ok, constraint = pcall(function()
local attachment = Instance.new("Attachment")
attachment.Name = BOOST_NAME
attachment.Parent = root
local velocity = Instance.new("LinearVelocity")
velocity.Name = BOOST_NAME
velocity.Attachment0 = attachment
velocity.MaxForce = 1e5
velocity.RelativeTo = Enum.ActuatorRelativeTo.World
-- Plane mode drives velocity only inside the X/Z plane. Y is left alone,
-- so gravity and jump height stay exactly as the game intends.
velocity.VelocityConstraintMode = Enum.VelocityConstraintMode.Plane
velocity.PrimaryTangentAxis = Vector3.new(1, 0, 0)
velocity.SecondaryTangentAxis = Vector3.new(0, 0, 1)
velocity.PlaneVelocity = Vector2.zero
velocity.Enabled = false
velocity.Parent = root
return velocity
end)
if not ok or not constraint then
boostFailureAt = os.clock()
purgeBoostParts(root)
boost.constraint = nil
boost.attachment = nil
return nil
end
boost.attachment = constraint.Attachment0
boost.constraint = constraint
boost.current = 0
return constraint
end
local function releaseBoost(constraint, root)
boost.current = 0
if constraint then
constraint.Enabled = false
constraint.VectorVelocity = Vector3.zero
end
boost.constraint = nil
boost.attachment = nil
purgeBoostParts(root)
end
local function onSolidGround()
local hum = humanoid()
if not hum then return false end
return hum.FloorMaterial ~= Enum.Material.Air
end
local function inWater()
return LocalPlayer:GetAttribute("InWater") == true
end
local function boostStep(deltaTime)
local root = rootPart()
local hum = humanoid()
if not root or not hum then
boost.constraint = nil
boost.attachment = nil
boost.current = 0
return
end
if not CFG.speedEnabled then
if boost.constraint then releaseBoost(boost.constraint, root) end
return
end
local constraint = ensureBoostConstraint()
if not constraint then return end
local move = hum.MoveDirection
local walking = move.Magnitude > 0.05
-- Airborne is allowed: only X and Z are constrained, so a jump keeps its
-- own vertical velocity and the boost simply carries it forward.
local allowed = walking and not inWater()
local target = 0
if allowed then
target = math.clamp(CFG.speedValue, SPEED_MIN, SPEED_MAX)
end
-- Exponential ramp: no instant 0 → 40 spike for the server to notice.
local alpha = 1 - math.exp(-BOOST_RAMP * deltaTime)
boost.current = boost.current + (target - boost.current) * alpha
if boost.current < 0.3 and target == 0 then
boost.current = 0
end
if boost.current > 0.3 and walking then
local direction = Vector3.new(move.X, 0, move.Z)
if direction.Magnitude > 0 then direction = direction.Unit end
-- Only the planar part is driven; whatever the character is doing on Y
-- (jumping, falling, walking on flat ground) is untouched.
constraint.PlaneVelocity = Vector2.new(
direction.X * boost.current,
direction.Z * boost.current
)
constraint.Enabled = true
else
constraint.Enabled = false
constraint.PlaneVelocity = Vector2.zero
end
end
-- ─────────────────────────────────────────────────────────────────────────────
-- 9. UI
-- ─────────────────────────────────────────────────────────────────────────────
-- The library draws into gethui() when the executor has one; clean leftovers
-- from a previous run so windows never stack up.
local function uiParent()
local ok, hidden = pcall(function()
return gethui and gethui()
end)
if ok and hidden then return hidden end
return LocalPlayer:FindFirstChildOfClass("PlayerGui") or game:GetService("CoreGui")
end
do
for _, gui in ipairs(uiParent():GetChildren()) do
if gui:IsA("ScreenGui")
and (gui.Name == "Robscript.com" or gui.Name == "Still Water Hub" or gui.Name == "RobscriptAlarm") then
gui:Destroy()
end
end
-- Marks folder from a run that died before it could unload itself.
local staleMarks = workspace:FindFirstChild(MARKS_NAME)
if staleMarks then staleMarks:Destroy() end
end
local Window = Airflow:CreateWindow({
Name = "Robscript.com",
LoadingSubtitle = "Still Water · ESP · radar",
ToggleUIKeybind = "RightControl",
ConfigurationSaving = { Enabled = true, FolderName = "Robscript" },
Home = false,
})
local EspTab = Window:CreateTab({ Name = "ESP", Icon = "eye" })
local ListenerTab = Window:CreateTab({ Name = "Listener", Icon = "ear" })
local PuzzleTab = Window:CreateTab({ Name = "Puzzles", Icon = "puzzle" })
local MoveTab = Window:CreateTab({ Name = "Movement", Icon = "move" })
local InfoTab = Window:CreateTab({ Name = "Game Info", Icon = "book-open" })
local SettingsTab = Window:CreateTab({ Name = "Settings", Icon = "settings" })
-- ── ESP: what is drawn ──────────────────────────────────────────────────────
local targetsGroup = EspTab:CreateGroupbox({ Name = "What to show", Side = "Left" })
local styleGroup = EspTab:CreateGroupbox({ Name = "How to show it", Side = "Right" })
targetsGroup:CreateToggle({
Name = "Listener",
Desc = "The monster, one per room",
Flag = "ShowListener",
CurrentValue = true,
Callback = function(on) CFG.showListener = on end,
})
targetsGroup:CreateToggle({
Name = "Teammates",
Desc = "Other players through walls",
Flag = "ShowTeammates",
CurrentValue = false,
Callback = function(on) CFG.showTeammates = on end,
})
targetsGroup:CreateToggle({
Name = "Pebble piles",
Desc = "Pickups you can refill from",
Flag = "ShowPickups",
CurrentValue = false,
Callback = function(on) CFG.showPickups = on end,
})
targetsGroup:CreateToggle({
Name = "Platforms",
Desc = "Every jumpable part in every room",
Flag = "ShowPlatforms",
CurrentValue = false,
Callback = function(on) CFG.showPlatforms = on end,
})
targetsGroup:CreateToggle({
Name = "Safe platforms only",
Desc = "Room 3 safe platforms and room 7 memory tiles",
Flag = "PlatformSafeOnly",
CurrentValue = false,
Callback = function(on) CFG.platformSafeOnly = on end,
})
targetsGroup:CreateToggle({
Name = "Checkpoints and final door",
Desc = "All 8 checkpoints, the room 9 exit, the way back",
Flag = "ShowExits",
CurrentValue = false,
Callback = function(on) CFG.showExits = on end,
})
targetsGroup:CreateToggle({
Name = "Door code digits",
Desc = "The numbers on the room 9 pillars",
Flag = "ShowCode",
CurrentValue = true,
Callback = function(on) CFG.showCode = on end,
})
targetsGroup:CreateToggle({
Name = "Memory tile numbers",
Desc = "Room 7 tile ids",
Flag = "ShowMemoryNumbers",
CurrentValue = false,
Callback = function(on) CFG.showMemoryNumbers = on end,
})
styleGroup:CreateToggle({
Name = "Highlights",
Desc = "Through-wall fill for the Listener, teammates, doors and code",
Flag = "Highlights",
CurrentValue = true,
Callback = function(on)
CFG.highlights = on
if not on then
for _, entry in pairs(visuals) do releaseHighlight(entry) end
end
end,
})
styleGroup:CreateToggle({
Name = "Box outlines",
Desc = "Platforms and pebbles; they do not use up the highlight limit",
Flag = "BoxOutlines",
CurrentValue = true,
Callback = function(on)
CFG.boxes = on
if not on then
for _, entry in pairs(visuals) do
if entry.box then entry.box:Destroy(); entry.box = nil end
end
end
end,
})
styleGroup:CreateToggle({
Name = "Labels",
Desc = "The floating text above objects",
Flag = "Labels",
CurrentValue = true,
Callback = function(on)
CFG.labels = on
if not on then
for _, entry in pairs(visuals) do
if entry.bb then entry.bb:Destroy(); entry.bb = nil; entry.label = nil end
end
end
end,
})
styleGroup:CreateToggle({
Name = "Distance in labels",
Desc = "Show studs next to the Listener state",
Flag = "LabelDistance",
CurrentValue = true,
Callback = function(on) CFG.labelDistance = on end,
})
styleGroup:CreateToggle({
Name = "Listener destination marker",
Desc = "A sphere where it is heading right now",
Flag = "ListenerTarget",
CurrentValue = false,
Callback = function(on) CFG.listenerTarget = on end,
})
styleGroup:CreateSlider({
Name = "Max distance",
Desc = "0 means no limit",
Range = { 0, 500 },
Increment = 10,
CurrentValue = 0,
Suffix = " studs",
Flag = "MaxDistance",
Callback = function(value) CFG.maxDistance = value end,
})
styleGroup:CreateSlider({
Name = "Fill opacity",
Range = { 0, 80 },
Increment = 5,
CurrentValue = 55,
Suffix = "%",
Flag = "FillOpacity",
Callback = function(value) CFG.fillOpacity = 1 - (value / 100) end,
})
styleGroup:CreateSlider({
Name = "Box opacity",
Range = { 10, 90 },
Increment = 5,
CurrentValue = 60,
Suffix = "%",
Flag = "BoxOpacity",
Callback = function(value) CFG.boxOpacity = value / 100 end,
})
styleGroup:CreateStatus({
Name = "Highlight budget",
Desc = "The engine renders 255 at a time",
Style = "Row",
Update = function()
local boxes = 0
for _, entry in pairs(visuals) do
if entry.box then boxes = boxes + 1 end
end
local tone = highlightCount > HIGHLIGHT_BUDGET * 0.9 and "" or ""
local close = highlightCount > HIGHLIGHT_BUDGET * 0.9 and "" or ""
return string.format("%s%d / %d%s highlights · %d boxes",
tone, highlightCount, HIGHLIGHT_BUDGET, close, boxes)
end,
})
styleGroup:CreateButton({
Name = "Clear all ESP",
Callback = function()
for part in pairs(visuals) do dropVisual(part) end
CFG.showListener = false
CFG.showTeammates = false
CFG.showPickups = false
CFG.showPlatforms = false
CFG.showExits = false
CFG.showCode = false
CFG.showMemoryNumbers = false
Window:Notify({ Title = "ESP cleared", Duration = 3 })
end,
})
-- ── Listener radar ──────────────────────────────────────────────────────────
local radarGroup = ListenerTab:CreateGroupbox({ Name = "Radar", Side = "Left" })
local roomsGroup = ListenerTab:CreateGroupbox({ Name = "Rooms by threat", Side = "Right" })
radarGroup:CreateStatus({
Name = "Nearest Listener",
Style = "Row",
Pin = true,
Update = function()
local current = updateNearest()
if not current.part then return "no data" end
local color = stateColor(current.state)
local red = math.floor(color.R * 255)
local green = math.floor(color.G * 255)
local blue = math.floor(color.B * 255)
local close = current.distance < 25 and " CLOSE" or ""
return string.format('%s · %d studs%s',
current.state, red, green, blue, current.distance, close)
end,
})
radarGroup:CreateStatus({
Name = "Hunting me",
Style = "Row",
Update = function()
return huntingMe() and "YES" or "no"
end,
})
radarGroup:CreateToggle({
Name = "Proximity alarm",
Desc = "Screen flash and a beep when it gets close",
Flag = "AlarmEnabled",
CurrentValue = false,
Callback = function(on) CFG.alarmEnabled = on end,
})
radarGroup:CreateSlider({
Name = "Alarm distance",
Range = { 15, 200 },
Increment = 5,
CurrentValue = 60,
Suffix = " studs",
Flag = "AlarmDistance",
Callback = function(value) CFG.alarmDistance = value end,
})
radarGroup:CreateToggle({
Name = "Red screen flash",
Flag = "AlarmFlash",
CurrentValue = false,
Callback = function(on) CFG.alarmFlash = on end,
})
roomsGroup:CreateStatusList({
Rows = 9,
MaxRows = 9,
Update = function()
local rows = {}
local list = listeners()
table.sort(list, function(a, b)
return (a:GetAttribute("Room") or 0) < (b:GetAttribute("Room") or 0)
end)
for _, part in ipairs(list) do
local room = tostring(part:GetAttribute("Room") or "?")
local state = tostring(part:GetAttribute("State") or "?")
local color = stateColor(state)
local red = math.floor(color.R * 255)
local green = math.floor(color.G * 255)
local blue = math.floor(color.B * 255)
table.insert(rows, {
Text = "Room " .. room,
Value = string.format('%s · %d studs',
red, green, blue, state, distanceTo(part.Position)),
Tone = state == "Patrol" and "Muted" or "Error",
})
end
return rows
end,
})
-- ── Puzzles ─────────────────────────────────────────────────────────────────
local codeGroup = PuzzleTab:CreateGroupbox({ Name = "Door code (room 9)", Side = "Left" })
local memoryGroup = PuzzleTab:CreateGroupbox({ Name = "Memory bridge (room 7)", Side = "Right" })
codeGroup:CreateLabel({
Text = "waiting for the pillars…",
Update = function()
local code = readDoorCode()
if not code then return "this room is not in the world" end
if code == "? ? ? ?" then return "digits not drawn yet" end
return "CODE: " .. code .. ""
end,
})
codeGroup:CreateButton({
Name = "Copy code",
Icon = "copy",
Callback = function()
local code = readDoorCode()
if not code or code == "? ? ? ?" then
Window:Notify({
Title = "Code is not known yet",
Content = "Walk up to the room 9 pillars first",
Type = "Warning",
Duration = 4,
})
return
end
local ok = pcall(function() Window:CopyToClipboard(code) end)
if not ok then pcall(function() setclipboard(code) end) end
Window:Notify({ Title = "Door code", Content = code, Type = "Success", Duration = 5 })
end,
})
codeGroup:CreateParagraph({
Title = "How it works",
Content = "The server places four CodeNumber parts in the room, each with a CodeIndex tag and a "
.. "SurfaceGui holding one digit. The script reads those digits straight from the world, so "
.. "the camera puzzle can be skipped entirely.",
})
memoryGroup:CreateLabel({
Text = "waiting for the flash…",
Update = function() return memoryText() end,
})
memoryGroup:CreateButton({
Name = "Reset recording",
Callback = function()
memory.sequence = {}
memory.finished = nil
memory.baseline = {}
memory.active = {}
Window:Notify({ Title = "Recording cleared", Duration = 3 })
end,
})
memoryGroup:CreateParagraph({
Title = "How it works",
Content = "The MemTile tiles in room 7 light up one after another. The script watches their colour, "
.. "material and transparency and records the order. If nothing is recorded, turn on "
.. "\"Memory tile numbers\" and read the ids by eye.",
})
-- ── Movement: speed boost only ──────────────────────────────────────────────
local boostGroup = MoveTab:CreateGroupbox({ Name = "Speed boost", Side = "Left" })
boostGroup:CreateToggle({
Name = "Speed boost",
Desc = "Horizontal only: jumping works normally while it is on",
Flag = "SpeedEnabled",
CurrentValue = false,
Callback = function(on) CFG.speedEnabled = on end,
})
boostGroup:CreateSlider({
Name = "Speed",
Range = { SPEED_MIN, SPEED_MAX },
Increment = 1,
CurrentValue = 30,
Suffix = " studs/s",
Flag = "SpeedValue",
Callback = function(value) CFG.speedValue = value end,
})
boostGroup:CreateStatus({
Name = "Current speed",
Style = "Row",
Update = function()
if not CFG.speedEnabled then return "off" end
if inWater() then return "paused: in water" end
local suffix = onSolidGround() and "" or " · airborne"
return string.format("%.0f studs/s%s", boost.current, suffix)
end,
})
boostGroup:CreateParagraph({
Title = "How it works, and why it is capped",
Content = "The game rewrites Humanoid.WalkSpeed every frame, so the boost is a LinearVelocity "
.. "constraint instead. It ramps over about a third of a second, applies only to the direction "
.. "you already walk, and pushes on the horizontal axes only — gravity and jumps are left "
.. "alone, so you keep full jump height while it is running. The slider stops at 40 on purpose: "
.. "a raw jump to a high velocity is exactly what a server-side movement check looks for.",
})
boostGroup:CreateParagraph({
Title = "If the position gets corrected",
Content = "That is the server's movement check. Lower the speed, keep the boost off while "
.. "swimming and jumping, and do not stack it with anything else that moves you.",
})
-- ── Game info ───────────────────────────────────────────────────────────────
local aboutGroup = InfoTab:CreateGroupbox({ Name = "About this game", Side = "Left" })
local mechanicGroup = InfoTab:CreateGroupbox({ Name = "Mechanics", Side = "Right" })
aboutGroup:CreateParagraph({
Title = "Still Water — a co-op horror run through flooded poolrooms",
Content = "Nine obstacle rooms joined by corridors with checkpoints. Each room holds one Listener: "
.. "it cannot see you, it hears you — footsteps in water and splashes. You cannot outrun it "
.. "(patrol 16, chase 120, your walk 22), so the game is about staying quiet.",
})
aboutGroup:CreateStatusList({
Rows = 9,
MaxRows = 9,
Update = function()
local rows = {}
local here = currentRoomIndex()
for roomIndex = 1, 9 do
local room = GAME.rooms[roomIndex]
table.insert(rows, {
Text = (roomIndex == here and "▶ " or " ") .. roomIndex .. ". " .. room.name,
Value = "",
Tone = (roomIndex == here) and "Accent" or "Muted",
})
end
return rows
end,
})
aboutGroup:CreateStatus({
Name = "Current room",
Style = "Row",
Update = function()
local index = currentRoomIndex()
if index == 0 then return "Lobby" end
local room = GAME.rooms[index]
return index .. ". " .. (room and room.name or "?")
end,
})
mechanicGroup:CreateParagraph({
Title = "Rewards",
Content = "20–70 coins per room (room 1 to room 9), +100 for escaping, +30 for a clean run, "
.. "+10 per rescue (capped at 20). Mode multiplier: STARTER x0.5, EASY x1, HARD x1.5.",
})
mechanicGroup:CreateParagraph({
Title = "Equipment",
Content = "Pebble is free, Bottle 150, Toy 300, Flare 450, Rescue Rope 250. A flare stuns it for "
.. "5 seconds, then it is immune for 8.",
})
mechanicGroup:CreateParagraph({
Title = "Upgrades",
Content = "Range, Skip, Battery, FlareRadius, RopeReach, RescueSpeed — 150, 400 and 900 coins per level.",
})
-- ── Settings ────────────────────────────────────────────────────────────────
SettingsTab:CreateConfigManager({ Side = "Left" })
SettingsTab:CreateThemeManager({ Side = "Right" })
local sessionGroup = SettingsTab:CreateGroupbox({ Name = "Session", Side = "Left" })
local miscGroup = SettingsTab:CreateGroupbox({ Name = "Misc", Side = "Right" })
sessionGroup:CreateStatus({
Name = "Run mode",
Style = "Row",
Pin = true,
Update = function()
local root = stillWater()
local mode = root and tostring(root:GetAttribute("Mode") or "—") or "—"
local data = GAME.modes[mode]
return data and string.format("%s x%.1f", mode, data.mult) or mode
end,
})
sessionGroup:CreateStatus({
Name = "Run timer",
Style = "Row",
Update = function()
return guiText("StillWaterUI", "ClientHUD", "Timer") or "—"
end,
})
sessionGroup:CreateStatus({
Name = "Coins this run",
Style = "Row",
Update = function()
return guiText("StillWaterRunUI", "RunUI", "Coins", "Amount") or "—"
end,
})
sessionGroup:CreateStatusList({
Rows = 4,
MaxRows = 6,
Update = function()
local gui = playerGui() and playerGui():FindFirstChild("StillWaterRunUI")
local runGui = gui and gui:FindFirstChild("RunUI")
local hotbar = runGui and runGui:FindFirstChild("Hotbar")
if not hotbar then return { "hotbar not available" } end
local rows = {}
for _, slot in ipairs(hotbar:GetChildren()) do
if slot:IsA("TextButton") then
local count = slot:FindFirstChild("Count")
local tag = slot:FindFirstChild("Tag")
local used = slot:FindFirstChild("Used")
local label = (tag and tag.Text ~= "" and tag.Text) or slot.Name
local amount = count and count.Text or "0"
local spent = (used and used.Text == "USED") and " (used)" or ""
table.insert(rows, string.format("%s x %s%s", label, amount, spent))
end
end
if #rows == 0 then rows[1] = "empty" end
return rows
end,
})
miscGroup:CreateButton({
Name = "Rejoin this server",
Style = "Danger",
Confirm = true,
Callback = function()
local ok = pcall(function() Window:Rejoin() end)
if not ok then
pcall(function()
game:GetService("TeleportService"):TeleportToPlaceInstance(game.PlaceId, game.JobId, LocalPlayer)
end)
end
end,
})
miscGroup:CreateButton({
Name = "Reset all ESP settings",
Callback = function()
CFG.showListener = true
CFG.showTeammates = false
CFG.showPickups = false
CFG.showPlatforms = false
CFG.platformSafeOnly = false
CFG.showExits = false
CFG.showCode = true
CFG.showMemoryNumbers = false
CFG.highlights = true
CFG.labels = true
CFG.labelDistance = true
CFG.listenerTarget = false
CFG.maxDistance = 0
CFG.fillOpacity = 0.55
Window:Notify({ Title = "ESP settings reset", Duration = 3 })
end,
})
Window:LoadAutoload()
-- ─────────────────────────────────────────────────────────────────────────────
-- 10. Loops
-- ─────────────────────────────────────────────────────────────────────────────
local lastFrameAt = os.clock()
keep(RunService.RenderStepped:Connect(function()
local now = os.clock()
local deltaTime = math.clamp(now - lastFrameAt, 0.001, 0.2)
lastFrameAt = now
boostStep(deltaTime)
end))
keep(RunService.Heartbeat:Connect(function()
dangerStep()
end))
task.spawn(function()
local frame = 0
while not STOPPED do
task.wait(0.1)
frame = frame + 1
pcall(pruneVisuals)
pcall(updateListenerVisuals)
-- Expensive passes run less often: platform ESP touches hundreds of parts.
if frame % 10 == 0 then
pcall(updatePlatformVisuals)
pcall(updateCodeLabels)
pcall(updateMemoryVisuals)
end
if frame % 3 == 0 then
pcall(updatePickupVisuals)
pcall(updatePlayerVisuals)
pcall(updateExitVisuals)
end
pcall(memoryStep)
end
end)
keep(LocalPlayer.CharacterAdded:Connect(function(model)
task.wait(1)
purgeBoostParts(model)
boost.constraint = nil
boost.attachment = nil
boost.current = 0
end))
-- ─────────────────────────────────────────────────────────────────────────────
-- 11. Public API
-- ─────────────────────────────────────────────────────────────────────────────
local api = {
cfg = CFG,
window = Window,
-- { part, distance, state, room } for the closest Listener
listener = function() return updateNearest() end,
-- "2 8 8 9", or nil when room 9 is not loaded
code = function() return readDoorCode() end,
-- last recorded memory-bridge sequence
memory = function() return memoryText() end,
-- current boost output in studs/s (0 when paused)
boostSpeed = function() return boost.current end,
clearEsp = function()
for part in pairs(visuals) do dropVisual(part) end
end,
unload = function()
stopAll()
for part in pairs(visuals) do
dropVisual(part)
end
highlightCount = 0
if marksFolder then
pcall(function() marksFolder:Destroy() end)
marksFolder = nil
end
purgeBoostParts(rootPart())
boost.constraint = nil
boost.attachment = nil
boost.current = 0
pcall(function() Window:Destroy() end)
if alarmGui then pcall(function() alarmGui:Destroy() end) end
if getgenv then
getgenv().Robscript = nil
getgenv().StillWaterHub = nil
end
end,
}
-- `Robscript` is the current handle; `StillWaterHub` stays as a legacy alias so
-- older snippets and notes keep working.
if getgenv then
getgenv().Robscript = api
getgenv().StillWaterHub = api
end
Window:Notify({
Title = "Robscript.com loaded",
Content = "RightControl opens the window. ESP is already on.",
Type = "Success",
Duration = 6,
})
return api