-- PROJECT SPIDER V3 — FREE SWING EDITION -- LocalScript: StarterPlayer > StarterPlayerScripts -- Q: Hold Swing | E: Zip | R: Hold Pull -- C: Toggle Crawl | F: Charge / Fire Slingshot -- SPACE: Reel while swinging / Leap while crawling -- SHIFT: Extend rope / Crawl faster | X: Cancel -- Mobile buttons are included. local Players = game:GetService("Players") local Workspace = game:GetService("Workspace") local RunService = game:GetService("RunService") local UserInputService = game:GetService("UserInputService") local Debris = game:GetService("Debris") local player = Players.LocalPlayer local playerGui = player:WaitForChild("PlayerGui") local CONFIG = { SwingForward = 42, SwingHeight = 52, SwingBoost = 30, SwingAcceleration = 60, SwingAssist = 16, SwingMaxSpeed = 145, RopeMinLength = 12, RopeMaxLength = 180, RopeReelSpeed = 26, ZipDistance = 90, ZipSpeed = 145, ZipDuration = 0.7, PullDistance = 100, PullSpeed = 110, PullDuration = 2.5, ClimbReach = 5.5, ClimbSpeed = 19, ClimbSprintSpeed = 29, SurfaceOffset = 2.3, SurfaceStickSpeed = 24, SurfaceGraceTime = 0.22, SlingDistance = 85, SlingChargeTime = 1.6, SlingMinSpeed = 95, SlingMaxSpeed = 180, SlingLift = 45, Cooldowns = { Swing = 0.12, Zip = 0.45, Pull = 0.6, Climb = 0.15, Sling = 6, }, } local COLORS = { Red = Color3.fromRGB(210, 35, 48), Blue = Color3.fromRGB(35, 115, 225), Dark = Color3.fromRGB(25, 28, 36), Active = Color3.fromRGB(30, 155, 110), White = Color3.fromRGB(245, 247, 255), } local previous = playerGui:FindFirstChild("SpiderUI") if previous then local oldShutdown = previous:FindFirstChild("Shutdown") if oldShutdown and oldShutdown:IsA("BindableEvent") then oldShutdown:Fire() end previous:Destroy() end local previousSling = playerGui:FindFirstChild("SpiderSlingshotSolo") if previousSling then previousSling:Destroy() end local gui = Instance.new("ScreenGui") gui.Name = "SpiderUI" gui.ResetOnSpawn = false gui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling gui.Parent = playerGui local shutdown = Instance.new("BindableEvent") shutdown.Name = "Shutdown" shutdown.Parent = gui local running = true local connections = {} local deathConnection local characterGeneration = 0 local character local humanoid local root local cooldowns = {} local keys = {} local actionHolds = {} local crawlTouches = {} local ropeTouches = {} local buttons = {} local notice = "" local noticeUntil = 0 local state = { mode = "Idle", objects = {}, } local stopMode local startSwing local startZip local startPull local toggleClimb local slingAction local leapOff local function connect(signal, callback) local connection = signal:Connect(callback) table.insert(connections, connection) return connection end local function alive() return running and character ~= nil and character.Parent ~= nil and root ~= nil and root.Parent ~= nil and humanoid ~= nil and humanoid.Health > 0 end local function unitOr(vector, fallback) if vector.Magnitude > 0.001 then return vector.Unit end return fallback end local function project(vector, normal) return vector - normal * vector:Dot(normal) end local function own(object) table.insert(state.objects, object) return object end local function tell(text) notice = text noticeUntil = os.clock() + 2.2 end local function ready(action) return os.clock() >= (cooldowns[action] or 0) end local function cooldown(action) cooldowns[action] = os.clock() + CONFIG.Cooldowns[action] end local function playSound(id, volume, speed) if not alive() then return end local sound = Instance.new("Sound") sound.SoundId = "rbxassetid://" .. tostring(id) sound.Volume = volume or 0.8 sound.PlaybackSpeed = speed or 1 sound.Parent = root sound:Play() Debris:AddItem(sound, 4) end local function cameraDirection() local camera = Workspace.CurrentCamera return camera and camera.CFrame.LookVector or root.CFrame.LookVector end local function isPointer(input) return input.UserInputType == Enum.UserInputType.Touch or input.UserInputType == Enum.UserInputType.MouseButton1 end local function makeButton(parent, name, text, color, size) local button = Instance.new("TextButton") button.Name = name button.Size = size or UDim2.fromOffset(70, 70) button.BackgroundColor3 = color button.TextColor3 = COLORS.White button.Text = text button.TextSize = 14 button.TextWrapped = true button.Font = Enum.Font.GothamBold button.AutoButtonColor = true button.Parent = parent local corner = Instance.new("UICorner") corner.CornerRadius = UDim.new(0, 18) corner.Parent = button local stroke = Instance.new("UIStroke") stroke.Color = COLORS.White stroke.Transparency = 0.72 stroke.Thickness = 1.5 stroke.Parent = button return button end local status = Instance.new("TextLabel") status.AnchorPoint = Vector2.new(0.5, 0) status.Position = UDim2.fromScale(0.5, 0.025) status.Size = UDim2.new(0.92, 0, 0, 46) status.BackgroundTransparency = 1 status.Font = Enum.Font.GothamBold status.TextSize = 14 status.TextWrapped = true status.TextColor3 = COLORS.White status.TextStrokeTransparency = 0.35 status.Text = "PROJECT SPIDER V3" status.Parent = gui local actionPanel = Instance.new("Frame") actionPanel.AnchorPoint = Vector2.new(1, 0.5) actionPanel.Position = UDim2.new(1, -14, 0.5, 0) actionPanel.Size = UDim2.fromOffset(76, 388) actionPanel.BackgroundTransparency = 1 actionPanel.Parent = gui local actionScale = Instance.new("UIScale") actionScale.Parent = actionPanel local actionLayout = Instance.new("UIListLayout") actionLayout.Padding = UDim.new(0, 8) actionLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center actionLayout.SortOrder = Enum.SortOrder.LayoutOrder actionLayout.Parent = actionPanel local definitions = { {"Swing", "SWING\n[Q]", COLORS.Red}, {"Zip", "ZIP\n[E]", COLORS.Blue}, {"Pull", "PULL\n[R]", COLORS.Dark}, {"Climb", "CRAWL\n[C]", COLORS.Red}, {"Sling", "SLING\n[F]", COLORS.Blue}, } for index, definition in ipairs(definitions) do local action, text, color = table.unpack(definition) local button = makeButton(actionPanel, action, text, color) button.LayoutOrder = index buttons[action] = { button = button, text = text, color = color, } end local crawlPanel = Instance.new("Frame") crawlPanel.AnchorPoint = Vector2.new(0.5, 1) crawlPanel.Position = UDim2.new(0.5, 0, 1, -20) crawlPanel.Size = UDim2.fromOffset(204, 136) crawlPanel.BackgroundTransparency = 1 crawlPanel.Visible = false crawlPanel.Parent = gui local crawlScale = Instance.new("UIScale") crawlScale.Parent = crawlPanel local function makeDirection(name, text, x, y) local button = makeButton( crawlPanel, name, text, COLORS.Dark, UDim2.fromOffset(62, 62) ) button.Position = UDim2.fromOffset(x, y) return button end local upButton = makeDirection("Up", "UP", 71, 0) local downButton = makeDirection("Down", "DOWN", 71, 72) local leftButton = makeDirection("Left", "LEFT", 0, 72) local rightButton = makeDirection("Right", "RIGHT", 142, 72) local leapButton = makeButton( gui, "LeapOff", "LEAP OFF [SPACE]", COLORS.Blue, UDim2.fromOffset(190, 42) ) leapButton.AnchorPoint = Vector2.new(0.5, 0) leapButton.Position = UDim2.new(0.5, 0, 0.025, 50) leapButton.Visible = false local ropePanel = Instance.new("Frame") ropePanel.AnchorPoint = Vector2.new(0.5, 1) ropePanel.Position = UDim2.new(0.5, 0, 1, -28) ropePanel.Size = UDim2.fromOffset(224, 54) ropePanel.BackgroundTransparency = 1 ropePanel.Visible = false ropePanel.Parent = gui local ropeScale = Instance.new("UIScale") ropeScale.Parent = ropePanel local reelButton = makeButton( ropePanel, "ReelIn", "REEL IN", COLORS.Blue, UDim2.fromOffset(106, 54) ) local extendButton = makeButton( ropePanel, "LetOut", "LET OUT", COLORS.Dark, UDim2.fromOffset(106, 54) ) extendButton.Position = UDim2.fromOffset(118, 0) -- Web abilities use invisible, non-colliding points in open air. local function makeAirAnchor(position) local part = own(Instance.new("Part")) part.Name = "SpiderAirAnchor" part.Size = Vector3.new(0.1, 0.1, 0.1) part.Anchored = true part.CanCollide = false part.CanTouch = false part.CanQuery = false part.CastShadow = false part.Transparency = 1 part.Position = position part.Parent = Workspace local attachment = own(Instance.new("Attachment")) attachment.Name = "WebAnchor" attachment.Parent = part return attachment end local function handPart(side) return character:FindFirstChild(side .. "Hand") or character:FindFirstChild(side .. " Arm") end local function makeHandWeb(side, target) local hand = handPart(side) if not hand then return end local attachment = own(Instance.new("Attachment")) attachment.Name = "Spider" .. side .. "Web" attachment.Position = Vector3.new(0, -hand.Size.Y * 0.45, 0) attachment.Parent = hand local beam = own(Instance.new("Beam")) beam.Name = "SpiderWeb" beam.Attachment0 = attachment beam.Attachment1 = target beam.Width0 = 0.085 beam.Width1 = 0.065 beam.FaceCamera = true beam.Segments = 12 beam.LightEmission = 0.35 beam.Color = ColorSequence.new(COLORS.White) beam.Parent = hand end local function startHandPose() state.pose = { targets = {}, shoulders = {}, } for _, side in ipairs({"Left", "Right"}) do local hand = handPart(side) local upperArm = character:FindFirstChild(side .. "UpperArm") if humanoid.RigType == Enum.HumanoidRigType.R15 and hand and upperArm then local target = own(Instance.new("Attachment")) target.Name = "Spider" .. side .. "Grip" target.Parent = root state.pose.targets[side] = target local ik = own(Instance.new("IKControl")) ik.Name = "Spider" .. side .. "ArmIK" ik.Type = Enum.IKControlType.Position ik.ChainRoot = upperArm ik.EndEffector = hand ik.Target = target ik.Weight = 1 ik.SmoothTime = 0.08 ik.Priority = 10 ik.Parent = humanoid else local torso = character:FindFirstChild("Torso") local shoulder = torso and torso:FindFirstChild(side .. " Shoulder") if shoulder and shoulder:IsA("Motor6D") then state.pose.shoulders[side] = { motor = shoulder, transform = shoulder.Transform, } end end end end local function updateHandPose() if not state.pose or not state.anchors then return end for _, side in ipairs({"Left", "Right"}) do local index = side == "Left" and 1 or 2 local anchor = state.anchors[index] or state.anchors[1] local target = state.pose.targets[side] if target and anchor and anchor.Parent then local shoulderPosition = root.Position + root.CFrame.UpVector * 1.25 local direction = unitOr( anchor.WorldPosition - shoulderPosition, root.CFrame.UpVector ) local separation = side == "Left" and -0.19 or 0.19 local grip = shoulderPosition + direction * 2 + root.CFrame.RightVector * separation target.Position = root.CFrame:PointToObjectSpace(grip) end end end local function attachWebs(firstPosition, secondPosition) local first = makeAirAnchor(firstPosition) local second = secondPosition and makeAirAnchor(secondPosition) or first state.anchors = {first, second} makeHandWeb("Left", first) makeHandWeb("Right", second) startHandPose() updateHandPose() end local function validAnchors() if not state.anchors then return false end for _, anchor in ipairs(state.anchors) do if not anchor:IsDescendantOf(Workspace) then return false end end return true end local function makePhysics(useVelocity) local attachment = own(Instance.new("Attachment")) attachment.Name = "SpiderMotion" attachment.Parent = root state.motionAttachment = attachment local orientation = own(Instance.new("AlignOrientation")) orientation.Mode = Enum.OrientationAlignmentMode.OneAttachment orientation.Attachment0 = attachment orientation.MaxTorque = 1000000 orientation.MaxAngularVelocity = 14 orientation.Responsiveness = 22 orientation.CFrame = root.CFrame.Rotation orientation.Parent = root state.orientation = orientation if useVelocity then local velocity = own(Instance.new("LinearVelocity")) velocity.Attachment0 = attachment velocity.RelativeTo = Enum.ActuatorRelativeTo.World velocity.VelocityConstraintMode = Enum.VelocityConstraintMode.Vector velocity.ForceLimitsEnabled = false velocity.VectorVelocity = Vector3.zero velocity.Parent = root state.velocity = velocity end end stopMode = function() local old = state if old.mode == "Idle" then return end state = { mode = "Idle", objects = {}, } if old.pose then for _, entry in pairs(old.pose.shoulders) do if entry.motor.Parent then entry.motor.Transform = entry.transform end end end for index = #old.objects, 1, -1 do old.objects[index]:Destroy() end if old.saved and old.saved.humanoid.Parent then local hum = old.saved.humanoid hum.AutoRotate = old.saved.autoRotate hum.PlatformStand = old.saved.platformStand if hum.Health > 0 and not hum.PlatformStand then hum:ChangeState(Enum.HumanoidStateType.Freefall) end end crawlPanel.Visible = false leapButton.Visible = false ropePanel.Visible = false table.clear(crawlTouches) table.clear(ropeTouches) if old.mode == "Swing" or old.mode == "Pull" or old.mode == "Climb" or old.mode == "Sling" then cooldown(old.mode) end end local function beginMode(mode, useVelocity) if not alive() then return false end stopMode() state.mode = mode state.started = os.clock() state.saved = { humanoid = humanoid, autoRotate = humanoid.AutoRotate, platformStand = humanoid.PlatformStand, } humanoid.AutoRotate = false humanoid.PlatformStand = true makePhysics(useVelocity) return true end local function faceDirection(direction, preferredUp) local forward = unitOr(direction, root.CFrame.LookVector) local up = preferredUp or Vector3.yAxis if math.abs(forward:Dot(up)) > 0.96 then up = root.CFrame.RightVector end if math.abs(forward:Dot(up)) > 0.96 then up = Vector3.zAxis end if math.abs(forward:Dot(up)) > 0.96 then up = Vector3.xAxis end state.orientation.CFrame = CFrame.lookAt( Vector3.zero, forward, up ) end local function freeSwingPosition() local look = cameraDirection() local fallback = unitOr( project(root.CFrame.LookVector, Vector3.yAxis), Vector3.new(0, 0, -1) ) local forward = unitOr( project(look, Vector3.yAxis), fallback ) return root.Position + forward * CONFIG.SwingForward + Vector3.yAxis * ( CONFIG.SwingHeight + math.max(look.Y, 0) * 28 ) end local function freeAimPosition(distance) return root.Position + Vector3.yAxis * 1.5 + unitOr(cameraDirection(), root.CFrame.LookVector) * distance end startSwing = function() if not alive() or not ready("Swing") then return end if state.mode == "Swing" then return end local position = freeSwingPosition() if not beginMode("Swing", false) then return end attachWebs(position) local rope = own(Instance.new("RopeConstraint")) rope.Attachment0 = state.motionAttachment rope.Attachment1 = state.anchors[1] rope.Length = math.clamp( (position - root.Position).Magnitude, CONFIG.RopeMinLength, CONFIG.RopeMaxLength ) rope.Visible = false rope.Restitution = 0 rope.Parent = root state.rope = rope local force = own(Instance.new("VectorForce")) force.Attachment0 = state.motionAttachment force.RelativeTo = Enum.ActuatorRelativeTo.World force.ApplyAtCenterOfMass = true force.Force = Vector3.zero force.Parent = root state.force = force local radial = unitOr(position - root.Position, Vector3.yAxis) local forward = unitOr( project(cameraDirection(), radial), unitOr(project(root.CFrame.LookVector, radial), Vector3.zero) ) root.AssemblyLinearVelocity += forward * CONFIG.SwingBoost ropePanel.Visible = true playSound(1284846268, 1) end startZip = function() if not alive() or not ready("Zip") then return end local position = freeAimPosition(CONFIG.ZipDistance) if not beginMode("Zip", true) then return end attachWebs(position) state.deadline = os.clock() + CONFIG.ZipDuration cooldown("Zip") playSound(9084017080, 1, 1.1) end startPull = function() if not alive() or not ready("Pull") then return end if state.mode == "Pull" then return end local position = freeAimPosition(CONFIG.PullDistance) if not beginMode("Pull", true) then return end attachWebs(position) state.deadline = os.clock() + CONFIG.PullDuration playSound(1284846268, 1, 0.95) end slingAction = function() if not alive() then return end if state.mode == "Sling" then if not validAnchors() then stopMode() return end local charge = math.clamp( (os.clock() - state.started) / CONFIG.SlingChargeTime, 0, 1 ) local middle = ( state.anchors[1].WorldPosition + state.anchors[2].WorldPosition ) * 0.5 local direction = unitOr( middle - root.Position, cameraDirection() ) local speed = CONFIG.SlingMinSpeed + (CONFIG.SlingMaxSpeed - CONFIG.SlingMinSpeed) * charge stopMode() root.AssemblyLinearVelocity = direction * speed + Vector3.yAxis * CONFIG.SlingLift playSound(9084017080, 1.1, 1.1) return end if not ready("Sling") then tell("Slingshot is recharging") return end local camera = Workspace.CurrentCamera if not camera then return end local middle = root.Position + Vector3.yAxis * 13 + camera.CFrame.LookVector * CONFIG.SlingDistance local first = middle - camera.CFrame.RightVector * 18 local second = middle + camera.CFrame.RightVector * 18 if not beginMode("Sling", true) then return end attachWebs(first, second) state.holdPosition = root.Position root.AssemblyLinearVelocity = Vector3.zero state.velocity.VectorVelocity = Vector3.zero playSound(1284846268, 1, 0.85) end -- Only surface crawling uses raycasts. local function surfaceRay(origin, direction) if not character then return nil end local params = RaycastParams.new() params.FilterType = Enum.RaycastFilterType.Exclude params.FilterDescendantsInstances = {character} params.IgnoreWater = true params.RespectCanCollide = true return Workspace:Raycast(origin, direction, params) end local function surfaceBasis(normal, preferred) local up = project(preferred, normal) if up.Magnitude < 0.1 then up = project(Vector3.yAxis, normal) end if up.Magnitude < 0.1 then up = project(Vector3.zAxis, normal) end if up.Magnitude < 0.1 then up = project(Vector3.xAxis, normal) end up = up.Unit local right = unitOr(up:Cross(normal), Vector3.xAxis) return up, right end local function initialSurface() local directions = { cameraDirection(), root.CFrame.LookVector, root.CFrame.UpVector, -root.CFrame.UpVector, root.CFrame.RightVector, -root.CFrame.RightVector, -root.CFrame.LookVector, } for _, direction in ipairs(directions) do local hit = surfaceRay( root.Position, direction * CONFIG.ClimbReach ) if hit then return hit end end return nil end toggleClimb = function() if state.mode == "Climb" then stopMode() return end if not alive() or not ready("Climb") then return end local hit = initialSurface() if not hit then tell("Move closer to a wall, floor, or ceiling to crawl") return end if not beginMode("Climb", true) then return end state.normal = hit.Normal state.surfaceUp = surfaceBasis(hit.Normal, root.CFrame.UpVector) state.bodyUp = state.surfaceUp state.lastSurface = os.clock() crawlPanel.Visible = true leapButton.Visible = true end local function crawlInput() local x, y = 0, 0 if keys[Enum.KeyCode.A] then x -= 1 end if keys[Enum.KeyCode.D] then x += 1 end if keys[Enum.KeyCode.W] then y += 1 end if keys[Enum.KeyCode.S] then y -= 1 end local directions = {} for _, direction in pairs(crawlTouches) do directions[direction] = true end if directions.Left then x -= 1 end if directions.Right then x += 1 end if directions.Up then y += 1 end if directions.Down then y -= 1 end local input = Vector2.new(x, y) return input.Magnitude > 1 and input.Unit or input end local function findClimbSurface(movement) local normal = state.normal local origin = root.Position if movement.Magnitude > 0.1 then local ahead = surfaceRay( origin, movement.Unit * (CONFIG.SurfaceOffset + 1) ) if ahead and ahead.Normal:Dot(normal) < 0.85 then return ahead end end local current = surfaceRay( origin, -normal * CONFIG.ClimbReach ) if current then return current end if movement.Magnitude > 0.1 then local direction = movement.Unit local forwardSurface = surfaceRay( origin + direction * 1.4, -normal * CONFIG.ClimbReach ) if forwardSurface then return forwardSurface end local around = surfaceRay( origin + direction * 2 - normal * (CONFIG.SurfaceOffset + 0.8), -direction * 3.5 ) if around then return around end end return nil end leapOff = function() if not alive() then return end if state.mode == "Climb" then local normal = state.normal local travel = state.bodyUp stopMode() root.AssemblyLinearVelocity = normal * 36 + travel * 22 + Vector3.yAxis * 22 elseif state.mode ~= "Idle" then stopMode() end end local function updateSwing(dt) local target = state.anchors[1].WorldPosition local radial = unitOr(target - root.Position, Vector3.yAxis) local velocity = root.AssemblyLinearVelocity local steering = project(humanoid.MoveDirection, radial) local cameraTangent = project(cameraDirection(), radial) local travelTangent = project(velocity, radial) local assistDirection if travelTangent.Magnitude > 8 then assistDirection = travelTangent.Unit else assistDirection = unitOr( cameraTangent, unitOr(project(root.CFrame.LookVector, radial), Vector3.zero) ) end local acceleration = steering * CONFIG.SwingAcceleration + assistDirection * CONFIG.SwingAssist state.force.Force = acceleration * root.AssemblyMass if velocity.Magnitude > CONFIG.SwingMaxSpeed then local excess = velocity.Magnitude - CONFIG.SwingMaxSpeed state.force.Force -= velocity.Unit * excess * root.AssemblyMass * 5 end local reelDirection = 0 if keys[Enum.KeyCode.Space] then reelDirection -= 1 end if keys[Enum.KeyCode.LeftShift] or keys[Enum.KeyCode.RightShift] then reelDirection += 1 end for _, direction in pairs(ropeTouches) do reelDirection += direction end state.rope.Length = math.clamp( state.rope.Length + math.clamp(reelDirection, -1, 1) * CONFIG.RopeReelSpeed * dt, CONFIG.RopeMinLength, CONFIG.RopeMaxLength ) local facing = travelTangent if facing.Magnitude < 2 then facing = cameraTangent end faceDirection(facing, radial) end local function updateTravel() local delta = state.anchors[1].WorldPosition - root.Position local distance = delta.Magnitude local isZip = state.mode == "Zip" local stopDistance = 5 if distance <= stopDistance or os.clock() >= state.deadline then if not isZip or distance <= stopDistance then root.AssemblyLinearVelocity *= 0.25 end stopMode() return end local direction = delta / distance local maxSpeed = isZip and CONFIG.ZipSpeed or CONFIG.PullSpeed local speed = math.min( maxSpeed, math.max(12, (distance - stopDistance) * 7) ) state.velocity.VectorVelocity = direction * speed faceDirection(direction) end local function updateClimb(dt) local input = crawlInput() local up, right = surfaceBasis(state.normal, state.surfaceUp) local movement = right * input.X + up * input.Y local hit = findClimbSurface(movement) if not hit then state.velocity.VectorVelocity = Vector3.zero if os.clock() - state.lastSurface > CONFIG.SurfaceGraceTime then stopMode() end return end state.lastSurface = os.clock() state.normal = hit.Normal state.surfaceUp, right = surfaceBasis(hit.Normal, state.surfaceUp) up = state.surfaceUp movement = right * input.X + up * input.Y local sprinting = keys[Enum.KeyCode.LeftShift] or keys[Enum.KeyCode.RightShift] local speed = sprinting and CONFIG.ClimbSprintSpeed or CONFIG.ClimbSpeed local gap = (root.Position - hit.Position):Dot(hit.Normal) local correction = math.clamp( (CONFIG.SurfaceOffset - gap) * 12, -CONFIG.SurfaceStickSpeed, CONFIG.SurfaceStickSpeed ) local surfaceVelocity = Vector3.zero if hit.Instance:IsA("BasePart") then surfaceVelocity = hit.Instance:GetVelocityAtPosition(hit.Position) end state.velocity.VectorVelocity = movement * speed + hit.Normal * correction + surfaceVelocity -- The head follows travel direction, including head-first descent. if movement.Magnitude > 0.05 then local desiredUp = movement.Unit local currentUp = unitOr( project(state.bodyUp, hit.Normal), up ) local angle = math.atan2( hit.Normal:Dot(currentUp:Cross(desiredUp)), math.clamp(currentUp:Dot(desiredUp), -1, 1) ) if math.abs(angle) > math.pi - 0.001 then angle = math.pi end state.bodyUp = CFrame.fromAxisAngle( hit.Normal, angle * math.min(dt * 10, 1) ):VectorToWorldSpace(currentUp) else state.bodyUp = unitOr( project(state.bodyUp, hit.Normal), up ) end faceDirection(-hit.Normal, state.bodyUp) end local function updateSling() local correction = (state.holdPosition - root.Position) * 12 if correction.Magnitude > 35 then correction = correction.Unit * 35 end state.velocity.VectorVelocity = correction local middle = ( state.anchors[1].WorldPosition + state.anchors[2].WorldPosition ) * 0.5 faceDirection(middle - root.Position) end local function performAction(action) if action == "Swing" then startSwing() elseif action == "Zip" then startZip() elseif action == "Pull" then startPull() elseif action == "Climb" then toggleClimb() elseif action == "Sling" then slingAction() end end local function actionStillHeld(action) for _, held in pairs(actionHolds) do if held == action then return true end end return false end local function releaseAction(action) if actionStillHeld(action) then return end if (action == "Swing" and state.mode == "Swing") or (action == "Pull" and state.mode == "Pull") then stopMode() end end for action, entry in pairs(buttons) do if action == "Swing" or action == "Pull" then connect(entry.button.InputBegan, function(input) if not isPointer(input) then return end actionHolds[input] = action performAction(action) end) else connect(entry.button.Activated, function() performAction(action) end) end end local function bindCrawlButton(button, direction) connect(button.InputBegan, function(input) if isPointer(input) and state.mode == "Climb" then crawlTouches[input] = direction end end) end bindCrawlButton(upButton, "Up") bindCrawlButton(downButton, "Down") bindCrawlButton(leftButton, "Left") bindCrawlButton(rightButton, "Right") local function bindRopeButton(button, direction) connect(button.InputBegan, function(input) if isPointer(input) and state.mode == "Swing" then ropeTouches[input] = direction end end) end bindRopeButton(reelButton, -1) bindRopeButton(extendButton, 1) connect(leapButton.Activated, leapOff) local keyActions = { [Enum.KeyCode.Q] = "Swing", [Enum.KeyCode.E] = "Zip", [Enum.KeyCode.R] = "Pull", [Enum.KeyCode.C] = "Climb", [Enum.KeyCode.F] = "Sling", } connect(UserInputService.InputBegan, function(input, processed) if processed or UserInputService:GetFocusedTextBox() then return end if input.UserInputType ~= Enum.UserInputType.Keyboard then return end keys[input.KeyCode] = true local action = keyActions[input.KeyCode] if action then if action == "Swing" or action == "Pull" then actionHolds[input.KeyCode] = action end performAction(action) elseif input.KeyCode == Enum.KeyCode.X then stopMode() end end) connect(UserInputService.InputEnded, function(input) keys[input.KeyCode] = nil local pointerAction = actionHolds[input] actionHolds[input] = nil if pointerAction then releaseAction(pointerAction) end local keyboardAction = actionHolds[input.KeyCode] actionHolds[input.KeyCode] = nil if keyboardAction then releaseAction(keyboardAction) end crawlTouches[input] = nil ropeTouches[input] = nil end) connect(UserInputService.JumpRequest, function() if UserInputService:GetFocusedTextBox() then return end -- Swing release is controlled by Q / the Swing button. if state.mode ~= "Swing" then leapOff() end end) local function clearInputs() table.clear(keys) table.clear(actionHolds) table.clear(crawlTouches) table.clear(ropeTouches) end connect(UserInputService.WindowFocusReleased, function() clearInputs() stopMode() end) connect(UserInputService.TextBoxFocused, function() clearInputs() stopMode() end) connect(RunService.PreSimulation, function() if not alive() or not state.pose then return end -- Apply the R6 pose after Animator has written its transforms. for side, entry in pairs(state.pose.shoulders) do if entry.motor.Parent then local spread = side == "Left" and -10 or 10 entry.motor.Transform = CFrame.Angles( math.rad(165), 0, math.rad(spread) ) end end end) connect(RunService.Heartbeat, function(dt) if not alive() then stopMode() return end if state.mode == "Idle" then return end if state.anchors and not validAnchors() then stopMode() return end if state.mode == "Swing" then updateSwing(dt) elseif state.mode == "Zip" or state.mode == "Pull" then updateTravel() elseif state.mode == "Climb" then updateClimb(dt) elseif state.mode == "Sling" then updateSling() end updateHandPose() end) connect(RunService.RenderStepped, function() local now = os.clock() local camera = Workspace.CurrentCamera if camera then local viewport = camera.ViewportSize local scale = math.clamp( math.min(viewport.Y / 720, viewport.X / 420), 0.6, 1 ) actionScale.Scale = scale crawlScale.Scale = scale ropeScale.Scale = scale end for action, entry in pairs(buttons) do local remaining = math.max( 0, (cooldowns[action] or 0) - now ) local active = state.mode == action entry.button.BackgroundColor3 = active and COLORS.Active or remaining > 0 and COLORS.Dark or entry.color if action == "Sling" and state.mode == "Sling" then local charge = math.clamp( (now - state.started) / CONFIG.SlingChargeTime, 0, 1 ) entry.button.Text = string.format( "FIRE\n%d%%", math.floor(charge * 100) ) elseif remaining > 0 and not active then entry.button.Text = string.format( "%s\n%.1f", action:upper(), remaining ) else entry.button.Text = entry.text end end if now < noticeUntil then status.Text = notice elseif state.mode == "Swing" then status.Text = "FREE SWING • Release to launch • Adjust rope below" elseif state.mode == "Climb" then status.Text = "CRAWL • WASD / arrows • SPACE to leap" elseif state.mode == "Sling" then status.Text = "CHARGING • Tap SLING / F again to launch" elseif state.mode == "Pull" then status.Text = "AIR WEB PULL • Release to detach" elseif state.mode == "Zip" then status.Text = "AIR WEB ZIP" else status.Text = "PROJECT SPIDER V3 • FREE SWING" end end) local function bindCharacter(newCharacter) characterGeneration += 1 local generation = characterGeneration stopMode() clearInputs() table.clear(cooldowns) if deathConnection then deathConnection:Disconnect() deathConnection = nil end character, humanoid, root = nil, nil, nil local newHumanoid = newCharacter:WaitForChild("Humanoid", 10) local newRoot = newCharacter:WaitForChild("HumanoidRootPart", 10) if not running or generation ~= characterGeneration or player.Character ~= newCharacter or not newHumanoid or not newRoot then return end character = newCharacter humanoid = newHumanoid root = newRoot deathConnection = humanoid.Died:Connect(function() stopMode() clearInputs() end) end connect(player.CharacterAdded, bindCharacter) connect(player.CharacterRemoving, function(removingCharacter) if removingCharacter == character then characterGeneration += 1 stopMode() clearInputs() character, humanoid, root = nil, nil, nil end end) local function cleanup() if not running then return end running = false characterGeneration += 1 stopMode() clearInputs() if deathConnection then deathConnection:Disconnect() deathConnection = nil end for _, connection in ipairs(connections) do connection:Disconnect() end table.clear(connections) end connect(shutdown.Event, function() cleanup() gui:Destroy() end) connect(gui.Destroying, cleanup) connect(script.Destroying, function() cleanup() gui:Destroy() end) if player.Character then task.spawn(bindCharacter, player.Character) end