Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- --// Services
- local RunService = game:GetService("RunService")
- local UserInputService = game:GetService("UserInputService")
- local ReplicatedStorage = game:GetService("ReplicatedStorage")
- local ContextActionService = game:GetService("ContextActionService")
- --// Player and Camera
- local Player = game.Players.LocalPlayer
- local CurrentCamera = workspace.CurrentCamera
- local Mouse = Player:GetMouse()
- --// Configuration Constants
- local MAX_FAILURES = 5 -- Number of wrong attempts allowed
- local SUCCESS_ROTATION_TICKS = 40 -- Number of frames needed to succeed
- local MAX_TOLERANCE = 1 -- Maximum tolerance (easy)
- local MIN_TOLERANCE = 0.1 -- Minimum tolerance (hard)
- local ACCEPTANCE_RANGE = 3 -- Angle in degrees for max tolerance
- local FAILURE_THRESHOLD = 45 -- Angle in degrees for min tolerance
- local FREEZE_ACTION = "FreezeMovement"
- --// Runtime State
- local IsLockpicking = false
- local IsMouseHeld = false
- local RenderConnection
- --// Input Handling
- UserInputService.InputBegan:Connect(function(Input,GameProcessedEvent)
- if GameProcessedEvent or Input.UserInputType ~= Enum.UserInputType.MouseButton1 then return end
- IsMouseHeld = true
- end)
- UserInputService.InputEnded:Connect(function(Input,GameProcessedEvent)
- if GameProcessedEvent or Input.UserInputType ~= Enum.UserInputType.MouseButton1 then return end
- IsMouseHeld = false
- end)
- --[[
- Note for the code above:
- Checks for either the beggining, or end of an input,
- it'll check if its processed within UI or is not of input type M1,
- if not it'll set IsMouseHeld to true if beggining, false when ending.
- ]]--
- local function GetMouseScreenAngle()
- local Position = Vector2.new(Mouse.X,Mouse.Y + 10) -- Get Mouse Pos. With a Y offset of 10, check footnotes.
- local Size = Vector2.new(Mouse.ViewSizeX,Mouse.ViewSizeY) -- Get ScreenSize
- local Normal = (Position / Size) - Vector2.new(0.5,0.5) -- Normalize the mouse position in the middle of the screen
- local Atan2 = -(math.atan2(Normal.Y, Normal.X)) --Get the angle in radians
- return Atan2
- end
- --[[
- Note for the code above:
- This'll get the angle of the mouse relative to the center of the screen.
- Mouse Y is offset by 10 to compensate for the lockpick being slightly higher than the center.
- Mouse.X and Mouse.Y both start at the topleft of the screen, just below the top bar.
- This grabs the position of the mouse, then normalizes it to be based on the center of the screen,
- then converts the 2d position into an angle in radians.
- --]]
- local function GetDegrees360(Radian)
- local Degrees = math.deg(Radian)
- return Degrees < 0 and Degrees + 360 or Degrees -- If degrees < 0 then It'll return degrees + 360, else it'll return degrees.
- end
- --[[
- Note for the code above:
- For ease of implementation in certain aspects of the code, the ability to convert radians into
- 360 degrees is necessary as math.deg only returns 180 to -180. To workaround this,
- if the angle is negative, it'll add 360 to it, and if it's positive, it'll return the same value.
- --]]
- local function GetAngleTolerance(AngleDifference)
- if AngleDifference <= ACCEPTANCE_RANGE then
- return MAX_TOLERANCE
- elseif AngleDifference >= FAILURE_THRESHOLD then
- return MIN_TOLERANCE
- else
- local Tolerance = (AngleDifference - ACCEPTANCE_RANGE) / (FAILURE_THRESHOLD - ACCEPTANCE_RANGE)
- return math.clamp(1 - Tolerance, MIN_TOLERANCE, MAX_TOLERANCE)
- end
- end
- --[[
- Note for the code above:
- "Tolerance" in this case, means the amount of movement that'll be tolerated before the lockpick
- fails. AngleDifference is the difference between the target angle, and the lockpicks angle.
- If the AngleDifference is less than or equal to the Acceptance range, then the Tolerance is
- at the maximum, allowing for a succesful unlock. If the AngleDifference is greater than or equal
- to the Failure threshold, then the Tolerance is at the minimum, meaning that any minor movement will
- result in a failed attempt. Tolerance is normalized, then clamped within MIN_TOLERANCE or MAX_TOLERANCE
- --]]
- local function CleanLockPick(LockPickVM)
- IsLockpicking = false
- ContextActionService:UnbindAction(FREEZE_ACTION)
- CurrentCamera.CameraType = Enum.CameraType.Custom
- if RenderConnection then RenderConnection:Disconnect() end
- if LockPickVM then LockPickVM:Destroy() end
- end
- --[[
- Note for the code above:
- This function is used to CleanLockPick the lockpick system.
- It's called when the lockpick is cancelled, failed, or when the lockpick is completed.
- This'll unfreeze the player and camera, disconnect the render connection,
- and destroy the lockpick viewmodel.
- --]]
- local function TriggerLockViewModel(LockPickVM,BasePickOffset,BaseLockOffset)
- --// Initialized variables.
- local Lock = LockPickVM.Lock
- local Pick = Lock.Pick
- if not IsLockpicking then
- CleanLockPick(LockPickVM)
- return
- end
- -- This'll clean the lockpick if the player stops lockpicking. ( Toggleable Bool )
- if RenderConnection then RenderConnection:Disconnect() end
- -- Disconnect any previous render connection to prevent any potential memory leaks.
- ContextActionService:BindAction(FREEZE_ACTION, function() return Enum.ContextActionResult.Sink end,false, unpack(Enum.PlayerActions:GetEnumItems()) )
- -- This'll disable all player actions, and sink them, meaning they won't affect the player or camera.
- CurrentCamera.CameraType = Enum.CameraType.Scriptable
- -- Disable any camera movement.
- local TARGET_ANGLE = math.random(0,360)
- --[[
- Note for the code above:
- Find a Target angle, this code is run once everytime a player opens the lockpick.
- The target angle is gonna be randomised, and the player will have to rotate the lockpick
- to find the target angle.
- --]]
- LockPickVM:PivotTo(CurrentCamera.CFrame)
- local originalLockPivot = Lock:GetPivot()
- -- Pivot to the camera, and obtain the Original locks pivot, before any movement.
- local SuccessFrames = 0
- local FailedAttempts = 0
- RenderConnection = RunService.RenderStepped:Connect(function()
- --// Update Offsets
- BasePickOffset = LockPickVM:GetPivot():ToObjectSpace(Pick:GetPivot())
- LockPickVM:PivotTo(CurrentCamera.CFrame)
- --// Rotation based on mouse
- local PickAngle = GetMouseScreenAngle() -- Gets the mouse angle
- local PickOffsetRotated = CFrame.new(BasePickOffset.Position) * CFrame.Angles(0,0,`PickAngle`)
- Pick:PivotTo(CurrentCamera.CFrame * PickOffsetRotated)
- --[[
- Note for the code above:
- This'll rotate the lockpick based on the mouse position.
- We get the mouse angle, and then change the pivot the lockpick to the original pivot, but
- with the given PickAngle derived from the mouse.
- --]]
- --// Guard clause seperating IsMouseHeld == false and IsMouseHeld == true
- if not IsMouseHeld then
- Lock:PivotTo(originalLockPivot)
- SuccessFrames = 0
- FailedAttempts = 0
- return
- end
- --[[
- Note for the code above:
- If the player isn't holding the mouse 1 button ( Default ) , this'll
- reset the lockpicks position. And resetting the failed attempts and success frames to 0.
- --]]
- --// If M1 is held down, the following code runs.
- Lock:PivotTo(Lock:GetPivot() * CFrame.Angles(0,0,0.025))
- SuccessFrames += 1
- -- Rotate the lock by a slight tick of 0.025, and add 1 success frame/tick.
- local AngleDifference = math.abs(TARGET_ANGLE - GetDegrees360(PickAngle))
- -- Difference between the target angle, and the picks current angle.
- local Tolerance = GetAngleTolerance(AngleDifference)
- -- This'll calculate the amount of tolerance based on the angle difference.
- --// IF FAILED TOO MUCH:
- if FailedAttempts >= MAX_FAILURES then
- script.Fail:Play()
- CleanLockPick(LockPickVM)
- return
- end
- -- If the failed attempts exceed the allowed amount, play the fail sound, and clean the lockpick.
- --// IF SUCCESS:
- if SuccessFrames >= SUCCESS_ROTATION_TICKS then
- if RenderConnection then RenderConnection:Disconnect() end -- End the connection.
- IsLockpicking = false -- Toggle the lockpick off, indicating the lockpick is no longer in use.
- script.Win:Play() -- Happy sound
- --// I made the lockpick drop here, but you can signify its done however you want.
- task.wait(1)
- for _,Children in LockPickVM:GetDescendants() do
- if not Children:IsA("BasePart") then continue end
- Children.Anchored = false
- end
- task.wait(0.5)
- CleanLockPick(LockPickVM) -- Clean everything up.
- return
- end
- --// IF WITHIN THE TARGET THRESHOLD ANGLE:
- if SuccessFrames >= (Tolerance * SUCCESS_ROTATION_TICKS ) then -- ( 0.1 * 40 = 4, 1 * 40 = 40)
- if not script.Attempt.IsPlaying then script.Attempt:Play() end
- SuccessFrames = 0
- FailedAttempts += 1
- Lock:PivotTo(originalLockPivot)
- return
- end
- --[[
- Note for the code above:
- The tolerance will increase the closer to the target angle, within the thresholds.
- If the success frames exceed the calculated tolerance, it'll reset the lockpick,
- and add 1 to the failed attempts.
- If the Tolerance is 1, and the SuccessFrames is equal to or exceed the required
- amount of ticks to succeed. The if statement above this, will trigger first. Initiating a
- "Win" or a succesful lockpick attempt.
- --]]
- end)
- end
- local function TriggerLock(ForceClose)
- local LockPickVM = CurrentCamera:FindFirstChild("LockpickViewModel") or game.ReplicatedStorage.LockpickViewModel:Clone()
- local Lock = LockPickVM.Lock
- local Pick = Lock.Pick
- -- Initializes the existence of a lock.
- local BasePickOffset = LockPickVM:GetPivot():ToObjectSpace(Pick:GetPivot())
- local BaseLockOffset = LockPickVM:GetPivot():ToObjectSpace(Lock:GetPivot())
- -- Get the base offset of the lock and the pick, for later use.
- if ForceClose then
- IsLockpicking = false
- CleanLockPick(LockPickVM)
- else
- LockPickVM.Parent = CurrentCamera
- IsLockpicking = not IsLockpicking
- TriggerLockViewModel(LockPickVM,BasePickOffset,BaseLockOffset)
- end
- --[[
- Note for the code above:
- If ForceClosed is triggered, it'll toggle off the IsLockpicking state and clean the lockpick.
- Else, it'll trigger the lockpick viewmodel, and set the IsLockpicking state to true.
- --]]
- end
- -- When a player interacts with a prompt, this'll trigger.
- ReplicatedStorage.Events.Interact.OnClientEvent:Connect(function(Interactable,ForceClose)
- TriggerLock(ForceClose)
- --[[
- Note for the code above:
- ForceClosed is used, so if the player walks away from the prompt, or is pushed away.
- It'll close the prompt automatically. This is used to prevent false toggles.
- You can use this for your own custom logic, I just use the prompt system I premade.
- --]]
- end)
Advertisement
Add Comment
Please, Sign In to add comment