local L=game:GetService("\080\108\097\121\101\114\115")local G=game.Workspace.CurrentCamera local Y=math.min local U=math.tan local Z=math.rad local i=math.huge local B=Ray.new local function p(L)return 1-(1-L.Transparency)*(1-L.LocalTransparencyModifier)end local function j(L,G)for G=#L,G+1,-1 do L[G]=nil end end local D,a,t do local function L()local L=Z(G.FieldOfView)local Y=G.ViewportSize local i=Y.X/Y.Y t=2*U(L/2)a=i*t end;(G:GetPropertyChangedSignal("\070\105\101\108\100\079\102\086\105\101\119")):Connect(L);(G:GetPropertyChangedSignal("\086\105\101\119\112\111\114\116\083\105\122\101")):Connect(L)L()D=G.NearPlaneZ;(G:GetPropertyChangedSignal("\078\101\097\114\080\108\097\110\101\090")):Connect(function()D=G.NearPlaneZ end)end local c={}do local G={}local function Y()local L=1 c={}for G,Y in pairs(G)do c[L]=Y L=L+1 end end local function U(L)local function U(U)G[L]=U Y()end local function Z()G[L]=nil Y()end L.CharacterAdded:Connect(U)L.CharacterRemoving:Connect(Z)if L.Character then U(L.Character)end end local function Z(L)G[L]=nil Y()end L.PlayerAdded:Connect(U)L.PlayerRemoving:Connect(Z)for L,G in ipairs(L:GetPlayers())do U(G)end Y()end local m local I;(G:GetPropertyChangedSignal("\067\097\109\101\114\097\083\117\098\106\101\099\116")):Connect(function()local L=G.CameraSubject if L:IsA("\072\117\109\097\110\111\105\100")then I=L.RootPart elseif L:IsA("\066\097\115\101\080\097\114\116")then I=L else I=nil end end)local function V(L)return p(L)<.25 and(L.CanCollide and(m~=(L:GetRootPart()or L)and not L:IsA("\084\114\117\115\115\080\097\114\116")))end local X={Vector2.new(.4,.0),Vector2.new(-0.4,.0);Vector2.new(.0,-0.4);Vector2.new(.0,.4),Vector2.new(.0,.2)}local o=64 local function P(L,G)local Y=#c repeat local U,Z=workspace:FindPartOnRayWithIgnoreList(B(L,G),c,false,true)if U then if U.CanCollide then j(c,Y)return Z,true end c[#c+1]=U end until not U j(c,Y)return L+G,false end local function l(L,G,Y,U)debug.profilebegin("\113\117\101\114\121\080\111\105\110\116")local Z=#c Y=Y+D local p=L+G*Y local a=i local t=i local m=L local I=0 repeat local Z,j=workspace:FindPartOnRayWithIgnoreList(B(m,p-m),c,false,true)I+=1 if Z then local i=I>=o if V(Z)or i then local G={Z}local D=workspace:FindPartOnRayWithWhitelist(B(p,j-p),G,true)local c=(j-L).Magnitude if D and not i then local L=false if U then L=workspace:FindPartOnRayWithWhitelist(B(U,p-U),G,true)or workspace:FindPartOnRayWithWhitelist(B(p,U-p),G,true)end if L then t=c elseif Y<a then a=c end else t=c end end c[#c+1]=Z m=j-G*.001 end until t<i or not Z j(c,Z)debug.profileend()return a-D,t-D end local function E(L,Y)debug.profilebegin("\113\117\101\114\121\086\105\101\119\112\111\114\116")local U=L.p local Z=L.rightVector local B=L.upVector local p=-L.lookVector local j=G.ViewportSize local c=i local m=i for L=0,1,1 do local i=Z*((L-.5)*a)for Z=0,1,1 do local a=B*((Z-.5)*t)local I=U+D*(i+a)local V=(G:ViewportPointToRay(j.x*L,j.y*Z)).Origin local X,o=l(I,p,Y,V)if o<c then c=o end if X<m then m=X end end end debug.profileend()return m,c end local function s(L,G,U)debug.profilebegin("\116\101\115\116\080\114\111\109\111\116\105\111\110")local Z=L.p local B=L.rightVector local p=L.upVector local j=-L.lookVector do debug.profilebegin("\101\120\116\114\097\112\111\108\097\116\101")local L=.0625 local i=1.25 local B=(P(Z,U.posVelocity*i)-Z).Magnitude local p=U.posVelocity.magnitude for L=0,Y(i,U.rotVelocity.magnitude+B/p),L do local Y=U.extrapolate(L)if l(Y.p,-Y.lookVector,G)>=G then return false end end debug.profileend()end do debug.profilebegin("\116\101\115\116\079\102\102\115\101\116\115")for L,Y in ipairs(X)do local U=Y local D=P(Z,B*U.x+p*U.y)if l(D,((Z+j*G)-D).Unit,G)==i then return false end end debug.profileend()end debug.profileend()return true end local function d(L,G,Y)debug.profilebegin("\112\111\112\112\101\114")m=I and I:GetRootPart()or I local U=G local Z,i=E(L,G)if i<U then U=i end if Z<U and s(L,G,Y)then U=Z end m=nil debug.profileend()return U end return d
--!nonstrict
--------------------------------------------------------------------------------
-- Popper.lua
-- Prevents your camera from clipping through walls.
--------------------------------------------------------------------------------

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),
}

-- Maximum number of rays that can be cast 
local QUERY_POINT_CAST_LIMIT = 64

--------------------------------------------------------------------------------
-- 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
	
	local numPierced = 0

	repeat
		local entryPart, entryPos = workspace:FindPartOnRayWithIgnoreList(ray(movingOrigin, target - movingOrigin), blacklist, false, true)
		numPierced += 1

		if entryPart then
			-- forces the current iteration into a hard limit to cap the number of raycasts
			local earlyAbort = numPierced >= QUERY_POINT_CAST_LIMIT
			
			if canOcclude(entryPart) or earlyAbort then
				local wl = {entryPart}
				local exitPart = workspace:FindPartOnRayWithWhitelist(ray(target, entryPos - target), wl, true)

				local lim = (entryPos - origin).Magnitude

				if exitPart and not earlyAbort 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
