pastefile1
@@ -0,0 +1,11807 @@
+dj = false
+skipping = false
+Audio = game:GetService("Workspace")["GLOBAL_SOUND"]
+fun = false
+
+local Material = loadstring(game:HttpGet("https://raw.githubusercontent.com/Kinlei/MaterialLua/master/Module.lua"))()
+
+local UI = Material.Load({
+ Title = "Club DJ fucker",
+ Style = 3,
+ SizeX = 400,
+ SizeY = 300,
+ Theme = "Aqua"
+})
+
+local Page = UI.New({
+ Title = "Main"
+})
+
+Page.Button({
+ Text = "Steal DJ",
+ Callback = function()
+ if dj == false then
+ dj = true
+ while wait() do
+ if dj == true then
+ game.ReplicatedStorage.Connection:InvokeServer(6)
+ end
+ end
+ else
+ dj = false
+ end
+ end,
+ Menu = {
+ Information = function(self)
+ UI.Banner({
+ Text = "This script steals the DJ spot, if someone leaves it"
+ })
+ end,
+ ["Check, if it works"] = function(self)
+ print("If you see this, the script probably works!")
+ end
+ }
+})
+
+Page.Button({
+ Text = "Be able to walk",
+ Callback = function()
+--[[
+local _p = game:WaitForChild("Players")
+local _plr = _p.ChildAdded:Wait()
+if _plr == _p.LocalPlayer then
+ _plr.ChildAdded:Connect(function(cccc)
+ if c.Name == "PlayerScriptsLoader" then
+ c.Disabled = true
+ end
+ end)
+end
+]]
+repeat wait()
+a = pcall(function()
+ game:WaitForChild("Players").LocalPlayer:WaitForChild("PlayerScripts").ChildAdded:Connect(function(c)
+ if c.Name == "PlayerScriptsLoader"then
+ c.Disabled = true
+ end
+ end)
+ end)
+ if a == true then break end
+until true == false
+game:WaitForChild("Players").LocalPlayer:WaitForChild("PlayerScripts").ChildAdded:Connect(function(c)
+ if c.Name == "PlayerScriptsLoader"then
+ c.Disabled = true
+ end
+end)
+
+
+function _CameraUI()
+ local Players = game:GetService("Players")
+ local TweenService = game:GetService("TweenService")
+
+ local LocalPlayer = Players.LocalPlayer
+ if not LocalPlayer then
+ Players:GetPropertyChangedSignal("LocalPlayer"):Wait()
+ LocalPlayer = Players.LocalPlayer
+ end
+
+ local function waitForChildOfClass(parent, class)
+ local child = parent:FindFirstChildOfClass(class)
+ while not child or child.ClassName ~= class do
+ child = parent.ChildAdded:Wait()
+ end
+ return child
+ end
+
+ local PlayerGui = waitForChildOfClass(LocalPlayer, "PlayerGui")
+
+ local TOAST_OPEN_SIZE = UDim2.new(0, 326, 0, 58)
+ local TOAST_CLOSED_SIZE = UDim2.new(0, 80, 0, 58)
+ local TOAST_BACKGROUND_COLOR = Color3.fromRGB(32, 32, 32)
+ local TOAST_BACKGROUND_TRANS = 0.4
+ local TOAST_FOREGROUND_COLOR = Color3.fromRGB(200, 200, 200)
+ local TOAST_FOREGROUND_TRANS = 0
+
+ -- Convenient syntax for creating a tree of instanes
+ local function create(className)
+ return function(props)
+ local inst = Instance.new(className)
+ local parent = props.Parent
+ props.Parent = nil
+ for name, val in pairs(props) do
+ if type(name) == "string" then
+ inst[name] = val
+ else
+ val.Parent = inst
+ end
+ end
+ -- Only set parent after all other properties are initialized
+ inst.Parent = parent
+ return inst
+ end
+ end
+
+ local initialized = false
+
+ local uiRoot
+ local toast
+ local toastIcon
+ local toastUpperText
+ local toastLowerText
+
+ local function initializeUI()
+ assert(not initialized)
+
+ uiRoot = create("ScreenGui"){
+ Name = "RbxCameraUI",
+ AutoLocalize = false,
+ Enabled = true,
+ DisplayOrder = -1, -- Appears behind default developer UI
+ IgnoreGuiInset = false,
+ ResetOnSpawn = false,
+ ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
+
+ create("ImageLabel"){
+ Name = "Toast",
+ Visible = false,
+ AnchorPoint = Vector2.new(0.5, 0),
+ BackgroundTransparency = 1,
+ BorderSizePixel = 0,
+ Position = UDim2.new(0.5, 0, 0, 8),
+ Size = TOAST_CLOSED_SIZE,
+ Image = "rbxasset://textures/ui/Camera/CameraToast9Slice.png",
+ ImageColor3 = TOAST_BACKGROUND_COLOR,
+ ImageRectSize = Vector2.new(6, 6),
+ ImageTransparency = 1,
+ ScaleType = Enum.ScaleType.Slice,
+ SliceCenter = Rect.new(3, 3, 3, 3),
+ ClipsDescendants = true,
+
+ create("Frame"){
+ Name = "IconBuffer",
+ BackgroundTransparency = 1,
+ BorderSizePixel = 0,
+ Position = UDim2.new(0, 0, 0, 0),
+ Size = UDim2.new(0, 80, 1, 0),
+
+ create("ImageLabel"){
+ Name = "Icon",
+ AnchorPoint = Vector2.new(0.5, 0.5),
+ BackgroundTransparency = 1,
+ Position = UDim2.new(0.5, 0, 0.5, 0),
+ Size = UDim2.new(0, 48, 0, 48),
+ ZIndex = 2,
+ Image = "rbxasset://textures/ui/Camera/CameraToastIcon.png",
+ ImageColor3 = TOAST_FOREGROUND_COLOR,
+ ImageTransparency = 1,
+ }
+ },
+
+ create("Frame"){
+ Name = "TextBuffer",
+ BackgroundTransparency = 1,
+ BorderSizePixel = 0,
+ Position = UDim2.new(0, 80, 0, 0),
+ Size = UDim2.new(1, -80, 1, 0),
+ ClipsDescendants = true,
+
+ create("TextLabel"){
+ Name = "Upper",
+ AnchorPoint = Vector2.new(0, 1),
+ BackgroundTransparency = 1,
+ Position = UDim2.new(0, 0, 0.5, 0),
+ Size = UDim2.new(1, 0, 0, 19),
+ Font = Enum.Font.GothamSemibold,
+ Text = "Camera control enabled",
+ TextColor3 = TOAST_FOREGROUND_COLOR,
+ TextTransparency = 1,
+ TextSize = 19,
+ TextXAlignment = Enum.TextXAlignment.Left,
+ TextYAlignment = Enum.TextYAlignment.Center,
+ },
+
+ create("TextLabel"){
+ Name = "Lower",
+ AnchorPoint = Vector2.new(0, 0),
+ BackgroundTransparency = 1,
+ Position = UDim2.new(0, 0, 0.5, 3),
+ Size = UDim2.new(1, 0, 0, 15),
+ Font = Enum.Font.Gotham,
+ Text = "Right mouse button to toggle",
+ TextColor3 = TOAST_FOREGROUND_COLOR,
+ TextTransparency = 1,
+ TextSize = 15,
+ TextXAlignment = Enum.TextXAlignment.Left,
+ TextYAlignment = Enum.TextYAlignment.Center,
+ },
+ },
+ },
+
+ Parent = PlayerGui,
+ }
+
+ toast = uiRoot.Toast
+ toastIcon = toast.IconBuffer.Icon
+ toastUpperText = toast.TextBuffer.Upper
+ toastLowerText = toast.TextBuffer.Lower
+
+ initialized = true
+ end
+
+ local CameraUI = {}
+
+ do
+ -- Instantaneously disable the toast or enable for opening later on. Used when switching camera modes.
+ function CameraUI.setCameraModeToastEnabled(enabled)
+ if not enabled and not initialized then
+ return
+ end
+
+ if not initialized then
+ initializeUI()
+ end
+
+ toast.Visible = enabled
+ if not enabled then
+ CameraUI.setCameraModeToastOpen(false)
+ end
+ end
+
+ local tweenInfo = TweenInfo.new(0.25, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
+
+ -- Tween the toast in or out. Toast must be enabled with setCameraModeToastEnabled.
+ function CameraUI.setCameraModeToastOpen(open)
+ assert(initialized)
+
+ TweenService:Create(toast, tweenInfo, {
+ Size = open and TOAST_OPEN_SIZE or TOAST_CLOSED_SIZE,
+ ImageTransparency = open and TOAST_BACKGROUND_TRANS or 1,
+ }):Play()
+
+ TweenService:Create(toastIcon, tweenInfo, {
+ ImageTransparency = open and TOAST_FOREGROUND_TRANS or 1,
+ }):Play()
+
+ TweenService:Create(toastUpperText, tweenInfo, {
+ TextTransparency = open and TOAST_FOREGROUND_TRANS or 1,
+ }):Play()
+
+ TweenService:Create(toastLowerText, tweenInfo, {
+ TextTransparency = open and TOAST_FOREGROUND_TRANS or 1,
+ }):Play()
+ end
+ end
+
+ return CameraUI
+end
+
+function _CameraToggleStateController()
+ local Players = game:GetService("Players")
+ local UserInputService = game:GetService("UserInputService")
+ local GameSettings = UserSettings():GetService("UserGameSettings")
+
+ local LocalPlayer = Players.LocalPlayer
+ if not LocalPlayer then
+ Players:GetPropertyChangedSignal("LocalPlayer"):Wait()
+ LocalPlayer = Players.LocalPlayer
+ end
+
+ local Mouse = LocalPlayer:GetMouse()
+
+ local Input = _CameraInput()
+ local CameraUI = _CameraUI()
+
+ local lastTogglePan = false
+ local lastTogglePanChange = tick()
+
+ local CROSS_MOUSE_ICON = "rbxasset://textures/Cursors/CrossMouseIcon.png"
+
+ local lockStateDirty = false
+ local wasTogglePanOnTheLastTimeYouWentIntoFirstPerson = false
+ local lastFirstPerson = false
+
+ CameraUI.setCameraModeToastEnabled(false)
+
+ return function(isFirstPerson)
+ local togglePan = Input.getTogglePan()
+ local toastTimeout = 3
+
+ if isFirstPerson and togglePan ~= lastTogglePan then
+ lockStateDirty = true
+ end
+
+ if lastTogglePan ~= togglePan or tick() - lastTogglePanChange > toastTimeout then
+ local doShow = togglePan and tick() - lastTogglePanChange < toastTimeout
+
+ CameraUI.setCameraModeToastOpen(doShow)
+
+ if togglePan then
+ lockStateDirty = false
+ end
+ lastTogglePanChange = tick()
+ lastTogglePan = togglePan
+ end
+
+ if isFirstPerson ~= lastFirstPerson then
+ if isFirstPerson then
+ wasTogglePanOnTheLastTimeYouWentIntoFirstPerson = Input.getTogglePan()
+ Input.setTogglePan(true)
+ elseif not lockStateDirty then
+ Input.setTogglePan(wasTogglePanOnTheLastTimeYouWentIntoFirstPerson)
+ end
+ end
+
+ if isFirstPerson then
+ if Input.getTogglePan() then
+ Mouse.Icon = CROSS_MOUSE_ICON
+ UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter
+ --GameSettings.RotationType = Enum.RotationType.CameraRelative
+ else
+ Mouse.Icon = ""
+ UserInputService.MouseBehavior = Enum.MouseBehavior.Default
+ --GameSettings.RotationType = Enum.RotationType.CameraRelative
+ end
+
+ elseif Input.getTogglePan() then
+ Mouse.Icon = CROSS_MOUSE_ICON
+ UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter
+ GameSettings.RotationType = Enum.RotationType.MovementRelative
+
+ elseif Input.getHoldPan() then
+ Mouse.Icon = ""
+ UserInputService.MouseBehavior = Enum.MouseBehavior.LockCurrentPosition
+ GameSettings.RotationType = Enum.RotationType.MovementRelative
+
+ else
+ Mouse.Icon = ""
+ UserInputService.MouseBehavior = Enum.MouseBehavior.Default
+ GameSettings.RotationType = Enum.RotationType.MovementRelative
+ end
+
+ lastFirstPerson = isFirstPerson
+ end
+end
+
+function _CameraInput()
+ local UserInputService = game:GetService("UserInputService")
+
+ local MB_TAP_LENGTH = 0.3 -- length of time for a short mouse button tap to be registered
+
+ local rmbDown, rmbUp
+ do
+ local rmbDownBindable = Instance.new("BindableEvent")
+ local rmbUpBindable = Instance.new("BindableEvent")
+
+ rmbDown = rmbDownBindable.Event
+ rmbUp = rmbUpBindable.Event
+
+ UserInputService.InputBegan:Connect(function(input, gpe)
+ if not gpe and input.UserInputType == Enum.UserInputType.MouseButton2 then
+ rmbDownBindable:Fire()
+ end
+ end)
+
+ UserInputService.InputEnded:Connect(function(input, gpe)
+ if input.UserInputType == Enum.UserInputType.MouseButton2 then
+ rmbUpBindable:Fire()
+ end
+ end)
+ end
+
+ local holdPan = false
+ local togglePan = false
+ local lastRmbDown = 0 -- tick() timestamp of the last right mouse button down event
+
+ local CameraInput = {}
+
+ function CameraInput.getHoldPan()
+ return holdPan
+ end
+
+ function CameraInput.getTogglePan()
+ return togglePan
+ end
+
+ function CameraInput.getPanning()
+ return togglePan or holdPan
+ end
+
+ function CameraInput.setTogglePan(value)
+ togglePan = value
+ end
+
+ local cameraToggleInputEnabled = false
+ local rmbDownConnection
+ local rmbUpConnection
+
+ function CameraInput.enableCameraToggleInput()
+ if cameraToggleInputEnabled then
+ return
+ end
+ cameraToggleInputEnabled = true
+
+ holdPan = false
+ togglePan = false
+
+ if rmbDownConnection then
+ rmbDownConnection:Disconnect()
+ end
+
+ if rmbUpConnection then
+ rmbUpConnection:Disconnect()
+ end
+
+ rmbDownConnection = rmbDown:Connect(function()
+ holdPan = true
+ lastRmbDown = tick()
+ end)
+
+ rmbUpConnection = rmbUp:Connect(function()
+ holdPan = false
+ if tick() - lastRmbDown < MB_TAP_LENGTH and (togglePan or UserInputService:GetMouseDelta().Magnitude < 2) then
+ togglePan = not togglePan
+ end
+ end)
+ end
+
+ function CameraInput.disableCameraToggleInput()
+ if not cameraToggleInputEnabled then
+ return
+ end
+ cameraToggleInputEnabled = false
+
+ if rmbDownConnection then
+ rmbDownConnection:Disconnect()
+ rmbDownConnection = nil
+ end
+ if rmbUpConnection then
+ rmbUpConnection:Disconnect()
+ rmbUpConnection = nil
+ end
+ end
+
+ return CameraInput
+end
+
+function _BaseCamera()
+ --[[
+ BaseCamera - Abstract base class for camera control modules
+ 2018 Camera Update - AllYourBlox
+ --]]
+
+ --[[ Local Constants ]]--
+ local UNIT_Z = Vector3.new(0,0,1)
+ local X1_Y0_Z1 = Vector3.new(1,0,1) --Note: not a unit vector, used for projecting onto XZ plane
+
+ local THUMBSTICK_DEADZONE = 0.2
+ local DEFAULT_DISTANCE = 12.5 -- Studs
+ local PORTRAIT_DEFAULT_DISTANCE = 25 -- Studs
+ local FIRST_PERSON_DISTANCE_THRESHOLD = 1.0 -- Below this value, snap into first person
+
+ local CAMERA_ACTION_PRIORITY = Enum.ContextActionPriority.Default.Value
+
+ -- Note: DotProduct check in CoordinateFrame::lookAt() prevents using values within about
+ -- 8.11 degrees of the +/- Y axis, that's why these limits are currently 80 degrees
+ local MIN_Y = math.rad(-80)
+ local MAX_Y = math.rad(80)
+
+ local TOUCH_ADJUST_AREA_UP = math.rad(30)
+ local TOUCH_ADJUST_AREA_DOWN = math.rad(-15)
+
+ local TOUCH_SENSITIVTY_ADJUST_MAX_Y = 2.1
+ local TOUCH_SENSITIVTY_ADJUST_MIN_Y = 0.5
+
+ local VR_ANGLE = math.rad(15)
+ local VR_LOW_INTENSITY_ROTATION = Vector2.new(math.rad(15), 0)
+ local VR_HIGH_INTENSITY_ROTATION = Vector2.new(math.rad(45), 0)
+ local VR_LOW_INTENSITY_REPEAT = 0.1
+ local VR_HIGH_INTENSITY_REPEAT = 0.4
+
+ local ZERO_VECTOR2 = Vector2.new(0,0)
+ local ZERO_VECTOR3 = Vector3.new(0,0,0)
+
+ local TOUCH_SENSITIVTY = Vector2.new(0.00945 * math.pi, 0.003375 * math.pi)
+ local MOUSE_SENSITIVITY = Vector2.new( 0.002 * math.pi, 0.0015 * math.pi )
+
+ local SEAT_OFFSET = Vector3.new(0,5,0)
+ local VR_SEAT_OFFSET = Vector3.new(0,4,0)
+ local HEAD_OFFSET = Vector3.new(0,1.5,0)
+ local R15_HEAD_OFFSET = Vector3.new(0, 1.5, 0)
+ local R15_HEAD_OFFSET_NO_SCALING = Vector3.new(0, 2, 0)
+ local HUMANOID_ROOT_PART_SIZE = Vector3.new(2, 2, 1)
+
+ local GAMEPAD_ZOOM_STEP_1 = 0
+ local GAMEPAD_ZOOM_STEP_2 = 10
+ local GAMEPAD_ZOOM_STEP_3 = 20
+
+ local PAN_SENSITIVITY = 20
+ local ZOOM_SENSITIVITY_CURVATURE = 0.5
+
+ local abs = math.abs
+ local sign = math.sign
+
+ local FFlagUserCameraToggle do
+ local success, result = pcall(function()
+ return UserSettings():IsUserFeatureEnabled("UserCameraToggle")
+ end)
+ FFlagUserCameraToggle = success and result
+ end
+
+ local FFlagUserDontAdjustSensitvityForPortrait do
+ local success, result = pcall(function()
+ return UserSettings():IsUserFeatureEnabled("UserDontAdjustSensitvityForPortrait")
+ end)
+ FFlagUserDontAdjustSensitvityForPortrait = success and result
+ end
+
+ local FFlagUserFixZoomInZoomOutDiscrepancy do
+ local success, result = pcall(function()
+ return UserSettings():IsUserFeatureEnabled("UserFixZoomInZoomOutDiscrepancy")
+ end)
+ FFlagUserFixZoomInZoomOutDiscrepancy = success and result
+ end
+
+ local Util = _CameraUtils()
+ local ZoomController = _ZoomController()
+ local CameraToggleStateController = _CameraToggleStateController()
+ local CameraInput = _CameraInput()
+ local CameraUI = _CameraUI()
+
+ --[[ Roblox Services ]]--
+ local Players = game:GetService("Players")
+ local UserInputService = game:GetService("UserInputService")
+ local StarterGui = game:GetService("StarterGui")
+ local GuiService = game:GetService("GuiService")
+ local ContextActionService = game:GetService("ContextActionService")
+ local VRService = game:GetService("VRService")
+ local UserGameSettings = UserSettings():GetService("UserGameSettings")
+
+ local player = Players.LocalPlayer
+
+ --[[ The Module ]]--
+ local BaseCamera = {}
+ BaseCamera.__index = BaseCamera
+
+ function BaseCamera.new()
+ local self = setmetatable({}, BaseCamera)
+
+ -- So that derived classes have access to this
+ self.FIRST_PERSON_DISTANCE_THRESHOLD = FIRST_PERSON_DISTANCE_THRESHOLD
+
+ self.cameraType = nil
+ self.cameraMovementMode = nil
+
+ self.lastCameraTransform = nil
+ self.rotateInput = ZERO_VECTOR2
+ self.userPanningCamera = false
+ self.lastUserPanCamera = tick()
+
+ self.humanoidRootPart = nil
+ self.humanoidCache = {}
+
+ -- Subject and position on last update call
+ self.lastSubject = nil
+ self.lastSubjectPosition = Vector3.new(0,5,0)
+
+ -- These subject distance members refer to the nominal camera-to-subject follow distance that the camera
+ -- is trying to maintain, not the actual measured value.
+ -- The default is updated when screen orientation or the min/max distances change,
+ -- to be sure the default is always in range and appropriate for the orientation.
+ self.defaultSubjectDistance = math.clamp(DEFAULT_DISTANCE, player.CameraMinZoomDistance, player.CameraMaxZoomDistance)
+ self.currentSubjectDistance = math.clamp(DEFAULT_DISTANCE, player.CameraMinZoomDistance, player.CameraMaxZoomDistance)
+
+ self.inFirstPerson = false
+ self.inMouseLockedMode = false
+ self.portraitMode = false
+ self.isSmallTouchScreen = false
+
+ -- Used by modules which want to reset the camera angle on respawn.
+ self.resetCameraAngle = true
+
+ self.enabled = false
+
+ -- Input Event Connections
+ self.inputBeganConn = nil
+ self.inputChangedConn = nil
+ self.inputEndedConn = nil
+
+ self.startPos = nil
+ self.lastPos = nil
+ self.panBeginLook = nil
+
+ self.panEnabled = true
+ self.keyPanEnabled = true
+ self.distanceChangeEnabled = true
+
+ self.PlayerGui = nil
+
+ self.cameraChangedConn = nil
+ self.viewportSizeChangedConn = nil
+
+ self.boundContextActions = {}
+
+ -- VR Support
+ self.shouldUseVRRotation = false
+ self.VRRotationIntensityAvailable = false
+ self.lastVRRotationIntensityCheckTime = 0
+ self.lastVRRotationTime = 0
+ self.vrRotateKeyCooldown = {}
+ self.cameraTranslationConstraints = Vector3.new(1, 1, 1)
+ self.humanoidJumpOrigin = nil
+ self.trackingHumanoid = nil
+ self.cameraFrozen = false
+ self.subjectStateChangedConn = nil
+
+ -- Gamepad support
+ self.activeGamepad = nil
+ self.gamepadPanningCamera = false
+ self.lastThumbstickRotate = nil
+ self.numOfSeconds = 0.7
+ self.currentSpeed = 0
+ self.maxSpeed = 6
+ self.vrMaxSpeed = 4
+ self.lastThumbstickPos = Vector2.new(0,0)
+ self.ySensitivity = 0.65
+ self.lastVelocity = nil
+ self.gamepadConnectedConn = nil
+ self.gamepadDisconnectedConn = nil
+ self.currentZoomSpeed = 1.0
+ self.L3ButtonDown = false
+ self.dpadLeftDown = false
+ self.dpadRightDown = false
+
+ -- Touch input support
+ self.isDynamicThumbstickEnabled = false
+ self.fingerTouches = {}
+ self.dynamicTouchInput = nil
+ self.numUnsunkTouches = 0
+ self.inputStartPositions = {}
+ self.inputStartTimes = {}
+ self.startingDiff = nil
+ self.pinchBeginZoom = nil
+ self.userPanningTheCamera = false
+ self.touchActivateConn = nil
+
+ -- Mouse locked formerly known as shift lock mode
+ self.mouseLockOffset = ZERO_VECTOR3
+
+ -- [[ NOTICE ]] --
+ -- Initialization things used to always execute at game load time, but now these camera modules are instantiated
+ -- when needed, so the code here may run well after the start of the game
+
+ if player.Character then
+ self:OnCharacterAdded(player.Character)
+ end
+
+ player.CharacterAdded:Connect(function(char)
+ self:OnCharacterAdded(char)
+ end)
+
+ if self.cameraChangedConn then self.cameraChangedConn:Disconnect() end
+ self.cameraChangedConn = workspace:GetPropertyChangedSignal("CurrentCamera"):Connect(function()
+ self:OnCurrentCameraChanged()
+ end)
+ self:OnCurrentCameraChanged()
+
+ if self.playerCameraModeChangeConn then self.playerCameraModeChangeConn:Disconnect() end
+ self.playerCameraModeChangeConn = player:GetPropertyChangedSignal("CameraMode"):Connect(function()
+ self:OnPlayerCameraPropertyChange()
+ end)
+
+ if self.minDistanceChangeConn then self.minDistanceChangeConn:Disconnect() end
+ self.minDistanceChangeConn = player:GetPropertyChangedSignal("CameraMinZoomDistance"):Connect(function()
+ self:OnPlayerCameraPropertyChange()
+ end)
+
+ if self.maxDistanceChangeConn then self.maxDistanceChangeConn:Disconnect() end
+ self.maxDistanceChangeConn = player:GetPropertyChangedSignal("CameraMaxZoomDistance"):Connect(function()
+ self:OnPlayerCameraPropertyChange()
+ end)
+
+ if self.playerDevTouchMoveModeChangeConn then self.playerDevTouchMoveModeChangeConn:Disconnect() end
+ self.playerDevTouchMoveModeChangeConn = player:GetPropertyChangedSignal("DevTouchMovementMode"):Connect(function()
+ self:OnDevTouchMovementModeChanged()
+ end)
+ self:OnDevTouchMovementModeChanged() -- Init
+
+ if self.gameSettingsTouchMoveMoveChangeConn then self.gameSettingsTouchMoveMoveChangeConn:Disconnect() end
+ self.gameSettingsTouchMoveMoveChangeConn = UserGameSettings:GetPropertyChangedSignal("TouchMovementMode"):Connect(function()
+ self:OnGameSettingsTouchMovementModeChanged()
+ end)
+ self:OnGameSettingsTouchMovementModeChanged() -- Init
+
+ UserGameSettings:SetCameraYInvertVisible()
+ UserGameSettings:SetGamepadCameraSensitivityVisible()
+
+ self.hasGameLoaded = game:IsLoaded()
+ if not self.hasGameLoaded then
+ self.gameLoadedConn = game.Loaded:Connect(function()
+ self.hasGameLoaded = true
+ self.gameLoadedConn:Disconnect()
+ self.gameLoadedConn = nil
+ end)
+ end
+
+ self:OnPlayerCameraPropertyChange()
+
+ return self
+ end
+
+ function BaseCamera:GetModuleName()
+ return "BaseCamera"
+ end
+
+ function BaseCamera:OnCharacterAdded(char)
+ self.resetCameraAngle = self.resetCameraAngle or self:GetEnabled()
+ self.humanoidRootPart = nil
+ if UserInputService.TouchEnabled then
+ self.PlayerGui = player:WaitForChild("PlayerGui")
+ for _, child in ipairs(char:GetChildren()) do
+ if child:IsA("Tool") then
+ self.isAToolEquipped = true
+ end
+ end
+ char.ChildAdded:Connect(function(child)
+ if child:IsA("Tool") then
+ self.isAToolEquipped = true
+ end
+ end)
+ char.ChildRemoved:Connect(function(child)
+ if child:IsA("Tool") then
+ self.isAToolEquipped = false
+ end
+ end)
+ end
+ end
+
+ function BaseCamera:GetHumanoidRootPart()
+ if not self.humanoidRootPart then
+ if player.Character then
+ local humanoid = player.Character:FindFirstChildOfClass("Humanoid")
+ if humanoid then
+ self.humanoidRootPart = humanoid.RootPart
+ end
+ end
+ end
+ return self.humanoidRootPart
+ end
+
+ function BaseCamera:GetBodyPartToFollow(humanoid, isDead)
+ -- If the humanoid is dead, prefer the head part if one still exists as a sibling of the humanoid
+ if humanoid:GetState() == Enum.HumanoidStateType.Dead then
+ local character = humanoid.Parent
+ if character and character:IsA("Model") then
+ return character:FindFirstChild("Head") or humanoid.RootPart
+ end
+ end
+
+ return humanoid.RootPart
+ end
+
+ function BaseCamera:GetSubjectPosition()
+ local result = self.lastSubjectPosition
+ local camera = game.Workspace.CurrentCamera
+ local cameraSubject = camera and camera.CameraSubject
+
+ if cameraSubject then
+ if cameraSubject:IsA("Humanoid") then
+ local humanoid = cameraSubject
+ local humanoidIsDead = humanoid:GetState() == Enum.HumanoidStateType.Dead
+
+ if VRService.VREnabled and humanoidIsDead and humanoid == self.lastSubject then
+ result = self.lastSubjectPosition
+ else
+ local bodyPartToFollow = humanoid.RootPart
+
+ -- If the humanoid is dead, prefer their head part as a follow target, if it exists
+ if humanoidIsDead then
+ if humanoid.Parent and humanoid.Parent:IsA("Model") then
+ bodyPartToFollow = humanoid.Parent:FindFirstChild("Head") or bodyPartToFollow
+ end
+ end
+
+ if bodyPartToFollow and bodyPartToFollow:IsA("BasePart") then
+ local heightOffset
+ if humanoid.RigType == Enum.HumanoidRigType.R15 then
+ if humanoid.AutomaticScalingEnabled then
+ heightOffset = R15_HEAD_OFFSET
+ if bodyPartToFollow == humanoid.RootPart then
+ local rootPartSizeOffset = (humanoid.RootPart.Size.Y/2) - (HUMANOID_ROOT_PART_SIZE.Y/2)
+ heightOffset = heightOffset + Vector3.new(0, rootPartSizeOffset, 0)
+ end
+ else
+ heightOffset = R15_HEAD_OFFSET_NO_SCALING
+ end
+ else
+ heightOffset = HEAD_OFFSET
+ end
+
+ if humanoidIsDead then
+ heightOffset = ZERO_VECTOR3
+ end
+
+ result = bodyPartToFollow.CFrame.p + bodyPartToFollow.CFrame:vectorToWorldSpace(heightOffset + humanoid.CameraOffset)
+ end
+ end
+
+ elseif cameraSubject:IsA("VehicleSeat") then
+ local offset = SEAT_OFFSET
+ if VRService.VREnabled then
+ offset = VR_SEAT_OFFSET
+ end
+ result = cameraSubject.CFrame.p + cameraSubject.CFrame:vectorToWorldSpace(offset)
+ elseif cameraSubject:IsA("SkateboardPlatform") then
+ result = cameraSubject.CFrame.p + SEAT_OFFSET
+ elseif cameraSubject:IsA("BasePart") then
+ result = cameraSubject.CFrame.p
+ elseif cameraSubject:IsA("Model") then
+ if cameraSubject.PrimaryPart then
+ result = cameraSubject:GetPrimaryPartCFrame().p
+ else
+ result = cameraSubject:GetModelCFrame().p
+ end
+ end
+ else
+ -- cameraSubject is nil
+ -- Note: Previous RootCamera did not have this else case and let self.lastSubject and self.lastSubjectPosition
+ -- both get set to nil in the case of cameraSubject being nil. This function now exits here to preserve the
+ -- last set valid values for these, as nil values are not handled cases
+ return
+ end
+
+ self.lastSubject = cameraSubject
+ self.lastSubjectPosition = result
+
+ return result
+ end
+
+ function BaseCamera:UpdateDefaultSubjectDistance()
+ if self.portraitMode then
+ self.defaultSubjectDistance = math.clamp(PORTRAIT_DEFAULT_DISTANCE, player.CameraMinZoomDistance, player.CameraMaxZoomDistance)
+ else
+ self.defaultSubjectDistance = math.clamp(DEFAULT_DISTANCE, player.CameraMinZoomDistance, player.CameraMaxZoomDistance)
+ end
+ end
+
+ function BaseCamera:OnViewportSizeChanged()
+ local camera = game.Workspace.CurrentCamera
+ local size = camera.ViewportSize
+ self.portraitMode = size.X < size.Y
+ self.isSmallTouchScreen = UserInputService.TouchEnabled and (size.Y < 500 or size.X < 700)
+
+ self:UpdateDefaultSubjectDistance()
+ end
+
+ -- Listener for changes to workspace.CurrentCamera
+ function BaseCamera:OnCurrentCameraChanged()
+ if UserInputService.TouchEnabled then
+ if self.viewportSizeChangedConn then
+ self.viewportSizeChangedConn:Disconnect()
+ self.viewportSizeChangedConn = nil
+ end
+
+ local newCamera = game.Workspace.CurrentCamera
+
+ if newCamera then
+ self:OnViewportSizeChanged()
+ self.viewportSizeChangedConn = newCamera:GetPropertyChangedSignal("ViewportSize"):Connect(function()
+ self:OnViewportSizeChanged()
+ end)
+ end
+ end
+
+ -- VR support additions
+ if self.cameraSubjectChangedConn then
+ self.cameraSubjectChangedConn:Disconnect()
+ self.cameraSubjectChangedConn = nil
+ end
+
+ local camera = game.Workspace.CurrentCamera
+ if camera then
+ self.cameraSubjectChangedConn = camera:GetPropertyChangedSignal("CameraSubject"):Connect(function()
+ self:OnNewCameraSubject()
+ end)
+ self:OnNewCameraSubject()
+ end
+ end
+
+ function BaseCamera:OnDynamicThumbstickEnabled()
+ if UserInputService.TouchEnabled then
+ self.isDynamicThumbstickEnabled = true
+ end
+ end
+
+ function BaseCamera:OnDynamicThumbstickDisabled()
+ self.isDynamicThumbstickEnabled = false
+ end
+
+ function BaseCamera:OnGameSettingsTouchMovementModeChanged()
+ if player.DevTouchMovementMode == Enum.DevTouchMovementMode.UserChoice then
+ if (UserGameSettings.TouchMovementMode == Enum.TouchMovementMode.DynamicThumbstick
+ or UserGameSettings.TouchMovementMode == Enum.TouchMovementMode.Default) then
+ self:OnDynamicThumbstickEnabled()
+ else
+ self:OnDynamicThumbstickDisabled()
+ end
+ end
+ end
+
+ function BaseCamera:OnDevTouchMovementModeChanged()
+ if player.DevTouchMovementMode.Name == "DynamicThumbstick" then
+ self:OnDynamicThumbstickEnabled()
+ else
+ self:OnGameSettingsTouchMovementModeChanged()
+ end
+ end
+
+ function BaseCamera:OnPlayerCameraPropertyChange()
+ -- This call forces re-evaluation of player.CameraMode and clamping to min/max distance which may have changed
+ self:SetCameraToSubjectDistance(self.currentSubjectDistance)
+ end
+
+ function BaseCamera:GetCameraHeight()
+ if VRService.VREnabled and not self.inFirstPerson then
+ return math.sin(VR_ANGLE) * self.currentSubjectDistance
+ end
+ return 0
+ end
+
+ function BaseCamera:InputTranslationToCameraAngleChange(translationVector, sensitivity)
+ if not FFlagUserDontAdjustSensitvityForPortrait then
+ local camera = game.Workspace.CurrentCamera
+ if camera and camera.ViewportSize.X > 0 and camera.ViewportSize.Y > 0 and (camera.ViewportSize.Y > camera.ViewportSize.X) then
+ -- Screen has portrait orientation, swap X and Y sensitivity
+ return translationVector * Vector2.new( sensitivity.Y, sensitivity.X)
+ end
+ end
+ return translationVector * sensitivity
+ end
+
+ function BaseCamera:Enable(enable)
+ if self.enabled ~= enable then
+ self.enabled = enable
+ if self.enabled then
+ self:ConnectInputEvents()
+ self:BindContextActions()
+
+ if player.CameraMode == Enum.CameraMode.LockFirstPerson then
+ self.currentSubjectDistance = 0.5
+ if not self.inFirstPerson then
+ self:EnterFirstPerson()
+ end
+ end
+ else
+ self:DisconnectInputEvents()
+ self:UnbindContextActions()
+ -- Clean up additional event listeners and reset a bunch of properties
+ self:Cleanup()
+ end
+ end
+ end
+
+ function BaseCamera:GetEnabled()
+ return self.enabled
+ end
+
+ function BaseCamera:OnInputBegan(input, processed)
+ if input.UserInputType == Enum.UserInputType.Touch then
+ self:OnTouchBegan(input, processed)
+ elseif input.UserInputType == Enum.UserInputType.MouseButton2 then
+ self:OnMouse2Down(input, processed)
+ elseif input.UserInputType == Enum.UserInputType.MouseButton3 then
+ self:OnMouse3Down(input, processed)
+ end
+ end
+
+ function BaseCamera:OnInputChanged(input, processed)
+ if input.UserInputType == Enum.UserInputType.Touch then
+ self:OnTouchChanged(input, processed)
+ elseif input.UserInputType == Enum.UserInputType.MouseMovement then
+ self:OnMouseMoved(input, processed)
+ end
+ end
+
+ function BaseCamera:OnInputEnded(input, processed)
+ if input.UserInputType == Enum.UserInputType.Touch then
+ self:OnTouchEnded(input, processed)
+ elseif input.UserInputType == Enum.UserInputType.MouseButton2 then
+ self:OnMouse2Up(input, processed)
+ elseif input.UserInputType == Enum.UserInputType.MouseButton3 then
+ self:OnMouse3Up(input, processed)
+ end
+ end
+
+ function BaseCamera:OnPointerAction(wheel, pan, pinch, processed)
+ if processed then
+ return
+ end
+
+ if pan.Magnitude > 0 then
+ local inversionVector = Vector2.new(1, UserGameSettings:GetCameraYInvertValue())
+ local rotateDelta = self:InputTranslationToCameraAngleChange(PAN_SENSITIVITY*pan, MOUSE_SENSITIVITY)*inversionVector
+ self.rotateInput = self.rotateInput + rotateDelta
+ end
+
+ local zoom = self.currentSubjectDistance
+ local zoomDelta = -(wheel + pinch)
+
+ if abs(zoomDelta) > 0 then
+ local newZoom
+ if self.inFirstPerson and zoomDelta > 0 then
+ newZoom = FIRST_PERSON_DISTANCE_THRESHOLD
+ else
+ if FFlagUserFixZoomInZoomOutDiscrepancy then
+ if (zoomDelta > 0) then
+ newZoom = zoom + zoomDelta*(1 + zoom*ZOOM_SENSITIVITY_CURVATURE)
+ else
+ newZoom = (zoom + zoomDelta) / (1 - zoomDelta*ZOOM_SENSITIVITY_CURVATURE)
+ end
+ else
+ newZoom = zoom + zoomDelta*(1 + zoom*ZOOM_SENSITIVITY_CURVATURE)
+ end
+ end
+
+ self:SetCameraToSubjectDistance(newZoom)
+ end
+ end
+
+ function BaseCamera:ConnectInputEvents()
+ self.pointerActionConn = UserInputService.PointerAction:Connect(function(wheel, pan, pinch, processed)
+ self:OnPointerAction(wheel, pan, pinch, processed)
+ end)
+
+ self.inputBeganConn = UserInputService.InputBegan:Connect(function(input, processed)
+ self:OnInputBegan(input, processed)
+ end)
+
+ self.inputChangedConn = UserInputService.InputChanged:Connect(function(input, processed)
+ self:OnInputChanged(input, processed)
+ end)
+
+ self.inputEndedConn = UserInputService.InputEnded:Connect(function(input, processed)
+ self:OnInputEnded(input, processed)
+ end)
+
+ self.menuOpenedConn = GuiService.MenuOpened:connect(function()
+ self:ResetInputStates()
+ end)
+
+ self.gamepadConnectedConn = UserInputService.GamepadDisconnected:connect(function(gamepadEnum)
+ if self.activeGamepad ~= gamepadEnum then return end
+ self.activeGamepad = nil
+ self:AssignActivateGamepad()
+ end)
+
+ self.gamepadDisconnectedConn = UserInputService.GamepadConnected:connect(function(gamepadEnum)
+ if self.activeGamepad == nil then
+ self:AssignActivateGamepad()
+ end
+ end)
+
+ self:AssignActivateGamepad()
+ if not FFlagUserCameraToggle then
+ self:UpdateMouseBehavior()
+ end
+ end
+
+ function BaseCamera:BindContextActions()
+ self:BindGamepadInputActions()
+ self:BindKeyboardInputActions()
+ end
+
+ function BaseCamera:AssignActivateGamepad()
+ local connectedGamepads = UserInputService:GetConnectedGamepads()
+ if #connectedGamepads > 0 then
+ for i = 1, #connectedGamepads do
+ if self.activeGamepad == nil then
+ self.activeGamepad = connectedGamepads[i]
+ elseif connectedGamepads[i].Value < self.activeGamepad.Value then
+ self.activeGamepad = connectedGamepads[i]
+ end
+ end
+ end
+
+ if self.activeGamepad == nil then -- nothing is connected, at least set up for gamepad1
+ self.activeGamepad = Enum.UserInputType.Gamepad1
+ end
+ end
+
+ function BaseCamera:DisconnectInputEvents()
+ if self.inputBeganConn then
+ self.inputBeganConn:Disconnect()
+ self.inputBeganConn = nil
+ end
+ if self.inputChangedConn then
+ self.inputChangedConn:Disconnect()
+ self.inputChangedConn = nil
+ end
+ if self.inputEndedConn then
+ self.inputEndedConn:Disconnect()
+ self.inputEndedConn = nil
+ end
+ end
+
+ function BaseCamera:UnbindContextActions()
+ for i = 1, #self.boundContextActions do
+ ContextActionService:UnbindAction(self.boundContextActions[i])
+ end
+ self.boundContextActions = {}
+ end
+
+ function BaseCamera:Cleanup()
+ if self.pointerActionConn then
+ self.pointerActionConn:Disconnect()
+ self.pointerActionConn = nil
+ end
+ if self.menuOpenedConn then
+ self.menuOpenedConn:Disconnect()
+ self.menuOpenedConn = nil
+ end
+ if self.mouseLockToggleConn then
+ self.mouseLockToggleConn:Disconnect()
+ self.mouseLockToggleConn = nil
+ end
+ if self.gamepadConnectedConn then
+ self.gamepadConnectedConn:Disconnect()
+ self.gamepadConnectedConn = nil
+ end
+ if self.gamepadDisconnectedConn then
+ self.gamepadDisconnectedConn:Disconnect()
+ self.gamepadDisconnectedConn = nil
+ end
+ if self.subjectStateChangedConn then
+ self.subjectStateChangedConn:Disconnect()
+ self.subjectStateChangedConn = nil
+ end
+ if self.viewportSizeChangedConn then
+ self.viewportSizeChangedConn:Disconnect()
+ self.viewportSizeChangedConn = nil
+ end
+ if self.touchActivateConn then
+ self.touchActivateConn:Disconnect()
+ self.touchActivateConn = nil
+ end
+
+ self.turningLeft = false
+ self.turningRight = false
+ self.lastCameraTransform = nil
+ self.lastSubjectCFrame = nil
+ self.userPanningTheCamera = false
+ self.rotateInput = Vector2.new()
+ self.gamepadPanningCamera = Vector2.new(0,0)
+
+ -- Reset input states
+ self.startPos = nil
+ self.lastPos = nil
+ self.panBeginLook = nil
+ self.isRightMouseDown = false
+ self.isMiddleMouseDown = false
+
+ self.fingerTouches = {}
+ self.dynamicTouchInput = nil
+ self.numUnsunkTouches = 0
+
+ self.startingDiff = nil
+ self.pinchBeginZoom = nil
+
+ -- Unlock mouse for example if right mouse button was being held down
+ if UserInputService.MouseBehavior ~= Enum.MouseBehavior.LockCenter then
+ UserInputService.MouseBehavior = Enum.MouseBehavior.Default
+ end
+ end
+
+ -- This is called when settings menu is opened
+ function BaseCamera:ResetInputStates()
+ self.isRightMouseDown = false
+ self.isMiddleMouseDown = false
+ self:OnMousePanButtonReleased() -- this function doesn't seem to actually need parameters
+
+ if UserInputService.TouchEnabled then
+ --[[menu opening was causing serious touch issues
+ this should disable all active touch events if
+ they're active when menu opens.]]
+ for inputObject in pairs(self.fingerTouches) do
+ self.fingerTouches[inputObject] = nil
+ end
+ self.dynamicTouchInput = nil
+ self.panBeginLook = nil
+ self.startPos = nil
+ self.lastPos = nil
+ self.userPanningTheCamera = false
+ self.startingDiff = nil
+ self.pinchBeginZoom = nil
+ self.numUnsunkTouches = 0
+ end
+ end
+
+ function BaseCamera:GetGamepadPan(name, state, input)
+ if input.UserInputType == self.activeGamepad and input.KeyCode == Enum.KeyCode.Thumbstick2 then
+ -- if self.L3ButtonDown then
+ -- -- L3 Thumbstick is depressed, right stick controls dolly in/out
+ -- if (input.Position.Y > THUMBSTICK_DEADZONE) then
+ -- self.currentZoomSpeed = 0.96
+ -- elseif (input.Position.Y < -THUMBSTICK_DEADZONE) then
+ -- self.currentZoomSpeed = 1.04
+ -- else
+ -- self.currentZoomSpeed = 1.00
+ -- end
+ -- else
+ if state == Enum.UserInputState.Cancel then
+ self.gamepadPanningCamera = ZERO_VECTOR2
+ return
+ end
+
+ local inputVector = Vector2.new(input.Position.X, -input.Position.Y)
+ if inputVector.magnitude > THUMBSTICK_DEADZONE then
+ self.gamepadPanningCamera = Vector2.new(input.Position.X, -input.Position.Y)
+ else
+ self.gamepadPanningCamera = ZERO_VECTOR2
+ end
+ --end
+ return Enum.ContextActionResult.Sink
+ end
+ return Enum.ContextActionResult.Pass
+ end
+
+ function BaseCamera:DoKeyboardPanTurn(name, state, input)
+ if not self.hasGameLoaded and VRService.VREnabled then
+ return Enum.ContextActionResult.Pass
+ end
+
+ if state == Enum.UserInputState.Cancel then
+ self.turningLeft = false
+ self.turningRight = false
+ return Enum.ContextActionResult.Sink
+ end
+
+ if self.panBeginLook == nil and self.keyPanEnabled then
+ if input.KeyCode == Enum.KeyCode.Left then
+ self.turningLeft = state == Enum.UserInputState.Begin
+ elseif input.KeyCode == Enum.KeyCode.Right then
+ self.turningRight = state == Enum.UserInputState.Begin
+ end
+ return Enum.ContextActionResult.Sink
+ end
+ return Enum.ContextActionResult.Pass
+ end
+
+ function BaseCamera:DoPanRotateCamera(rotateAngle)
+ local angle = Util.RotateVectorByAngleAndRound(self:GetCameraLookVector() * Vector3.new(1,0,1), rotateAngle, math.pi*0.25)
+ if angle ~= 0 then
+ self.rotateInput = self.rotateInput + Vector2.new(angle, 0)
+ self.lastUserPanCamera = tick()
+ self.lastCameraTransform = nil
+ end
+ end
+
+ function BaseCamera:DoGamepadZoom(name, state, input)
+ if input.UserInputType == self.activeGamepad then
+ if input.KeyCode == Enum.KeyCode.ButtonR3 then
+ if state == Enum.UserInputState.Begin then
+ if self.distanceChangeEnabled then
+ local dist = self:GetCameraToSubjectDistance()
+
+ if dist > (GAMEPAD_ZOOM_STEP_2 + GAMEPAD_ZOOM_STEP_3)/2 then
+ self:SetCameraToSubjectDistance(GAMEPAD_ZOOM_STEP_2)
+ elseif dist > (GAMEPAD_ZOOM_STEP_1 + GAMEPAD_ZOOM_STEP_2)/2 then
+ self:SetCameraToSubjectDistance(GAMEPAD_ZOOM_STEP_1)
+ else
+ self:SetCameraToSubjectDistance(GAMEPAD_ZOOM_STEP_3)
+ end
+ end
+ end
+ elseif input.KeyCode == Enum.KeyCode.DPadLeft then
+ self.dpadLeftDown = (state == Enum.UserInputState.Begin)
+ elseif input.KeyCode == Enum.KeyCode.DPadRight then
+ self.dpadRightDown = (state == Enum.UserInputState.Begin)
+ end
+
+ if self.dpadLeftDown then
+ self.currentZoomSpeed = 1.04
+ elseif self.dpadRightDown then
+ self.currentZoomSpeed = 0.96
+ else
+ self.currentZoomSpeed = 1.00
+ end
+ return Enum.ContextActionResult.Sink
+ end
+ return Enum.ContextActionResult.Pass
+ -- elseif input.UserInputType == self.activeGamepad and input.KeyCode == Enum.KeyCode.ButtonL3 then
+ -- if (state == Enum.UserInputState.Begin) then
+ -- self.L3ButtonDown = true
+ -- elseif (state == Enum.UserInputState.End) then
+ -- self.L3ButtonDown = false
+ -- self.currentZoomSpeed = 1.00
+ -- end
+ -- end
+ end
+
+ function BaseCamera:DoKeyboardZoom(name, state, input)
+ if not self.hasGameLoaded and VRService.VREnabled then
+ return Enum.ContextActionResult.Pass
+ end
+
+ if state ~= Enum.UserInputState.Begin then
+ return Enum.ContextActionResult.Pass
+ end
+
+ if self.distanceChangeEnabled and player.CameraMode ~= Enum.CameraMode.LockFirstPerson then
+ if input.KeyCode == Enum.KeyCode.I then
+ self:SetCameraToSubjectDistance( self.currentSubjectDistance - 5 )
+ elseif input.KeyCode == Enum.KeyCode.O then
+ self:SetCameraToSubjectDistance( self.currentSubjectDistance + 5 )
+ end
+ return Enum.ContextActionResult.Sink
+ end
+ return Enum.ContextActionResult.Pass
+ end
+
+ function BaseCamera:BindAction(actionName, actionFunc, createTouchButton, ...)
+ table.insert(self.boundContextActions, actionName)
+ ContextActionService:BindActionAtPriority(actionName, actionFunc, createTouchButton,
+ CAMERA_ACTION_PRIORITY, ...)
+ end
+
+ function BaseCamera:BindGamepadInputActions()
+ self:BindAction("BaseCameraGamepadPan", function(name, state, input) return self:GetGamepadPan(name, state, input) end,
+ false, Enum.KeyCode.Thumbstick2)
+ self:BindAction("BaseCameraGamepadZoom", function(name, state, input) return self:DoGamepadZoom(name, state, input) end,
+ false, Enum.KeyCode.DPadLeft, Enum.KeyCode.DPadRight, Enum.KeyCode.ButtonR3)
+ end
+
+ function BaseCamera:BindKeyboardInputActions()
+ self:BindAction("BaseCameraKeyboardPanArrowKeys", function(name, state, input) return self:DoKeyboardPanTurn(name, state, input) end,
+ false, Enum.KeyCode.Left, Enum.KeyCode.Right)
+ self:BindAction("BaseCameraKeyboardZoom", function(name, state, input) return self:DoKeyboardZoom(name, state, input) end,
+ false, Enum.KeyCode.I, Enum.KeyCode.O)
+ end
+
+ local function isInDynamicThumbstickArea(input)
+ local playerGui = player:FindFirstChildOfClass("PlayerGui")
+ local touchGui = playerGui and playerGui:FindFirstChild("TouchGui")
+ local touchFrame = touchGui and touchGui:FindFirstChild("TouchControlFrame")
+ local thumbstickFrame = touchFrame and touchFrame:FindFirstChild("DynamicThumbstickFrame")
+
+ if not thumbstickFrame then
+ return false
+ end
+
+ local frameCornerTopLeft = thumbstickFrame.AbsolutePosition
+ local frameCornerBottomRight = frameCornerTopLeft + thumbstickFrame.AbsoluteSize
+ if input.Position.X >= frameCornerTopLeft.X and input.Position.Y >= frameCornerTopLeft.Y then
+ if input.Position.X <= frameCornerBottomRight.X and input.Position.Y <= frameCornerBottomRight.Y then
+ return true
+ end
+ end
+
+ return false
+ end
+
+ ---Adjusts the camera Y touch Sensitivity when moving away from the center and in the TOUCH_SENSITIVTY_ADJUST_AREA
+ function BaseCamera:AdjustTouchSensitivity(delta, sensitivity)
+ local cameraCFrame = game.Workspace.CurrentCamera and game.Workspace.CurrentCamera.CFrame
+ if not cameraCFrame then
+ return sensitivity
+ end
+ local currPitchAngle = cameraCFrame:ToEulerAnglesYXZ()
+
+ local multiplierY = TOUCH_SENSITIVTY_ADJUST_MAX_Y
+ if currPitchAngle > TOUCH_ADJUST_AREA_UP and delta.Y < 0 then
+ local fractionAdjust = (currPitchAngle - TOUCH_ADJUST_AREA_UP)/(MAX_Y - TOUCH_ADJUST_AREA_UP)
+ fractionAdjust = 1 - (1 - fractionAdjust)^3
+ multiplierY = TOUCH_SENSITIVTY_ADJUST_MAX_Y - fractionAdjust * (
+ TOUCH_SENSITIVTY_ADJUST_MAX_Y - TOUCH_SENSITIVTY_ADJUST_MIN_Y)
+ elseif currPitchAngle < TOUCH_ADJUST_AREA_DOWN and delta.Y > 0 then
+ local fractionAdjust = (currPitchAngle - TOUCH_ADJUST_AREA_DOWN)/(MIN_Y - TOUCH_ADJUST_AREA_DOWN)
+ fractionAdjust = 1 - (1 - fractionAdjust)^3
+ multiplierY = TOUCH_SENSITIVTY_ADJUST_MAX_Y - fractionAdjust * (
+ TOUCH_SENSITIVTY_ADJUST_MAX_Y - TOUCH_SENSITIVTY_ADJUST_MIN_Y)
+ end
+
+ return Vector2.new(
+ sensitivity.X,
+ sensitivity.Y * multiplierY
+ )
+ end
+
+ function BaseCamera:OnTouchBegan(input, processed)
+ local canUseDynamicTouch = self.isDynamicThumbstickEnabled and not processed
+ if canUseDynamicTouch then
+ if self.dynamicTouchInput == nil and isInDynamicThumbstickArea(input) then
+ -- First input in the dynamic thumbstick area should always be ignored for camera purposes
+ -- Even if the dynamic thumbstick does not process it immediately
+ self.dynamicTouchInput = input
+ return
+ end
+ self.fingerTouches[input] = processed
+ self.inputStartPositions[input] = input.Position
+ self.inputStartTimes[input] = tick()
+ self.numUnsunkTouches = self.numUnsunkTouches + 1
+ end
+ end
+
+ function BaseCamera:OnTouchChanged(input, processed)
+ if self.fingerTouches[input] == nil then
+ if self.isDynamicThumbstickEnabled then
+ return
+ end
+ self.fingerTouches[input] = processed
+ if not processed then
+ self.numUnsunkTouches = self.numUnsunkTouches + 1
+ end
+ end
+
+ if self.numUnsunkTouches == 1 then
+ if self.fingerTouches[input] == false then
+ self.panBeginLook = self.panBeginLook or self:GetCameraLookVector()
+ self.startPos = self.startPos or input.Position
+ self.lastPos = self.lastPos or self.startPos
+ self.userPanningTheCamera = true
+
+ local delta = input.Position - self.lastPos
+ delta = Vector2.new(delta.X, delta.Y * UserGameSettings:GetCameraYInvertValue())
+ if self.panEnabled then
+ local adjustedTouchSensitivity = TOUCH_SENSITIVTY
+ self:AdjustTouchSensitivity(delta, TOUCH_SENSITIVTY)
+
+ local desiredXYVector = self:InputTranslationToCameraAngleChange(delta, adjustedTouchSensitivity)
+ self.rotateInput = self.rotateInput + desiredXYVector
+ end
+ self.lastPos = input.Position
+ end
+ else
+ self.panBeginLook = nil
+ self.startPos = nil
+ self.lastPos = nil
+ self.userPanningTheCamera = false
+ end
+ if self.numUnsunkTouches == 2 then
+ local unsunkTouches = {}
+ for touch, wasSunk in pairs(self.fingerTouches) do
+ if not wasSunk then
+ table.insert(unsunkTouches, touch)
+ end
+ end
+ if #unsunkTouches == 2 then
+ local difference = (unsunkTouches[1].Position - unsunkTouches[2].Position).magnitude
+ if self.startingDiff and self.pinchBeginZoom then
+ local scale = difference / math.max(0.01, self.startingDiff)
+ local clampedScale = math.clamp(scale, 0.1, 10)
+ if self.distanceChangeEnabled then
+ self:SetCameraToSubjectDistance(self.pinchBeginZoom / clampedScale)
+ end
+ else
+ self.startingDiff = difference
+ self.pinchBeginZoom = self:GetCameraToSubjectDistance()
+ end
+ end
+ else
+ self.startingDiff = nil
+ self.pinchBeginZoom = nil
+ end
+ end
+
+ function BaseCamera:OnTouchEnded(input, processed)
+ if input == self.dynamicTouchInput then
+ self.dynamicTouchInput = nil
+ return
+ end
+
+ if self.fingerTouches[input] == false then
+ if self.numUnsunkTouches == 1 then
+ self.panBeginLook = nil
+ self.startPos = nil
+ self.lastPos = nil
+ self.userPanningTheCamera = false
+ elseif self.numUnsunkTouches == 2 then
+ self.startingDiff = nil
+ self.pinchBeginZoom = nil
+ end
+ end
+
+ if self.fingerTouches[input] ~= nil and self.fingerTouches[input] == false then
+ self.numUnsunkTouches = self.numUnsunkTouches - 1
+ end
+ self.fingerTouches[input] = nil
+ self.inputStartPositions[input] = nil
+ self.inputStartTimes[input] = nil
+ end
+
+ function BaseCamera:OnMouse2Down(input, processed)
+ if processed then return end
+
+ self.isRightMouseDown = true
+ self:OnMousePanButtonPressed(input, processed)
+ end
+
+ function BaseCamera:OnMouse2Up(input, processed)
+ self.isRightMouseDown = false
+ self:OnMousePanButtonReleased(input, processed)
+ end
+
+ function BaseCamera:OnMouse3Down(input, processed)
+ if processed then return end
+
+ self.isMiddleMouseDown = true
+ self:OnMousePanButtonPressed(input, processed)
+ end
+
+ function BaseCamera:OnMouse3Up(input, processed)
+ self.isMiddleMouseDown = false
+ self:OnMousePanButtonReleased(input, processed)
+ end
+
+ function BaseCamera:OnMouseMoved(input, processed)
+ if not self.hasGameLoaded and VRService.VREnabled then
+ return
+ end
+
+ local inputDelta = input.Delta
+ inputDelta = Vector2.new(inputDelta.X, inputDelta.Y * UserGameSettings:GetCameraYInvertValue())
+
+ local isInputPanning = FFlagUserCameraToggle and CameraInput.getPanning()
+ local isBeginLook = self.startPos and self.lastPos and self.panBeginLook
+ local isPanning = isBeginLook or self.inFirstPerson or self.inMouseLockedMode or isInputPanning
+
+ if self.panEnabled and isPanning then
+ local desiredXYVector = self:InputTranslationToCameraAngleChange(inputDelta, MOUSE_SENSITIVITY)
+ self.rotateInput = self.rotateInput + desiredXYVector
+ end
+
+ if self.startPos and self.lastPos and self.panBeginLook then
+ self.lastPos = self.lastPos + input.Delta
+ end
+ end
+
+ function BaseCamera:OnMousePanButtonPressed(input, processed)
+ if processed then return end
+ if not FFlagUserCameraToggle then
+ self:UpdateMouseBehavior()
+ end
+ self.panBeginLook = self.panBeginLook or self:GetCameraLookVector()
+ self.startPos = self.startPos or input.Position
+ self.lastPos = self.lastPos or self.startPos
+ self.userPanningTheCamera = true
+ end
+
+ function BaseCamera:OnMousePanButtonReleased(input, processed)
+ if not FFlagUserCameraToggle then
+ self:UpdateMouseBehavior()
+ end
+ if not (self.isRightMouseDown or self.isMiddleMouseDown) then
+ self.panBeginLook = nil
+ self.startPos = nil
+ self.lastPos = nil
+ self.userPanningTheCamera = false
+ end
+ end
+
+ function BaseCamera:UpdateMouseBehavior()
+ if FFlagUserCameraToggle and self.isCameraToggle then
+ CameraUI.setCameraModeToastEnabled(true)
+ CameraInput.enableCameraToggleInput()
+ CameraToggleStateController(self.inFirstPerson)
+ else
+ if FFlagUserCameraToggle then
+ CameraUI.setCameraModeToastEnabled(false)
+ CameraInput.disableCameraToggleInput()
+ end
+ -- first time transition to first person mode or mouse-locked third person
+ if self.inFirstPerson or self.inMouseLockedMode then
+ --UserGameSettings.RotationType = Enum.RotationType.CameraRelative
+ UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter
+ else
+ UserGameSettings.RotationType = Enum.RotationType.MovementRelative
+ if self.isRightMouseDown or self.isMiddleMouseDown then
+ UserInputService.MouseBehavior = Enum.MouseBehavior.LockCurrentPosition
+ else
+ UserInputService.MouseBehavior = Enum.MouseBehavior.Default
+ end
+ end
+ end
+ end
+
+ function BaseCamera:UpdateForDistancePropertyChange()
+ -- Calling this setter with the current value will force checking that it is still
+ -- in range after a change to the min/max distance limits
+ self:SetCameraToSubjectDistance(self.currentSubjectDistance)
+ end
+
+ function BaseCamera:SetCameraToSubjectDistance(desiredSubjectDistance)
+ local lastSubjectDistance = self.currentSubjectDistance
+
+ -- By default, camera modules will respect LockFirstPerson and override the currentSubjectDistance with 0
+ -- regardless of what Player.CameraMinZoomDistance is set to, so that first person can be made
+ -- available by the developer without needing to allow players to mousewheel dolly into first person.
+ -- Some modules will override this function to remove or change first-person capability.
+ if player.CameraMode == Enum.CameraMode.LockFirstPerson then
+ self.currentSubjectDistance = 0.5
+ if not self.inFirstPerson then
+ self:EnterFirstPerson()
+ end
+ else
+ local newSubjectDistance = math.clamp(desiredSubjectDistance, player.CameraMinZoomDistance, player.CameraMaxZoomDistance)
+ if newSubjectDistance < FIRST_PERSON_DISTANCE_THRESHOLD then
+ self.currentSubjectDistance = 0.5
+ if not self.inFirstPerson then
+ self:EnterFirstPerson()
+ end
+ else
+ self.currentSubjectDistance = newSubjectDistance
+ if self.inFirstPerson then
+ self:LeaveFirstPerson()
+ end
+ end
+ end
+
+ -- Pass target distance and zoom direction to the zoom controller
+ ZoomController.SetZoomParameters(self.currentSubjectDistance, math.sign(desiredSubjectDistance - lastSubjectDistance))
+
+ -- Returned only for convenience to the caller to know the outcome
+ return self.currentSubjectDistance
+ end
+
+ function BaseCamera:SetCameraType( cameraType )
+ --Used by derived classes
+ self.cameraType = cameraType
+ end
+
+ function BaseCamera:GetCameraType()
+ return self.cameraType
+ end
+
+ -- Movement mode standardized to Enum.ComputerCameraMovementMode values
+ function BaseCamera:SetCameraMovementMode( cameraMovementMode )
+ self.cameraMovementMode = cameraMovementMode
+ end
+
+ function BaseCamera:GetCameraMovementMode()
+ return self.cameraMovementMode
+ end
+
+ function BaseCamera:SetIsMouseLocked(mouseLocked)
+ self.inMouseLockedMode = mouseLocked
+ if not FFlagUserCameraToggle then
+ self:UpdateMouseBehavior()
+ end
+ end
+
+ function BaseCamera:GetIsMouseLocked()
+ return self.inMouseLockedMode
+ end
+
+ function BaseCamera:SetMouseLockOffset(offsetVector)
+ self.mouseLockOffset = offsetVector
+ end
+
+ function BaseCamera:GetMouseLockOffset()
+ return self.mouseLockOffset
+ end
+
+ function BaseCamera:InFirstPerson()
+ return self.inFirstPerson
+ end
+
+ function BaseCamera:EnterFirstPerson()
+ -- Overridden in ClassicCamera, the only module which supports FirstPerson
+ end
+
+ function BaseCamera:LeaveFirstPerson()
+ -- Overridden in ClassicCamera, the only module which supports FirstPerson
+ end
+
+ -- Nominal distance, set by dollying in and out with the mouse wheel or equivalent, not measured distance
+ function BaseCamera:GetCameraToSubjectDistance()
+ return self.currentSubjectDistance
+ end
+
+ -- Actual measured distance to the camera Focus point, which may be needed in special circumstances, but should
+ -- never be used as the starting point for updating the nominal camera-to-subject distance (self.currentSubjectDistance)
+ -- since that is a desired target value set only by mouse wheel (or equivalent) input, PopperCam, and clamped to min max camera distance
+ function BaseCamera:GetMeasuredDistanceToFocus()
+ local camera = game.Workspace.CurrentCamera
+ if camera then
+ return (camera.CoordinateFrame.p - camera.Focus.p).magnitude
+ end
+ return nil
+ end
+
+ function BaseCamera:GetCameraLookVector()
+ return game.Workspace.CurrentCamera and game.Workspace.CurrentCamera.CFrame.lookVector or UNIT_Z
+ end
+
+ -- Replacements for RootCamera:RotateCamera() which did not actually rotate the camera
+ -- suppliedLookVector is not normally passed in, it's used only by Watch camera
+ function BaseCamera:CalculateNewLookCFrame(suppliedLookVector)
+ local currLookVector = suppliedLookVector or self:GetCameraLookVector()
+ local currPitchAngle = math.asin(currLookVector.y)
+ local yTheta = math.clamp(self.rotateInput.y, -MAX_Y + currPitchAngle, -MIN_Y + currPitchAngle)
+ local constrainedRotateInput = Vector2.new(self.rotateInput.x, yTheta)
+ local startCFrame = CFrame.new(ZERO_VECTOR3, currLookVector)
+ local newLookCFrame = CFrame.Angles(0, -constrainedRotateInput.x, 0) * startCFrame * CFrame.Angles(-constrainedRotateInput.y,0,0)
+ return newLookCFrame
+ end
+ function BaseCamera:CalculateNewLookVector(suppliedLookVector)
+ local newLookCFrame = self:CalculateNewLookCFrame(suppliedLookVector)
+ return newLookCFrame.lookVector
+ end
+
+ function BaseCamera:CalculateNewLookVectorVR()
+ local subjectPosition = self:GetSubjectPosition()
+ local vecToSubject = (subjectPosition - game.Workspace.CurrentCamera.CFrame.p)
+ local currLookVector = (vecToSubject * X1_Y0_Z1).unit
+ local vrRotateInput = Vector2.new(self.rotateInput.x, 0)
+ local startCFrame = CFrame.new(ZERO_VECTOR3, currLookVector)
+ local yawRotatedVector = (CFrame.Angles(0, -vrRotateInput.x, 0) * startCFrame * CFrame.Angles(-vrRotateInput.y,0,0)).lookVector
+ return (yawRotatedVector * X1_Y0_Z1).unit
+ end
+
+ function BaseCamera:GetHumanoid()
+ local character = player and player.Character
+ if character then
+ local resultHumanoid = self.humanoidCache[player]
+ if resultHumanoid and resultHumanoid.Parent == character then
+ return resultHumanoid
+ else
+ self.humanoidCache[player] = nil -- Bust Old Cache
+ local humanoid = character:FindFirstChildOfClass("Humanoid")
+ if humanoid then
+ self.humanoidCache[player] = humanoid
+ end
+ return humanoid
+ end
+ end
+ return nil
+ end
+
+ function BaseCamera:GetHumanoidPartToFollow(humanoid, humanoidStateType)
+ if humanoidStateType == Enum.HumanoidStateType.Dead then
+ local character = humanoid.Parent
+ if character then
+ return character:FindFirstChild("Head") or humanoid.Torso
+ else
+ return humanoid.Torso
+ end
+ else
+ return humanoid.Torso
+ end
+ end
+
+ function BaseCamera:UpdateGamepad()
+ local gamepadPan = self.gamepadPanningCamera
+ if gamepadPan and (self.hasGameLoaded or not VRService.VREnabled) then
+ gamepadPan = Util.GamepadLinearToCurve(gamepadPan)
+ local currentTime = tick()
+ if gamepadPan.X ~= 0 or gamepadPan.Y ~= 0 then
+ self.userPanningTheCamera = true
+ elseif gamepadPan == ZERO_VECTOR2 then
+ self.lastThumbstickRotate = nil
+ if self.lastThumbstickPos == ZERO_VECTOR2 then
+ self.currentSpeed = 0
+ end
+ end
+
+ local finalConstant = 0
+
+ if self.lastThumbstickRotate then
+ if VRService.VREnabled then
+ self.currentSpeed = self.vrMaxSpeed
+ else
+ local elapsedTime = (currentTime - self.lastThumbstickRotate) * 10
+ self.currentSpeed = self.currentSpeed + (self.maxSpeed * ((elapsedTime*elapsedTime)/self.numOfSeconds))
+
+ if self.currentSpeed > self.maxSpeed then self.currentSpeed = self.maxSpeed end
+
+ if self.lastVelocity then
+ local velocity = (gamepadPan - self.lastThumbstickPos)/(currentTime - self.lastThumbstickRotate)
+ local velocityDeltaMag = (velocity - self.lastVelocity).magnitude
+
+ if velocityDeltaMag > 12 then
+ self.currentSpeed = self.currentSpeed * (20/velocityDeltaMag)
+ if self.currentSpeed > self.maxSpeed then self.currentSpeed = self.maxSpeed end
+ end
+ end
+ end
+
+ finalConstant = UserGameSettings.GamepadCameraSensitivity * self.currentSpeed
+ self.lastVelocity = (gamepadPan - self.lastThumbstickPos)/(currentTime - self.lastThumbstickRotate)
+ end
+
+ self.lastThumbstickPos = gamepadPan
+ self.lastThumbstickRotate = currentTime
+
+ return Vector2.new( gamepadPan.X * finalConstant, gamepadPan.Y * finalConstant * self.ySensitivity * UserGameSettings:GetCameraYInvertValue())
+ end
+
+ return ZERO_VECTOR2
+ end
+
+ -- [[ VR Support Section ]] --
+
+ function BaseCamera:ApplyVRTransform()
+ if not VRService.VREnabled then
+ return
+ end
+
+ --we only want this to happen in first person VR
+ local rootJoint = self.humanoidRootPart and self.humanoidRootPart:FindFirstChild("RootJoint")
+ if not rootJoint then
+ return
+ end
+
+ local cameraSubject = game.Workspace.CurrentCamera.CameraSubject
+ local isInVehicle = cameraSubject and cameraSubject:IsA("VehicleSeat")
+
+ if self.inFirstPerson and not isInVehicle then
+ local vrFrame = VRService:GetUserCFrame(Enum.UserCFrame.Head)
+ local vrRotation = vrFrame - vrFrame.p
+ rootJoint.C0 = CFrame.new(vrRotation:vectorToObjectSpace(vrFrame.p)) * CFrame.new(0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 1, 0)
+ else
+ rootJoint.C0 = CFrame.new(0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 1, 0)
+ end
+ end
+
+ function BaseCamera:IsInFirstPerson()
+ return self.inFirstPerson
+ end
+
+ function BaseCamera:ShouldUseVRRotation()
+ if not VRService.VREnabled then
+ return false
+ end
+
+ if not self.VRRotationIntensityAvailable and tick() - self.lastVRRotationIntensityCheckTime < 1 then
+ return false
+ end
+
+ local success, vrRotationIntensity = pcall(function() return StarterGui:GetCore("VRRotationIntensity") end)
+ self.VRRotationIntensityAvailable = success and vrRotationIntensity ~= nil
+ self.lastVRRotationIntensityCheckTime = tick()
+
+ self.shouldUseVRRotation = success and vrRotationIntensity ~= nil and vrRotationIntensity ~= "Smooth"
+
+ return self.shouldUseVRRotation
+ end
+
+ function BaseCamera:GetVRRotationInput()
+ local vrRotateSum = ZERO_VECTOR2
+ local success, vrRotationIntensity = pcall(function() return StarterGui:GetCore("VRRotationIntensity") end)
+
+ if not success then
+ return
+ end
+
+ local vrGamepadRotation = self.GamepadPanningCamera or ZERO_VECTOR2
+ local delayExpired = (tick() - self.lastVRRotationTime) >= self:GetRepeatDelayValue(vrRotationIntensity)
+
+ if math.abs(vrGamepadRotation.x) >= self:GetActivateValue() then
+ if (delayExpired or not self.vrRotateKeyCooldown[Enum.KeyCode.Thumbstick2]) then
+ local sign = 1
+ if vrGamepadRotation.x < 0 then
+ sign = -1
+ end
+ vrRotateSum = vrRotateSum + self:GetRotateAmountValue(vrRotationIntensity) * sign
+ self.vrRotateKeyCooldown[Enum.KeyCode.Thumbstick2] = true
+ end
+ elseif math.abs(vrGamepadRotation.x) < self:GetActivateValue() - 0.1 then
+ self.vrRotateKeyCooldown[Enum.KeyCode.Thumbstick2] = nil
+ end
+ if self.turningLeft then
+ if delayExpired or not self.vrRotateKeyCooldown[Enum.KeyCode.Left] then
+ vrRotateSum = vrRotateSum - self:GetRotateAmountValue(vrRotationIntensity)
+ self.vrRotateKeyCooldown[Enum.KeyCode.Left] = true
+ end
+ else
+ self.vrRotateKeyCooldown[Enum.KeyCode.Left] = nil
+ end
+ if self.turningRight then
+ if (delayExpired or not self.vrRotateKeyCooldown[Enum.KeyCode.Right]) then
+ vrRotateSum = vrRotateSum + self:GetRotateAmountValue(vrRotationIntensity)
+ self.vrRotateKeyCooldown[Enum.KeyCode.Right] = true
+ end
+ else
+ self.vrRotateKeyCooldown[Enum.KeyCode.Right] = nil
+ end
+
+ if vrRotateSum ~= ZERO_VECTOR2 then
+ self.lastVRRotationTime = tick()
+ end
+
+ return vrRotateSum
+ end
+
+ function BaseCamera:CancelCameraFreeze(keepConstraints)
+ if not keepConstraints then
+ self.cameraTranslationConstraints = Vector3.new(self.cameraTranslationConstraints.x, 1, self.cameraTranslationConstraints.z)
+ end
+ if self.cameraFrozen then
+ self.trackingHumanoid = nil
+ self.cameraFrozen = false
+ end
+ end
+
+ function BaseCamera:StartCameraFreeze(subjectPosition, humanoidToTrack)
+ if not self.cameraFrozen then
+ self.humanoidJumpOrigin = subjectPosition
+ self.trackingHumanoid = humanoidToTrack
+ self.cameraTranslationConstraints = Vector3.new(self.cameraTranslationConstraints.x, 0, self.cameraTranslationConstraints.z)
+ self.cameraFrozen = true
+ end
+ end
+
+ function BaseCamera:OnNewCameraSubject()
+ if self.subjectStateChangedConn then
+ self.subjectStateChangedConn:Disconnect()
+ self.subjectStateChangedConn = nil
+ end
+
+ local humanoid = workspace.CurrentCamera and workspace.CurrentCamera.CameraSubject
+ if self.trackingHumanoid ~= humanoid then
+ self:CancelCameraFreeze()
+ end
+ if humanoid and humanoid:IsA("Humanoid") then
+ self.subjectStateChangedConn = humanoid.StateChanged:Connect(function(oldState, newState)
+ if VRService.VREnabled and newState == Enum.HumanoidStateType.Jumping and not self.inFirstPerson then
+ self:StartCameraFreeze(self:GetSubjectPosition(), humanoid)
+ elseif newState ~= Enum.HumanoidStateType.Jumping and newState ~= Enum.HumanoidStateType.Freefall then
+ self:CancelCameraFreeze(true)
+ end
+ end)
+ end
+ end
+
+ function BaseCamera:GetVRFocus(subjectPosition, timeDelta)
+ local lastFocus = self.LastCameraFocus or subjectPosition
+ if not self.cameraFrozen then
+ self.cameraTranslationConstraints = Vector3.new(self.cameraTranslationConstraints.x, math.min(1, self.cameraTranslationConstraints.y + 0.42 * timeDelta), self.cameraTranslationConstraints.z)
+ end
+
+ local newFocus
+ if self.cameraFrozen and self.humanoidJumpOrigin and self.humanoidJumpOrigin.y > lastFocus.y then
+ newFocus = CFrame.new(Vector3.new(subjectPosition.x, math.min(self.humanoidJumpOrigin.y, lastFocus.y + 5 * timeDelta), subjectPosition.z))
+ else
+ newFocus = CFrame.new(Vector3.new(subjectPosition.x, lastFocus.y, subjectPosition.z):lerp(subjectPosition, self.cameraTranslationConstraints.y))
+ end
+
+ if self.cameraFrozen then
+ -- No longer in 3rd person
+ if self.inFirstPerson then -- not VRService.VREnabled
+ self:CancelCameraFreeze()
+ end
+ -- This case you jumped off a cliff and want to keep your character in view
+ -- 0.5 is to fix floating point error when not jumping off cliffs
+ if self.humanoidJumpOrigin and subjectPosition.y < (self.humanoidJumpOrigin.y - 0.5) then
+ self:CancelCameraFreeze()
+ end
+ end
+
+ return newFocus
+ end
+
+ function BaseCamera:GetRotateAmountValue(vrRotationIntensity)
+ vrRotationIntensity = vrRotationIntensity or StarterGui:GetCore("VRRotationIntensity")
+ if vrRotationIntensity then
+ if vrRotationIntensity == "Low" then
+ return VR_LOW_INTENSITY_ROTATION
+ elseif vrRotationIntensity == "High" then
+ return VR_HIGH_INTENSITY_ROTATION
+ end
+ end
+ return ZERO_VECTOR2
+ end
+
+ function BaseCamera:GetRepeatDelayValue(vrRotationIntensity)
+ vrRotationIntensity = vrRotationIntensity or StarterGui:GetCore("VRRotationIntensity")
+ if vrRotationIntensity then
+ if vrRotationIntensity == "Low" then
+ return VR_LOW_INTENSITY_REPEAT
+ elseif vrRotationIntensity == "High" then
+ return VR_HIGH_INTENSITY_REPEAT
+ end
+ end
+ return 0
+ end
+
+ function BaseCamera:Update(dt)
+ error("BaseCamera:Update() This is a virtual function that should never be getting called.", 2)
+ end
+
+ BaseCamera.UpCFrame = CFrame.new()
+
+ function BaseCamera:UpdateUpCFrame(cf)
+ self.UpCFrame = cf
+ end
+ local ZERO = Vector3.new(0, 0, 0)
+ function BaseCamera:CalculateNewLookCFrame(suppliedLookVector)
+ local currLookVector = suppliedLookVector or self:GetCameraLookVector()
+ currLookVector = self.UpCFrame:VectorToObjectSpace(currLookVector)
+
+ local currPitchAngle = math.asin(currLookVector.y)
+ local yTheta = math.clamp(self.rotateInput.y, -MAX_Y + currPitchAngle, -MIN_Y + currPitchAngle)
+ local constrainedRotateInput = Vector2.new(self.rotateInput.x, yTheta)
+ local startCFrame = CFrame.new(ZERO, currLookVector)
+ local newLookCFrame = CFrame.Angles(0, -constrainedRotateInput.x, 0) * startCFrame * CFrame.Angles(-constrainedRotateInput.y,0,0)
+
+ return newLookCFrame
+ end
+
+ return BaseCamera
+end
+
+function _BaseOcclusion()
+ --[[ The Module ]]--
+ local BaseOcclusion = {}
+ BaseOcclusion.__index = BaseOcclusion
+ setmetatable(BaseOcclusion, {
+ __call = function(_, ...)
+ return BaseOcclusion.new(...)
+ end
+ })
+
+ function BaseOcclusion.new()
+ local self = setmetatable({}, BaseOcclusion)
+ return self
+ end
+
+ -- Called when character is added
+ function BaseOcclusion:CharacterAdded(char, player)
+ end
+
+ -- Called when character is about to be removed
+ function BaseOcclusion:CharacterRemoving(char, player)
+ end
+
+ function BaseOcclusion:OnCameraSubjectChanged(newSubject)
+ end
+
+ --[[ Derived classes are required to override and implement all of the following functions ]]--
+ function BaseOcclusion:GetOcclusionMode()
+ -- Must be overridden in derived classes to return an Enum.DevCameraOcclusionMode value
+ warn("BaseOcclusion GetOcclusionMode must be overridden by derived classes")
+ return nil
+ end
+
+ function BaseOcclusion:Enable(enabled)
+ warn("BaseOcclusion Enable must be overridden by derived classes")
+ end
+
+ function BaseOcclusion:Update(dt, desiredCameraCFrame, desiredCameraFocus)
+ warn("BaseOcclusion Update must be overridden by derived classes")
+ return desiredCameraCFrame, desiredCameraFocus
+ end
+
+ return BaseOcclusion
+end
+
+function _Popper()
+
+ local Players = game:GetService("Players")
+
+ local camera = game.Workspace.CurrentCamera
+
+ local min = math.min
+ local tan = math.tan
+ local rad = math.rad
+ local inf = math.huge
+ local ray = Ray.new
+
+ local function getTotalTransparency(part)
+ return 1 - (1 - part.Transparency)*(1 - part.LocalTransparencyModifier)
+ end
+
+ local function eraseFromEnd(t, toSize)
+ for i = #t, toSize + 1, -1 do
+ t[i] = nil
+ end
+ end
+
+ local nearPlaneZ, projX, projY do
+ local function updateProjection()
+ local fov = rad(camera.FieldOfView)
+ local view = camera.ViewportSize
+ local ar = view.X/view.Y
+
+ projY = 2*tan(fov/2)
+ projX = ar*projY
+ end
+
+ camera:GetPropertyChangedSignal("FieldOfView"):Connect(updateProjection)
+ camera:GetPropertyChangedSignal("ViewportSize"):Connect(updateProjection)
+
+ updateProjection()
+
+ nearPlaneZ = camera.NearPlaneZ
+ camera:GetPropertyChangedSignal("NearPlaneZ"):Connect(function()
+ nearPlaneZ = camera.NearPlaneZ
+ end)
+ end
+
+ local blacklist = {} do
+ local charMap = {}
+
+ local function refreshIgnoreList()
+ local n = 1
+ blacklist = {}
+ for _, character in pairs(charMap) do
+ blacklist[n] = character
+ n = n + 1
+ end
+ end
+
+ local function playerAdded(player)
+ local function characterAdded(character)
+ charMap[player] = character
+ refreshIgnoreList()
+ end
+ local function characterRemoving()
+ charMap[player] = nil
+ refreshIgnoreList()
+ end
+
+ player.CharacterAdded:Connect(characterAdded)
+ player.CharacterRemoving:Connect(characterRemoving)
+ if player.Character then
+ characterAdded(player.Character)
+ end
+ end
+
+ local function playerRemoving(player)
+ charMap[player] = nil
+ refreshIgnoreList()
+ end
+
+ Players.PlayerAdded:Connect(playerAdded)
+ Players.PlayerRemoving:Connect(playerRemoving)
+
+ for _, player in ipairs(Players:GetPlayers()) do
+ playerAdded(player)
+ end
+ refreshIgnoreList()
+ end
+
+ --------------------------------------------------------------------------------------------
+ -- Popper uses the level geometry find an upper bound on subject-to-camera distance.
+ --
+ -- Hard limits are applied immediately and unconditionally. They are generally caused
+ -- when level geometry intersects with the near plane (with exceptions, see below).
+ --
+ -- Soft limits are only applied under certain conditions.
+ -- They are caused when level geometry occludes the subject without actually intersecting
+ -- with the near plane at the target distance.
+ --
+ -- Soft limits can be promoted to hard limits and hard limits can be demoted to soft limits.
+ -- We usually don"t want the latter to happen.
+ --
+ -- A soft limit will be promoted to a hard limit if an obstruction
+ -- lies between the current and target camera positions.
+ --------------------------------------------------------------------------------------------
+
+ local subjectRoot
+ local subjectPart
+
+ camera:GetPropertyChangedSignal("CameraSubject"):Connect(function()
+ local subject = camera.CameraSubject
+ if subject:IsA("Humanoid") then
+ subjectPart = subject.RootPart
+ elseif subject:IsA("BasePart") then
+ subjectPart = subject
+ else
+ subjectPart = nil
+ end
+ end)
+
+ local function canOcclude(part)
+ -- Occluders must be:
+ -- 1. Opaque
+ -- 2. Interactable
+ -- 3. Not in the same assembly as the subject
+
+ return
+ getTotalTransparency(part) < 0.25 and
+ part.CanCollide and
+ subjectRoot ~= (part:GetRootPart() or part) and
+ not part:IsA("TrussPart")
+ end
+
+ -- Offsets for the volume visibility test
+ local SCAN_SAMPLE_OFFSETS = {
+ Vector2.new( 0.4, 0.0),
+ Vector2.new(-0.4, 0.0),
+ Vector2.new( 0.0,-0.4),
+ Vector2.new( 0.0, 0.4),
+ Vector2.new( 0.0, 0.2),
+ }
+
+ --------------------------------------------------------------------------------
+ -- Piercing raycasts
+
+ local function getCollisionPoint(origin, dir)
+ local originalSize = #blacklist
+ repeat
+ local hitPart, hitPoint = workspace:FindPartOnRayWithIgnoreList(
+ ray(origin, dir), blacklist, false, true
+ )
+
+ if hitPart then
+ if hitPart.CanCollide then
+ eraseFromEnd(blacklist, originalSize)
+ return hitPoint, true
+ end
+ blacklist[#blacklist + 1] = hitPart
+ end
+ until not hitPart
+
+ eraseFromEnd(blacklist, originalSize)
+ return origin + dir, false
+ end
+
+ --------------------------------------------------------------------------------
+
+ local function queryPoint(origin, unitDir, dist, lastPos)
+ debug.profilebegin("queryPoint")
+
+ local originalSize = #blacklist
+
+ dist = dist + nearPlaneZ
+ local target = origin + unitDir*dist
+
+ local softLimit = inf
+ local hardLimit = inf
+ local movingOrigin = origin
+
+ repeat
+ local entryPart, entryPos = workspace:FindPartOnRayWithIgnoreList(ray(movingOrigin, target - movingOrigin), blacklist, false, true)
+
+ if entryPart then
+ if canOcclude(entryPart) then
+ local wl = {entryPart}
+ local exitPart = workspace:FindPartOnRayWithWhitelist(ray(target, entryPos - target), wl, true)
+
+ local lim = (entryPos - origin).Magnitude
+
+ if exitPart then
+ local promote = false
+ if lastPos then
+ promote =
+ workspace:FindPartOnRayWithWhitelist(ray(lastPos, target - lastPos), wl, true) or
+ workspace:FindPartOnRayWithWhitelist(ray(target, lastPos - target), wl, true)
+ end
+
+ if promote then
+ -- Ostensibly a soft limit, but the camera has passed through it in the last frame, so promote to a hard limit.
+ hardLimit = lim
+ elseif dist < softLimit then
+ -- Trivial soft limit
+ softLimit = lim
+ end
+ else
+ -- Trivial hard limit
+ hardLimit = lim
+ end
+ end
+
+ blacklist[#blacklist + 1] = entryPart
+ movingOrigin = entryPos - unitDir*1e-3
+ end
+ until hardLimit < inf or not entryPart
+
+ eraseFromEnd(blacklist, originalSize)
+
+ debug.profileend()
+ return softLimit - nearPlaneZ, hardLimit - nearPlaneZ
+ end
+
+ local function queryViewport(focus, dist)
+ debug.profilebegin("queryViewport")
+
+ local fP = focus.p
+ local fX = focus.rightVector
+ local fY = focus.upVector
+ local fZ = -focus.lookVector
+
+ local viewport = camera.ViewportSize
+
+ local hardBoxLimit = inf
+ local softBoxLimit = inf
+
+ -- Center the viewport on the PoI, sweep points on the edge towards the target, and take the minimum limits
+ for viewX = 0, 1 do
+ local worldX = fX*((viewX - 0.5)*projX)
+
+ for viewY = 0, 1 do
+ local worldY = fY*((viewY - 0.5)*projY)
+
+ local origin = fP + nearPlaneZ*(worldX + worldY)
+ local lastPos = camera:ViewportPointToRay(
+ viewport.x*viewX,
+ viewport.y*viewY
+ ).Origin
+
+ local softPointLimit, hardPointLimit = queryPoint(origin, fZ, dist, lastPos)
+
+ if hardPointLimit < hardBoxLimit then
+ hardBoxLimit = hardPointLimit
+ end
+ if softPointLimit < softBoxLimit then
+ softBoxLimit = softPointLimit
+ end
+ end
+ end
+ debug.profileend()
+
+ return softBoxLimit, hardBoxLimit
+ end
+
+ local function testPromotion(focus, dist, focusExtrapolation)
+ debug.profilebegin("testPromotion")
+
+ local fP = focus.p
+ local fX = focus.rightVector
+ local fY = focus.upVector
+ local fZ = -focus.lookVector
+
+ do
+ -- Dead reckoning the camera rotation and focus
+ debug.profilebegin("extrapolate")
+
+ local SAMPLE_DT = 0.0625
+ local SAMPLE_MAX_T = 1.25
+
+ local maxDist = (getCollisionPoint(fP, focusExtrapolation.posVelocity*SAMPLE_MAX_T) - fP).Magnitude
+ -- Metric that decides how many samples to take
+ local combinedSpeed = focusExtrapolation.posVelocity.magnitude
+
+ for dt = 0, min(SAMPLE_MAX_T, focusExtrapolation.rotVelocity.magnitude + maxDist/combinedSpeed), SAMPLE_DT do
+ local cfDt = focusExtrapolation.extrapolate(dt) -- Extrapolated CFrame at time dt
+
+ if queryPoint(cfDt.p, -cfDt.lookVector, dist) >= dist then
+ return false
+ end
+ end
+
+ debug.profileend()
+ end
+
+ do
+ -- Test screen-space offsets from the focus for the presence of soft limits
+ debug.profilebegin("testOffsets")
+
+ for _, offset in ipairs(SCAN_SAMPLE_OFFSETS) do
+ local scaledOffset = offset
+ local pos = getCollisionPoint(fP, fX*scaledOffset.x + fY*scaledOffset.y)
+ if queryPoint(pos, (fP + fZ*dist - pos).Unit, dist) == inf then
+ return false
+ end
+ end
+
+ debug.profileend()
+ end
+
+ debug.profileend()
+ return true
+ end
+
+ local function Popper(focus, targetDist, focusExtrapolation)
+ debug.profilebegin("popper")
+
+ subjectRoot = subjectPart and subjectPart:GetRootPart() or subjectPart
+
+ local dist = targetDist
+ local soft, hard = queryViewport(focus, targetDist)
+ if hard < dist then
+ dist = hard
+ end
+ if soft < dist and testPromotion(focus, targetDist, focusExtrapolation) then
+ dist = soft
+ end
+
+ subjectRoot = nil
+
+ debug.profileend()
+ return dist
+ end
+
+ return Popper
+end
+
+function _ZoomController()
+ local ZOOM_STIFFNESS = 4.5
+ local ZOOM_DEFAULT = 12.5
+ local ZOOM_ACCELERATION = 0.0375
+
+ local MIN_FOCUS_DIST = 0.5
+ local DIST_OPAQUE = 1
+
+ local Popper = _Popper()
+
+ local clamp = math.clamp
+ local exp = math.exp
+ local min = math.min
+ local max = math.max
+ local pi = math.pi
+
+ local cameraMinZoomDistance, cameraMaxZoomDistance do
+ local Player = game:GetService("Players").LocalPlayer
+
+ local function updateBounds()
+ cameraMinZoomDistance = Player.CameraMinZoomDistance
+ cameraMaxZoomDistance = Player.CameraMaxZoomDistance
+ end
+
+ updateBounds()
+
+ Player:GetPropertyChangedSignal("CameraMinZoomDistance"):Connect(updateBounds)
+ Player:GetPropertyChangedSignal("CameraMaxZoomDistance"):Connect(updateBounds)
+ end
+
+ local ConstrainedSpring = {} do
+ ConstrainedSpring.__index = ConstrainedSpring
+
+ function ConstrainedSpring.new(freq, x, minValue, maxValue)
+ x = clamp(x, minValue, maxValue)
+ return setmetatable({
+ freq = freq, -- Undamped frequency (Hz)
+ x = x, -- Current position
+ v = 0, -- Current velocity
+ minValue = minValue, -- Minimum bound
+ maxValue = maxValue, -- Maximum bound
+ goal = x, -- Goal position
+ }, ConstrainedSpring)
+ end
+
+ function ConstrainedSpring:Step(dt)
+ local freq = self.freq*2*pi -- Convert from Hz to rad/s
+ local x = self.x
+ local v = self.v
+ local minValue = self.minValue
+ local maxValue = self.maxValue
+ local goal = self.goal
+
+ -- Solve the spring ODE for position and velocity after time t, assuming critical damping:
+ -- 2*f*x'[t] + x''[t] = f^2*(g - x[t])
+ -- Knowns are x[0] and x'[0].
+ -- Solve for x[t] and x'[t].
+
+ local offset = goal - x
+ local step = freq*dt
+ local decay = exp(-step)
+
+ local x1 = goal + (v*dt - offset*(step + 1))*decay
+ local v1 = ((offset*freq - v)*step + v)*decay
+
+ -- Constrain
+ if x1 < minValue then
+ x1 = minValue
+ v1 = 0
+ elseif x1 > maxValue then
+ x1 = maxValue
+ v1 = 0
+ end
+
+ self.x = x1
+ self.v = v1
+
+ return x1
+ end
+ end
+
+ local zoomSpring = ConstrainedSpring.new(ZOOM_STIFFNESS, ZOOM_DEFAULT, MIN_FOCUS_DIST, cameraMaxZoomDistance)
+
+ local function stepTargetZoom(z, dz, zoomMin, zoomMax)
+ z = clamp(z + dz*(1 + z*ZOOM_ACCELERATION), zoomMin, zoomMax)
+ if z < DIST_OPAQUE then
+ z = dz <= 0 and zoomMin or DIST_OPAQUE
+ end
+ return z
+ end
+
+ local zoomDelta = 0
+
+ local Zoom = {} do
+ function Zoom.Update(renderDt, focus, extrapolation)
+ local poppedZoom = math.huge
+
+ if zoomSpring.goal > DIST_OPAQUE then
+ -- Make a pessimistic estimate of zoom distance for this step without accounting for poppercam
+ local maxPossibleZoom = max(
+ zoomSpring.x,
+ stepTargetZoom(zoomSpring.goal, zoomDelta, cameraMinZoomDistance, cameraMaxZoomDistance)
+ )
+
+ -- Run the Popper algorithm on the feasible zoom range, [MIN_FOCUS_DIST, maxPossibleZoom]
+ poppedZoom = Popper(
+ focus*CFrame.new(0, 0, MIN_FOCUS_DIST),
+ maxPossibleZoom - MIN_FOCUS_DIST,
+ extrapolation
+ ) + MIN_FOCUS_DIST
+ end
+
+ zoomSpring.minValue = MIN_FOCUS_DIST
+ zoomSpring.maxValue = min(cameraMaxZoomDistance, poppedZoom)
+
+ return zoomSpring:Step(renderDt)
+ end
+
+ function Zoom.SetZoomParameters(targetZoom, newZoomDelta)
+ zoomSpring.goal = targetZoom
+ zoomDelta = newZoomDelta
+ end
+ end
+
+ return Zoom
+end
+
+function _MouseLockController()
+ --[[ Constants ]]--
+ local DEFAULT_MOUSE_LOCK_CURSOR = "rbxasset://textures/MouseLockedCursor.png"
+
+ local CONTEXT_ACTION_NAME = "MouseLockSwitchAction"
+ local MOUSELOCK_ACTION_PRIORITY = Enum.ContextActionPriority.Default.Value
+
+ --[[ Services ]]--
+ local PlayersService = game:GetService("Players")
+ local ContextActionService = game:GetService("ContextActionService")
+ local Settings = UserSettings() -- ignore warning
+ local GameSettings = Settings.GameSettings
+ local Mouse = PlayersService.LocalPlayer:GetMouse()
+
+ --[[ The Module ]]--
+ local MouseLockController = {}
+ MouseLockController.__index = MouseLockController
+
+ function MouseLockController.new()
+ local self = setmetatable({}, MouseLockController)
+
+ self.isMouseLocked = false
+ self.savedMouseCursor = nil
+ self.boundKeys = {Enum.KeyCode.LeftShift, Enum.KeyCode.RightShift} -- defaults
+
+ self.mouseLockToggledEvent = Instance.new("BindableEvent")
+
+ local boundKeysObj = script:FindFirstChild("BoundKeys")
+ if (not boundKeysObj) or (not boundKeysObj:IsA("StringValue")) then
+ -- If object with correct name was found, but it's not a StringValue, destroy and replace
+ if boundKeysObj then
+ boundKeysObj:Destroy()
+ end
+
+ boundKeysObj = Instance.new("StringValue")
+ boundKeysObj.Name = "BoundKeys"
+ boundKeysObj.Value = "LeftShift,RightShift"
+ boundKeysObj.Parent = script
+ end
+
+ if boundKeysObj then
+ boundKeysObj.Changed:Connect(function(value)
+ self:OnBoundKeysObjectChanged(value)
+ end)
+ self:OnBoundKeysObjectChanged(boundKeysObj.Value) -- Initial setup call
+ end
+
+ -- Watch for changes to user's ControlMode and ComputerMovementMode settings and update the feature availability accordingly
+ GameSettings.Changed:Connect(function(property)
+ if property == "ControlMode" or property == "ComputerMovementMode" then
+ self:UpdateMouseLockAvailability()
+ end
+ end)
+
+ -- Watch for changes to DevEnableMouseLock and update the feature availability accordingly
+ PlayersService.LocalPlayer:GetPropertyChangedSignal("DevEnableMouseLock"):Connect(function()
+ self:UpdateMouseLockAvailability()
+ end)
+
+ -- Watch for changes to DevEnableMouseLock and update the feature availability accordingly
+ PlayersService.LocalPlayer:GetPropertyChangedSignal("DevComputerMovementMode"):Connect(function()
+ self:UpdateMouseLockAvailability()
+ end)
+
+ self:UpdateMouseLockAvailability()
+
+ return self
+ end
+
+ function MouseLockController:GetIsMouseLocked()
+ return self.isMouseLocked
+ end
+
+ function MouseLockController:GetBindableToggleEvent()
+ return self.mouseLockToggledEvent.Event
+ end
+
+ function MouseLockController:GetMouseLockOffset()
+ local offsetValueObj = script:FindFirstChild("CameraOffset")
+ if offsetValueObj and offsetValueObj:IsA("Vector3Value") then
+ return offsetValueObj.Value
+ else
+ -- If CameraOffset object was found but not correct type, destroy
+ if offsetValueObj then
+ offsetValueObj:Destroy()
+ end
+ offsetValueObj = Instance.new("Vector3Value")
+ offsetValueObj.Name = "CameraOffset"
+ offsetValueObj.Value = Vector3.new(1.75,0,0) -- Legacy Default Value
+ offsetValueObj.Parent = script
+ end
+
+ if offsetValueObj and offsetValueObj.Value then
+ return offsetValueObj.Value
+ end
+
+ return Vector3.new(1.75,0,0)
+ end
+
+ function MouseLockController:UpdateMouseLockAvailability()
+ local devAllowsMouseLock = PlayersService.LocalPlayer.DevEnableMouseLock
+ local devMovementModeIsScriptable = PlayersService.LocalPlayer.DevComputerMovementMode == Enum.DevComputerMovementMode.Scriptable
+ local userHasMouseLockModeEnabled = GameSettings.ControlMode == Enum.ControlMode.MouseLockSwitch
+ local userHasClickToMoveEnabled = GameSettings.ComputerMovementMode == Enum.ComputerMovementMode.ClickToMove
+ local MouseLockAvailable = devAllowsMouseLock and userHasMouseLockModeEnabled and not userHasClickToMoveEnabled and not devMovementModeIsScriptable
+
+ if MouseLockAvailable~=self.enabled then
+ self:EnableMouseLock(MouseLockAvailable)
+ end
+ end
+
+ function MouseLockController:OnBoundKeysObjectChanged(newValue)
+ self.boundKeys = {} -- Overriding defaults, note: possibly with nothing at all if boundKeysObj.Value is "" or contains invalid values
+ for token in string.gmatch(newValue,"[^%s,]+") do
+ for _, keyEnum in pairs(Enum.KeyCode:GetEnumItems()) do
+ if token == keyEnum.Name then
+ self.boundKeys[#self.boundKeys+1] = keyEnum
+ break
+ end
+ end
+ end
+ self:UnbindContextActions()
+ self:BindContextActions()
+ end
+
+ --[[ Local Functions ]]--
+ function MouseLockController:OnMouseLockToggled()
+ self.isMouseLocked = not self.isMouseLocked
+
+ if self.isMouseLocked then
+ local cursorImageValueObj = script:FindFirstChild("CursorImage")
+ if cursorImageValueObj and cursorImageValueObj:IsA("StringValue") and cursorImageValueObj.Value then
+ self.savedMouseCursor = Mouse.Icon
+ Mouse.Icon = cursorImageValueObj.Value
+ else
+ if cursorImageValueObj then
+ cursorImageValueObj:Destroy()
+ end
+ cursorImageValueObj = Instance.new("StringValue")
+ cursorImageValueObj.Name = "CursorImage"
+ cursorImageValueObj.Value = DEFAULT_MOUSE_LOCK_CURSOR
+ cursorImageValueObj.Parent = script
+ self.savedMouseCursor = Mouse.Icon
+ Mouse.Icon = DEFAULT_MOUSE_LOCK_CURSOR
+ end
+ else
+ if self.savedMouseCursor then
+ Mouse.Icon = self.savedMouseCursor
+ self.savedMouseCursor = nil
+ end
+ end
+
+ self.mouseLockToggledEvent:Fire()
+ end
+
+ function MouseLockController:DoMouseLockSwitch(name, state, input)
+ if state == Enum.UserInputState.Begin then
+ self:OnMouseLockToggled()
+ return Enum.ContextActionResult.Sink
+ end
+ return Enum.ContextActionResult.Pass
+ end
+
+ function MouseLockController:BindContextActions()
+ ContextActionService:BindActionAtPriority(CONTEXT_ACTION_NAME, function(name, state, input)
+ return self:DoMouseLockSwitch(name, state, input)
+ end, false, MOUSELOCK_ACTION_PRIORITY, unpack(self.boundKeys))
+ end
+
+ function MouseLockController:UnbindContextActions()
+ ContextActionService:UnbindAction(CONTEXT_ACTION_NAME)
+ end
+
+ function MouseLockController:IsMouseLocked()
+ return self.enabled and self.isMouseLocked
+ end
+
+ function MouseLockController:EnableMouseLock(enable)
+ if enable ~= self.enabled then
+
+ self.enabled = enable
+
+ if self.enabled then
+ -- Enabling the mode
+ self:BindContextActions()
+ else
+ -- Disabling
+ -- Restore mouse cursor
+ if Mouse.Icon~="" then
+ Mouse.Icon = ""
+ end
+
+ self:UnbindContextActions()
+
+ -- If the mode is disabled while being used, fire the event to toggle it off
+ if self.isMouseLocked then
+ self.mouseLockToggledEvent:Fire()
+ end
+
+ self.isMouseLocked = false
+ end
+
+ end
+ end
+
+ return MouseLockController
+end
+
+function _TransparencyController()
+
+ local MAX_TWEEN_RATE = 2.8 -- per second
+
+ local Util = _CameraUtils()
+
+ --[[ The Module ]]--
+ local TransparencyController = {}
+ TransparencyController.__index = TransparencyController
+
+ function TransparencyController.new()
+ local self = setmetatable({}, TransparencyController)
+
+ self.lastUpdate = tick()
+ self.transparencyDirty = false
+ self.enabled = false
+ self.lastTransparency = nil
+
+ self.descendantAddedConn, self.descendantRemovingConn = nil, nil
+ self.toolDescendantAddedConns = {}
+ self.toolDescendantRemovingConns = {}
+ self.cachedParts = {}
+
+ return self
+ end
+
+
+ function TransparencyController:HasToolAncestor(object)
+ if object.Parent == nil then return false end
+ return object.Parent:IsA('Tool') or self:HasToolAncestor(object.Parent)
+ end
+
+ function TransparencyController:IsValidPartToModify(part)
+ if part:IsA('BasePart') or part:IsA('Decal') then
+ return not self:HasToolAncestor(part)
+ end
+ return false
+ end
+
+ function TransparencyController:CachePartsRecursive(object)
+ if object then
+ if self:IsValidPartToModify(object) then
+ self.cachedParts[object] = true
+ self.transparencyDirty = true
+ end
+ for _, child in pairs(object:GetChildren()) do
+ self:CachePartsRecursive(child)
+ end
+ end
+ end
+
+ function TransparencyController:TeardownTransparency()
+ for child, _ in pairs(self.cachedParts) do
+ child.LocalTransparencyModifier = 0
+ end
+ self.cachedParts = {}
+ self.transparencyDirty = true
+ self.lastTransparency = nil
+
+ if self.descendantAddedConn then
+ self.descendantAddedConn:disconnect()
+ self.descendantAddedConn = nil
+ end
+ if self.descendantRemovingConn then
+ self.descendantRemovingConn:disconnect()
+ self.descendantRemovingConn = nil
+ end
+ for object, conn in pairs(self.toolDescendantAddedConns) do
+ conn:Disconnect()
+ self.toolDescendantAddedConns[object] = nil
+ end
+ for object, conn in pairs(self.toolDescendantRemovingConns) do
+ conn:Disconnect()
+ self.toolDescendantRemovingConns[object] = nil
+ end
+ end
+
+ function TransparencyController:SetupTransparency(character)
+ self:TeardownTransparency()
+
+ if self.descendantAddedConn then self.descendantAddedConn:disconnect() end
+ self.descendantAddedConn = character.DescendantAdded:Connect(function(object)
+ -- This is a part we want to invisify
+ if self:IsValidPartToModify(object) then
+ self.cachedParts[object] = true
+ self.transparencyDirty = true
+ -- There is now a tool under the character
+ elseif object:IsA('Tool') then
+ if self.toolDescendantAddedConns[object] then self.toolDescendantAddedConns[object]:Disconnect() end
+ self.toolDescendantAddedConns[object] = object.DescendantAdded:Connect(function(toolChild)
+ self.cachedParts[toolChild] = nil
+ if toolChild:IsA('BasePart') or toolChild:IsA('Decal') then
+ -- Reset the transparency
+ toolChild.LocalTransparencyModifier = 0
+ end
+ end)
+ if self.toolDescendantRemovingConns[object] then self.toolDescendantRemovingConns[object]:disconnect() end
+ self.toolDescendantRemovingConns[object] = object.DescendantRemoving:Connect(function(formerToolChild)
+ wait() -- wait for new parent
+ if character and formerToolChild and formerToolChild:IsDescendantOf(character) then
+ if self:IsValidPartToModify(formerToolChild) then
+ self.cachedParts[formerToolChild] = true
+ self.transparencyDirty = true
+ end
+ end
+ end)
+ end
+ end)
+ if self.descendantRemovingConn then self.descendantRemovingConn:disconnect() end
+ self.descendantRemovingConn = character.DescendantRemoving:connect(function(object)
+ if self.cachedParts[object] then
+ self.cachedParts[object] = nil
+ -- Reset the transparency
+ object.LocalTransparencyModifier = 0
+ end
+ end)
+ self:CachePartsRecursive(character)
+ end
+
+
+ function TransparencyController:Enable(enable)
+ if self.enabled ~= enable then
+ self.enabled = enable
+ self:Update()
+ end
+ end
+
+ function TransparencyController:SetSubject(subject)
+ local character = nil
+ if subject and subject:IsA("Humanoid") then
+ character = subject.Parent
+ end
+ if subject and subject:IsA("VehicleSeat") and subject.Occupant then
+ character = subject.Occupant.Parent
+ end
+ if character then
+ self:SetupTransparency(character)
+ else
+ self:TeardownTransparency()
+ end
+ end
+
+ function TransparencyController:Update()
+ local instant = false
+ local now = tick()
+ local currentCamera = workspace.CurrentCamera
+
+ if currentCamera then
+ local transparency = 0
+ if not self.enabled then
+ instant = true
+ else
+ local distance = (currentCamera.Focus.p - currentCamera.CoordinateFrame.p).magnitude
+ transparency = (distance<2) and (1.0-(distance-0.5)/1.5) or 0 --(7 - distance) / 5
+ if transparency < 0.5 then
+ transparency = 0
+ end
+
+ if self.lastTransparency then
+ local deltaTransparency = transparency - self.lastTransparency
+
+ -- Don't tween transparency if it is instant or your character was fully invisible last frame
+ if not instant and transparency < 1 and self.lastTransparency < 0.95 then
+ local maxDelta = MAX_TWEEN_RATE * (now - self.lastUpdate)
+ deltaTransparency = math.clamp(deltaTransparency, -maxDelta, maxDelta)
+ end
+ transparency = self.lastTransparency + deltaTransparency
+ else
+ self.transparencyDirty = true
+ end
+
+ transparency = math.clamp(Util.Round(transparency, 2), 0, 1)
+ end
+
+ if self.transparencyDirty or self.lastTransparency ~= transparency then
+ for child, _ in pairs(self.cachedParts) do
+ child.LocalTransparencyModifier = transparency
+ end
+ self.transparencyDirty = false
+ self.lastTransparency = transparency
+ end
+ end
+ self.lastUpdate = now
+ end
+
+ return TransparencyController
+end
+
+function _Poppercam()
+ local ZoomController = _ZoomController()
+
+ local TransformExtrapolator = {} do
+ TransformExtrapolator.__index = TransformExtrapolator
+
+ local CF_IDENTITY = CFrame.new()
+
+ local function cframeToAxis(cframe)
+ local axis, angle = cframe:toAxisAngle()
+ return axis*angle
+ end
+
+ local function axisToCFrame(axis)
+ local angle = axis.magnitude
+ if angle > 1e-5 then
+ return CFrame.fromAxisAngle(axis, angle)
+ end
+ return CF_IDENTITY
+ end
+
+ local function extractRotation(cf)
+ local _, _, _, xx, yx, zx, xy, yy, zy, xz, yz, zz = cf:components()
+ return CFrame.new(0, 0, 0, xx, yx, zx, xy, yy, zy, xz, yz, zz)
+ end
+
+ function TransformExtrapolator.new()
+ return setmetatable({
+ lastCFrame = nil,
+ }, TransformExtrapolator)
+ end
+
+ function TransformExtrapolator:Step(dt, currentCFrame)
+ local lastCFrame = self.lastCFrame or currentCFrame
+ self.lastCFrame = currentCFrame
+
+ local currentPos = currentCFrame.p
+ local currentRot = extractRotation(currentCFrame)
+
+ local lastPos = lastCFrame.p
+ local lastRot = extractRotation(lastCFrame)
+
+ -- Estimate velocities from the delta between now and the last frame
+ -- This estimation can be a little noisy.
+ local dp = (currentPos - lastPos)/dt
+ local dr = cframeToAxis(currentRot*lastRot:inverse())/dt
+
+ local function extrapolate(t)
+ local p = dp*t + currentPos
+ local r = axisToCFrame(dr*t)*currentRot
+ return r + p
+ end
+
+ return {
+ extrapolate = extrapolate,
+ posVelocity = dp,
+ rotVelocity = dr,
+ }
+ end
+
+ function TransformExtrapolator:Reset()
+ self.lastCFrame = nil
+ end
+ end
+
+ --[[ The Module ]]--
+ local BaseOcclusion = _BaseOcclusion()
+ local Poppercam = setmetatable({}, BaseOcclusion)
+ Poppercam.__index = Poppercam
+
+ function Poppercam.new()
+ local self = setmetatable(BaseOcclusion.new(), Poppercam)
+ self.focusExtrapolator = TransformExtrapolator.new()
+ return self
+ end
+
+ function Poppercam:GetOcclusionMode()
+ return Enum.DevCameraOcclusionMode.Zoom
+ end
+
+ function Poppercam:Enable(enable)
+ self.focusExtrapolator:Reset()
+ end
+
+ function Poppercam:Update(renderDt, desiredCameraCFrame, desiredCameraFocus, cameraController)
+ local rotatedFocus = CFrame.new(desiredCameraFocus.p, desiredCameraCFrame.p)*CFrame.new(
+ 0, 0, 0,
+ -1, 0, 0,
+ 0, 1, 0,
+ 0, 0, -1
+ )
+ local extrapolation = self.focusExtrapolator:Step(renderDt, rotatedFocus)
+ local zoom = ZoomController.Update(renderDt, rotatedFocus, extrapolation)
+ return rotatedFocus*CFrame.new(0, 0, zoom), desiredCameraFocus
+ end
+
+ -- Called when character is added
+ function Poppercam:CharacterAdded(character, player)
+ end
+
+ -- Called when character is about to be removed
+ function Poppercam:CharacterRemoving(character, player)
+ end
+
+ function Poppercam:OnCameraSubjectChanged(newSubject)
+ end
+
+ local ZoomController = _ZoomController()
+
+ function Poppercam:Update(renderDt, desiredCameraCFrame, desiredCameraFocus, cameraController)
+ local rotatedFocus = desiredCameraFocus * (desiredCameraCFrame - desiredCameraCFrame.p)
+ local extrapolation = self.focusExtrapolator:Step(renderDt, rotatedFocus)
+ local zoom = ZoomController.Update(renderDt, rotatedFocus, extrapolation)
+ return rotatedFocus*CFrame.new(0, 0, zoom), desiredCameraFocus
+ end
+
+ return Poppercam
+end
+
+function _Invisicam()
+
+ --[[ Top Level Roblox Services ]]--
+ local PlayersService = game:GetService("Players")
+
+ --[[ Constants ]]--
+ local ZERO_VECTOR3 = Vector3.new(0,0,0)
+ local USE_STACKING_TRANSPARENCY = true -- Multiple items between the subject and camera get transparency values that add up to TARGET_TRANSPARENCY
+ local TARGET_TRANSPARENCY = 0.75 -- Classic Invisicam's Value, also used by new invisicam for parts hit by head and torso rays
+ local TARGET_TRANSPARENCY_PERIPHERAL = 0.5 -- Used by new SMART_CIRCLE mode for items not hit by head and torso rays
+
+ local MODE = {
+ --CUSTOM = 1, -- Retired, unused
+ LIMBS = 2, -- Track limbs
+ MOVEMENT = 3, -- Track movement
+ CORNERS = 4, -- Char model corners
+ CIRCLE1 = 5, -- Circle of casts around character
+ CIRCLE2 = 6, -- Circle of casts around character, camera relative
+ LIMBMOVE = 7, -- LIMBS mode + MOVEMENT mode
+ SMART_CIRCLE = 8, -- More sample points on and around character
+ CHAR_OUTLINE = 9, -- Dynamic outline around the character
+ }
+
+ local LIMB_TRACKING_SET = {
+ -- Body parts common to R15 and R6
+ ['Head'] = true,
+
+ -- Body parts unique to R6
+ ['Left Arm'] = true,
+ ['Right Arm'] = true,
+ ['Left Leg'] = true,
+ ['Right Leg'] = true,
+
+ -- Body parts unique to R15
+ ['LeftLowerArm'] = true,
+ ['RightLowerArm'] = true,
+ ['LeftUpperLeg'] = true,
+ ['RightUpperLeg'] = true
+ }
+
+ local CORNER_FACTORS = {
+ Vector3.new(1,1,-1),
+ Vector3.new(1,-1,-1),
+ Vector3.new(-1,-1,-1),
+ Vector3.new(-1,1,-1)
+ }
+
+ local CIRCLE_CASTS = 10
+ local MOVE_CASTS = 3
+ local SMART_CIRCLE_CASTS = 24
+ local SMART_CIRCLE_INCREMENT = 2.0 * math.pi / SMART_CIRCLE_CASTS
+ local CHAR_OUTLINE_CASTS = 24
+
+ -- Used to sanitize user-supplied functions
+ local function AssertTypes(param, ...)
+ local allowedTypes = {}
+ local typeString = ''
+ for _, typeName in pairs({...}) do
+ allowedTypes[typeName] = true
+ typeString = typeString .. (typeString == '' and '' or ' or ') .. typeName
+ end
+ local theType = type(param)
+ assert(allowedTypes[theType], typeString .. " type expected, got: " .. theType)
+ end
+
+ -- Helper function for Determinant of 3x3, not in CameraUtils for performance reasons
+ local function Det3x3(a,b,c,d,e,f,g,h,i)
+ return (a*(e*i-f*h)-b*(d*i-f*g)+c*(d*h-e*g))
+ end
+
+ -- Smart Circle mode needs the intersection of 2 rays that are known to be in the same plane
+ -- because they are generated from cross products with a common vector. This function is computing
+ -- that intersection, but it's actually the general solution for the point halfway between where
+ -- two skew lines come nearest to each other, which is more forgiving.
+ local function RayIntersection(p0, v0, p1, v1)
+ local v2 = v0:Cross(v1)
+ local d1 = p1.x - p0.x
+ local d2 = p1.y - p0.y
+ local d3 = p1.z - p0.z
+ local denom = Det3x3(v0.x,-v1.x,v2.x,v0.y,-v1.y,v2.y,v0.z,-v1.z,v2.z)
+
+ if (denom == 0) then
+ return ZERO_VECTOR3 -- No solution (rays are parallel)
+ end
+
+ local t0 = Det3x3(d1,-v1.x,v2.x,d2,-v1.y,v2.y,d3,-v1.z,v2.z) / denom
+ local t1 = Det3x3(v0.x,d1,v2.x,v0.y,d2,v2.y,v0.z,d3,v2.z) / denom
+ local s0 = p0 + t0 * v0
+ local s1 = p1 + t1 * v1
+ local s = s0 + 0.5 * ( s1 - s0 )
+
+ -- 0.25 studs is a threshold for deciding if the rays are
+ -- close enough to be considered intersecting, found through testing
+ if (s1-s0).Magnitude < 0.25 then
+ return s
+ else
+ return ZERO_VECTOR3
+ end
+ end
+
+
+
+ --[[ The Module ]]--
+ local BaseOcclusion = _BaseOcclusion()
+ local Invisicam = setmetatable({}, BaseOcclusion)
+ Invisicam.__index = Invisicam
+
+ function Invisicam.new()
+ local self = setmetatable(BaseOcclusion.new(), Invisicam)
+
+ self.char = nil
+ self.humanoidRootPart = nil
+ self.torsoPart = nil
+ self.headPart = nil
+
+ self.childAddedConn = nil
+ self.childRemovedConn = nil
+
+ self.behaviors = {} -- Map of modes to behavior fns
+ self.behaviors[MODE.LIMBS] = self.LimbBehavior
+ self.behaviors[MODE.MOVEMENT] = self.MoveBehavior
+ self.behaviors[MODE.CORNERS] = self.CornerBehavior
+ self.behaviors[MODE.CIRCLE1] = self.CircleBehavior
+ self.behaviors[MODE.CIRCLE2] = self.CircleBehavior
+ self.behaviors[MODE.LIMBMOVE] = self.LimbMoveBehavior
+ self.behaviors[MODE.SMART_CIRCLE] = self.SmartCircleBehavior
+ self.behaviors[MODE.CHAR_OUTLINE] = self.CharacterOutlineBehavior
+
+ self.mode = MODE.SMART_CIRCLE
+ self.behaviorFunction = self.SmartCircleBehavior
+
+ self.savedHits = {} -- Objects currently being faded in/out
+ self.trackedLimbs = {} -- Used in limb-tracking casting modes
+
+ self.camera = game.Workspace.CurrentCamera
+
+ self.enabled = false
+ return self
+ end
+
+ function Invisicam:Enable(enable)
+ self.enabled = enable
+
+ if not enable then
+ self:Cleanup()
+ end
+ end
+
+ function Invisicam:GetOcclusionMode()
+ return Enum.DevCameraOcclusionMode.Invisicam
+ end
+
+ --[[ Module functions ]]--
+ function Invisicam:LimbBehavior(castPoints)
+ for limb, _ in pairs(self.trackedLimbs) do
+ castPoints[#castPoints + 1] = limb.Position
+ end
+ end
+
+ function Invisicam:MoveBehavior(castPoints)
+ for i = 1, MOVE_CASTS do
+ local position, velocity = self.humanoidRootPart.Position, self.humanoidRootPart.Velocity
+ local horizontalSpeed = Vector3.new(velocity.X, 0, velocity.Z).Magnitude / 2
+ local offsetVector = (i - 1) * self.humanoidRootPart.CFrame.lookVector * horizontalSpeed
+ castPoints[#castPoints + 1] = position + offsetVector
+ end
+ end
+
+ function Invisicam:CornerBehavior(castPoints)
+ local cframe = self.humanoidRootPart.CFrame
+ local centerPoint = cframe.p
+ local rotation = cframe - centerPoint
+ local halfSize = self.char:GetExtentsSize() / 2 --NOTE: Doesn't update w/ limb animations
+ castPoints[#castPoints + 1] = centerPoint
+ for i = 1, #CORNER_FACTORS do
+ castPoints[#castPoints + 1] = centerPoint + (rotation * (halfSize * CORNER_FACTORS[i]))
+ end
+ end
+
+ function Invisicam:CircleBehavior(castPoints)
+ local cframe
+ if self.mode == MODE.CIRCLE1 then
+ cframe = self.humanoidRootPart.CFrame
+ else
+ local camCFrame = self.camera.CoordinateFrame
+ cframe = camCFrame - camCFrame.p + self.humanoidRootPart.Position
+ end
+ castPoints[#castPoints + 1] = cframe.p
+ for i = 0, CIRCLE_CASTS - 1 do
+ local angle = (2 * math.pi / CIRCLE_CASTS) * i
+ local offset = 3 * Vector3.new(math.cos(angle), math.sin(angle), 0)
+ castPoints[#castPoints + 1] = cframe * offset
+ end
+ end
+
+ function Invisicam:LimbMoveBehavior(castPoints)
+ self:LimbBehavior(castPoints)
+ self:MoveBehavior(castPoints)
+ end
+
+ function Invisicam:CharacterOutlineBehavior(castPoints)
+ local torsoUp = self.torsoPart.CFrame.upVector.unit
+ local torsoRight = self.torsoPart.CFrame.rightVector.unit
+
+ -- Torso cross of points for interior coverage
+ castPoints[#castPoints + 1] = self.torsoPart.CFrame.p
+ castPoints[#castPoints + 1] = self.torsoPart.CFrame.p + torsoUp
+ castPoints[#castPoints + 1] = self.torsoPart.CFrame.p - torsoUp
+ castPoints[#castPoints + 1] = self.torsoPart.CFrame.p + torsoRight
+ castPoints[#castPoints + 1] = self.torsoPart.CFrame.p - torsoRight
+ if self.headPart then
+ castPoints[#castPoints + 1] = self.headPart.CFrame.p
+ end
+
+ local cframe = CFrame.new(ZERO_VECTOR3,Vector3.new(self.camera.CoordinateFrame.lookVector.X,0,self.camera.CoordinateFrame.lookVector.Z))
+ local centerPoint = (self.torsoPart and self.torsoPart.Position or self.humanoidRootPart.Position)
+
+ local partsWhitelist = {self.torsoPart}
+ if self.headPart then
+ partsWhitelist[#partsWhitelist + 1] = self.headPart
+ end
+
+ for i = 1, CHAR_OUTLINE_CASTS do
+ local angle = (2 * math.pi * i / CHAR_OUTLINE_CASTS)
+ local offset = cframe * (3 * Vector3.new(math.cos(angle), math.sin(angle), 0))
+
+ offset = Vector3.new(offset.X, math.max(offset.Y, -2.25), offset.Z)
+
+ local ray = Ray.new(centerPoint + offset, -3 * offset)
+ local hit, hitPoint = game.Workspace:FindPartOnRayWithWhitelist(ray, partsWhitelist, false, false)
+
+ if hit then
+ -- Use hit point as the cast point, but nudge it slightly inside the character so that bumping up against
+ -- walls is less likely to cause a transparency glitch
+ castPoints[#castPoints + 1] = hitPoint + 0.2 * (centerPoint - hitPoint).unit
+ end
+ end
+ end
+
+ function Invisicam:SmartCircleBehavior(castPoints)
+ local torsoUp = self.torsoPart.CFrame.upVector.unit
+ local torsoRight = self.torsoPart.CFrame.rightVector.unit
+
+ -- SMART_CIRCLE mode includes rays to head and 5 to the torso.
+ -- Hands, arms, legs and feet are not included since they
+ -- are not canCollide and can therefore go inside of parts
+ castPoints[#castPoints + 1] = self.torsoPart.CFrame.p
+ castPoints[#castPoints + 1] = self.torsoPart.CFrame.p + torsoUp
+ castPoints[#castPoints + 1] = self.torsoPart.CFrame.p - torsoUp
+ castPoints[#castPoints + 1] = self.torsoPart.CFrame.p + torsoRight
+ castPoints[#castPoints + 1] = self.torsoPart.CFrame.p - torsoRight
+ if self.headPart then
+ castPoints[#castPoints + 1] = self.headPart.CFrame.p
+ end
+
+ local cameraOrientation = self.camera.CFrame - self.camera.CFrame.p
+ local torsoPoint = Vector3.new(0,0.5,0) + (self.torsoPart and self.torsoPart.Position or self.humanoidRootPart.Position)
+ local radius = 2.5
+
+ -- This loop first calculates points in a circle of radius 2.5 around the torso of the character, in the
+ -- plane orthogonal to the camera's lookVector. Each point is then raycast to, to determine if it is within
+ -- the free space surrounding the player (not inside anything). Two iterations are done to adjust points that
+ -- are inside parts, to try to move them to valid locations that are still on their camera ray, so that the
+ -- circle remains circular from the camera's perspective, but does not cast rays into walls or parts that are
+ -- behind, below or beside the character and not really obstructing view of the character. This minimizes
+ -- the undesirable situation where the character walks up to an exterior wall and it is made invisible even
+ -- though it is behind the character.
+ for i = 1, SMART_CIRCLE_CASTS do
+ local angle = SMART_CIRCLE_INCREMENT * i - 0.5 * math.pi
+ local offset = radius * Vector3.new(math.cos(angle), math.sin(angle), 0)
+ local circlePoint = torsoPoint + cameraOrientation * offset
+
+ -- Vector from camera to point on the circle being tested
+ local vp = circlePoint - self.camera.CFrame.p
+
+ local ray = Ray.new(torsoPoint, circlePoint - torsoPoint)
+ local hit, hp, hitNormal = game.Workspace:FindPartOnRayWithIgnoreList(ray, {self.char}, false, false )
+ local castPoint = circlePoint
+
+ if hit then
+ local hprime = hp + 0.1 * hitNormal.unit -- Slightly offset hit point from the hit surface
+ local v0 = hprime - torsoPoint -- Vector from torso to offset hit point
+
+ local perp = (v0:Cross(vp)).unit
+
+ -- Vector from the offset hit point, along the hit surface
+ local v1 = (perp:Cross(hitNormal)).unit
+
+ -- Vector from camera to offset hit
+ local vprime = (hprime - self.camera.CFrame.p).unit
+
+ -- This dot product checks to see if the vector along the hit surface would hit the correct
+ -- side of the invisicam cone, or if it would cross the camera look vector and hit the wrong side
+ if ( v0.unit:Dot(-v1) < v0.unit:Dot(vprime)) then
+ castPoint = RayIntersection(hprime, v1, circlePoint, vp)
+
+ if castPoint.Magnitude > 0 then
+ local ray = Ray.new(hprime, castPoint - hprime)
+ local hit, hitPoint, hitNormal = game.Workspace:FindPartOnRayWithIgnoreList(ray, {self.char}, false, false )
+
+ if hit then
+ local hprime2 = hitPoint + 0.1 * hitNormal.unit
+ castPoint = hprime2
+ end
+ else
+ castPoint = hprime
+ end
+ else
+ castPoint = hprime
+ end
+
+ local ray = Ray.new(torsoPoint, (castPoint - torsoPoint))
+ local hit, hitPoint, hitNormal = game.Workspace:FindPartOnRayWithIgnoreList(ray, {self.char}, false, false )
+
+ if hit then
+ local castPoint2 = hitPoint - 0.1 * (castPoint - torsoPoint).unit
+ castPoint = castPoint2
+ end
+ end
+
+ castPoints[#castPoints + 1] = castPoint
+ end
+ end
+
+ function Invisicam:CheckTorsoReference()
+ if self.char then
+ self.torsoPart = self.char:FindFirstChild("Torso")
+ if not self.torsoPart then
+ self.torsoPart = self.char:FindFirstChild("UpperTorso")
+ if not self.torsoPart then
+ self.torsoPart = self.char:FindFirstChild("HumanoidRootPart")
+ end
+ end
+
+ self.headPart = self.char:FindFirstChild("Head")
+ end
+ end
+
+ function Invisicam:CharacterAdded(char, player)
+ -- We only want the LocalPlayer's character
+ if player~=PlayersService.LocalPlayer then return end
+
+ if self.childAddedConn then
+ self.childAddedConn:Disconnect()
+ self.childAddedConn = nil
+ end
+ if self.childRemovedConn then
+ self.childRemovedConn:Disconnect()
+ self.childRemovedConn = nil
+ end
+
+ self.char = char
+
+ self.trackedLimbs = {}
+ local function childAdded(child)
+ if child:IsA("BasePart") then
+ if LIMB_TRACKING_SET[child.Name] then
+ self.trackedLimbs[child] = true
+ end
+
+ if child.Name == "Torso" or child.Name == "UpperTorso" then
+ self.torsoPart = child
+ end
+
+ if child.Name == "Head" then
+ self.headPart = child
+ end
+ end
+ end
+
+ local function childRemoved(child)
+ self.trackedLimbs[child] = nil
+
+ -- If removed/replaced part is 'Torso' or 'UpperTorso' double check that we still have a TorsoPart to use
+ self:CheckTorsoReference()
+ end
+
+ self.childAddedConn = char.ChildAdded:Connect(childAdded)
+ self.childRemovedConn = char.ChildRemoved:Connect(childRemoved)
+ for _, child in pairs(self.char:GetChildren()) do
+ childAdded(child)
+ end
+ end
+
+ function Invisicam:SetMode(newMode)
+ AssertTypes(newMode, 'number')
+ for _, modeNum in pairs(MODE) do
+ if modeNum == newMode then
+ self.mode = newMode
+ self.behaviorFunction = self.behaviors[self.mode]
+ return
+ end
+ end
+ error("Invalid mode number")
+ end
+
+ function Invisicam:GetObscuredParts()
+ return self.savedHits
+ end
+
+ -- Want to turn off Invisicam? Be sure to call this after.
+ function Invisicam:Cleanup()
+ for hit, originalFade in pairs(self.savedHits) do
+ hit.LocalTransparencyModifier = originalFade
+ end
+ end
+
+ function Invisicam:Update(dt, desiredCameraCFrame, desiredCameraFocus)
+ -- Bail if there is no Character
+ if not self.enabled or not self.char then
+ return desiredCameraCFrame, desiredCameraFocus
+ end
+
+ self.camera = game.Workspace.CurrentCamera
+
+ -- TODO: Move this to a GetHumanoidRootPart helper, probably combine with CheckTorsoReference
+ -- Make sure we still have a HumanoidRootPart
+ if not self.humanoidRootPart then
+ local humanoid = self.char:FindFirstChildOfClass("Humanoid")
+ if humanoid and humanoid.RootPart then
+ self.humanoidRootPart = humanoid.RootPart
+ else
+ -- Not set up with Humanoid? Try and see if there's one in the Character at all:
+ self.humanoidRootPart = self.char:FindFirstChild("HumanoidRootPart")
+ if not self.humanoidRootPart then
+ -- Bail out, since we're relying on HumanoidRootPart existing
+ return desiredCameraCFrame, desiredCameraFocus
+ end
+ end
+
+ -- TODO: Replace this with something more sensible
+ local ancestryChangedConn
+ ancestryChangedConn = self.humanoidRootPart.AncestryChanged:Connect(function(child, parent)
+ if child == self.humanoidRootPart and not parent then
+ self.humanoidRootPart = nil
+ if ancestryChangedConn and ancestryChangedConn.Connected then
+ ancestryChangedConn:Disconnect()
+ ancestryChangedConn = nil
+ end
+ end
+ end)
+ end
+
+ if not self.torsoPart then
+ self:CheckTorsoReference()
+ if not self.torsoPart then
+ -- Bail out, since we're relying on Torso existing, should never happen since we fall back to using HumanoidRootPart as torso
+ return desiredCameraCFrame, desiredCameraFocus
+ end
+ end
+
+ -- Make a list of world points to raycast to
+ local castPoints = {}
+ self.behaviorFunction(self, castPoints)
+
+ -- Cast to get a list of objects between the camera and the cast points
+ local currentHits = {}
+ local ignoreList = {self.char}
+ local function add(hit)
+ currentHits[hit] = true
+ if not self.savedHits[hit] then
+ self.savedHits[hit] = hit.LocalTransparencyModifier
+ end
+ end
+
+ local hitParts
+ local hitPartCount = 0
+
+ -- Hash table to treat head-ray-hit parts differently than the rest of the hit parts hit by other rays
+ -- head/torso ray hit parts will be more transparent than peripheral parts when USE_STACKING_TRANSPARENCY is enabled
+ local headTorsoRayHitParts = {}
+
+ local perPartTransparencyHeadTorsoHits = TARGET_TRANSPARENCY
+ local perPartTransparencyOtherHits = TARGET_TRANSPARENCY
+
+ if USE_STACKING_TRANSPARENCY then
+
+ -- This first call uses head and torso rays to find out how many parts are stacked up
+ -- for the purpose of calculating required per-part transparency
+ local headPoint = self.headPart and self.headPart.CFrame.p or castPoints[1]
+ local torsoPoint = self.torsoPart and self.torsoPart.CFrame.p or castPoints[2]
+ hitParts = self.camera:GetPartsObscuringTarget({headPoint, torsoPoint}, ignoreList)
+
+ -- Count how many things the sample rays passed through, including decals. This should only
+ -- count decals facing the camera, but GetPartsObscuringTarget does not return surface normals,
+ -- so my compromise for now is to just let any decal increase the part count by 1. Only one
+ -- decal per part will be considered.
+ for i = 1, #hitParts do
+ local hitPart = hitParts[i]
+ hitPartCount = hitPartCount + 1 -- count the part itself
+ headTorsoRayHitParts[hitPart] = true
+ for _, child in pairs(hitPart:GetChildren()) do
+ if child:IsA('Decal') or child:IsA('Texture') then
+ hitPartCount = hitPartCount + 1 -- count first decal hit, then break
+ break
+ end
+ end
+ end
+
+ if (hitPartCount > 0) then
+ perPartTransparencyHeadTorsoHits = math.pow( ((0.5 * TARGET_TRANSPARENCY) + (0.5 * TARGET_TRANSPARENCY / hitPartCount)), 1 / hitPartCount )
+ perPartTransparencyOtherHits = math.pow( ((0.5 * TARGET_TRANSPARENCY_PERIPHERAL) + (0.5 * TARGET_TRANSPARENCY_PERIPHERAL / hitPartCount)), 1 / hitPartCount )
+ end
+ end
+
+ -- Now get all the parts hit by all the rays
+ hitParts = self.camera:GetPartsObscuringTarget(castPoints, ignoreList)
+
+ local partTargetTransparency = {}
+
+ -- Include decals and textures
+ for i = 1, #hitParts do
+ local hitPart = hitParts[i]
+
+ partTargetTransparency[hitPart] =headTorsoRayHitParts[hitPart] and perPartTransparencyHeadTorsoHits or perPartTransparencyOtherHits
+
+ -- If the part is not already as transparent or more transparent than what invisicam requires, add it to the list of
+ -- parts to be modified by invisicam
+ if hitPart.Transparency < partTargetTransparency[hitPart] then
+ add(hitPart)
+ end
+
+ -- Check all decals and textures on the part
+ for _, child in pairs(hitPart:GetChildren()) do
+ if child:IsA('Decal') or child:IsA('Texture') then
+ if (child.Transparency < partTargetTransparency[hitPart]) then
+ partTargetTransparency[child] = partTargetTransparency[hitPart]
+ add(child)
+ end
+ end
+ end
+ end
+
+ -- Invisibilize objects that are in the way, restore those that aren't anymore
+ for hitPart, originalLTM in pairs(self.savedHits) do
+ if currentHits[hitPart] then
+ -- LocalTransparencyModifier gets whatever value is required to print the part's total transparency to equal perPartTransparency
+ hitPart.LocalTransparencyModifier = (hitPart.Transparency < 1) and ((partTargetTransparency[hitPart] - hitPart.Transparency) / (1.0 - hitPart.Transparency)) or 0
+ else -- Restore original pre-invisicam value of LTM
+ hitPart.LocalTransparencyModifier = originalLTM
+ self.savedHits[hitPart] = nil
+ end
+ end
+
+ -- Invisicam does not change the camera values
+ return desiredCameraCFrame, desiredCameraFocus
+ end
+
+ return Invisicam
+end
+
+function _LegacyCamera()
+
+ local ZERO_VECTOR2 = Vector2.new(0,0)
+
+ local Util = _CameraUtils()
+
+ --[[ Services ]]--
+ local PlayersService = game:GetService('Players')
+
+ --[[ The Module ]]--
+ local BaseCamera = _BaseCamera()
+ local LegacyCamera = setmetatable({}, BaseCamera)
+ LegacyCamera.__index = LegacyCamera
+
+ function LegacyCamera.new()
+ local self = setmetatable(BaseCamera.new(), LegacyCamera)
+
+ self.cameraType = Enum.CameraType.Fixed
+ self.lastUpdate = tick()
+ self.lastDistanceToSubject = nil
+
+ return self
+ end
+
+ function LegacyCamera:GetModuleName()
+ return "LegacyCamera"
+ end
+
+ --[[ Functions overridden from BaseCamera ]]--
+ function LegacyCamera:SetCameraToSubjectDistance(desiredSubjectDistance)
+ return BaseCamera.SetCameraToSubjectDistance(self,desiredSubjectDistance)
+ end
+
+ function LegacyCamera:Update(dt)
+
+ -- Cannot update until cameraType has been set
+ if not self.cameraType then return end
+
+ local now = tick()
+ local timeDelta = (now - self.lastUpdate)
+ local camera = workspace.CurrentCamera
+ local newCameraCFrame = camera.CFrame
+ local newCameraFocus = camera.Focus
+ local player = PlayersService.LocalPlayer
+
+ if self.lastUpdate == nil or timeDelta > 1 then
+ self.lastDistanceToSubject = nil
+ end
+ local subjectPosition = self:GetSubjectPosition()
+
+ if self.cameraType == Enum.CameraType.Fixed then
+ if self.lastUpdate then
+ -- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from
+ local delta = math.min(0.1, now - self.lastUpdate)
+ local gamepadRotation = self:UpdateGamepad()
+ self.rotateInput = self.rotateInput + (gamepadRotation * delta)
+ end
+
+ if subjectPosition and player and camera then
+ local distanceToSubject = self:GetCameraToSubjectDistance()
+ local newLookVector = self:CalculateNewLookVector()
+ self.rotateInput = ZERO_VECTOR2
+
+ newCameraFocus = camera.Focus -- Fixed camera does not change focus
+ newCameraCFrame = CFrame.new(camera.CFrame.p, camera.CFrame.p + (distanceToSubject * newLookVector))
+ end
+ elseif self.cameraType == Enum.CameraType.Attach then
+ if subjectPosition and camera then
+ local distanceToSubject = self:GetCameraToSubjectDistance()
+ local humanoid = self:GetHumanoid()
+ if self.lastUpdate and humanoid and humanoid.RootPart then
+
+ -- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from
+ local delta = math.min(0.1, now - self.lastUpdate)
+ local gamepadRotation = self:UpdateGamepad()
+ self.rotateInput = self.rotateInput + (gamepadRotation * delta)
+
+ local forwardVector = humanoid.RootPart.CFrame.lookVector
+
+ local y = Util.GetAngleBetweenXZVectors(forwardVector, self:GetCameraLookVector())
+ if Util.IsFinite(y) then
+ -- Preserve vertical rotation from user input
+ self.rotateInput = Vector2.new(y, self.rotateInput.Y)
+ end
+ end
+
+ local newLookVector = self:CalculateNewLookVector()
+ self.rotateInput = ZERO_VECTOR2
+
+ newCameraFocus = CFrame.new(subjectPosition)
+ newCameraCFrame = CFrame.new(subjectPosition - (distanceToSubject * newLookVector), subjectPosition)
+ end
+ elseif self.cameraType == Enum.CameraType.Watch then
+ if subjectPosition and player and camera then
+ local cameraLook = nil
+
+ local humanoid = self:GetHumanoid()
+ if humanoid and humanoid.RootPart then
+ local diffVector = subjectPosition - camera.CFrame.p
+ cameraLook = diffVector.unit
+
+ if self.lastDistanceToSubject and self.lastDistanceToSubject == self:GetCameraToSubjectDistance() then
+ -- Don't clobber the zoom if they zoomed the camera
+ local newDistanceToSubject = diffVector.magnitude
+ self:SetCameraToSubjectDistance(newDistanceToSubject)
+ end
+ end
+
+ local distanceToSubject = self:GetCameraToSubjectDistance()
+ local newLookVector = self:CalculateNewLookVector(cameraLook)
+ self.rotateInput = ZERO_VECTOR2
+
+ newCameraFocus = CFrame.new(subjectPosition)
+ newCameraCFrame = CFrame.new(subjectPosition - (distanceToSubject * newLookVector), subjectPosition)
+
+ self.lastDistanceToSubject = distanceToSubject
+ end
+ else
+ -- Unsupported type, return current values unchanged
+ return camera.CFrame, camera.Focus
+ end
+
+ self.lastUpdate = now
+ return newCameraCFrame, newCameraFocus
+ end
+
+ return LegacyCamera
+end
+
+function _OrbitalCamera()
+
+ -- Local private variables and constants
+ local UNIT_Z = Vector3.new(0,0,1)
+ local X1_Y0_Z1 = Vector3.new(1,0,1) --Note: not a unit vector, used for projecting onto XZ plane
+ local ZERO_VECTOR3 = Vector3.new(0,0,0)
+ local ZERO_VECTOR2 = Vector2.new(0,0)
+ local TAU = 2 * math.pi
+
+ --[[ Gamepad Support ]]--
+ local THUMBSTICK_DEADZONE = 0.2
+
+ -- Do not edit these values, they are not the developer-set limits, they are limits
+ -- to the values the camera system equations can correctly handle
+ local MIN_ALLOWED_ELEVATION_DEG = -80
+ local MAX_ALLOWED_ELEVATION_DEG = 80
+
+ local externalProperties = {}
+ externalProperties["InitialDistance"] = 25
+ externalProperties["MinDistance"] = 10
+ externalProperties["MaxDistance"] = 100
+ externalProperties["InitialElevation"] = 35
+ externalProperties["MinElevation"] = 35
+ externalProperties["MaxElevation"] = 35
+ externalProperties["ReferenceAzimuth"] = -45 -- Angle around the Y axis where the camera starts. -45 offsets the camera in the -X and +Z directions equally
+ externalProperties["CWAzimuthTravel"] = 90 -- How many degrees the camera is allowed to rotate from the reference position, CW as seen from above
+ externalProperties["CCWAzimuthTravel"] = 90 -- How many degrees the camera is allowed to rotate from the reference position, CCW as seen from above
+ externalProperties["UseAzimuthLimits"] = false -- Full rotation around Y axis available by default
+
+ local Util = _CameraUtils()
+
+ --[[ Services ]]--
+ local PlayersService = game:GetService('Players')
+ local VRService = game:GetService("VRService")
+
+ --[[ The Module ]]--
+ local BaseCamera = _BaseCamera()
+ local OrbitalCamera = setmetatable({}, BaseCamera)
+ OrbitalCamera.__index = OrbitalCamera
+
+
+ function OrbitalCamera.new()
+ local self = setmetatable(BaseCamera.new(), OrbitalCamera)
+
+ self.lastUpdate = tick()
+
+ -- OrbitalCamera-specific members
+ self.changedSignalConnections = {}
+ self.refAzimuthRad = nil
+ self.curAzimuthRad = nil
+ self.minAzimuthAbsoluteRad = nil
+ self.maxAzimuthAbsoluteRad = nil
+ self.useAzimuthLimits = nil
+ self.curElevationRad = nil
+ self.minElevationRad = nil
+ self.maxElevationRad = nil
+ self.curDistance = nil
+ self.minDistance = nil
+ self.maxDistance = nil
+
+ -- Gamepad
+ self.r3ButtonDown = false
+ self.l3ButtonDown = false
+ self.gamepadDollySpeedMultiplier = 1
+
+ self.lastUserPanCamera = tick()
+
+ self.externalProperties = {}
+ self.externalProperties["InitialDistance"] = 25
+ self.externalProperties["MinDistance"] = 10
+ self.externalProperties["MaxDistance"] = 100
+ self.externalProperties["InitialElevation"] = 35
+ self.externalProperties["MinElevation"] = 35
+ self.externalProperties["MaxElevation"] = 35
+ self.externalProperties["ReferenceAzimuth"] = -45 -- Angle around the Y axis where the camera starts. -45 offsets the camera in the -X and +Z directions equally
+ self.externalProperties["CWAzimuthTravel"] = 90 -- How many degrees the camera is allowed to rotate from the reference position, CW as seen from above
+ self.externalProperties["CCWAzimuthTravel"] = 90 -- How many degrees the camera is allowed to rotate from the reference position, CCW as seen from above
+ self.externalProperties["UseAzimuthLimits"] = false -- Full rotation around Y axis available by default
+ self:LoadNumberValueParameters()
+
+ return self
+ end
+
+ function OrbitalCamera:LoadOrCreateNumberValueParameter(name, valueType, updateFunction)
+ local valueObj = script:FindFirstChild(name)
+
+ if valueObj and valueObj:isA(valueType) then
+ -- Value object exists and is the correct type, use its value
+ self.externalProperties[name] = valueObj.Value
+ elseif self.externalProperties[name] ~= nil then
+ -- Create missing (or replace incorrectly-typed) valueObject with default value
+ valueObj = Instance.new(valueType)
+ valueObj.Name = name
+ valueObj.Parent = script
+ valueObj.Value = self.externalProperties[name]
+ else
+ print("externalProperties table has no entry for ",name)
+ return
+ end
+
+ if updateFunction then
+ if self.changedSignalConnections[name] then
+ self.changedSignalConnections[name]:Disconnect()
+ end
+ self.changedSignalConnections[name] = valueObj.Changed:Connect(function(newValue)
+ self.externalProperties[name] = newValue
+ updateFunction(self)
+ end)
+ end
+ end
+
+ function OrbitalCamera:SetAndBoundsCheckAzimuthValues()
+ self.minAzimuthAbsoluteRad = math.rad(self.externalProperties["ReferenceAzimuth"]) - math.abs(math.rad(self.externalProperties["CWAzimuthTravel"]))
+ self.maxAzimuthAbsoluteRad = math.rad(self.externalProperties["ReferenceAzimuth"]) + math.abs(math.rad(self.externalProperties["CCWAzimuthTravel"]))
+ self.useAzimuthLimits = self.externalProperties["UseAzimuthLimits"]
+ if self.useAzimuthLimits then
+ self.curAzimuthRad = math.max(self.curAzimuthRad, self.minAzimuthAbsoluteRad)
+ self.curAzimuthRad = math.min(self.curAzimuthRad, self.maxAzimuthAbsoluteRad)
+ end
+ end
+
+ function OrbitalCamera:SetAndBoundsCheckElevationValues()
+ -- These degree values are the direct user input values. It is deliberate that they are
+ -- ranged checked only against the extremes, and not against each other. Any time one
+ -- is changed, both of the internal values in radians are recalculated. This allows for
+ -- A developer to change the values in any order and for the end results to be that the
+ -- internal values adjust to match intent as best as possible.
+ local minElevationDeg = math.max(self.externalProperties["MinElevation"], MIN_ALLOWED_ELEVATION_DEG)
+ local maxElevationDeg = math.min(self.externalProperties["MaxElevation"], MAX_ALLOWED_ELEVATION_DEG)
+
+ -- Set internal values in radians
+ self.minElevationRad = math.rad(math.min(minElevationDeg, maxElevationDeg))
+ self.maxElevationRad = math.rad(math.max(minElevationDeg, maxElevationDeg))
+ self.curElevationRad = math.max(self.curElevationRad, self.minElevationRad)
+ self.curElevationRad = math.min(self.curElevationRad, self.maxElevationRad)
+ end
+
+ function OrbitalCamera:SetAndBoundsCheckDistanceValues()
+ self.minDistance = self.externalProperties["MinDistance"]
+ self.maxDistance = self.externalProperties["MaxDistance"]
+ self.curDistance = math.max(self.curDistance, self.minDistance)
+ self.curDistance = math.min(self.curDistance, self.maxDistance)
+ end
+
+ -- This loads from, or lazily creates, NumberValue objects for exposed parameters
+ function OrbitalCamera:LoadNumberValueParameters()
+ -- These initial values do not require change listeners since they are read only once
+ self:LoadOrCreateNumberValueParameter("InitialElevation", "NumberValue", nil)
+ self:LoadOrCreateNumberValueParameter("InitialDistance", "NumberValue", nil)
+
+ -- Note: ReferenceAzimuth is also used as an initial value, but needs a change listener because it is used in the calculation of the limits
+ self:LoadOrCreateNumberValueParameter("ReferenceAzimuth", "NumberValue", self.SetAndBoundsCheckAzimuthValue)
+ self:LoadOrCreateNumberValueParameter("CWAzimuthTravel", "NumberValue", self.SetAndBoundsCheckAzimuthValues)
+ self:LoadOrCreateNumberValueParameter("CCWAzimuthTravel", "NumberValue", self.SetAndBoundsCheckAzimuthValues)
+ self:LoadOrCreateNumberValueParameter("MinElevation", "NumberValue", self.SetAndBoundsCheckElevationValues)
+ self:LoadOrCreateNumberValueParameter("MaxElevation", "NumberValue", self.SetAndBoundsCheckElevationValues)
+ self:LoadOrCreateNumberValueParameter("MinDistance", "NumberValue", self.SetAndBoundsCheckDistanceValues)
+ self:LoadOrCreateNumberValueParameter("MaxDistance", "NumberValue", self.SetAndBoundsCheckDistanceValues)
+ self:LoadOrCreateNumberValueParameter("UseAzimuthLimits", "BoolValue", self.SetAndBoundsCheckAzimuthValues)
+
+ -- Internal values set (in radians, from degrees), plus sanitization
+ self.curAzimuthRad = math.rad(self.externalProperties["ReferenceAzimuth"])
+ self.curElevationRad = math.rad(self.externalProperties["InitialElevation"])
+ self.curDistance = self.externalProperties["InitialDistance"]
+
+ self:SetAndBoundsCheckAzimuthValues()
+ self:SetAndBoundsCheckElevationValues()
+ self:SetAndBoundsCheckDistanceValues()
+ end
+
+ function OrbitalCamera:GetModuleName()
+ return "OrbitalCamera"
+ end
+
+ function OrbitalCamera:SetInitialOrientation(humanoid)
+ if not humanoid or not humanoid.RootPart then
+ warn("OrbitalCamera could not set initial orientation due to missing humanoid")
+ return
+ end
+ local newDesiredLook = (humanoid.RootPart.CFrame.lookVector - Vector3.new(0,0.23,0)).unit
+ local horizontalShift = Util.GetAngleBetweenXZVectors(newDesiredLook, self:GetCameraLookVector())
+ local vertShift = math.asin(self:GetCameraLookVector().y) - math.asin(newDesiredLook.y)
+ if not Util.IsFinite(horizontalShift) then
+ horizontalShift = 0
+ end
+ if not Util.IsFinite(vertShift) then
+ vertShift = 0
+ end
+ self.rotateInput = Vector2.new(horizontalShift, vertShift)
+ end
+
+ --[[ Functions of BaseCamera that are overridden by OrbitalCamera ]]--
+ function OrbitalCamera:GetCameraToSubjectDistance()
+ return self.curDistance
+ end
+
+ function OrbitalCamera:SetCameraToSubjectDistance(desiredSubjectDistance)
+ print("OrbitalCamera SetCameraToSubjectDistance ",desiredSubjectDistance)
+ local player = PlayersService.LocalPlayer
+ if player then
+ self.currentSubjectDistance = math.clamp(desiredSubjectDistance, self.minDistance, self.maxDistance)
+
+ -- OrbitalCamera is not allowed to go into the first-person range
+ self.currentSubjectDistance = math.max(self.currentSubjectDistance, self.FIRST_PERSON_DISTANCE_THRESHOLD)
+ end
+ self.inFirstPerson = false
+ self:UpdateMouseBehavior()
+ return self.currentSubjectDistance
+ end
+
+ function OrbitalCamera:CalculateNewLookVector(suppliedLookVector, xyRotateVector)
+ local currLookVector = suppliedLookVector or self:GetCameraLookVector()
+ local currPitchAngle = math.asin(currLookVector.y)
+ local yTheta = math.clamp(xyRotateVector.y, currPitchAngle - math.rad(MAX_ALLOWED_ELEVATION_DEG), currPitchAngle - math.rad(MIN_ALLOWED_ELEVATION_DEG))
+ local constrainedRotateInput = Vector2.new(xyRotateVector.x, yTheta)
+ local startCFrame = CFrame.new(ZERO_VECTOR3, currLookVector)
+ local newLookVector = (CFrame.Angles(0, -constrainedRotateInput.x, 0) * startCFrame * CFrame.Angles(-constrainedRotateInput.y,0,0)).lookVector
+ return newLookVector
+ end
+
+ function OrbitalCamera:GetGamepadPan(name, state, input)
+ if input.UserInputType == self.activeGamepad and input.KeyCode == Enum.KeyCode.Thumbstick2 then
+ if self.r3ButtonDown or self.l3ButtonDown then
+ -- R3 or L3 Thumbstick is depressed, right stick controls dolly in/out
+ if (input.Position.Y > THUMBSTICK_DEADZONE) then
+ self.gamepadDollySpeedMultiplier = 0.96
+ elseif (input.Position.Y < -THUMBSTICK_DEADZONE) then
+ self.gamepadDollySpeedMultiplier = 1.04
+ else
+ self.gamepadDollySpeedMultiplier = 1.00
+ end
+ else
+ if state == Enum.UserInputState.Cancel then
+ self.gamepadPanningCamera = ZERO_VECTOR2
+ return
+ end
+
+ local inputVector = Vector2.new(input.Position.X, -input.Position.Y)
+ if inputVector.magnitude > THUMBSTICK_DEADZONE then
+ self.gamepadPanningCamera = Vector2.new(input.Position.X, -input.Position.Y)
+ else
+ self.gamepadPanningCamera = ZERO_VECTOR2
+ end
+ end
+ return Enum.ContextActionResult.Sink
+ end
+ return Enum.ContextActionResult.Pass
+ end
+
+ function OrbitalCamera:DoGamepadZoom(name, state, input)
+ if input.UserInputType == self.activeGamepad and (input.KeyCode == Enum.KeyCode.ButtonR3 or input.KeyCode == Enum.KeyCode.ButtonL3) then
+ if (state == Enum.UserInputState.Begin) then
+ self.r3ButtonDown = input.KeyCode == Enum.KeyCode.ButtonR3
+ self.l3ButtonDown = input.KeyCode == Enum.KeyCode.ButtonL3
+ elseif (state == Enum.UserInputState.End) then
+ if (input.KeyCode == Enum.KeyCode.ButtonR3) then
+ self.r3ButtonDown = false
+ elseif (input.KeyCode == Enum.KeyCode.ButtonL3) then
+ self.l3ButtonDown = false
+ end
+ if (not self.r3ButtonDown) and (not self.l3ButtonDown) then
+ self.gamepadDollySpeedMultiplier = 1.00
+ end
+ end
+ return Enum.ContextActionResult.Sink
+ end
+ return Enum.ContextActionResult.Pass
+ end
+
+ function OrbitalCamera:BindGamepadInputActions()
+ self:BindAction("OrbitalCamGamepadPan", function(name, state, input) return self:GetGamepadPan(name, state, input) end,
+ false, Enum.KeyCode.Thumbstick2)
+ self:BindAction("OrbitalCamGamepadZoom", function(name, state, input) return self:DoGamepadZoom(name, state, input) end,
+ false, Enum.KeyCode.ButtonR3, Enum.KeyCode.ButtonL3)
+ end
+
+
+ -- [[ Update ]]--
+ function OrbitalCamera:Update(dt)
+ local now = tick()
+ local timeDelta = (now - self.lastUpdate)
+ local userPanningTheCamera = (self.UserPanningTheCamera == true)
+ local camera = workspace.CurrentCamera
+ local newCameraCFrame = camera.CFrame
+ local newCameraFocus = camera.Focus
+ local player = PlayersService.LocalPlayer
+ local cameraSubject = camera and camera.CameraSubject
+ local isInVehicle = cameraSubject and cameraSubject:IsA('VehicleSeat')
+ local isOnASkateboard = cameraSubject and cameraSubject:IsA('SkateboardPlatform')
+
+ if self.lastUpdate == nil or timeDelta > 1 then
+ self.lastCameraTransform = nil
+ end
+
+ if self.lastUpdate then
+ local gamepadRotation = self:UpdateGamepad()
+
+ if self:ShouldUseVRRotation() then
+ self.RotateInput = self.RotateInput + self:GetVRRotationInput()
+ else
+ -- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from
+ local delta = math.min(0.1, timeDelta)
+
+ if gamepadRotation ~= ZERO_VECTOR2 then
+ userPanningTheCamera = true
+ self.rotateInput = self.rotateInput + (gamepadRotation * delta)
+ end
+
+ local angle = 0
+ if not (isInVehicle or isOnASkateboard) then
+ angle = angle + (self.TurningLeft and -120 or 0)
+ angle = angle + (self.TurningRight and 120 or 0)
+ end
+
+ if angle ~= 0 then
+ self.rotateInput = self.rotateInput + Vector2.new(math.rad(angle * delta), 0)
+ userPanningTheCamera = true
+ end
+ end
+ end
+
+ -- Reset tween speed if user is panning
+ if userPanningTheCamera then
+ self.lastUserPanCamera = tick()
+ end
+
+ local subjectPosition = self:GetSubjectPosition()
+
+ if subjectPosition and player and camera then
+
+ -- Process any dollying being done by gamepad
+ -- TODO: Move this
+ if self.gamepadDollySpeedMultiplier ~= 1 then
+ self:SetCameraToSubjectDistance(self.currentSubjectDistance * self.gamepadDollySpeedMultiplier)
+ end
+
+ local VREnabled = VRService.VREnabled
+ newCameraFocus = VREnabled and self:GetVRFocus(subjectPosition, timeDelta) or CFrame.new(subjectPosition)
+
+ local cameraFocusP = newCameraFocus.p
+ if VREnabled and not self:IsInFirstPerson() then
+ local cameraHeight = self:GetCameraHeight()
+ local vecToSubject = (subjectPosition - camera.CFrame.p)
+ local distToSubject = vecToSubject.magnitude
+
+ -- Only move the camera if it exceeded a maximum distance to the subject in VR
+ if distToSubject > self.currentSubjectDistance or self.rotateInput.x ~= 0 then
+ local desiredDist = math.min(distToSubject, self.currentSubjectDistance)
+
+ -- Note that CalculateNewLookVector is overridden from BaseCamera
+ vecToSubject = self:CalculateNewLookVector(vecToSubject.unit * X1_Y0_Z1, Vector2.new(self.rotateInput.x, 0)) * desiredDist
+
+ local newPos = cameraFocusP - vecToSubject
+ local desiredLookDir = camera.CFrame.lookVector
+ if self.rotateInput.x ~= 0 then
+ desiredLookDir = vecToSubject
+ end
+ local lookAt = Vector3.new(newPos.x + desiredLookDir.x, newPos.y, newPos.z + desiredLookDir.z)
+ self.RotateInput = ZERO_VECTOR2
+
+ newCameraCFrame = CFrame.new(newPos, lookAt) + Vector3.new(0, cameraHeight, 0)
+ end
+ else
+ -- self.RotateInput is a Vector2 of mouse movement deltas since last update
+ self.curAzimuthRad = self.curAzimuthRad - self.rotateInput.x
+
+ if self.useAzimuthLimits then
+ self.curAzimuthRad = math.clamp(self.curAzimuthRad, self.minAzimuthAbsoluteRad, self.maxAzimuthAbsoluteRad)
+ else
+ self.curAzimuthRad = (self.curAzimuthRad ~= 0) and (math.sign(self.curAzimuthRad) * (math.abs(self.curAzimuthRad) % TAU)) or 0
+ end
+
+ self.curElevationRad = math.clamp(self.curElevationRad + self.rotateInput.y, self.minElevationRad, self.maxElevationRad)
+
+ local cameraPosVector = self.currentSubjectDistance * ( CFrame.fromEulerAnglesYXZ( -self.curElevationRad, self.curAzimuthRad, 0 ) * UNIT_Z )
+ local camPos = subjectPosition + cameraPosVector
+
+ newCameraCFrame = CFrame.new(camPos, subjectPosition)
+
+ self.rotateInput = ZERO_VECTOR2
+ end
+
+ self.lastCameraTransform = newCameraCFrame
+ self.lastCameraFocus = newCameraFocus
+ if (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then
+ self.lastSubjectCFrame = cameraSubject.CFrame
+ else
+ self.lastSubjectCFrame = nil
+ end
+ end
+
+ self.lastUpdate = now
+ return newCameraCFrame, newCameraFocus
+ end
+
+ return OrbitalCamera
+end
+
+function _ClassicCamera()
+
+ -- Local private variables and constants
+ local ZERO_VECTOR2 = Vector2.new(0,0)
+
+ local tweenAcceleration = math.rad(220) --Radians/Second^2
+ local tweenSpeed = math.rad(0) --Radians/Second
+ local tweenMaxSpeed = math.rad(250) --Radians/Second
+ local TIME_BEFORE_AUTO_ROTATE = 2.0 --Seconds, used when auto-aligning camera with vehicles
+
+ local INITIAL_CAMERA_ANGLE = CFrame.fromOrientation(math.rad(-15), 0, 0)
+
+ local FFlagUserCameraToggle do
+ local success, result = pcall(function()
+ return UserSettings():IsUserFeatureEnabled("UserCameraToggle")
+ end)
+ FFlagUserCameraToggle = success and result
+ end
+
+ --[[ Services ]]--
+ local PlayersService = game:GetService('Players')
+ local VRService = game:GetService("VRService")
+
+ local CameraInput = _CameraInput()
+ local Util = _CameraUtils()
+
+ --[[ The Module ]]--
+ local BaseCamera = _BaseCamera()
+ local ClassicCamera = setmetatable({}, BaseCamera)
+ ClassicCamera.__index = ClassicCamera
+
+ function ClassicCamera.new()
+ local self = setmetatable(BaseCamera.new(), ClassicCamera)
+
+ self.isFollowCamera = false
+ self.isCameraToggle = false
+ self.lastUpdate = tick()
+ self.cameraToggleSpring = Util.Spring.new(5, 0)
+
+ return self
+ end
+
+ function ClassicCamera:GetCameraToggleOffset(dt)
+ assert(FFlagUserCameraToggle)
+
+ if self.isCameraToggle then
+ local zoom = self.currentSubjectDistance
+
+ if CameraInput.getTogglePan() then
+ self.cameraToggleSpring.goal = math.clamp(Util.map(zoom, 0.5, self.FIRST_PERSON_DISTANCE_THRESHOLD, 0, 1), 0, 1)
+ else
+ self.cameraToggleSpring.goal = 0
+ end
+
+ local distanceOffset = math.clamp(Util.map(zoom, 0.5, 64, 0, 1), 0, 1) + 1
+ return Vector3.new(0, self.cameraToggleSpring:step(dt)*distanceOffset, 0)
+ end
+
+ return Vector3.new()
+ end
+
+ -- Movement mode standardized to Enum.ComputerCameraMovementMode values
+ function ClassicCamera:SetCameraMovementMode(cameraMovementMode)
+ BaseCamera.SetCameraMovementMode(self, cameraMovementMode)
+
+ self.isFollowCamera = cameraMovementMode == Enum.ComputerCameraMovementMode.Follow
+ self.isCameraToggle = cameraMovementMode == Enum.ComputerCameraMovementMode.CameraToggle
+ end
+
+ function ClassicCamera:Update()
+ local now = tick()
+ local timeDelta = now - self.lastUpdate
+
+ local camera = workspace.CurrentCamera
+ local newCameraCFrame = camera.CFrame
+ local newCameraFocus = camera.Focus
+
+ local overrideCameraLookVector = nil
+ if self.resetCameraAngle then
+ local rootPart = self:GetHumanoidRootPart()
+ if rootPart then
+ overrideCameraLookVector = (rootPart.CFrame * INITIAL_CAMERA_ANGLE).lookVector
+ else
+ overrideCameraLookVector = INITIAL_CAMERA_ANGLE.lookVector
+ end
+ self.resetCameraAngle = false
+ end
+
+ local player = PlayersService.LocalPlayer
+ local humanoid = self:GetHumanoid()
+ local cameraSubject = camera.CameraSubject
+ local isInVehicle = cameraSubject and cameraSubject:IsA('VehicleSeat')
+ local isOnASkateboard = cameraSubject and cameraSubject:IsA('SkateboardPlatform')
+ local isClimbing = humanoid and humanoid:GetState() == Enum.HumanoidStateType.Climbing
+
+ if self.lastUpdate == nil or timeDelta > 1 then
+ self.lastCameraTransform = nil
+ end
+
+ if self.lastUpdate then
+ local gamepadRotation = self:UpdateGamepad()
+
+ if self:ShouldUseVRRotation() then
+ self.rotateInput = self.rotateInput + self:GetVRRotationInput()
+ else
+ -- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from
+ local delta = math.min(0.1, timeDelta)
+
+ if gamepadRotation ~= ZERO_VECTOR2 then
+ self.rotateInput = self.rotateInput + (gamepadRotation * delta)
+ end
+
+ local angle = 0
+ if not (isInVehicle or isOnASkateboard) then
+ angle = angle + (self.turningLeft and -120 or 0)
+ angle = angle + (self.turningRight and 120 or 0)
+ end
+
+ if angle ~= 0 then
+ self.rotateInput = self.rotateInput + Vector2.new(math.rad(angle * delta), 0)
+ end
+ end
+ end
+
+ local cameraHeight = self:GetCameraHeight()
+
+ -- Reset tween speed if user is panning
+ if self.userPanningTheCamera then
+ tweenSpeed = 0
+ self.lastUserPanCamera = tick()
+ end
+
+ local userRecentlyPannedCamera = now - self.lastUserPanCamera < TIME_BEFORE_AUTO_ROTATE
+ local subjectPosition = self:GetSubjectPosition()
+
+ if subjectPosition and player and camera then
+ local zoom = self:GetCameraToSubjectDistance()
+ if zoom < 0.5 then
+ zoom = 0.5
+ end
+
+ if self:GetIsMouseLocked() and not self:IsInFirstPerson() then
+ -- We need to use the right vector of the camera after rotation, not before
+ local newLookCFrame = self:CalculateNewLookCFrame(overrideCameraLookVector)
+
+ local offset = self:GetMouseLockOffset()
+ local cameraRelativeOffset = offset.X * newLookCFrame.rightVector + offset.Y * newLookCFrame.upVector + offset.Z * newLookCFrame.lookVector
+
+ --offset can be NAN, NAN, NAN if newLookVector has only y component
+ if Util.IsFiniteVector3(cameraRelativeOffset) then
+ subjectPosition = subjectPosition + cameraRelativeOffset
+ end
+ else
+ if not self.userPanningTheCamera and self.lastCameraTransform then
+
+ local isInFirstPerson = self:IsInFirstPerson()
+
+ if (isInVehicle or isOnASkateboard or (self.isFollowCamera and isClimbing)) and self.lastUpdate and humanoid and humanoid.Torso then
+ if isInFirstPerson then
+ if self.lastSubjectCFrame and (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then
+ local y = -Util.GetAngleBetweenXZVectors(self.lastSubjectCFrame.lookVector, cameraSubject.CFrame.lookVector)
+ if Util.IsFinite(y) then
+ self.rotateInput = self.rotateInput + Vector2.new(y, 0)
+ end
+ tweenSpeed = 0
+ end
+ elseif not userRecentlyPannedCamera then
+ local forwardVector = humanoid.Torso.CFrame.lookVector
+ if isOnASkateboard then
+ forwardVector = cameraSubject.CFrame.lookVector
+ end
+
+ tweenSpeed = math.clamp(tweenSpeed + tweenAcceleration * timeDelta, 0, tweenMaxSpeed)
+
+ local percent = math.clamp(tweenSpeed * timeDelta, 0, 1)
+ if self:IsInFirstPerson() and not (self.isFollowCamera and self.isClimbing) then
+ percent = 1
+ end
+
+ local y = Util.GetAngleBetweenXZVectors(forwardVector, self:GetCameraLookVector())
+ if Util.IsFinite(y) and math.abs(y) > 0.0001 then
+ self.rotateInput = self.rotateInput + Vector2.new(y * percent, 0)
+ end
+ end
+
+ elseif self.isFollowCamera and (not (isInFirstPerson or userRecentlyPannedCamera) and not VRService.VREnabled) then
+ -- Logic that was unique to the old FollowCamera module
+ local lastVec = -(self.lastCameraTransform.p - subjectPosition)
+
+ local y = Util.GetAngleBetweenXZVectors(lastVec, self:GetCameraLookVector())
+
+ -- This cutoff is to decide if the humanoid's angle of movement,
+ -- relative to the camera's look vector, is enough that
+ -- we want the camera to be following them. The point is to provide
+ -- a sizable dead zone to allow more precise forward movements.
+ local thetaCutoff = 0.4
+
+ -- Check for NaNs
+ if Util.IsFinite(y) and math.abs(y) > 0.0001 and math.abs(y) > thetaCutoff * timeDelta then
+ self.rotateInput = self.rotateInput + Vector2.new(y, 0)
+ end
+ end
+ end
+ end
+
+ if not self.isFollowCamera then
+ local VREnabled = VRService.VREnabled
+
+ if VREnabled then
+ newCameraFocus = self:GetVRFocus(subjectPosition, timeDelta)
+ else
+ newCameraFocus = CFrame.new(subjectPosition)
+ end
+
+ local cameraFocusP = newCameraFocus.p
+ if VREnabled and not self:IsInFirstPerson() then
+ local vecToSubject = (subjectPosition - camera.CFrame.p)
+ local distToSubject = vecToSubject.magnitude
+
+ -- Only move the camera if it exceeded a maximum distance to the subject in VR
+ if distToSubject > zoom or self.rotateInput.x ~= 0 then
+ local desiredDist = math.min(distToSubject, zoom)
+ vecToSubject = self:CalculateNewLookVectorVR() * desiredDist
+ local newPos = cameraFocusP - vecToSubject
+ local desiredLookDir = camera.CFrame.lookVector
+ if self.rotateInput.x ~= 0 then
+ desiredLookDir = vecToSubject
+ end
+ local lookAt = Vector3.new(newPos.x + desiredLookDir.x, newPos.y, newPos.z + desiredLookDir.z)
+ self.rotateInput = ZERO_VECTOR2
+
+ newCameraCFrame = CFrame.new(newPos, lookAt) + Vector3.new(0, cameraHeight, 0)
+ end
+ else
+ local newLookVector = self:CalculateNewLookVector(overrideCameraLookVector)
+ self.rotateInput = ZERO_VECTOR2
+ newCameraCFrame = CFrame.new(cameraFocusP - (zoom * newLookVector), cameraFocusP)
+ end
+ else -- is FollowCamera
+ local newLookVector = self:CalculateNewLookVector(overrideCameraLookVector)
+ self.rotateInput = ZERO_VECTOR2
+
+ if VRService.VREnabled then
+ newCameraFocus = self:GetVRFocus(subjectPosition, timeDelta)
+ else
+ newCameraFocus = CFrame.new(subjectPosition)
+ end
+ newCameraCFrame = CFrame.new(newCameraFocus.p - (zoom * newLookVector), newCameraFocus.p) + Vector3.new(0, cameraHeight, 0)
+ end
+
+ if FFlagUserCameraToggle then
+ local toggleOffset = self:GetCameraToggleOffset(timeDelta)
+ newCameraFocus = newCameraFocus + toggleOffset
+ newCameraCFrame = newCameraCFrame + toggleOffset
+ end
+
+ self.lastCameraTransform = newCameraCFrame
+ self.lastCameraFocus = newCameraFocus
+ if (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then
+ self.lastSubjectCFrame = cameraSubject.CFrame
+ else
+ self.lastSubjectCFrame = nil
+ end
+ end
+
+ self.lastUpdate = now
+ return newCameraCFrame, newCameraFocus
+ end
+
+ function ClassicCamera:EnterFirstPerson()
+ self.inFirstPerson = true
+ self:UpdateMouseBehavior()
+ end
+
+ function ClassicCamera:LeaveFirstPerson()
+ self.inFirstPerson = false
+ self:UpdateMouseBehavior()
+ end
+
+ return ClassicCamera
+end
+
+function _CameraUtils()
+
+ local CameraUtils = {}
+
+ local FFlagUserCameraToggle do
+ local success, result = pcall(function()
+ return UserSettings():IsUserFeatureEnabled("UserCameraToggle")
+ end)
+ FFlagUserCameraToggle = success and result
+ end
+
+ local function round(num)
+ return math.floor(num + 0.5)
+ end
+
+ -- Critically damped spring class for fluid motion effects
+ local Spring = {} do
+ Spring.__index = Spring
+
+ -- Initialize to a given undamped frequency and default position
+ function Spring.new(freq, pos)
+ return setmetatable({
+ freq = freq,
+ goal = pos,
+ pos = pos,
+ vel = 0,
+ }, Spring)
+ end
+
+ -- Advance the spring simulation by `dt` seconds
+ function Spring:step(dt)
+ local f = self.freq*2*math.pi
+ local g = self.goal
+ local p0 = self.pos
+ local v0 = self.vel
+
+ local offset = p0 - g
+ local decay = math.exp(-f*dt)
+
+ local p1 = (offset*(1 + f*dt) + v0*dt)*decay + g
+ local v1 = (v0*(1 - f*dt) - offset*(f*f*dt))*decay
+
+ self.pos = p1
+ self.vel = v1
+
+ return p1
+ end
+ end
+
+ CameraUtils.Spring = Spring
+
+ -- map a value from one range to another
+ function CameraUtils.map(x, inMin, inMax, outMin, outMax)
+ return (x - inMin)*(outMax - outMin)/(inMax - inMin) + outMin
+ end
+
+ -- From TransparencyController
+ function CameraUtils.Round(num, places)
+ local decimalPivot = 10^places
+ return math.floor(num * decimalPivot + 0.5) / decimalPivot
+ end
+
+ function CameraUtils.IsFinite(val)
+ return val == val and val ~= math.huge and val ~= -math.huge
+ end
+
+ function CameraUtils.IsFiniteVector3(vec3)
+ return CameraUtils.IsFinite(vec3.X) and CameraUtils.IsFinite(vec3.Y) and CameraUtils.IsFinite(vec3.Z)
+ end
+
+ -- Legacy implementation renamed
+ function CameraUtils.GetAngleBetweenXZVectors(v1, v2)
+ return math.atan2(v2.X*v1.Z-v2.Z*v1.X, v2.X*v1.X+v2.Z*v1.Z)
+ end
+
+ function CameraUtils.RotateVectorByAngleAndRound(camLook, rotateAngle, roundAmount)
+ if camLook.Magnitude > 0 then
+ camLook = camLook.unit
+ local currAngle = math.atan2(camLook.z, camLook.x)
+ local newAngle = round((math.atan2(camLook.z, camLook.x) + rotateAngle) / roundAmount) * roundAmount
+ return newAngle - currAngle
+ end
+ return 0
+ end
+
+ -- K is a tunable parameter that changes the shape of the S-curve
+ -- the larger K is the more straight/linear the curve gets
+ local k = 0.35
+ local lowerK = 0.8
+ local function SCurveTranform(t)
+ t = math.clamp(t, -1, 1)
+ if t >= 0 then
+ return (k*t) / (k - t + 1)
+ end
+ return -((lowerK*-t) / (lowerK + t + 1))
+ end
+
+ local DEADZONE = 0.1
+ local function toSCurveSpace(t)
+ return (1 + DEADZONE) * (2*math.abs(t) - 1) - DEADZONE
+ end
+
+ local function fromSCurveSpace(t)
+ return t/2 + 0.5
+ end
+
+ function CameraUtils.GamepadLinearToCurve(thumbstickPosition)
+ local function onAxis(axisValue)
+ local sign = 1
+ if axisValue < 0 then
+ sign = -1
+ end
+ local point = fromSCurveSpace(SCurveTranform(toSCurveSpace(math.abs(axisValue))))
+ point = point * sign
+ return math.clamp(point, -1, 1)
+ end
+ return Vector2.new(onAxis(thumbstickPosition.x), onAxis(thumbstickPosition.y))
+ end
+
+ -- This function converts 4 different, redundant enumeration types to one standard so the values can be compared
+ function CameraUtils.ConvertCameraModeEnumToStandard(enumValue)
+ if enumValue == Enum.TouchCameraMovementMode.Default then
+ return Enum.ComputerCameraMovementMode.Follow
+ end
+
+ if enumValue == Enum.ComputerCameraMovementMode.Default then
+ return Enum.ComputerCameraMovementMode.Classic
+ end
+
+ if enumValue == Enum.TouchCameraMovementMode.Classic or
+ enumValue == Enum.DevTouchCameraMovementMode.Classic or
+ enumValue == Enum.DevComputerCameraMovementMode.Classic or
+ enumValue == Enum.ComputerCameraMovementMode.Classic then
+ return Enum.ComputerCameraMovementMode.Classic
+ end
+
+ if enumValue == Enum.TouchCameraMovementMode.Follow or
+ enumValue == Enum.DevTouchCameraMovementMode.Follow or
+ enumValue == Enum.DevComputerCameraMovementMode.Follow or
+ enumValue == Enum.ComputerCameraMovementMode.Follow then
+ return Enum.ComputerCameraMovementMode.Follow
+ end
+
+ if enumValue == Enum.TouchCameraMovementMode.Orbital or
+ enumValue == Enum.DevTouchCameraMovementMode.Orbital or
+ enumValue == Enum.DevComputerCameraMovementMode.Orbital or
+ enumValue == Enum.ComputerCameraMovementMode.Orbital then
+ return Enum.ComputerCameraMovementMode.Orbital
+ end
+
+ if FFlagUserCameraToggle then
+ if enumValue == Enum.ComputerCameraMovementMode.CameraToggle or
+ enumValue == Enum.DevComputerCameraMovementMode.CameraToggle then
+ return Enum.ComputerCameraMovementMode.CameraToggle
+ end
+ end
+
+ -- Note: Only the Dev versions of the Enums have UserChoice as an option
+ if enumValue == Enum.DevTouchCameraMovementMode.UserChoice or
+ enumValue == Enum.DevComputerCameraMovementMode.UserChoice then
+ return Enum.DevComputerCameraMovementMode.UserChoice
+ end
+
+ -- For any unmapped options return Classic camera
+ return Enum.ComputerCameraMovementMode.Classic
+ end
+
+ return CameraUtils
+end
+
+function _CameraModule()
+ local CameraModule = {}
+ CameraModule.__index = CameraModule
+
+ local FFlagUserCameraToggle do
+ local success, result = pcall(function()
+ return UserSettings():IsUserFeatureEnabled("UserCameraToggle")
+ end)
+ FFlagUserCameraToggle = success and result
+ end
+
+ local FFlagUserRemoveTheCameraApi do
+ local success, result = pcall(function()
+ return UserSettings():IsUserFeatureEnabled("UserRemoveTheCameraApi")
+ end)
+ FFlagUserRemoveTheCameraApi = success and result
+ end
+
+ -- NOTICE: Player property names do not all match their StarterPlayer equivalents,
+ -- with the differences noted in the comments on the right
+ local PLAYER_CAMERA_PROPERTIES =
+ {
+ "CameraMinZoomDistance",
+ "CameraMaxZoomDistance",
+ "CameraMode",
+ "DevCameraOcclusionMode",
+ "DevComputerCameraMode", -- Corresponds to StarterPlayer.DevComputerCameraMovementMode
+ "DevTouchCameraMode", -- Corresponds to StarterPlayer.DevTouchCameraMovementMode
+
+ -- Character movement mode
+ "DevComputerMovementMode",
+ "DevTouchMovementMode",
+ "DevEnableMouseLock", -- Corresponds to StarterPlayer.EnableMouseLockOption
+ }
+
+ local USER_GAME_SETTINGS_PROPERTIES =
+ {
+ "ComputerCameraMovementMode",
+ "ComputerMovementMode",
+ "ControlMode",
+ "GamepadCameraSensitivity",
+ "MouseSensitivity",
+ "RotationType",
+ "TouchCameraMovementMode",
+ "TouchMovementMode",
+ }
+
+ --[[ Roblox Services ]]--
+ local Players = game:GetService("Players")
+ local RunService = game:GetService("RunService")
+ local UserInputService = game:GetService("UserInputService")
+ local UserGameSettings = UserSettings():GetService("UserGameSettings")
+
+ -- Camera math utility library
+ local CameraUtils = _CameraUtils()
+
+ -- Load Roblox Camera Controller Modules
+ local ClassicCamera = _ClassicCamera()
+ local OrbitalCamera = _OrbitalCamera()
+ local LegacyCamera = _LegacyCamera()
+
+ -- Load Roblox Occlusion Modules
+ local Invisicam = _Invisicam()
+ local Poppercam = _Poppercam()
+
+ -- Load the near-field character transparency controller and the mouse lock "shift lock" controller
+ local TransparencyController = _TransparencyController()
+ local MouseLockController = _MouseLockController()
+
+ -- Table of camera controllers that have been instantiated. They are instantiated as they are used.
+ local instantiatedCameraControllers = {}
+ local instantiatedOcclusionModules = {}
+
+ -- Management of which options appear on the Roblox User Settings screen
+ do
+ local PlayerScripts = Players.LocalPlayer:WaitForChild("PlayerScripts")
+
+ PlayerScripts:RegisterTouchCameraMovementMode(Enum.TouchCameraMovementMode.Default)
+ PlayerScripts:RegisterTouchCameraMovementMode(Enum.TouchCameraMovementMode.Follow)
+ PlayerScripts:RegisterTouchCameraMovementMode(Enum.TouchCameraMovementMode.Classic)
+
+ PlayerScripts:RegisterComputerCameraMovementMode(Enum.ComputerCameraMovementMode.Default)
+ PlayerScripts:RegisterComputerCameraMovementMode(Enum.ComputerCameraMovementMode.Follow)
+ PlayerScripts:RegisterComputerCameraMovementMode(Enum.ComputerCameraMovementMode.Classic)
+ if FFlagUserCameraToggle then
+ PlayerScripts:RegisterComputerCameraMovementMode(Enum.ComputerCameraMovementMode.CameraToggle)
+ end
+ end
+
+ CameraModule.FFlagUserCameraToggle = FFlagUserCameraToggle
+
+
+ function CameraModule.new()
+ local self = setmetatable({},CameraModule)
+
+ -- Current active controller instances
+ self.activeCameraController = nil
+ self.activeOcclusionModule = nil
+ self.activeTransparencyController = nil
+ self.activeMouseLockController = nil
+
+ self.currentComputerCameraMovementMode = nil
+
+ -- Connections to events
+ self.cameraSubjectChangedConn = nil
+ self.cameraTypeChangedConn = nil
+
+ -- Adds CharacterAdded and CharacterRemoving event handlers for all current players
+ for _,player in pairs(Players:GetPlayers()) do
+ self:OnPlayerAdded(player)
+ end
+
+ -- Adds CharacterAdded and CharacterRemoving event handlers for all players who join in the future
+ Players.PlayerAdded:Connect(function(player)
+ self:OnPlayerAdded(player)
+ end)
+
+ self.activeTransparencyController = TransparencyController.new()
+ self.activeTransparencyController:Enable(true)
+
+ if not UserInputService.TouchEnabled then
+ self.activeMouseLockController = MouseLockController.new()
+ local toggleEvent = self.activeMouseLockController:GetBindableToggleEvent()
+ if toggleEvent then
+ toggleEvent:Connect(function()
+ self:OnMouseLockToggled()
+ end)
+ end
+ end
+
+ self:ActivateCameraController(self:GetCameraControlChoice())
+ self:ActivateOcclusionModule(Players.LocalPlayer.DevCameraOcclusionMode)
+ self:OnCurrentCameraChanged() -- Does initializations and makes first camera controller
+ RunService:BindToRenderStep("cameraRenderUpdate", Enum.RenderPriority.Camera.Value, function(dt) self:Update(dt) end)
+
+ -- Connect listeners to camera-related properties
+ for _, propertyName in pairs(PLAYER_CAMERA_PROPERTIES) do
+ Players.LocalPlayer:GetPropertyChangedSignal(propertyName):Connect(function()
+ self:OnLocalPlayerCameraPropertyChanged(propertyName)
+ end)
+ end
+
+ for _, propertyName in pairs(USER_GAME_SETTINGS_PROPERTIES) do
+ UserGameSettings:GetPropertyChangedSignal(propertyName):Connect(function()
+ self:OnUserGameSettingsPropertyChanged(propertyName)
+ end)
+ end
+ game.Workspace:GetPropertyChangedSignal("CurrentCamera"):Connect(function()
+ self:OnCurrentCameraChanged()
+ end)
+
+ self.lastInputType = UserInputService:GetLastInputType()
+ UserInputService.LastInputTypeChanged:Connect(function(newLastInputType)
+ self.lastInputType = newLastInputType
+ end)
+
+ return self
+ end
+
+ function CameraModule:GetCameraMovementModeFromSettings()
+ local cameraMode = Players.LocalPlayer.CameraMode
+
+ -- Lock First Person trumps all other settings and forces ClassicCamera
+ if cameraMode == Enum.CameraMode.LockFirstPerson then
+ return CameraUtils.ConvertCameraModeEnumToStandard(Enum.ComputerCameraMovementMode.Classic)
+ end
+
+ local devMode, userMode
+ if UserInputService.TouchEnabled then
+ devMode = CameraUtils.ConvertCameraModeEnumToStandard(Players.LocalPlayer.DevTouchCameraMode)
+ userMode = CameraUtils.ConvertCameraModeEnumToStandard(UserGameSettings.TouchCameraMovementMode)
+ else
+ devMode = CameraUtils.ConvertCameraModeEnumToStandard(Players.LocalPlayer.DevComputerCameraMode)
+ userMode = CameraUtils.ConvertCameraModeEnumToStandard(UserGameSettings.ComputerCameraMovementMode)
+ end
+
+ if devMode == Enum.DevComputerCameraMovementMode.UserChoice then
+ -- Developer is allowing user choice, so user setting is respected
+ return userMode
+ end
+
+ return devMode
+ end
+
+ function CameraModule:ActivateOcclusionModule( occlusionMode )
+ local newModuleCreator
+ if occlusionMode == Enum.DevCameraOcclusionMode.Zoom then
+ newModuleCreator = Poppercam
+ elseif occlusionMode == Enum.DevCameraOcclusionMode.Invisicam then
+ newModuleCreator = Invisicam
+ else
+ warn("CameraScript ActivateOcclusionModule called with unsupported mode")
+ return
+ end
+
+ -- First check to see if there is actually a change. If the module being requested is already
+ -- the currently-active solution then just make sure it's enabled and exit early
+ if self.activeOcclusionModule and self.activeOcclusionModule:GetOcclusionMode() == occlusionMode then
+ if not self.activeOcclusionModule:GetEnabled() then
+ self.activeOcclusionModule:Enable(true)
+ end
+ return
+ end
+
+ -- Save a reference to the current active module (may be nil) so that we can disable it if
+ -- we are successful in activating its replacement
+ local prevOcclusionModule = self.activeOcclusionModule
+
+ -- If there is no active module, see if the one we need has already been instantiated
+ self.activeOcclusionModule = instantiatedOcclusionModules[newModuleCreator]
+
+ -- If the module was not already instantiated and selected above, instantiate it
+ if not self.activeOcclusionModule then
+ self.activeOcclusionModule = newModuleCreator.new()
+ if self.activeOcclusionModule then
+ instantiatedOcclusionModules[newModuleCreator] = self.activeOcclusionModule
+ end
+ end
+
+ -- If we were successful in either selecting or instantiating the module,
+ -- enable it if it's not already the currently-active enabled module
+ if self.activeOcclusionModule then
+ local newModuleOcclusionMode = self.activeOcclusionModule:GetOcclusionMode()
+ -- Sanity check that the module we selected or instantiated actually supports the desired occlusionMode
+ if newModuleOcclusionMode ~= occlusionMode then
+ warn("CameraScript ActivateOcclusionModule mismatch: ",self.activeOcclusionModule:GetOcclusionMode(),"~=",occlusionMode)
+ end
+
+ -- Deactivate current module if there is one
+ if prevOcclusionModule then
+ -- Sanity check that current module is not being replaced by itself (that should have been handled above)
+ if prevOcclusionModule ~= self.activeOcclusionModule then
+ prevOcclusionModule:Enable(false)
+ else
+ warn("CameraScript ActivateOcclusionModule failure to detect already running correct module")
+ end
+ end
+
+ -- Occlusion modules need to be initialized with information about characters and cameraSubject
+ -- Invisicam needs the LocalPlayer's character
+ -- Poppercam needs all player characters and the camera subject
+ if occlusionMode == Enum.DevCameraOcclusionMode.Invisicam then
+ -- Optimization to only send Invisicam what we know it needs
+ if Players.LocalPlayer.Character then
+ self.activeOcclusionModule:CharacterAdded(Players.LocalPlayer.Character, Players.LocalPlayer )
+ end
+ else
+ -- When Poppercam is enabled, we send it all existing player characters for its raycast ignore list
+ for _, player in pairs(Players:GetPlayers()) do
+ if player and player.Character then
+ self.activeOcclusionModule:CharacterAdded(player.Character, player)
+ end
+ end
+ self.activeOcclusionModule:OnCameraSubjectChanged(game.Workspace.CurrentCamera.CameraSubject)
+ end
+
+ -- Activate new choice
+ self.activeOcclusionModule:Enable(true)
+ end
+ end
+
+ -- When supplied, legacyCameraType is used and cameraMovementMode is ignored (should be nil anyways)
+ -- Next, if userCameraCreator is passed in, that is used as the cameraCreator
+ function CameraModule:ActivateCameraController(cameraMovementMode, legacyCameraType)
+ local newCameraCreator = nil
+
+ if legacyCameraType~=nil then
+ --[[
+ This function has been passed a CameraType enum value. Some of these map to the use of
+ the LegacyCamera module, the value "Custom" will be translated to a movementMode enum
+ value based on Dev and User settings, and "Scriptable" will disable the camera controller.
+ --]]
+
+ if legacyCameraType == Enum.CameraType.Scriptable then
+ if self.activeCameraController then
+ self.activeCameraController:Enable(false)
+ self.activeCameraController = nil
+ return
+ end
+ elseif legacyCameraType == Enum.CameraType.Custom then
+ cameraMovementMode = self:GetCameraMovementModeFromSettings()
+
+ elseif legacyCameraType == Enum.CameraType.Track then
+ -- Note: The TrackCamera module was basically an older, less fully-featured
+ -- version of ClassicCamera, no longer actively maintained, but it is re-implemented in
+ -- case a game was dependent on its lack of ClassicCamera's extra functionality.
+ cameraMovementMode = Enum.ComputerCameraMovementMode.Classic
+
+ elseif legacyCameraType == Enum.CameraType.Follow then
+ cameraMovementMode = Enum.ComputerCameraMovementMode.Follow
+
+ elseif legacyCameraType == Enum.CameraType.Orbital then
+ cameraMovementMode = Enum.ComputerCameraMovementMode.Orbital
+
+ elseif legacyCameraType == Enum.CameraType.Attach or
+ legacyCameraType == Enum.CameraType.Watch or
+ legacyCameraType == Enum.CameraType.Fixed then
+ newCameraCreator = LegacyCamera
+ else
+ warn("CameraScript encountered an unhandled Camera.CameraType value: ",legacyCameraType)
+ end
+ end
+
+ if not newCameraCreator then
+ if cameraMovementMode == Enum.ComputerCameraMovementMode.Classic or
+ cameraMovementMode == Enum.ComputerCameraMovementMode.Follow or
+ cameraMovementMode == Enum.ComputerCameraMovementMode.Default or
+ (FFlagUserCameraToggle and cameraMovementMode == Enum.ComputerCameraMovementMode.CameraToggle) then
+ newCameraCreator = ClassicCamera
+ elseif cameraMovementMode == Enum.ComputerCameraMovementMode.Orbital then
+ newCameraCreator = OrbitalCamera
+ else
+ warn("ActivateCameraController did not select a module.")
+ return
+ end
+ end
+
+ -- Create the camera control module we need if it does not already exist in instantiatedCameraControllers
+ local newCameraController
+ if not instantiatedCameraControllers[newCameraCreator] then
+ newCameraController = newCameraCreator.new()
+ instantiatedCameraControllers[newCameraCreator] = newCameraController
+ else
+ newCameraController = instantiatedCameraControllers[newCameraCreator]
+ end
+
+ -- If there is a controller active and it's not the one we need, disable it,
+ -- if it is the one we need, make sure it's enabled
+ if self.activeCameraController then
+ if self.activeCameraController ~= newCameraController then
+ self.activeCameraController:Enable(false)
+ self.activeCameraController = newCameraController
+ self.activeCameraController:Enable(true)
+ elseif not self.activeCameraController:GetEnabled() then
+ self.activeCameraController:Enable(true)
+ end
+ elseif newCameraController ~= nil then
+ self.activeCameraController = newCameraController
+ self.activeCameraController:Enable(true)
+ end
+
+ if self.activeCameraController then
+ if cameraMovementMode~=nil then
+ self.activeCameraController:SetCameraMovementMode(cameraMovementMode)
+ elseif legacyCameraType~=nil then
+ -- Note that this is only called when legacyCameraType is not a type that
+ -- was convertible to a ComputerCameraMovementMode value, i.e. really only applies to LegacyCamera
+ self.activeCameraController:SetCameraType(legacyCameraType)
+ end
+ end
+ end
+
+ -- Note: The active transparency controller could be made to listen for this event itself.
+ function CameraModule:OnCameraSubjectChanged()
+ if self.activeTransparencyController then
+ self.activeTransparencyController:SetSubject(game.Workspace.CurrentCamera.CameraSubject)
+ end
+
+ if self.activeOcclusionModule then
+ self.activeOcclusionModule:OnCameraSubjectChanged(game.Workspace.CurrentCamera.CameraSubject)
+ end
+ end
+
+ function CameraModule:OnCameraTypeChanged(newCameraType)
+ if newCameraType == Enum.CameraType.Scriptable then
+ if UserInputService.MouseBehavior == Enum.MouseBehavior.LockCenter then
+ UserInputService.MouseBehavior = Enum.MouseBehavior.Default
+ end
+ end
+
+ -- Forward the change to ActivateCameraController to handle
+ self:ActivateCameraController(nil, newCameraType)
+ end
+
+ -- Note: Called whenever workspace.CurrentCamera changes, but also on initialization of this script
+ function CameraModule:OnCurrentCameraChanged()
+ local currentCamera = game.Workspace.CurrentCamera
+ if not currentCamera then return end
+
+ if self.cameraSubjectChangedConn then
+ self.cameraSubjectChangedConn:Disconnect()
+ end
+
+ if self.cameraTypeChangedConn then
+ self.cameraTypeChangedConn:Disconnect()
+ end
+
+ self.cameraSubjectChangedConn = currentCamera:GetPropertyChangedSignal("CameraSubject"):Connect(function()
+ self:OnCameraSubjectChanged(currentCamera.CameraSubject)
+ end)
+
+ self.cameraTypeChangedConn = currentCamera:GetPropertyChangedSignal("CameraType"):Connect(function()
+ self:OnCameraTypeChanged(currentCamera.CameraType)
+ end)
+
+ self:OnCameraSubjectChanged(currentCamera.CameraSubject)
+ self:OnCameraTypeChanged(currentCamera.CameraType)
+ end
+
+ function CameraModule:OnLocalPlayerCameraPropertyChanged(propertyName)
+ if propertyName == "CameraMode" then
+ -- CameraMode is only used to turn on/off forcing the player into first person view. The
+ -- Note: The case "Classic" is used for all other views and does not correspond only to the ClassicCamera module
+ if Players.LocalPlayer.CameraMode == Enum.CameraMode.LockFirstPerson then
+ -- Locked in first person, use ClassicCamera which supports this
+ if not self.activeCameraController or self.activeCameraController:GetModuleName() ~= "ClassicCamera" then
+ self:ActivateCameraController(CameraUtils.ConvertCameraModeEnumToStandard(Enum.DevComputerCameraMovementMode.Classic))
+ end
+
+ if self.activeCameraController then
+ self.activeCameraController:UpdateForDistancePropertyChange()
+ end
+ elseif Players.LocalPlayer.CameraMode == Enum.CameraMode.Classic then
+ -- Not locked in first person view
+ local cameraMovementMode =self: GetCameraMovementModeFromSettings()
+ self:ActivateCameraController(CameraUtils.ConvertCameraModeEnumToStandard(cameraMovementMode))
+ else
+ warn("Unhandled value for property player.CameraMode: ",Players.LocalPlayer.CameraMode)
+ end
+
+ elseif propertyName == "DevComputerCameraMode" or
+ propertyName == "DevTouchCameraMode" then
+ local cameraMovementMode = self:GetCameraMovementModeFromSettings()
+ self:ActivateCameraController(CameraUtils.ConvertCameraModeEnumToStandard(cameraMovementMode))
+
+ elseif propertyName == "DevCameraOcclusionMode" then
+ self:ActivateOcclusionModule(Players.LocalPlayer.DevCameraOcclusionMode)
+
+ elseif propertyName == "CameraMinZoomDistance" or propertyName == "CameraMaxZoomDistance" then
+ if self.activeCameraController then
+ self.activeCameraController:UpdateForDistancePropertyChange()
+ end
+ elseif propertyName == "DevTouchMovementMode" then
+ elseif propertyName == "DevComputerMovementMode" then
+ elseif propertyName == "DevEnableMouseLock" then
+ -- This is the enabling/disabling of "Shift Lock" mode, not LockFirstPerson (which is a CameraMode)
+ -- Note: Enabling and disabling of MouseLock mode is normally only a publish-time choice made via
+ -- the corresponding EnableMouseLockOption checkbox of StarterPlayer, and this script does not have
+ -- support for changing the availability of MouseLock at runtime (this would require listening to
+ -- Player.DevEnableMouseLock changes)
+ end
+ end
+
+ function CameraModule:OnUserGameSettingsPropertyChanged(propertyName)
+ if propertyName == "ComputerCameraMovementMode" then
+ local cameraMovementMode = self:GetCameraMovementModeFromSettings()
+ self:ActivateCameraController(CameraUtils.ConvertCameraModeEnumToStandard(cameraMovementMode))
+ end
+ end
+
+ --[[
+ Main RenderStep Update. The camera controller and occlusion module both have opportunities
+ to set and modify (respectively) the CFrame and Focus before it is set once on CurrentCamera.
+ The camera and occlusion modules should only return CFrames, not set the CFrame property of
+ CurrentCamera directly.
+ --]]
+ function CameraModule:Update(dt)
+ if self.activeCameraController then
+ if FFlagUserCameraToggle then
+ self.activeCameraController:UpdateMouseBehavior()
+ end
+
+ local newCameraCFrame, newCameraFocus = self.activeCameraController:Update(dt)
+ self.activeCameraController:ApplyVRTransform()
+ if self.activeOcclusionModule then
+ newCameraCFrame, newCameraFocus = self.activeOcclusionModule:Update(dt, newCameraCFrame, newCameraFocus)
+ end
+
+ -- Here is where the new CFrame and Focus are set for this render frame
+ game.Workspace.CurrentCamera.CFrame = newCameraCFrame
+ game.Workspace.CurrentCamera.Focus = newCameraFocus
+
+ -- Update to character local transparency as needed based on camera-to-subject distance
+ if self.activeTransparencyController then
+ self.activeTransparencyController:Update()
+ end
+ end
+ end
+
+ -- Formerly getCurrentCameraMode, this function resolves developer and user camera control settings to
+ -- decide which camera control module should be instantiated. The old method of converting redundant enum types
+ function CameraModule:GetCameraControlChoice()
+ local player = Players.LocalPlayer
+
+ if player then
+ if self.lastInputType == Enum.UserInputType.Touch or UserInputService.TouchEnabled then
+ -- Touch
+ if player.DevTouchCameraMode == Enum.DevTouchCameraMovementMode.UserChoice then
+ return CameraUtils.ConvertCameraModeEnumToStandard( UserGameSettings.TouchCameraMovementMode )
+ else
+ return CameraUtils.ConvertCameraModeEnumToStandard( player.DevTouchCameraMode )
+ end
+ else
+ -- Computer
+ if player.DevComputerCameraMode == Enum.DevComputerCameraMovementMode.UserChoice then
+ local computerMovementMode = CameraUtils.ConvertCameraModeEnumToStandard(UserGameSettings.ComputerCameraMovementMode)
+ return CameraUtils.ConvertCameraModeEnumToStandard(computerMovementMode)
+ else
+ return CameraUtils.ConvertCameraModeEnumToStandard(player.DevComputerCameraMode)
+ end
+ end
+ end
+ end
+
+ function CameraModule:OnCharacterAdded(char, player)
+ if self.activeOcclusionModule then
+ self.activeOcclusionModule:CharacterAdded(char, player)
+ end
+ end
+
+ function CameraModule:OnCharacterRemoving(char, player)
+ if self.activeOcclusionModule then
+ self.activeOcclusionModule:CharacterRemoving(char, player)
+ end
+ end
+
+ function CameraModule:OnPlayerAdded(player)
+ player.CharacterAdded:Connect(function(char)
+ self:OnCharacterAdded(char, player)
+ end)
+ player.CharacterRemoving:Connect(function(char)
+ self:OnCharacterRemoving(char, player)
+ end)
+ end
+
+ function CameraModule:OnMouseLockToggled()
+ if self.activeMouseLockController then
+ local mouseLocked = self.activeMouseLockController:GetIsMouseLocked()
+ local mouseLockOffset = self.activeMouseLockController:GetMouseLockOffset()
+ if self.activeCameraController then
+ self.activeCameraController:SetIsMouseLocked(mouseLocked)
+ self.activeCameraController:SetMouseLockOffset(mouseLockOffset)
+ end
+ end
+ end
+ --begin edit
+ local Camera = CameraModule
+ local IDENTITYCF = CFrame.new()
+ local lastUpCFrame = IDENTITYCF
+
+ Camera.UpVector = Vector3.new(0, 1, 0)
+ Camera.TransitionRate = 0.15
+ Camera.UpCFrame = IDENTITYCF
+
+ function Camera:GetUpVector(oldUpVector)
+ return oldUpVector
+ end
+ local function getRotationBetween(u, v, axis)
+ local dot, uxv = u:Dot(v), u:Cross(v)
+ if (dot < -0.99999) then return CFrame.fromAxisAngle(axis, math.pi) end
+ return CFrame.new(0, 0, 0, uxv.x, uxv.y, uxv.z, 1 + dot)
+ end
+ function Camera:CalculateUpCFrame()
+ local oldUpVector = self.UpVector
+ local newUpVector = self:GetUpVector(oldUpVector)
+
+ local backup = game.Workspace.CurrentCamera.CFrame.RightVector
+ local transitionCF = getRotationBetween(oldUpVector, newUpVector, backup)
+ local vecSlerpCF = IDENTITYCF:Lerp(transitionCF, self.TransitionRate)
+
+ self.UpVector = vecSlerpCF * oldUpVector
+ self.UpCFrame = vecSlerpCF * self.UpCFrame
+
+ lastUpCFrame = self.UpCFrame
+ end
+
+ function Camera:Update(dt)
+ if self.activeCameraController then
+ if Camera.FFlagUserCameraToggle then
+ self.activeCameraController:UpdateMouseBehavior()
+ end
+
+ local newCameraCFrame, newCameraFocus = self.activeCameraController:Update(dt)
+ self.activeCameraController:ApplyVRTransform()
+
+ self:CalculateUpCFrame()
+ self.activeCameraController:UpdateUpCFrame(self.UpCFrame)
+
+ -- undo shift-lock offset
+
+ local lockOffset = Vector3.new(0, 0, 0)
+ if (self.activeMouseLockController and self.activeMouseLockController:GetIsMouseLocked()) then
+ lockOffset = self.activeMouseLockController:GetMouseLockOffset()
+ end
+
+ local offset = newCameraFocus:ToObjectSpace(newCameraCFrame)
+ local camRotation = self.UpCFrame * offset
+ newCameraFocus = newCameraFocus - newCameraCFrame:VectorToWorldSpace(lockOffset) + camRotation:VectorToWorldSpace(lockOffset)
+ newCameraCFrame = newCameraFocus * camRotation
+
+ --local offset = newCameraFocus:Inverse() * newCameraCFrame
+ --newCameraCFrame = newCameraFocus * self.UpCFrame * offset
+
+ if (self.activeCameraController.lastCameraTransform) then
+ self.activeCameraController.lastCameraTransform = newCameraCFrame
+ self.activeCameraController.lastCameraFocus = newCameraFocus
+ end
+
+ if self.activeOcclusionModule then
+ newCameraCFrame, newCameraFocus = self.activeOcclusionModule:Update(dt, newCameraCFrame, newCameraFocus)
+ end
+
+ game.Workspace.CurrentCamera.CFrame = newCameraCFrame
+ game.Workspace.CurrentCamera.Focus = newCameraFocus
+
+ if self.activeTransparencyController then
+ self.activeTransparencyController:Update()
+ end
+ end
+ end
+
+ function Camera:IsFirstPerson()
+ if self.activeCameraController then
+ return self.activeCameraController:InFirstPerson()
+ end
+ return false
+ end
+
+ function Camera:IsMouseLocked()
+ if self.activeCameraController then
+ return self.activeCameraController:GetIsMouseLocked()
+ end
+ return false
+ end
+ function Camera:IsToggleMode()
+ if self.activeCameraController then
+ return self.activeCameraController.isCameraToggle
+ end
+ return false
+ end
+ function Camera:IsCamRelative()
+ return self:IsMouseLocked() or self:IsFirstPerson()
+ --return self:IsToggleMode(), self:IsMouseLocked(), self:IsFirstPerson()
+ end
+ --
+ local Utils = _CameraUtils()
+ function Utils.GetAngleBetweenXZVectors(v1, v2)
+ local upCFrame = lastUpCFrame
+ v1 = upCFrame:VectorToObjectSpace(v1)
+ v2 = upCFrame:VectorToObjectSpace(v2)
+ return math.atan2(v2.X*v1.Z-v2.Z*v1.X, v2.X*v1.X+v2.Z*v1.Z)
+ end
+ --end edit
+ local cameraModuleObject = CameraModule.new()
+ local cameraApi = {}
+ return cameraModuleObject
+end
+
+function _ClickToMoveDisplay()
+ local ClickToMoveDisplay = {}
+
+ local FAILURE_ANIMATION_ID = "rbxassetid://2874840706"
+
+ local TrailDotIcon = "rbxasset://textures/ui/traildot.png"
+ local EndWaypointIcon = "rbxasset://textures/ui/waypoint.png"
+
+ local WaypointsAlwaysOnTop = false
+
+ local WAYPOINT_INCLUDE_FACTOR = 2
+ local LAST_DOT_DISTANCE = 3
+
+ local WAYPOINT_BILLBOARD_SIZE = UDim2.new(0, 1.68 * 25, 0, 2 * 25)
+
+ local ENDWAYPOINT_SIZE_OFFSET_MIN = Vector2.new(0, 0.5)
+ local ENDWAYPOINT_SIZE_OFFSET_MAX = Vector2.new(0, 1)
+
+ local FAIL_WAYPOINT_SIZE_OFFSET_CENTER = Vector2.new(0, 0.5)
+ local FAIL_WAYPOINT_SIZE_OFFSET_LEFT = Vector2.new(0.1, 0.5)
+ local FAIL_WAYPOINT_SIZE_OFFSET_RIGHT = Vector2.new(-0.1, 0.5)
+
+ local FAILURE_TWEEN_LENGTH = 0.125
+ local FAILURE_TWEEN_COUNT = 4
+
+ local TWEEN_WAYPOINT_THRESHOLD = 5
+
+ local TRAIL_DOT_PARENT_NAME = "ClickToMoveDisplay"
+
+ local TrailDotSize = Vector2.new(1.5, 1.5)
+
+ local TRAIL_DOT_MIN_SCALE = 1
+ local TRAIL_DOT_MIN_DISTANCE = 10
+ local TRAIL_DOT_MAX_SCALE = 2.5
+ local TRAIL_DOT_MAX_DISTANCE = 100
+
+ local PlayersService = game:GetService("Players")
+ local TweenService = game:GetService("TweenService")
+ local RunService = game:GetService("RunService")
+ local Workspace = game:GetService("Workspace")
+
+ local LocalPlayer = PlayersService.LocalPlayer
+
+ local function CreateWaypointTemplates()
+ local TrailDotTemplate = Instance.new("Part")
+ TrailDotTemplate.Size = Vector3.new(1, 1, 1)
+ TrailDotTemplate.Anchored = true
+ TrailDotTemplate.CanCollide = false
+ TrailDotTemplate.Name = "TrailDot"
+ TrailDotTemplate.Transparency = 1
+ local TrailDotImage = Instance.new("ImageHandleAdornment")
+ TrailDotImage.Name = "TrailDotImage"
+ TrailDotImage.Size = TrailDotSize
+ TrailDotImage.SizeRelativeOffset = Vector3.new(0, 0, -0.1)
+ TrailDotImage.AlwaysOnTop = WaypointsAlwaysOnTop
+ TrailDotImage.Image = TrailDotIcon
+ TrailDotImage.Adornee = TrailDotTemplate
+ TrailDotImage.Parent = TrailDotTemplate
+
+ local EndWaypointTemplate = Instance.new("Part")
+ EndWaypointTemplate.Size = Vector3.new(2, 2, 2)
+ EndWaypointTemplate.Anchored = true
+ EndWaypointTemplate.CanCollide = false
+ EndWaypointTemplate.Name = "EndWaypoint"
+ EndWaypointTemplate.Transparency = 1
+ local EndWaypointImage = Instance.new("ImageHandleAdornment")
+ EndWaypointImage.Name = "TrailDotImage"
+ EndWaypointImage.Size = TrailDotSize
+ EndWaypointImage.SizeRelativeOffset = Vector3.new(0, 0, -0.1)
+ EndWaypointImage.AlwaysOnTop = WaypointsAlwaysOnTop
+ EndWaypointImage.Image = TrailDotIcon
+ EndWaypointImage.Adornee = EndWaypointTemplate
+ EndWaypointImage.Parent = EndWaypointTemplate
+ local EndWaypointBillboard = Instance.new("BillboardGui")
+ EndWaypointBillboard.Name = "EndWaypointBillboard"
+ EndWaypointBillboard.Size = WAYPOINT_BILLBOARD_SIZE
+ EndWaypointBillboard.LightInfluence = 0
+ EndWaypointBillboard.SizeOffset = ENDWAYPOINT_SIZE_OFFSET_MIN
+ EndWaypointBillboard.AlwaysOnTop = true
+ EndWaypointBillboard.Adornee = EndWaypointTemplate
+ EndWaypointBillboard.Parent = EndWaypointTemplate
+ local EndWaypointImageLabel = Instance.new("ImageLabel")
+ EndWaypointImageLabel.Image = EndWaypointIcon
+ EndWaypointImageLabel.BackgroundTransparency = 1
+ EndWaypointImageLabel.Size = UDim2.new(1, 0, 1, 0)
+ EndWaypointImageLabel.Parent = EndWaypointBillboard
+
+
+ local FailureWaypointTemplate = Instance.new("Part")
+ FailureWaypointTemplate.Size = Vector3.new(2, 2, 2)
+ FailureWaypointTemplate.Anchored = true
+ FailureWaypointTemplate.CanCollide = false
+ FailureWaypointTemplate.Name = "FailureWaypoint"
+ FailureWaypointTemplate.Transparency = 1
+ local FailureWaypointImage = Instance.new("ImageHandleAdornment")
+ FailureWaypointImage.Name = "TrailDotImage"
+ FailureWaypointImage.Size = TrailDotSize
+ FailureWaypointImage.SizeRelativeOffset = Vector3.new(0, 0, -0.1)
+ FailureWaypointImage.AlwaysOnTop = WaypointsAlwaysOnTop
+ FailureWaypointImage.Image = TrailDotIcon
+ FailureWaypointImage.Adornee = FailureWaypointTemplate
+ FailureWaypointImage.Parent = FailureWaypointTemplate
+ local FailureWaypointBillboard = Instance.new("BillboardGui")
+ FailureWaypointBillboard.Name = "FailureWaypointBillboard"
+ FailureWaypointBillboard.Size = WAYPOINT_BILLBOARD_SIZE
+ FailureWaypointBillboard.LightInfluence = 0
+ FailureWaypointBillboard.SizeOffset = FAIL_WAYPOINT_SIZE_OFFSET_CENTER
+ FailureWaypointBillboard.AlwaysOnTop = true
+ FailureWaypointBillboard.Adornee = FailureWaypointTemplate
+ FailureWaypointBillboard.Parent = FailureWaypointTemplate
+ local FailureWaypointFrame = Instance.new("Frame")
+ FailureWaypointFrame.BackgroundTransparency = 1
+ FailureWaypointFrame.Size = UDim2.new(0, 0, 0, 0)
+ FailureWaypointFrame.Position = UDim2.new(0.5, 0, 1, 0)
+ FailureWaypointFrame.Parent = FailureWaypointBillboard
+ local FailureWaypointImageLabel = Instance.new("ImageLabel")
+ FailureWaypointImageLabel.Image = EndWaypointIcon
+ FailureWaypointImageLabel.BackgroundTransparency = 1
+ FailureWaypointImageLabel.Position = UDim2.new(
+ 0, -WAYPOINT_BILLBOARD_SIZE.X.Offset/2, 0, -WAYPOINT_BILLBOARD_SIZE.Y.Offset
+ )
+ FailureWaypointImageLabel.Size = WAYPOINT_BILLBOARD_SIZE
+ FailureWaypointImageLabel.Parent = FailureWaypointFrame
+
+ return TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate
+ end
+
+ local TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate = CreateWaypointTemplates()
+
+ local function getTrailDotParent()
+ local camera = Workspace.CurrentCamera
+ local trailParent = camera:FindFirstChild(TRAIL_DOT_PARENT_NAME)
+ if not trailParent then
+ trailParent = Instance.new("Model")
+ trailParent.Name = TRAIL_DOT_PARENT_NAME
+ trailParent.Parent = camera
+ end
+ return trailParent
+ end
+
+ local function placePathWaypoint(waypointModel, position)
+ local ray = Ray.new(position + Vector3.new(0, 2.5, 0), Vector3.new(0, -10, 0))
+ local hitPart, hitPoint, hitNormal = Workspace:FindPartOnRayWithIgnoreList(
+ ray,
+ { Workspace.CurrentCamera, LocalPlayer.Character }
+ )
+ if hitPart then
+ waypointModel.CFrame = CFrame.new(hitPoint, hitPoint + hitNormal)
+ waypointModel.Parent = getTrailDotParent()
+ end
+ end
+
+ local TrailDot = {}
+ TrailDot.__index = TrailDot
+
+ function TrailDot:Destroy()
+ self.DisplayModel:Destroy()
+ end
+
+ function TrailDot:NewDisplayModel(position)
+ local newDisplayModel = TrailDotTemplate:Clone()
+ placePathWaypoint(newDisplayModel, position)
+ return newDisplayModel
+ end
+
+ function TrailDot.new(position, closestWaypoint)
+ local self = setmetatable({}, TrailDot)
+
+ self.DisplayModel = self:NewDisplayModel(position)
+ self.ClosestWayPoint = closestWaypoint
+
+ return self
+ end
+
+ local EndWaypoint = {}
+ EndWaypoint.__index = EndWaypoint
+
+ function EndWaypoint:Destroy()
+ self.Destroyed = true
+ self.Tween:Cancel()
+ self.DisplayModel:Destroy()
+ end
+
+ function EndWaypoint:NewDisplayModel(position)
+ local newDisplayModel = EndWaypointTemplate:Clone()
+ placePathWaypoint(newDisplayModel, position)
+ return newDisplayModel
+ end
+
+ function EndWaypoint:CreateTween()
+ local tweenInfo = TweenInfo.new(0.5, Enum.EasingStyle.Sine, Enum.EasingDirection.Out, -1, true)
+ local tween = TweenService:Create(
+ self.DisplayModel.EndWaypointBillboard,
+ tweenInfo,
+ { SizeOffset = ENDWAYPOINT_SIZE_OFFSET_MAX }
+ )
+ tween:Play()
+ return tween
+ end
+
+ function EndWaypoint:TweenInFrom(originalPosition)
+ local currentPositon = self.DisplayModel.Position
+ local studsOffset = originalPosition - currentPositon
+ self.DisplayModel.EndWaypointBillboard.StudsOffset = Vector3.new(0, studsOffset.Y, 0)
+ local tweenInfo = TweenInfo.new(1, Enum.EasingStyle.Sine, Enum.EasingDirection.Out)
+ local tween = TweenService:Create(
+ self.DisplayModel.EndWaypointBillboard,
+ tweenInfo,
+ { StudsOffset = Vector3.new(0, 0, 0) }
+ )
+ tween:Play()
+ return tween
+ end
+
+ function EndWaypoint.new(position, closestWaypoint, originalPosition)
+ local self = setmetatable({}, EndWaypoint)
+
+ self.DisplayModel = self:NewDisplayModel(position)
+ self.Destroyed = false
+ if originalPosition and (originalPosition - position).magnitude > TWEEN_WAYPOINT_THRESHOLD then
+ self.Tween = self:TweenInFrom(originalPosition)
+ coroutine.wrap(function()
+ self.Tween.Completed:Wait()
+ if not self.Destroyed then
+ self.Tween = self:CreateTween()
+ end
+ end)()
+ else
+ self.Tween = self:CreateTween()
+ end
+ self.ClosestWayPoint = closestWaypoint
+
+ return self
+ end
+
+ local FailureWaypoint = {}
+ FailureWaypoint.__index = FailureWaypoint
+
+ function FailureWaypoint:Hide()
+ self.DisplayModel.Parent = nil
+ end
+
+ function FailureWaypoint:Destroy()
+ self.DisplayModel:Destroy()
+ end
+
+ function FailureWaypoint:NewDisplayModel(position)
+ local newDisplayModel = FailureWaypointTemplate:Clone()
+ placePathWaypoint(newDisplayModel, position)
+ local ray = Ray.new(position + Vector3.new(0, 2.5, 0), Vector3.new(0, -10, 0))
+ local hitPart, hitPoint, hitNormal = Workspace:FindPartOnRayWithIgnoreList(
+ ray, { Workspace.CurrentCamera, LocalPlayer.Character }
+ )
+ if hitPart then
+ newDisplayModel.CFrame = CFrame.new(hitPoint, hitPoint + hitNormal)
+ newDisplayModel.Parent = getTrailDotParent()
+ end
+ return newDisplayModel
+ end
+
+ function FailureWaypoint:RunFailureTween()
+ wait(FAILURE_TWEEN_LENGTH) -- Delay one tween length betfore starting tweening
+ -- Tween out from center
+ local tweenInfo = TweenInfo.new(FAILURE_TWEEN_LENGTH/2, Enum.EasingStyle.Sine, Enum.EasingDirection.Out)
+ local tweenLeft = TweenService:Create(self.DisplayModel.FailureWaypointBillboard, tweenInfo,
+ { SizeOffset = FAIL_WAYPOINT_SIZE_OFFSET_LEFT })
+ tweenLeft:Play()
+
+ local tweenLeftRoation = TweenService:Create(self.DisplayModel.FailureWaypointBillboard.Frame, tweenInfo,
+ { Rotation = 10 })
+ tweenLeftRoation:Play()
+
+ tweenLeft.Completed:wait()
+
+ -- Tween back and forth
+ tweenInfo = TweenInfo.new(FAILURE_TWEEN_LENGTH, Enum.EasingStyle.Sine, Enum.EasingDirection.Out,
+ FAILURE_TWEEN_COUNT - 1, true)
+ local tweenSideToSide = TweenService:Create(self.DisplayModel.FailureWaypointBillboard, tweenInfo,
+ { SizeOffset = FAIL_WAYPOINT_SIZE_OFFSET_RIGHT})
+ tweenSideToSide:Play()
+
+ -- Tween flash dark and roate left and right
+ tweenInfo = TweenInfo.new(FAILURE_TWEEN_LENGTH, Enum.EasingStyle.Sine, Enum.EasingDirection.Out,
+ FAILURE_TWEEN_COUNT - 1, true)
+ local tweenFlash = TweenService:Create(self.DisplayModel.FailureWaypointBillboard.Frame.ImageLabel, tweenInfo,
+ { ImageColor3 = Color3.new(0.75, 0.75, 0.75)})
+ tweenFlash:Play()
+
+ local tweenRotate = TweenService:Create(self.DisplayModel.FailureWaypointBillboard.Frame, tweenInfo,
+ { Rotation = -10 })
+ tweenRotate:Play()
+
+ tweenSideToSide.Completed:wait()
+
+ -- Tween back to center
+ tweenInfo = TweenInfo.new(FAILURE_TWEEN_LENGTH/2, Enum.EasingStyle.Sine, Enum.EasingDirection.Out)
+ local tweenCenter = TweenService:Create(self.DisplayModel.FailureWaypointBillboard, tweenInfo,
+ { SizeOffset = FAIL_WAYPOINT_SIZE_OFFSET_CENTER })
+ tweenCenter:Play()
+
+ local tweenRoation = TweenService:Create(self.DisplayModel.FailureWaypointBillboard.Frame, tweenInfo,
+ { Rotation = 0 })
+ tweenRoation:Play()
+
+ tweenCenter.Completed:wait()
+
+ wait(FAILURE_TWEEN_LENGTH) -- Delay one tween length betfore removing
+ end
+
+ function FailureWaypoint.new(position)
+ local self = setmetatable({}, FailureWaypoint)
+
+ self.DisplayModel = self:NewDisplayModel(position)
+
+ return self
+ end
+
+ local failureAnimation = Instance.new("Animation")
+ failureAnimation.AnimationId = FAILURE_ANIMATION_ID
+
+ local lastHumanoid = nil
+ local lastFailureAnimationTrack = nil
+
+ local function getFailureAnimationTrack(myHumanoid)
+ if myHumanoid == lastHumanoid then
+ return lastFailureAnimationTrack
+ end
+ lastFailureAnimationTrack = myHumanoid:LoadAnimation(failureAnimation)
+ lastFailureAnimationTrack.Priority = Enum.AnimationPriority.Action
+ lastFailureAnimationTrack.Looped = false
+ return lastFailureAnimationTrack
+ end
+
+ local function findPlayerHumanoid()
+ local character = LocalPlayer.Character
+ if character then
+ return character:FindFirstChildOfClass("Humanoid")
+ end
+ end
+
+ local function createTrailDots(wayPoints, originalEndWaypoint)
+ local newTrailDots = {}
+ local count = 1
+ for i = 1, #wayPoints - 1 do
+ local closeToEnd = (wayPoints[i].Position - wayPoints[#wayPoints].Position).magnitude < LAST_DOT_DISTANCE
+ local includeWaypoint = i % WAYPOINT_INCLUDE_FACTOR == 0 and not closeToEnd
+ if includeWaypoint then
+ local trailDot = TrailDot.new(wayPoints[i].Position, i)
+ newTrailDots[count] = trailDot
+ count = count + 1
+ end
+ end
+
+ local newEndWaypoint = EndWaypoint.new(wayPoints[#wayPoints].Position, #wayPoints, originalEndWaypoint)
+ table.insert(newTrailDots, newEndWaypoint)
+
+ local reversedTrailDots = {}
+ count = 1
+ for i = #newTrailDots, 1, -1 do
+ reversedTrailDots[count] = newTrailDots[i]
+ count = count + 1
+ end
+ return reversedTrailDots
+ end
+
+ local function getTrailDotScale(distanceToCamera, defaultSize)
+ local rangeLength = TRAIL_DOT_MAX_DISTANCE - TRAIL_DOT_MIN_DISTANCE
+ local inRangePoint = math.clamp(distanceToCamera - TRAIL_DOT_MIN_DISTANCE, 0, rangeLength)/rangeLength
+ local scale = TRAIL_DOT_MIN_SCALE + (TRAIL_DOT_MAX_SCALE - TRAIL_DOT_MIN_SCALE)*inRangePoint
+ return defaultSize * scale
+ end
+
+ local createPathCount = 0
+ -- originalEndWaypoint is optional, causes the waypoint to tween from that position.
+ function ClickToMoveDisplay.CreatePathDisplay(wayPoints, originalEndWaypoint)
+ createPathCount = createPathCount + 1
+ local trailDots = createTrailDots(wayPoints, originalEndWaypoint)
+
+ local function removePathBeforePoint(wayPointNumber)
+ -- kill all trailDots before and at wayPointNumber
+ for i = #trailDots, 1, -1 do
+ local trailDot = trailDots[i]
+ if trailDot.ClosestWayPoint <= wayPointNumber then
+ trailDot:Destroy()
+ trailDots[i] = nil
+ else
+ break
+ end
+ end
+ end
+
+ local reiszeTrailDotsUpdateName = "ClickToMoveResizeTrail" ..createPathCount
+ local function resizeTrailDots()
+ if #trailDots == 0 then
+ RunService:UnbindFromRenderStep(reiszeTrailDotsUpdateName)
+ return
+ end
+ local cameraPos = Workspace.CurrentCamera.CFrame.p
+ for i = 1, #trailDots do
+ local trailDotImage = trailDots[i].DisplayModel:FindFirstChild("TrailDotImage")
+ if trailDotImage then
+ local distanceToCamera = (trailDots[i].DisplayModel.Position - cameraPos).magnitude
+ trailDotImage.Size = getTrailDotScale(distanceToCamera, TrailDotSize)
+ end
+ end
+ end
+ RunService:BindToRenderStep(reiszeTrailDotsUpdateName, Enum.RenderPriority.Camera.Value - 1, resizeTrailDots)
+
+ local function removePath()
+ removePathBeforePoint(#wayPoints)
+ end
+
+ return removePath, removePathBeforePoint
+ end
+
+ local lastFailureWaypoint = nil
+ function ClickToMoveDisplay.DisplayFailureWaypoint(position)
+ if lastFailureWaypoint then
+ lastFailureWaypoint:Hide()
+ end
+ local failureWaypoint = FailureWaypoint.new(position)
+ lastFailureWaypoint = failureWaypoint
+ coroutine.wrap(function()
+ failureWaypoint:RunFailureTween()
+ failureWaypoint:Destroy()
+ failureWaypoint = nil
+ end)()
+ end
+
+ function ClickToMoveDisplay.CreateEndWaypoint(position)
+ return EndWaypoint.new(position)
+ end
+
+ function ClickToMoveDisplay.PlayFailureAnimation()
+ local myHumanoid = findPlayerHumanoid()
+ if myHumanoid then
+ local animationTrack = getFailureAnimationTrack(myHumanoid)
+ animationTrack:Play()
+ end
+ end
+
+ function ClickToMoveDisplay.CancelFailureAnimation()
+ if lastFailureAnimationTrack ~= nil and lastFailureAnimationTrack.IsPlaying then
+ lastFailureAnimationTrack:Stop()
+ end
+ end
+
+ function ClickToMoveDisplay.SetWaypointTexture(texture)
+ TrailDotIcon = texture
+ TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate = CreateWaypointTemplates()
+ end
+
+ function ClickToMoveDisplay.GetWaypointTexture()
+ return TrailDotIcon
+ end
+
+ function ClickToMoveDisplay.SetWaypointRadius(radius)
+ TrailDotSize = Vector2.new(radius, radius)
+ TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate = CreateWaypointTemplates()
+ end
+
+ function ClickToMoveDisplay.GetWaypointRadius()
+ return TrailDotSize.X
+ end
+
+ function ClickToMoveDisplay.SetEndWaypointTexture(texture)
+ EndWaypointIcon = texture
+ TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate = CreateWaypointTemplates()
+ end
+
+ function ClickToMoveDisplay.GetEndWaypointTexture()
+ return EndWaypointIcon
+ end
+
+ function ClickToMoveDisplay.SetWaypointsAlwaysOnTop(alwaysOnTop)
+ WaypointsAlwaysOnTop = alwaysOnTop
+ TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate = CreateWaypointTemplates()
+ end
+
+ function ClickToMoveDisplay.GetWaypointsAlwaysOnTop()
+ return WaypointsAlwaysOnTop
+ end
+
+ return ClickToMoveDisplay
+end
+
+function _BaseCharacterController()
+
+ local ZERO_VECTOR3 = Vector3.new(0,0,0)
+
+ --[[ The Module ]]--
+ local BaseCharacterController = {}
+ BaseCharacterController.__index = BaseCharacterController
+
+ function BaseCharacterController.new()
+ local self = setmetatable({}, BaseCharacterController)
+ self.enabled = false
+ self.moveVector = ZERO_VECTOR3
+ self.moveVectorIsCameraRelative = true
+ self.isJumping = false
+ return self
+ end
+
+ function BaseCharacterController:OnRenderStepped(dt)
+ -- By default, nothing to do
+ end
+
+ function BaseCharacterController:GetMoveVector()
+ return self.moveVector
+ end
+
+ function BaseCharacterController:IsMoveVectorCameraRelative()
+ return self.moveVectorIsCameraRelative
+ end
+
+ function BaseCharacterController:GetIsJumping()
+ return self.isJumping
+ end
+
+ -- Override in derived classes to set self.enabled and return boolean indicating
+ -- whether Enable/Disable was successful. Return true if controller is already in the requested state.
+ function BaseCharacterController:Enable(enable)
+ error("BaseCharacterController:Enable must be overridden in derived classes and should not be called.")
+ return false
+ end
+
+ return BaseCharacterController
+end
+
+function _VehicleController()
+ local ContextActionService = game:GetService("ContextActionService")
+
+ --[[ Constants ]]--
+ -- Set this to true if you want to instead use the triggers for the throttle
+ local useTriggersForThrottle = true
+ -- Also set this to true if you want the thumbstick to not affect throttle, only triggers when a gamepad is conected
+ local onlyTriggersForThrottle = false
+ local ZERO_VECTOR3 = Vector3.new(0,0,0)
+
+ local AUTO_PILOT_DEFAULT_MAX_STEERING_ANGLE = 35
+
+
+ -- Note that VehicleController does not derive from BaseCharacterController, it is a special case
+ local VehicleController = {}
+ VehicleController.__index = VehicleController
+
+ function VehicleController.new(CONTROL_ACTION_PRIORITY)
+ local self = setmetatable({}, VehicleController)
+
+ self.CONTROL_ACTION_PRIORITY = CONTROL_ACTION_PRIORITY
+
+ self.enabled = false
+ self.vehicleSeat = nil
+ self.throttle = 0
+ self.steer = 0
+
+ self.acceleration = 0
+ self.decceleration = 0
+ self.turningRight = 0
+ self.turningLeft = 0
+
+ self.vehicleMoveVector = ZERO_VECTOR3
+
+ self.autoPilot = {}
+ self.autoPilot.MaxSpeed = 0
+ self.autoPilot.MaxSteeringAngle = 0
+
+ return self
+ end
+
+ function VehicleController:BindContextActions()
+ if useTriggersForThrottle then
+ ContextActionService:BindActionAtPriority("throttleAccel", (function(actionName, inputState, inputObject)
+ self:OnThrottleAccel(actionName, inputState, inputObject)
+ return Enum.ContextActionResult.Pass
+ end), false, self.CONTROL_ACTION_PRIORITY, Enum.KeyCode.ButtonR2)
+ ContextActionService:BindActionAtPriority("throttleDeccel", (function(actionName, inputState, inputObject)
+ self:OnThrottleDeccel(actionName, inputState, inputObject)
+ return Enum.ContextActionResult.Pass
+ end), false, self.CONTROL_ACTION_PRIORITY, Enum.KeyCode.ButtonL2)
+ end
+ ContextActionService:BindActionAtPriority("arrowSteerRight", (function(actionName, inputState, inputObject)
+ self:OnSteerRight(actionName, inputState, inputObject)
+ return Enum.ContextActionResult.Pass
+ end), false, self.CONTROL_ACTION_PRIORITY, Enum.KeyCode.Right)
+ ContextActionService:BindActionAtPriority("arrowSteerLeft", (function(actionName, inputState, inputObject)
+ self:OnSteerLeft(actionName, inputState, inputObject)
+ return Enum.ContextActionResult.Pass
+ end), false, self.CONTROL_ACTION_PRIORITY, Enum.KeyCode.Left)
+ end
+
+ function VehicleController:Enable(enable, vehicleSeat)
+ if enable == self.enabled and vehicleSeat == self.vehicleSeat then
+ return
+ end
+
+ self.enabled = enable
+ self.vehicleMoveVector = ZERO_VECTOR3
+
+ if enable then
+ if vehicleSeat then
+ self.vehicleSeat = vehicleSeat
+
+ self:SetupAutoPilot()
+ self:BindContextActions()
+ end
+ else
+ if useTriggersForThrottle then
+ ContextActionService:UnbindAction("throttleAccel")
+ ContextActionService:UnbindAction("throttleDeccel")
+ end
+ ContextActionService:UnbindAction("arrowSteerRight")
+ ContextActionService:UnbindAction("arrowSteerLeft")
+ self.vehicleSeat = nil
+ end
+ end
+
+ function VehicleController:OnThrottleAccel(actionName, inputState, inputObject)
+ if inputState == Enum.UserInputState.End or inputState == Enum.UserInputState.Cancel then
+ self.acceleration = 0
+ else
+ self.acceleration = -1
+ end
+ self.throttle = self.acceleration + self.decceleration
+ end
+
+ function VehicleController:OnThrottleDeccel(actionName, inputState, inputObject)
+ if inputState == Enum.UserInputState.End or inputState == Enum.UserInputState.Cancel then
+ self.decceleration = 0
+ else
+ self.decceleration = 1
+ end
+ self.throttle = self.acceleration + self.decceleration
+ end
+
+ function VehicleController:OnSteerRight(actionName, inputState, inputObject)
+ if inputState == Enum.UserInputState.End or inputState == Enum.UserInputState.Cancel then
+ self.turningRight = 0
+ else
+ self.turningRight = 1
+ end
+ self.steer = self.turningRight + self.turningLeft
+ end
+
+ function VehicleController:OnSteerLeft(actionName, inputState, inputObject)
+ if inputState == Enum.UserInputState.End or inputState == Enum.UserInputState.Cancel then
+ self.turningLeft = 0
+ else
+ self.turningLeft = -1
+ end
+ self.steer = self.turningRight + self.turningLeft
+ end
+
+ -- Call this from a function bound to Renderstep with Input Priority
+ function VehicleController:Update(moveVector, cameraRelative, usingGamepad)
+ if self.vehicleSeat then
+ if cameraRelative then
+ -- This is the default steering mode
+ moveVector = moveVector + Vector3.new(self.steer, 0, self.throttle)
+ if usingGamepad and onlyTriggersForThrottle and useTriggersForThrottle then
+ self.vehicleSeat.ThrottleFloat = -self.throttle
+ else
+ self.vehicleSeat.ThrottleFloat = -moveVector.Z
+ end
+ self.vehicleSeat.SteerFloat = moveVector.X
+
+ return moveVector, true
+ else
+ -- This is the path following mode
+ local localMoveVector = self.vehicleSeat.Occupant.RootPart.CFrame:VectorToObjectSpace(moveVector)
+
+ self.vehicleSeat.ThrottleFloat = self:ComputeThrottle(localMoveVector)
+ self.vehicleSeat.SteerFloat = self:ComputeSteer(localMoveVector)
+
+ return ZERO_VECTOR3, true
+ end
+ end
+ return moveVector, false
+ end
+
+ function VehicleController:ComputeThrottle(localMoveVector)
+ if localMoveVector ~= ZERO_VECTOR3 then
+ local throttle = -localMoveVector.Z
+ return throttle
+ else
+ return 0.0
+ end
+ end
+
+ function VehicleController:ComputeSteer(localMoveVector)
+ if localMoveVector ~= ZERO_VECTOR3 then
+ local steerAngle = -math.atan2(-localMoveVector.x, -localMoveVector.z) * (180 / math.pi)
+ return steerAngle / self.autoPilot.MaxSteeringAngle
+ else
+ return 0.0
+ end
+ end
+
+ function VehicleController:SetupAutoPilot()
+ -- Setup default
+ self.autoPilot.MaxSpeed = self.vehicleSeat.MaxSpeed
+ self.autoPilot.MaxSteeringAngle = AUTO_PILOT_DEFAULT_MAX_STEERING_ANGLE
+
+ -- VehicleSeat should have a MaxSteeringAngle as well.
+ -- Or we could look for a child "AutoPilotConfigModule" to find these values
+ -- Or allow developer to set them through the API as like the CLickToMove customization API
+ end
+
+ return VehicleController
+end
+
+function _TouchJump()
+
+ local Players = game:GetService("Players")
+ local GuiService = game:GetService("GuiService")
+
+ --[[ Constants ]]--
+ local TOUCH_CONTROL_SHEET = "rbxasset://textures/ui/Input/TouchControlsSheetV2.png"
+
+ --[[ The Module ]]--
+ local BaseCharacterController = _BaseCharacterController()
+ local TouchJump = setmetatable({}, BaseCharacterController)
+ TouchJump.__index = TouchJump
+
+ function TouchJump.new()
+ local self = setmetatable(BaseCharacterController.new(), TouchJump)
+
+ self.parentUIFrame = nil
+ self.jumpButton = nil
+ self.characterAddedConn = nil
+ self.humanoidStateEnabledChangedConn = nil
+ self.humanoidJumpPowerConn = nil
+ self.humanoidParentConn = nil
+ self.externallyEnabled = false
+ self.jumpPower = 0
+ self.jumpStateEnabled = true
+ self.isJumping = false
+ self.humanoid = nil -- saved reference because property change connections are made using it
+
+ return self
+ end
+
+ function TouchJump:EnableButton(enable)
+ if enable then
+ if not self.jumpButton then
+ self:Create()
+ end
+ local humanoid = Players.LocalPlayer.Character and Players.LocalPlayer.Character:FindFirstChildOfClass("Humanoid")
+ if humanoid and self.externallyEnabled then
+ if self.externallyEnabled then
+ if humanoid.JumpPower > 0 then
+ self.jumpButton.Visible = true
+ end
+ end
+ end
+ else
+ self.jumpButton.Visible = false
+ self.isJumping = false
+ self.jumpButton.ImageRectOffset = Vector2.new(1, 146)
+ end
+ end
+
+ function TouchJump:UpdateEnabled()
+ if self.jumpPower > 0 and self.jumpStateEnabled then
+ self:EnableButton(true)
+ else
+ self:EnableButton(false)
+ end
+ end
+
+ function TouchJump:HumanoidChanged(prop)
+ local humanoid = Players.LocalPlayer.Character and Players.LocalPlayer.Character:FindFirstChildOfClass("Humanoid")
+ if humanoid then
+ if prop == "JumpPower" then
+ self.jumpPower = humanoid.JumpPower
+ self:UpdateEnabled()
+ elseif prop == "Parent" then
+ if not humanoid.Parent then
+ self.humanoidChangeConn:Disconnect()
+ end
+ end
+ end
+ end
+
+ function TouchJump:HumanoidStateEnabledChanged(state, isEnabled)
+ if state == Enum.HumanoidStateType.Jumping then
+ self.jumpStateEnabled = isEnabled
+ self:UpdateEnabled()
+ end
+ end
+
+ function TouchJump:CharacterAdded(char)
+ if self.humanoidChangeConn then
+ self.humanoidChangeConn:Disconnect()
+ self.humanoidChangeConn = nil
+ end
+
+ self.humanoid = char:FindFirstChildOfClass("Humanoid")
+ while not self.humanoid do
+ char.ChildAdded:wait()
+ self.humanoid = char:FindFirstChildOfClass("Humanoid")
+ end
+
+ self.humanoidJumpPowerConn = self.humanoid:GetPropertyChangedSignal("JumpPower"):Connect(function()
+ self.jumpPower = self.humanoid.JumpPower
+ self:UpdateEnabled()
+ end)
+
+ self.humanoidParentConn = self.humanoid:GetPropertyChangedSignal("Parent"):Connect(function()
+ if not self.humanoid.Parent then
+ self.humanoidJumpPowerConn:Disconnect()
+ self.humanoidJumpPowerConn = nil
+ self.humanoidParentConn:Disconnect()
+ self.humanoidParentConn = nil
+ end
+ end)
+
+ self.humanoidStateEnabledChangedConn = self.humanoid.StateEnabledChanged:Connect(function(state, enabled)
+ self:HumanoidStateEnabledChanged(state, enabled)
+ end)
+
+ self.jumpPower = self.humanoid.JumpPower
+ self.jumpStateEnabled = self.humanoid:GetStateEnabled(Enum.HumanoidStateType.Jumping)
+ self:UpdateEnabled()
+ end
+
+ function TouchJump:SetupCharacterAddedFunction()
+ self.characterAddedConn = Players.LocalPlayer.CharacterAdded:Connect(function(char)
+ self:CharacterAdded(char)
+ end)
+ if Players.LocalPlayer.Character then
+ self:CharacterAdded(Players.LocalPlayer.Character)
+ end
+ end
+
+ function TouchJump:Enable(enable, parentFrame)
+ if parentFrame then
+ self.parentUIFrame = parentFrame
+ end
+ self.externallyEnabled = enable
+ self:EnableButton(enable)
+ end
+
+ function TouchJump:Create()
+ if not self.parentUIFrame then
+ return
+ end
+
+ if self.jumpButton then
+ self.jumpButton:Destroy()
+ self.jumpButton = nil
+ end
+
+ local minAxis = math.min(self.parentUIFrame.AbsoluteSize.x, self.parentUIFrame.AbsoluteSize.y)
+ local isSmallScreen = minAxis <= 500
+ local jumpButtonSize = isSmallScreen and 70 or 120
+
+ self.jumpButton = Instance.new("ImageButton")
+ self.jumpButton.Name = "JumpButton"
+ self.jumpButton.Visible = false
+ self.jumpButton.BackgroundTransparency = 1
+ self.jumpButton.Image = TOUCH_CONTROL_SHEET
+ self.jumpButton.ImageRectOffset = Vector2.new(1, 146)
+ self.jumpButton.ImageRectSize = Vector2.new(144, 144)
+ self.jumpButton.Size = UDim2.new(0, jumpButtonSize, 0, jumpButtonSize)
+
+ self.jumpButton.Position = isSmallScreen and UDim2.new(1, -(jumpButtonSize*1.5-10), 1, -jumpButtonSize - 20) or
+ UDim2.new(1, -(jumpButtonSize*1.5-10), 1, -jumpButtonSize * 1.75)
+
+ local touchObject = nil
+ self.jumpButton.InputBegan:connect(function(inputObject)
+ --A touch that starts elsewhere on the screen will be sent to a frame's InputBegan event
+ --if it moves over the frame. So we check that this is actually a new touch (inputObject.UserInputState ~= Enum.UserInputState.Begin)
+ if touchObject or inputObject.UserInputType ~= Enum.UserInputType.Touch
+ or inputObject.UserInputState ~= Enum.UserInputState.Begin then
+ return
+ end
+
+ touchObject = inputObject
+ self.jumpButton.ImageRectOffset = Vector2.new(146, 146)
+ self.isJumping = true
+ end)
+
+ local OnInputEnded = function()
+ touchObject = nil
+ self.isJumping = false
+ self.jumpButton.ImageRectOffset = Vector2.new(1, 146)
+ end
+
+ self.jumpButton.InputEnded:connect(function(inputObject)
+ if inputObject == touchObject then
+ OnInputEnded()
+ end
+ end)
+
+ GuiService.MenuOpened:connect(function()
+ if touchObject then
+ OnInputEnded()
+ end
+ end)
+
+ if not self.characterAddedConn then
+ self:SetupCharacterAddedFunction()
+ end
+
+ self.jumpButton.Parent = self.parentUIFrame
+ end
+
+ return TouchJump
+end
+
+function _ClickToMoveController()
+ --[[ Roblox Services ]]--
+ local UserInputService = game:GetService("UserInputService")
+ local PathfindingService = game:GetService("PathfindingService")
+ local Players = game:GetService("Players")
+ local DebrisService = game:GetService('Debris')
+ local StarterGui = game:GetService("StarterGui")
+ local Workspace = game:GetService("Workspace")
+ local CollectionService = game:GetService("CollectionService")
+ local GuiService = game:GetService("GuiService")
+
+ --[[ Configuration ]]
+ local ShowPath = true
+ local PlayFailureAnimation = true
+ local UseDirectPath = false
+ local UseDirectPathForVehicle = true
+ local AgentSizeIncreaseFactor = 1.0
+ local UnreachableWaypointTimeout = 8
+
+ --[[ Constants ]]--
+ local movementKeys = {
+ [Enum.KeyCode.W] = true;
+ [Enum.KeyCode.A] = true;
+ [Enum.KeyCode.S] = true;
+ [Enum.KeyCode.D] = true;
+ [Enum.KeyCode.Up] = true;
+ [Enum.KeyCode.Down] = true;
+ }
+
+ local FFlagUserNavigationClickToMoveSkipPassedWaypointsSuccess, FFlagUserNavigationClickToMoveSkipPassedWaypointsResult = pcall(function() return UserSettings():IsUserFeatureEnabled("UserNavigationClickToMoveSkipPassedWaypoints") end)
+ local FFlagUserNavigationClickToMoveSkipPassedWaypoints = FFlagUserNavigationClickToMoveSkipPassedWaypointsSuccess and FFlagUserNavigationClickToMoveSkipPassedWaypointsResult
+
+ local Player = Players.LocalPlayer
+
+ local ClickToMoveDisplay = _ClickToMoveDisplay()
+
+ local ZERO_VECTOR3 = Vector3.new(0,0,0)
+ local ALMOST_ZERO = 0.000001
+
+
+ --------------------------UTIL LIBRARY-------------------------------
+ local Utility = {}
+ do
+ local function FindCharacterAncestor(part)
+ if part then
+ local humanoid = part:FindFirstChildOfClass("Humanoid")
+ if humanoid then
+ return part, humanoid
+ else
+ return FindCharacterAncestor(part.Parent)
+ end
+ end
+ end
+ Utility.FindCharacterAncestor = FindCharacterAncestor
+
+ local function Raycast(ray, ignoreNonCollidable, ignoreList)
+ ignoreList = ignoreList or {}
+ local hitPart, hitPos, hitNorm, hitMat = Workspace:FindPartOnRayWithIgnoreList(ray, ignoreList)
+ if hitPart then
+ if ignoreNonCollidable and hitPart.CanCollide == false then
+ -- We always include character parts so a user can click on another character
+ -- to walk to them.
+ local _, humanoid = FindCharacterAncestor(hitPart)
+ if humanoid == nil then
+ table.insert(ignoreList, hitPart)
+ return Raycast(ray, ignoreNonCollidable, ignoreList)
+ end
+ end
+ return hitPart, hitPos, hitNorm, hitMat
+ end
+ return nil, nil
+ end
+ Utility.Raycast = Raycast
+ end
+
+ local humanoidCache = {}
+ local function findPlayerHumanoid(player)
+ local character = player and player.Character
+ if character then
+ local resultHumanoid = humanoidCache[player]
+ if resultHumanoid and resultHumanoid.Parent == character then
+ return resultHumanoid
+ else
+ humanoidCache[player] = nil -- Bust Old Cache
+ local humanoid = character:FindFirstChildOfClass("Humanoid")
+ if humanoid then
+ humanoidCache[player] = humanoid
+ end
+ return humanoid
+ end
+ end
+ end
+
+ --------------------------CHARACTER CONTROL-------------------------------
+ local CurrentIgnoreList
+ local CurrentIgnoreTag = nil
+
+ local TaggedInstanceAddedConnection = nil
+ local TaggedInstanceRemovedConnection = nil
+
+ local function GetCharacter()
+ return Player and Player.Character
+ end
+
+ local function UpdateIgnoreTag(newIgnoreTag)
+ if newIgnoreTag == CurrentIgnoreTag then
+ return
+ end
+ if TaggedInstanceAddedConnection then
+ TaggedInstanceAddedConnection:Disconnect()
+ TaggedInstanceAddedConnection = nil
+ end
+ if TaggedInstanceRemovedConnection then
+ TaggedInstanceRemovedConnection:Disconnect()
+ TaggedInstanceRemovedConnection = nil
+ end
+ CurrentIgnoreTag = newIgnoreTag
+ CurrentIgnoreList = {GetCharacter()}
+ if CurrentIgnoreTag ~= nil then
+ local ignoreParts = CollectionService:GetTagged(CurrentIgnoreTag)
+ for _, ignorePart in ipairs(ignoreParts) do
+ table.insert(CurrentIgnoreList, ignorePart)
+ end
+ TaggedInstanceAddedConnection = CollectionService:GetInstanceAddedSignal(
+ CurrentIgnoreTag):Connect(function(ignorePart)
+ table.insert(CurrentIgnoreList, ignorePart)
+ end)
+ TaggedInstanceRemovedConnection = CollectionService:GetInstanceRemovedSignal(
+ CurrentIgnoreTag):Connect(function(ignorePart)
+ for i = 1, #CurrentIgnoreList do
+ if CurrentIgnoreList[i] == ignorePart then
+ CurrentIgnoreList[i] = CurrentIgnoreList[#CurrentIgnoreList]
+ table.remove(CurrentIgnoreList)
+ break
+ end
+ end
+ end)
+ end
+ end
+
+ local function getIgnoreList()
+ if CurrentIgnoreList then
+ return CurrentIgnoreList
+ end
+ CurrentIgnoreList = {}
+ table.insert(CurrentIgnoreList, GetCharacter())
+ return CurrentIgnoreList
+ end
+
+ -----------------------------------PATHER--------------------------------------
+
+ local function Pather(endPoint, surfaceNormal, overrideUseDirectPath)
+ local this = {}
+
+ local directPathForHumanoid
+ local directPathForVehicle
+ if overrideUseDirectPath ~= nil then
+ directPathForHumanoid = overrideUseDirectPath
+ directPathForVehicle = overrideUseDirectPath
+ else
+ directPathForHumanoid = UseDirectPath
+ directPathForVehicle = UseDirectPathForVehicle
+ end
+
+ this.Cancelled = false
+ this.Started = false
+
+ this.Finished = Instance.new("BindableEvent")
+ this.PathFailed = Instance.new("BindableEvent")
+
+ this.PathComputing = false
+ this.PathComputed = false
+
+ this.OriginalTargetPoint = endPoint
+ this.TargetPoint = endPoint
+ this.TargetSurfaceNormal = surfaceNormal
+
+ this.DiedConn = nil
+ this.SeatedConn = nil
+ this.BlockedConn = nil
+ this.TeleportedConn = nil
+
+ this.CurrentPoint = 0
+
+ this.HumanoidOffsetFromPath = ZERO_VECTOR3
+
+ this.CurrentWaypointPosition = nil
+ this.CurrentWaypointPlaneNormal = ZERO_VECTOR3
+ this.CurrentWaypointPlaneDistance = 0
+ this.CurrentWaypointNeedsJump = false;
+
+ this.CurrentHumanoidPosition = ZERO_VECTOR3
+ this.CurrentHumanoidVelocity = 0
+
+ this.NextActionMoveDirection = ZERO_VECTOR3
+ this.NextActionJump = false
+
+ this.Timeout = 0
+
+ this.Humanoid = findPlayerHumanoid(Player)
+ this.OriginPoint = nil
+ this.AgentCanFollowPath = false
+ this.DirectPath = false
+ this.DirectPathRiseFirst = false
+
+ local rootPart = this.Humanoid and this.Humanoid.RootPart
+ if rootPart then
+ -- Setup origin
+ this.OriginPoint = rootPart.CFrame.p
+
+ -- Setup agent
+ local agentRadius = 2
+ local agentHeight = 5
+ local agentCanJump = true
+
+ local seat = this.Humanoid.SeatPart
+ if seat and seat:IsA("VehicleSeat") then
+ -- Humanoid is seated on a vehicle
+ local vehicle = seat:FindFirstAncestorOfClass("Model")
+ if vehicle then
+ -- Make sure the PrimaryPart is set to the vehicle seat while we compute the extends.
+ local tempPrimaryPart = vehicle.PrimaryPart
+ vehicle.PrimaryPart = seat
+
+ -- For now, only direct path
+ if directPathForVehicle then
+ local extents = vehicle:GetExtentsSize()
+ agentRadius = AgentSizeIncreaseFactor * 0.5 * math.sqrt(extents.X * extents.X + extents.Z * extents.Z)
+ agentHeight = AgentSizeIncreaseFactor * extents.Y
+ agentCanJump = false
+ this.AgentCanFollowPath = true
+ this.DirectPath = directPathForVehicle
+ end
+
+ -- Reset PrimaryPart
+ vehicle.PrimaryPart = tempPrimaryPart
+ end
+ else
+ local extents = GetCharacter():GetExtentsSize()
+ agentRadius = AgentSizeIncreaseFactor * 0.5 * math.sqrt(extents.X * extents.X + extents.Z * extents.Z)
+ agentHeight = AgentSizeIncreaseFactor * extents.Y
+ agentCanJump = (this.Humanoid.JumpPower > 0)
+ this.AgentCanFollowPath = true
+ this.DirectPath = directPathForHumanoid
+ this.DirectPathRiseFirst = this.Humanoid.Sit
+ end
+
+ -- Build path object
+ this.pathResult = PathfindingService:CreatePath({AgentRadius = agentRadius, AgentHeight = agentHeight, AgentCanJump = agentCanJump})
+ end
+
+ function this:Cleanup()
+ if this.stopTraverseFunc then
+ this.stopTraverseFunc()
+ this.stopTraverseFunc = nil
+ end
+
+ if this.MoveToConn then
+ this.MoveToConn:Disconnect()
+ this.MoveToConn = nil
+ end
+
+ if this.BlockedConn then
+ this.BlockedConn:Disconnect()
+ this.BlockedConn = nil
+ end
+
+ if this.DiedConn then
+ this.DiedConn:Disconnect()
+ this.DiedConn = nil
+ end
+
+ if this.SeatedConn then
+ this.SeatedConn:Disconnect()
+ this.SeatedConn = nil
+ end
+
+ if this.TeleportedConn then
+ this.TeleportedConn:Disconnect()
+ this.TeleportedConn = nil
+ end
+
+ this.Started = false
+ end
+
+ function this:Cancel()
+ this.Cancelled = true
+ this:Cleanup()
+ end
+
+ function this:IsActive()
+ return this.AgentCanFollowPath and this.Started and not this.Cancelled
+ end
+
+ function this:OnPathInterrupted()
+ -- Stop moving
+ this.Cancelled = true
+ this:OnPointReached(false)
+ end
+
+ function this:ComputePath()
+ if this.OriginPoint then
+ if this.PathComputed or this.PathComputing then return end
+ this.PathComputing = true
+ if this.AgentCanFollowPath then
+ if this.DirectPath then
+ this.pointList = {
+ PathWaypoint.new(this.OriginPoint, Enum.PathWaypointAction.Walk),
+ PathWaypoint.new(this.TargetPoint, this.DirectPathRiseFirst and Enum.PathWaypointAction.Jump or Enum.PathWaypointAction.Walk)
+ }
+ this.PathComputed = true
+ else
+ this.pathResult:ComputeAsync(this.OriginPoint, this.TargetPoint)
+ this.pointList = this.pathResult:GetWaypoints()
+ this.BlockedConn = this.pathResult.Blocked:Connect(function(blockedIdx) this:OnPathBlocked(blockedIdx) end)
+ this.PathComputed = this.pathResult.Status == Enum.PathStatus.Success
+ end
+ end
+ this.PathComputing = false
+ end
+ end
+
+ function this:IsValidPath()
+ this:ComputePath()
+ return this.PathComputed and this.AgentCanFollowPath
+ end
+
+ this.Recomputing = false
+ function this:OnPathBlocked(blockedWaypointIdx)
+ local pathBlocked = blockedWaypointIdx >= this.CurrentPoint
+ if not pathBlocked or this.Recomputing then
+ return
+ end
+
+ this.Recomputing = true
+
+ if this.stopTraverseFunc then
+ this.stopTraverseFunc()
+ this.stopTraverseFunc = nil
+ end
+
+ this.OriginPoint = this.Humanoid.RootPart.CFrame.p
+
+ this.pathResult:ComputeAsync(this.OriginPoint, this.TargetPoint)
+ this.pointList = this.pathResult:GetWaypoints()
+ if #this.pointList > 0 then
+ this.HumanoidOffsetFromPath = this.pointList[1].Position - this.OriginPoint
+ end
+ this.PathComputed = this.pathResult.Status == Enum.PathStatus.Success
+
+ if ShowPath then
+ this.stopTraverseFunc, this.setPointFunc = ClickToMoveDisplay.CreatePathDisplay(this.pointList)
+ end
+ if this.PathComputed then
+ this.CurrentPoint = 1 -- The first waypoint is always the start location. Skip it.
+ this:OnPointReached(true) -- Move to first point
+ else
+ this.PathFailed:Fire()
+ this:Cleanup()
+ end
+
+ this.Recomputing = false
+ end
+
+ function this:OnRenderStepped(dt)
+ if this.Started and not this.Cancelled then
+ -- Check for Timeout (if a waypoint is not reached within the delay, we fail)
+ this.Timeout = this.Timeout + dt
+ if this.Timeout > UnreachableWaypointTimeout then
+ this:OnPointReached(false)
+ return
+ end
+
+ -- Get Humanoid position and velocity
+ this.CurrentHumanoidPosition = this.Humanoid.RootPart.Position + this.HumanoidOffsetFromPath
+ this.CurrentHumanoidVelocity = this.Humanoid.RootPart.Velocity
+
+ -- Check if it has reached some waypoints
+ while this.Started and this:IsCurrentWaypointReached() do
+ this:OnPointReached(true)
+ end
+
+ -- If still started, update actions
+ if this.Started then
+ -- Move action
+ this.NextActionMoveDirection = this.CurrentWaypointPosition - this.CurrentHumanoidPosition
+ if this.NextActionMoveDirection.Magnitude > ALMOST_ZERO then
+ this.NextActionMoveDirection = this.NextActionMoveDirection.Unit
+ else
+ this.NextActionMoveDirection = ZERO_VECTOR3
+ end
+ -- Jump action
+ if this.CurrentWaypointNeedsJump then
+ this.NextActionJump = true
+ this.CurrentWaypointNeedsJump = false -- Request jump only once
+ else
+ this.NextActionJump = false
+ end
+ end
+ end
+ end
+
+ function this:IsCurrentWaypointReached()
+ local reached = false
+
+ -- Check we do have a plane, if not, we consider the waypoint reached
+ if this.CurrentWaypointPlaneNormal ~= ZERO_VECTOR3 then
+ -- Compute distance of Humanoid from destination plane
+ local dist = this.CurrentWaypointPlaneNormal:Dot(this.CurrentHumanoidPosition) - this.CurrentWaypointPlaneDistance
+ -- Compute the component of the Humanoid velocity that is towards the plane
+ local velocity = -this.CurrentWaypointPlaneNormal:Dot(this.CurrentHumanoidVelocity)
+ -- Compute the threshold from the destination plane based on Humanoid velocity
+ local threshold = math.max(1.0, 0.0625 * velocity)
+ -- If we are less then threshold in front of the plane (between 0 and threshold) or if we are behing the plane (less then 0), we consider we reached it
+ reached = dist < threshold
+ else
+ reached = true
+ end
+
+ if reached then
+ this.CurrentWaypointPosition = nil
+ this.CurrentWaypointPlaneNormal = ZERO_VECTOR3
+ this.CurrentWaypointPlaneDistance = 0
+ end
+
+ return reached
+ end
+
+ function this:OnPointReached(reached)
+
+ if reached and not this.Cancelled then
+ -- First, destroyed the current displayed waypoint
+ if this.setPointFunc then
+ this.setPointFunc(this.CurrentPoint)
+ end
+
+ local nextWaypointIdx = this.CurrentPoint + 1
+
+ if nextWaypointIdx > #this.pointList then
+ -- End of path reached
+ if this.stopTraverseFunc then
+ this.stopTraverseFunc()
+ end
+ this.Finished:Fire()
+ this:Cleanup()
+ else
+ local currentWaypoint = this.pointList[this.CurrentPoint]
+ local nextWaypoint = this.pointList[nextWaypointIdx]
+
+ -- If airborne, only allow to keep moving
+ -- if nextWaypoint.Action ~= Jump, or path mantains a direction
+ -- Otherwise, wait until the humanoid gets to the ground
+ local currentState = this.Humanoid:GetState()
+ local isInAir = currentState == Enum.HumanoidStateType.FallingDown
+ or currentState == Enum.HumanoidStateType.Freefall
+ or currentState == Enum.HumanoidStateType.Jumping
+
+ if isInAir then
+ local shouldWaitForGround = nextWaypoint.Action == Enum.PathWaypointAction.Jump
+ if not shouldWaitForGround and this.CurrentPoint > 1 then
+ local prevWaypoint = this.pointList[this.CurrentPoint - 1]
+
+ local prevDir = currentWaypoint.Position - prevWaypoint.Position
+ local currDir = nextWaypoint.Position - currentWaypoint.Position
+
+ local prevDirXZ = Vector2.new(prevDir.x, prevDir.z).Unit
+ local currDirXZ = Vector2.new(currDir.x, currDir.z).Unit
+
+ local THRESHOLD_COS = 0.996 -- ~cos(5 degrees)
+ shouldWaitForGround = prevDirXZ:Dot(currDirXZ) < THRESHOLD_COS
+ end
+
+ if shouldWaitForGround then
+ this.Humanoid.FreeFalling:Wait()
+
+ -- Give time to the humanoid's state to change
+ -- Otherwise, the jump flag in Humanoid
+ -- will be reset by the state change
+ wait(0.1)
+ end
+ end
+
+ -- Move to the next point
+ if FFlagUserNavigationClickToMoveSkipPassedWaypoints then
+ this:MoveToNextWayPoint(currentWaypoint, nextWaypoint, nextWaypointIdx)
+ else
+ if this.setPointFunc then
+ this.setPointFunc(nextWaypointIdx)
+ end
+ if nextWaypoint.Action == Enum.PathWaypointAction.Jump then
+ this.Humanoid.Jump = true
+ end
+ this.Humanoid:MoveTo(nextWaypoint.Position)
+
+ this.CurrentPoint = nextWaypointIdx
+ end
+ end
+ else
+ this.PathFailed:Fire()
+ this:Cleanup()
+ end
+ end
+
+ function this:MoveToNextWayPoint(currentWaypoint, nextWaypoint, nextWaypointIdx)
+ -- Build next destination plane
+ -- (plane normal is perpendicular to the y plane and is from next waypoint towards current one (provided the two waypoints are not at the same location))
+ -- (plane location is at next waypoint)
+ this.CurrentWaypointPlaneNormal = currentWaypoint.Position - nextWaypoint.Position
+ this.CurrentWaypointPlaneNormal = Vector3.new(this.CurrentWaypointPlaneNormal.X, 0, this.CurrentWaypointPlaneNormal.Z)
+ if this.CurrentWaypointPlaneNormal.Magnitude > ALMOST_ZERO then
+ this.CurrentWaypointPlaneNormal = this.CurrentWaypointPlaneNormal.Unit
+ this.CurrentWaypointPlaneDistance = this.CurrentWaypointPlaneNormal:Dot(nextWaypoint.Position)
+ else
+ -- Next waypoint is the same as current waypoint so no plane
+ this.CurrentWaypointPlaneNormal = ZERO_VECTOR3
+ this.CurrentWaypointPlaneDistance = 0
+ end
+
+ -- Should we jump
+ this.CurrentWaypointNeedsJump = nextWaypoint.Action == Enum.PathWaypointAction.Jump;
+
+ -- Remember next waypoint position
+ this.CurrentWaypointPosition = nextWaypoint.Position
+
+ -- Move to next point
+ this.CurrentPoint = nextWaypointIdx
+
+ -- Finally reset Timeout
+ this.Timeout = 0
+ end
+
+ function this:Start(overrideShowPath)
+ if not this.AgentCanFollowPath then
+ this.PathFailed:Fire()
+ return
+ end
+
+ if this.Started then return end
+ this.Started = true
+
+ ClickToMoveDisplay.CancelFailureAnimation()
+
+ if ShowPath then
+ if overrideShowPath == nil or overrideShowPath then
+ this.stopTraverseFunc, this.setPointFunc = ClickToMoveDisplay.CreatePathDisplay(this.pointList, this.OriginalTargetPoint)
+ end
+ end
+
+ if #this.pointList > 0 then
+ -- Determine the humanoid offset from the path's first point
+ -- Offset of the first waypoint from the path's origin point
+ this.HumanoidOffsetFromPath = Vector3.new(0, this.pointList[1].Position.Y - this.OriginPoint.Y, 0)
+
+ -- As well as its current position and velocity
+ this.CurrentHumanoidPosition = this.Humanoid.RootPart.Position + this.HumanoidOffsetFromPath
+ this.CurrentHumanoidVelocity = this.Humanoid.RootPart.Velocity
+
+ -- Connect to events
+ this.SeatedConn = this.Humanoid.Seated:Connect(function(isSeated, seat) this:OnPathInterrupted() end)
+ this.DiedConn = this.Humanoid.Died:Connect(function() this:OnPathInterrupted() end)
+ this.TeleportedConn = this.Humanoid.RootPart:GetPropertyChangedSignal("CFrame"):Connect(function() this:OnPathInterrupted() end)
+
+ -- Actually start
+ this.CurrentPoint = 1 -- The first waypoint is always the start location. Skip it.
+ this:OnPointReached(true) -- Move to first point
+ else
+ this.PathFailed:Fire()
+ if this.stopTraverseFunc then
+ this.stopTraverseFunc()
+ end
+ end
+ end
+
+ --We always raycast to the ground in the case that the user clicked a wall.
+ local offsetPoint = this.TargetPoint + this.TargetSurfaceNormal*1.5
+ local ray = Ray.new(offsetPoint, Vector3.new(0,-1,0)*50)
+ local newHitPart, newHitPos = Workspace:FindPartOnRayWithIgnoreList(ray, getIgnoreList())
+ if newHitPart then
+ this.TargetPoint = newHitPos
+ end
+ this:ComputePath()
+
+ return this
+ end
+
+ -------------------------------------------------------------------------
+
+ local function CheckAlive()
+ local humanoid = findPlayerHumanoid(Player)
+ return humanoid ~= nil and humanoid.Health > 0
+ end
+
+ local function GetEquippedTool(character)
+ if character ~= nil then
+ for _, child in pairs(character:GetChildren()) do
+ if child:IsA('Tool') then
+ return child
+ end
+ end
+ end
+ end
+
+ local ExistingPather = nil
+ local ExistingIndicator = nil
+ local PathCompleteListener = nil
+ local PathFailedListener = nil
+
+ local function CleanupPath()
+ if ExistingPather then
+ ExistingPather:Cancel()
+ ExistingPather = nil
+ end
+ if PathCompleteListener then
+ PathCompleteListener:Disconnect()
+ PathCompleteListener = nil
+ end
+ if PathFailedListener then
+ PathFailedListener:Disconnect()
+ PathFailedListener = nil
+ end
+ if ExistingIndicator then
+ ExistingIndicator:Destroy()
+ end
+ end
+
+ local function HandleMoveTo(thisPather, hitPt, hitChar, character, overrideShowPath)
+ if ExistingPather then
+ CleanupPath()
+ end
+ ExistingPather = thisPather
+ thisPather:Start(overrideShowPath)
+
+ PathCompleteListener = thisPather.Finished.Event:Connect(function()
+ CleanupPath()
+ if hitChar then
+ local currentWeapon = GetEquippedTool(character)
+ if currentWeapon then
+ currentWeapon:Activate()
+ end
+ end
+ end)
+ PathFailedListener = thisPather.PathFailed.Event:Connect(function()
+ CleanupPath()
+ if overrideShowPath == nil or overrideShowPath then
+ local shouldPlayFailureAnim = PlayFailureAnimation and not (ExistingPather and ExistingPather:IsActive())
+ if shouldPlayFailureAnim then
+ ClickToMoveDisplay.PlayFailureAnimation()
+ end
+ ClickToMoveDisplay.DisplayFailureWaypoint(hitPt)
+ end
+ end)
+ end
+
+ local function ShowPathFailedFeedback(hitPt)
+ if ExistingPather and ExistingPather:IsActive() then
+ ExistingPather:Cancel()
+ end
+ if PlayFailureAnimation then
+ ClickToMoveDisplay.PlayFailureAnimation()
+ end
+ ClickToMoveDisplay.DisplayFailureWaypoint(hitPt)
+ end
+
+ function OnTap(tapPositions, goToPoint, wasTouchTap)
+ -- Good to remember if this is the latest tap event
+ local camera = Workspace.CurrentCamera
+ local character = Player.Character
+
+ if not CheckAlive() then return end
+
+ -- This is a path tap position
+ if #tapPositions == 1 or goToPoint then
+ if camera then
+ local unitRay = camera:ScreenPointToRay(tapPositions[1].x, tapPositions[1].y)
+ local ray = Ray.new(unitRay.Origin, unitRay.Direction*1000)
+
+ local myHumanoid = findPlayerHumanoid(Player)
+ local hitPart, hitPt, hitNormal = Utility.Raycast(ray, true, getIgnoreList())
+
+ local hitChar, hitHumanoid = Utility.FindCharacterAncestor(hitPart)
+ if wasTouchTap and hitHumanoid and StarterGui:GetCore("AvatarContextMenuEnabled") then
+ local clickedPlayer = Players:GetPlayerFromCharacter(hitHumanoid.Parent)
+ if clickedPlayer then
+ CleanupPath()
+ return
+ end
+ end
+ if goToPoint then
+ hitPt = goToPoint
+ hitChar = nil
+ end
+ if hitPt and character then
+ -- Clean up current path
+ CleanupPath()
+ local thisPather = Pather(hitPt, hitNormal)
+ if thisPather:IsValidPath() then
+ HandleMoveTo(thisPather, hitPt, hitChar, character)
+ else
+ -- Clean up
+ thisPather:Cleanup()
+ -- Feedback here for when we don't have a good path
+ ShowPathFailedFeedback(hitPt)
+ end
+ end
+ end
+ elseif #tapPositions >= 2 then
+ if camera then
+ -- Do shoot
+ local currentWeapon = GetEquippedTool(character)
+ if currentWeapon then
+ currentWeapon:Activate()
+ end
+ end
+ end
+ end
+
+ local function DisconnectEvent(event)
+ if event then
+ event:Disconnect()
+ end
+ end
+
+ --[[ The ClickToMove Controller Class ]]--
+ local KeyboardController = _Keyboard()
+ local ClickToMove = setmetatable({}, KeyboardController)
+ ClickToMove.__index = ClickToMove
+
+ function ClickToMove.new(CONTROL_ACTION_PRIORITY)
+ local self = setmetatable(KeyboardController.new(CONTROL_ACTION_PRIORITY), ClickToMove)
+
+ self.fingerTouches = {}
+ self.numUnsunkTouches = 0
+ -- PC simulation
+ self.mouse1Down = tick()
+ self.mouse1DownPos = Vector2.new()
+ self.mouse2DownTime = tick()
+ self.mouse2DownPos = Vector2.new()
+ self.mouse2UpTime = tick()
+
+ self.keyboardMoveVector = ZERO_VECTOR3
+
+ self.tapConn = nil
+ self.inputBeganConn = nil
+ self.inputChangedConn = nil
+ self.inputEndedConn = nil
+ self.humanoidDiedConn = nil
+ self.characterChildAddedConn = nil
+ self.onCharacterAddedConn = nil
+ self.characterChildRemovedConn = nil
+ self.renderSteppedConn = nil
+ self.menuOpenedConnection = nil
+
+ self.running = false
+
+ self.wasdEnabled = false
+
+ return self
+ end
+
+ function ClickToMove:DisconnectEvents()
+ DisconnectEvent(self.tapConn)
+ DisconnectEvent(self.inputBeganConn)
+ DisconnectEvent(self.inputChangedConn)
+ DisconnectEvent(self.inputEndedConn)
+ DisconnectEvent(self.humanoidDiedConn)
+ DisconnectEvent(self.characterChildAddedConn)
+ DisconnectEvent(self.onCharacterAddedConn)
+ DisconnectEvent(self.renderSteppedConn)
+ DisconnectEvent(self.characterChildRemovedConn)
+ DisconnectEvent(self.menuOpenedConnection)
+ end
+
+ function ClickToMove:OnTouchBegan(input, processed)
+ if self.fingerTouches[input] == nil and not processed then
+ self.numUnsunkTouches = self.numUnsunkTouches + 1
+ end
+ self.fingerTouches[input] = processed
+ end
+
+ function ClickToMove:OnTouchChanged(input, processed)
+ if self.fingerTouches[input] == nil then
+ self.fingerTouches[input] = processed
+ if not processed then
+ self.numUnsunkTouches = self.numUnsunkTouches + 1
+ end
+ end
+ end
+
+ function ClickToMove:OnTouchEnded(input, processed)
+ if self.fingerTouches[input] ~= nil and self.fingerTouches[input] == false then
+ self.numUnsunkTouches = self.numUnsunkTouches - 1
+ end
+ self.fingerTouches[input] = nil
+ end
+
+
+ function ClickToMove:OnCharacterAdded(character)
+ self:DisconnectEvents()
+
+ self.inputBeganConn = UserInputService.InputBegan:Connect(function(input, processed)
+ if input.UserInputType == Enum.UserInputType.Touch then
+ self:OnTouchBegan(input, processed)
+ end
+
+ -- Cancel path when you use the keyboard controls if wasd is enabled.
+ if self.wasdEnabled and processed == false and input.UserInputType == Enum.UserInputType.Keyboard
+ and movementKeys[input.KeyCode] then
+ CleanupPath()
+ ClickToMoveDisplay.CancelFailureAnimation()
+ end
+ if input.UserInputType == Enum.UserInputType.MouseButton1 then
+ self.mouse1DownTime = tick()
+ self.mouse1DownPos = input.Position
+ end
+ if input.UserInputType == Enum.UserInputType.MouseButton2 then
+ self.mouse2DownTime = tick()
+ self.mouse2DownPos = input.Position
+ end
+ end)
+
+ self.inputChangedConn = UserInputService.InputChanged:Connect(function(input, processed)
+ if input.UserInputType == Enum.UserInputType.Touch then
+ self:OnTouchChanged(input, processed)
+ end
+ end)
+
+ self.inputEndedConn = UserInputService.InputEnded:Connect(function(input, processed)
+ if input.UserInputType == Enum.UserInputType.Touch then
+ self:OnTouchEnded(input, processed)
+ end
+
+ if input.UserInputType == Enum.UserInputType.MouseButton2 then
+ self.mouse2UpTime = tick()
+ local currPos = input.Position
+ -- We allow click to move during path following or if there is no keyboard movement
+ local allowed = ExistingPather or self.keyboardMoveVector.Magnitude <= 0
+ if self.mouse2UpTime - self.mouse2DownTime < 0.25 and (currPos - self.mouse2DownPos).magnitude < 5 and allowed then
+ local positions = {currPos}
+ OnTap(positions)
+ end
+ end
+ end)
+
+ self.tapConn = UserInputService.TouchTap:Connect(function(touchPositions, processed)
+ if not processed then
+ OnTap(touchPositions, nil, true)
+ end
+ end)
+
+ self.menuOpenedConnection = GuiService.MenuOpened:Connect(function()
+ CleanupPath()
+ end)
+
+ local function OnCharacterChildAdded(child)
+ if UserInputService.TouchEnabled then
+ if child:IsA('Tool') then
+ child.ManualActivationOnly = true
+ end
+ end
+ if child:IsA('Humanoid') then
+ DisconnectEvent(self.humanoidDiedConn)
+ self.humanoidDiedConn = child.Died:Connect(function()
+ if ExistingIndicator then
+ DebrisService:AddItem(ExistingIndicator.Model, 1)
+ end
+ end)
+ end
+ end
+
+ self.characterChildAddedConn = character.ChildAdded:Connect(function(child)
+ OnCharacterChildAdded(child)
+ end)
+ self.characterChildRemovedConn = character.ChildRemoved:Connect(function(child)
+ if UserInputService.TouchEnabled then
+ if child:IsA('Tool') then
+ child.ManualActivationOnly = false
+ end
+ end
+ end)
+ for _, child in pairs(character:GetChildren()) do
+ OnCharacterChildAdded(child)
+ end
+ end
+
+ function ClickToMove:Start()
+ self:Enable(true)
+ end
+
+ function ClickToMove:Stop()
+ self:Enable(false)
+ end
+
+ function ClickToMove:CleanupPath()
+ CleanupPath()
+ end
+
+ function ClickToMove:Enable(enable, enableWASD, touchJumpController)
+ if enable then
+ if not self.running then
+ if Player.Character then -- retro-listen
+ self:OnCharacterAdded(Player.Character)
+ end
+ self.onCharacterAddedConn = Player.CharacterAdded:Connect(function(char)
+ self:OnCharacterAdded(char)
+ end)
+ self.running = true
+ end
+ self.touchJumpController = touchJumpController
+ if self.touchJumpController then
+ self.touchJumpController:Enable(self.jumpEnabled)
+ end
+ else
+ if self.running then
+ self:DisconnectEvents()
+ CleanupPath()
+ -- Restore tool activation on shutdown
+ if UserInputService.TouchEnabled then
+ local character = Player.Character
+ if character then
+ for _, child in pairs(character:GetChildren()) do
+ if child:IsA('Tool') then
+ child.ManualActivationOnly = false
+ end
+ end
+ end
+ end
+ self.running = false
+ end
+ if self.touchJumpController and not self.jumpEnabled then
+ self.touchJumpController:Enable(true)
+ end
+ self.touchJumpController = nil
+ end
+
+ -- Extension for initializing Keyboard input as this class now derives from Keyboard
+ if UserInputService.KeyboardEnabled and enable ~= self.enabled then
+
+ self.forwardValue = 0
+ self.backwardValue = 0
+ self.leftValue = 0
+ self.rightValue = 0
+
+ self.moveVector = ZERO_VECTOR3
+
+ if enable then
+ self:BindContextActions()
+ self:ConnectFocusEventListeners()
+ else
+ self:UnbindContextActions()
+ self:DisconnectFocusEventListeners()
+ end
+ end
+
+ self.wasdEnabled = enable and enableWASD or false
+ self.enabled = enable
+ end
+
+ function ClickToMove:OnRenderStepped(dt)
+ -- Reset jump
+ self.isJumping = false
+
+ -- Handle Pather
+ if ExistingPather then
+ -- Let the Pather update
+ ExistingPather:OnRenderStepped(dt)
+
+ -- If we still have a Pather, set the resulting actions
+ if ExistingPather then
+ -- Setup move (NOT relative to camera)
+ self.moveVector = ExistingPather.NextActionMoveDirection
+ self.moveVectorIsCameraRelative = false
+
+ -- Setup jump (but do NOT prevent the base Keayboard class from requesting jumps as well)
+ if ExistingPather.NextActionJump then
+ self.isJumping = true
+ end
+ else
+ self.moveVector = self.keyboardMoveVector
+ self.moveVectorIsCameraRelative = true
+ end
+ else
+ self.moveVector = self.keyboardMoveVector
+ self.moveVectorIsCameraRelative = true
+ end
+
+ -- Handle Keyboard's jump
+ if self.jumpRequested then
+ self.isJumping = true
+ end
+ end
+
+ -- Overrides Keyboard:UpdateMovement(inputState) to conditionally consider self.wasdEnabled and let OnRenderStepped handle the movement
+ function ClickToMove:UpdateMovement(inputState)
+ if inputState == Enum.UserInputState.Cancel then
+ self.keyboardMoveVector = ZERO_VECTOR3
+ elseif self.wasdEnabled then
+ self.keyboardMoveVector = Vector3.new(self.leftValue + self.rightValue, 0, self.forwardValue + self.backwardValue)
+ end
+ end
+
+ -- Overrides Keyboard:UpdateJump() because jump is handled in OnRenderStepped
+ function ClickToMove:UpdateJump()
+ -- Nothing to do (handled in OnRenderStepped)
+ end
+
+ --Public developer facing functions
+ function ClickToMove:SetShowPath(value)
+ ShowPath = value
+ end
+
+ function ClickToMove:GetShowPath()
+ return ShowPath
+ end
+
+ function ClickToMove:SetWaypointTexture(texture)
+ ClickToMoveDisplay.SetWaypointTexture(texture)
+ end
+
+ function ClickToMove:GetWaypointTexture()
+ return ClickToMoveDisplay.GetWaypointTexture()
+ end
+
+ function ClickToMove:SetWaypointRadius(radius)
+ ClickToMoveDisplay.SetWaypointRadius(radius)
+ end
+
+ function ClickToMove:GetWaypointRadius()
+ return ClickToMoveDisplay.GetWaypointRadius()
+ end
+
+ function ClickToMove:SetEndWaypointTexture(texture)
+ ClickToMoveDisplay.SetEndWaypointTexture(texture)
+ end
+
+ function ClickToMove:GetEndWaypointTexture()
+ return ClickToMoveDisplay.GetEndWaypointTexture()
+ end
+
+ function ClickToMove:SetWaypointsAlwaysOnTop(alwaysOnTop)
+ ClickToMoveDisplay.SetWaypointsAlwaysOnTop(alwaysOnTop)
+ end
+
+ function ClickToMove:GetWaypointsAlwaysOnTop()
+ return ClickToMoveDisplay.GetWaypointsAlwaysOnTop()
+ end
+
+ function ClickToMove:SetFailureAnimationEnabled(enabled)
+ PlayFailureAnimation = enabled
+ end
+
+ function ClickToMove:GetFailureAnimationEnabled()
+ return PlayFailureAnimation
+ end
+
+ function ClickToMove:SetIgnoredPartsTag(tag)
+ UpdateIgnoreTag(tag)
+ end
+
+ function ClickToMove:GetIgnoredPartsTag()
+ return CurrentIgnoreTag
+ end
+
+ function ClickToMove:SetUseDirectPath(directPath)
+ UseDirectPath = directPath
+ end
+
+ function ClickToMove:GetUseDirectPath()
+ return UseDirectPath
+ end
+
+ function ClickToMove:SetAgentSizeIncreaseFactor(increaseFactorPercent)
+ AgentSizeIncreaseFactor = 1.0 + (increaseFactorPercent / 100.0)
+ end
+
+ function ClickToMove:GetAgentSizeIncreaseFactor()
+ return (AgentSizeIncreaseFactor - 1.0) * 100.0
+ end
+
+ function ClickToMove:SetUnreachableWaypointTimeout(timeoutInSec)
+ UnreachableWaypointTimeout = timeoutInSec
+ end
+
+ function ClickToMove:GetUnreachableWaypointTimeout()
+ return UnreachableWaypointTimeout
+ end
+
+ function ClickToMove:SetUserJumpEnabled(jumpEnabled)
+ self.jumpEnabled = jumpEnabled
+ if self.touchJumpController then
+ self.touchJumpController:Enable(jumpEnabled)
+ end
+ end
+
+ function ClickToMove:GetUserJumpEnabled()
+ return self.jumpEnabled
+ end
+
+ function ClickToMove:MoveTo(position, showPath, useDirectPath)
+ local character = Player.Character
+ if character == nil then
+ return false
+ end
+ local thisPather = Pather(position, Vector3.new(0, 1, 0), useDirectPath)
+ if thisPather and thisPather:IsValidPath() then
+ HandleMoveTo(thisPather, position, nil, character, showPath)
+ return true
+ end
+ return false
+ end
+
+ return ClickToMove
+end
+
+function _TouchThumbstick()
+ local Players = game:GetService("Players")
+ local GuiService = game:GetService("GuiService")
+ local UserInputService = game:GetService("UserInputService")
+ --[[ Constants ]]--
+ local ZERO_VECTOR3 = Vector3.new(0,0,0)
+ local TOUCH_CONTROL_SHEET = "rbxasset://textures/ui/TouchControlsSheet.png"
+ --[[ The Module ]]--
+ local BaseCharacterController = _BaseCharacterController()
+ local TouchThumbstick = setmetatable({}, BaseCharacterController)
+ TouchThumbstick.__index = TouchThumbstick
+ function TouchThumbstick.new()
+ local self = setmetatable(BaseCharacterController.new(), TouchThumbstick)
+
+ self.isFollowStick = false
+
+ self.thumbstickFrame = nil
+ self.moveTouchObject = nil
+ self.onTouchMovedConn = nil
+ self.onTouchEndedConn = nil
+ self.screenPos = nil
+ self.stickImage = nil
+ self.thumbstickSize = nil -- Float
+
+ return self
+ end
+ function TouchThumbstick:Enable(enable, uiParentFrame)
+ if enable == nil then return false end -- If nil, return false (invalid argument)
+ enable = enable and true or false -- Force anything non-nil to boolean before comparison
+ if self.enabled == enable then return true end -- If no state change, return true indicating already in requested state
+
+ self.moveVector = ZERO_VECTOR3
+ self.isJumping = false
+
+ if enable then
+ -- Enable
+ if not self.thumbstickFrame then
+ self:Create(uiParentFrame)
+ end
+ self.thumbstickFrame.Visible = true
+ else
+ -- Disable
+ self.thumbstickFrame.Visible = false
+ self:OnInputEnded()
+ end
+ self.enabled = enable
+ end
+ function TouchThumbstick:OnInputEnded()
+ self.thumbstickFrame.Position = self.screenPos
+ self.stickImage.Position = UDim2.new(0, self.thumbstickFrame.Size.X.Offset/2 - self.thumbstickSize/4, 0, self.thumbstickFrame.Size.Y.Offset/2 - self.thumbstickSize/4)
+
+ self.moveVector = ZERO_VECTOR3
+ self.isJumping = false
+ self.thumbstickFrame.Position = self.screenPos
+ self.moveTouchObject = nil
+ end
+ function TouchThumbstick:Create(parentFrame)
+
+ if self.thumbstickFrame then
+ self.thumbstickFrame:Destroy()
+ self.thumbstickFrame = nil
+ if self.onTouchMovedConn then
+ self.onTouchMovedConn:Disconnect()
+ self.onTouchMovedConn = nil
+ end
+ if self.onTouchEndedConn then
+ self.onTouchEndedConn:Disconnect()
+ self.onTouchEndedConn = nil
+ end
+ end
+
+ local minAxis = math.min(parentFrame.AbsoluteSize.x, parentFrame.AbsoluteSize.y)
+ local isSmallScreen = minAxis <= 500
+ self.thumbstickSize = isSmallScreen and 70 or 120
+ self.screenPos = isSmallScreen and UDim2.new(0, (self.thumbstickSize/2) - 10, 1, -self.thumbstickSize - 20) or
+ UDim2.new(0, self.thumbstickSize/2, 1, -self.thumbstickSize * 1.75)
+
+ self.thumbstickFrame = Instance.new("Frame")
+ self.thumbstickFrame.Name = "ThumbstickFrame"
+ self.thumbstickFrame.Active = true
+ self.thumbstickFrame.Visible = false
+ self.thumbstickFrame.Size = UDim2.new(0, self.thumbstickSize, 0, self.thumbstickSize)
+ self.thumbstickFrame.Position = self.screenPos
+ self.thumbstickFrame.BackgroundTransparency = 1
+
+ local outerImage = Instance.new("ImageLabel")
+ outerImage.Name = "OuterImage"
+ outerImage.Image = TOUCH_CONTROL_SHEET
+ outerImage.ImageRectOffset = Vector2.new()
+ outerImage.ImageRectSize = Vector2.new(220, 220)
+ outerImage.BackgroundTransparency = 1
+ outerImage.Size = UDim2.new(0, self.thumbstickSize, 0, self.thumbstickSize)
+ outerImage.Position = UDim2.new(0, 0, 0, 0)
+ outerImage.Parent = self.thumbstickFrame
+
+ self.stickImage = Instance.new("ImageLabel")
+ self.stickImage.Name = "StickImage"
+ self.stickImage.Image = TOUCH_CONTROL_SHEET
+ self.stickImage.ImageRectOffset = Vector2.new(220, 0)
+ self.stickImage.ImageRectSize = Vector2.new(111, 111)
+ self.stickImage.BackgroundTransparency = 1
+ self.stickImage.Size = UDim2.new(0, self.thumbstickSize/2, 0, self.thumbstickSize/2)
+ self.stickImage.Position = UDim2.new(0, self.thumbstickSize/2 - self.thumbstickSize/4, 0, self.thumbstickSize/2 - self.thumbstickSize/4)
+ self.stickImage.ZIndex = 2
+ self.stickImage.Parent = self.thumbstickFrame
+
+ local centerPosition = nil
+ local deadZone = 0.05
+
+ local function DoMove(direction)
+
+ local currentMoveVector = direction / (self.thumbstickSize/2)
+
+ -- Scaled Radial Dead Zone
+ local inputAxisMagnitude = currentMoveVector.magnitude
+ if inputAxisMagnitude < deadZone then
+ currentMoveVector = Vector3.new()
+ else
+ currentMoveVector = currentMoveVector.unit * ((inputAxisMagnitude - deadZone) / (1 - deadZone))
+ -- NOTE: Making currentMoveVector a unit vector will cause the player to instantly go max speed
+ -- must check for zero length vector is using unit
+ currentMoveVector = Vector3.new(currentMoveVector.x, 0, currentMoveVector.y)
+ end
+
+ self.moveVector = currentMoveVector
+ end
+
+ local function MoveStick(pos)
+ local relativePosition = Vector2.new(pos.x - centerPosition.x, pos.y - centerPosition.y)
+ local length = relativePosition.magnitude
+ local maxLength = self.thumbstickFrame.AbsoluteSize.x/2
+ if self.isFollowStick and length > maxLength then
+ local offset = relativePosition.unit * maxLength
+ self.thumbstickFrame.Position = UDim2.new(
+ 0, pos.x - self.thumbstickFrame.AbsoluteSize.x/2 - offset.x,
+ 0, pos.y - self.thumbstickFrame.AbsoluteSize.y/2 - offset.y)
+ else
+ length = math.min(length, maxLength)
+ relativePosition = relativePosition.unit * length
+ end
+ self.stickImage.Position = UDim2.new(0, relativePosition.x + self.stickImage.AbsoluteSize.x/2, 0, relativePosition.y + self.stickImage.AbsoluteSize.y/2)
+ end
+
+ -- input connections
+ self.thumbstickFrame.InputBegan:Connect(function(inputObject)
+ --A touch that starts elsewhere on the screen will be sent to a frame's InputBegan event
+ --if it moves over the frame. So we check that this is actually a new touch (inputObject.UserInputState ~= Enum.UserInputState.Begin)
+ if self.moveTouchObject or inputObject.UserInputType ~= Enum.UserInputType.Touch
+ or inputObject.UserInputState ~= Enum.UserInputState.Begin then
+ return
+ end
+
+ self.moveTouchObject = inputObject
+ self.thumbstickFrame.Position = UDim2.new(0, inputObject.Position.x - self.thumbstickFrame.Size.X.Offset/2, 0, inputObject.Position.y - self.thumbstickFrame.Size.Y.Offset/2)
+ centerPosition = Vector2.new(self.thumbstickFrame.AbsolutePosition.x + self.thumbstickFrame.AbsoluteSize.x/2,
+ self.thumbstickFrame.AbsolutePosition.y + self.thumbstickFrame.AbsoluteSize.y/2)
+ local direction = Vector2.new(inputObject.Position.x - centerPosition.x, inputObject.Position.y - centerPosition.y)
+ end)
+
+ self.onTouchMovedConn = UserInputService.TouchMoved:Connect(function(inputObject, isProcessed)
+ if inputObject == self.moveTouchObject then
+ centerPosition = Vector2.new(self.thumbstickFrame.AbsolutePosition.x + self.thumbstickFrame.AbsoluteSize.x/2,
+ self.thumbstickFrame.AbsolutePosition.y + self.thumbstickFrame.AbsoluteSize.y/2)
+ local direction = Vector2.new(inputObject.Position.x - centerPosition.x, inputObject.Position.y - centerPosition.y)
+ DoMove(direction)
+ MoveStick(inputObject.Position)
+ end
+ end)
+
+ self.onTouchEndedConn = UserInputService.TouchEnded:Connect(function(inputObject, isProcessed)
+ if inputObject == self.moveTouchObject then
+ self:OnInputEnded()
+ end
+ end)
+
+ GuiService.MenuOpened:Connect(function()
+ if self.moveTouchObject then
+ self:OnInputEnded()
+ end
+ end)
+
+ self.thumbstickFrame.Parent = parentFrame
+ end
+ return TouchThumbstick
+end
+
+function _DynamicThumbstick()
+ local ZERO_VECTOR3 = Vector3.new(0,0,0)
+ local TOUCH_CONTROLS_SHEET = "rbxasset://textures/ui/Input/TouchControlsSheetV2.png"
+
+ local DYNAMIC_THUMBSTICK_ACTION_NAME = "DynamicThumbstickAction"
+ local DYNAMIC_THUMBSTICK_ACTION_PRIORITY = Enum.ContextActionPriority.High.Value
+
+ local MIDDLE_TRANSPARENCIES = {
+ 1 - 0.89,
+ 1 - 0.70,
+ 1 - 0.60,
+ 1 - 0.50,
+ 1 - 0.40,
+ 1 - 0.30,
+ 1 - 0.25
+ }
+ local NUM_MIDDLE_IMAGES = #MIDDLE_TRANSPARENCIES
+
+ local FADE_IN_OUT_BACKGROUND = true
+ local FADE_IN_OUT_MAX_ALPHA = 0.35
+
+ local FADE_IN_OUT_HALF_DURATION_DEFAULT = 0.3
+ local FADE_IN_OUT_BALANCE_DEFAULT = 0.5
+ local ThumbstickFadeTweenInfo = TweenInfo.new(0.15, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut)
+
+ local Players = game:GetService("Players")
+ local GuiService = game:GetService("GuiService")
+ local UserInputService = game:GetService("UserInputService")
+ local ContextActionService = game:GetService("ContextActionService")
+ local RunService = game:GetService("RunService")
+ local TweenService = game:GetService("TweenService")
+
+ local LocalPlayer = Players.LocalPlayer
+ if not LocalPlayer then
+ Players:GetPropertyChangedSignal("LocalPlayer"):Wait()
+ LocalPlayer = Players.LocalPlayer
+ end
+
+ --[[ The Module ]]--
+ local BaseCharacterController = _BaseCharacterController()
+ local DynamicThumbstick = setmetatable({}, BaseCharacterController)
+ DynamicThumbstick.__index = DynamicThumbstick
+
+ function DynamicThumbstick.new()
+ local self = setmetatable(BaseCharacterController.new(), DynamicThumbstick)
+
+ self.moveTouchObject = nil
+ self.moveTouchLockedIn = false
+ self.moveTouchFirstChanged = false
+ self.moveTouchStartPosition = nil
+
+ self.startImage = nil
+ self.endImage = nil
+ self.middleImages = {}
+
+ self.startImageFadeTween = nil
+ self.endImageFadeTween = nil
+ self.middleImageFadeTweens = {}
+
+ self.isFirstTouch = true
+
+ self.thumbstickFrame = nil
+
+ self.onRenderSteppedConn = nil
+
+ self.fadeInAndOutBalance = FADE_IN_OUT_BALANCE_DEFAULT
+ self.fadeInAndOutHalfDuration = FADE_IN_OUT_HALF_DURATION_DEFAULT
+ self.hasFadedBackgroundInPortrait = false
+ self.hasFadedBackgroundInLandscape = false
+
+ self.tweenInAlphaStart = nil
+ self.tweenOutAlphaStart = nil
+
+ return self
+ end
+
+ -- Note: Overrides base class GetIsJumping with get-and-clear behavior to do a single jump
+ -- rather than sustained jumping. This is only to preserve the current behavior through the refactor.
+ function DynamicThumbstick:GetIsJumping()
+ local wasJumping = self.isJumping
+ self.isJumping = false
+ return wasJumping
+ end
+
+ function DynamicThumbstick:Enable(enable, uiParentFrame)
+ if enable == nil then return false end -- If nil, return false (invalid argument)
+ enable = enable and true or false -- Force anything non-nil to boolean before comparison
+ if self.enabled == enable then return true end -- If no state change, return true indicating already in requested state
+
+ if enable then
+ -- Enable
+ if not self.thumbstickFrame then
+ self:Create(uiParentFrame)
+ end
+
+ self:BindContextActions()
+ else
+ ContextActionService:UnbindAction(DYNAMIC_THUMBSTICK_ACTION_NAME)
+ -- Disable
+ self:OnInputEnded() -- Cleanup
+ end
+
+ self.enabled = enable
+ self.thumbstickFrame.Visible = enable
+ end
+
+ -- Was called OnMoveTouchEnded in previous version
+ function DynamicThumbstick:OnInputEnded()
+ self.moveTouchObject = nil
+ self.moveVector = ZERO_VECTOR3
+ self:FadeThumbstick(false)
+ end
+
+ function DynamicThumbstick:FadeThumbstick(visible)
+ if not visible and self.moveTouchObject then
+ return
+ end
+ if self.isFirstTouch then return end
+
+ if self.startImageFadeTween then
+ self.startImageFadeTween:Cancel()
+ end
+ if self.endImageFadeTween then
+ self.endImageFadeTween:Cancel()
+ end
+ for i = 1, #self.middleImages do
+ if self.middleImageFadeTweens[i] then
+ self.middleImageFadeTweens[i]:Cancel()
+ end
+ end
+
+ if visible then
+ self.startImageFadeTween = TweenService:Create(self.startImage, ThumbstickFadeTweenInfo, { ImageTransparency = 0 })
+ self.startImageFadeTween:Play()
+
+ self.endImageFadeTween = TweenService:Create(self.endImage, ThumbstickFadeTweenInfo, { ImageTransparency = 0.2 })
+ self.endImageFadeTween:Play()
+
+ for i = 1, #self.middleImages do
+ self.middleImageFadeTweens[i] = TweenService:Create(self.middleImages[i], ThumbstickFadeTweenInfo, { ImageTransparency = MIDDLE_TRANSPARENCIES[i] })
+ self.middleImageFadeTweens[i]:Play()
+ end
+ else
+ self.startImageFadeTween = TweenService:Create(self.startImage, ThumbstickFadeTweenInfo, { ImageTransparency = 1 })
+ self.startImageFadeTween:Play()
+
+ self.endImageFadeTween = TweenService:Create(self.endImage, ThumbstickFadeTweenInfo, { ImageTransparency = 1 })
+ self.endImageFadeTween:Play()
+
+ for i = 1, #self.middleImages do
+ self.middleImageFadeTweens[i] = TweenService:Create(self.middleImages[i], ThumbstickFadeTweenInfo, { ImageTransparency = 1 })
+ self.middleImageFadeTweens[i]:Play()
+ end
+ end
+ end
+
+ function DynamicThumbstick:FadeThumbstickFrame(fadeDuration, fadeRatio)
+ self.fadeInAndOutHalfDuration = fadeDuration * 0.5
+ self.fadeInAndOutBalance = fadeRatio
+ self.tweenInAlphaStart = tick()
+ end
+
+ function DynamicThumbstick:InputInFrame(inputObject)
+ local frameCornerTopLeft = self.thumbstickFrame.AbsolutePosition
+ local frameCornerBottomRight = frameCornerTopLeft + self.thumbstickFrame.AbsoluteSize
+ local inputPosition = inputObject.Position
+ if inputPosition.X >= frameCornerTopLeft.X and inputPosition.Y >= frameCornerTopLeft.Y then
+ if inputPosition.X <= frameCornerBottomRight.X and inputPosition.Y <= frameCornerBottomRight.Y then
+ return true
+ end
+ end
+ return false
+ end
+
+ function DynamicThumbstick:DoFadeInBackground()
+ local playerGui = LocalPlayer:FindFirstChildOfClass("PlayerGui")
+ local hasFadedBackgroundInOrientation = false
+
+ -- only fade in/out the background once per orientation
+ if playerGui then
+ if playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.LandscapeLeft or
+ playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.LandscapeRight then
+ hasFadedBackgroundInOrientation = self.hasFadedBackgroundInLandscape
+ self.hasFadedBackgroundInLandscape = true
+ elseif playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.Portrait then
+ hasFadedBackgroundInOrientation = self.hasFadedBackgroundInPortrait
+ self.hasFadedBackgroundInPortrait = true
+ end
+ end
+
+ if not hasFadedBackgroundInOrientation then
+ self.fadeInAndOutHalfDuration = FADE_IN_OUT_HALF_DURATION_DEFAULT
+ self.fadeInAndOutBalance = FADE_IN_OUT_BALANCE_DEFAULT
+ self.tweenInAlphaStart = tick()
+ end
+ end
+
+ function DynamicThumbstick:DoMove(direction)
+ local currentMoveVector = direction
+
+ -- Scaled Radial Dead Zone
+ local inputAxisMagnitude = currentMoveVector.magnitude
+ if inputAxisMagnitude < self.radiusOfDeadZone then
+ currentMoveVector = ZERO_VECTOR3
+ else
+ currentMoveVector = currentMoveVector.unit*(
+ 1 - math.max(0, (self.radiusOfMaxSpeed - currentMoveVector.magnitude)/self.radiusOfMaxSpeed)
+ )
+ currentMoveVector = Vector3.new(currentMoveVector.x, 0, currentMoveVector.y)
+ end
+
+ self.moveVector = currentMoveVector
+ end
+
+
+ function DynamicThumbstick:LayoutMiddleImages(startPos, endPos)
+ local startDist = (self.thumbstickSize / 2) + self.middleSize
+ local vector = endPos - startPos
+ local distAvailable = vector.magnitude - (self.thumbstickRingSize / 2) - self.middleSize
+ local direction = vector.unit
+
+ local distNeeded = self.middleSpacing * NUM_MIDDLE_IMAGES
+ local spacing = self.middleSpacing
+
+ if distNeeded < distAvailable then
+ spacing = distAvailable / NUM_MIDDLE_IMAGES
+ end
+
+ for i = 1, NUM_MIDDLE_IMAGES do
+ local image = self.middleImages[i]
+ local distWithout = startDist + (spacing * (i - 2))
+ local currentDist = startDist + (spacing * (i - 1))
+
+ if distWithout < distAvailable then
+ local pos = endPos - direction * currentDist
+ local exposedFraction = math.clamp(1 - ((currentDist - distAvailable) / spacing), 0, 1)
+
+ image.Visible = true
+ image.Position = UDim2.new(0, pos.X, 0, pos.Y)
+ image.Size = UDim2.new(0, self.middleSize * exposedFraction, 0, self.middleSize * exposedFraction)
+ else
+ image.Visible = false
+ end
+ end
+ end
+
+ function DynamicThumbstick:MoveStick(pos)
+ local vector2StartPosition = Vector2.new(self.moveTouchStartPosition.X, self.moveTouchStartPosition.Y)
+ local startPos = vector2StartPosition - self.thumbstickFrame.AbsolutePosition
+ local endPos = Vector2.new(pos.X, pos.Y) - self.thumbstickFrame.AbsolutePosition
+ self.endImage.Position = UDim2.new(0, endPos.X, 0, endPos.Y)
+ self:LayoutMiddleImages(startPos, endPos)
+ end
+
+ function DynamicThumbstick:BindContextActions()
+ local function inputBegan(inputObject)
+ if self.moveTouchObject then
+ return Enum.ContextActionResult.Pass
+ end
+
+ if not self:InputInFrame(inputObject) then
+ return Enum.ContextActionResult.Pass
+ end
+
+ if self.isFirstTouch then
+ self.isFirstTouch = false
+ local tweenInfo = TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.Out,0,false,0)
+ TweenService:Create(self.startImage, tweenInfo, {Size = UDim2.new(0, 0, 0, 0)}):Play()
+ TweenService:Create(
+ self.endImage,
+ tweenInfo,
+ {Size = UDim2.new(0, self.thumbstickSize, 0, self.thumbstickSize), ImageColor3 = Color3.new(0,0,0)}
+ ):Play()
+ end
+
+ self.moveTouchLockedIn = false
+ self.moveTouchObject = inputObject
+ self.moveTouchStartPosition = inputObject.Position
+ self.moveTouchFirstChanged = true
+
+ if FADE_IN_OUT_BACKGROUND then
+ self:DoFadeInBackground()
+ end
+
+ return Enum.ContextActionResult.Pass
+ end
+
+ local function inputChanged(inputObject)
+ if inputObject == self.moveTouchObject then
+ if self.moveTouchFirstChanged then
+ self.moveTouchFirstChanged = false
+
+ local startPosVec2 = Vector2.new(
+ inputObject.Position.X - self.thumbstickFrame.AbsolutePosition.X,
+ inputObject.Position.Y - self.thumbstickFrame.AbsolutePosition.Y
+ )
+ self.startImage.Visible = true
+ self.startImage.Position = UDim2.new(0, startPosVec2.X, 0, startPosVec2.Y)
+ self.endImage.Visible = true
+ self.endImage.Position = self.startImage.Position
+
+ self:FadeThumbstick(true)
+ self:MoveStick(inputObject.Position)
+ end
+
+ self.moveTouchLockedIn = true
+
+ local direction = Vector2.new(
+ inputObject.Position.x - self.moveTouchStartPosition.x,
+ inputObject.Position.y - self.moveTouchStartPosition.y
+ )
+ if math.abs(direction.x) > 0 or math.abs(direction.y) > 0 then
+ self:DoMove(direction)
+ self:MoveStick(inputObject.Position)
+ end
+ return Enum.ContextActionResult.Sink
+ end
+ return Enum.ContextActionResult.Pass
+ end
+
+ local function inputEnded(inputObject)
+ if inputObject == self.moveTouchObject then
+ self:OnInputEnded()
+ if self.moveTouchLockedIn then
+ return Enum.ContextActionResult.Sink
+ end
+ end
+ return Enum.ContextActionResult.Pass
+ end
+
+ local function handleInput(actionName, inputState, inputObject)
+ if inputState == Enum.UserInputState.Begin then
+ return inputBegan(inputObject)
+ elseif inputState == Enum.UserInputState.Change then
+ return inputChanged(inputObject)
+ elseif inputState == Enum.UserInputState.End then
+ return inputEnded(inputObject)
+ elseif inputState == Enum.UserInputState.Cancel then
+ self:OnInputEnded()
+ end
+ end
+
+ ContextActionService:BindActionAtPriority(
+ DYNAMIC_THUMBSTICK_ACTION_NAME,
+ handleInput,
+ false,
+ DYNAMIC_THUMBSTICK_ACTION_PRIORITY,
+ Enum.UserInputType.Touch)
+ end
+
+ function DynamicThumbstick:Create(parentFrame)
+ if self.thumbstickFrame then
+ self.thumbstickFrame:Destroy()
+ self.thumbstickFrame = nil
+ if self.onRenderSteppedConn then
+ self.onRenderSteppedConn:Disconnect()
+ self.onRenderSteppedConn = nil
+ end
+ end
+
+ self.thumbstickSize = 45
+ self.thumbstickRingSize = 20
+ self.middleSize = 10
+ self.middleSpacing = self.middleSize + 4
+ self.radiusOfDeadZone = 2
+ self.radiusOfMaxSpeed = 20
+
+ local screenSize = parentFrame.AbsoluteSize
+ local isBigScreen = math.min(screenSize.x, screenSize.y) > 500
+ if isBigScreen then
+ self.thumbstickSize = self.thumbstickSize * 2
+ self.thumbstickRingSize = self.thumbstickRingSize * 2
+ self.middleSize = self.middleSize * 2
+ self.middleSpacing = self.middleSpacing * 2
+ self.radiusOfDeadZone = self.radiusOfDeadZone * 2
+ self.radiusOfMaxSpeed = self.radiusOfMaxSpeed * 2
+ end
+
+ local function layoutThumbstickFrame(portraitMode)
+ if portraitMode then
+ self.thumbstickFrame.Size = UDim2.new(1, 0, 0.4, 0)
+ self.thumbstickFrame.Position = UDim2.new(0, 0, 0.6, 0)
+ else
+ self.thumbstickFrame.Size = UDim2.new(0.4, 0, 2/3, 0)
+ self.thumbstickFrame.Position = UDim2.new(0, 0, 1/3, 0)
+ end
+ end
+
+ self.thumbstickFrame = Instance.new("Frame")
+ self.thumbstickFrame.BorderSizePixel = 0
+ self.thumbstickFrame.Name = "DynamicThumbstickFrame"
+ self.thumbstickFrame.Visible = false
+ self.thumbstickFrame.BackgroundTransparency = 1.0
+ self.thumbstickFrame.BackgroundColor3 = Color3.fromRGB(0, 0, 0)
+ self.thumbstickFrame.Active = false
+ layoutThumbstickFrame(false)
+
+ self.startImage = Instance.new("ImageLabel")
+ self.startImage.Name = "ThumbstickStart"
+ self.startImage.Visible = true
+ self.startImage.BackgroundTransparency = 1
+ self.startImage.Image = TOUCH_CONTROLS_SHEET
+ self.startImage.ImageRectOffset = Vector2.new(1,1)
+ self.startImage.ImageRectSize = Vector2.new(144, 144)
+ self.startImage.ImageColor3 = Color3.new(0, 0, 0)
+ self.startImage.AnchorPoint = Vector2.new(0.5, 0.5)
+ self.startImage.Position = UDim2.new(0, self.thumbstickRingSize * 3.3, 1, -self.thumbstickRingSize * 2.8)
+ self.startImage.Size = UDim2.new(0, self.thumbstickRingSize * 3.7, 0, self.thumbstickRingSize * 3.7)
+ self.startImage.ZIndex = 10
+ self.startImage.Parent = self.thumbstickFrame
+
+ self.endImage = Instance.new("ImageLabel")
+ self.endImage.Name = "ThumbstickEnd"
+ self.endImage.Visible = true
+ self.endImage.BackgroundTransparency = 1
+ self.endImage.Image = TOUCH_CONTROLS_SHEET
+ self.endImage.ImageRectOffset = Vector2.new(1,1)
+ self.endImage.ImageRectSize = Vector2.new(144, 144)
+ self.endImage.AnchorPoint = Vector2.new(0.5, 0.5)
+ self.endImage.Position = self.startImage.Position
+ self.endImage.Size = UDim2.new(0, self.thumbstickSize * 0.8, 0, self.thumbstickSize * 0.8)
+ self.endImage.ZIndex = 10
+ self.endImage.Parent = self.thumbstickFrame
+
+ for i = 1, NUM_MIDDLE_IMAGES do
+ self.middleImages[i] = Instance.new("ImageLabel")
+ self.middleImages[i].Name = "ThumbstickMiddle"
+ self.middleImages[i].Visible = false
+ self.middleImages[i].BackgroundTransparency = 1
+ self.middleImages[i].Image = TOUCH_CONTROLS_SHEET
+ self.middleImages[i].ImageRectOffset = Vector2.new(1,1)
+ self.middleImages[i].ImageRectSize = Vector2.new(144, 144)
+ self.middleImages[i].ImageTransparency = MIDDLE_TRANSPARENCIES[i]
+ self.middleImages[i].AnchorPoint = Vector2.new(0.5, 0.5)
+ self.middleImages[i].ZIndex = 9
+ self.middleImages[i].Parent = self.thumbstickFrame
+ end
+
+ local CameraChangedConn = nil
+ local function onCurrentCameraChanged()
+ if CameraChangedConn then
+ CameraChangedConn:Disconnect()
+ CameraChangedConn = nil
+ end
+ local newCamera = workspace.CurrentCamera
+ if newCamera then
+ local function onViewportSizeChanged()
+ local size = newCamera.ViewportSize
+ local portraitMode = size.X < size.Y
+ layoutThumbstickFrame(portraitMode)
+ end
+ CameraChangedConn = newCamera:GetPropertyChangedSignal("ViewportSize"):Connect(onViewportSizeChanged)
+ onViewportSizeChanged()
+ end
+ end
+ workspace:GetPropertyChangedSignal("CurrentCamera"):Connect(onCurrentCameraChanged)
+ if workspace.CurrentCamera then
+ onCurrentCameraChanged()
+ end
+
+ self.moveTouchStartPosition = nil
+
+ self.startImageFadeTween = nil
+ self.endImageFadeTween = nil
+ self.middleImageFadeTweens = {}
+
+ self.onRenderSteppedConn = RunService.RenderStepped:Connect(function()
+ if self.tweenInAlphaStart ~= nil then
+ local delta = tick() - self.tweenInAlphaStart
+ local fadeInTime = (self.fadeInAndOutHalfDuration * 2 * self.fadeInAndOutBalance)
+ self.thumbstickFrame.BackgroundTransparency = 1 - FADE_IN_OUT_MAX_ALPHA*math.min(delta/fadeInTime, 1)
+ if delta > fadeInTime then
+ self.tweenOutAlphaStart = tick()
+ self.tweenInAlphaStart = nil
+ end
+ elseif self.tweenOutAlphaStart ~= nil then
+ local delta = tick() - self.tweenOutAlphaStart
+ local fadeOutTime = (self.fadeInAndOutHalfDuration * 2) - (self.fadeInAndOutHalfDuration * 2 * self.fadeInAndOutBalance)
+ self.thumbstickFrame.BackgroundTransparency = 1 - FADE_IN_OUT_MAX_ALPHA + FADE_IN_OUT_MAX_ALPHA*math.min(delta/fadeOutTime, 1)
+ if delta > fadeOutTime then
+ self.tweenOutAlphaStart = nil
+ end
+ end
+ end)
+
+ self.onTouchEndedConn = UserInputService.TouchEnded:connect(function(inputObject)
+ if inputObject == self.moveTouchObject then
+ self:OnInputEnded()
+ end
+ end)
+
+ GuiService.MenuOpened:connect(function()
+ if self.moveTouchObject then
+ self:OnInputEnded()
+ end
+ end)
+
+ local playerGui = LocalPlayer:FindFirstChildOfClass("PlayerGui")
+ while not playerGui do
+ LocalPlayer.ChildAdded:wait()
+ playerGui = LocalPlayer:FindFirstChildOfClass("PlayerGui")
+ end
+
+ local playerGuiChangedConn = nil
+ local originalScreenOrientationWasLandscape = playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.LandscapeLeft or
+ playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.LandscapeRight
+
+ local function longShowBackground()
+ self.fadeInAndOutHalfDuration = 2.5
+ self.fadeInAndOutBalance = 0.05
+ self.tweenInAlphaStart = tick()
+ end
+
+ playerGuiChangedConn = playerGui:GetPropertyChangedSignal("CurrentScreenOrientation"):Connect(function()
+ if (originalScreenOrientationWasLandscape and playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.Portrait) or
+ (not originalScreenOrientationWasLandscape and playerGui.CurrentScreenOrientation ~= Enum.ScreenOrientation.Portrait) then
+
+ playerGuiChangedConn:disconnect()
+ longShowBackground()
+
+ if originalScreenOrientationWasLandscape then
+ self.hasFadedBackgroundInPortrait = true
+ else
+ self.hasFadedBackgroundInLandscape = true
+ end
+ end
+ end)
+
+ self.thumbstickFrame.Parent = parentFrame
+
+ if game:IsLoaded() then
+ longShowBackground()
+ else
+ coroutine.wrap(function()
+ game.Loaded:Wait()
+ longShowBackground()
+ end)()
+ end
+ end
+
+ return DynamicThumbstick
+end
+
+function _Gamepad()
+ local UserInputService = game:GetService("UserInputService")
+ local ContextActionService = game:GetService("ContextActionService")
+
+ --[[ Constants ]]--
+ local ZERO_VECTOR3 = Vector3.new(0,0,0)
+ local NONE = Enum.UserInputType.None
+ local thumbstickDeadzone = 0.2
+
+ --[[ The Module ]]--
+ local BaseCharacterController = _BaseCharacterController()
+ local Gamepad = setmetatable({}, BaseCharacterController)
+ Gamepad.__index = Gamepad
+
+ function Gamepad.new(CONTROL_ACTION_PRIORITY)
+ local self = setmetatable(BaseCharacterController.new(), Gamepad)
+
+ self.CONTROL_ACTION_PRIORITY = CONTROL_ACTION_PRIORITY
+
+ self.forwardValue = 0
+ self.backwardValue = 0
+ self.leftValue = 0
+ self.rightValue = 0
+
+ self.activeGamepad = NONE -- Enum.UserInputType.Gamepad1, 2, 3...
+ self.gamepadConnectedConn = nil
+ self.gamepadDisconnectedConn = nil
+ return self
+ end
+
+ function Gamepad:Enable(enable)
+ if not UserInputService.GamepadEnabled then
+ return false
+ end
+
+ if enable == self.enabled then
+ -- Module is already in the state being requested. True is returned here since the module will be in the state
+ -- expected by the code that follows the Enable() call. This makes more sense than returning false to indicate
+ -- no action was necessary. False indicates failure to be in requested/expected state.
+ return true
+ end
+
+ self.forwardValue = 0
+ self.backwardValue = 0
+ self.leftValue = 0
+ self.rightValue = 0
+ self.moveVector = ZERO_VECTOR3
+ self.isJumping = false
+
+ if enable then
+ self.activeGamepad = self:GetHighestPriorityGamepad()
+ if self.activeGamepad ~= NONE then
+ self:BindContextActions()
+ self:ConnectGamepadConnectionListeners()
+ else
+ -- No connected gamepads, failure to enable
+ return false
+ end
+ else
+ self:UnbindContextActions()
+ self:DisconnectGamepadConnectionListeners()
+ self.activeGamepad = NONE
+ end
+
+ self.enabled = enable
+ return true
+ end
+
+ -- This function selects the lowest number gamepad from the currently-connected gamepad
+ -- and sets it as the active gamepad
+ function Gamepad:GetHighestPriorityGamepad()
+ local connectedGamepads = UserInputService:GetConnectedGamepads()
+ local bestGamepad = NONE -- Note that this value is higher than all valid gamepad values
+ for _, gamepad in pairs(connectedGamepads) do
+ if gamepad.Value < bestGamepad.Value then
+ bestGamepad = gamepad
+ end
+ end
+ return bestGamepad
+ end
+
+ function Gamepad:BindContextActions()
+
+ if self.activeGamepad == NONE then
+ -- There must be an active gamepad to set up bindings
+ return false
+ end
+
+ local handleJumpAction = function(actionName, inputState, inputObject)
+ self.isJumping = (inputState == Enum.UserInputState.Begin)
+ return Enum.ContextActionResult.Sink
+ end
+
+ local handleThumbstickInput = function(actionName, inputState, inputObject)
+
+ if inputState == Enum.UserInputState.Cancel then
+ self.moveVector = ZERO_VECTOR3
+ return Enum.ContextActionResult.Sink
+ end
+
+ if self.activeGamepad ~= inputObject.UserInputType then
+ return Enum.ContextActionResult.Pass
+ end
+ if inputObject.KeyCode ~= Enum.KeyCode.Thumbstick1 then return end
+
+ if inputObject.Position.magnitude > thumbstickDeadzone then
+ self.moveVector = Vector3.new(inputObject.Position.X, 0, -inputObject.Position.Y)
+ else
+ self.moveVector = ZERO_VECTOR3
+ end
+ return Enum.ContextActionResult.Sink
+ end
+
+ ContextActionService:BindActivate(self.activeGamepad, Enum.KeyCode.ButtonR2)
+ ContextActionService:BindActionAtPriority("jumpAction", handleJumpAction, false,
+ self.CONTROL_ACTION_PRIORITY, Enum.KeyCode.ButtonA)
+ ContextActionService:BindActionAtPriority("moveThumbstick", handleThumbstickInput, false,
+ self.CONTROL_ACTION_PRIORITY, Enum.KeyCode.Thumbstick1)
+
+ return true
+ end
+
+ function Gamepad:UnbindContextActions()
+ if self.activeGamepad ~= NONE then
+ ContextActionService:UnbindActivate(self.activeGamepad, Enum.KeyCode.ButtonR2)
+ end
+ ContextActionService:UnbindAction("moveThumbstick")
+ ContextActionService:UnbindAction("jumpAction")
+ end
+
+ function Gamepad:OnNewGamepadConnected()
+ -- A new gamepad has been connected.
+ local bestGamepad = self:GetHighestPriorityGamepad()
+
+ if bestGamepad == self.activeGamepad then
+ -- A new gamepad was connected, but our active gamepad is not changing
+ return
+ end
+
+ if bestGamepad == NONE then
+ -- There should be an active gamepad when GamepadConnected fires, so this should not
+ -- normally be hit. If there is no active gamepad, unbind actions but leave
+ -- the module enabled and continue to listen for a new gamepad connection.
+ warn("Gamepad:OnNewGamepadConnected found no connected gamepads")
+ self:UnbindContextActions()
+ return
+ end
+
+ if self.activeGamepad ~= NONE then
+ -- Switching from one active gamepad to another
+ self:UnbindContextActions()
+ end
+
+ self.activeGamepad = bestGamepad
+ self:BindContextActions()
+ end
+
+ function Gamepad:OnCurrentGamepadDisconnected()
+ if self.activeGamepad ~= NONE then
+ ContextActionService:UnbindActivate(self.activeGamepad, Enum.KeyCode.ButtonR2)
+ end
+
+ local bestGamepad = self:GetHighestPriorityGamepad()
+
+ if self.activeGamepad ~= NONE and bestGamepad == self.activeGamepad then
+ warn("Gamepad:OnCurrentGamepadDisconnected found the supposedly disconnected gamepad in connectedGamepads.")
+ self:UnbindContextActions()
+ self.activeGamepad = NONE
+ return
+ end
+
+ if bestGamepad == NONE then
+ -- No active gamepad, unbinding actions but leaving gamepad connection listener active
+ self:UnbindContextActions()
+ self.activeGamepad = NONE
+ else
+ -- Set new gamepad as active and bind to tool activation
+ self.activeGamepad = bestGamepad
+ ContextActionService:BindActivate(self.activeGamepad, Enum.KeyCode.ButtonR2)
+ end
+ end
+
+ function Gamepad:ConnectGamepadConnectionListeners()
+ self.gamepadConnectedConn = UserInputService.GamepadConnected:Connect(function(gamepadEnum)
+ self:OnNewGamepadConnected()
+ end)
+
+ self.gamepadDisconnectedConn = UserInputService.GamepadDisconnected:Connect(function(gamepadEnum)
+ if self.activeGamepad == gamepadEnum then
+ self:OnCurrentGamepadDisconnected()
+ end
+ end)
+
+ end
+
+ function Gamepad:DisconnectGamepadConnectionListeners()
+ if self.gamepadConnectedConn then
+ self.gamepadConnectedConn:Disconnect()
+ self.gamepadConnectedConn = nil
+ end
+
+ if self.gamepadDisconnectedConn then
+ self.gamepadDisconnectedConn:Disconnect()
+ self.gamepadDisconnectedConn = nil
+ end
+ end
+
+ return Gamepad
+end
+
+function _Keyboard()
+
+ --[[ Roblox Services ]]--
+ local UserInputService = game:GetService("UserInputService")
+ local ContextActionService = game:GetService("ContextActionService")
+
+ --[[ Constants ]]--
+ local ZERO_VECTOR3 = Vector3.new(0,0,0)
+
+ --[[ The Module ]]--
+ local BaseCharacterController = _BaseCharacterController()
+ local Keyboard = setmetatable({}, BaseCharacterController)
+ Keyboard.__index = Keyboard
+
+ function Keyboard.new(CONTROL_ACTION_PRIORITY)
+ local self = setmetatable(BaseCharacterController.new(), Keyboard)
+
+ self.CONTROL_ACTION_PRIORITY = CONTROL_ACTION_PRIORITY
+
+ self.textFocusReleasedConn = nil
+ self.textFocusGainedConn = nil
+ self.windowFocusReleasedConn = nil
+
+ self.forwardValue = 0
+ self.backwardValue = 0
+ self.leftValue = 0
+ self.rightValue = 0
+
+ self.jumpEnabled = true
+
+ return self
+ end
+
+ function Keyboard:Enable(enable)
+ if not UserInputService.KeyboardEnabled then
+ return false
+ end
+
+ if enable == self.enabled then
+ -- Module is already in the state being requested. True is returned here since the module will be in the state
+ -- expected by the code that follows the Enable() call. This makes more sense than returning false to indicate
+ -- no action was necessary. False indicates failure to be in requested/expected state.
+ return true
+ end
+
+ self.forwardValue = 0
+ self.backwardValue = 0
+ self.leftValue = 0
+ self.rightValue = 0
+ self.moveVector = ZERO_VECTOR3
+ self.jumpRequested = false
+ self:UpdateJump()
+
+ if enable then
+ self:BindContextActions()
+ self:ConnectFocusEventListeners()
+ else
+ self:UnbindContextActions()
+ self:DisconnectFocusEventListeners()
+ end
+
+ self.enabled = enable
+ return true
+ end
+
+ function Keyboard:UpdateMovement(inputState)
+ if inputState == Enum.UserInputState.Cancel then
+ self.moveVector = ZERO_VECTOR3
+ else
+ self.moveVector = Vector3.new(self.leftValue + self.rightValue, 0, self.forwardValue + self.backwardValue)
+ end
+ end
+
+ function Keyboard:UpdateJump()
+ self.isJumping = self.jumpRequested
+ end
+
+ function Keyboard:BindContextActions()
+
+ -- Note: In the previous version of this code, the movement values were not zeroed-out on UserInputState. Cancel, now they are,
+ -- which fixes them from getting stuck on.
+ -- We return ContextActionResult.Pass here for legacy reasons.
+ -- Many games rely on gameProcessedEvent being false on UserInputService.InputBegan for these control actions.
+ local handleMoveForward = function(actionName, inputState, inputObject)
+ self.forwardValue = (inputState == Enum.UserInputState.Begin) and -1 or 0
+ self:UpdateMovement(inputState)
+ return Enum.ContextActionResult.Pass
+ end
+
+ local handleMoveBackward = function(actionName, inputState, inputObject)
+ self.backwardValue = (inputState == Enum.UserInputState.Begin) and 1 or 0
+ self:UpdateMovement(inputState)
+ return Enum.ContextActionResult.Pass
+ end
+
+ local handleMoveLeft = function(actionName, inputState, inputObject)
+ self.leftValue = (inputState == Enum.UserInputState.Begin) and -1 or 0
+ self:UpdateMovement(inputState)
+ return Enum.ContextActionResult.Pass
+ end
+
+ local handleMoveRight = function(actionName, inputState, inputObject)
+ self.rightValue = (inputState == Enum.UserInputState.Begin) and 1 or 0
+ self:UpdateMovement(inputState)
+ return Enum.ContextActionResult.Pass
+ end
+
+ local handleJumpAction = function(actionName, inputState, inputObject)
+ self.jumpRequested = self.jumpEnabled and (inputState == Enum.UserInputState.Begin)
+ self:UpdateJump()
+ return Enum.ContextActionResult.Pass
+ end
+
+ -- TODO: Revert to KeyCode bindings so that in the future the abstraction layer from actual keys to
+ -- movement direction is done in Lua
+ ContextActionService:BindActionAtPriority("moveForwardAction", handleMoveForward, false,
+ self.CONTROL_ACTION_PRIORITY, Enum.PlayerActions.CharacterForward)
+ ContextActionService:BindActionAtPriority("moveBackwardAction", handleMoveBackward, false,
+ self.CONTROL_ACTION_PRIORITY, Enum.PlayerActions.CharacterBackward)
+ ContextActionService:BindActionAtPriority("moveLeftAction", handleMoveLeft, false,
+ self.CONTROL_ACTION_PRIORITY, Enum.PlayerActions.CharacterLeft)
+ ContextActionService:BindActionAtPriority("moveRightAction", handleMoveRight, false,
+ self.CONTROL_ACTION_PRIORITY, Enum.PlayerActions.CharacterRight)
+ ContextActionService:BindActionAtPriority("jumpAction", handleJumpAction, false,
+ self.CONTROL_ACTION_PRIORITY, Enum.PlayerActions.CharacterJump)
+ end
+
+ function Keyboard:UnbindContextActions()
+ ContextActionService:UnbindAction("moveForwardAction")
+ ContextActionService:UnbindAction("moveBackwardAction")
+ ContextActionService:UnbindAction("moveLeftAction")
+ ContextActionService:UnbindAction("moveRightAction")
+ ContextActionService:UnbindAction("jumpAction")
+ end
+
+ function Keyboard:ConnectFocusEventListeners()
+ local function onFocusReleased()
+ self.moveVector = ZERO_VECTOR3
+ self.forwardValue = 0
+ self.backwardValue = 0
+ self.leftValue = 0
+ self.rightValue = 0
+ self.jumpRequested = false
+ self:UpdateJump()
+ end
+
+ local function onTextFocusGained(textboxFocused)
+ self.jumpRequested = false
+ self:UpdateJump()
+ end
+
+ self.textFocusReleasedConn = UserInputService.TextBoxFocusReleased:Connect(onFocusReleased)
+ self.textFocusGainedConn = UserInputService.TextBoxFocused:Connect(onTextFocusGained)
+ self.windowFocusReleasedConn = UserInputService.WindowFocused:Connect(onFocusReleased)
+ end
+
+ function Keyboard:DisconnectFocusEventListeners()
+ if self.textFocusReleasedCon then
+ self.textFocusReleasedCon:Disconnect()
+ self.textFocusReleasedCon = nil
+ end
+ if self.textFocusGainedConn then
+ self.textFocusGainedConn:Disconnect()
+ self.textFocusGainedConn = nil
+ end
+ if self.windowFocusReleasedConn then
+ self.windowFocusReleasedConn:Disconnect()
+ self.windowFocusReleasedConn = nil
+ end
+ end
+
+ return Keyboard
+end
+
+function _ControlModule()
+ local ControlModule = {}
+ ControlModule.__index = ControlModule
+
+ --[[ Roblox Services ]]--
+ local Players = game:GetService("Players")
+ local RunService = game:GetService("RunService")
+ local UserInputService = game:GetService("UserInputService")
+ local Workspace = game:GetService("Workspace")
+ local UserGameSettings = UserSettings():GetService("UserGameSettings")
+
+ -- Roblox User Input Control Modules - each returns a new() constructor function used to create controllers as needed
+ local Keyboard = _Keyboard()
+ local Gamepad = _Gamepad()
+ local DynamicThumbstick = _DynamicThumbstick()
+
+ local FFlagUserMakeThumbstickDynamic do
+ local success, value = pcall(function()
+ return UserSettings():IsUserFeatureEnabled("UserMakeThumbstickDynamic")
+ end)
+ FFlagUserMakeThumbstickDynamic = success and value
+ end
+
+ local TouchThumbstick = FFlagUserMakeThumbstickDynamic and DynamicThumbstick or _TouchThumbstick()
+
+ -- These controllers handle only walk/run movement, jumping is handled by the
+ -- TouchJump controller if any of these are active
+ local ClickToMove = _ClickToMoveController()
+ local TouchJump = _TouchJump()
+
+ local VehicleController = _VehicleController()
+
+ local CONTROL_ACTION_PRIORITY = Enum.ContextActionPriority.Default.Value
+
+ -- Mapping from movement mode and lastInputType enum values to control modules to avoid huge if elseif switching
+ local movementEnumToModuleMap = {
+ [Enum.TouchMovementMode.DPad] = DynamicThumbstick,
+ [Enum.DevTouchMovementMode.DPad] = DynamicThumbstick,
+ [Enum.TouchMovementMode.Thumbpad] = DynamicThumbstick,
+ [Enum.DevTouchMovementMode.Thumbpad] = DynamicThumbstick,
+ [Enum.TouchMovementMode.Thumbstick] = TouchThumbstick,
+ [Enum.DevTouchMovementMode.Thumbstick] = TouchThumbstick,
+ [Enum.TouchMovementMode.DynamicThumbstick] = DynamicThumbstick,
+ [Enum.DevTouchMovementMode.DynamicThumbstick] = DynamicThumbstick,
+ [Enum.TouchMovementMode.ClickToMove] = ClickToMove,
+ [Enum.DevTouchMovementMode.ClickToMove] = ClickToMove,
+
+ -- Current default
+ [Enum.TouchMovementMode.Default] = DynamicThumbstick,
+
+ [Enum.ComputerMovementMode.Default] = Keyboard,
+ [Enum.ComputerMovementMode.KeyboardMouse] = Keyboard,
+ [Enum.DevComputerMovementMode.KeyboardMouse] = Keyboard,
+ [Enum.DevComputerMovementMode.Scriptable] = nil,
+ [Enum.ComputerMovementMode.ClickToMove] = ClickToMove,
+ [Enum.DevComputerMovementMode.ClickToMove] = ClickToMove,
+ }
+
+ -- Keyboard controller is really keyboard and mouse controller
+ local computerInputTypeToModuleMap = {
+ [Enum.UserInputType.Keyboard] = Keyboard,
+ [Enum.UserInputType.MouseButton1] = Keyboard,
+ [Enum.UserInputType.MouseButton2] = Keyboard,
+ [Enum.UserInputType.MouseButton3] = Keyboard,
+ [Enum.UserInputType.MouseWheel] = Keyboard,
+ [Enum.UserInputType.MouseMovement] = Keyboard,
+ [Enum.UserInputType.Gamepad1] = Gamepad,
+ [Enum.UserInputType.Gamepad2] = Gamepad,
+ [Enum.UserInputType.Gamepad3] = Gamepad,
+ [Enum.UserInputType.Gamepad4] = Gamepad,
+ }
+
+ local lastInputType
+
+ function ControlModule.new()
+ local self = setmetatable({},ControlModule)
+
+ -- The Modules above are used to construct controller instances as-needed, and this
+ -- table is a map from Module to the instance created from it
+ self.controllers = {}
+
+ self.activeControlModule = nil -- Used to prevent unnecessarily expensive checks on each input event
+ self.activeController = nil
+ self.touchJumpController = nil
+ self.moveFunction = Players.LocalPlayer.Move
+ self.humanoid = nil
+ self.lastInputType = Enum.UserInputType.None
+
+ -- For Roblox self.vehicleController
+ self.humanoidSeatedConn = nil
+ self.vehicleController = nil
+
+ self.touchControlFrame = nil
+
+ self.vehicleController = VehicleController.new(CONTROL_ACTION_PRIORITY)
+
+ Players.LocalPlayer.CharacterAdded:Connect(function(char) self:OnCharacterAdded(char) end)
+ Players.LocalPlayer.CharacterRemoving:Connect(function(char) self:OnCharacterRemoving(char) end)
+ if Players.LocalPlayer.Character then
+ self:OnCharacterAdded(Players.LocalPlayer.Character)
+ end
+
+ RunService:BindToRenderStep("ControlScriptRenderstep", Enum.RenderPriority.Input.Value, function(dt)
+ self:OnRenderStepped(dt)
+ end)
+
+ UserInputService.LastInputTypeChanged:Connect(function(newLastInputType)
+ self:OnLastInputTypeChanged(newLastInputType)
+ end)
+
+
+ UserGameSettings:GetPropertyChangedSignal("TouchMovementMode"):Connect(function()
+ self:OnTouchMovementModeChange()
+ end)
+ Players.LocalPlayer:GetPropertyChangedSignal("DevTouchMovementMode"):Connect(function()
+ self:OnTouchMovementModeChange()
+ end)
+
+ UserGameSettings:GetPropertyChangedSignal("ComputerMovementMode"):Connect(function()
+ self:OnComputerMovementModeChange()
+ end)
+ Players.LocalPlayer:GetPropertyChangedSignal("DevComputerMovementMode"):Connect(function()
+ self:OnComputerMovementModeChange()
+ end)
+
+ --[[ Touch Device UI ]]--
+ self.playerGui = nil
+ self.touchGui = nil
+ self.playerGuiAddedConn = nil
+
+ if UserInputService.TouchEnabled then
+ self.playerGui = Players.LocalPlayer:FindFirstChildOfClass("PlayerGui")
+ if self.playerGui then
+ self:CreateTouchGuiContainer()
+ self:OnLastInputTypeChanged(UserInputService:GetLastInputType())
+ else
+ self.playerGuiAddedConn = Players.LocalPlayer.ChildAdded:Connect(function(child)
+ if child:IsA("PlayerGui") then
+ self.playerGui = child
+ self:CreateTouchGuiContainer()
+ self.playerGuiAddedConn:Disconnect()
+ self.playerGuiAddedConn = nil
+ self:OnLastInputTypeChanged(UserInputService:GetLastInputType())
+ end
+ end)
+ end
+ else
+ self:OnLastInputTypeChanged(UserInputService:GetLastInputType())
+ end
+
+ return self
+ end
+
+ -- Convenience function so that calling code does not have to first get the activeController
+ -- and then call GetMoveVector on it. When there is no active controller, this function returns
+ -- nil so that this case can be distinguished from no current movement (which returns zero vector).
+ function ControlModule:GetMoveVector()
+ if self.activeController then
+ return self.activeController:GetMoveVector()
+ end
+ return Vector3.new(0,0,0)
+ end
+
+ function ControlModule:GetActiveController()
+ return self.activeController
+ end
+
+ function ControlModule:EnableActiveControlModule()
+ if self.activeControlModule == ClickToMove then
+ -- For ClickToMove, when it is the player's choice, we also enable the full keyboard controls.
+ -- When the developer is forcing click to move, the most keyboard controls (WASD) are not available, only jump.
+ self.activeController:Enable(
+ true,
+ Players.LocalPlayer.DevComputerMovementMode == Enum.DevComputerMovementMode.UserChoice,
+ self.touchJumpController
+ )
+ elseif self.touchControlFrame then
+ self.activeController:Enable(true, self.touchControlFrame)
+ else
+ self.activeController:Enable(true)
+ end
+ end
+
+ function ControlModule:Enable(enable)
+ if not self.activeController then
+ return
+ end
+
+ if enable == nil then
+ enable = true
+ end
+ if enable then
+ self:EnableActiveControlModule()
+ else
+ self:Disable()
+ end
+ end
+
+ -- For those who prefer distinct functions
+ function ControlModule:Disable()
+ if self.activeController then
+ self.activeController:Enable(false)
+
+ if self.moveFunction then
+ self.moveFunction(Players.LocalPlayer, Vector3.new(0,0,0), true)
+ end
+ end
+ end
+
+
+ -- Returns module (possibly nil) and success code to differentiate returning nil due to error vs Scriptable
+ function ControlModule:SelectComputerMovementModule()
+ if not (UserInputService.KeyboardEnabled or UserInputService.GamepadEnabled) then
+ return nil, false
+ end
+
+ local computerModule
+ local DevMovementMode = Players.LocalPlayer.DevComputerMovementMode
+
+ if DevMovementMode == Enum.DevComputerMovementMode.UserChoice then
+ computerModule = computerInputTypeToModuleMap[lastInputType]
+ if UserGameSettings.ComputerMovementMode == Enum.ComputerMovementMode.ClickToMove and computerModule == Keyboard then
+ -- User has ClickToMove set in Settings, prefer ClickToMove controller for keyboard and mouse lastInputTypes
+ computerModule = ClickToMove
+ end
+ else
+ -- Developer has selected a mode that must be used.
+ computerModule = movementEnumToModuleMap[DevMovementMode]
+
+ -- computerModule is expected to be nil here only when developer has selected Scriptable
+ if (not computerModule) and DevMovementMode ~= Enum.DevComputerMovementMode.Scriptable then
+ warn("No character control module is associated with DevComputerMovementMode ", DevMovementMode)
+ end
+ end
+
+ if computerModule then
+ return computerModule, true
+ elseif DevMovementMode == Enum.DevComputerMovementMode.Scriptable then
+ -- Special case where nil is returned and we actually want to set self.activeController to nil for Scriptable
+ return nil, true
+ else
+ -- This case is for when computerModule is nil because of an error and no suitable control module could
+ -- be found.
+ return nil, false
+ end
+ end
+
+ -- Choose current Touch control module based on settings (user, dev)
+ -- Returns module (possibly nil) and success code to differentiate returning nil due to error vs Scriptable
+ function ControlModule:SelectTouchModule()
+ if not UserInputService.TouchEnabled then
+ return nil, false
+ end
+ local touchModule
+ local DevMovementMode = Players.LocalPlayer.DevTouchMovementMode
+ if DevMovementMode == Enum.DevTouchMovementMode.UserChoice then
+ touchModule = movementEnumToModuleMap[UserGameSettings.TouchMovementMode]
+ elseif DevMovementMode == Enum.DevTouchMovementMode.Scriptable then
+ return nil, true
+ else
+ touchModule = movementEnumToModuleMap[DevMovementMode]
+ end
+ return touchModule, true
+ end
+
+ local function calculateRawMoveVector(humanoid, cameraRelativeMoveVector)
+ local camera = Workspace.CurrentCamera
+ if not camera then
+ return cameraRelativeMoveVector
+ end
+
+ if humanoid:GetState() == Enum.HumanoidStateType.Swimming then
+ return camera.CFrame:VectorToWorldSpace(cameraRelativeMoveVector)
+ end
+
+ local c, s
+ local _, _, _, R00, R01, R02, _, _, R12, _, _, R22 = camera.CFrame:GetComponents()
+ if R12 < 1 and R12 > -1 then
+ -- X and Z components from back vector.
+ c = R22
+ s = R02
+ else
+ -- In this case the camera is looking straight up or straight down.
+ -- Use X components from right and up vectors.
+ c = R00
+ s = -R01*math.sign(R12)
+ end
+ local norm = math.sqrt(c*c + s*s)
+ return Vector3.new(
+ (c*cameraRelativeMoveVector.x + s*cameraRelativeMoveVector.z)/norm,
+ 0,
+ (c*cameraRelativeMoveVector.z - s*cameraRelativeMoveVector.x)/norm
+ )
+ end
+
+ function ControlModule:OnRenderStepped(dt)
+ if self.activeController and self.activeController.enabled and self.humanoid then
+ -- Give the controller a chance to adjust its state
+ self.activeController:OnRenderStepped(dt)
+
+ -- Now retrieve info from the controller
+ local moveVector = self.activeController:GetMoveVector()
+ local cameraRelative = self.activeController:IsMoveVectorCameraRelative()
+
+ local clickToMoveController = self:GetClickToMoveController()
+ if self.activeController ~= clickToMoveController then
+ if moveVector.magnitude > 0 then
+ -- Clean up any developer started MoveTo path
+ clickToMoveController:CleanupPath()
+ else
+ -- Get move vector for developer started MoveTo
+ clickToMoveController:OnRenderStepped(dt)
+ moveVector = clickToMoveController:GetMoveVector()
+ cameraRelative = clickToMoveController:IsMoveVectorCameraRelative()
+ end
+ end
+
+ -- Are we driving a vehicle ?
+ local vehicleConsumedInput = false
+ if self.vehicleController then
+ moveVector, vehicleConsumedInput = self.vehicleController:Update(moveVector, cameraRelative, self.activeControlModule==Gamepad)
+ end
+
+ -- If not, move the player
+ -- Verification of vehicleConsumedInput is commented out to preserve legacy behavior,
+ -- in case some game relies on Humanoid.MoveDirection still being set while in a VehicleSeat
+ --if not vehicleConsumedInput then
+ if cameraRelative then
+ moveVector = calculateRawMoveVector(self.humanoid, moveVector)
+ end
+ self.moveFunction(Players.LocalPlayer, moveVector, false)
+ --end
+
+ -- And make them jump if needed
+ self.humanoid.Jump = self.activeController:GetIsJumping() or (self.touchJumpController and self.touchJumpController:GetIsJumping())
+ end
+ end
+
+ function ControlModule:OnHumanoidSeated(active, currentSeatPart)
+ if active then
+ if currentSeatPart and currentSeatPart:IsA("VehicleSeat") then
+ if not self.vehicleController then
+ self.vehicleController = self.vehicleController.new(CONTROL_ACTION_PRIORITY)
+ end
+ self.vehicleController:Enable(true, currentSeatPart)
+ end
+ else
+ if self.vehicleController then
+ self.vehicleController:Enable(false, currentSeatPart)
+ end
+ end
+ end
+
+ function ControlModule:OnCharacterAdded(char)
+ self.humanoid = char:FindFirstChildOfClass("Humanoid")
+ while not self.humanoid do
+ char.ChildAdded:wait()
+ self.humanoid = char:FindFirstChildOfClass("Humanoid")
+ end
+
+ if self.touchGui then
+ self.touchGui.Enabled = true
+ end
+
+ if self.humanoidSeatedConn then
+ self.humanoidSeatedConn:Disconnect()
+ self.humanoidSeatedConn = nil
+ end
+ self.humanoidSeatedConn = self.humanoid.Seated:Connect(function(active, currentSeatPart)
+ self:OnHumanoidSeated(active, currentSeatPart)
+ end)
+ end
+
+ function ControlModule:OnCharacterRemoving(char)
+ self.humanoid = nil
+
+ if self.touchGui then
+ self.touchGui.Enabled = false
+ end
+ end
+
+ -- Helper function to lazily instantiate a controller if it does not yet exist,
+ -- disable the active controller if it is different from the on being switched to,
+ -- and then enable the requested controller. The argument to this function must be
+ -- a reference to one of the control modules, i.e. Keyboard, Gamepad, etc.
+ function ControlModule:SwitchToController(controlModule)
+ if not controlModule then
+ if self.activeController then
+ self.activeController:Enable(false)
+ end
+ self.activeController = nil
+ self.activeControlModule = nil
+ else
+ if not self.controllers[controlModule] then
+ self.controllers[controlModule] = controlModule.new(CONTROL_ACTION_PRIORITY)
+ end
+
+ if self.activeController ~= self.controllers[controlModule] then
+ if self.activeController then
+ self.activeController:Enable(false)
+ end
+ self.activeController = self.controllers[controlModule]
+ self.activeControlModule = controlModule -- Only used to check if controller switch is necessary
+
+ if self.touchControlFrame and (self.activeControlModule == ClickToMove
+ or self.activeControlModule == TouchThumbstick
+ or self.activeControlModule == DynamicThumbstick) then
+ if not self.controllers[TouchJump] then
+ self.controllers[TouchJump] = TouchJump.new()
+ end
+ self.touchJumpController = self.controllers[TouchJump]
+ self.touchJumpController:Enable(true, self.touchControlFrame)
+ else
+ if self.touchJumpController then
+ self.touchJumpController:Enable(false)
+ end
+ end
+
+ self:EnableActiveControlModule()
+ end
+ end
+ end
+
+ function ControlModule:OnLastInputTypeChanged(newLastInputType)
+ if lastInputType == newLastInputType then
+ warn("LastInputType Change listener called with current type.")
+ end
+ lastInputType = newLastInputType
+
+ if lastInputType == Enum.UserInputType.Touch then
+ -- TODO: Check if touch module already active
+ local touchModule, success = self:SelectTouchModule()
+ if success then
+ while not self.touchControlFrame do
+ wait()
+ end
+ self:SwitchToController(touchModule)
+ end
+ elseif computerInputTypeToModuleMap[lastInputType] ~= nil then
+ local computerModule = self:SelectComputerMovementModule()
+ if computerModule then
+ self:SwitchToController(computerModule)
+ end
+ end
+ end
+
+ -- Called when any relevant values of GameSettings or LocalPlayer change, forcing re-evalulation of
+ -- current control scheme
+ function ControlModule:OnComputerMovementModeChange()
+ local controlModule, success = self:SelectComputerMovementModule()
+ if success then
+ self:SwitchToController(controlModule)
+ end
+ end
+
+ function ControlModule:OnTouchMovementModeChange()
+ local touchModule, success = self:SelectTouchModule()
+ if success then
+ while not self.touchControlFrame do
+ wait()
+ end
+ self:SwitchToController(touchModule)
+ end
+ end
+
+ function ControlModule:CreateTouchGuiContainer()
+ if self.touchGui then self.touchGui:Destroy() end
+
+ -- Container for all touch device guis
+ self.touchGui = Instance.new("ScreenGui")
+ self.touchGui.Name = "TouchGui"
+ self.touchGui.ResetOnSpawn = false
+ self.touchGui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling
+ self.touchGui.Enabled = self.humanoid ~= nil
+
+ self.touchControlFrame = Instance.new("Frame")
+ self.touchControlFrame.Name = "TouchControlFrame"
+ self.touchControlFrame.Size = UDim2.new(1, 0, 1, 0)
+ self.touchControlFrame.BackgroundTransparency = 1
+ self.touchControlFrame.Parent = self.touchGui
+
+ self.touchGui.Parent = self.playerGui
+ end
+
+ function ControlModule:GetClickToMoveController()
+ if not self.controllers[ClickToMove] then
+ self.controllers[ClickToMove] = ClickToMove.new(CONTROL_ACTION_PRIORITY)
+ end
+ return self.controllers[ClickToMove]
+ end
+
+ function ControlModule:IsJumping()
+ if self.activeController then
+ return self.activeController:GetIsJumping() or (self.touchJumpController and self.touchJumpController:GetIsJumping())
+ end
+ return false
+ end
+
+ return ControlModule.new()
+end
+
+function _PlayerModule()
+ local PlayerModule = {}
+ PlayerModule.__index = PlayerModule
+ function PlayerModule.new()
+ local self = setmetatable({},PlayerModule)
+ self.cameras = _CameraModule()
+ self.controls = _ControlModule()
+ return self
+ end
+ function PlayerModule:GetCameras()
+ return self.cameras
+ end
+ function PlayerModule:GetControls()
+ return self.controls
+ end
+ function PlayerModule:GetClickToMoveController()
+ return self.controls:GetClickToMoveController()
+ end
+ return PlayerModule.new()
+end
+
+function _sounds()
+
+ local SetState = Instance.new("BindableEvent",script)
+
+ local Players = game:GetService("Players")
+ local RunService = game:GetService("RunService")
+
+ local SOUND_DATA = {
+ Climbing = {
+ SoundId = "rbxasset://sounds/action_footsteps_plastic.mp3",
+ Looped = true,
+ },
+ Died = {
+ SoundId = "rbxasset://sounds/uuhhh.mp3",
+ },
+ FreeFalling = {
+ SoundId = "rbxasset://sounds/action_falling.mp3",
+ Looped = true,
+ },
+ GettingUp = {
+ SoundId = "rbxasset://sounds/action_get_up.mp3",
+ },
+ Jumping = {
+ SoundId = "rbxasset://sounds/action_jump.mp3",
+ },
+ Landing = {
+ SoundId = "rbxasset://sounds/action_jump_land.mp3",
+ },
+ Running = {
+ SoundId = "rbxasset://sounds/action_footsteps_plastic.mp3",
+ Looped = true,
+ Pitch = 1.85,
+ },
+ Splash = {
+ SoundId = "rbxasset://sounds/impact_water.mp3",
+ },
+ Swimming = {
+ SoundId = "rbxasset://sounds/action_swim.mp3",
+ Looped = true,
+ Pitch = 1.6,
+ },
+ }
+
+ -- wait for the first of the passed signals to fire
+ local function waitForFirst(...)
+ local shunt = Instance.new("BindableEvent")
+ local slots = {...}
+
+ local function fire(...)
+ for i = 1, #slots do
+ slots[i]:Disconnect()
+ end
+
+ return shunt:Fire(...)
+ end
+
+ for i = 1, #slots do
+ slots[i] = slots[i]:Connect(fire)
+ end
+
+ return shunt.Event:Wait()
+ end
+
+ -- map a value from one range to another
+ local function map(x, inMin, inMax, outMin, outMax)
+ return (x - inMin)*(outMax - outMin)/(inMax - inMin) + outMin
+ end
+
+ local function playSound(sound)
+ sound.TimePosition = 0
+ sound.Playing = true
+ end
+
+ local function stopSound(sound)
+ sound.Playing = false
+ sound.TimePosition = 0
+ end
+
+ local function shallowCopy(t)
+ local out = {}
+ for k, v in pairs(t) do
+ out[k] = v
+ end
+ return out
+ end
+
+ local function initializeSoundSystem(player, humanoid, rootPart)
+ local sounds = {}
+
+ -- initialize sounds
+ for name, props in pairs(SOUND_DATA) do
+ local sound = Instance.new("Sound")
+ sound.Name = name
+
+ -- set default values
+ sound.Archivable = false
+ sound.EmitterSize = 5
+ sound.MaxDistance = 150
+ sound.Volume = 0.65
+
+ for propName, propValue in pairs(props) do
+ sound[propName] = propValue
+ end
+
+ sound.Parent = rootPart
+ sounds[name] = sound
+ end
+
+ local playingLoopedSounds = {}
+
+ local function stopPlayingLoopedSounds(except)
+ for sound in pairs(shallowCopy(playingLoopedSounds)) do
+ if sound ~= except then
+ sound.Playing = false
+ playingLoopedSounds[sound] = nil
+ end
+ end
+ end
+
+ -- state transition callbacks
+ local stateTransitions = {
+ [Enum.HumanoidStateType.FallingDown] = function()
+ stopPlayingLoopedSounds()
+ end,
+
+ [Enum.HumanoidStateType.GettingUp] = function()
+ stopPlayingLoopedSounds()
+ playSound(sounds.GettingUp)
+ end,
+
+ [Enum.HumanoidStateType.Jumping] = function()
+ stopPlayingLoopedSounds()
+ playSound(sounds.Jumping)
+ end,
+
+ [Enum.HumanoidStateType.Swimming] = function()
+ local verticalSpeed = math.abs(rootPart.Velocity.Y)
+ if verticalSpeed > 0.1 then
+ sounds.Splash.Volume = math.clamp(map(verticalSpeed, 100, 350, 0.28, 1), 0, 1)
+ playSound(sounds.Splash)
+ end
+ stopPlayingLoopedSounds(sounds.Swimming)
+ sounds.Swimming.Playing = true
+ playingLoopedSounds[sounds.Swimming] = true
+ end,
+
+ [Enum.HumanoidStateType.Freefall] = function()
+ sounds.FreeFalling.Volume = 0
+ stopPlayingLoopedSounds(sounds.FreeFalling)
+ playingLoopedSounds[sounds.FreeFalling] = true
+ end,
+
+ [Enum.HumanoidStateType.Landed] = function()
+ stopPlayingLoopedSounds()
+ local verticalSpeed = math.abs(rootPart.Velocity.Y)
+ if verticalSpeed > 75 then
+ sounds.Landing.Volume = math.clamp(map(verticalSpeed, 50, 100, 0, 1), 0, 1)
+ playSound(sounds.Landing)
+ end
+ end,
+
+ [Enum.HumanoidStateType.Running] = function()
+ stopPlayingLoopedSounds(sounds.Running)
+ sounds.Running.Playing = true
+ playingLoopedSounds[sounds.Running] = true
+ end,
+
+ [Enum.HumanoidStateType.Climbing] = function()
+ local sound = sounds.Climbing
+ if math.abs(rootPart.Velocity.Y) > 0.1 then
+ sound.Playing = true
+ stopPlayingLoopedSounds(sound)
+ else
+ stopPlayingLoopedSounds()
+ end
+ playingLoopedSounds[sound] = true
+ end,
+
+ [Enum.HumanoidStateType.Seated] = function()
+ stopPlayingLoopedSounds()
+ end,
+
+ [Enum.HumanoidStateType.Dead] = function()
+ stopPlayingLoopedSounds()
+ playSound(sounds.Died)
+ end,
+ }
+
+ -- updaters for looped sounds
+ local loopedSoundUpdaters = {
+ [sounds.Climbing] = function(dt, sound, vel)
+ sound.Playing = vel.Magnitude > 0.1
+ end,
+
+ [sounds.FreeFalling] = function(dt, sound, vel)
+ if vel.Magnitude > 75 then
+ sound.Volume = math.clamp(sound.Volume + 0.9*dt, 0, 1)
+ else
+ sound.Volume = 0
+ end
+ end,
+
+ [sounds.Running] = function(dt, sound, vel)
+ sound.Playing = vel.Magnitude > 0.5 and humanoid.MoveDirection.Magnitude > 0.5
+ end,
+ }
+
+ -- state substitutions to avoid duplicating entries in the state table
+ local stateRemap = {
+ [Enum.HumanoidStateType.RunningNoPhysics] = Enum.HumanoidStateType.Running,
+ }
+
+ local activeState = stateRemap[humanoid:GetState()] or humanoid:GetState()
+ local activeConnections = {}
+
+ local stateChangedConn = humanoid.StateChanged:Connect(function(_, state)
+ state = stateRemap[state] or state
+
+ if state ~= activeState then
+ local transitionFunc = stateTransitions[state]
+
+ if transitionFunc then
+ transitionFunc()
+ end
+
+ activeState = state
+ end
+ end)
+
+ local customStateChangedConn = SetState.Event:Connect(function(state)
+ state = stateRemap[state] or state
+
+ if state ~= activeState then
+ local transitionFunc = stateTransitions[state]
+
+ if transitionFunc then
+ transitionFunc()
+ end
+
+ activeState = state
+ end
+ end)
+
+ local steppedConn = RunService.Stepped:Connect(function(_, worldDt)
+ -- update looped sounds on stepped
+ for sound in pairs(playingLoopedSounds) do
+ local updater = loopedSoundUpdaters[sound]
+
+ if updater then
+ updater(worldDt, sound, rootPart.Velocity)
+ end
+ end
+ end)
+
+ local humanoidAncestryChangedConn
+ local rootPartAncestryChangedConn
+ local characterAddedConn
+
+ local function terminate()
+ stateChangedConn:Disconnect()
+ customStateChangedConn:Disconnect()
+ steppedConn:Disconnect()
+ humanoidAncestryChangedConn:Disconnect()
+ rootPartAncestryChangedConn:Disconnect()
+ characterAddedConn:Disconnect()
+ end
+
+ humanoidAncestryChangedConn = humanoid.AncestryChanged:Connect(function(_, parent)
+ if not parent then
+ terminate()
+ end
+ end)
+
+ rootPartAncestryChangedConn = rootPart.AncestryChanged:Connect(function(_, parent)
+ if not parent then
+ terminate()
+ end
+ end)
+
+ characterAddedConn = player.CharacterAdded:Connect(terminate)
+ end
+
+ local function playerAdded(player)
+ local function characterAdded(character)
+ -- Avoiding memory leaks in the face of Character/Humanoid/RootPart lifetime has a few complications:
+ -- * character deparenting is a Remove instead of a Destroy, so signals are not cleaned up automatically.
+ -- ** must use a waitForFirst on everything and listen for hierarchy changes.
+ -- * the character might not be in the dm by the time CharacterAdded fires
+ -- ** constantly check consistency with player.Character and abort if CharacterAdded is fired again
+ -- * Humanoid may not exist immediately, and by the time it's inserted the character might be deparented.
+ -- * RootPart probably won't exist immediately.
+ -- ** by the time RootPart is inserted and Humanoid.RootPart is set, the character or the humanoid might be deparented.
+
+ if not character.Parent then
+ waitForFirst(character.AncestryChanged, player.CharacterAdded)
+ end
+
+ if player.Character ~= character or not character.Parent then
+ return
+ end
+
+ local humanoid = character:FindFirstChildOfClass("Humanoid")
+ while character:IsDescendantOf(game) and not humanoid do
+ waitForFirst(character.ChildAdded, character.AncestryChanged, player.CharacterAdded)
+ humanoid = character:FindFirstChildOfClass("Humanoid")
+ end
+
+ if player.Character ~= character or not character:IsDescendantOf(game) then
+ return
+ end
+
+ -- must rely on HumanoidRootPart naming because Humanoid.RootPart does not fire changed signals
+ local rootPart = character:FindFirstChild("HumanoidRootPart")
+ while character:IsDescendantOf(game) and not rootPart do
+ waitForFirst(character.ChildAdded, character.AncestryChanged, humanoid.AncestryChanged, player.CharacterAdded)
+ rootPart = character:FindFirstChild("HumanoidRootPart")
+ end
+
+ if rootPart and humanoid:IsDescendantOf(game) and character:IsDescendantOf(game) and player.Character == character then
+ initializeSoundSystem(player, humanoid, rootPart)
+ end
+ end
+
+ if player.Character then
+ characterAdded(player.Character)
+ end
+ player.CharacterAdded:Connect(characterAdded)
+ end
+
+ Players.PlayerAdded:Connect(playerAdded)
+ for _, player in ipairs(Players:GetPlayers()) do
+ playerAdded(player)
+ end
+ return SetState
+end
+
+function _StateTracker()
+ local EPSILON = 0.1
+
+ local SPEED = {
+ ["onRunning"] = true,
+ ["onClimbing"] = true
+ }
+
+ local INAIR = {
+ ["onFreeFall"] = true,
+ ["onJumping"] = true
+ }
+
+ local STATEMAP = {
+ ["onRunning"] = Enum.HumanoidStateType.Running,
+ ["onJumping"] = Enum.HumanoidStateType.Jumping,
+ ["onFreeFall"] = Enum.HumanoidStateType.Freefall
+ }
+
+ local StateTracker = {}
+ StateTracker.__index = StateTracker
+
+ function StateTracker.new(humanoid, soundState)
+ local self = setmetatable({}, StateTracker)
+
+ self.Humanoid = humanoid
+ self.HRP = humanoid.RootPart
+
+ self.Speed = 0
+ self.State = "onRunning"
+ self.Jumped = false
+ self.JumpTick = tick()
+
+ self.SoundState = soundState
+
+ self._ChangedEvent = Instance.new("BindableEvent")
+ self.Changed = self._ChangedEvent.Event
+
+ return self
+ end
+
+ function StateTracker:Destroy()
+ self._ChangedEvent:Destroy()
+ end
+
+ function StateTracker:RequestedJump()
+ self.Jumped = true
+ self.JumpTick = tick()
+ end
+
+ function StateTracker:OnStep(gravityUp, grounded, isMoving)
+ local cVelocity = self.HRP.Velocity
+ local gVelocity = cVelocity:Dot(gravityUp)
+
+ local oldState, oldSpeed = self.State, self.Speed
+
+ local newState
+ local newSpeed = cVelocity.Magnitude
+
+ if (not grounded) then
+ if (gVelocity > 0) then
+ if (self.Jumped) then
+ newState = "onJumping"
+ else
+ newState = "onFreeFall"
+ end
+ else
+ if (self.Jumped) then
+ self.Jumped = false
+ end
+ newState = "onFreeFall"
+ end
+ else
+ if (self.Jumped and tick() - self.JumpTick > 0.1) then
+ self.Jumped = false
+ end
+ newSpeed = (cVelocity - gVelocity*gravityUp).Magnitude
+ newState = "onRunning"
+ end
+
+ newSpeed = isMoving and newSpeed or 0
+
+ if (oldState ~= newState or (SPEED[newState] and math.abs(oldSpeed - newSpeed) > EPSILON)) then
+ self.State = newState
+ self.Speed = newSpeed
+ self.SoundState:Fire(STATEMAP[newState])
+ self._ChangedEvent:Fire(self.State, self.Speed)
+ end
+ end
+
+ return StateTracker
+end
+function _InitObjects()
+ local model = workspace:FindFirstChild("objects") or game:GetObjects("rbxassetid://5045408489")[1]
+ local SPHERE = model:WaitForChild("Sphere")
+ local FLOOR = model:WaitForChild("Floor")
+ local VFORCE = model:WaitForChild("VectorForce")
+ local BGYRO = model:WaitForChild("BodyGyro")
+ local function initObjects(self)
+ local hrp = self.HRP
+ local humanoid = self.Humanoid
+ local sphere = SPHERE:Clone()
+ sphere.Parent = self.Character
+ local floor = FLOOR:Clone()
+ floor.Parent = self.Character
+ local isR15 = (humanoid.RigType == Enum.HumanoidRigType.R15)
+ local height = isR15 and (humanoid.HipHeight + 0.05) or 2
+ local weld = Instance.new("Weld")
+ weld.C0 = CFrame.new(0, -height, 0.1)
+ weld.Part0 = hrp
+ weld.Part1 = sphere
+ weld.Parent = sphere
+ local weld2 = Instance.new("Weld")
+ weld2.C0 = CFrame.new(0, -(height + 1.5), 0)
+ weld2.Part0 = hrp
+ weld2.Part1 = floor
+ weld2.Parent = floor
+ local gyro = BGYRO:Clone()
+ gyro.CFrame = hrp.CFrame
+ gyro.Parent = hrp
+ local vForce = VFORCE:Clone()
+ vForce.Attachment0 = isR15 and hrp:WaitForChild("RootRigAttachment") or hrp:WaitForChild("RootAttachment")
+ vForce.Parent = hrp
+ return sphere, gyro, vForce, floor
+ end
+ return initObjects
+end
+local plr = game.Players.LocalPlayer
+local ms = plr:GetMouse()
+local char
+plr.CharacterAdded:Connect(function(c)
+ char = c
+end)
+function _R6()
+ function r6()
+ local Figure = char
+ local Torso = Figure:WaitForChild("Torso")
+ local RightShoulder = Torso:WaitForChild("Right Shoulder")
+ local LeftShoulder = Torso:WaitForChild("Left Shoulder")
+ local RightHip = Torso:WaitForChild("Right Hip")
+ local LeftHip = Torso:WaitForChild("Left Hip")
+ local Neck = Torso:WaitForChild("Neck")
+ local Humanoid = Figure:WaitForChild("Humanoid")
+ local pose = "Standing"
+ local currentAnim = ""
+ local currentAnimInstance = nil
+ local currentAnimTrack = nil
+ local currentAnimKeyframeHandler = nil
+ local currentAnimSpeed = 1.0
+ local animTable = {}
+ local animNames = {
+ idle = {
+ { id = "http://www.roblox.com/asset/?id=180435571", weight = 9 },
+ { id = "http://www.roblox.com/asset/?id=180435792", weight = 1 }
+ },
+ walk = {
+ { id = "http://www.roblox.com/asset/?id=180426354", weight = 10 }
+ },
+ run = {
+ { id = "run.xml", weight = 10 }
+ },
+ jump = {
+ { id = "http://www.roblox.com/asset/?id=125750702", weight = 10 }
+ },
+ fall = {
+ { id = "http://www.roblox.com/asset/?id=180436148", weight = 10 }
+ },
+ climb = {
+ { id = "http://www.roblox.com/asset/?id=180436334", weight = 10 }
+ },
+ sit = {
+ { id = "http://www.roblox.com/asset/?id=178130996", weight = 10 }
+ },
+ toolnone = {
+ { id = "http://www.roblox.com/asset/?id=182393478", weight = 10 }
+ },
+ toolslash = {
+ { id = "http://www.roblox.com/asset/?id=129967390", weight = 10 }
+ -- { id = "slash.xml", weight = 10 }
+ },
+ toollunge = {
+ { id = "http://www.roblox.com/asset/?id=129967478", weight = 10 }
+ },
+ wave = {
+ { id = "http://www.roblox.com/asset/?id=128777973", weight = 10 }
+ },
+ point = {
+ { id = "http://www.roblox.com/asset/?id=128853357", weight = 10 }
+ },
+ dance1 = {
+ { id = "http://www.roblox.com/asset/?id=182435998", weight = 10 },
+ { id = "http://www.roblox.com/asset/?id=182491037", weight = 10 },
+ { id = "http://www.roblox.com/asset/?id=182491065", weight = 10 }
+ },
+ dance2 = {
+ { id = "http://www.roblox.com/asset/?id=182436842", weight = 10 },
+ { id = "http://www.roblox.com/asset/?id=182491248", weight = 10 },
+ { id = "http://www.roblox.com/asset/?id=182491277", weight = 10 }
+ },
+ dance3 = {
+ { id = "http://www.roblox.com/asset/?id=182436935", weight = 10 },
+ { id = "http://www.roblox.com/asset/?id=182491368", weight = 10 },
+ { id = "http://www.roblox.com/asset/?id=182491423", weight = 10 }
+ },
+ laugh = {
+ { id = "http://www.roblox.com/asset/?id=129423131", weight = 10 }
+ },
+ cheer = {
+ { id = "http://www.roblox.com/asset/?id=129423030", weight = 10 }
+ },
+ }
+ local dances = {"dance1", "dance2", "dance3"}
+ -- Existance in this list signifies that it is an emote, the value indicates if it is a looping emote
+ local emoteNames = { wave = false, point = false, dance1 = true, dance2 = true, dance3 = true, laugh = false, cheer = false}
+ function configureAnimationSet(name, fileList)
+ if (animTable[name] ~= nil) then
+ for _, connection in pairs(animTable[name].connections) do
+ connection:disconnect()
+ end
+ end
+ animTable[name] = {}
+ animTable[name].count = 0
+ animTable[name].totalWeight = 0
+ animTable[name].connections = {}
+ -- check for config values
+ local config = script:FindFirstChild(name)
+ if (config ~= nil) then
+ -- print("Loading anims " .. name)
+ table.insert(animTable[name].connections, config.ChildAdded:connect(function(child) configureAnimationSet(name, fileList) end))
+ table.insert(animTable[name].connections, config.ChildRemoved:connect(function(child) configureAnimationSet(name, fileList) end))
+ local idx = 1
+ for _, childPart in pairs(config:GetChildren()) do
+ if (childPart:IsA("Animation")) then
+ table.insert(animTable[name].connections, childPart.Changed:connect(function(property) configureAnimationSet(name, fileList) end))
+ animTable[name][idx] = {}
+ animTable[name][idx].anim = childPart
+ local weightObject = childPart:FindFirstChild("Weight")
+ if (weightObject == nil) then
+ animTable[name][idx].weight = 1
+ else
+ animTable[name][idx].weight = weightObject.Value
+ end
+ animTable[name].count = animTable[name].count + 1
+ animTable[name].totalWeight = animTable[name].totalWeight + animTable[name][idx].weight
+ -- print(name .. " [" .. idx .. "] " .. animTable[name][idx].anim.AnimationId .. " (" .. animTable[name][idx].weight .. ")")
+ idx = idx + 1
+ end
+ end
+ end
+ -- fallback to defaults
+ if (animTable[name].count <= 0) then
+ for idx, anim in pairs(fileList) do
+ animTable[name][idx] = {}
+ animTable[name][idx].anim = Instance.new("Animation")
+ animTable[name][idx].anim.Name = name
+ animTable[name][idx].anim.AnimationId = anim.id
+ animTable[name][idx].weight = anim.weight
+ animTable[name].count = animTable[name].count + 1
+ animTable[name].totalWeight = animTable[name].totalWeight + anim.weight
+ -- print(name .. " [" .. idx .. "] " .. anim.id .. " (" .. anim.weight .. ")")
+ end
+ end
+ end
+ -- Setup animation objects
+ function scriptChildModified(child)
+ local fileList = animNames[child.Name]
+ if (fileList ~= nil) then
+ configureAnimationSet(child.Name, fileList)
+ end
+ end
+
+ script.ChildAdded:connect(scriptChildModified)
+ script.ChildRemoved:connect(scriptChildModified)
+
+
+ for name, fileList in pairs(animNames) do
+ configureAnimationSet(name, fileList)
+ end
+
+ -- ANIMATION
+
+ -- declarations
+ local toolAnim = "None"
+ local toolAnimTime = 0
+
+ local jumpAnimTime = 0
+ local jumpAnimDuration = 0.3
+
+ local toolTransitionTime = 0.1
+ local fallTransitionTime = 0.3
+ local jumpMaxLimbVelocity = 0.75
+
+ -- functions
+
+ function stopAllAnimations()
+ local oldAnim = currentAnim
+
+ -- return to idle if finishing an emote
+ if (emoteNames[oldAnim] ~= nil and emoteNames[oldAnim] == false) then
+ oldAnim = "idle"
+ end
+
+ currentAnim = ""
+ currentAnimInstance = nil
+ if (currentAnimKeyframeHandler ~= nil) then
+ currentAnimKeyframeHandler:disconnect()
+ end
+
+ if (currentAnimTrack ~= nil) then
+ currentAnimTrack:Stop()
+ currentAnimTrack:Destroy()
+ currentAnimTrack = nil
+ end
+ return oldAnim
+ end
+
+ function setAnimationSpeed(speed)
+ if speed ~= currentAnimSpeed then
+ currentAnimSpeed = speed
+ currentAnimTrack:AdjustSpeed(currentAnimSpeed)
+ end
+ end
+
+ function keyFrameReachedFunc(frameName)
+ if (frameName == "End") then
+
+ local repeatAnim = currentAnim
+ -- return to idle if finishing an emote
+ if (emoteNames[repeatAnim] ~= nil and emoteNames[repeatAnim] == false) then
+ repeatAnim = "idle"
+ end
+
+ local animSpeed = currentAnimSpeed
+ playAnimation(repeatAnim, 0.0, Humanoid)
+ setAnimationSpeed(animSpeed)
+ end
+ end
+
+ -- Preload animations
+ function playAnimation(animName, transitionTime, humanoid)
+
+ local roll = math.random(1, animTable[animName].totalWeight)
+ local origRoll = roll
+ local idx = 1
+ while (roll > animTable[animName][idx].weight) do
+ roll = roll - animTable[animName][idx].weight
+ idx = idx + 1
+ end
+ -- print(animName .. " " .. idx .. " [" .. origRoll .. "]")
+ local anim = animTable[animName][idx].anim
+
+ -- switch animation
+ if (anim ~= currentAnimInstance) then
+
+ if (currentAnimTrack ~= nil) then
+ currentAnimTrack:Stop(transitionTime)
+ currentAnimTrack:Destroy()
+ end
+
+ currentAnimSpeed = 1.0
+
+ -- load it to the humanoid; get AnimationTrack
+ currentAnimTrack = humanoid:LoadAnimation(anim)
+ currentAnimTrack.Priority = Enum.AnimationPriority.Core
+
+ -- play the animation
+ currentAnimTrack:Play(transitionTime)
+ currentAnim = animName
+ currentAnimInstance = anim
+
+ -- set up keyframe name triggers
+ if (currentAnimKeyframeHandler ~= nil) then
+ currentAnimKeyframeHandler:disconnect()
+ end
+ currentAnimKeyframeHandler = currentAnimTrack.KeyframeReached:connect(keyFrameReachedFunc)
+
+ end
+
+ end
+
+ -------------------------------------------------------------------------------------------
+ -------------------------------------------------------------------------------------------
+
+ local toolAnimName = ""
+ local toolAnimTrack = nil
+ local toolAnimInstance = nil
+ local currentToolAnimKeyframeHandler = nil
+
+ function toolKeyFrameReachedFunc(frameName)
+ if (frameName == "End") then
+ -- print("Keyframe : ".. frameName)
+ playToolAnimation(toolAnimName, 0.0, Humanoid)
+ end
+ end
+
+
+ function playToolAnimation(animName, transitionTime, humanoid, priority)
+
+ local roll = math.random(1, animTable[animName].totalWeight)
+ local origRoll = roll
+ local idx = 1
+ while (roll > animTable[animName][idx].weight) do
+ roll = roll - animTable[animName][idx].weight
+ idx = idx + 1
+ end
+ -- print(animName .. " * " .. idx .. " [" .. origRoll .. "]")
+ local anim = animTable[animName][idx].anim
+
+ if (toolAnimInstance ~= anim) then
+
+ if (toolAnimTrack ~= nil) then
+ toolAnimTrack:Stop()
+ toolAnimTrack:Destroy()
+ transitionTime = 0
+ end
+
+ -- load it to the humanoid; get AnimationTrack
+ toolAnimTrack = humanoid:LoadAnimation(anim)
+ if priority then
+ toolAnimTrack.Priority = priority
+ end
+
+ -- play the animation
+ toolAnimTrack:Play(transitionTime)
+ toolAnimName = animName
+ toolAnimInstance = anim
+
+ currentToolAnimKeyframeHandler = toolAnimTrack.KeyframeReached:connect(toolKeyFrameReachedFunc)
+ end
+ end
+
+ function stopToolAnimations()
+ local oldAnim = toolAnimName
+
+ if (currentToolAnimKeyframeHandler ~= nil) then
+ currentToolAnimKeyframeHandler:disconnect()
+ end
+
+ toolAnimName = ""
+ toolAnimInstance = nil
+ if (toolAnimTrack ~= nil) then
+ toolAnimTrack:Stop()
+ toolAnimTrack:Destroy()
+ toolAnimTrack = nil
+ end
+
+
+ return oldAnim
+ end
+
+ -------------------------------------------------------------------------------------------
+ -------------------------------------------------------------------------------------------
+
+
+ function onRunning(speed)
+ if speed > 0.01 then
+ playAnimation("walk", 0.1, Humanoid)
+ if currentAnimInstance and currentAnimInstance.AnimationId == "http://www.roblox.com/asset/?id=180426354" then
+ setAnimationSpeed(speed / 14.5)
+ end
+ pose = "Running"
+ else
+ if emoteNames[currentAnim] == nil then
+ playAnimation("idle", 0.1, Humanoid)
+ pose = "Standing"
+ end
+ end
+ end
+
+ function onDied()
+ pose = "Dead"
+ end
+
+ function onJumping()
+ playAnimation("jump", 0.1, Humanoid)
+ jumpAnimTime = jumpAnimDuration
+ pose = "Jumping"
+ end
+
+ function onClimbing(speed)
+ playAnimation("climb", 0.1, Humanoid)
+ setAnimationSpeed(speed / 12.0)
+ pose = "Climbing"
+ end
+
+ function onGettingUp()
+ pose = "GettingUp"
+ end
+
+ function onFreeFall()
+ if (jumpAnimTime <= 0) then
+ playAnimation("fall", fallTransitionTime, Humanoid)
+ end
+ pose = "FreeFall"
+ end
+
+ function onFallingDown()
+ pose = "FallingDown"
+ end
+
+ function onSeated()
+ pose = "Seated"
+ end
+
+ function onPlatformStanding()
+ pose = "PlatformStanding"
+ end
+
+ function onSwimming(speed)
+ if speed > 0 then
+ pose = "Running"
+ else
+ pose = "Standing"
+ end
+ end
+
+ function getTool()
+ for _, kid in ipairs(Figure:GetChildren()) do
+ if kid.className == "Tool" then return kid end
+ end
+ return nil
+ end
+
+ function getToolAnim(tool)
+ for _, c in ipairs(tool:GetChildren()) do
+ if c.Name == "toolanim" and c.className == "StringValue" then
+ return c
+ end
+ end
+ return nil
+ end
+
+ function animateTool()
+
+ if (toolAnim == "None") then
+ playToolAnimation("toolnone", toolTransitionTime, Humanoid, Enum.AnimationPriority.Idle)
+ return
+ end
+
+ if (toolAnim == "Slash") then
+ playToolAnimation("toolslash", 0, Humanoid, Enum.AnimationPriority.Action)
+ return
+ end
+
+ if (toolAnim == "Lunge") then
+ playToolAnimation("toollunge", 0, Humanoid, Enum.AnimationPriority.Action)
+ return
+ end
+ end
+
+ function moveSit()
+ RightShoulder.MaxVelocity = 0.15
+ LeftShoulder.MaxVelocity = 0.15
+ RightShoulder:SetDesiredAngle(3.14 /2)
+ LeftShoulder:SetDesiredAngle(-3.14 /2)
+ RightHip:SetDesiredAngle(3.14 /2)
+ LeftHip:SetDesiredAngle(-3.14 /2)
+ end
+
+ local lastTick = 0
+
+ function move(time)
+ local amplitude = 1
+ local frequency = 1
+ local deltaTime = time - lastTick
+ lastTick = time
+
+ local climbFudge = 0
+ local setAngles = false
+
+ if (jumpAnimTime > 0) then
+ jumpAnimTime = jumpAnimTime - deltaTime
+ end
+
+ if (pose == "FreeFall" and jumpAnimTime <= 0) then
+ playAnimation("fall", fallTransitionTime, Humanoid)
+ elseif (pose == "Seated") then
+ playAnimation("sit", 0.5, Humanoid)
+ return
+ elseif (pose == "Running") then
+ playAnimation("walk", 0.1, Humanoid)
+ elseif (pose == "Dead" or pose == "GettingUp" or pose == "FallingDown" or pose == "Seated" or pose == "PlatformStanding") then
+ -- print("Wha " .. pose)
+ stopAllAnimations()
+ amplitude = 0.1
+ frequency = 1
+ setAngles = true
+ end
+
+ if (setAngles) then
+ local desiredAngle = amplitude * math.sin(time * frequency)
+
+ RightShoulder:SetDesiredAngle(desiredAngle + climbFudge)
+ LeftShoulder:SetDesiredAngle(desiredAngle - climbFudge)
+ RightHip:SetDesiredAngle(-desiredAngle)
+ LeftHip:SetDesiredAngle(-desiredAngle)
+ end
+
+ -- Tool Animation handling
+ local tool = getTool()
+ if tool and tool:FindFirstChild("Handle") then
+
+ local animStringValueObject = getToolAnim(tool)
+
+ if animStringValueObject then
+ toolAnim = animStringValueObject.Value
+ -- message recieved, delete StringValue
+ animStringValueObject.Parent = nil
+ toolAnimTime = time + .3
+ end
+
+ if time > toolAnimTime then
+ toolAnimTime = 0
+ toolAnim = "None"
+ end
+
+ animateTool()
+ else
+ stopToolAnimations()
+ toolAnim = "None"
+ toolAnimInstance = nil
+ toolAnimTime = 0
+ end
+ end
+
+
+ local events = {}
+ local eventHum = Humanoid
+
+ local function onUnhook()
+ for i = 1, #events do
+ events[i]:Disconnect()
+ end
+ events = {}
+ end
+
+ local function onHook()
+ onUnhook()
+
+ pose = eventHum.Sit and "Seated" or "Standing"
+
+ events = {
+ eventHum.Died:connect(onDied),
+ eventHum.Running:connect(onRunning),
+ eventHum.Jumping:connect(onJumping),
+ eventHum.Climbing:connect(onClimbing),
+ eventHum.GettingUp:connect(onGettingUp),
+ eventHum.FreeFalling:connect(onFreeFall),
+ eventHum.FallingDown:connect(onFallingDown),
+ eventHum.Seated:connect(onSeated),
+ eventHum.PlatformStanding:connect(onPlatformStanding),
+ eventHum.Swimming:connect(onSwimming)
+ }
+ end
+
+
+ onHook()
+
+ -- setup emote chat hook
+ game:GetService("Players").LocalPlayer.Chatted:connect(function(msg)
+ local emote = ""
+ if msg == "/e dance" then
+ emote = dances[math.random(1, #dances)]
+ elseif (string.sub(msg, 1, 3) == "/e ") then
+ emote = string.sub(msg, 4)
+ elseif (string.sub(msg, 1, 7) == "/emote ") then
+ emote = string.sub(msg, 8)
+ end
+
+ if (pose == "Standing" and emoteNames[emote] ~= nil) then
+ playAnimation(emote, 0.1, Humanoid)
+ end
+
+ end)
+
+
+ -- main program
+
+ -- initialize to idle
+ playAnimation("idle", 0.1, Humanoid)
+ pose = "Standing"
+
+ spawn(function()
+ while Figure.Parent ~= nil do
+ local _, time = wait(0.1)
+ move(time)
+ end
+ end)
+
+ return {
+ onRunning = onRunning,
+ onDied = onDied,
+ onJumping = onJumping,
+ onClimbing = onClimbing,
+ onGettingUp = onGettingUp,
+ onFreeFall = onFreeFall,
+ onFallingDown = onFallingDown,
+ onSeated = onSeated,
+ onPlatformStanding = onPlatformStanding,
+ onHook = onHook,
+ onUnhook = onUnhook
+ }
+
+ end
+ return r6()
+end
+
+function _R15()
+ local function r15()
+
+ local Character = char
+ local Humanoid = Character:WaitForChild("Humanoid")
+ local pose = "Standing"
+
+ local userNoUpdateOnLoopSuccess, userNoUpdateOnLoopValue = pcall(function() return UserSettings():IsUserFeatureEnabled("UserNoUpdateOnLoop") end)
+ local userNoUpdateOnLoop = userNoUpdateOnLoopSuccess and userNoUpdateOnLoopValue
+ local userAnimationSpeedDampeningSuccess, userAnimationSpeedDampeningValue = pcall(function() return UserSettings():IsUserFeatureEnabled("UserAnimationSpeedDampening") end)
+ local userAnimationSpeedDampening = userAnimationSpeedDampeningSuccess and userAnimationSpeedDampeningValue
+
+ local animateScriptEmoteHookFlagExists, animateScriptEmoteHookFlagEnabled = pcall(function()
+ return UserSettings():IsUserFeatureEnabled("UserAnimateScriptEmoteHook")
+ end)
+ local FFlagAnimateScriptEmoteHook = animateScriptEmoteHookFlagExists and animateScriptEmoteHookFlagEnabled
+
+ local AnimationSpeedDampeningObject = script:FindFirstChild("ScaleDampeningPercent")
+ local HumanoidHipHeight = 2
+
+ local EMOTE_TRANSITION_TIME = 0.1
+
+ local currentAnim = ""
+ local currentAnimInstance = nil
+ local currentAnimTrack = nil
+ local currentAnimKeyframeHandler = nil
+ local currentAnimSpeed = 1.0
+
+ local runAnimTrack = nil
+ local runAnimKeyframeHandler = nil
+
+ local animTable = {}
+ local animNames = {
+ idle = {
+ { id = "http://www.roblox.com/asset/?id=507766666", weight = 1 },
+ { id = "http://www.roblox.com/asset/?id=507766951", weight = 1 },
+ { id = "http://www.roblox.com/asset/?id=507766388", weight = 9 }
+ },
+ walk = {
+ { id = "http://www.roblox.com/asset/?id=507777826", weight = 10 }
+ },
+ run = {
+ { id = "http://www.roblox.com/asset/?id=507767714", weight = 10 }
+ },
+ swim = {
+ { id = "http://www.roblox.com/asset/?id=507784897", weight = 10 }
+ },
+ swimidle = {
+ { id = "http://www.roblox.com/asset/?id=507785072", weight = 10 }
+ },
+ jump = {
+ { id = "http://www.roblox.com/asset/?id=507765000", weight = 10 }
+ },
+ fall = {
+ { id = "http://www.roblox.com/asset/?id=507767968", weight = 10 }
+ },
+ climb = {
+ { id = "http://www.roblox.com/asset/?id=507765644", weight = 10 }
+ },
+ sit = {
+ { id = "http://www.roblox.com/asset/?id=2506281703", weight = 10 }
+ },
+ toolnone = {
+ { id = "http://www.roblox.com/asset/?id=507768375", weight = 10 }
+ },
+ toolslash = {
+ { id = "http://www.roblox.com/asset/?id=522635514", weight = 10 }
+ },
+ toollunge = {
+ { id = "http://www.roblox.com/asset/?id=522638767", weight = 10 }
+ },
+ wave = {
+ { id = "http://www.roblox.com/asset/?id=507770239", weight = 10 }
+ },
+ point = {
+ { id = "http://www.roblox.com/asset/?id=507770453", weight = 10 }
+ },
+ dance = {
+ { id = "http://www.roblox.com/asset/?id=507771019", weight = 10 },
+ { id = "http://www.roblox.com/asset/?id=507771955", weight = 10 },
+ { id = "http://www.roblox.com/asset/?id=507772104", weight = 10 }
+ },
+ dance2 = {
+ { id = "http://www.roblox.com/asset/?id=507776043", weight = 10 },
+ { id = "http://www.roblox.com/asset/?id=507776720", weight = 10 },
+ { id = "http://www.roblox.com/asset/?id=507776879", weight = 10 }
+ },
+ dance3 = {
+ { id = "http://www.roblox.com/asset/?id=507777268", weight = 10 },
+ { id = "http://www.roblox.com/asset/?id=507777451", weight = 10 },
+ { id = "http://www.roblox.com/asset/?id=507777623", weight = 10 }
+ },
+ laugh = {
+ { id = "http://www.roblox.com/asset/?id=507770818", weight = 10 }
+ },
+ cheer = {
+ { id = "http://www.roblox.com/asset/?id=507770677", weight = 10 }
+ },
+ }
+
+ -- Existance in this list signifies that it is an emote, the value indicates if it is a looping emote
+ local emoteNames = { wave = false, point = false, dance = true, dance2 = true, dance3 = true, laugh = false, cheer = false}
+
+ local PreloadAnimsUserFlag = false
+ local PreloadedAnims = {}
+ local successPreloadAnim, msgPreloadAnim = pcall(function()
+ PreloadAnimsUserFlag = UserSettings():IsUserFeatureEnabled("UserPreloadAnimations")
+ end)
+ if not successPreloadAnim then
+ PreloadAnimsUserFlag = false
+ end
+
+ math.randomseed(tick())
+
+ function findExistingAnimationInSet(set, anim)
+ if set == nil or anim == nil then
+ return 0
+ end
+
+ for idx = 1, set.count, 1 do
+ if set[idx].anim.AnimationId == anim.AnimationId then
+ return idx
+ end
+ end
+
+ return 0
+ end
+
+ function configureAnimationSet(name, fileList)
+ if (animTable[name] ~= nil) then
+ for _, connection in pairs(animTable[name].connections) do
+ connection:disconnect()
+ end
+ end
+ animTable[name] = {}
+ animTable[name].count = 0
+ animTable[name].totalWeight = 0
+ animTable[name].connections = {}
+
+ local allowCustomAnimations = true
+
+ local success, msg = pcall(function() allowCustomAnimations = game:GetService("StarterPlayer").AllowCustomAnimations end)
+ if not success then
+ allowCustomAnimations = true
+ end
+
+ -- check for config values
+ local config = script:FindFirstChild(name)
+ if (allowCustomAnimations and config ~= nil) then
+ table.insert(animTable[name].connections, config.ChildAdded:connect(function(child) configureAnimationSet(name, fileList) end))
+ table.insert(animTable[name].connections, config.ChildRemoved:connect(function(child) configureAnimationSet(name, fileList) end))
+
+ local idx = 0
+ for _, childPart in pairs(config:GetChildren()) do
+ if (childPart:IsA("Animation")) then
+ local newWeight = 1
+ local weightObject = childPart:FindFirstChild("Weight")
+ if (weightObject ~= nil) then
+ newWeight = weightObject.Value
+ end
+ animTable[name].count = animTable[name].count + 1
+ idx = animTable[name].count
+ animTable[name][idx] = {}
+ animTable[name][idx].anim = childPart
+ animTable[name][idx].weight = newWeight
+ animTable[name].totalWeight = animTable[name].totalWeight + animTable[name][idx].weight
+ table.insert(animTable[name].connections, childPart.Changed:connect(function(property) configureAnimationSet(name, fileList) end))
+ table.insert(animTable[name].connections, childPart.ChildAdded:connect(function(property) configureAnimationSet(name, fileList) end))
+ table.insert(animTable[name].connections, childPart.ChildRemoved:connect(function(property) configureAnimationSet(name, fileList) end))
+ end
+ end
+ end
+
+ -- fallback to defaults
+ if (animTable[name].count <= 0) then
+ for idx, anim in pairs(fileList) do
+ animTable[name][idx] = {}
+ animTable[name][idx].anim = Instance.new("Animation")
+ animTable[name][idx].anim.Name = name
+ animTable[name][idx].anim.AnimationId = anim.id
+ animTable[name][idx].weight = anim.weight
+ animTable[name].count = animTable[name].count + 1
+ animTable[name].totalWeight = animTable[name].totalWeight + anim.weight
+ end
+ end
+
+ -- preload anims
+ if PreloadAnimsUserFlag then
+ for i, animType in pairs(animTable) do
+ for idx = 1, animType.count, 1 do
+ if PreloadedAnims[animType[idx].anim.AnimationId] == nil then
+ Humanoid:LoadAnimation(animType[idx].anim)
+ PreloadedAnims[animType[idx].anim.AnimationId] = true
+ end
+ end
+ end
+ end
+ end
+
+ ------------------------------------------------------------------------------------------------------------
+
+ function configureAnimationSetOld(name, fileList)
+ if (animTable[name] ~= nil) then
+ for _, connection in pairs(animTable[name].connections) do
+ connection:disconnect()
+ end
+ end
+ animTable[name] = {}
+ animTable[name].count = 0
+ animTable[name].totalWeight = 0
+ animTable[name].connections = {}
+
+ local allowCustomAnimations = true
+
+ local success, msg = pcall(function() allowCustomAnimations = game:GetService("StarterPlayer").AllowCustomAnimations end)
+ if not success then
+ allowCustomAnimations = true
+ end
+
+ -- check for config values
+ local config = script:FindFirstChild(name)
+ if (allowCustomAnimations and config ~= nil) then
+ table.insert(animTable[name].connections, config.ChildAdded:connect(function(child) configureAnimationSet(name, fileList) end))
+ table.insert(animTable[name].connections, config.ChildRemoved:connect(function(child) configureAnimationSet(name, fileList) end))
+ local idx = 1
+ for _, childPart in pairs(config:GetChildren()) do
+ if (childPart:IsA("Animation")) then
+ table.insert(animTable[name].connections, childPart.Changed:connect(function(property) configureAnimationSet(name, fileList) end))
+ animTable[name][idx] = {}
+ animTable[name][idx].anim = childPart
+ local weightObject = childPart:FindFirstChild("Weight")
+ if (weightObject == nil) then
+ animTable[name][idx].weight = 1
+ else
+ animTable[name][idx].weight = weightObject.Value
+ end
+ animTable[name].count = animTable[name].count + 1
+ animTable[name].totalWeight = animTable[name].totalWeight + animTable[name][idx].weight
+ idx = idx + 1
+ end
+ end
+ end
+
+ -- fallback to defaults
+ if (animTable[name].count <= 0) then
+ for idx, anim in pairs(fileList) do
+ animTable[name][idx] = {}
+ animTable[name][idx].anim = Instance.new("Animation")
+ animTable[name][idx].anim.Name = name
+ animTable[name][idx].anim.AnimationId = anim.id
+ animTable[name][idx].weight = anim.weight
+ animTable[name].count = animTable[name].count + 1
+ animTable[name].totalWeight = animTable[name].totalWeight + anim.weight
+ -- print(name .. " [" .. idx .. "] " .. anim.id .. " (" .. anim.weight .. ")")
+ end
+ end
+
+ -- preload anims
+ if PreloadAnimsUserFlag then
+ for i, animType in pairs(animTable) do
+ for idx = 1, animType.count, 1 do
+ Humanoid:LoadAnimation(animType[idx].anim)
+ end
+ end
+ end
+ end
+
+ -- Setup animation objects
+ function scriptChildModified(child)
+ local fileList = animNames[child.Name]
+ if (fileList ~= nil) then
+ configureAnimationSet(child.Name, fileList)
+ end
+ end
+
+ script.ChildAdded:connect(scriptChildModified)
+ script.ChildRemoved:connect(scriptChildModified)
+
+
+ for name, fileList in pairs(animNames) do
+ configureAnimationSet(name, fileList)
+ end
+
+ -- ANIMATION
+
+ -- declarations
+ local toolAnim = "None"
+ local toolAnimTime = 0
+
+ local jumpAnimTime = 0
+ local jumpAnimDuration = 0.31
+
+ local toolTransitionTime = 0.1
+ local fallTransitionTime = 0.2
+
+ local currentlyPlayingEmote = false
+
+ -- functions
+
+ function stopAllAnimations()
+ local oldAnim = currentAnim
+
+ -- return to idle if finishing an emote
+ if (emoteNames[oldAnim] ~= nil and emoteNames[oldAnim] == false) then
+ oldAnim = "idle"
+ end
+
+ if FFlagAnimateScriptEmoteHook and currentlyPlayingEmote then
+ oldAnim = "idle"
+ currentlyPlayingEmote = false
+ end
+
+ currentAnim = ""
+ currentAnimInstance = nil
+ if (currentAnimKeyframeHandler ~= nil) then
+ currentAnimKeyframeHandler:disconnect()
+ end
+
+ if (currentAnimTrack ~= nil) then
+ currentAnimTrack:Stop()
+ currentAnimTrack:Destroy()
+ currentAnimTrack = nil
+ end
+
+ -- clean up walk if there is one
+ if (runAnimKeyframeHandler ~= nil) then
+ runAnimKeyframeHandler:disconnect()
+ end
+
+ if (runAnimTrack ~= nil) then
+ runAnimTrack:Stop()
+ runAnimTrack:Destroy()
+ runAnimTrack = nil
+ end
+
+ return oldAnim
+ end
+
+ function getHeightScale()
+ if Humanoid then
+ if not Humanoid.AutomaticScalingEnabled then
+ return 1
+ end
+
+ local scale = Humanoid.HipHeight / HumanoidHipHeight
+ if userAnimationSpeedDampening then
+ if AnimationSpeedDampeningObject == nil then
+ AnimationSpeedDampeningObject = script:FindFirstChild("ScaleDampeningPercent")
+ end
+ if AnimationSpeedDampeningObject ~= nil then
+ scale = 1 + (Humanoid.HipHeight - HumanoidHipHeight) * AnimationSpeedDampeningObject.Value / HumanoidHipHeight
+ end
+ end
+ return scale
+ end
+ return 1
+ end
+
+ local smallButNotZero = 0.0001
+ function setRunSpeed(speed)
+ local speedScaled = speed * 1.25
+ local heightScale = getHeightScale()
+ local runSpeed = speedScaled / heightScale
+
+ if runSpeed ~= currentAnimSpeed then
+ if runSpeed < 0.33 then
+ currentAnimTrack:AdjustWeight(1.0)
+ runAnimTrack:AdjustWeight(smallButNotZero)
+ elseif runSpeed < 0.66 then
+ local weight = ((runSpeed - 0.33) / 0.33)
+ currentAnimTrack:AdjustWeight(1.0 - weight + smallButNotZero)
+ runAnimTrack:AdjustWeight(weight + smallButNotZero)
+ else
+ currentAnimTrack:AdjustWeight(smallButNotZero)
+ runAnimTrack:AdjustWeight(1.0)
+ end
+ currentAnimSpeed = runSpeed
+ runAnimTrack:AdjustSpeed(runSpeed)
+ currentAnimTrack:AdjustSpeed(runSpeed)
+ end
+ end
+
+ function setAnimationSpeed(speed)
+ if currentAnim == "walk" then
+ setRunSpeed(speed)
+ else
+ if speed ~= currentAnimSpeed then
+ currentAnimSpeed = speed
+ currentAnimTrack:AdjustSpeed(currentAnimSpeed)
+ end
+ end
+ end
+
+ function keyFrameReachedFunc(frameName)
+ if (frameName == "End") then
+ if currentAnim == "walk" then
+ if userNoUpdateOnLoop == true then
+ if runAnimTrack.Looped ~= true then
+ runAnimTrack.TimePosition = 0.0
+ end
+ if currentAnimTrack.Looped ~= true then
+ currentAnimTrack.TimePosition = 0.0
+ end
+ else
+ runAnimTrack.TimePosition = 0.0
+ currentAnimTrack.TimePosition = 0.0
+ end
+ else
+ local repeatAnim = currentAnim
+ -- return to idle if finishing an emote
+ if (emoteNames[repeatAnim] ~= nil and emoteNames[repeatAnim] == false) then
+ repeatAnim = "idle"
+ end
+
+ if FFlagAnimateScriptEmoteHook and currentlyPlayingEmote then
+ if currentAnimTrack.Looped then
+ -- Allow the emote to loop
+ return
+ end
+
+ repeatAnim = "idle"
+ currentlyPlayingEmote = false
+ end
+
+ local animSpeed = currentAnimSpeed
+ playAnimation(repeatAnim, 0.15, Humanoid)
+ setAnimationSpeed(animSpeed)
+ end
+ end
+ end
+
+ function rollAnimation(animName)
+ local roll = math.random(1, animTable[animName].totalWeight)
+ local origRoll = roll
+ local idx = 1
+ while (roll > animTable[animName][idx].weight) do
+ roll = roll - animTable[animName][idx].weight
+ idx = idx + 1
+ end
+ return idx
+ end
+
+ local function switchToAnim(anim, animName, transitionTime, humanoid)
+ -- switch animation
+ if (anim ~= currentAnimInstance) then
+
+ if (currentAnimTrack ~= nil) then
+ currentAnimTrack:Stop(transitionTime)
+ currentAnimTrack:Destroy()
+ end
+
+ if (runAnimTrack ~= nil) then
+ runAnimTrack:Stop(transitionTime)
+ runAnimTrack:Destroy()
+ if userNoUpdateOnLoop == true then
+ runAnimTrack = nil
+ end
+ end
+
+ currentAnimSpeed = 1.0
+
+ -- load it to the humanoid; get AnimationTrack
+ currentAnimTrack = humanoid:LoadAnimation(anim)
+ currentAnimTrack.Priority = Enum.AnimationPriority.Core
+
+ -- play the animation
+ currentAnimTrack:Play(transitionTime)
+ currentAnim = animName
+ currentAnimInstance = anim
+
+ -- set up keyframe name triggers
+ if (currentAnimKeyframeHandler ~= nil) then
+ currentAnimKeyframeHandler:disconnect()
+ end
+ currentAnimKeyframeHandler = currentAnimTrack.KeyframeReached:connect(keyFrameReachedFunc)
+
+ -- check to see if we need to blend a walk/run animation
+ if animName == "walk" then
+ local runAnimName = "run"
+ local runIdx = rollAnimation(runAnimName)
+
+ runAnimTrack = humanoid:LoadAnimation(animTable[runAnimName][runIdx].anim)
+ runAnimTrack.Priority = Enum.AnimationPriority.Core
+ runAnimTrack:Play(transitionTime)
+
+ if (runAnimKeyframeHandler ~= nil) then
+ runAnimKeyframeHandler:disconnect()
+ end
+ runAnimKeyframeHandler = runAnimTrack.KeyframeReached:connect(keyFrameReachedFunc)
+ end
+ end
+ end
+
+ function playAnimation(animName, transitionTime, humanoid)
+ local idx = rollAnimation(animName)
+ local anim = animTable[animName][idx].anim
+
+ switchToAnim(anim, animName, transitionTime, humanoid)
+ currentlyPlayingEmote = false
+ end
+
+ function playEmote(emoteAnim, transitionTime, humanoid)
+ switchToAnim(emoteAnim, emoteAnim.Name, transitionTime, humanoid)
+ currentlyPlayingEmote = true
+ end
+
+ -------------------------------------------------------------------------------------------
+ -------------------------------------------------------------------------------------------
+
+ local toolAnimName = ""
+ local toolAnimTrack = nil
+ local toolAnimInstance = nil
+ local currentToolAnimKeyframeHandler = nil
+
+ function toolKeyFrameReachedFunc(frameName)
+ if (frameName == "End") then
+ playToolAnimation(toolAnimName, 0.0, Humanoid)
+ end
+ end
+
+
+ function playToolAnimation(animName, transitionTime, humanoid, priority)
+ local idx = rollAnimation(animName)
+ local anim = animTable[animName][idx].anim
+
+ if (toolAnimInstance ~= anim) then
+
+ if (toolAnimTrack ~= nil) then
+ toolAnimTrack:Stop()
+ toolAnimTrack:Destroy()
+ transitionTime = 0
+ end
+
+ -- load it to the humanoid; get AnimationTrack
+ toolAnimTrack = humanoid:LoadAnimation(anim)
+ if priority then
+ toolAnimTrack.Priority = priority
+ end
+
+ -- play the animation
+ toolAnimTrack:Play(transitionTime)
+ toolAnimName = animName
+ toolAnimInstance = anim
+
+ currentToolAnimKeyframeHandler = toolAnimTrack.KeyframeReached:connect(toolKeyFrameReachedFunc)
+ end
+ end
+
+ function stopToolAnimations()
+ local oldAnim = toolAnimName
+
+ if (currentToolAnimKeyframeHandler ~= nil) then
+ currentToolAnimKeyframeHandler:disconnect()
+ end
+
+ toolAnimName = ""
+ toolAnimInstance = nil
+ if (toolAnimTrack ~= nil) then
+ toolAnimTrack:Stop()
+ toolAnimTrack:Destroy()
+ toolAnimTrack = nil
+ end
+
+ return oldAnim
+ end
+
+ -------------------------------------------------------------------------------------------
+ -------------------------------------------------------------------------------------------
+ -- STATE CHANGE HANDLERS
+
+ function onRunning(speed)
+ if speed > 0.75 then
+ local scale = 16.0
+ playAnimation("walk", 0.2, Humanoid)
+ setAnimationSpeed(speed / scale)
+ pose = "Running"
+ else
+ if emoteNames[currentAnim] == nil and not currentlyPlayingEmote then
+ playAnimation("idle", 0.2, Humanoid)
+ pose = "Standing"
+ end
+ end
+ end
+
+ function onDied()
+ pose = "Dead"
+ end
+
+ function onJumping()
+ playAnimation("jump", 0.1, Humanoid)
+ jumpAnimTime = jumpAnimDuration
+ pose = "Jumping"
+ end
+
+ function onClimbing(speed)
+ local scale = 5.0
+ playAnimation("climb", 0.1, Humanoid)
+ setAnimationSpeed(speed / scale)
+ pose = "Climbing"
+ end
+
+ function onGettingUp()
+ pose = "GettingUp"
+ end
+
+ function onFreeFall()
+ if (jumpAnimTime <= 0) then
+ playAnimation("fall", fallTransitionTime, Humanoid)
+ end
+ pose = "FreeFall"
+ end
+
+ function onFallingDown()
+ pose = "FallingDown"
+ end
+
+ function onSeated()
+ pose = "Seated"
+ end
+
+ function onPlatformStanding()
+ pose = "PlatformStanding"
+ end
+
+ -------------------------------------------------------------------------------------------
+ -------------------------------------------------------------------------------------------
+
+ function onSwimming(speed)
+ if speed > 1.00 then
+ local scale = 10.0
+ playAnimation("swim", 0.4, Humanoid)
+ setAnimationSpeed(speed / scale)
+ pose = "Swimming"
+ else
+ playAnimation("swimidle", 0.4, Humanoid)
+ pose = "Standing"
+ end
+ end
+
+ function animateTool()
+ if (toolAnim == "None") then
+ playToolAnimation("toolnone", toolTransitionTime, Humanoid, Enum.AnimationPriority.Idle)
+ return
+ end
+
+ if (toolAnim == "Slash") then
+ playToolAnimation("toolslash", 0, Humanoid, Enum.AnimationPriority.Action)
+ return
+ end
+
+ if (toolAnim == "Lunge") then
+ playToolAnimation("toollunge", 0, Humanoid, Enum.AnimationPriority.Action)
+ return
+ end
+ end
+
+ function getToolAnim(tool)
+ for _, c in ipairs(tool:GetChildren()) do
+ if c.Name == "toolanim" and c.className == "StringValue" then
+ return c
+ end
+ end
+ return nil
+ end
+
+ local lastTick = 0
+
+ function stepAnimate(currentTime)
+ local amplitude = 1
+ local frequency = 1
+ local deltaTime = currentTime - lastTick
+ lastTick = currentTime
+
+ local climbFudge = 0
+ local setAngles = false
+
+ if (jumpAnimTime > 0) then
+ jumpAnimTime = jumpAnimTime - deltaTime
+ end
+
+ if (pose == "FreeFall" and jumpAnimTime <= 0) then
+ playAnimation("fall", fallTransitionTime, Humanoid)
+ elseif (pose == "Seated") then
+ playAnimation("sit", 0.5, Humanoid)
+ return
+ elseif (pose == "Running") then
+ playAnimation("walk", 0.2, Humanoid)
+ elseif (pose == "Dead" or pose == "GettingUp" or pose == "FallingDown" or pose == "Seated" or pose == "PlatformStanding") then
+ stopAllAnimations()
+ amplitude = 0.1
+ frequency = 1
+ setAngles = true
+ end
+
+ -- Tool Animation handling
+ local tool = Character:FindFirstChildOfClass("Tool")
+ if tool and tool:FindFirstChild("Handle") then
+ local animStringValueObject = getToolAnim(tool)
+
+ if animStringValueObject then
+ toolAnim = animStringValueObject.Value
+ -- message recieved, delete StringValue
+ animStringValueObject.Parent = nil
+ toolAnimTime = currentTime + .3
+ end
+
+ if currentTime > toolAnimTime then
+ toolAnimTime = 0
+ toolAnim = "None"
+ end
+
+ animateTool()
+ else
+ stopToolAnimations()
+ toolAnim = "None"
+ toolAnimInstance = nil
+ toolAnimTime = 0
+ end
+ end
+
+ -- connect events
+
+ local events = {}
+ local eventHum = Humanoid
+
+ local function onUnhook()
+ for i = 1, #events do
+ events[i]:Disconnect()
+ end
+ events = {}
+ end
+
+ local function onHook()
+ onUnhook()
+
+ pose = eventHum.Sit and "Seated" or "Standing"
+
+ events = {
+ eventHum.Died:connect(onDied),
+ eventHum.Running:connect(onRunning),
+ eventHum.Jumping:connect(onJumping),
+ eventHum.Climbing:connect(onClimbing),
+ eventHum.GettingUp:connect(onGettingUp),
+ eventHum.FreeFalling:connect(onFreeFall),
+ eventHum.FallingDown:connect(onFallingDown),
+ eventHum.Seated:connect(onSeated),
+ eventHum.PlatformStanding:connect(onPlatformStanding),
+ eventHum.Swimming:connect(onSwimming)
+ }
+ end
+
+
+ onHook()
+
+ -- setup emote chat hook
+ game:GetService("Players").LocalPlayer.Chatted:connect(function(msg)
+ local emote = ""
+ if (string.sub(msg, 1, 3) == "/e ") then
+ emote = string.sub(msg, 4)
+ elseif (string.sub(msg, 1, 7) == "/emote ") then
+ emote = string.sub(msg, 8)
+ end
+
+ if (pose == "Standing" and emoteNames[emote] ~= nil) then
+ playAnimation(emote, EMOTE_TRANSITION_TIME, Humanoid)
+ end
+ end)
+
+ --[[ emote bindable hook
+ if FFlagAnimateScriptEmoteHook then
+ script:WaitForChild("PlayEmote").OnInvoke = function(emote)
+ -- Only play emotes when idling
+ if pose ~= "Standing" then
+ return
+ end
+ if emoteNames[emote] ~= nil then
+ -- Default emotes
+ playAnimation(emote, EMOTE_TRANSITION_TIME, Humanoid)
+ return true
+ elseif typeof(emote) == "Instance" and emote:IsA("Animation") then
+ -- Non-default emotes
+ playEmote(emote, EMOTE_TRANSITION_TIME, Humanoid)
+ return true
+ end
+ -- Return false to indicate that the emote could not be played
+ return false
+ end
+ end
+ ]]
+ -- initialize to idle
+ playAnimation("idle", 0.1, Humanoid)
+ pose = "Standing"
+ -- loop to handle timed state transitions and tool animations
+ spawn(function()
+ while Character.Parent ~= nil do
+ local _, currentGameTime = wait(0.1)
+ stepAnimate(currentGameTime)
+ end
+ end)
+ return {
+ onRunning = onRunning,
+ onDied = onDied,
+ onJumping = onJumping,
+ onClimbing = onClimbing,
+ onGettingUp = onGettingUp,
+ onFreeFall = onFreeFall,
+ onFallingDown = onFallingDown,
+ onSeated = onSeated,
+ onPlatformStanding = onPlatformStanding,
+ onHook = onHook,
+ onUnhook = onUnhook
+ }
+ end
+ return r15()
+end
+while true do
+ wait(.1)
+ if plr.Character ~= nil then
+ char = plr.Character
+ break
+ end
+end
+function _Controller()
+ local humanoid = char:WaitForChild("Humanoid")
+ local animFuncs = {}
+ if (humanoid.RigType == Enum.HumanoidRigType.R6) then
+ animFuncs = _R6()
+ else
+ animFuncs = _R15()
+ end
+ print("Animation succes")
+ return animFuncs
+end
+function _AnimationHandler()
+local AnimationHandler = {}
+AnimationHandler.__index = AnimationHandler
+
+function AnimationHandler.new(humanoid, animate)
+ local self = setmetatable({}, AnimationHandler)
+
+ self._AnimFuncs = _Controller()
+ self.Humanoid = humanoid
+
+ return self
+end
+
+function AnimationHandler:EnableDefault(bool)
+ if (bool) then
+ self._AnimFuncs.onHook()
+ else
+ self._AnimFuncs.onUnhook()
+ end
+end
+
+function AnimationHandler:Run(name, ...)
+ self._AnimFuncs[name](...)
+end
+
+return AnimationHandler
+end
+
+function _GravityController()
+
+local ZERO = Vector3.new(0, 0, 0)
+local UNIT_X = Vector3.new(1, 0, 0)
+local UNIT_Y = Vector3.new(0, 1, 0)
+local UNIT_Z = Vector3.new(0, 0, 1)
+local VEC_XY = Vector3.new(1, 0, 1)
+
+local IDENTITYCF = CFrame.new()
+
+local JUMPMODIFIER = 1.2
+local TRANSITION = 0.15
+local WALKF = 200 / 3
+
+local UIS = game:GetService("UserInputService")
+local RUNSERVICE = game:GetService("RunService")
+
+local InitObjects = _InitObjects()
+local AnimationHandler = _AnimationHandler()
+local StateTracker = _StateTracker()
+
+-- Class
+
+local GravityController = {}
+GravityController.__index = GravityController
+
+-- Private Functions
+
+local function getRotationBetween(u, v, axis)
+ local dot, uxv = u:Dot(v), u:Cross(v)
+ if (dot < -0.99999) then return CFrame.fromAxisAngle(axis, math.pi) end
+ return CFrame.new(0, 0, 0, uxv.x, uxv.y, uxv.z, 1 + dot)
+end
+
+local function lookAt(pos, forward, up)
+ local r = forward:Cross(up)
+ local u = r:Cross(forward)
+ return CFrame.fromMatrix(pos, r.Unit, u.Unit)
+end
+
+local function getMass(array)
+ local mass = 0
+ for _, part in next, array do
+ if (part:IsA("BasePart")) then
+ mass = mass + part:GetMass()
+ end
+ end
+ return mass
+end
+
+-- Public Constructor
+local ExecutedPlayerModule = _PlayerModule()
+local ExecutedSounds = _sounds()
+function GravityController.new(player)
+ local self = setmetatable({}, GravityController)
+
+ --[[ Camera
+ local loaded = player.PlayerScripts:WaitForChild("PlayerScriptsLoader"):WaitForChild("Loaded")
+ if (not loaded.Value) then
+ --loaded.Changed:Wait()
+ end
+ ]]
+ local playerModule = ExecutedPlayerModule
+ self.Controls = playerModule:GetControls()
+ self.Camera = playerModule:GetCameras()
+
+ -- Player and character
+ self.Player = player
+ self.Character = player.Character
+ self.Humanoid = player.Character:WaitForChild("Humanoid")
+ self.HRP = player.Character:WaitForChild("HumanoidRootPart")
+
+ -- Animation
+ self.AnimationHandler = AnimationHandler.new(self.Humanoid, self.Character:WaitForChild("Animate"))
+ self.AnimationHandler:EnableDefault(false)
+ local ssss = game:GetService("Players").LocalPlayer.PlayerScripts:FindFirstChild("SetState") or Instance.new("BindableEvent",game:GetService("Players").LocalPlayer.PlayerScripts)
+ local soundState = ExecutedSounds
+ ssss.Name = "SetState"
+
+ self.StateTracker = StateTracker.new(self.Humanoid, soundState)
+ self.StateTracker.Changed:Connect(function(name, speed)
+ self.AnimationHandler:Run(name, speed)
+ end)
+
+ -- Collider and forces
+ local collider, gyro, vForce, floor = InitObjects(self)
+
+ floor.Touched:Connect(function() end)
+ collider.Touched:Connect(function() end)
+
+ self.Collider = collider
+ self.VForce = vForce
+ self.Gyro = gyro
+ self.Floor = floor
+
+ -- Attachment to parts
+ self.LastPart = workspace.Terrain
+ self.LastPartCFrame = IDENTITYCF
+
+ -- Gravity properties
+ self.GravityUp = UNIT_Y
+ self.Ignores = {self.Character}
+
+ function self.Camera.GetUpVector(this, oldUpVector)
+ return self.GravityUp
+ end
+
+ -- Events etc
+ self.Humanoid.PlatformStand = true
+
+ self.CharacterMass = getMass(self.Character:GetDescendants())
+ self.Character.AncestryChanged:Connect(function() self.CharacterMass = getMass(self.Character:GetDescendants()) end)
+
+ self.JumpCon = RUNSERVICE.RenderStepped:Connect(function(dt)
+ if (self.Controls:IsJumping()) then
+ self:OnJumpRequest()
+ end
+ end)
+
+ self.DeathCon = self.Humanoid.Died:Connect(function() self:Destroy() end)
+ self.SeatCon = self.Humanoid.Seated:Connect(function(active) if (active) then self:Destroy() end end)
+ self.HeartCon = RUNSERVICE.Heartbeat:Connect(function(dt) self:OnHeartbeatStep(dt) end)
+ RUNSERVICE:BindToRenderStep("GravityStep", Enum.RenderPriority.Input.Value + 1, function(dt) self:OnGravityStep(dt) end)
+
+
+ return self
+end
+
+-- Public Methods
+
+function GravityController:Destroy()
+ self.JumpCon:Disconnect()
+ self.DeathCon:Disconnect()
+ self.SeatCon:Disconnect()
+ self.HeartCon:Disconnect()
+
+ RUNSERVICE:UnbindFromRenderStep("GravityStep")
+
+ self.Collider:Destroy()
+ self.VForce:Destroy()
+ self.Gyro:Destroy()
+ self.StateTracker:Destroy()
+
+ self.Humanoid.PlatformStand = false
+ self.AnimationHandler:EnableDefault(true)
+
+ self.GravityUp = UNIT_Y
+end
+
+function GravityController:GetGravityUp(oldGravity)
+ return oldGravity
+end
+
+function GravityController:IsGrounded(isJumpCheck)
+ if (not isJumpCheck) then
+ local parts = self.Floor:GetTouchingParts()
+ for _, part in next, parts do
+ if (not part:IsDescendantOf(self.Character)) then
+ return true
+ end
+ end
+ else
+ if (self.StateTracker.Jumped) then
+ return false
+ end
+
+ -- 1. check we are touching something with the collider
+ local valid = {}
+ local parts = self.Collider:GetTouchingParts()
+ for _, part in next, parts do
+ if (not part:IsDescendantOf(self.Character)) then
+ table.insert(valid, part)
+ end
+ end
+
+ if (#valid > 0) then
+ -- 2. do a decently long downwards raycast
+ local max = math.cos(self.Humanoid.MaxSlopeAngle)
+ local ray = Ray.new(self.Collider.Position, -10 * self.GravityUp)
+ local hit, pos, normal = workspace:FindPartOnRayWithWhitelist(ray, valid, true)
+
+ -- 3. use slope to decide on jump
+ if (hit and max <= self.GravityUp:Dot(normal)) then
+ return true
+ end
+ end
+ end
+ return false
+end
+
+function GravityController:OnJumpRequest()
+ if (not self.StateTracker.Jumped and self:IsGrounded(true)) then
+ local hrpVel = self.HRP.Velocity
+ self.HRP.Velocity = hrpVel + self.GravityUp*self.Humanoid.JumpPower*JUMPMODIFIER
+ self.StateTracker:RequestedJump()
+ end
+end
+
+function GravityController:GetMoveVector()
+ return self.Controls:GetMoveVector()
+end
+
+function GravityController:OnHeartbeatStep(dt)
+ local ray = Ray.new(self.Collider.Position, -1.1*self.GravityUp)
+ local hit, pos, normal = workspace:FindPartOnRayWithIgnoreList(ray, self.Ignores)
+ local lastPart = self.LastPart
+
+ if (hit and lastPart and lastPart == hit) then
+ local offset = self.LastPartCFrame:ToObjectSpace(self.HRP.CFrame)
+ self.HRP.CFrame = hit.CFrame:ToWorldSpace(offset)
+ end
+
+ self.LastPart = hit
+ self.LastPartCFrame = hit and hit.CFrame
+end
+
+function GravityController:OnGravityStep(dt)
+ -- update gravity up vector
+ local oldGravity = self.GravityUp
+ local newGravity = self:GetGravityUp(oldGravity)
+
+ local rotation = getRotationBetween(oldGravity, newGravity, workspace.CurrentCamera.CFrame.RightVector)
+ rotation = IDENTITYCF:Lerp(rotation, TRANSITION)
+
+ self.GravityUp = rotation * oldGravity
+
+ -- get world move vector
+ local camCF = workspace.CurrentCamera.CFrame
+ local fDot = camCF.LookVector:Dot(newGravity)
+ local cForward = math.abs(fDot) > 0.5 and -math.sign(fDot)*camCF.UpVector or camCF.LookVector
+
+ local left = cForward:Cross(-newGravity).Unit
+ local forward = -left:Cross(newGravity).Unit
+
+ local move = self:GetMoveVector()
+ local worldMove = forward*move.z - left*move.x
+ worldMove = worldMove:Dot(worldMove) > 1 and worldMove.Unit or worldMove
+
+ local isInputMoving = worldMove:Dot(worldMove) > 0
+
+ -- get the desired character cframe
+ local hrpCFLook = self.HRP.CFrame.LookVector
+ local charF = hrpCFLook:Dot(forward)*forward + hrpCFLook:Dot(left)*left
+ local charR = charF:Cross(newGravity).Unit
+ local newCharCF = CFrame.fromMatrix(ZERO, charR, newGravity, -charF)
+
+ local newCharRotation = IDENTITYCF
+ if (isInputMoving) then
+ newCharRotation = IDENTITYCF:Lerp(getRotationBetween(charF, worldMove, newGravity), 0.7)
+ end
+
+ -- calculate forces
+ local g = workspace.Gravity
+ local gForce = g * self.CharacterMass * (UNIT_Y - newGravity)
+
+ local cVelocity = self.HRP.Velocity
+ local tVelocity = self.Humanoid.WalkSpeed * worldMove
+ local gVelocity = cVelocity:Dot(newGravity)*newGravity
+ local hVelocity = cVelocity - gVelocity
+
+ if (hVelocity:Dot(hVelocity) < 1) then
+ hVelocity = ZERO
+ end
+
+ local dVelocity = tVelocity - hVelocity
+ local walkForceM = math.min(10000, WALKF * self.CharacterMass * dVelocity.Magnitude / (dt*60))
+ local walkForce = walkForceM > 0 and dVelocity.Unit*walkForceM or ZERO
+
+ -- mouse lock
+ local charRotation = newCharRotation * newCharCF
+
+ if (self.Camera:IsCamRelative()) then
+ local lv = workspace.CurrentCamera.CFrame.LookVector
+ local hlv = lv - charRotation.UpVector:Dot(lv)*charRotation.UpVector
+ charRotation = lookAt(ZERO, hlv, charRotation.UpVector)
+ end
+
+ -- get state
+ self.StateTracker:OnStep(self.GravityUp, self:IsGrounded(), isInputMoving)
+
+ -- update values
+ self.VForce.Force = walkForce + gForce
+ self.Gyro.CFrame = charRotation
+end
+return GravityController
+end
+function _Draw3D()
+ local module = {}
+
+ -- Style Guide
+
+ module.StyleGuide = {
+ Point = {
+ Thickness = 0.5;
+ Color = Color3.new(0, 1, 0);
+ },
+
+ Line = {
+ Thickness = 0.1;
+ Color = Color3.new(1, 1, 0);
+ },
+
+ Ray = {
+ Thickness = 0.1;
+ Color = Color3.new(1, 0, 1);
+ },
+
+ Triangle = {
+ Thickness = 0.05;
+ };
+
+ CFrame = {
+ Thickness = 0.1;
+ RightColor3 = Color3.new(1, 0, 0);
+ UpColor3 = Color3.new(0, 1, 0);
+ BackColor3 = Color3.new(0, 0, 1);
+ PartProperties = {
+ Material = Enum.Material.SmoothPlastic;
+ };
+ }
+ }
+
+ -- CONSTANTS
+
+ local WEDGE = Instance.new("WedgePart")
+ WEDGE.Material = Enum.Material.SmoothPlastic
+ WEDGE.Anchored = true
+ WEDGE.CanCollide = false
+
+ local PART = Instance.new("Part")
+ PART.Size = Vector3.new(0.1, 0.1, 0.1)
+ PART.Anchored = true
+ PART.CanCollide = false
+ PART.TopSurface = Enum.SurfaceType.Smooth
+ PART.BottomSurface = Enum.SurfaceType.Smooth
+ PART.Material = Enum.Material.SmoothPlastic
+
+ -- Functions
+
+ local function draw(properties, style)
+ local part = PART:Clone()
+ for k, v in next, properties do
+ part[k] = v
+ end
+ if (style) then
+ for k, v in next, style do
+ if (k ~= "Thickness") then
+ part[k] = v
+ end
+ end
+ end
+ return part
+ end
+
+ function module.Draw(parent, properties)
+ properties.Parent = parent
+ return draw(properties, nil)
+ end
+
+ function module.Point(parent, cf_v3)
+ local thickness = module.StyleGuide.Point.Thickness
+ return draw({
+ Size = Vector3.new(thickness, thickness, thickness);
+ CFrame = (typeof(cf_v3) == "CFrame" and cf_v3 or CFrame.new(cf_v3));
+ Parent = parent;
+ }, module.StyleGuide.Point)
+ end
+
+ function module.Line(parent, a, b)
+ local thickness = module.StyleGuide.Line.Thickness
+ return draw({
+ CFrame = CFrame.new((a + b)/2, b);
+ Size = Vector3.new(thickness, thickness, (b - a).Magnitude);
+ Parent = parent;
+ }, module.StyleGuide.Line)
+ end
+
+ function module.Ray(parent, origin, direction)
+ local thickness = module.StyleGuide.Ray.Thickness
+ return draw({
+ CFrame = CFrame.new(origin + direction/2, origin + direction);
+ Size = Vector3.new(thickness, thickness, direction.Magnitude);
+ Parent = parent;
+ }, module.StyleGuide.Ray)
+ end
+
+ function module.Triangle(parent, a, b, c)
+ local ab, ac, bc = b - a, c - a, c - b
+ local abd, acd, bcd = ab:Dot(ab), ac:Dot(ac), bc:Dot(bc)
+
+ if (abd > acd and abd > bcd) then
+ c, a = a, c
+ elseif (acd > bcd and acd > abd) then
+ a, b = b, a
+ end
+
+ ab, ac, bc = b - a, c - a, c - b
+
+ local right = ac:Cross(ab).Unit
+ local up = bc:Cross(right).Unit
+ local back = bc.Unit
+
+ local height = math.abs(ab:Dot(up))
+ local width1 = math.abs(ab:Dot(back))
+ local width2 = math.abs(ac:Dot(back))
+
+ local thickness = module.StyleGuide.Triangle.Thickness
+
+ local w1 = WEDGE:Clone()
+ w1.Size = Vector3.new(thickness, height, width1)
+ w1.CFrame = CFrame.fromMatrix((a + b)/2, right, up, back)
+ w1.Parent = parent
+
+ local w2 = WEDGE:Clone()
+ w2.Size = Vector3.new(thickness, height, width2)
+ w2.CFrame = CFrame.fromMatrix((a + c)/2, -right, up, -back)
+ w2.Parent = parent
+
+ for k, v in next, module.StyleGuide.Triangle do
+ if (k ~= "Thickness") then
+ w1[k] = v
+ w2[k] = v
+ end
+ end
+
+ return w1, w2
+ end
+
+ function module.CFrame(parent, cf)
+ local origin = cf.Position
+ local r = cf.RightVector
+ local u = cf.UpVector
+ local b = -cf.LookVector
+
+ local thickness = module.StyleGuide.CFrame.Thickness
+
+ local right = draw({
+ CFrame = CFrame.new(origin + r/2, origin + r);
+ Size = Vector3.new(thickness, thickness, r.Magnitude);
+ Color = module.StyleGuide.CFrame.RightColor3;
+ Parent = parent;
+ }, module.StyleGuide.CFrame.PartProperties)
+
+ local up = draw({
+ CFrame = CFrame.new(origin + u/2, origin + u);
+ Size = Vector3.new(thickness, thickness, r.Magnitude);
+ Color = module.StyleGuide.CFrame.UpColor3;
+ Parent = parent;
+ }, module.StyleGuide.CFrame.PartProperties)
+
+ local back = draw({
+ CFrame = CFrame.new(origin + b/2, origin + b);
+ Size = Vector3.new(thickness, thickness, u.Magnitude);
+ Color = module.StyleGuide.CFrame.BackColor3;
+ Parent = parent;
+ }, module.StyleGuide.CFrame.PartProperties)
+
+ return right, up, back
+ end
+
+ -- Return
+
+ return module
+end
+function _Draw2D()
+ local module = {}
+
+ -- Style Guide
+
+ module.StyleGuide = {
+ Point = {
+ BorderSizePixel = 0;
+ Size = UDim2.new(0, 4, 0, 4);
+ BorderColor3 = Color3.new(0, 0, 0);
+ BackgroundColor3 = Color3.new(0, 1, 0);
+ },
+
+ Line = {
+ Thickness = 1;
+ BorderSizePixel = 0;
+ BorderColor3 = Color3.new(0, 0, 0);
+ BackgroundColor3 = Color3.new(0, 1, 0);
+ },
+
+ Ray = {
+ Thickness = 1;
+ BorderSizePixel = 0;
+ BorderColor3 = Color3.new(0, 0, 0);
+ BackgroundColor3 = Color3.new(0, 1, 0);
+ },
+
+ Triangle = {
+ ImageTransparency = 0;
+ ImageColor3 = Color3.new(0, 1, 0);
+ }
+ }
+
+ -- CONSTANTS
+
+ local HALF = Vector2.new(0.5, 0.5)
+
+ local RIGHT = "rbxassetid://2798177521"
+ local LEFT = "rbxassetid://2798177955"
+
+ local IMG = Instance.new("ImageLabel")
+ IMG.BackgroundTransparency = 1
+ IMG.AnchorPoint = HALF
+ IMG.BorderSizePixel = 0
+
+ local FRAME = Instance.new("Frame")
+ FRAME.BorderSizePixel = 0
+ FRAME.Size = UDim2.new(0, 0, 0, 0)
+ FRAME.BackgroundColor3 = Color3.new(1, 1, 1)
+
+ -- Functions
+
+ function draw(properties, style)
+ local frame = FRAME:Clone()
+ for k, v in next, properties do
+ frame[k] = v
+ end
+ if (style) then
+ for k, v in next, style do
+ if (k ~= "Thickness") then
+ frame[k] = v
+ end
+ end
+ end
+ return frame
+ end
+
+ function module.Draw(parent, properties)
+ properties.Parent = parent
+ return draw(properties, nil)
+ end
+
+ function module.Point(parent, v2)
+ return draw({
+ AnchorPoint = HALF;
+ Position = UDim2.new(0, v2.x, 0, v2.y);
+ Parent = parent;
+ }, module.StyleGuide.Point)
+ end
+
+ function module.Line(parent, a, b)
+ local v = (b - a)
+ local m = (a + b)/2
+
+ return draw({
+ AnchorPoint = HALF;
+ Position = UDim2.new(0, m.x, 0, m.y);
+ Size = UDim2.new(0, module.StyleGuide.Line.Thickness, 0, v.magnitude);
+ Rotation = math.deg(math.atan2(v.y, v.x)) - 90;
+ BackgroundColor3 = Color3.new(1, 1, 0);
+ Parent = parent;
+ }, module.StyleGuide.Line)
+ end
+
+ function module.Ray(parent, origin, direction)
+ local a, b = origin, origin + direction
+ local v = (b - a)
+ local m = (a + b)/2
+
+ return draw({
+ AnchorPoint = HALF;
+ Position = UDim2.new(0, m.x, 0, m.y);
+ Size = UDim2.new(0, module.StyleGuide.Ray.Thickness, 0, v.magnitude);
+ Rotation = math.deg(math.atan2(v.y, v.x)) - 90;
+ Parent = parent;
+ }, module.StyleGuide.Ray)
+ end
+
+ function module.Triangle(parent, a, b, c)
+ local ab, ac, bc = b - a, c - a, c - b
+ local abd, acd, bcd = ab:Dot(ab), ac:Dot(ac), bc:Dot(bc)
+
+ if (abd > acd and abd > bcd) then
+ c, a = a, c
+ elseif (acd > bcd and acd > abd) then
+ a, b = b, a
+ end
+
+ ab, ac, bc = b - a, c - a, c - b
+
+ local unit = bc.unit
+ local height = unit:Cross(ab)
+ local flip = (height >= 0)
+ local theta = math.deg(math.atan2(unit.y, unit.x)) + (flip and 0 or 180)
+
+ local m1 = (a + b)/2
+ local m2 = (a + c)/2
+
+ local w1 = IMG:Clone()
+ w1.Image = flip and RIGHT or LEFT
+ w1.AnchorPoint = HALF
+ w1.Size = UDim2.new(0, math.abs(unit:Dot(ab)), 0, height)
+ w1.Position = UDim2.new(0, m1.x, 0, m1.y)
+ w1.Rotation = theta
+ w1.Parent = parent
+
+ local w2 = IMG:Clone()
+ w2.Image = flip and LEFT or RIGHT
+ w2.AnchorPoint = HALF
+ w2.Size = UDim2.new(0, math.abs(unit:Dot(ac)), 0, height)
+ w2.Position = UDim2.new(0, m2.x, 0, m2.y)
+ w2.Rotation = theta
+ w2.Parent = parent
+
+ for k, v in next, module.StyleGuide.Triangle do
+ w1[k] = v
+ w2[k] = v
+ end
+
+ return w1, w2
+ end
+
+ -- Return
+
+ return module
+end
+function _DrawClass()
+ local Draw2DModule = _Draw2D()
+ local Draw3DModule = _Draw3D()
+
+ --
+
+ local DrawClass = {}
+ local DrawClassStorage = setmetatable({}, {__mode = "k"})
+ DrawClass.__index = DrawClass
+
+ function DrawClass.new(parent)
+ local self = setmetatable({}, DrawClass)
+
+ self.Parent = parent
+ DrawClassStorage[self] = {}
+
+ self.Draw3D = {}
+ for key, func in next, Draw3DModule do
+ self.Draw3D[key] = function(...)
+ local returns = {func(self.Parent, ...)}
+ for i = 1, #returns do
+ table.insert(DrawClassStorage[self], returns[i])
+ end
+ return unpack(returns)
+ end
+ end
+
+ self.Draw2D = {}
+ for key, func in next, Draw2DModule do
+ self.Draw2D[key] = function(...)
+ local returns = {func(self.Parent, ...)}
+ for i = 1, #returns do
+ table.insert(DrawClassStorage[self], returns[i])
+ end
+ return unpack(returns)
+ end
+ end
+
+ return self
+ end
+
+ --
+
+ function DrawClass:Clear()
+ local t = DrawClassStorage[self]
+ while (#t > 0) do
+ local part = table.remove(t)
+ if (part) then
+ part:Destroy()
+ end
+ end
+ DrawClassStorage[self] = {}
+ end
+
+ --
+
+ return DrawClass
+end
+
+
+--END TEST
+
+local PLAYERS = game:GetService("Players")
+
+local GravityController = _GravityController()
+local Controller = GravityController.new(PLAYERS.LocalPlayer)
+
+local DrawClass = _DrawClass()
+
+local PI2 = math.pi*2
+local ZERO = Vector3.new(0, 0, 0)
+
+local LOWER_RADIUS_OFFSET = 3
+local NUM_DOWN_RAYS = 24
+local ODD_DOWN_RAY_START_RADIUS = 3
+local EVEN_DOWN_RAY_START_RADIUS = 2
+local ODD_DOWN_RAY_END_RADIUS = 1.66666
+local EVEN_DOWN_RAY_END_RADIUS = 1
+
+local NUM_FEELER_RAYS = 9
+local FEELER_LENGTH = 2
+local FEELER_START_OFFSET = 2
+local FEELER_RADIUS = 3.5
+local FEELER_APEX_OFFSET = 1
+local FEELER_WEIGHTING = 8
+
+function GetGravityUp(self, oldGravityUp)
+ local ignoreList = {}
+ for i, player in next, PLAYERS:GetPlayers() do
+ ignoreList[i] = player.Character
+ end
+
+ -- get the normal
+
+ local hrpCF = self.HRP.CFrame
+ local isR15 = (self.Humanoid.RigType == Enum.HumanoidRigType.R15)
+
+ local origin = isR15 and hrpCF.p or hrpCF.p + 0.35*oldGravityUp
+ local radialVector = math.abs(hrpCF.LookVector:Dot(oldGravityUp)) < 0.999 and hrpCF.LookVector:Cross(oldGravityUp) or hrpCF.RightVector:Cross(oldGravityUp)
+
+ local centerRayLength = 25
+ local centerRay = Ray.new(origin, -centerRayLength * oldGravityUp)
+ local centerHit, centerHitPoint, centerHitNormal = workspace:FindPartOnRayWithIgnoreList(centerRay, ignoreList)
+
+ --[[disable
+ DrawClass:Clear()
+ DrawClass.Draw3D.Ray(centerRay.Origin, centerRay.Direction)
+ ]]
+ local downHitCount = 0
+ local totalHitCount = 0
+ local centerRayHitCount = 0
+ local evenRayHitCount = 0
+ local oddRayHitCount = 0
+
+ local mainDownNormal = ZERO
+ if (centerHit) then
+ mainDownNormal = centerHitNormal
+ centerRayHitCount = 0
+ end
+
+ local downRaySum = ZERO
+ for i = 1, NUM_DOWN_RAYS do
+ local dtheta = PI2 * ((i-1)/NUM_DOWN_RAYS)
+
+ local angleWeight = 0.25 + 0.75 * math.abs(math.cos(dtheta))
+ local isEvenRay = (i%2 == 0)
+ local startRadius = isEvenRay and EVEN_DOWN_RAY_START_RADIUS or ODD_DOWN_RAY_START_RADIUS
+ local endRadius = isEvenRay and EVEN_DOWN_RAY_END_RADIUS or ODD_DOWN_RAY_END_RADIUS
+ local downRayLength = centerRayLength
+
+ local offset = CFrame.fromAxisAngle(oldGravityUp, dtheta) * radialVector
+ local dir = (LOWER_RADIUS_OFFSET * -oldGravityUp + (endRadius - startRadius) * offset)
+ local ray = Ray.new(origin + startRadius * offset, downRayLength * dir.unit)
+ local hit, hitPoint, hitNormal = workspace:FindPartOnRayWithIgnoreList(ray, ignoreList)
+ --[[disable
+ DrawClass.Draw3D.Ray(ray.Origin, ray.Direction)
+ ]]
+ if (hit) then
+ downRaySum = downRaySum + angleWeight * hitNormal
+ downHitCount = downHitCount + 1
+ if isEvenRay then
+ evenRayHitCount = evenRayHitCount + 1
+ else
+ oddRayHitCount = oddRayHitCount + 1
+ end
+ end
+ end
+
+ local feelerHitCount = 0
+ local feelerNormalSum = ZERO
+
+ for i = 1, NUM_FEELER_RAYS do
+ local dtheta = 2 * math.pi * ((i-1)/NUM_FEELER_RAYS)
+ local angleWeight = 0.25 + 0.75 * math.abs(math.cos(dtheta))
+ local offset = CFrame.fromAxisAngle(oldGravityUp, dtheta) * radialVector
+ local dir = (FEELER_RADIUS * offset + LOWER_RADIUS_OFFSET * -oldGravityUp).unit
+ local feelerOrigin = origin - FEELER_APEX_OFFSET * -oldGravityUp + FEELER_START_OFFSET * dir
+ local ray = Ray.new(feelerOrigin, FEELER_LENGTH * dir)
+ local hit, hitPoint, hitNormal = workspace:FindPartOnRayWithIgnoreList(ray, ignoreList)
+ --[[disable
+ DrawClass.Draw3D.Ray(ray.Origin, ray.Direction)
+ ]]
+ if (hit) then
+ feelerNormalSum = feelerNormalSum + FEELER_WEIGHTING * angleWeight * hitNormal --* hitDistSqInv
+ feelerHitCount = feelerHitCount + 1
+ end
+ end
+
+ if (centerRayHitCount + downHitCount + feelerHitCount > 0) then
+ local normalSum = mainDownNormal + downRaySum + feelerNormalSum
+ if (normalSum ~= ZERO) then
+ return normalSum.unit
+ end
+ end
+
+ return oldGravityUp
+end
+
+Controller.GetGravityUp = GetGravityUp
+
+-- E is toggle
+game:GetService("ContextActionService"):BindAction("Toggle", function(action, state, input)
+ if not (state == Enum.UserInputState.Begin) then
+ return
+ end
+
+ if (Controller) then
+ Controller:Destroy()
+ Controller = nil
+ else
+ Controller = GravityController.new(PLAYERS.LocalPlayer)
+ Controller.GetGravityUp = GetGravityUp
+ end
+end, false, Enum.KeyCode.Z)
+print("end")
+ end,
+ Menu = {
+ Information = function(self)
+ UI.Banner({
+ Text = "This lets you walk"
+ })
+ end,
+ ["Check, if it works"] = function(self)
+ print("If you see this, the script probably works!")
+ end
+ }
+})
+
+Page.Button({
+ Text = "Skip the song",
+ Callback = function()
+ ab = Audio.TimeLength
+ Audio.TimePosition = ab
+ wait(0.2)
+ Audio.TimePosition = 0
+ end,
+ Menu = {
+ Information = function(self)
+ UI.Banner({
+ Text = "This script lets you skip the song that is playing right now"
+ })
+ end,
+ ["Check, if it works"] = function(self)
+ print("If you see this, the script probably works!")
+ end
+ }
+})
+
+Page.Button({
+ Text = "Loop skip other people",
+ Callback = function()
+ if skipping == false then
+ skipping = true
+ while wait() do
+ if skipping == true then
+ thetext = game:GetService("Workspace").DJBar.SurfaceGui.Container.TimeLeft.Text
+ newtext1 = thetext:split("DJ: ")
+ newtext2 = newtext1[2]
+ newtext3 = newtext2:split(" -")
+ currentdj = newtext3[1]
+ if currentdj == game.Players.LocalPlayer.Name then
+ else
+ ab = Audio.TimeLength
+ Audio.TimePosition = ab
+ wait(0.2)
+ Audio.TimePosition = 0
+ end
+ end
+ end
+ else
+ skipping = false
+ end
+ end,
+ Menu = {
+ Information = function(self)
+ UI.Banner({
+ Text = "This script loop skips people other than you"
+ })
+ end,
+ ["Check, if it works"] = function(self)
+ print("If you see this, the script probably works!")
+ end
+ }
+})
+
+Page.Button({
+ Text = "Stop loop skip other people",
+ Callback = function()
+ skipping = false
+ end,
+ Menu = {
+ Information = function(self)
+ UI.Banner({
+ Text = "This script stops loop skipping people other than you"
+ })
+ end,
+ ["Check, if it works"] = function(self)
+ print("If you see this, the script probably works!")
+ end
+ }
+})
+
+Page.Button({
+ Text = "Funnify the song",
+ Callback = function()
+ fun = true
+ while wait() do
+ if fun == true then
+ randomtime = math.random(0.2,0.3)
+ length = Audio.TimeLength
+ times = math.random(0.00,length)
+ Audio.TimePosition = (times)
+ Audio.Playing = false
+ wait(randomtime)
+ length = Audio.TimeLength
+ times = math.random(0.00,length)
+ Audio.TimePosition = (times)
+ Audio.Playing = true
+ wait(randomtime)
+ end
+ end
+ end,
+ Menu = {
+ Information = function(self)
+ UI.Banner({
+ Text = "This script makes the song funny (Basically glitch the song)"
+ })
+ end,
+ ["Check, if it works"] = function(self)
+ print("If you see this, the script probably works!")
+ end
+ }
+})
+
+Page.Button({
+ Text = "Unfunnify the song",
+ Callback = function()
+ fun = false
+ end,
+ Menu = {
+ Information = function(self)
+ UI.Banner({
+ Text = "This script makes the song unfunny"
+ })
+ end,
+ ["Check, if it works"] = function(self)
+ print("If you see this, the script probably works!")
+ end
+ }
+})
+
+Page.Button({
+ Text = "Turn the song off",
+ Callback = function()
+ Audio.Playing = false
+ end,
+ Menu = {
+ Information = function(self)
+ UI.Banner({
+ Text = "Simple, turn the song off"
+ })
+ end,
+ ["Check, if it works"] = function(self)
+ print("If you see this, the script probably works!")
+ end
+ }
+})
+
+Page.Button({
+ Text = "Turn the song on",
+ Callback = function()
+ Audio.Playing = true
+ end,
+ Menu = {
+ Information = function(self)
+ UI.Banner({
+ Text = "Simple, turn the song back on"
+ })
+ end,
+ ["Check, if it works"] = function(self)
+ print("If you see this, the script probably works!")
+ end
+ }
+})
+
+Page.Button({
+ Text = "Restart the song",
+ Callback = function()
+ Audio.TimePosition = 0
+ end,
+ Menu = {
+ Information = function(self)
+ UI.Banner({
+ Text = "Simple, restart the song"
+ })
+ end,
+ ["Check, if it works"] = function(self)
+ print("If you see this, the script probably works!")
+ end
+ }
+})
+
+Page.Button({
+ Text = "Random the song",
+ Callback = function()
+ local easd = math.random(0,Audio.TimeLength)
+ Audio.TimePosition = easd
+ end,
+ Menu = {
+ Information = function(self)
+ UI.Banner({
+ Text = "Set the time position of the song to random"
+ })
+ end,
+ ["Check, if it works"] = function(self)
+ print("If you see this, the script probably works!")
+ end
+ }
+})
\ No newline at end of file