local o=game:GetService("\080\108\097\121\101\114\115")local c=game.Workspace.CurrentCamera local C=math.min local z=math.tan local x=math.rad local y=math.huge local b=Ray.new local function V(o)return 1-(1-o.Transparency)*(1-o.LocalTransparencyModifier)end local function w(o,c)for c=#o,c+1,-1 do o[c]=nil end end local r,U,R do local function o()local o=x(c.FieldOfView)local C=c.ViewportSize local y=C.X/C.Y R=2*z(o/2)U=y*R end;(c:GetPropertyChangedSignal("\070\105\101\108\100\079\102\086\105\101\119")):Connect(o);(c:GetPropertyChangedSignal("\086\105\101\119\112\111\114\116\083\105\122\101")):Connect(o)o()r=c.NearPlaneZ;(c:GetPropertyChangedSignal("\078\101\097\114\080\108\097\110\101\090")):Connect(function()r=c.NearPlaneZ end)end local e={}do local c={}local function C()local o=1 e={}for c,C in pairs(c)do e[o]=C o=o+1 end end local function z(o)local function z(z)c[o]=z C()end local function x()c[o]=nil C()end o.CharacterAdded:Connect(z)o.CharacterRemoving:Connect(x)if o.Character then z(o.Character)end end local function x(o)c[o]=nil C()end o.PlayerAdded:Connect(z)o.PlayerRemoving:Connect(x)for o,c in ipairs(o:GetPlayers())do z(c)end C()end local k local H;(c:GetPropertyChangedSignal("\067\097\109\101\114\097\083\117\098\106\101\099\116")):Connect(function()local o=c.CameraSubject if o:IsA("\072\117\109\097\110\111\105\100")then H=o.RootPart elseif o:IsA("\066\097\115\101\080\097\114\116")then H=o else H=nil end end)local function Y(o)return V(o)<.25 and(o.CanCollide and(k~=(o:GetRootPart()or o)and not o: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 l=64 local function B(o,c)local C=#e repeat local z,x=workspace:FindPartOnRayWithIgnoreList(b(o,c),e,false,true)if z then if z.CanCollide then w(e,C)return x,true end e[#e+1]=z end until not z w(e,C)return o+c,false end local function u(o,c,C,z)debug.profilebegin("\113\117\101\114\121\080\111\105\110\116")local x=#e C=C+r local V=o+c*C local U=y local R=y local k=o local H=0 repeat local x,w=workspace:FindPartOnRayWithIgnoreList(b(k,V-k),e,false,true)H+=1 if x then local y=H>=l if Y(x)or y then local c={x}local r=workspace:FindPartOnRayWithWhitelist(b(V,w-V),c,true)local e=(w-o).Magnitude if r and not y then local o=false if z then o=workspace:FindPartOnRayWithWhitelist(b(z,V-z),c,true)or workspace:FindPartOnRayWithWhitelist(b(V,z-V),c,true)end if o then R=e elseif C<U then U=e end else R=e end end e[#e+1]=x k=w-c*.001 end until R<y or not x w(e,x)debug.profileend()return U-r,R-r end local function v(o,C)debug.profilebegin("\113\117\101\114\121\086\105\101\119\112\111\114\116")local z=o.p local x=o.rightVector local b=o.upVector local V=-o.lookVector local w=c.ViewportSize local e=y local k=y for o=0,1,1 do local y=x*((o-.5)*U)for x=0,1,1 do local U=b*((x-.5)*R)local H=z+r*(y+U)local Y=(c:ViewportPointToRay(w.x*o,w.y*x)).Origin local X,l=u(H,V,C,Y)if l<e then e=l end if X<k then k=X end end end debug.profileend()return k,e end local function s(o,c,z)debug.profilebegin("\116\101\115\116\080\114\111\109\111\116\105\111\110")local x=o.p local b=o.rightVector local V=o.upVector local w=-o.lookVector do debug.profilebegin("\101\120\116\114\097\112\111\108\097\116\101")local o=.0625 local y=1.25 local b=(B(x,z.posVelocity*y)-x).Magnitude local V=z.posVelocity.magnitude for o=0,C(y,z.rotVelocity.magnitude+b/V),o do local C=z.extrapolate(o)if u(C.p,-C.lookVector,c)>=c then return false end end debug.profileend()end do debug.profilebegin("\116\101\115\116\079\102\102\115\101\116\115")for o,C in ipairs(X)do local z=C local r=B(x,b*z.x+V*z.y)if u(r,((x+w*c)-r).Unit,c)==y then return false end end debug.profileend()end debug.profileend()return true end local function N(o,c,C)debug.profilebegin("\112\111\112\112\101\114")k=H and H:GetRootPart()or H local z=c local x,y=v(o,c)if y<z then z=y end if x<z and s(o,c,C)then z=x end k=nil debug.profileend()return z end return N
--------------------------------------------------------------------------------
-- 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
