--[[ | 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/ --]] StormFox2.Thunder = {} ---Returns true if it is thundering. ---@return boolean ---@shared function StormFox2.Thunder.IsThundering() return StormFox2.Data.Get("nThunder", 0) > 0 end ---Returns the amount of posible strikes pr minute. ---@return number ---@shared function StormFox2.Thunder.GetActivity() return StormFox2.Data.Get("nThunder", 0) end if SERVER then local THUNDER_MAKE_SKYBOX = 0 local THUNDER_TRACE_ERROR = 1 local THUNDER_SUCCESS = 2 local function ETHull(pos,pos2,size,mask) local t = util.TraceHull( { start = pos, endpos = pos2, maxs = Vector(size,size,4), mins = Vector(-size,-size,0), mask = mask } ) t.HitPos = t.HitPos or pos + pos2 return t end -- Damages an entity local function StrikeDamageEnt( ent ) if not ent or not IsValid(ent) then return end local effectdata = EffectData() effectdata:SetOrigin( ent:GetPos() ) effectdata:SetEntity(ent) effectdata:SetMagnitude(2) effectdata:SetScale(3) for i = 1,100 do util.Effect( "TeslaHitboxes", effectdata, true, true ) end local ctd = DamageInfo() ctd:IsDamageType(DMG_SHOCK) ctd:SetDamage(math.Rand(90,200)) local vr = VectorRand() vr.z = math.abs(vr.z) ctd:SetDamageForce(vr * 1000) ctd:SetInflictor(game.GetWorld()) ctd:SetAttacker(game.GetWorld()) ent:TakeDamageInfo(ctd) end -- Damanges an area local function StrikeDamagePos( vPos ) local b_InWater = bit.band( util.PointContents( vPos ), CONTENTS_WATER ) == CONTENTS_WATER local t = {} for _,ent in ipairs(ents.FindInSphere(vPos,750)) do -- It hit in water, and you're nearby if b_InWater and ent:WaterLevel() > 0 then StrikeDamageEnt(ent) table.insert(t, ent) elseif ent:GetPos():Distance( vPos ) < 150 then StrikeDamageEnt(ent) table.insert(t, ent) end end hook.Run("StormFox2.Thunder.OnStrike", vPos, t) end -- Make STrike local Sway = 100 -- Traces a lightningstrike from sky to ground. local BrushZ = math.max(1, ( StormFox2.Map.MaxSize().z - StormFox2.Map.MinSize().z ) / 20000) local function MakeStrikeDown( vPos, nTraceSize ) if not nTraceSize then nTraceSize = 512 end -- Find the sky position at the area local SkyPos = StormFox2.DownFall.FindSky( vPos, vector_up, 8 ) if not SkyPos then -- Unable to find sky above. Get the higest point SkyPos = Vector(vPos.x, vPos.y, StormFox2.Map.MaxSize().z) SkyPos = StormFox2.DownFall.FindSky( SkyPos, vector_up, 1 ) or SkyPos end --debugoverlay.Box(SkyPos, Vector(1,1,1) * -20, Vector(1,1,1) * 20, 15, Color(255,0,0)) -- Find the strike distance (Some maps are suuuper tall) local tr = ETHull(SkyPos - vector_up * 10, Vector( vPos.x, vPos.y, StormFox2.Map.MinSize().z), 256, MASK_SOLID_BRUSHONLY ) if tr.AllSolid or tr.Fraction == 0 then -- This is inside solid and should instead be done in the skybox. return THUNDER_MAKE_SKYBOX, SkyPos end SkyPos.z = math.min( SkyPos.z, tr.HitPos.z + 8000 ) local line = {} table.insert(line,pos) -- Create a line down local olddir = Vector(0,0,-1) local m_dis = math.max(1, ( SkyPos.z - StormFox2.Map.MinSize().z ) / 20000) local pos = SkyPos - vector_up * 5 local _n = nTraceSize / 40 for i = 20, 1, -1 do -- Random sway local randir = Angle(math.Rand(-Sway,Sway) + 90,math.random(360),math.Rand(-Sway,Sway)):Forward() + olddir randir:Normalize() randir.z = -math.abs(randir.z) olddir = randir local pos2 = pos + randir * math.Rand(800, 1000) * m_dis local m_dis = math.max(1, ( StormFox2.Map.MaxSize().z - StormFox2.Map.MinSize().z ) / 20000) local tr = ETHull(pos, pos2, nTraceSize - i * _n ) --debugoverlay.Line(pos, pos2, 10) if not tr.Hit then table.insert(line, pos2) pos = pos2 else if tr.HitSky then -- We hit the side of the skybox. Go to other way olddir = -olddir else table.insert(line, tr.HitPos) return THUNDER_SUCCESS, line, tr end end end return line, tr end -- Creates a lightniingstrike up ( Useful when forcing a lightning strike to hit something ) local function MakeStrikeUp( vPos ) local SkyPos = StormFox2.DownFall.FindSky( vPos, vector_up, 8 ) if not SkyPos then -- Unable to find sky above. Get the higest point SkyPos = Vector(vPos.x, vPos.y, StormFox2.Map.MaxSize().z) SkyPos = StormFox2.DownFall.FindSky( SkyPos, vector_up, 1 ) or SkyPos end local olddir = Vector(0,0,-1) local pos = vPos local m_dis = math.max(1, ( StormFox2.Map.MaxSize().z - StormFox2.Map.MinSize().z ) / 20000) local list = {pos} for i = 1, 20 do local randir = Angle(math.Rand(-Sway,Sway) + 90,math.random(360),math.Rand(-Sway,Sway)):Forward() + olddir randir:Normalize() randir.z = -math.abs(randir.z) olddir = randir local pos2 = pos + -randir * math.Rand(800, 1000) * m_dis if pos2.z >= SkyPos.z then table.insert(list, Vector(pos2.x, pos2.y, SkyPos.z)) break else pos = pos2 table.insert(list, pos) end end return table.Reverse(list) end local function LightFluff(tList, b_InSkybox ) local n_Length = math.Rand(.4,0.7) local n_Light = math.random(200, 250) net.Start(StormFox2.Net.Thunder) net.WriteBool( true ) net.WriteUInt(#tList, 5) for i = 1, #tList do net.WriteVector(tList[i]) end net.WriteBool( b_InSkybox ) net.WriteFloat(n_Length) net.WriteUInt(n_Light,8) net.Broadcast() end ---Creates a lightningstrike at a given position. Will return a hit entity as a second argument. ---@param pos Vector ---@return boolean success ---@return Entity ---@server function StormFox2.Thunder.CreateAt( pos ) local t_Var, tList, tr local b_InSkybox = false local vMapMin = StormFox2.Map.MinSize() local vMapMax = StormFox2.Map.MaxSize() if not pos then pos = Vector( math.Rand(vMapMin.x, vMapMax.x), math.Rand(vMapMin.y, vMapMax.y), math.Rand(vMapMax.z, vMapMin.z / 2) ) end local bInside = pos.x >= vMapMin.x and pos.x <= vMapMax.x and vMapMin.y and pos.y <= vMapMax.y if bInside then t_Var, tList, tr = MakeStrikeDown( pos ) if t_Var == THUNDER_MAKE_SKYBOX then bInside = false end end if not bInside then -- Outside the map tList = MakeStrikeUp( Vector(pos.x, pos.y, StormFox2.Map.MinSize().z) ) b_InSkybox = true end if not tList then return false end -- Unable to create lightning strike here. local hPos = tr and tr.HitPos or tList[#tList] if not hPos then return end if tr and IsValid( tr.Entity ) then table.insert(tList, tr.Entity:GetPos() + tr.Entity:OBBCenter()) hPos = tr.Entity:GetPos() + tr.Entity:OBBCenter() end StrikeDamagePos( hPos ) LightFluff(tList, b_InSkybox ) return true, tr and IsValid( tr.Entity ) and tr.Entity end ---Creates a lightning strike to hit the given position / entity. ---@param zPosOrEnt Vector|Entity ---@param bRangeDamage number ---@return boolean success ---@server function StormFox2.Thunder.Strike( zPosOrEnt, bRangeDamage ) -- Strike the entity if not bRangeDamage and zPosOrEnt.Health then local ent = zPosOrEnt timer.Simple(0.4, function() StrikeDamageEnt( ent ) end) end if zPosOrEnt.GetPos then if zPosOrEnt.OBBCenter then zPosOrEnt = zPosOrEnt:GetPos() + zPosOrEnt:OBBCenter() else zPosOrEnt = zPosOrEnt:GetPos() end end if bRangeDamage then timer.Simple(0.4, function() StrikeDamagePos( zPosOrEnt ) end) end local b_InSkybox = not StormFox2.Map.IsInside( zPosOrEnt ) --if b_InSkybox then -- zPosOrEnt = StormFox2.Map.WorldtoSkybox( zPosOrEnt ) --end local tList = MakeStrikeUp( zPosOrEnt ) LightFluff(tList, false ) return true end ---Creates a rumble. ---@param pos Vector ---@param bLight boolean ---@server function StormFox2.Thunder.Rumble( pos, bLight ) if not pos then local vMapMin = StormFox2.Map.MinSize() local vMapMax = StormFox2.Map.MaxSize() pos = Vector( math.Rand(vMapMin.x, vMapMax.x), math.Rand(vMapMin.y, vMapMax.y), math.Rand(vMapMax.z, vMapMin.z / 2) ) end local n_Length = bLight and math.Rand(.4,0.7) or 0 local n_Light = bLight and math.random(150, 250) or 0 net.Start( StormFox2.Net.Thunder ) net.WriteBool( false ) net.WriteVector( pos ) net.WriteFloat(n_Length) net.WriteUInt(n_Light,8) net.Broadcast() end -- Enables thunder and makes them spawn at random, until set off or another weather gets selected local b = false local n = math.max(StormFox2.Map.MaxSize().x, StormFox2.Map.MaxSize().y, -StormFox2.Map.MinSize().x,-StormFox2.Map.MinSize().y) if StormFox2.Map.Has3DSkybox() then n = n * 1.5 end do local n = 0 ---Enables / Disables thunder. ---@param bEnable boolean ---@param nActivityPrMinute? number ---@param nTimeAmount? number ---@server function StormFox2.Thunder.SetEnabled( bEnable, nActivityPrMinute, nTimeAmount ) n = 0 if bEnable then StormFox2.Network.Set("nThunder", nActivityPrMinute) if nTimeAmount then StormFox2.Network.Set("nThunder", 0, nTimeAmount) end else StormFox2.Network.Set("nThunder", 0) end end hook.Add("Think", "StormFox2.thunder.activity", function() if not StormFox2.Thunder.IsThundering() then return end if n >= CurTime() then return end local a = StormFox2.Thunder.GetActivity() n = CurTime() + math.random(50, 60) / a -- Strike or rumble if math.random(1,3) < 3 then StormFox2.Thunder.CreateAt() else StormFox2.Thunder.Rumble( nil, math.random(10) > 1 ) end end) end else lightningStrikes = lightningStrikes or {} local _Light, _Stop, _Length = 0,0,0 ---Returns light created by thunder. ---@return number ---@client function StormFox2.Thunder.GetLight() if _Light <= 0 then return 0 end if _Stop < CurTime() then _Light = 0 return 0 end local t = (_Stop - CurTime()) / _Length local c = math.abs(math.sin( t * math.pi )) local l = _Light * c return math.random(l, l * 0.5) -- Flicker a bit end -- 0 - 2000 local CloseStrikes = {"sound/stormfox2/amb/thunder_strike.ogg"} -- 2000 - 20000 local MediumStrikes = {"sound/stormfox2/amb/thunder_strike.ogg", "sound/stormfox2/amb/thunder_strike2.ogg"} -- 20000 + local FarStrikes = {} if IsMounted("csgo") or IsMounted("left4dead2") then table.insert(FarStrikes, "ambient/weather/thunderstorm/lightning_strike_1.wav") table.insert(FarStrikes, "ambient/weather/thunderstorm/lightning_strike_4.wav") end if #FarStrikes < 1 then table.insert(FarStrikes, "sound/stormfox2/amb/thunder_strike2.ogg") end local snd_buffer = {} local function StrikeEffect( pos, n_Length ) local dlight = DynamicLight( 1 ) if ( dlight ) then dlight.pos = pos dlight.r = 255 dlight.g = 255 dlight.b = 255 dlight.brightness = 6 dlight.Decay = 3000 / n_Length dlight.Size = 256 * 8 dlight.DieTime = CurTime() + n_Length end local effectdata = EffectData() local s = math.random(5, 8) effectdata:SetOrigin( pos + vector_up * 4 ) effectdata:SetMagnitude( s / 2 ) effectdata:SetNormal(vector_up) effectdata:SetRadius( 8 ) util.Effect( "Sparks", effectdata, true, true ) end local function PlayStrike( vPos, nViewDis, viewPos ) local snd = "" if nViewDis <= 2000 then snd = table.Random(CloseStrikes) elseif nViewDis <= 15000 then snd = table.Random(MediumStrikes) else snd = table.Random(FarStrikes) end if string.sub(snd, 0, 6 ) == "sound/" or string.sub(snd,-4) == ".ogg" then sound.PlayFile( snd, "3dnoplay", function( station, errCode, errStr ) if ( IsValid( station ) ) then station:Set3DFadeDistance( 0, 10 ) station:SetVolume( 1) station:SetPos(vPos) station:Play() end end) else surface.PlaySound( snd ) end end --[[ Sound moves about 343 meters pr second 52.49 hU = 1 meter ~ 18.004 hU pr second ]] local b = true local function SndThink() if #snd_buffer < 1 then hook.Remove("Think","StormFox2.Thunder.SndDis") b = false return end local r = {} local view = StormFox2.util.ViewEntity():GetPos() local c = CurTime() - 0.2 for k,v in ipairs( snd_buffer ) do local travel = (c - v[2]) * 18004 local vDis = view:Distance( v[1] ) if vDis - travel < 0 then table.insert(r, k) PlayStrike( v[1], vDis, view ) end end for i = #r, 1, -1 do table.remove(snd_buffer, r[i]) end end hook.Add("Think","StormFox2.Thunder.SndDis", SndThink) local function Strike( tList, b_InSkybox, n_Length, n_Light ) table.insert(lightningStrikes, {CurTime() + n_Length, n_Length, b_InSkybox, tList, true}) local pos = tList[#tList][1] sound.Play("ambient/energy/weld" .. math.random(1,2) .. ".wav", pos) if not b then hook.Add("Think","StormFox2.Thunder.SndDis", SndThink) end table.insert(snd_buffer, {pos, CurTime()}) local c = CurTime() _Light = 255 _Length = .7 _Stop = math.max(c + _Length, _Stop) end local function Rumble( vPos, n_Length, n_Light ) -- Thunder is at 120dB local c = CurTime() _Light = n_Light _Length = n_Length _Stop = math.max(c + n_Length, _Stop) sound.Play("ambient/atmosphere/thunder" .. math.random(3,4) .. ".wav", StormFox2.util.ViewEntity():GetPos(), 150) end local Sway = 120 net.Receive( StormFox2.Net.Thunder, function(len) local b_Strike = net.ReadBool() if b_Strike then local tList = {} local old local n = net.ReadUInt(5) for i = 1, n do local randir = Angle(math.Rand(-Sway,Sway) + 90,math.random(360),math.Rand(-Sway,Sway)) local new = net.ReadVector() if old then randir = randir:Forward() + (new - old):Angle():Forward() * 2 else randir = randir:Forward() end old = new tList[i] = {new,math.Rand(1.2,1.5),randir,math.random(0,1)} --debugoverlay.Sphere(new, 15, 15, Color(255,255,255), true) end local b_InSkybox = net.ReadBool() Strike(tList, b_InSkybox, net.ReadFloat(), net.ReadUInt(8)) else local vPos = net.ReadVector() Rumble(vPos, net.ReadFloat(), net.ReadUInt(8) ) end end) -- Render Strikes local tex = {(Material("stormfox2/effects/lightning"))} local texend = {(Material("stormfox2/effects/lightning_end")),(Material("stormfox2/effects/lightning_end2"))} for k, v in ipairs( texend ) do v:SetFloat("$nofog",1) end local t = 0 hook.Add("PostDrawOpaqueRenderables","StormFox2.Render.Lightning",function(a,sky) if a or #lightningStrikes < 1 then return end if sky then return end -- Doesn't work yet local r = {} local c = CurTime() local col = Color( 255, 255, 255, 255) for k, v in ipairs( lightningStrikes ) do -- Render world or skybox --if v[3] ~= sky then continue end -- Remove if dead if v[1] < c then table.insert(r, k) continue end local life = 1 - ( v[1] - c ) / v[2] -- 0 - 1 if life < 0.6 then col.a = 425 * life else col.a = 637.5 * (1 - life) end local fuzzy = life < 0.6 local i = 0.6 / #v[4] if v[5] and not fuzzy then StrikeEffect( v[4][#v[4]][1] , life ) lightningStrikes[k][5] = false end -- Render beams render.SetMaterial(tex[1]) render.StartBeam(#v[4]) local l = math.Rand(0.4, 0.8) for k2, v2 in ipairs( v[4] ) do if life < k2 * i then break end local tp = 0.1 render.AddBeam( v2[1], 400, (l * (k2 - 1)) % 1, col ) end render.EndBeam() -- Render strikes if fuzzy then for k2, v2 in ipairs( v[4] ) do if life < k2 * i or k2 + 3 >= #v[4] then break end local n2 = life * 2- k2 * 0.04 local vec = v2[1] local tp = 1 / #v[4] * k2 local n = k2 % #texend + 1 render.SetMaterial(texend[n]) local w,h = texend[n]:Width() * n2,texend[n]:Height() * n2 render.DrawBeam( vec, vec + v2[3] * h * v2[2], w * v2[2], 1 - n2, 1, col ) end else local v1 = v[4][1][1] local v2 = v[4][#v[4]][1] local vc = (v1 + v2) / 2 vc.z = v2.z local vc2 = Vector(vc.x,vc.y,v1.z) local a = math.max(0, 1 - life - 0.2) col.a = a * 1275 render.SetMaterial(Material("stormfox2/effects/lightning_light")) render.DrawBeam(vc, vc2, 24400, 0.3 , 0.7, col) end end for i = #r, 1, -1 do table.remove(lightningStrikes, r[i]) end end) end