You are on page 1of 219

--[[

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:C
onnect(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:C
onnect(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(f
unction()
self:UpdateMouseLockAvailability()
end)

-- Watch for changes to DevEnableMouseLock and update the feature


availability accordingly

PlayersService.LocalPlayer:GetPropertyChangedSignal("DevComputerMovementMode"):Conn
ect(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.De
fault)

PlayerScripts:RegisterComputerCameraMovementMode(Enum.ComputerCameraMovementMode.Fo
llow)
PlayerScripts:RegisterComputerCameraMovementMode(Enum.ComputerCameraMovementMode.Cl
assic)
if FFlagUserCameraToggle then

PlayerScripts:RegisterComputerCameraMovementMode(Enum.ComputerCameraMovementMode.Ca
meraToggle)
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.TouchCameraMovementMod
e)
else
devMode =
CameraUtils.ConvertCameraModeEnumToStandard(Players.LocalPlayer.DevComputerCameraMo
de)
userMode =
CameraUtils.ConvertCameraModeEnumToStandard(UserGameSettings.ComputerCameraMovement
Mode)
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.Came
raSubject)
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.CameraSub
ject)
end

if self.activeOcclusionModule then
self.activeOcclusionModule:OnCameraSubjectChanged(game.Workspace.CurrentCamera.Came
raSubject)
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.DevC
omputerCameraMovementMode.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(cameraMov
ementMode))
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(cameraMov
ementMode))

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(cameraMov
ementMode))
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.TouchCameraMovementMo
de )
else
return
CameraUtils.ConvertCameraModeEnumToStandard( player.DevTouchCameraMode )
end
else
-- Computer
if player.DevComputerCameraMode ==
Enum.DevComputerCameraMovementMode.UserChoice then
local computerMovementMode =
CameraUtils.ConvertCameraModeEnumToStandard(UserGameSettings.ComputerCameraMovement
Mode)
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(functi
on()
self:OnTouchMovementModeChange()
end)

UserGameSettings:GetPropertyChangedSignal("ComputerMovementMode"):Connect(function(
)
self:OnComputerMovementModeChange()
end)

Players.LocalPlayer:GetPropertyChangedSignal("DevComputerMovementMode"):Connect(fun
ction()
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")

You might also like