Files
wnsrc/lua/stormfox2/lib/sh_downfall.lua
lifestorm 73479cff9e Upload
2024-08-04 22:55:00 +03:00

1007 lines
29 KiB
Lua

--[[
| 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