-- 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()