Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- -- grab core services
- local Players = game:GetService("Players") -- need this to get the player
- local RunService = game:GetService("RunService") -- need this for smooth movement every frame
- local UserInputService = game:GetService("UserInputService") -- need this to detect mouse clicks
- local TweenService = game:GetService("TweenService") -- need this for smooth cooldown animation
- local StarterGui = game:GetService("StarterGui") -- need this to control core gui
- local ContextActionService = game:GetService("ContextActionService") -- got this but not using it
- -- get basic refs
- local player = Players.LocalPlayer -- the person playing the game
- local camera = workspace.CurrentCamera -- need camera to shoot rays from mouse position
- -- wait till we got a char
- local character = player.Character or player.CharacterAdded:Wait() -- might not have spawned yet so wait
- local humanoid = character:WaitForChild("Humanoid") -- controls player movement and health
- local rootPart = character:WaitForChild("HumanoidRootPart") -- the main part we move around
- -- simple state thing to track what we're doing
- local state = {} -- using a table to track if player can do actions
- state.__index = state -- makes the table work like an object
- state.current = "Idle" -- start in idle so player can do stuff
- function state:set(newState) -- changes what the player is doing
- self.current = newState -- updates the state
- end -- done with function
- function state:is(val) -- checks if we're in a certain state
- return self.current == val -- returns true or false
- end -- done with function
- -- basic config stuff
- local GRAPPLE_SPEED = 120 -- studs per second for grapple movement
- local CLIMB_SPEED = 1 -- studs per frame for climbing up walls
- local CLIMB_DURATION = 1.5 -- seconds to climb before stopping
- local WALL_CHECK_DISTANCE = 5 -- studs to check if wall is close enough
- local COOLDOWN_TIME = 1.2 -- seconds before player can use abilities again
- -- visuals and state flags
- local grapplePoint = nil -- where the player is grappling to
- local isGrappling = false -- prevents multiple grapples at once
- local isClimbing = false -- prevents multiple climbs at once
- local beam = nil -- the rope visual effect
- local attachments = {} -- holds the beam connection points
- -- ui setup for cooldown bar
- local ui = Instance.new("ScreenGui") -- container for all ui elements
- ui.Name = "GrappleUI" -- name it so we can find it later
- ui.ResetOnSpawn = false -- keep ui when player dies
- ui.Parent = player:WaitForChild("PlayerGui") -- put it where player can see it
- local cooldownBar = Instance.new("Frame") -- background of cooldown timer
- cooldownBar.Size = UDim2.new(0, 100, 0, 6) -- small horizontal bar
- cooldownBar.Position = UDim2.new(0.5, -50, 0.95, 0) -- bottom center of screen
- cooldownBar.BackgroundColor3 = Color3.fromRGB(60, 180, 255) -- blue color
- cooldownBar.BorderSizePixel = 0 -- no ugly border
- cooldownBar.Visible = false -- hide until cooldown starts
- cooldownBar.Parent = ui -- add to screen
- local cooldownFill = Instance.new("Frame") -- white part that shrinks
- cooldownFill.Size = UDim2.new(1, 0, 1, 0) -- fills whole bar at start
- cooldownFill.BackgroundColor3 = Color3.fromRGB(255, 255, 255) -- white color
- cooldownFill.BorderSizePixel = 0 -- no border
- cooldownFill.Parent = cooldownBar -- put inside cooldown bar
- -- buttons for mobile users
- if UserInputService.TouchEnabled then -- only make buttons on phones/tablets
- local grappleButton = Instance.new("TextButton") -- button to grapple
- grappleButton.Size = UDim2.new(0, 100, 0, 40) -- decent size for fingers
- grappleButton.Position = UDim2.new(1, -110, 1, -100) -- bottom right corner
- grappleButton.Text = "Grapple" -- what it says
- grappleButton.BackgroundColor3 = Color3.fromRGB(70, 70, 255) -- blue like grapple
- grappleButton.TextColor3 = Color3.new(1, 1, 1) -- white text
- grappleButton.Font = Enum.Font.SourceSansBold -- readable font
- grappleButton.TextSize = 20 -- big enough to read
- grappleButton.Parent = ui -- add to screen
- local climbButton = Instance.new("TextButton") -- button to climb
- climbButton.Size = UDim2.new(0, 100, 0, 40) -- same size as grapple
- climbButton.Position = UDim2.new(1, -110, 1, -150) -- above grapple button
- climbButton.Text = "Climb" -- what it says
- climbButton.BackgroundColor3 = Color3.fromRGB(70, 255, 70) -- green for climb
- climbButton.TextColor3 = Color3.new(1, 1, 1) -- white text
- climbButton.Font = Enum.Font.SourceSansBold -- same font
- climbButton.TextSize = 20 -- same size
- climbButton.Parent = ui -- add to screen
- -- hook up buttons to actions
- grappleButton.MouseButton1Click:Connect(function() -- when button pressed
- fireGrapple() -- do the grapple
- end) -- done connecting
- climbButton.MouseButton1Click:Connect(function() -- when button pressed
- startClimbing() -- do the climb
- end) -- done connecting
- end -- done with mobile buttons
- -- does the cooldown bar anim
- local function showCooldown(duration) -- shows timer after ability use
- cooldownBar.Visible = true -- show the bar
- cooldownFill.Size = UDim2.new(1, 0, 1, 0) -- reset to full size
- local tween = TweenService:Create(cooldownFill, TweenInfo.new(duration), { -- animate shrinking
- Size = UDim2.new(0, 0, 1, 0) -- shrink to nothing
- }) -- tween setup done
- tween:Play() -- start animation
- tween.Completed:Wait() -- wait until done
- cooldownBar.Visible = false -- hide when cooldown over
- end -- done with function
- -- draws the beam from player to point
- local function setupGrappleVisual() -- creates the rope effect
- local a1 = Instance.new("Attachment") -- point on player
- a1.Name = "GrappleStart" -- name for debugging
- a1.Position = Vector3.zero -- center of player part
- a1.Parent = rootPart -- attach to player
- local a2 = Instance.new("Attachment") -- point at target
- a2.Name = "GrappleEnd" -- name for debugging
- a2.Parent = workspace.Terrain -- put in world
- local b = Instance.new("Beam") -- the rope visual
- b.Attachment0 = a1 -- connect from player
- b.Attachment1 = a2 -- connect to target
- b.Width0 = 0.1 -- thin rope
- b.Width1 = 0.1 -- same width whole way
- b.Color = ColorSequence.new(Color3.fromRGB(200, 200, 255)) -- light blue rope
- b.LightEmission = 1 -- make it glow
- b.FaceCamera = true -- always face player view
- b.Parent = a1 -- attach to start point
- attachments.start = a1 -- save for later
- attachments.end_ = a2 -- save for later
- beam = b -- save beam reference
- end -- done with function
- -- kills the visual
- local function removeGrappleVisual() -- cleans up rope when done
- if beam then beam:Destroy() end -- remove rope if exists
- if attachments.start then attachments.start:Destroy() end -- remove start point if exists
- if attachments.end_ then attachments.end_:Destroy() end -- remove end point if exists
- attachments = {} -- clear saved references
- beam = nil -- clear beam reference
- end -- done with function
- -- checks if there's a wall in front
- local function isNearWall() -- sees if player can climb
- local rayOrigin = rootPart.Position -- start from player center
- local rayDirection = rootPart.CFrame.LookVector * WALL_CHECK_DISTANCE -- check forward direction
- local raycastParams = RaycastParams.new() -- setup ray settings
- raycastParams.FilterDescendantsInstances = {character} -- dont hit yourself
- raycastParams.FilterType = Enum.RaycastFilterType.Exclude -- ignore listed stuff
- local result = workspace:Raycast(rayOrigin, rayDirection, raycastParams) -- shoot the ray
- if result and result.Instance and not result.Instance:IsDescendantOf(character) then -- hit something that isnt player
- return true, result.Position -- yes theres a wall and heres where
- end -- done checking
- return false -- no wall found
- end -- done with function
- -- handles grapple move step
- local function moveTo(position) -- moves player to grapple point
- local direction = (position - rootPart.Position) -- vector from player to target
- local distance = direction.Magnitude -- how far to travel
- local duration = distance / GRAPPLE_SPEED -- time it should take
- local t0 = tick() -- remember start time
- state:set("Grappling") -- player is busy grappling
- isGrappling = true -- flag to prevent other actions
- RunService:BindToRenderStep("GrappleStep", Enum.RenderPriority.Character.Value + 1, function() -- run every frame
- local dt = tick() - t0 -- how long since start
- if dt >= duration or not grapplePoint then -- reached target or cancelled
- RunService:UnbindFromRenderStep("GrappleStep") -- stop moving
- isGrappling = false -- not grappling anymore
- state:set("Cooldown") -- start cooldown period
- task.delay(COOLDOWN_TIME, function() -- after cooldown time
- if not isClimbing then -- unless climbing started
- state:set("Idle") -- go back to idle
- end -- done checking
- end) -- done with delay
- showCooldown(COOLDOWN_TIME) -- show cooldown visual
- removeGrappleVisual() -- remove rope
- return -- exit this frame
- end -- done checking if finished
- local step = rootPart.CFrame.Position:Lerp(grapplePoint, math.clamp(dt / duration, 0, 1)) -- smooth position between start and end
- local look = (grapplePoint - rootPart.Position).Unit -- direction to look
- rootPart.CFrame = CFrame.new(step, step + look) -- move and rotate player
- if attachments.end_ then -- if rope end exists
- attachments.end_.WorldPosition = grapplePoint -- keep rope connected to target
- end -- done updating rope
- end) -- done with frame function
- end -- done with function
- -- shoot ray from mouse, see what we hit
- function fireGrapple() -- main grapple function
- if isGrappling or isClimbing or state.current ~= "Idle" then return end -- cant grapple if busy
- local mousePos = UserInputService:GetMouseLocation() -- where mouse is on screen
- local unitRay = camera:ViewportPointToRay(mousePos.X, mousePos.Y) -- ray from camera through mouse
- local rayOrigin = unitRay.Origin -- where ray starts
- local rayDirection = unitRay.Direction * 1000 -- ray direction times big number for range
- local raycastParams = RaycastParams.new() -- setup ray settings
- raycastParams.FilterDescendantsInstances = {character} -- dont hit yourself
- raycastParams.FilterType = Enum.RaycastFilterType.Exclude -- ignore listed stuff
- local result = workspace:Raycast(rayOrigin, rayDirection, raycastParams) -- shoot ray into world
- if not result or not result.Instance then return end -- didnt hit anything so stop
- grapplePoint = result.Position -- save where we hit
- setupGrappleVisual() -- make the rope
- moveTo(result.Position) -- start moving there
- end -- done with function
- -- starts climbing if near wall
- function startClimbing() -- main climb function
- if isClimbing or isGrappling or state.current ~= "Idle" then return end -- cant climb if busy
- local nearWall, wallPos = isNearWall() -- check if wall nearby
- if not nearWall then return end -- no wall so stop
- state:set("Climbing") -- player is busy climbing
- isClimbing = true -- flag to prevent other actions
- local climbStart = tick() -- remember when started
- humanoid.PlatformStand = true -- stops normal physics so we control movement
- rootPart.Velocity = Vector3.new(0, 0, 0) -- stop any existing movement
- RunService:BindToRenderStep("ClimbStep", Enum.RenderPriority.Character.Value + 2, function() -- run every frame
- local dt = tick() - climbStart -- how long climbing
- if dt > CLIMB_DURATION then -- climbed long enough
- RunService:UnbindFromRenderStep("ClimbStep") -- stop climbing movement
- humanoid.PlatformStand = false -- return to normal physics
- isClimbing = false -- not climbing anymore
- state:set("Cooldown") -- start cooldown period
- task.delay(COOLDOWN_TIME, function() -- after cooldown time
- if not isGrappling then -- unless grappling started
- state:set("Idle") -- go back to idle
- end -- done checking
- end) -- done with delay
- showCooldown(COOLDOWN_TIME) -- show cooldown visual
- return -- exit this frame
- end -- done checking if finished
- local climbSpeed = CLIMB_SPEED * dt -- how much to move up based on time
- local newPos = rootPart.Position + Vector3.new(0, climbSpeed, 0) -- add upward movement
- rootPart.CFrame = CFrame.new(newPos, newPos + rootPart.CFrame.LookVector) -- move up and keep facing same way
- end) -- done with frame function
- end -- done with function
- -- right click = grapple, left = climb
- UserInputService.InputBegan:Connect(function(input, processed) -- when any input happens
- if processed then return end -- game already handled it so ignore
- if input.UserInputType == Enum.UserInputType.MouseButton2 then -- right mouse button
- fireGrapple() -- do grapple
- elseif input.UserInputType == Enum.UserInputType.MouseButton1 then -- left mouse button
- startClimbing() -- do climb
- end -- done checking input
- end) -- done connecting input
- -- reset stuff on death
- humanoid.Died:Connect(function() -- when player dies
- state:set("Idle") -- reset state so respawn works right
- isClimbing = false -- clear climb flag
- isGrappling = false -- clear grapple flag
- removeGrappleVisual() -- clean up any rope
- end) -- done connecting death
- -- rebind stuff when respawn
- player.CharacterAdded:Connect(function(char) -- when player respawns
- character = char -- update character reference
- humanoid = char:WaitForChild("Humanoid") -- get new humanoid
- rootPart = char:WaitForChild("HumanoidRootPart") -- get new root part
- state:set("Idle") -- start in idle state
- end) -- done connecting respawn
- -- keep backpack gui on
- StarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.Backpack, true) -- makes sure inventory stays visible
Advertisement
Add Comment
Please, Sign In to add comment