
Key RequiredUniversal

-- Grab Pack Script (LocalScript in StarterCharacterScripts)
-- Refined: Launch & Grab only, improved hand contact detection
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local TweenService = game:GetService("TweenService")
local Debris = game:GetService("Debris")
local player = Players.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
local humanoid = character:WaitForChild("Humanoid")
local humanoidRootPart = character:WaitForChild("HumanoidRootPart")
local camera = workspace.CurrentCamera
-- ============================================
-- CONFIGURATION
-- ============================================
local Config = {
-- Hand movement
HandMaxLength = 120,
HandSpeed = 90,
RetractSpeed = 95,
HandStickRadius = 3.5, -- how close hand must be to stick to a surface
-- Launch
LaunchForce = 145,
LaunchVerticalBoost = 32,
LaunchDuration = 0.30,
-- String visuals
StringRadius = 0.07,
StringPulseSpeed= 3,
-- Hand visuals
HandSize = Vector3.new(1.2, 1.2, 1.2),
WristSize = Vector3.new(0.5, 0.5, 0.5),
-- Colors
BlueColor = Color3.fromRGB(0, 120, 255),
RedColor = Color3.fromRGB(220, 30, 30),
StringBlueColor = Color3.fromRGB(0, 80, 200),
StringRedColor = Color3.fromRGB(180, 20, 20),
-- Timing
ShootCooldown = 0.20,
DoubleClickWindow = 0.40,
-- Grab
MaxGrabbableSize = 80,
GrabWeldStiffness = 5000, -- how firmly held objects are kept in place
-- Backpack bob
BobSpeed = 2.5,
BobAmount = 0.04,
}
-- ============================================
-- STATE
-- ============================================
local GrabPack = {
equipped = false,
bobTimer = 0,
hands = {
blue = {
active = false,
extended = false,
grabbed = false,
grabTarget = nil,
grabOffset = CFrame.new(), -- offset from grabbed part
grabWeld = nil,
grabNormal = Vector3.new(0,1,0),
position = Vector3.new(),
direction = Vector3.new(),
wrapPoints = {},
model = nil,
string = {},
returning = false,
clickCount = 0,
clickTimer = 0,
lastShotTime = 0,
pulseTimer = 0,
-- identity
color = Color3.fromRGB(0, 120, 255),
stringColor = Color3.fromRGB(0, 80, 200),
side = "Left",
},
red = {
active = false,
extended = false,
grabbed = false,
grabTarget = nil,
grabOffset = CFrame.new(),
grabWeld = nil,
grabNormal = Vector3.new(0,1,0),
position = Vector3.new(),
direction = Vector3.new(),
wrapPoints = {},
model = nil,
string = {},
returning = false,
clickCount = 0,
clickTimer = 0,
lastShotTime = 0,
pulseTimer = 0,
color = Color3.fromRGB(220, 30, 30),
stringColor = Color3.fromRGB(180, 20, 20),
side = "Right",
},
},
backpackModel = nil,
gui = nil,
}
-- ============================================
-- UTILITY
-- ============================================
local function createPart(parent, size, color, transparency, anchored, cancollide, material)
local p = Instance.new("Part")
p.Size = size or Vector3.new(1,1,1)
p.Color = color or Color3.new(1,1,1)
p.Transparency = transparency or 0
p.Anchored = anchored ~= nil and anchored or true
p.CanCollide = cancollide ~= nil and cancollide or false
p.Material = material or Enum.Material.SmoothPlastic
p.CastShadow = false
p.Parent = parent
return p
end
local function playSound(id, pos, vol, pitch)
local p = Instance.new("Part")
p.Anchored = true; p.CanCollide = false
p.Transparency = 1; p.Size = Vector3.new(0.1,0.1,0.1)
p.Position = pos or Vector3.new()
p.Parent = workspace
local s = Instance.new("Sound", p)
s.SoundId = id or ""; s.Volume = vol or 0.6
s.PlaybackSpeed = pitch or 1
s.RollOffMaxDistance = 70
s:Play()
Debris:AddItem(p, 4)
end
local function tweenTransparency(part, target, dur)
TweenService:Create(part,
TweenInfo.new(dur or 0.12, Enum.EasingStyle.Quad),
{Transparency = target}
):Play()
end
-- Apply a short-lived BodyVelocity on the root part
local function applyRootVelocity(velocity, duration)
local old = humanoidRootPart:FindFirstChild("GrabPackBV")
if old then old:Destroy() end
local bv = Instance.new("BodyVelocity")
bv.Name = "GrabPackBV"
bv.Velocity = velocity
bv.MaxForce = Vector3.new(1e5, 1e5, 1e5)
bv.P = 1e4
bv.Parent = humanoidRootPart
Debris:AddItem(bv, duration or 0.3)
end
local function clearRootVelocity()
local bv = humanoidRootPart:FindFirstChild("GrabPackBV")
if bv then bv:Destroy() end
end
-- ============================================
-- STRING ORIGIN (wrist position)
-- ============================================
local function getStringOrigin(handData)
local armName = handData.side == "Left" and "LeftHand" or "RightHand"
local arm = character:FindFirstChild(armName)
if arm then return arm.Position end
return humanoidRootPart.Position
+ Vector3.new(handData.side == "Left" and -1.5 or 1.5, 0, 0)
end
-- ============================================
-- RAYCAST PARAMS (exclude character)
-- ============================================
local function makeRayParams(extraExcludes)
local p = RaycastParams.new()
local excludes = {character}
if extraExcludes then
for _, v in ipairs(extraExcludes) do
table.insert(excludes, v)
end
end
p.FilterDescendantsInstances = excludes
p.FilterType = Enum.RaycastFilterType.Exclude
return p
end
-- ============================================
-- BACKPACK MODEL
-- ============================================
local function buildBackpack()
local folder = Instance.new("Folder")
folder.Name = "GrabPackModel"
folder.Parent = workspace
local body = createPart(folder, Vector3.new(1.8, 2.0, 0.65), Color3.fromRGB(28, 28, 33))
local trim = createPart(folder, Vector3.new(1.85, 2.05, 0.1), Color3.fromRGB(55, 55, 65))
local strap1 = createPart(folder, Vector3.new(0.15, 1.8, 0.1), Color3.fromRGB(45, 45, 45))
local strap2 = createPart(folder, Vector3.new(0.15, 1.8, 0.1), Color3.fromRGB(45, 45, 45))
local blueHousing = createPart(folder, Vector3.new(0.55,0.55,0.55), Config.BlueColor)
blueHousing.Shape = Enum.PartType.Ball
blueHousing.Material = Enum.Material.Neon
local redHousing = createPart(folder, Vector3.new(0.55,0.55,0.55), Config.RedColor)
redHousing.Shape = Enum.PartType.Ball
redHousing.Material = Enum.Material.Neon
local lightBlue = createPart(folder, Vector3.new(0.15,0.15,0.15), Config.BlueColor)
lightBlue.Material = Enum.Material.Neon; lightBlue.Shape = Enum.PartType.Ball
local lightRed = createPart(folder, Vector3.new(0.15,0.15,0.15), Config.RedColor)
lightRed.Material = Enum.Material.Neon; lightRed.Shape = Enum.PartType.Ball
GrabPack.backpackModel = {
folder = folder, body = body, trim = trim,
strap1 = strap1, strap2 = strap2,
blueHousing = blueHousing, redHousing = redHousing,
lightBlue = lightBlue, lightRed = lightRed,
}
end
local function updateBackpackPosition()
if not GrabPack.backpackModel then return end
local torso = character:FindFirstChild("UpperTorso") or character:FindFirstChild("Torso")
if not torso then return end
GrabPack.bobTimer = GrabPack.bobTimer + (1/60) * Config.BobSpeed
local bob = math.sin(GrabPack.bobTimer) * Config.BobAmount
local cf = torso.CFrame * CFrame.new(0, bob, 0.78)
local m = GrabPack.backpackModel
m.body.CFrame = cf
m.trim.CFrame = cf * CFrame.new(0, 0, 0.3)
m.strap1.CFrame = cf * CFrame.new(-0.6, 0, -0.1)
m.strap2.CFrame = cf * CFrame.new( 0.6, 0, -0.1)
m.blueHousing.CFrame = cf * CFrame.new(-0.72, 0.32, -0.2)
m.redHousing.CFrame = cf * CFrame.new( 0.72, 0.32, -0.2)
m.lightBlue.CFrame = cf * CFrame.new(-0.5, -0.7, -0.28)
m.lightRed.CFrame = cf * CFrame.new( 0.5, -0.7, -0.28)
-- Pulse indicator lights
local t = tick()
local blueActive = GrabPack.hands.blue.active
local redActive = GrabPack.hands.red.active
m.lightBlue.Transparency = blueActive and (0.2 + math.abs(math.sin(t*4))*0.4) or 0.6
m.lightRed.Transparency = redActive and (0.2 + math.abs(math.sin(t*4))*0.4) or 0.6
end
-- ============================================
-- HAND MODEL
-- ============================================
local function setHandVisible(handData, visible)
if not handData.model then return end
local t = visible and 0 or 1
tweenTransparency(handData.model.palm, t, 0.10)
tweenTransparency(handData.model.wrist, t, 0.10)
tweenTransparency(handData.model.thumb, t, 0.10)
tweenTransparency(handData.model.forearm, t, 0.10)
for _, f in ipairs(handData.model.fingers) do tweenTransparency(f, t, 0.10) end
for _, k in ipairs(handData.model.knuckles) do tweenTransparency(k, t, 0.10) end
handData.model.visible = visible
if handData.model.light then
handData.model.light.Enabled = visible
end
end
local function buildHand(handData)
local folder = Instance.new("Folder")
folder.Name = handData.side .. "Hand"
folder.Parent = workspace
local palm = createPart(folder, Config.HandSize, handData.color, 1)
palm.Shape = Enum.PartType.Ball
local fingers = {}
for _ = 1, 4 do
table.insert(fingers,
createPart(folder, Vector3.new(0.18, 0.52, 0.18), handData.color, 1))
end
local thumb = createPart(folder, Vector3.new(0.18, 0.42, 0.18), handData.color, 1)
local wrist = createPart(folder, Config.WristSize, handData.color, 1)
local forearm = createPart(folder, Vector3.new(0.38, 0.38, 0.9), handData.color, 1)
local knuckles = {}
for _ = 1, 4 do
local k = createPart(folder, Vector3.new(0.14,0.14,0.14), handData.color, 1)
k.Shape = Enum.PartType.Ball
table.insert(knuckles, k)
end
local core = createPart(folder, Vector3.new(0.5,0.5,0.5), handData.color, 1,
true, false, Enum.Material.Neon)
core.Shape = Enum.PartType.Ball
local light = Instance.new("PointLight")
light.Color = handData.color
light.Brightness = 2.5
light.Range = 10
light.Enabled = false
light.Parent = palm
-- Selection box to highlight grabbed objects
local selBox = Instance.new("SelectionBox")
selBox.LineThickness = 0.05
selBox.Color3 = handData.color
selBox.SurfaceTransparency = 0.8
selBox.SurfaceColor3 = handData.color
selBox.Visible = false
selBox.Parent = folder
handData.model = {
folder = folder,
palm = palm,
fingers = fingers,
thumb = thumb,
wrist = wrist,
forearm = forearm,
knuckles = knuckles,
core = core,
light = light,
selBox = selBox,
visible = false,
}
setHandVisible(handData, false)
end
local function updateHandModel(handData, dt)
if not handData.model then return end
local pos = handData.position
local dir = handData.direction
if dir.Magnitude < 0.01 then dir = Vector3.new(0,0,-1) end
local cf = CFrame.new(pos, pos + dir)
handData.model.palm.CFrame = cf
handData.model.wrist.CFrame = cf * CFrame.new(0, 0, 0.55)
handData.model.forearm.CFrame = cf * CFrame.new(0, 0, 1.0)
handData.model.core.CFrame = cf
-- Finger curl
local curl = handData.grabbed and 0.8 or 0.3
local fingerOffsets = {
CFrame.new(-0.35, 0.40, 0.2) * CFrame.Angles(curl, 0, 0),
CFrame.new(-0.12, 0.45, 0.2) * CFrame.Angles(curl, 0, 0),
CFrame.new( 0.12, 0.45, 0.2) * CFrame.Angles(curl, 0, 0),
CFrame.new( 0.35, 0.40, 0.2) * CFrame.Angles(curl, 0, 0),
}
for i, f in ipairs(handData.model.fingers) do
f.CFrame = cf * fingerOffsets[i]
end
handData.model.thumb.CFrame = cf * CFrame.new(-0.5, 0.1, 0.3) * CFrame.Angles(0, 0, 0.8)
local knuckleOffsets = {
CFrame.new(-0.35, 0.52, 0.05),
CFrame.new(-0.12, 0.57, 0.05),
CFrame.new( 0.12, 0.57, 0.05),
CFrame.new( 0.35, 0.52, 0.05),
}
for i, k in ipairs(handData.model.knuckles) do
k.CFrame = cf * knuckleOffsets[i]
end
-- Neon core pulse
handData.pulseTimer = (handData.pulseTimer or 0) + dt * Config.StringPulseSpeed
local pulse = math.abs(math.sin(handData.pulseTimer))
if handData.grabbed then
handData.model.core.Color = handData.color
handData.model.core.Transparency = 0.3 + pulse * 0.4
handData.model.light.Brightness = 2 + pulse * 2
else
handData.model.core.Color = handData.color
handData.model.core.Transparency = 1
handData.model.light.Brightness = 2.5
end
-- Highlight grabbed object
if handData.grabbed and handData.grabTarget then
handData.model.selBox.Adornee = handData.grabTarget
handData.model.selBox.Visible = true
else
handData.model.selBox.Visible = false
end
end
-- ============================================
-- STRING SYSTEM
-- ============================================
local function clearString(handData)
for _, seg in ipairs(handData.string) do
if seg and seg.Parent then seg:Destroy() end
end
handData.string = {}
end
local function updateString(handData, dt)
clearString(handData)
if not handData.active then return end
local folder = handData.model and handData.model.folder or workspace
local origin = getStringOrigin(handData)
local points = {origin}
for _, wp in ipairs(handData.wrapPoints) do
table.insert(points, wp.position)
end
table.insert(points, handData.position)
handData.pulseTimer = (handData.pulseTimer or 0) + (dt or 0) * Config.StringPulseSpeed
local pulseAlpha = 0.5 + math.abs(math.sin(handData.pulseTimer)) * 0.5
local pulseColor = handData.stringColor:Lerp(Color3.new(1,1,1), pulseAlpha * 0.18)
for i = 1, #points - 1 do
local p1, p2 = points[i], points[i+1]
local dist = (p2 - p1).Magnitude
if dist < 0.01 then continue end
local seg = Instance.new("Part")
seg.Size = Vector3.new(Config.StringRadius*2, Config.StringRadius*2, dist)
seg.CFrame = CFrame.new((p1+p2)/2, p2)
seg.Color = pulseColor
seg.Anchored = true; seg.CanCollide = false
seg.Material = Enum.Material.Neon; seg.CastShadow = false
seg.Parent = folder
table.insert(handData.string, seg)
end
end
-- ============================================
-- WRAP / UNWRAP
-- ============================================
local function checkWrap(handData, oldPos, newPos)
local params = makeRayParams(handData.model and {handData.model.folder} or nil)
local dir = newPos - oldPos
if dir.Magnitude < 0.01 then return false, nil end
local result = workspace:Raycast(oldPos, dir, params)
if result then
local wrapPos = result.Position + result.Normal * 0.05
table.insert(handData.wrapPoints, {
position = wrapPos,
normal = result.Normal,
instance = result.Instance,
})
return true, result.Position
end
return false, nil
end
local function checkUnwrap(handData)
if #handData.wrapPoints == 0 then return end
local origin = getStringOrigin(handData)
local lastWrap = handData.wrapPoints[#handData.wrapPoints]
local prevPos = #handData.wrapPoints > 1
and handData.wrapPoints[#handData.wrapPoints - 1].position
or origin
if (handData.position - prevPos).Magnitude
< (handData.position - lastWrap.position).Magnitude * 0.5 then
table.remove(handData.wrapPoints, #handData.wrapPoints)
end
end
-- ============================================
-- AIM (multi-cast for reliable hit detection)
-- ============================================
local function getAimTarget(handData)
local params = makeRayParams(handData.model and {handData.model.folder} or nil)
local unitRay = camera:ScreenPointToRay(
camera.ViewportSize.X / 2,
camera.ViewportSize.Y / 2
)
-- Primary centre ray
local result = workspace:Raycast(
unitRay.Origin,
unitRay.Direction * Config.HandMaxLength,
params
)
if result then
return result.Position + result.Normal * 0.08, result.Instance, result.Normal
end
-- Secondary spread rays for better surface contact
local spread = 0.06
local offsets = {
Vector3.new( spread, 0, 0), Vector3.new(-spread, 0, 0),
Vector3.new(0, spread, 0), Vector3.new(0, -spread, 0),
Vector3.new( spread, spread, 0), Vector3.new(-spread, -spread, 0),
}
for _, off in ipairs(offsets) do
local tweakedDir = (unitRay.Direction + off).Unit * Config.HandMaxLength
local r2 = workspace:Raycast(unitRay.Origin, tweakedDir, params)
if r2 then
return r2.Position + r2.Normal * 0.08, r2.Instance, r2.Normal
end
end
-- Nothing hit — return max range point
return unitRay.Origin + unitRay.Direction * Config.HandMaxLength, nil, nil
end
-- ============================================
-- GRAB (attach hand to any BasePart)
-- ============================================
local function attemptGrab(handData)
local inst = handData.targetInstance
if not inst then return false end
if not inst:IsA("BasePart") then return false end
if inst:IsDescendantOf(character) then return false end
local sz = inst.Size
if sz.X > Config.MaxGrabbableSize
or sz.Y > Config.MaxGrabbableSize
or sz.Z > Config.MaxGrabbableSize then
return false
end
handData.grabbed = true
handData.grabTarget = inst
handData.grabNormal = handData.targetNormal or Vector3.new(0,1,0)
-- Store offset so we can track unanchored parts
handData.grabOffset = inst.CFrame:ToObjectSpace(CFrame.new(handData.position))
playSound("rbxassetid://9120386436", handData.position, 0.7, 1)
return true
end
-- ============================================
-- RETRACT
-- ============================================
local function retractHand(handData)
-- Release grabbed object
if handData.grabWeld then
handData.grabWeld:Destroy()
handData.grabWeld = nil
end
handData.extended = false
handData.grabbed = false
handData.grabTarget = nil
handData.returning = true
if handData.model then
handData.model.selBox.Visible = false
end
playSound("rbxassetid://9120386436", handData.position, 0.45, 1.1)
end
-- ============================================
-- LAUNCH (propel player toward grabbed point)
-- ============================================
local function launchToHand(handData)
if not handData.grabbed then return end
local target = handData.position
local dir = (target - humanoidRootPart.Position)
if dir.Magnitude < 0.1 then return end
local launchDir = (dir.Unit
+ Vector3.new(0, Config.LaunchVerticalBoost / Config.LaunchForce, 0)).Unit
applyRootVelocity(launchDir * Config.LaunchForce, Config.LaunchDuration)
playSound("rbxassetid://9120386436", humanoidRootPart.Position, 0.8, 0.95)
-- Auto-retract after launch
task.delay(Config.LaunchDuration + 0.08, function()
retractHand(handData)
end)
handData.clickCount = 0
end
-- ============================================
-- SHOOT
-- ============================================
local function shootHand(handData)
local now = tick()
if now - (handData.lastShotTime or 0) < Config.ShootCooldown then return end
-- Already active: double-tap = launch, single tap = retract
if handData.active then
handData.clickCount = handData.clickCount + 1
handData.clickTimer = 0
if handData.clickCount >= 2 and handData.grabbed then
launchToHand(handData)
elseif not handData.grabbed then
retractHand(handData)
end
return
end
-- Fire new shot
handData.active = true
handData.extended = false
handData.grabbed = false
handData.returning = false
handData.wrapPoints = {}
handData.clickCount = 1
handData.clickTimer = 0
handData.lastShotTime = now
local targetPos, targetInst, targetNormal = getAimTarget(handData)
handData.targetPos = targetPos
handData.targetInstance = targetInst
handData.targetNormal = targetNormal or Vector3.new(0, 1, 0)
local origin = getStringOrigin(handData)
handData.position = origin
handData.direction = (targetPos - origin).Unit
handData.distance = 0
setHandVisible(handData, true)
playSound("rbxassetid://9120386436", origin, 0.7, 1 + math.random() * 0.1)
end
-- ============================================
-- HAND MOVEMENT UPDATE
-- ============================================
local function updateHandMovement(handData, dt)
if not handData.active then return end
local origin = getStringOrigin(handData)
-- ── Retracting ───────────────────────────────
if handData.returning then
local target = #handData.wrapPoints > 0
and handData.wrapPoints[#handData.wrapPoints].position
or origin
local moveDir = target - handData.position
local moveDist = moveDir.Magnitude
if moveDist < 0.5 then
if #handData.wrapPoints > 0 then
table.remove(handData.wrapPoints, #handData.wrapPoints)
else
-- Hand fully retracted
handData.active = false
handData.extended = false
handData.returning = false
handData.grabbed = false
handData.clickCount = 0
handData.wrapPoints = {}
setHandVisible(handData, false)
clearString(handData)
end
else
local step = math.min(Config.RetractSpeed * dt, moveDist)
handData.position = handData.position + moveDir.Unit * step
handData.direction = moveDir.Unit
end
-- ── Holding a grabbed part ────────────────────
elseif handData.grabbed then
if handData.grabTarget and handData.grabTarget.Parent then
-- Track unanchored parts
if not handData.grabTarget.Anchored then
local worldGrabPos = handData.grabTarget.CFrame
* handData.grabOffset
handData.position = worldGrabPos.Position
end
checkUnwrap(handData)
else
-- Part disappeared
retractHand(handData)
end
-- ── Extending ────────────────────────────────
else
local targetPos = handData.targetPos
local moveDir = targetPos - handData.position
local moveDist = moveDir.Magnitude
local oldPos = handData.position
local step = math.min(Config.HandSpeed * dt, moveDist)
local newPos = handData.position
+ (moveDist > 0.001 and moveDir.Unit * step or Vector3.new())
local wrapped, wrapPos = checkWrap(handData, oldPos, newPos)
handData.position = wrapped and (wrapPos or newPos) or newPos
if moveDist > 0.001 then
handData.direction = moveDir.Unit
end
-- Arrived at target?
if moveDist <= Config.HandStickRadius then
handData.extended = true
handData.position = targetPos
if not attemptGrab(handData) then
-- Nothing to grab – retract after a moment
task.delay(0.5, function()
if handData.active and not handData.grabbed then
retractHand(handData)
end
end)
end
end
-- Max range reached
if (handData.position - origin).Magnitude >= Config.HandMaxLength then
handData.extended = true
if not handData.grabbed then
task.delay(0.5, function()
if handData.active and not handData.grabbed then
retractHand(handData)
end
end)
end
end
end
end
-- ============================================
-- CLICK TIMER
-- ============================================
local function updateClickTimers(dt)
for _, h in pairs(GrabPack.hands) do
if h.clickTimer < Config.DoubleClickWindow then
h.clickTimer = h.clickTimer + dt
end
end
end
-- ============================================
-- GUI
-- ============================================
local function buildGUI()
local sg = Instance.new("ScreenGui")
sg.Name = "GrabPackGUI"
sg.ResetOnSpawn = false
sg.IgnoreGuiInset = true
sg.Parent = player.PlayerGui
-- Panel
local panel = Instance.new("Frame", sg)
panel.Size = UDim2.new(0, 255, 0, 90)
panel.Position = UDim2.new(0.5, -127, 1, -112)
panel.BackgroundColor3 = Color3.fromRGB(12, 12, 12)
panel.BackgroundTransparency = 0.22
panel.BorderSizePixel = 0
Instance.new("UICorner", panel).CornerRadius = UDim.new(0, 14)
local stroke = Instance.new("UIStroke", panel)
stroke.Color = Color3.fromRGB(80,80,90); stroke.Thickness = 1.2; stroke.Transparency = 0.4
local function makeBtn(color, x, label, keyText)
local btn = Instance.new("TextButton", panel)
btn.Size = UDim2.new(0, 108, 0, 68); btn.Position = UDim2.new(0, x, 0, 11)
btn.BackgroundColor3 = color; btn.Text = ""; btn.BorderSizePixel = 0
Instance.new("UICorner", btn).CornerRadius = UDim.new(0, 10)
local title = Instance.new("TextLabel", btn)
title.Size = UDim2.new(1,0,0.42,0); title.Position = UDim2.new(0,0,0.04,0)
title.BackgroundTransparency = 1; title.Text = label
title.TextColor3 = Color3.new(1,1,1); title.TextScaled = true
title.Font = Enum.Font.GothamBold
local status = Instance.new("TextLabel", btn)
status.Name = "Status"; status.Size = UDim2.new(1,0,0.36,0)
status.Position = UDim2.new(0,0,0.46,0)
status.BackgroundTransparency = 1; status.Text = "READY"
status.TextColor3 = Color3.new(1,1,1); status.TextScaled = true
status.Font = Enum.Font.Gotham
local coolBar = Instance.new("Frame", btn)
coolBar.Name = "CooldownBar"; coolBar.Size = UDim2.new(0,0,0,3)
coolBar.Position = UDim2.new(0,0,1,-3)
coolBar.BackgroundColor3 = Color3.new(1,1,1); coolBar.BorderSizePixel = 0
coolBar.BackgroundTransparency = 0.3
Instance.new("UICorner", coolBar).CornerRadius = UDim.new(0,2)
local keyLbl = Instance.new("TextLabel", panel)
keyLbl.Size = UDim2.new(0,108,0,14); keyLbl.Position = UDim2.new(0,x,1,-18)
keyLbl.BackgroundTransparency = 1; keyLbl.Text = keyText
keyLbl.TextColor3 = color:Lerp(Color3.new(1,1,1),0.4)
keyLbl.TextScaled = true; keyLbl.Font = Enum.Font.GothamBold
return btn, status, coolBar
end
local blueBtn, blueStatus, blueCool = makeBtn(Config.BlueColor, 10, "✋ BLUE", "[Q] Grab / Launch")
local redBtn, redStatus, redCool = makeBtn(Config.RedColor, 133, "✋ RED", "[E] Grab / Launch")
-- Crosshair
local ch = Instance.new("Frame", sg)
ch.Name = "Crosshair"; ch.Size = UDim2.new(0,26,0,26)
ch.Position = UDim2.new(0.5,-13,0.5,-13); ch.BackgroundTransparency = 1
local function chBar(sz, pos)
local f = Instance.new("Frame", ch)
f.Size = sz; f.Position = pos
f.BackgroundColor3 = Color3.new(1,1,1); f.BorderSizePixel = 0
return f
end
chBar(UDim2.new(1,0,0,2), UDim2.new(0,0,0.5,-1))
chBar(UDim2.new(0,2,1,0), UDim2.new(0.5,-1,0,0))
local chDot = Instance.new("Frame", ch)
chDot.Size = UDim2.new(0,4,0,4); chDot.Position = UDim2.new(0.5,-2,0.5,-2)
chDot.BackgroundColor3 = Color3.new(1,1,1); chDot.BorderSizePixel = 0
Instance.new("UICorner", chDot).CornerRadius = UDim.new(1,0)
local chRing = Instance.new("Frame", ch)
chRing.Name = "Ring"; chRing.Size = UDim2.new(0,20,0,20)
chRing.Position = UDim2.new(0.5,-10,0.5,-10)
chRing.BackgroundTransparency = 1; chRing.BorderSizePixel = 0
Instance.new("UICorner", chRing).CornerRadius = UDim.new(1,0)
local rs = Instance.new("UIStroke", chRing)
rs.Thickness = 1.5; rs.Color = Color3.new(1,1,1); rs.Transparency = 0.4
-- Instructions
local instr = Instance.new("TextLabel", sg)
instr.Size = UDim2.new(0,320,0,20); instr.Position = UDim2.new(0.5,-160,1,-130)
instr.BackgroundTransparency = 1
instr.Text = "Q/E = Shoot | R = Retract | Double-tap Q/E = Launch"
instr.TextColor3 = Color3.fromRGB(200,200,200); instr.TextScaled = true
instr.Font = Enum.Font.Gotham
GrabPack.gui = {
screen = sg,
panel = panel,
blueBtn = blueBtn,
redBtn = redBtn,
blueStatus = blueStatus,
redStatus = redStatus,
blueCool = blueCool,
redCool = redCool,
chRing = chRing,
}
end
local function updateGUI(dt)
if not GrabPack.gui then return end
local now = tick()
local gui = GrabPack.gui
local function updateBtn(h, btn, statusLbl, coolBar)
if h.grabbed then
statusLbl.Text = "✅ GRABBED"; btn.BackgroundTransparency = 0
elseif h.extended then
statusLbl.Text = "→ LOCKED"; btn.BackgroundTransparency = 0.1
elseif h.active then
statusLbl.Text = "⟶ FLYING"; btn.BackgroundTransparency = 0.15
else
statusLbl.Text = "READY"; btn.BackgroundTransparency = 0
end
local elapsed = math.clamp((now - (h.lastShotTime or 0)) / Config.ShootCooldown, 0, 1)
TweenService:Create(coolBar,
TweenInfo.new(0.05, Enum.EasingStyle.Linear),
{Size = UDim2.new(elapsed, 0, 0, 3)}
):Play()
end
updateBtn(GrabPack.hands.blue, gui.blueBtn, gui.blueStatus, gui.blueCool)
updateBtn(GrabPack.hands.red, gui.redBtn, gui.redStatus, gui.redCool)
-- Crosshair ring colour
local rs = gui.chRing:FindFirstChildOfClass("UIStroke")
if rs then
if GrabPack.hands.blue.grabbed then
rs.Color = Config.BlueColor
elseif GrabPack.hands.red.grabbed then
rs.Color = Config.RedColor
elseif GrabPack.hands.blue.active then
rs.Color = Config.BlueColor:Lerp(Color3.new(1,1,1), 0.35)
elseif GrabPack.hands.red.active then
rs.Color = Config.RedColor:Lerp(Color3.new(1,1,1), 0.35)
else
rs.Color = Color3.new(1,1,1)
end
end
end
-- ============================================
-- INPUT
-- ============================================
local function connectInput()
local gui = GrabPack.gui
gui.blueBtn.MouseButton1Click:Connect(function()
shootHand(GrabPack.hands.blue)
end)
gui.redBtn.MouseButton1Click:Connect(function()
shootHand(GrabPack.hands.red)
end)
UserInputService.InputBegan:Connect(function(input, processed)
if processed then return end
local kc = input.KeyCode
if kc == Enum.KeyCode.Q then
shootHand(GrabPack.hands.blue)
elseif kc == Enum.KeyCode.E then
shootHand(GrabPack.hands.red)
elseif kc == Enum.KeyCode.R then
retractHand(GrabPack.hands.blue)
retractHand(GrabPack.hands.red)
end
end)
end
-- ============================================
-- INITIALIZE
-- ============================================
local function initialize()
buildBackpack()
buildHand(GrabPack.hands.blue)
buildHand(GrabPack.hands.red)
buildGUI()
connectInput()
GrabPack.equipped = true
print("[GrabPack] Initialized")
print(" Q – Shoot Blue hand")
print(" E – Shoot Red hand")
print(" R – Retract both hands")
print(" Q/E × 2 – Launch toward grabbed point")
end
-- ============================================
-- MAIN LOOP
-- ============================================
RunService.RenderStepped:Connect(function(dt)
if not GrabPack.equipped then return end
if not character or not character.Parent then return end
updateBackpackPosition()
for _, h in pairs(GrabPack.hands) do
updateHandMovement(h, dt)
if h.active then
updateHandModel(h, dt)
updateString(h, dt)
end
end
updateClickTimers(dt)
updateGUI(dt)
end)
-- ============================================
-- CLEANUP
-- ============================================
player.CharacterRemoving:Connect(function()
GrabPack.equipped = false
clearRootVelocity()
if GrabPack.backpackModel then
GrabPack.backpackModel.folder:Destroy()
end
for _, h in pairs(GrabPack.hands) do
clearString(h)
if h.model then h.model.folder:Destroy() end
end
if GrabPack.gui then GrabPack.gui.screen:Destroy() end
end)
initialize()



u made this with claude ai lol
@WuzRERBorn It is a demonstration not made with AI it points out what features I have made in the script so far.
is this server sided or client sided
@yoitsman123 Client side