--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| 	Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]

--[[-------------------------------------------------------------------------
	downfall_meta:GetNextParticle()

---------------------------------------------------------------------------]]
local max,min,t_insert,abs,clamp = math.max,math.min,table.insert,math.abs,math.Clamp

-- Particle emitters
if CLIENT then
	_STORMFOX_PEM = _STORMFOX_PEM or {}
	local tab = {
		__index = function(self, b)
			if b == "3D" then
				if IsValid(self._PEM) then return self._PEM end
				self._PEM = ParticleEmitter(Vector(0,0,0),true)
				self._PEM:SetNoDraw(true)
				return self._PEM
			elseif b == "2D" then
				if IsValid(self._PEM2D) then return self._PEM2D end
				self._PEM2D = ParticleEmitter(Vector(0,0,0))
				self._PEM2D:SetNoDraw(true)
				return self._PEM2D
			end
		end
	}
	setmetatable(_STORMFOX_PEM, tab)
	timer.Create("StormFox2.ParticleFlush", 60 * 2, 0, function()
		if _STORMFOX_PEM._PEM2D then
			_STORMFOX_PEM._PEM2D:Finish()
			_STORMFOX_PEM._PEM2D = nil
		end
		if _STORMFOX_PEM._PEM then
			_STORMFOX_PEM._PEM:Finish()
			_STORMFOX_PEM._PEM = nil
		end
	end)
end

-- Downfall mask
local mask = bit.bor( CONTENTS_SOLID, CONTENTS_MOVEABLE, CONTENTS_MONSTER, CONTENTS_WINDOW, CONTENTS_DEBRIS, CONTENTS_HITBOX, CONTENTS_WATER, CONTENTS_SLIME )
local util_TraceHull,bit_band,Vector,IsValid = util.TraceHull,bit.band,Vector,IsValid

StormFox2.DownFall = {}
StormFox2.DownFall.Mask = mask

SF_DOWNFALL_HIT_NIL = -1
SF_DOWNFALL_HIT_GROUND = 0
SF_DOWNFALL_HIT_WATER = 1
SF_DOWNFALL_HIT_GLASS = 2
SF_DOWNFALL_HIT_CONCRETE = 3
SF_DOWNFALL_HIT_WOOD = 4
SF_DOWNFALL_HIT_METAL = 5

local con = GetConVar("sv_gravity")
-- Returns the gravity

---Returns the current gravity.
---@return number
---@shared
local function GLGravity()
	if con then
		return con:GetInt() / 600
	else -- Err
		return 1
	end
end

-- This will return the gravity on the server.
StormFox2.DownFall.GetGravity = GLGravity
--		game.GetTimeScale()

-- Traces
do
	local t = {
		start = Vector(0,0,0),
		endpos = Vector(0,0,0),
		maxs = Vector(1,1,4),
		mins = Vector(-1,-1,0),
		mask = mask
	}
	local function GetViewEntity()
		if SERVER then return end
		return LocalPlayer():GetViewEntity() or LocalPlayer()
	end
	-- MaterialScanner
	local c_t = {}
	-- Errors
		c_t["**displacement**"] = "default"
		c_t["**studio**"] = "default"
		c_t["default_silent"] = "default"
		c_t["floatingstandable"] = "default"
		c_t["item"] = "default"
		c_t["ladder"] = "default"
		c_t["no_decal"] = "default"
		c_t["player"] = "default"
		c_t["player_control_clip"] = "default"
	-- Concrete / Rock / Ground
		c_t["boulder"] = "concrete"
		c_t["concrete_block"] = "concrete"
		c_t["gravel"] = "concrete"
		c_t["rock"] = "concrete"
		c_t["brick"] = "concrete"
		c_t["baserock"] = "concrete"
		c_t["dirt"] = "concrete"
		c_t["grass"] = "concrete"
		c_t["gravel"] = "concrete"
		c_t["mud"] = "concrete"
		c_t["quicksand"] = "concrete"
		c_t["slipperyslime"] = "concrete"
		c_t["sand"] = "concrete"
		c_t["antlionsand"] = "concrete"
	-- Metal
		c_t["canister"] = "metal"
		c_t["chain"] = "metal"
		c_t["chainlink"] = "metal"
		c_t["paintcan"] = "metal"
		c_t["popcan"] = "metal"
		c_t["roller"] = "metal"
	-- Wood
		c_t["roller"] = "wood"
		c_t["roller"] = "metal"
		c_t["roller"] = "metal"
		c_t["roller"] = "metal"
		c_t["roller"] = "metal"

	-- Convert surfaceprops
	local function ConvertSurfaceProp( sp )
		sp = sp:lower()
		if c_t[sp] then 
			return c_t[sp]
		end
		-- Guess
		if string.find( sp, "window", 1, true) or string.find( sp, "glass", 1, true) then
			return "glass"
		end
		if string.find( sp, "wood",1,true) then
			return "wood"
		end
		if string.find( sp, "metal",1,true) then
			return "metal"
		end
		if string.find( sp, "concrete",1,true) then
			return "concrete"
		end
		if string.find( sp, "water",1,true) then
			return "water"
		end
		return "default"
	end
	local m_t = {}
	local function SurfacePropIDToHIT( id )
		if not id then return end
		if id < 0 then return SF_DOWNFALL_HIT_GROUND end
		if m_t[ id ] then return m_t[ id ] end
		-- ConvertSurfaceProp
		local name = util.GetSurfacePropName( id )
		name = ConvertSurfaceProp( name )
		if name == "default" then
			m_t[ id ] = SF_DOWNFALL_HIT_GROUND
		elseif name == "metal" then
			m_t[ id ] = SF_DOWNFALL_HIT_METAL
		elseif name == "water" then
			m_t[ id ] = SF_DOWNFALL_HIT_WATER
		elseif name == "wood" then
			m_t[ id ] = SF_DOWNFALL_HIT_WOOD
		elseif name == "glass" then
			m_t[ id ] = SF_DOWNFALL_HIT_GLASS
		elseif name == "concrete" then
			m_t[ id ] = SF_DOWNFALL_HIT_CONCRETE
		else
			m_t[ id ] = SF_DOWNFALL_HIT_GROUND
		end
		return m_t[ id ]
	end
	local function MaterialToHIT( str )
		if m_t[ str ] then return m_t[ str ] end
		local sp = Material( str ):GetKeyValues()["$surfaceprop"]
		if sp and #sp > 0 then
			sp = ConvertSurfaceProp( sp )
		else
			sp = ConvertSurfaceProp( str )
		end
		if sp == "default" then
			m_t[ str ] = SF_DOWNFALL_HIT_GROUND
		elseif sp == "metal" then
			m_t[ str ] = SF_DOWNFALL_HIT_METAL
		elseif sp == "water" then
			m_t[ str ] = SF_DOWNFALL_HIT_WATER
		elseif sp == "wood" then
			m_t[ str ] = SF_DOWNFALL_HIT_WOOD
		elseif sp == "glass" then
			m_t[ str ] = SF_DOWNFALL_HIT_GLASS
		elseif sp == "concrete" then
			m_t[ str ] = SF_DOWNFALL_HIT_CONCRETE
		else
			m_t[ str ] = SF_DOWNFALL_HIT_GROUND
		end
		return m_t[ str ]
	end
	local function IsMaterialEmpty( t )
		return t.HitTexture == "TOOLS/TOOLSINVISIBLE" or t.HitTexture == "**empty**" or t.HitTexture == "TOOLS/TOOLSNODRAW"
	end
	
	---Returns a raindrop pos from a sky position. #2 is hittype: -1 = no hit, 0 = ground, 1 = water, 2 = glass.
	---@param pos Vector
	---@param norm Vector
	---@param nRadius number
	---@param filter Entity
	---@return Vector? Pos
	---@return number? HitType
	---@return Vector? HitNormal
	---@shared
	local function TraceDown(pos, norm, nRadius, filter)
		nRadius = nRadius or 1
		if not pos or not norm then return end
		t.start = pos
		t.endpos = pos + norm * 262144
		t.maxs.x = nRadius
		t.maxs.y = nRadius
		t.mins.x = -nRadius
		t.mins.y = -nRadius
		t.filter = filter or GetViewEntity()
		local tr = util_TraceHull(t)
		if tr and (tr.AllSolid or tr.StartSolid or tr.HitSky) then
			return
		elseif nRadius > 5 and tr.Fraction * -norm.z < 0.0005 then -- About 150 hammer-units
		--	print("Dis: " .. 262144 * tr.Fraction * -norm.z)
			return 
		end
		if not tr or not tr.Hit then
			return tr.HitPos, SF_DOWNFALL_HIT_NIL
		elseif not IsValid(tr.Entity) then
			return tr.HitPos, SurfacePropIDToHIT( tr.SurfaceProps or -1 ) or MaterialToHIT( tr.HitTexture ) or SF_DOWNFALL_HIT_GROUND
		else
			local mat = tr.Entity:GetMaterial():lower()
			local mod = tr.Entity:GetModel():lower()
			return tr.HitPos, MaterialToHIT( #mat > 0 and mat or mod )
		end
		return tr.HitPos, SF_DOWNFALL_HIT_GROUND, tr.HitNormal
	end
	StormFox2.DownFall.TraceDown = TraceDown

	---Returns the skypos. If it didn't find the sky it will return last position as #2
	---@param vFrom Vector
	---@param vNormal Vector
	---@param nTries? number
	---@return Vector? SkyPos
	---@return Vector? LastEmptyHit
	---@shared
	local function FindSky(vFrom, vNormal, nTries)
		local last,lastFakeSky
		for i = 1,nTries do
			local t = util.TraceLine( {
				start = vFrom,
				endpos = vFrom + vNormal * 262144,
				mask = MASK_SOLID_BRUSHONLY
			} )
			if not t.Hit then break end -- Just empty void from this point on
			-- Check if we're in the void
			if t.HitPos.z > 32768 then break end -- Max map-size is 32768^2
			--if t.HitTexture == "TOOLS/TOOLSINVISIBLE" then return end
			-- We found the sky!
			if t.HitSky then
				-- In case there is the tiniest gab between skybox and the last brush, ignore it.
				if t.StartSolid then
					local zDis = (t.HitPos.z - vFrom.z ) * (t.Fraction - t.FractionLeftSolid)
					if zDis < 1 then
						return nil, t.HitPos
					end
				end
				return t.HitPos
			end
			-- Check for fake sky. Some maps don't have a brush called "skybox" .. for some reason.
			if IsMaterialEmpty(t) and not t.HitSky then
				-- Check if far away
				lastFakeSky = lastFakeSky or t.HitPos
			else
				lastFakeSky = nil
			end
			last = t.HitPos
			vFrom = t.HitPos + vNormal
		end
		if lastFakeSky then
			return lastFakeSky
		end
		return nil, last
	end
	StormFox2.DownFall.FindSky = FindSky
	
	---Locates the skybox above vFrom and returns a raindrop pos. #2 is hittype: -2 No sky, -1 = no hit/invald, 0 = ground, 1 = water, 2 = glass, #3 is hitnormal
	---@param vFrom Vector
	---@param vNorm Vector
	---@param nRadius number
	---@param filter Entity
	---@return Vector? RainHitPos
	---@return number HitType
	---@shared
	function StormFox2.DownFall.CheckDrop(vFrom, vNorm, nRadius, filter)
		t.mask = mask
		local sky,_ = FindSky(vFrom, -vNorm, 7)
		if not sky then return vFrom, -2 end -- Unable to find a skybox above this position
		return TraceDown(sky + vNorm * math.max(nRadius * 2, 4), vNorm * 262144, nRadius, filter)
	end

	-- Does the same as StormFox2.DownFall.CheckDrop, but will cache
	local t_cache = {}
	local t_cache_hit = {}
	local c_i = 0

	-- Does the same as StormFox2.DownFall.CheckDrop, but will fallback on cached positions when failed.
	---@param vFrom Vector
	---@param vNorm Vector
	---@param nRadius number
	---@param filter Entity
	---@return Vector? RainHitPos
	---@return number HitType
	---@shared
	function StormFox2.DownFall.CheckDropCache( vFrom, vNorm, nRadius, filter )
		local pos,n = StormFox2.DownFall.CheckDrop( vFrom, vNorm, nRadius, filter )
		if pos and n > -1 then
			c_i = (c_i % 10) + 1
			t_cache[c_i] = pos
			t_cache_hit[c_i] = pos
			return pos,n
		end
		if #t_cache < 1 then return pos,n end
		local n = math.random(1, #t_cache)
		return t_cache[n],t_cache_hit[n]
	end

	local cos,sin,rad = math.cos, math.sin, math.rad
	
	-- #1 = Hit Position, #2 hitType, #3 The offset from view, #4 hitNormal
	local vZ = Vector(0,0,0)

	-- Calculates and locates a downfall-drop infront/nearby of the client.
	---@param nDis number
	---@param nSize number
	---@param nTries number
	---@param vNorm? Vector
	---@param ignoreVel boolean
	---@param nMaxDistance number
	---@param tTemplate table
	---@return Vector DropPos
	---@return number HitType
	---@return Vector offset
	---@return Vector hitNorm
	---@return boolean ShouldRandomRage
	---@shared
	function StormFox2.DownFall.CalculateDrop( nDis, nSize, nTries, vNorm, ignoreVel, nMaxDistance, tTemplate )
		vNorm = vNorm or StormFox2.Wind.GetNorm()
		local view = StormFox2.util.GetCalcView()
		local v_vel = StormFox2.util.ViewEntity():GetVelocity() or vZ
		local v_pos = (view.pos and Vector(view.pos.x, view.pos.y, view.pos.z) or vZ) + Vector(vNorm.x,vNorm.y,0) * -tTemplate:GetSpeed() * -200
		if not ignoreVel then
			v_pos = v_pos + Vector(v_vel.x,v_vel.y,0) / 2
		end
		for i = 1, nTries do
			-- Get a random angle
			local d = math.Rand(nDis / 200,4)
			local deg = math.random(d * 45)
			if math.random() > 0.5 then
				deg = -deg
			end
			-- Calculate the offset
			local yaw = rad(view.ang.y + deg)
			local offset = v_pos + Vector(cos(yaw),sin(yaw)) * nDis
			local pos, n, hitNorm = StormFox2.DownFall.CheckDrop( offset, vNorm, nSize)
			
			if pos and n > -2 and pos:DistToSqr(v_pos) < 11000000 then -- TODO: Why does this happen? Position shouldn't be that waaaay away.
				local bRandomAge = not ignoreVel and nDis > nMaxDistance - v_vel:Length2D()
				return pos,n,offset, hitNorm, bRandomAge
			end
		end
	end
end

if CLIENT then
	local render_SetMaterial, render_DrawBeam, render_DrawSprite = render.SetMaterial, render.DrawBeam, render.DrawSprite

	---Creats a regular particle and returns it
	---@param sMaterial Material
	---@param vPos Vector
	---@param bUse3D boolean
	---@return userdata CLuaParticle
	---@client
	function StormFox2.DownFall.AddParticle( sMaterial, vPos, bUse3D )
		if bUse3D then
			return _STORMFOX_PEM["3D"]:Add( sMaterial, vPos )
		end
		return _STORMFOX_PEM["2D"]:Add( sMaterial, vPos )
	end
	
	-- Particle Template. Particles "copy" these values when they spawn.
	local pt_meta = {}
	pt_meta.__index = pt_meta
	debug.getregistry()["SFParticleTemplate"] = pt_meta
	pt_meta.MetaName = "ParticleTemplate"
	pt_meta.g = 1
	pt_meta.r_H = 400 -- Default render height
	pt_meta.r_H2 = 800 -- Default max render height (This is used to kill particles)
	AccessorFunc(pt_meta, "iMat", "Material")
	AccessorFunc(pt_meta, "w", "Width")
	AccessorFunc(pt_meta, "h", "Height")
	AccessorFunc(pt_meta, "c", "Color")
	AccessorFunc(pt_meta, "g", "Speed")
	AccessorFunc(pt_meta, "r_H", "RenderHeight")
	AccessorFunc(pt_meta, "i_G", "IgnoreGravity")
	-- Think function .. This will be called each time SmartTemplate gets called
	function pt_meta:Think() end
	-- Sets the alpha
	function pt_meta:SetAlpha( nAlpha )
		if self.c == color_white then
			self.c = Color(255,255,255)
		end
		self.c.a = nAlpha
	end
	function pt_meta:GetAlpha()
		return self.c.a
	end
	function pt_meta:SetSize( nWidth, nHeight )
		self.w = nWidth
		self.h = nHeight
	end
	function pt_meta:SetRenderHeight( f )
		self.r_H = f
		self.r_H2 = f * 2
	end
	function pt_meta:GetSize()
		return self.w or 1, self.h or 1
	end
		-- On hit (Overwrite it)
	function pt_meta:OnHit( vPos, vNormal, nHitType, zPart )
	end
	-- On Explosion
	function pt_meta:OnExplosion( vExposionPos, nDistance, iRange, iMagnitude)
	end
	function pt_meta:GetNorm()
		return self.vNorm or StormFox2.Wind.GetNorm() or Vector(0,0,-1)
	end
	function pt_meta:SetNorm( vNorm )
		self.vNorm = vNorm
	end
	function pt_meta:SetRandomAngle( r_a )
		self._ra = r_a
	end
	function pt_meta:SetRoll( nRoll )
		self._roll = nRoll
	end
	function pt_meta:SetFadeIn( b )
		self._bFIn = b
	end
	function pt_meta:_REM()
		local v = self.data or self
		v.num = v.num - 1
	end

	---Creates a particle template.
	---@param sMaterial Material
	---@param bBeam boolean
	---@param bFollow boolean
	---@return table tTemplate
	function StormFox2.DownFall.CreateTemplate(sMaterial, bBeam, bFollow)
		local t = {}
		setmetatable(t,pt_meta)
		t:SetMaterial(sMaterial)
		t.c = color_white
		t.bBeam = bBeam or false
		t.w = 32
		t.h = 32
		t.g = 1
		t.r_H = 400
		t.r_H2 = 800
		t.i_G = false
		t.bFollow = bFollow
		t.num = 0
		return t
	end
	-- Particles
	local p_meta = {}
	debug.getregistry()["SFParticle"] = p_meta
	p_meta.__index = function(self, key)
		return p_meta[key] or self.data[key]
	end
	-- Creates a particle from the template. Can return nil if something happen
	function pt_meta:CreateParticle( vEndPos, vNorm, hitType, hitNorm, maxDistance )
		local view = StormFox2.util.GetCalcView().pos
		local z_view = view.z
		local t = {}
		t.data = self
		t.vNorm = vNorm
		t.endpos = vEndPos
		t.hitType = hitType or SF_DOWNFALL_HIT_NIL
		t.hitNorm = hitNorm or Vector(0,0,-1)
		--t.to = CurTime()
		setmetatable(t, p_meta)
		local cG = self.g
		if not t:GetIgnoreGravity() then
			cG = self.g * GLGravity()
		end
		local dir_z = min(t:GetNorm().z,-0.1) -- Winddir should always be down
		-- Calc the starting position.
		if cG > 0 then -- Start from the sky and down
			local l = z_view + (t.r_H or 200) - t.endpos.z + math.Rand(0, t.h)
			t.curlength = l * -dir_z
			if t.curlength <= 0 then -- Something went wrong
				self.h = 0
				return
			end
		elseif cG < 0 then -- Start from ground and up
			local l = max(0, z_view - (t.r_H or 200) - t.endpos.z) -- Ground or below renderheight
			t.curlength = l * -dir_z
		end
		if self.bFollow then
			t.endpos = vEndPos - view
			t.bFollow = true
		end
		
		-- Check if outside
		local p = t:CalcPos()
		if maxDistance and p:Distance(Vector(view.x, view.y, p.z)) > maxDistance then
			return
		end
		self.num = self.num + 1
		return t, (t.r_H or 200) * 2 / abs( cG ) -- Secondary is how long we thing it will take for the particle to die. We also want this to be steady.
	end
	-- Returns the amount of particles spawned
	function pt_meta:GetNumber()
		return self.num or 0
	end
	-- Calculates the current position of the particle
	function p_meta:CalcPos()
		self.pos = self.endpos - self:GetNorm() * self.curlength
		return self.pos
	end
	-- Returns the current position
	function p_meta:GetPos()
		if self.pos then return self.pos end
		return self:CalcPos()
	end
	-- Sets the alpha of the particle, but won't overwrite template's color.
	function p_meta:SetAlpha( nAlpha )
		if not rawget(self, c) then -- Don't overwrite the template alpha. Create our own color and then modify it.
			self.c = Color(self.c.r, self.c.g, self.c.b, nAlpha)
		else
			self.c.a = nAlpha
		end
	end
	function p_meta:GetHitNormal()
		return self.hitNorm or Vector(0,0,-1)
	end
	-- Sets the "age" of the particle between 0 - 1
	function p_meta:SetAge( f )
		self.curlength = self.curlength * f
	end
	--
	function p_meta:GetDistance()
		if self._distance then
			return self._distance
		end
		--print("ERROR")
	end
	-- Sets the max-distance from view
	function p_meta:SetMaxDistance( f )
		self.mDis2Sqr = f ^ 2
	end
	-- Renders the particles
	function p_meta:Render(viewPos)
		local pos = self:GetPos()
		if self.bFollow then
			pos = pos + viewPos
		end
		if self._renh and self._bFIn and self._renh < 0.5 then -- We're fading out
			self.cL = Color(self.c.r, self.c.g, self.c.b, self._renh * 2 * self.c.a)
		elseif self._bFIn and (self._bFInA or 0) < 1 then -- We're fading in
			self._bFInA = min((self._bFInA or 0) + FrameTime(), 1)
			self.cL = Color(self.c.r, self.c.g, self.c.b, self._bFInA * self.c.a)
		else
			self.cL = nil
		end
		render_SetMaterial(self.iMat)
		if self.bBeam then
			if self._renh then
				if self._renh <= 0 then return end
				local sr = 1 - self._renh
				local sh = self:GetNorm() * self.h * 0.91
				render_DrawBeam(pos - sh, pos - sh * sr, self.w, 0, self._renh, self.cL or self.c)
			else
				render_DrawBeam(pos - self:GetNorm() * self.h, pos, self.w, 0, 1, self.cL or self.c)
			end
		else
			render_DrawSprite(pos, self.w, self.h, self.c)
		end
	end
	-- Checks the view
	function p_meta:IsInsideView()
		return self._iv == nil and true or self._iv
	end

	local function IVCheck( view, part )
		local vN = (part:GetPos() - view.pos)
		vN:Normalize()
		local dot = vN:Dot(view.ang:Forward())
		part._iv = dot > -( view.fov / 90 ) + 1
	end

	--[[
		StormFox2.DownFall.CreateTemplate(sMaterial, bBeam)
		Creates a template. This particle-data is shared between all other particles that are made from this.
		Do note that you can overwrite this data on each other individual particle as well.

		template:CreateParticle( vPos, startlength )
		Creates a particle from the template. This particle can also be modified using the template functions.
	]]

	-- Moves and kills the particles
	local t_sfp = {}
	local e_check = 0
	local function ParticleTick()
		if #t_sfp < 1 then return end
		if e_check > #t_sfp then
			e_check = 0
		end
		local view = StormFox2.util.GetCalcView()
		local viewp = view.pos
		local v_vel = StormFox2.util.ViewEntity():GetVelocity()
		local v_l = max(v_vel.x,v_vel.y)
			v_vel = v_vel / 4 + viewp
		local z_view = viewp.z
		local fr = FrameTime() * 600 -- * game.GetTimeScale()
		local die = {}
		local gg = GLGravity() -- Global Gravity
		local e_check_n = e_check + 50
		for n,t in ipairs(t_sfp) do
			local part = t[2]
			-- The length it moves (Could also be negative)
			local move = part.g
			if not part:GetIgnoreGravity() then
				move = part.g * gg 
			end
			if e_check_n < n and part.mDis2Sqr and not part.bFollow then -- Check the max-distance
				if (part:GetPos() - v_vel):Length2DSqr() > part.mDis2Sqr + v_l then
					part.hitType = SF_DOWNFALL_HIT_NIL
					die[#die + 1] = n
					continue
				end
				IVCheck( view, part)
			end
			part.curlength = part.curlength - move * fr
			-- Check if it dies
			if move > 0 then
				local zp = part:CalcPos().z
				if zp < part.endpos.z then
					-- Hit ground
					if zp < part.endpos.z - part.h or part.h < 10 then
						die[#die + 1] = n
					else
						part._renh = 1 - (part.endpos.z - zp) / (part.h * 0.9)
					end
				elseif zp < z_view - part.r_H or zp > z_view + part.r_H2 + part.h then
					-- Die in air
					part.hitType = SF_DOWNFALL_HIT_NIL
					die[#die + 1] = n
				end
			elseif move < 0 then -- It moves up in the sky. Should allways be hittype SF_DOWNFALL_HIT_NIL
				if part:CalcPos().z > z_view + part.r_H then
					-- Die
					die[#die + 1] = n
				end
			end
		end
		e_check = e_check + 50
		-- Kill particles
		for i = #die, 1, -1 do
			local t = table.remove(t_sfp, die[i])
			local part = t[2]
			part:_REM()
			--print("					Real Death: ", CurTime() - (part.to or 0))
			if part.hitType ~= SF_DOWNFALL_HIT_NIL and part.OnHit then
				part:OnHit( part.endpos, part:GetHitNormal(), part.hitType, part )
			end
		end
	end
	-- Renders all particles. t_sfp should be in render-order
	local viewpos = Vector(0,0,0)
	local function ParticleRender()
		local v = StormFox2.util.GetCalcView().pos or EyePos()
		viewpos.x = v.x
		viewpos.x = v.y
		for _,t in ipairs(t_sfp) do
		--	render.DrawLine(t[2]:GetPos(), t[2].endpos, color_white, true)
			if t[2]:IsInsideView() then
				t[2]:Render(viewpos)
			end
		end
	end

	---Spawns a particle from the template.
	---@param tTemplate table
	---@param vEndPos Vector
	---@param hitType number
	---@param hitNorm Vector
	---@param nDistance number
	---@param vNorm Vector
	---@param maxDistance number
	---@return table SFParticle
	---@client
	function StormFox2.DownFall.AddTemplateSimple( tTemplate, vEndPos, hitType, hitNorm, nDistance, vNorm, maxDistance )
		local part = tTemplate:CreateParticle( vEndPos, vNorm, hitType, hitNorm, maxDistance )
		if not part then return end
		if not nDistance then
			local p = StormFox2.util.GetCalcView().pos
			nDistance = Vector(p.x,p.y,vEndPos.z):Distance( vEndPos )
		end
		-- Add by distance
		local n = #t_sfp
		local t = {nDistance, part}
		if n < 1 then
			t_insert(t_sfp, t)
		else
			for i=1,n do
				if nDistance > t_sfp[i][1] then
					t_insert(t_sfp, i, t)
					return part
				end
			end
			t_insert(t_sfp, n, t)
		end
		return part
	end


	local v_d = Vector(0,0,-1)
	---Tries to add a particle. Also has cache build in.
	---@param tTemplate table
	---@param nMaxDistance number
	---@param nDistance number
	---@param traceSize number
	---@param vNorm Vector
	---@return boolean success
	---@client
	function StormFox2.DownFall.AddTemplate( tTemplate, nMaxDistance, nDistance, traceSize, vNorm )
		vNorm = vNorm or StormFox2.Wind.GetNorm()
		if tTemplate._ra then -- Random angle
			vNorm = Vector(vNorm.x, vNorm.y, vNorm.z) + Vector(math.Rand(-tTemplate._ra, tTemplate._ra),math.Rand(-tTemplate._ra, tTemplate._ra),0)
			vNorm:GetNormal()
		end
		local vEnd, nHitType, vCenter, hitNorm, bRandomAge = StormFox2.DownFall.CalculateDrop( nDistance, traceSize, 1, vNorm, tTemplate.bFollow, nMaxDistance, tTemplate )
		-- pos,n,offset, hitNorm
		if not tTemplate.m_cache then tTemplate.m_cache = {} end
		if not vEnd then 
			if tTemplate.m_cache and #tTemplate.m_cache > 0 then
				local t = table.remove(tTemplate.m_cache, 1)
				vEnd = t[1]
				nHitType = t[2]
				vCenter = t[3]
				hitNorm = t[4]
				nMaxDistance = t[5]
				vNorm = t[6]
			else
				return false 
			end
		else
			if t_insert(tTemplate.m_cache, {vEnd,nHitType, vCenter, hitNorm, nMaxDistance, vNorm}) > 10 then
				table.remove(tTemplate.m_cache,1)
			end
		end		
		-- Large particles from an angle looks odd.
		if tTemplate.w >= 20 and (vNorm.x ~= 0 or vNorm.y ~= 0) then
			local l = 1 - vNorm:Dot(v_d)  --* tTemplate.w * 1.1
			vEnd = vEnd + vNorm * -l * 500
		end
		local part = StormFox2.DownFall.AddTemplateSimple( tTemplate, vEnd, nHitType, hitNorm, nDistance, vNorm, nMaxDistance and nMaxDistance + 50 )
		if not part then return false end
		if bRandomAge or true then
			local n = part.h / (part.r_H * 2)
			part:SetAge( math.Rand(-n, 1))
		end
		if nMaxDistance then
			part:SetMaxDistance( nMaxDistance + 50 )
		end
		return part
	end

	-- Max particles by quality setting.
	-- 7 = 1
	-- 0 = 0.1
	local function max_particles()
		local qt = StormFox2.Client.GetQualityNumber() + 0.2
		if qt >= 7 then return 1 end
		if qt <= 0.2 then return 0.02 end
		return qt / 7
	end

	local sm_timer = 0.05
	---Returns the how many particles we should create pr 0.1 second.
	---@param tTemplate table
	---@param nAimAmount number
	---@return number
	---@return number AliveTime
	---@client
	function StormFox2.DownFall.CalcTemplateTimer( tTemplate, nAimAmount )
		local speed = abs( tTemplate.g * GLGravity() ) * 600
		--	print("FT",1 / FrameTime())
		--	print("nAimAmount: " .. nAimAmount)
		--	print("nAimAmountPrT: " .. nAimAmount * FrameTime())
		--	print("SPEED", speed)
		--  
		local alive_time = tTemplate.r_H / speed -- How long would it be alive? (Only half the time, since players are usually are on the ground)
		local a = nAimAmount / alive_time * sm_timer
		return a * max_particles(), alive_time
	end


	local emp_t = {}

	--- Automaticlly spawns particles and returns a table of them.
	--- Should be called 10 times pr second.
	---@param tTemplate table
	---@param nMinDistance number
	---@param nMaxDistance number
	---@param nAimAmount number
	---@param traceSize number
	---@param vNorm Vector
	---@return table
	---@client
	function StormFox2.DownFall.SmartTemplate( tTemplate, nMinDistance, nMaxDistance, nAimAmount, traceSize, vNorm )
		if tTemplate.Think then
			tTemplate:Think()
		end
		local am = tTemplate:GetNumber()
		if am >= nAimAmount then return emp_t end
		if tTemplate.s_timer and tTemplate.s_timer > CurTime() then return emp_t end
		tTemplate.s_timer = CurTime() + sm_timer
		local b = min(1, am / nAimAmount) -- Full amount
		local n,at = StormFox2.DownFall.CalcTemplateTimer( tTemplate, nAimAmount  )-- How many times this need to run pr tick
		local t = {}
		local _d = math.Rand(nMaxDistance, nMinDistance)
		for i = 1, n do
			if i%7 == 0 then
				_d = math.Rand(nMaxDistance, nMinDistance)
			end
			local p = StormFox2.DownFall.AddTemplate( tTemplate, nMaxDistance, _d, traceSize or 5, vNorm )
			if p then
				p._distance = _d
				p:SetAge( 1 + math.Rand(0,at) )
				t_insert(t, p) 
			end
		end
		return t
	end

	hook.Add("Think","StormFox2.Downfall.Tick", ParticleTick)
	hook.Add("PostDrawTranslucentRenderables", "StormFox2.Downfall.Render", function(depth,sky)
		if depth or sky then return end -- Don't render in skybox or depth.
		-- Render particles on the floor
		if _STORMFOX_PEM._PEM2D then
			_STORMFOX_PEM._PEM2D:Draw()
		end
		if _STORMFOX_PEM._PEM then
			_STORMFOX_PEM._PEM:Draw()
		end	
		if LocalPlayer():WaterLevel() >= 3 then return end -- Don't render SF particles under wanter.
		ParticleRender() -- Render sf particles
	end)

	---Returns the list of particles.
	---@return table
	---@client
	function StormFox2.DownFall.DebugList()
		return t_sfp
	end

	hook.Add("StormFox2.Entitys.OnExplosion", "StormFox2.Downfall.Explosion", function(pos, iRadius, iMagnitude)
		for i = #t_sfp, 1, -1 do
			local part = t_sfp[i][2]
			local dis = part:GetPos():Distance(pos)
			if dis * 1.2 > iRadius then continue end -- Adding just a bit more
			if part.OnExplosion then
				part:OnExplosion(pos, clamp(1 - (dis / iRadius), 0, 1),iRadius, iMagnitude)
			end
			part:_REM()
			table.remove(t_sfp, i)
		end
	end)
end

hook.Add("stormfox2.postlib", "AddDepthSetting", function()
	StormFox2.Setting.AddSV("depthfilter",true,nil,"Effects")
end)

-- 2D (Not made yet)
if CLIENT and false then
	local meta = {}
	AccessorFunc(meta, "_x", "X")
	AccessorFunc(meta, "_y", "y")
	AccessorFunc(meta, "_w", "Width")
	AccessorFunc(meta, "_h", "Height")
	AccessorFunc(meta, "_mat", "Material")
	AccessorFunc(meta, "_a", "Angle")
	AccessorFunc(meta, "_l", "Life")
	AccessorFunc(meta, "_as", "AngleSpeed")
	AccessorFunc(meta, "_c", "Color")
	AccessorFunc(meta, "_g", "Weight") -- Will make gravity effect it.
	function meta:SetSize( nSize )
		self:SetWidth( mSize )
		self:SetHeight( mSize )
	end
	function meta:SetVelocity( nXSpeed, nYSpeed )
		self._vx = nXSpeed
		self._vy = nYSpeed
	end
	function meta:SetFade( nBool )
		self._f = nBool
	end
	function meta:SetPos( x, y )
		self._x = x
		self._y = y
	end
	function StormFox2.DownFall.CreateScreenParticle(mMat)
		local t = {}
		-- Mat
		t._mat = mMat
		-- Pos
		t._x = 0
		t._y = 0
		t._w = 32
		t._h = 32
		-- Ang
		t._a = 0
		t._as = 0
		-- Vel
		t._vx = 0
		t._vy = 0
		-- Grav
		t._g = 0
		-- Should fade
		t._f = true
		-- Col
		t._c = color_white
		setmetatable(t, meta)
		return t
	end
	function StormFox2.DownFall.AddScreenParticle(obj2D, posx, posy)
		local t = {}
		-- Pos
		t._x = posx
		t._y = posy
		t._w = 32
		t._h = 32
		-- Ang
		t._a = 0
		t._as = 0
		-- Vel
		t._vx = 0
		t._vy = 0
		-- Grav
		t._g = 0
		-- Should fade
		t._f = true
		-- Col
		t._c = color_white
		setmetatable(t, meta)
		table.insert(_STORMFOX_SCE2d, {t, CurTime()})
		return t
	end
	hook.Add("HUDPaintBackground", "StormFox2.Downfall.2D_Rain", function()
		if #_STORMFOX_SCE2d < 1 then return end
		-- In
		if LocalPlayer() and LocalPlayer():WaterLevel() >= 3 then

		end

		local del = {}
		local screenGrav = 1
		for id, p in ipairs( _STORMFOX_SCE2d ) do
			local o = p[1]
			local t = CurTime() - p[4] + o:GetLife()
			-- Check if dead
			if t <= 0 or p[3] > ScrH() then -- Dead or outside
				table.insert(del, id)
				continue
			end
			-- Move
			local g = o._g and o._g * screenGrav or 0
			_STORMFOX_SCE2d[id][2] = p[2] + o._vx * FrameTime()
			_STORMFOX_SCE2d[id][3] = p[3] + (o._vy + g) * FrameTime()
			o:SetAngle( o:GetAngle() + o._ys )
			-- Render
			if o._f then

			end
			surface.SetDrawColor(o._c.r, o._c.g, o._c.b, o._c.a)

		end
	end)
end
