
KeylessUniversal

--[[
SpiderWebSwing (LocalScript)
PLACEMENT: StarterPlayer > StarterPlayerScripts
(It needs to live here because it has its own persistent UI/settings that
should survive dying and respawning. The character is re-acquired
automatically every time you respawn.)
FEATURES:
- Only ONE web at a time: shooting a new one instantly removes the old one.
- The string visually shoots out from your hand to the target over a
fraction of a second (speed scales with distance, WEB_TRAVEL_SPEED)
instead of just appearing at full length. The real physics rope only
attaches once it "lands".
- Speed boost: your current speed is amplified when you shoot a web while
already moving, and holding WASD while swinging accelerates you further
(up to a cap, MAX_SWING_SPEED).
- Real swing feel: while a web is attached, the Humanoid is put into
"Physics" state so Roblox's normal walking control never takes over and
zeroes your speed when you're near the ground at the bottom of the arc.
- Release with high speed (see SLIDE_MIN_SPEED) and you fly off, then
gradually slow down (SLIDE_FRICTION) instead of stopping dead.
- Loose/"ragdoll" feel WITHOUT looking ragdolled: the web attaches
slightly above your center of mass (SWING_HAND_HEIGHT), which gives a
natural lean during swings instead of perfectly rigid motion, and
AutoRotate is turned off so you're not forced to face your direction
of travel. A weak AlignOrientation (ORIENTATION_MAX_TORQUE /
ORIENTATION_RESPONSIVENESS) only stops you from ending up permanently
upside-down - your body always looks completely normal (no loose
limbs, no broken joints). This loose feel starts the moment you shoot
a web and lasts through the whole time you're airborne - including
after you release it - and only ends the instant you actually touch
the ground. While a web is still attached it NEVER ends early (even
if you brush the ground mid-swing), so the pendulum can't get "stuck"
at the bottom of a low arc.
- A small settings UI that's open by default. Drag the header
("SPIDER WEB") to move the panel, click "–" to collapse it to just the
header, or click "X" to fully close it (this also automatically turns
the whole mechanic OFF). Click the spider tab on the left edge to
reopen it. In the panel you can:
* Turn the whole mechanic ON/OFF.
* Click "Change" next to an action, then press any key or mouse
button to rebind it - or press Esc while it's listening to make
that action fully unbound.
DEFAULT CONTROLS (changeable in the UI; click "Change" then press any
key/mouse button, or press Esc while it's listening to unbind it):
Left Click -> Shoot web
F -> Release web
Hold E -> Climb (reel in)
Hold Q -> Let out more rope
]]
local Players = game:GetService("Players")
local UserInputService = game:GetService("UserInputService")
local RunService = game:GetService("RunService")
-- ===================== SETTINGS =====================
local MAX_WEB_DISTANCE = 350 -- how far you can shoot a web (studs)
local MIN_TARGET_SIZE = 3 -- ignore tiny junk smaller than this (studs, largest side)
local ROPE_THICKNESS = 0.06
local ROPE_SLACK = 1.05 -- MUST be >= 1. Below 1 makes the rope "shorter" than the
-- actual distance right from the start, which makes the
-- physics engine yank you hard toward the anchor instantly
-- (feels like a teleport). 1.0 = taut immediately, higher
-- gives a bit of slack/drop before the swing kicks in.
local MIN_ROPE_LENGTH = 4
local REEL_IN_SPEED = 18 -- studs/second while climbing (E)
local LET_OUT_SPEED = 14 -- studs/second while letting out rope (Q)
local LAUNCH_BOOST_MULTIPLIER = 1.4 -- amplifies your current speed when the web is shot
local SWING_THRUST = 110 -- extra force (studs/s^2) in the direction you press while swinging
local MAX_SWING_SPEED = 160 -- cap on horizontal swing speed
local SLIDE_MIN_SPEED = 15 -- release with at least this much speed (studs/s) to slide instead of stopping dead
local SLIDE_FRICTION = 2.5 -- how fast the slide slows down (higher = stops sooner)
local SLIDE_MAX_DURATION = 1.5 -- safety cap in seconds for how long the slide can last
local SWING_HAND_HEIGHT = 1.6 -- height (studs) above your center where the web
-- attaches to YOU. Keeps the rope's pull from going
-- straight through your center of mass, giving a
-- natural lean during swings instead of perfectly
-- rigid motion (the "ragdoll feel" without looking
-- ragdolled).
local ORIENTATION_MAX_TORQUE = 6000 -- how hard you're corrected if you lean/tip too far.
-- Lower = looser/more "ragdoll", higher = stiffer.
local ORIENTATION_RESPONSIVENESS = 3 -- how fast that correction happens (lower = softer/slower).
local WEB_TRAVEL_SPEED = 300 -- studs/second the visual string travels while "shooting out"
local WEB_TRAVEL_MIN_TIME = 0.05 -- seconds, floor so very close shots don't look instant/jumpy
local WEB_TRAVEL_MAX_TIME = 0.3 -- seconds, cap so very far shots don't feel sluggish
local GROUND_CHECK_DISTANCE = 3.5 -- studs below your feet checked to detect landing
-- ======================================================
local player = Players.LocalPlayer
local mouse = player:GetMouse()
-- ===================== ACTIONS / KEYBINDS =====================
local actions = {
Shoot = {name = "Shoot web", input = Enum.UserInputType.MouseButton1},
Release = {name = "Release web", input = Enum.KeyCode.F},
ReelIn = {name = "Climb (reel in)", input = Enum.KeyCode.E},
LetOut = {name = "Let out", input = Enum.KeyCode.Q},
}
local scriptEnabled = true
-- ===================== CHARACTER =====================
local character, humanoid, rootPart
local activeWebs = {} -- list of {rope, attachment0, attachment1} - just the visual/physical rope
local reeling, lettingOut = false, false
local sliding = false
local slideVelocity = Vector3.new()
local slideEndTime = 0
-- The "ragdoll" rig (loose orientation assist) is tracked separately from
-- the rope itself, because it now needs to keep existing after you release
-- the web - it only goes away once you actually touch the ground.
local ragdollOrientationAttachment = nil
local ragdollAlignOrientation = nil
-- Tracks the visual "string shooting out" animation so a newer shot, a
-- release, or a respawn can cleanly cancel an in-flight one.
local travelToken = 0
local currentTravelPart = nil
local function cancelWebTravel()
travelToken = travelToken + 1
if currentTravelPart then
currentTravelPart:Destroy()
currentTravelPart = nil
end
end
local function destroyWebEntry(entry)
if entry.rope then entry.rope:Destroy() end
if entry.attachment0 then entry.attachment0:Destroy() end
if entry.attachment1 then entry.attachment1:Destroy() end
end
local function ensureRagdollRig()
if ragdollAlignOrientation and ragdollAlignOrientation.Parent then
return -- already active (e.g. chaining a new web before landing)
end
if not rootPart then return end
local orientationAttachment = Instance.new("Attachment")
orientationAttachment.Name = "SwingOrientationAttachment"
orientationAttachment.Parent = rootPart
local alignOrientation = Instance.new("AlignOrientation")
alignOrientation.Name = "SwingUprightAssist"
alignOrientation.Mode = Enum.OrientationAlignmentMode.OneAttachment
alignOrientation.Attachment0 = orientationAttachment
alignOrientation.RigidityEnabled = false
alignOrientation.MaxTorque = ORIENTATION_MAX_TORQUE
alignOrientation.Responsiveness = ORIENTATION_RESPONSIVENESS
alignOrientation.CFrame = rootPart.CFrame
alignOrientation.Parent = rootPart
ragdollOrientationAttachment = orientationAttachment
ragdollAlignOrientation = alignOrientation
if humanoid then
humanoid.AutoRotate = false
end
end
local function endRagdoll()
if ragdollAlignOrientation then ragdollAlignOrientation:Destroy() end
if ragdollOrientationAttachment then ragdollOrientationAttachment:Destroy() end
ragdollAlignOrientation = nil
ragdollOrientationAttachment = nil
if humanoid then
humanoid.AutoRotate = true
end
end
local function destroyAllWebs()
for _, entry in ipairs(activeWebs) do
destroyWebEntry(entry)
end
activeWebs = {}
end
local function onCharacterAdded(newCharacter)
activeWebs = {} -- the old instances belonged to the previous body and are already gone
sliding = false
ragdollOrientationAttachment = nil
ragdollAlignOrientation = nil
cancelWebTravel()
character = newCharacter
humanoid = character:WaitForChild("Humanoid")
rootPart = character:WaitForChild("HumanoidRootPart")
humanoid.Died:Connect(destroyAllWebs)
end
onCharacterAdded(player.Character or player.CharacterAdded:Wait())
player.CharacterAdded:Connect(onCharacterAdded)
-- ===================== WEB LOGIC =====================
local function isValidTarget(hitPart, hitPosition)
if not hitPart or not hitPart:IsA("BasePart") then
return false
end
if hitPart:IsDescendantOf(character) then
return false
end
if not hitPart.Anchored then
return false -- only grab fixed/static things (buildings, ground, etc.)
end
local ancestorModel = hitPart:FindFirstAncestorOfClass("Model")
if ancestorModel and Players:GetPlayerFromCharacter(ancestorModel) then
return false -- don't let players attach a web to each other
end
local size = hitPart.Size
if math.max(size.X, size.Y, size.Z) < MIN_TARGET_SIZE then
return false
end
if (hitPosition - rootPart.Position).Magnitude > MAX_WEB_DISTANCE then
return false
end
return true
end
local function applyLaunchBoost()
local vel = rootPart.AssemblyLinearVelocity
local horizontal = Vector3.new(vel.X, 0, vel.Z)
if horizontal.Magnitude > 2 then
local boosted = horizontal * LAUNCH_BOOST_MULTIPLIER
if boosted.Magnitude > MAX_SWING_SPEED then
boosted = boosted.Unit * MAX_SWING_SPEED
end
rootPart.AssemblyLinearVelocity = Vector3.new(boosted.X, vel.Y, boosted.Z)
end
end
-- Actually attaches the real physics rope once the visual string has
-- finished travelling to its target.
local function attachWeb(hitPart, hitPosition)
if not scriptEnabled then return end
if not rootPart or not rootPart.Parent then return end
local anchorAttachment = Instance.new("Attachment")
anchorAttachment.Name = "WebAnchor"
anchorAttachment.Parent = hitPart -- Parent MUST be set before WorldPosition,
anchorAttachment.WorldPosition = hitPosition -- otherwise the position is computed wrong
-- and the anchor can end up far away
-- (this caused the old teleport bug).
local playerAttachment = Instance.new("Attachment")
playerAttachment.Name = "WebHand"
playerAttachment.Position = Vector3.new(0, SWING_HAND_HEIGHT, 0) -- not at the center of mass -> natural lean
playerAttachment.Parent = rootPart
local distance = (hitPosition - rootPart.Position).Magnitude
local rope = Instance.new("RopeConstraint")
rope.Name = "SpiderWebRope"
rope.Attachment0 = playerAttachment
rope.Attachment1 = anchorAttachment
rope.Length = math.max(distance * ROPE_SLACK, MIN_ROPE_LENGTH)
rope.Restitution = 0
rope.Visible = true
rope.Color = BrickColor.new("White")
rope.Thickness = ROPE_THICKNESS
rope.Parent = rootPart
table.insert(activeWebs, {
rope = rope,
attachment0 = playerAttachment,
attachment1 = anchorAttachment,
})
applyLaunchBoost()
end
local function shootWeb()
if not scriptEnabled then return end
if not rootPart or not rootPart.Parent then return end
local hitPart = mouse.Target
local hit = mouse.Hit
if not hitPart or not hit then return end
local hitPosition = hit.Position
if not isValidTarget(hitPart, hitPosition) then return end
-- only one web at a time: the old one (attached, or still shooting out) disappears immediately
destroyAllWebs()
cancelWebTravel()
sliding = false
-- weak "keep me upright" force: only stops you from ending up permanently
-- upside-down, but is soft enough to let you lean/sway naturally with the
-- swing's motion (your body still looks completely normal - just looser
-- and more physics-driven). Starts right away (before the string even
-- lands) so shooting feels immediately responsive, and stays active
-- until you actually touch the ground.
ensureRagdollRig()
if humanoid then
-- Physics state = the Humanoid completely stops managing horizontal
-- speed, so the rope's pendulum physics can take over without
-- "sticking" when you're close to the ground at the bottom of the arc.
humanoid:ChangeState(Enum.HumanoidStateType.Physics)
end
-- Shoot the string out visually instead of having it just appear at full
-- length: a thin glowing line travels from your hand to the target at a
-- fixed speed, then the real physics rope attaches once it "lands".
travelToken = travelToken + 1
local myToken = travelToken
local startPosition = rootPart.Position + Vector3.new(0, SWING_HAND_HEIGHT, 0)
local travelDistance = (hitPosition - startPosition).Magnitude
local travelTime = math.clamp(travelDistance / WEB_TRAVEL_SPEED, WEB_TRAVEL_MIN_TIME, WEB_TRAVEL_MAX_TIME)
local travelPart = Instance.new("Part")
travelPart.Name = "WebTravelVisual"
travelPart.Anchored = true
travelPart.CanCollide = false
travelPart.CanQuery = false
travelPart.CanTouch = false
travelPart.CastShadow = false
travelPart.Material = Enum.Material.Neon
travelPart.Color = Color3.new(1, 1, 1)
travelPart.Size = Vector3.new(ROPE_THICKNESS, ROPE_THICKNESS, 0.05)
travelPart.CFrame = CFrame.new(startPosition, hitPosition)
travelPart.Parent = workspace
currentTravelPart = travelPart
task.spawn(function()
local elapsed = 0
while elapsed < travelTime do
if travelToken ~= myToken then
return -- cancelled: replaced by a new shot, released, disabled, or respawned
end
elapsed = elapsed + RunService.Heartbeat:Wait()
local alpha = math.clamp(elapsed / travelTime, 0, 1)
local tip = startPosition:Lerp(hitPosition, alpha)
local length = math.max((tip - startPosition).Magnitude, 0.05)
travelPart.Size = Vector3.new(ROPE_THICKNESS, ROPE_THICKNESS, length)
travelPart.CFrame = CFrame.new(startPosition, tip) * CFrame.new(0, 0, -length / 2)
end
if travelToken ~= myToken then return end
travelPart:Destroy()
currentTravelPart = nil
attachWeb(hitPart, hitPosition)
end)
end
-- Releases the web on purpose (kept separate from destroyAllWebs, which is
-- also used internally when a new web replaces an old one, or on death/disable).
local function releaseWebs()
local hadWeb = #activeWebs > 0
local vel = (rootPart and rootPart.AssemblyLinearVelocity) or Vector3.new()
local horizontal = Vector3.new(vel.X, 0, vel.Z)
destroyAllWebs()
cancelWebTravel()
if not hadWeb then return end
if horizontal.Magnitude >= SLIDE_MIN_SPEED then
-- fly off with the speed you had, then gradually slow down instead
-- of stopping dead when you land
sliding = true
slideVelocity = horizontal
slideEndTime = tick() + SLIDE_MAX_DURATION
else
sliding = false
end
-- Deliberately NOT restoring AutoRotate/Humanoid state here anymore:
-- you stay loose/"ragdolled" for as long as you're still in the air,
-- and only snap back to normal control once you actually touch the
-- ground (handled in the Heartbeat loop below).
end
local actionHandlers = {
Shoot = shootWeb,
Release = releaseWebs,
ReelIn = function() reeling = true end,
LetOut = function() lettingOut = true end,
}
local actionHandlersEnded = {
ReelIn = function() reeling = false end,
LetOut = function() lettingOut = false end,
}
-- ===================== PHYSICS LOOP (climbing + speed) =====================
RunService.Heartbeat:Connect(function(dt)
if not scriptEnabled then return end
if not humanoid or not rootPart then return end
-- reel in / let out
if reeling or lettingOut then
for _, entry in ipairs(activeWebs) do
local rope = entry.rope
if rope and rope.Parent then
if reeling then
rope.Length = math.max(MIN_ROPE_LENGTH, rope.Length - REEL_IN_SPEED * dt)
elseif lettingOut then
rope.Length = math.min(MAX_WEB_DISTANCE, rope.Length + LET_OUT_SPEED * dt)
end
end
end
end
-- extra speed while swinging (hold WASD)
if #activeWebs > 0 then
local moveDir = humanoid.MoveDirection
if moveDir.Magnitude > 0.05 then
local vel = rootPart.AssemblyLinearVelocity
local newVel = vel + moveDir * SWING_THRUST * dt
local horizontal = Vector3.new(newVel.X, 0, newVel.Z)
if horizontal.Magnitude > MAX_SWING_SPEED then
horizontal = horizontal.Unit * MAX_SWING_SPEED
newVel = Vector3.new(horizontal.X, newVel.Y, horizontal.Z)
end
rootPart.AssemblyLinearVelocity = newVel
end
end
-- ragdoll upkeep: stays active the whole time you're airborne - from the
-- moment you shoot a web, through releasing it, until you actually touch
-- the ground. While a web is still attached we NEVER end it here (even if
-- you brush the ground mid-swing), so the pendulum can't get "stuck" at
-- the bottom of a low arc.
if ragdollAlignOrientation then
if #activeWebs == 0 and humanoid.FloorMaterial ~= Enum.Material.Air then
-- touched the ground after releasing: stop being ragdolled
endRagdoll()
if humanoid:GetState() == Enum.HumanoidStateType.Physics then
humanoid:ChangeState(Enum.HumanoidStateType.Landed)
end
else
if humanoid:GetState() ~= Enum.HumanoidStateType.Physics then
humanoid:ChangeState(Enum.HumanoidStateType.Physics)
end
-- update the "keep upright" target: same yaw as right now, but
-- with no tilt/roll. Lets you lean/sway freely with the motion in
-- the short term, but you'll never end up permanently upside-down.
local look = rootPart.CFrame.LookVector
local flatLook = Vector3.new(look.X, 0, look.Z)
if flatLook.Magnitude > 0.001 then
ragdollAlignOrientation.CFrame = CFrame.lookAt(rootPart.Position, rootPart.Position + flatLook)
end
end
end
-- slide after releasing the web: gradually brake (like friction) instead
-- of letting the Humanoid zero your speed the instant you land.
-- Deliberately does NOT touch the Humanoid's state - you stay a fully
-- normal, animated character the whole time, since no web exists anymore.
if sliding then
if tick() > slideEndTime or slideVelocity.Magnitude < 2 then
sliding = false
else
slideVelocity = slideVelocity * math.max(0, 1 - SLIDE_FRICTION * dt)
local vel = rootPart.AssemblyLinearVelocity
rootPart.AssemblyLinearVelocity = Vector3.new(slideVelocity.X, vel.Y, slideVelocity.Z)
end
end
end)
-- ===================== UI =====================
local playerGui = player:WaitForChild("PlayerGui")
local screenGui = Instance.new("ScreenGui")
screenGui.Name = "SpiderWebGUI"
screenGui.ResetOnSpawn = false
screenGui.IgnoreGuiInset = true
screenGui.Parent = playerGui
local panel = Instance.new("Frame")
panel.Name = "Panel"
panel.AutomaticSize = Enum.AutomaticSize.Y
panel.Size = UDim2.new(0, 260, 0, 0)
panel.Position = UDim2.new(0, 10, 0.5, 0)
panel.AnchorPoint = Vector2.new(0, 0.5)
panel.BackgroundColor3 = Color3.fromRGB(20, 20, 24)
panel.BackgroundTransparency = 0.08
panel.BorderSizePixel = 0
panel.Visible = true
panel.Parent = screenGui
Instance.new("UICorner", panel).CornerRadius = UDim.new(0, 10)
local stroke = Instance.new("UIStroke")
stroke.Color = Color3.fromRGB(255, 255, 255)
stroke.Transparency = 0.85
stroke.Parent = panel
local outerLayout = Instance.new("UIListLayout")
outerLayout.SortOrder = Enum.SortOrder.LayoutOrder
outerLayout.Padding = UDim.new(0, 8)
outerLayout.Parent = panel
local padding = Instance.new("UIPadding")
padding.PaddingTop = UDim.new(0, 10)
padding.PaddingBottom = UDim.new(0, 10)
padding.PaddingLeft = UDim.new(0, 12)
padding.PaddingRight = UDim.new(0, 12)
padding.Parent = panel
-- Header row (drag this to move the panel)
local header = Instance.new("Frame")
header.Name = "Header"
header.Size = UDim2.new(1, 0, 0, 22)
header.BackgroundTransparency = 1
header.LayoutOrder = 0
header.Parent = panel
local title = Instance.new("TextLabel")
title.Size = UDim2.new(1, -52, 1, 0)
title.BackgroundTransparency = 1
title.Text = "SPIDER WEB"
title.TextColor3 = Color3.fromRGB(255, 255, 255)
title.Font = Enum.Font.GothamBold
title.TextSize = 15
title.TextXAlignment = Enum.TextXAlignment.Left
title.Parent = header
local minimizeButton = Instance.new("TextButton")
minimizeButton.Name = "MinimizeButton"
minimizeButton.Size = UDim2.new(0, 22, 0, 22)
minimizeButton.Position = UDim2.new(1, -50, 0, 0)
minimizeButton.BackgroundColor3 = Color3.fromRGB(45, 45, 52)
minimizeButton.BorderSizePixel = 0
minimizeButton.Text = "–"
minimizeButton.TextColor3 = Color3.fromRGB(255, 255, 255)
minimizeButton.Font = Enum.Font.GothamBold
minimizeButton.TextSize = 16
minimizeButton.Parent = header
Instance.new("UICorner", minimizeButton).CornerRadius = UDim.new(0, 6)
local closeButton = Instance.new("TextButton")
closeButton.Name = "CloseButton"
closeButton.Size = UDim2.new(0, 22, 0, 22)
closeButton.Position = UDim2.new(1, -24, 0, 0)
closeButton.BackgroundColor3 = Color3.fromRGB(70, 40, 40)
closeButton.BorderSizePixel = 0
closeButton.Text = "X"
closeButton.TextColor3 = Color3.fromRGB(255, 255, 255)
closeButton.Font = Enum.Font.GothamBold
closeButton.TextSize = 14
closeButton.Parent = header
Instance.new("UICorner", closeButton).CornerRadius = UDim.new(0, 6)
-- Content (hidden entirely when minimized, panel shrinks automatically)
local content = Instance.new("Frame")
content.Name = "Content"
content.AutomaticSize = Enum.AutomaticSize.Y
content.Size = UDim2.new(1, 0, 0, 0)
content.BackgroundTransparency = 1
content.LayoutOrder = 1
content.Parent = panel
local contentLayout = Instance.new("UIListLayout")
contentLayout.SortOrder = Enum.SortOrder.LayoutOrder
contentLayout.Padding = UDim.new(0, 6)
contentLayout.Parent = content
-- Enable/disable row
local toggleRow = Instance.new("Frame")
toggleRow.Size = UDim2.new(1, 0, 0, 28)
toggleRow.BackgroundTransparency = 1
toggleRow.LayoutOrder = 0
toggleRow.Parent = content
local toggleLabel = Instance.new("TextLabel")
toggleLabel.Size = UDim2.new(0.55, 0, 1, 0)
toggleLabel.BackgroundTransparency = 1
toggleLabel.Text = "Enabled"
toggleLabel.TextColor3 = Color3.fromRGB(220, 220, 220)
toggleLabel.Font = Enum.Font.Gotham
toggleLabel.TextSize = 13
toggleLabel.TextXAlignment = Enum.TextXAlignment.Left
toggleLabel.Parent = toggleRow
local toggleButton = Instance.new("TextButton")
toggleButton.Size = UDim2.new(0.45, 0, 1, 0)
toggleButton.Position = UDim2.new(0.55, 0, 0, 0)
toggleButton.BackgroundColor3 = Color3.fromRGB(60, 180, 90)
toggleButton.BorderSizePixel = 0
toggleButton.Text = "ON"
toggleButton.TextColor3 = Color3.fromRGB(255, 255, 255)
toggleButton.Font = Enum.Font.GothamBold
toggleButton.TextSize = 13
toggleButton.Parent = toggleRow
Instance.new("UICorner", toggleButton).CornerRadius = UDim.new(0, 6)
local function setScriptEnabled(state)
scriptEnabled = state
if scriptEnabled then
toggleButton.Text = "ON"
toggleButton.BackgroundColor3 = Color3.fromRGB(60, 180, 90)
else
toggleButton.Text = "OFF"
toggleButton.BackgroundColor3 = Color3.fromRGB(190, 60, 60)
reeling, lettingOut = false, false
sliding = false
destroyAllWebs()
cancelWebTravel()
endRagdoll()
if humanoid then
humanoid:ChangeState(Enum.HumanoidStateType.Freefall)
end
end
end
toggleButton.MouseButton1Click:Connect(function()
setScriptEnabled(not scriptEnabled)
end)
-- keybind rows
local function inputToText(bound)
if not bound then return "None" end
if bound == Enum.UserInputType.MouseButton1 then return "Left Click" end
if bound == Enum.UserInputType.MouseButton2 then return "Right Click" end
if bound == Enum.UserInputType.MouseButton3 then return "Middle Click" end
if bound.EnumType == Enum.KeyCode then return bound.Name end
return tostring(bound)
end
local rowsOrder = {"Shoot", "Release", "ReelIn", "LetOut"}
local bindButtons = {}
for i, key in ipairs(rowsOrder) do
local action = actions[key]
local row = Instance.new("Frame")
row.Size = UDim2.new(1, 0, 0, 26)
row.BackgroundTransparency = 1
row.LayoutOrder = i
row.Parent = content
local label = Instance.new("TextLabel")
label.Size = UDim2.new(0.5, 0, 1, 0)
label.BackgroundTransparency = 1
label.Text = action.name
label.TextColor3 = Color3.fromRGB(220, 220, 220)
label.Font = Enum.Font.Gotham
label.TextSize = 12
label.TextXAlignment = Enum.TextXAlignment.Left
label.TextTruncate = Enum.TextTruncate.AtEnd
label.Parent = row
local bindBtn = Instance.new("TextButton")
bindBtn.Size = UDim2.new(0.5, 0, 1, 0)
bindBtn.Position = UDim2.new(0.5, 0, 0, 0)
bindBtn.BackgroundColor3 = Color3.fromRGB(45, 45, 52)
bindBtn.BorderSizePixel = 0
bindBtn.TextColor3 = Color3.fromRGB(255, 255, 255)
bindBtn.Font = Enum.Font.GothamBold
bindBtn.TextSize = 12
bindBtn.Text = inputToText(action.input)
bindBtn.Parent = row
Instance.new("UICorner", bindBtn).CornerRadius = UDim.new(0, 6)
bindButtons[key] = bindBtn
end
-- minimize: collapses the panel down to just the header
local minimized = false
local function setMinimized(state)
minimized = state
content.Visible = not minimized
minimizeButton.Text = minimized and "+" or "–"
end
minimizeButton.MouseButton1Click:Connect(function()
setMinimized(not minimized)
end)
-- close: fully hides the panel AND turns the whole mechanic off
local function closePanel()
panel.Visible = false
setScriptEnabled(false)
end
closeButton.MouseButton1Click:Connect(closePanel)
-- drag the header to move the panel
local dragging = false
local dragInput, dragStart, startPos
local function updateDrag(input)
local delta = input.Position - dragStart
panel.Position = UDim2.new(
startPos.X.Scale, startPos.X.Offset + delta.X,
startPos.Y.Scale, startPos.Y.Offset + delta.Y
)
end
header.InputBegan:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseButton1
or input.UserInputType == Enum.UserInputType.Touch then
dragging = true
dragStart = input.Position
startPos = panel.Position
input.Changed:Connect(function()
if input.UserInputState == Enum.UserInputState.End then
dragging = false
end
end)
end
end)
header.InputChanged:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseMovement
or input.UserInputType == Enum.UserInputType.Touch then
dragInput = input
end
end)
UserInputService.InputChanged:Connect(function(input)
if input == dragInput and dragging then
updateDrag(input)
end
end)
-- ===================== INPUT (dispatch + rebinding) =====================
local listeningFor = nil -- which action is waiting for a new key press
local function beginListening(actionKey)
if listeningFor then
bindButtons[listeningFor].Text = inputToText(actions[listeningFor].input)
end
listeningFor = actionKey
bindButtons[actionKey].Text = "Press a key..."
end
for key, btn in pairs(bindButtons) do
btn.MouseButton1Click:Connect(function()
beginListening(key)
end)
end
local function getInputBindFromEvent(input)
if input.UserInputType == Enum.UserInputType.Keyboard then
if input.KeyCode == Enum.KeyCode.Unknown then return nil end
return input.KeyCode
elseif input.UserInputType == Enum.UserInputType.MouseButton1
or input.UserInputType == Enum.UserInputType.MouseButton2
or input.UserInputType == Enum.UserInputType.MouseButton3 then
return input.UserInputType
end
return nil
end
local function inputMatches(io, bound)
if not bound then return false end
if typeof(bound) ~= "EnumItem" then return false end
if bound.EnumType == Enum.KeyCode then
return io.KeyCode == bound
elseif bound.EnumType == Enum.UserInputType then
return io.UserInputType == bound
end
return false
end
UserInputService.InputBegan:Connect(function(input, gameProcessed)
if listeningFor then
if input.UserInputType == Enum.UserInputType.Keyboard and input.KeyCode == Enum.KeyCode.Escape then
-- Esc = make the action unbound (no key) instead of binding it to Esc
actions[listeningFor].input = nil
bindButtons[listeningFor].Text = "None"
listeningFor = nil
return
end
local newBind = getInputBindFromEvent(input)
if newBind then
-- clear any duplicate on another action
for otherKey, otherAction in pairs(actions) do
if otherKey ~= listeningFor and otherAction.input == newBind then
otherAction.input = nil
if bindButtons[otherKey] then
bindButtons[otherKey].Text = "None"
end
end
end
actions[listeningFor].input = newBind
bindButtons[listeningFor].Text = inputToText(newBind)
else
bindButtons[listeningFor].Text = inputToText(actions[listeningFor].input)
end
listeningFor = nil
return
end
if gameProcessed then return end
for key, action in pairs(actions) do
if action.input and inputMatches(input, action.input) and actionHandlers[key] then
actionHandlers[key]()
end
end
end)
UserInputService.InputEnded:Connect(function(input, _gameProcessed)
for key, action in pairs(actions) do
if action.input and inputMatches(input, action.input) and actionHandlersEnded[key] then
actionHandlersEnded[key]()
end
end
end)No comments yet for this script.



