local K=.016666666666667 RunService.Stepped:Connect(function(q,H)K=H end)local q=setmetatable({},BaseCamera)q.__index=q function q.new()local K=setmetatable(BaseCamera.new(),q)K:Reset()return K end function q.Reset(K)K.vehicleCameraCore=VehicleCameraCore.new(K:GetSubjectCFrame())K.pitchSpring=Spring.new(0,-math.rad(VehicleCameraConfig.pitchBaseAngle))K.yawSpring=Spring.new(0,YAW_DEFAULT)K.lastPanTick=0 local q=workspace.CurrentCamera local H=q and q.CameraSubject assert(q)assert(H)assert(H:IsA("\086\101\104\105\099\108\101\083\101\097\116"))local r=H:GetConnectedParts(true)local G,U=CameraUtils.getLooseBoundingSphere(r)U=math.max(U,EPSILON)K.assemblyRadius=U K.assemblyOffset=H.CFrame:Inverse()*G K:_StepInitialZoom()end function q._StepInitialZoom(K)K:SetCameraToSubjectDistance(math.max(ZoomController.GetZoomRadius(),K.assemblyRadius*VehicleCameraConfig.initialZoomRadiusMul))end function q._StepRotation(H,K,q)local r=H.yawSpring local G=H.pitchSpring local U=CameraInput.getRotation(true)local v=-U.X local M=-U.Y r.pos=sanitizeAngle(r.pos+v)G.pos=sanitizeAngle(math.clamp(G.pos+M,-PITCH_LIMIT,PITCH_LIMIT))if CameraInput.getRotationActivated()then H.lastPanTick=os.clock()end local c=-math.rad(VehicleCameraConfig.pitchBaseAngle)local P=math.rad(VehicleCameraConfig.pitchDeadzoneAngle)if os.clock()-H.lastPanTick>VehicleCameraConfig.autocorrectDelay then local K=mapClamp(q,VehicleCameraConfig.autocorrectMinCarSpeed,VehicleCameraConfig.autocorrectMaxCarSpeed,0,VehicleCameraConfig.autocorrectResponse)r.freq=K G.freq=K if r.freq<EPSILON then r.vel=0 end if G.freq<EPSILON then G.vel=0 end if math.abs(sanitizeAngle(c-G.pos))<=P then G.goal=G.pos else G.goal=c end else r.freq=0 r.vel=0 G.freq=0 G.vel=0 G.goal=c end return CFrame.fromEulerAnglesYXZ(G:step(K),r:step(K),0)end function q._GetThirdPersonLocalOffset(K)return K.assemblyOffset+Vector3.new(0,K.assemblyRadius*VehicleCameraConfig.verticalCenterOffset,0)end function q._GetFirstPersonLocalOffset(q,K)local H=localPlayer.Character if H and H.Parent then local q=H:FindFirstChild("\072\101\097\100")if q and q:IsA("\066\097\115\101\080\097\114\116")then return K:Inverse()*q.Position end end return q:_GetThirdPersonLocalOffset()end function q.Update(q)local H=workspace.CurrentCamera local r=H and H.CameraSubject local G=q.vehicleCameraCore assert(H)assert(r)assert(r:IsA("\086\101\104\105\099\108\101\083\101\097\116"))local U=K K=0 local v=q:GetSubjectCFrame()local M=q:GetSubjectVelocity()local c=q:GetSubjectRotVelocity()local P=math.abs(M:Dot(v.ZVector))local X=yawVelocity(c,v)local Y=pitchVelocity(c,v)local a=q:StepZoom()local k=q:_StepRotation(U,P)local C=mapClamp(a,ZOOM_MINIMUM,q.assemblyRadius,1,0)local n=q:_GetThirdPersonLocalOffset()local l=q:_GetFirstPersonLocalOffset(v)local Q=n:Lerp(l,C)G:setTransform(v)local E=G:step(U,Y,X,C)local d=(CFrame.new(v*Q)*E)*k local i=d*CFrame.new(0,0,a)return i,d end function q.ApplyVRTransform(K) end function q.EnterFirstPerson(K)K.inFirstPerson=true K:UpdateMouseBehavior()end function q.LeaveFirstPerson(K)K.inFirstPerson=false K:UpdateMouseBehavior()end return q
-- track physics solver time delta separately from the render loop to correctly synchronize time delta
local worldDt = 1 / 60
RunService.Stepped:Connect(function(_, _worldDt)
	worldDt = _worldDt
end)

local VehicleCamera = setmetatable({}, BaseCamera)
VehicleCamera.__index = VehicleCamera

function VehicleCamera.new()
	local self = setmetatable(BaseCamera.new(), VehicleCamera)
	self:Reset()
	return self
end

function VehicleCamera:Reset()
	self.vehicleCameraCore = VehicleCameraCore.new(self:GetSubjectCFrame())
	self.pitchSpring = Spring.new(0, -math.rad(VehicleCameraConfig.pitchBaseAngle))
	self.yawSpring = Spring.new(0, YAW_DEFAULT)
	self.lastPanTick = 0

	local camera = workspace.CurrentCamera
	local cameraSubject = camera and camera.CameraSubject

	assert(camera)
	assert(cameraSubject)
	assert(cameraSubject:IsA("VehicleSeat"))

	local assemblyParts = cameraSubject:GetConnectedParts(true) -- passing true to recursively get all assembly parts
	local assemblyPosition, assemblyRadius = CameraUtils.getLooseBoundingSphere(assemblyParts)

	assemblyRadius = math.max(assemblyRadius, EPSILON)

	self.assemblyRadius = assemblyRadius
	self.assemblyOffset = cameraSubject.CFrame:Inverse() * assemblyPosition -- seat-space offset of the assembly bounding sphere center

	self:_StepInitialZoom()
end

function VehicleCamera:_StepInitialZoom()
	self:SetCameraToSubjectDistance(
		math.max(ZoomController.GetZoomRadius(), self.assemblyRadius * VehicleCameraConfig.initialZoomRadiusMul)
	)
end

function VehicleCamera:_StepRotation(dt, vdotz)
	local yawSpring = self.yawSpring
	local pitchSpring = self.pitchSpring

	local rotationInput = CameraInput.getRotation(true)
	local dYaw = -rotationInput.X
	local dPitch = -rotationInput.Y

	yawSpring.pos = sanitizeAngle(yawSpring.pos + dYaw)
	pitchSpring.pos = sanitizeAngle(math.clamp(pitchSpring.pos + dPitch, -PITCH_LIMIT, PITCH_LIMIT))

	if CameraInput.getRotationActivated() then
		self.lastPanTick = os.clock()
	end

	local pitchBaseAngle = -math.rad(VehicleCameraConfig.pitchBaseAngle)
	local pitchDeadzoneAngle = math.rad(VehicleCameraConfig.pitchDeadzoneAngle)

	if os.clock() - self.lastPanTick > VehicleCameraConfig.autocorrectDelay then
		-- adjust autocorrect response based on forward velocity
		local autocorrectResponse = mapClamp(
			vdotz,
			VehicleCameraConfig.autocorrectMinCarSpeed,
			VehicleCameraConfig.autocorrectMaxCarSpeed,
			0,
			VehicleCameraConfig.autocorrectResponse
		)

		yawSpring.freq = autocorrectResponse
		pitchSpring.freq = autocorrectResponse

		-- zero out response under a threshold
		if yawSpring.freq < EPSILON then
			yawSpring.vel = 0
		end

		if pitchSpring.freq < EPSILON then
			pitchSpring.vel = 0
		end

		if math.abs(sanitizeAngle(pitchBaseAngle - pitchSpring.pos)) <= pitchDeadzoneAngle then
			-- do nothing within the deadzone
			pitchSpring.goal = pitchSpring.pos
		else
			pitchSpring.goal = pitchBaseAngle
		end
	else
		yawSpring.freq = 0
		yawSpring.vel = 0

		pitchSpring.freq = 0
		pitchSpring.vel = 0

		pitchSpring.goal = pitchBaseAngle
	end

	return CFrame.fromEulerAnglesYXZ(pitchSpring:step(dt), yawSpring:step(dt), 0)
end

function VehicleCamera:_GetThirdPersonLocalOffset()
	return self.assemblyOffset + Vector3.new(0, self.assemblyRadius * VehicleCameraConfig.verticalCenterOffset, 0)
end

function VehicleCamera:_GetFirstPersonLocalOffset(subjectCFrame)
	local character = localPlayer.Character

	if character and character.Parent then
		local head = character:FindFirstChild("Head")

		if head and head:IsA("BasePart") then
			return subjectCFrame:Inverse() * head.Position
		end
	end

	return self:_GetThirdPersonLocalOffset()
end

function VehicleCamera:Update()
	local camera = workspace.CurrentCamera
	local cameraSubject = camera and camera.CameraSubject
	local vehicleCameraCore = self.vehicleCameraCore

	assert(camera)
	assert(cameraSubject)
	assert(cameraSubject:IsA("VehicleSeat"))

	-- consume the physics solver time delta to account for mismatched physics/render cycles
	local dt = worldDt
	worldDt = 0

	-- get subject info
	local subjectCFrame = self:GetSubjectCFrame()
	local subjectVel = self:GetSubjectVelocity()
	local subjectRotVel = self:GetSubjectRotVelocity()

	-- measure the local-to-world-space forward velocity of the vehicle
	local vDotZ = math.abs(subjectVel:Dot(subjectCFrame.ZVector))
	local yawVel = yawVelocity(subjectRotVel, subjectCFrame)
	local pitchVel = pitchVelocity(subjectRotVel, subjectCFrame)

	-- step camera components forward
	local zoom = self:StepZoom()
	local objectRotation = self:_StepRotation(dt, vDotZ)

	-- mix third and first person offsets in local space
	local firstPerson = mapClamp(zoom, ZOOM_MINIMUM, self.assemblyRadius, 1, 0)

	local tpOffset = self:_GetThirdPersonLocalOffset()
	local fpOffset = self:_GetFirstPersonLocalOffset(subjectCFrame)
	local localOffset = tpOffset:Lerp(fpOffset, firstPerson)

	-- step core forward
	vehicleCameraCore:setTransform(subjectCFrame)
	local processedRotation = vehicleCameraCore:step(dt, pitchVel, yawVel, firstPerson)

	-- calculate final focus & cframe
	local focus = CFrame.new(subjectCFrame * localOffset) * processedRotation * objectRotation
	local cf = focus * CFrame.new(0, 0, zoom)

	return cf, focus
end

function VehicleCamera:ApplyVRTransform()
	-- no-op override; VR transform is not applied in vehicles
end

function VehicleCamera:EnterFirstPerson()
	self.inFirstPerson = true
	self:UpdateMouseBehavior()
end

function VehicleCamera:LeaveFirstPerson()
	self.inFirstPerson = false
	self:UpdateMouseBehavior()
end

return VehicleCamera
