EggsPlants

Lockpick Script

Jul 27th, 2025
305
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 10.64 KB | Software | 0 0
  1. --// Services
  2. local RunService = game:GetService("RunService")
  3. local UserInputService = game:GetService("UserInputService")
  4. local ReplicatedStorage = game:GetService("ReplicatedStorage")
  5. local ContextActionService = game:GetService("ContextActionService")
  6.  
  7. --// Player and Camera
  8. local Player = game.Players.LocalPlayer
  9. local CurrentCamera = workspace.CurrentCamera
  10. local Mouse = Player:GetMouse()
  11.  
  12.  
  13. --// Configuration Constants
  14. local MAX_FAILURES = 5                -- Number of wrong attempts allowed
  15. local SUCCESS_ROTATION_TICKS = 40     -- Number of frames needed to succeed
  16. local MAX_TOLERANCE = 1               -- Maximum tolerance (easy)
  17. local MIN_TOLERANCE = 0.1             -- Minimum tolerance (hard)
  18. local ACCEPTANCE_RANGE = 3            -- Angle in degrees for max tolerance
  19. local FAILURE_THRESHOLD = 45          -- Angle in degrees for min tolerance
  20. local FREEZE_ACTION = "FreezeMovement"
  21.  
  22. --// Runtime State
  23. local IsLockpicking = false
  24. local IsMouseHeld = false
  25. local RenderConnection
  26.  
  27.  
  28. --// Input Handling
  29. UserInputService.InputBegan:Connect(function(Input,GameProcessedEvent)
  30.     if GameProcessedEvent or Input.UserInputType ~= Enum.UserInputType.MouseButton1 then return end
  31.     IsMouseHeld = true
  32. end)
  33.  
  34. UserInputService.InputEnded:Connect(function(Input,GameProcessedEvent)
  35.     if GameProcessedEvent or Input.UserInputType ~= Enum.UserInputType.MouseButton1 then return end
  36.     IsMouseHeld = false
  37. end)
  38.  
  39. --[[
  40.     Note for the code above:
  41.     Checks for either the beggining, or end of an input,
  42.     it'll check if its processed within UI or is not of input type M1,
  43.     if not it'll set IsMouseHeld to true if beggining, false when ending.
  44. ]]--
  45.  
  46. local function GetMouseScreenAngle()
  47.     local Position = Vector2.new(Mouse.X,Mouse.Y + 10) -- Get Mouse Pos. With a Y offset of 10, check footnotes.
  48.     local Size = Vector2.new(Mouse.ViewSizeX,Mouse.ViewSizeY) -- Get ScreenSize
  49.     local Normal = (Position / Size) - Vector2.new(0.5,0.5) -- Normalize the mouse position in the middle of the screen
  50.     local Atan2 = -(math.atan2(Normal.Y, Normal.X)) --Get the angle  in radians
  51.     return Atan2
  52. end
  53.  
  54. --[[
  55.     Note for the code above:
  56.     This'll get the angle of the mouse relative to the center of the screen.
  57.     Mouse Y is offset by 10 to compensate for the lockpick being slightly higher than the center.
  58.     Mouse.X and Mouse.Y both start at the topleft of the screen, just below the top bar.
  59.     This grabs the position of the mouse, then normalizes it to be based on the center of the screen,
  60.     then converts the 2d position into an angle in radians.
  61. --]]
  62.  
  63. local function GetDegrees360(Radian)
  64.     local Degrees = math.deg(Radian)
  65.     return Degrees < 0 and Degrees + 360 or Degrees -- If degrees < 0 then It'll return degrees + 360, else it'll return degrees.
  66. end
  67.  
  68. --[[
  69.     Note for the code above:
  70.     For ease of implementation in certain aspects of the code, the ability to convert radians into
  71.     360 degrees is necessary as math.deg only returns 180 to -180. To workaround this,
  72.     if the angle is negative, it'll add 360 to it, and if it's positive, it'll return the same value.
  73. --]]
  74.  
  75. local function GetAngleTolerance(AngleDifference)
  76.     if AngleDifference <= ACCEPTANCE_RANGE then
  77.         return MAX_TOLERANCE
  78.     elseif AngleDifference >= FAILURE_THRESHOLD then
  79.         return MIN_TOLERANCE
  80.     else
  81.         local Tolerance = (AngleDifference - ACCEPTANCE_RANGE) / (FAILURE_THRESHOLD - ACCEPTANCE_RANGE)
  82.         return math.clamp(1 - Tolerance, MIN_TOLERANCE, MAX_TOLERANCE)
  83.     end
  84. end
  85.  
  86. --[[
  87.     Note for the code above:
  88.     "Tolerance" in this case, means the amount of movement that'll be tolerated before the lockpick
  89.     fails. AngleDifference is the difference between the target angle, and the lockpicks angle.
  90.     If the AngleDifference is less than or equal to the Acceptance range, then the Tolerance is
  91.     at the maximum, allowing for a succesful unlock. If the AngleDifference is greater than or equal
  92.     to the Failure threshold, then the Tolerance is at the minimum, meaning that any minor movement will
  93.     result in a failed attempt. Tolerance is normalized, then clamped within MIN_TOLERANCE or MAX_TOLERANCE
  94. --]]
  95.  
  96. local function CleanLockPick(LockPickVM)
  97.     IsLockpicking = false
  98.     ContextActionService:UnbindAction(FREEZE_ACTION)
  99.     CurrentCamera.CameraType = Enum.CameraType.Custom
  100.     if RenderConnection then RenderConnection:Disconnect() end
  101.     if LockPickVM then LockPickVM:Destroy() end
  102. end
  103.  
  104. --[[
  105.     Note for the code above:
  106.     This function is used to CleanLockPick the lockpick system.
  107.     It's called when the lockpick is cancelled, failed, or when the lockpick is completed.
  108.     This'll unfreeze the player and camera, disconnect the render connection,
  109.     and destroy the lockpick viewmodel.
  110. --]]
  111.  
  112. local function TriggerLockViewModel(LockPickVM,BasePickOffset,BaseLockOffset)
  113.     --// Initialized variables.
  114.     local Lock = LockPickVM.Lock
  115.     local Pick = Lock.Pick
  116.    
  117.     if not IsLockpicking then
  118.         CleanLockPick(LockPickVM)
  119.         return
  120.     end
  121.     -- This'll clean the lockpick if the player stops lockpicking. ( Toggleable Bool )
  122.    
  123.     if RenderConnection then RenderConnection:Disconnect() end
  124.     -- Disconnect any previous render connection to prevent any potential memory leaks.
  125.    
  126.     ContextActionService:BindAction(FREEZE_ACTION, function() return Enum.ContextActionResult.Sink end,false, unpack(Enum.PlayerActions:GetEnumItems()) )
  127.     -- This'll disable all player actions, and sink them, meaning they won't affect the player or camera.
  128.    
  129.     CurrentCamera.CameraType = Enum.CameraType.Scriptable
  130.     -- Disable any camera movement.
  131.  
  132.     local TARGET_ANGLE = math.random(0,360)
  133.     --[[
  134.         Note for the code above:
  135.         Find a Target angle, this code is run once everytime a player opens the lockpick.
  136.         The target angle is gonna be randomised, and the player will have to rotate the lockpick
  137.         to find the target angle.
  138.     --]]
  139.    
  140.     LockPickVM:PivotTo(CurrentCamera.CFrame)
  141.     local originalLockPivot = Lock:GetPivot()
  142.     -- Pivot to the camera, and obtain the Original locks pivot, before any movement.
  143.     local SuccessFrames  = 0
  144.     local FailedAttempts  = 0
  145.  
  146.     RenderConnection = RunService.RenderStepped:Connect(function() 
  147.         --// Update Offsets
  148.         BasePickOffset = LockPickVM:GetPivot():ToObjectSpace(Pick:GetPivot())
  149.         LockPickVM:PivotTo(CurrentCamera.CFrame)
  150.        
  151.         --// Rotation based on mouse
  152.         local PickAngle = GetMouseScreenAngle() -- Gets the mouse angle
  153.         local PickOffsetRotated = CFrame.new(BasePickOffset.Position) * CFrame.Angles(0,0,`PickAngle`)
  154.         Pick:PivotTo(CurrentCamera.CFrame * PickOffsetRotated)
  155.         --[[
  156.             Note for the code above:
  157.             This'll rotate the lockpick based on the mouse position.
  158.             We get the mouse angle, and then change the pivot the lockpick to the original pivot, but
  159.             with the given PickAngle derived from the mouse.
  160.         --]]
  161.        
  162.         --// Guard clause seperating IsMouseHeld == false and IsMouseHeld == true
  163.         if not IsMouseHeld then
  164.             Lock:PivotTo(originalLockPivot)
  165.             SuccessFrames = 0
  166.             FailedAttempts = 0
  167.             return
  168.         end
  169.         --[[
  170.             Note for the code above:
  171.             If the player isn't holding the mouse 1 button ( Default ) , this'll
  172.             reset the lockpicks position. And resetting the failed attempts and success frames to 0.
  173.         --]]
  174.        
  175.         --// If M1 is held down, the following code runs.
  176.        
  177.         Lock:PivotTo(Lock:GetPivot() * CFrame.Angles(0,0,0.025))   
  178.         SuccessFrames += 1
  179.         -- Rotate the lock by a slight tick of 0.025, and add 1 success frame/tick.
  180.        
  181.         local AngleDifference = math.abs(TARGET_ANGLE - GetDegrees360(PickAngle))
  182.         -- Difference between the target angle, and the picks current angle.
  183.        
  184.         local Tolerance = GetAngleTolerance(AngleDifference)
  185.         -- This'll calculate the amount of tolerance based on the angle difference.
  186.        
  187.         --// IF FAILED TOO MUCH:
  188.         if FailedAttempts >= MAX_FAILURES then
  189.             script.Fail:Play()
  190.             CleanLockPick(LockPickVM)
  191.             return
  192.         end
  193.         -- If the failed attempts exceed the allowed amount, play the fail sound, and clean the lockpick.
  194.        
  195.         --// IF SUCCESS:
  196.         if SuccessFrames >= SUCCESS_ROTATION_TICKS then
  197.             if RenderConnection then RenderConnection:Disconnect() end -- End the connection.
  198.             IsLockpicking = false -- Toggle the lockpick off, indicating the lockpick is no longer in use.
  199.             script.Win:Play() -- Happy sound
  200.            
  201.             --// I made the lockpick drop here, but you can signify its done however you want.
  202.             task.wait(1)
  203.             for _,Children in LockPickVM:GetDescendants() do
  204.                 if not Children:IsA("BasePart") then continue end
  205.                 Children.Anchored = false
  206.             end
  207.             task.wait(0.5)
  208.            
  209.             CleanLockPick(LockPickVM) -- Clean everything up.
  210.             return
  211.         end
  212.        
  213.         --// IF WITHIN THE TARGET THRESHOLD ANGLE:
  214.         if SuccessFrames >= (Tolerance * SUCCESS_ROTATION_TICKS ) then -- ( 0.1 * 40 = 4, 1 * 40 = 40)
  215.             if not script.Attempt.IsPlaying then script.Attempt:Play() end
  216.             SuccessFrames = 0
  217.             FailedAttempts += 1
  218.             Lock:PivotTo(originalLockPivot)
  219.             return
  220.         end
  221.         --[[
  222.             Note for the code above:
  223.             The tolerance will increase the closer to the target angle, within the thresholds.
  224.            
  225.             If the success frames exceed the calculated tolerance, it'll reset the lockpick,
  226.             and add 1 to the failed attempts.
  227.            
  228.             If the Tolerance is 1, and the SuccessFrames is equal to or exceed the required
  229.             amount of ticks to succeed. The if statement above this, will trigger first. Initiating a
  230.             "Win" or a succesful lockpick attempt.
  231.         --]]
  232.     end)
  233. end
  234.  
  235. local function TriggerLock(ForceClose)
  236.     local LockPickVM = CurrentCamera:FindFirstChild("LockpickViewModel") or game.ReplicatedStorage.LockpickViewModel:Clone()
  237.     local Lock = LockPickVM.Lock
  238.     local Pick = Lock.Pick
  239.     -- Initializes the existence of a lock.
  240.    
  241.     local BasePickOffset = LockPickVM:GetPivot():ToObjectSpace(Pick:GetPivot())
  242.     local BaseLockOffset = LockPickVM:GetPivot():ToObjectSpace(Lock:GetPivot())
  243.     -- Get the base offset of the lock and the pick, for later use.
  244.  
  245.     if ForceClose then
  246.         IsLockpicking = false
  247.         CleanLockPick(LockPickVM)
  248.     else
  249.         LockPickVM.Parent = CurrentCamera
  250.         IsLockpicking = not IsLockpicking
  251.         TriggerLockViewModel(LockPickVM,BasePickOffset,BaseLockOffset)
  252.     end
  253.     --[[
  254.         Note for the code above:
  255.        
  256.         If ForceClosed is triggered, it'll toggle off the IsLockpicking state and clean the lockpick.
  257.         Else, it'll trigger the lockpick viewmodel, and set the IsLockpicking state to true.
  258.        
  259.     --]]
  260.    
  261. end
  262.  
  263. -- When a player interacts with a prompt, this'll trigger.
  264. ReplicatedStorage.Events.Interact.OnClientEvent:Connect(function(Interactable,ForceClose)
  265.     TriggerLock(ForceClose)
  266.     --[[
  267.         Note for the code above:
  268.        
  269.         ForceClosed is used, so if the player walks away from the prompt, or is pushed away.
  270.         It'll close the prompt automatically. This is used to prevent false toggles.
  271.         You can use this for your own custom logic, I just use the prompt system I premade.
  272.        
  273.     --]]
  274. end)
  275.  
Tags: Roblox
Advertisement
Add Comment
Please, Sign In to add comment