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

local enable = CreateConVar("pac_sv_projectiles", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED})
local pac_sv_projectile_max_attract_radius = CreateConVar("pac_sv_projectile_max_attract_radius", 300, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED})
local pac_sv_projectile_max_damage_radius = CreateConVar("pac_sv_projectile_max_damage_radius", 100, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED})

do -- projectile entity
	local ENT = {}

	ENT.Type = "anim"
	ENT.Base = "base_anim"
	ENT.ClassName = "pac_projectile"

	function ENT:SetupDataTables()
		self:NetworkVar("Bool", 0, "AimDir")
		self:NetworkVar("Vector", 0, "OldVelocity")
	end

	if CLIENT then
		function ENT:Draw()
			if self:GetAimDir() then
				if self:GetOldVelocity() ~= vector_origin then
					self:SetRenderAngles(self:GetOldVelocity():Angle())
				elseif self:GetVelocity() ~= vector_origin then
					self:SetRenderAngles(self:GetVelocity():Angle())
				end
			end

			if self:GetParent():IsValid() and not self.done then
				self:SetPredictable(true)
				self.done = true
			end
		end

		net.Receive("pac_projectile_collide_event", function()
			local self = net.ReadEntity()
			local data = net.ReadTable()

			self.pac_event_collision_data = data
		end)
	end

	if SERVER then
		pac.AddHook("EntityTakeDamage", "pac_projectile", function(ent, dmg)
			local a, i = dmg:GetAttacker(), dmg:GetInflictor()

			if a == i and a:IsValid() and a.projectile_owner then
				local owner = a.projectile_owner
				if owner:IsValid() then
					dmg:SetAttacker(a.projectile_owner)
				end
			end
		end)

		ENT.projectile_owner = NULL

		function ENT:Initialize()
			self.next_target = 0
		end

		function ENT:SetData(ply, pos, ang, part)

			self.projectile_owner = ply

			local radius = math.Clamp(part.Radius, 1, pac_sv_projectile_max_damage_radius:GetFloat())

			if part.Sphere then
				self:PhysicsInitSphere(radius)
			else
				self:PhysicsInitBox(Vector(1,1,1) * - radius, Vector(1,1,1) * radius)
			end

			local phys = self:GetPhysicsObject()
			phys:EnableGravity(part.Gravity)
			phys:AddVelocity((ang:Forward() + (VectorRand():Angle():Forward() * part.Spread)) * part.Speed * 1000)
			if part.AddOwnerSpeed then
				phys:AddVelocity(ply:GetVelocity())
			end

			if part.Collisions then
				if part.CollideWithSelf then
					self:SetCollisionGroup(COLLISION_GROUP_NONE)
				else
					self:SetCollisionGroup(COLLISION_GROUP_INTERACTIVE_DEBRIS)
				end
			else
				phys:EnableCollisions(false)
			end

			phys:SetMass(math.Clamp(part.Mass, 0.001, 50000))
			phys:SetDamping(0, 0)

			self:SetAimDir(part.AimDir)

			self.part_data = part
		end

		local damage_types = {
			generic = 0, --generic damage
			crush = 1, --caused by physics interaction
			bullet = 2, --bullet damage
			slash = 4, --sharp objects, such as manhacks or other npcs attacks
			burn = 8, --damage from fire
			vehicle = 16, --hit by a vehicle
			fall = 32, --fall damage
			blast = 64, --explosion damage
			club = 128, --crowbar damage
			shock = 256, --electrical damage, shows smoke at the damage position
			sonic = 512, --sonic damage,used by the gargantua and houndeye npcs
			energybeam = 1024, --laser
			nevergib = 4096, --don't create gibs
			alwaysgib = 8192, --always create gibs
			drown = 16384, --drown damage
			paralyze = 32768, --same as dmg_poison
			nervegas = 65536, --neurotoxin damage
			poison = 131072, --poison damage
			acid = 1048576, --
			airboat = 33554432, --airboat gun damage
			blast_surface = 134217728, --this won't hurt the player underwater
			buckshot = 536870912, --the pellets fired from a shotgun
			direct = 268435456, --
			dissolve = 67108864, --forces the entity to dissolve on death
			drownrecover = 524288, --damage applied to the player to restore health after drowning
			physgun = 8388608, --damage done by the gravity gun
			plasma = 16777216, --
			prevent_physics_force = 2048, --
			radiation = 262144, --radiation
			removenoragdoll = 4194304, --don't create a ragdoll on death
			slowburn = 2097152, --

			explosion = -1, -- util.BlastDamageInfo
			fire = -1, -- ent:Ignite(5)

			-- env_entity_dissolver
			dissolve_energy = 0,
			dissolve_heavy_electrical = 1,
			dissolve_light_electrical = 2,
			dissolve_core_effect = 3,

			heal = -1,
			armor = -1,
		}

		local dissolver_entity = NULL
		local function dissolve(target, attacker, typ)
			local ent = dissolver_entity:IsValid() and dissolver_entity or ents.Create("env_entity_dissolver")
			ent:Spawn()
			target:SetName(tostring({}))
			ent:SetKeyValue("dissolvetype", tostring(typ))
			ent:Fire("Dissolve", target:GetName())
			timer.Simple(5, function() SafeRemoveEntity(ent) end)
			dissolver_entity = ent
		end

		function ENT:Think()
			if not self.projectile_owner:IsValid() then
				self:Remove()
			end
		end

		function ENT:PhysicsUpdate(phys)
			if not self.part_data then return end

			phys:SetVelocity(phys:GetVelocity() / math.max(1 + (self.part_data.Damping / 100), 1))

			if self.part_data.Attract ~= 0 then
				local ply = self.projectile_owner

				if self.part_data.AttractMode == "hitpos" then
					local pos = ply:GetEyeTrace().HitPos

					local dir = pos - phys:GetPos()
					dir:Normalize()
					dir = dir * self.part_data.Attract

					phys:SetVelocity(phys:GetVelocity() + dir)
				elseif self.part_data.AttractMode == "hitpos_radius" then
					local pos = ply:EyePos() + ply:GetAimVector() * self.part_data.AttractRadius

					local dir = pos - phys:GetPos()
					dir:Normalize()
					dir = dir * self.part_data.Attract

					phys:SetVelocity(phys:GetVelocity() + dir)
				elseif self.part_data.AttractMode == "closest_to_projectile" or self.part_data.AttractMode == "closest_to_hitpos" then
					if self.next_target < CurTime() then
						local radius = math.Clamp(self.part_data.AttractRadius, 0, pac_sv_projectile_max_attract_radius:GetFloat())
						local pos

						if self.part_data.AttractMode == "closest_to_projectile" then
							pos = phys:GetPos()
						else
							pos = ply:GetEyeTrace().HitPos
						end

						local closest_1 = {}
						local closest_2 = {}

						for _, ent in ipairs(ents.FindInSphere(pos, radius)) do
							if
								ent ~= self and
								ent ~= ply and
								ent:GetPhysicsObject():IsValid() and
								ent:GetClass() ~= self:GetClass()
							then
								local data = {dist = ent:NearestPoint(ent:GetPos()):Distance(pos), ent = ent}
								if ent:IsPlayer() or ent:IsNPC() then
									table.insert(closest_1, data)
								else
									table.insert(closest_2, data)
								end
							end
						end

						if closest_1[1] then
							table.sort(closest_1, function(a, b) return a.dist < b.dist end)
							self.target_ent = closest_1[1].ent
						elseif closest_2[1] then
							table.sort(closest_2, function(a, b) return a.dist < b.dist end)
							self.target_ent = closest_2[1].ent
						end

						self.next_target = CurTime() + 0.15
					end

					if self.target_ent and self.target_ent:IsValid() then
						local dir = self.target_ent:NearestPoint(phys:GetPos()) - phys:GetPos()
						dir:Normalize()
						dir = dir * self.part_data.Attract

						phys:SetVelocity(phys:GetVelocity() + dir)
					end
				end
			end
		end

		util.AddNetworkString("pac_projectile_collide_event")

		function ENT:PhysicsCollide(data, phys)
			if not self.part_data then return end
			if not self.projectile_owner:IsValid() then return end

			net.Start("pac_projectile_collide_event", true)
				net.WriteEntity(self)
				net.WriteTable({}) -- nothing for now
			net.SendPVS(data.HitPos)

			local ply = self.projectile_owner

			if self.part_data.Bounce ~= 0 then
				phys:SetVelocity(data.OurOldVelocity - 2 * (data.HitNormal:Dot(data.OurOldVelocity) * data.HitNormal) * self.part_data.Bounce)
			end

			if self.part_data.Sticky and (self.part_data.Bounce == 0 or not data.HitEntity:IsWorld()) then
				phys:SetVelocity(Vector(0,0,0))
				phys:Sleep()
				phys:EnableMotion(false)
				phys:EnableCollisions(false)

				timer.Simple(0, function() if self:IsValid() then self:SetCollisionGroup(COLLISION_GROUP_DEBRIS) end end)

				 if not data.HitEntity:IsWorld() then
					if data.HitEntity:GetBoneCount() then
						local closest = {}
						for id = 1, data.HitEntity:GetBoneCount() do
							local pos = data.HitEntity:GetBonePosition(id)
							if pos then
								table.insert(closest, {dist = pos:Distance(data.HitPos), id = id, pos = pos})
							end
						end
						if closest[1] then
							table.sort(closest, function(a, b) return a.dist < b.dist end)
							self:FollowBone(data.HitEntity, closest[1].id)
							self:SetLocalPos(util.TraceLine({start = data.HitPos, endpos = closest[1].pos}).HitPos - closest[1].pos)
						else
							self:SetPos(data.HitPos)
							self:SetParent(data.HitEntity)
						end
					else
						self:SetPos(data.HitPos)
						self:SetParent(data.HitEntity)
					end
				end

				self:SetOldVelocity(data.OurOldVelocity)
			end

			if self.part_data.BulletImpact then
				self:FireBullets{
					Attacker = ply,
					Damage = 0,
					Force = 0,
					Num = 1,
					Src = data.HitPos - data.HitNormal,
					Dir = data.HitNormal,
					Distance = 10,
				}
			end

			if self.part_data.DamageType:sub(0, 9) == "dissolve_" and damage_types[self.part_data.DamageType] then
				if data.HitEntity:IsPlayer() then
					local info = DamageInfo()
					info:SetAttacker(ply)
					info:SetInflictor(self)
					info:SetDamageForce(data.OurOldVelocity)
					info:SetDamagePosition(data.HitPos)
					info:SetDamage(100000)
					info:SetDamageType(damage_types.dissolve)

					data.HitEntity:TakeDamageInfo(info)
				else
					local can = hook.Run("CanProperty", ply, "remover", data.HitEntity)
					if can ~= false then
						dissolve(data.HitEntity, ply, damage_types[self.part_data.DamageType])
					end
				end
			end

			local damage_radius = math.Clamp(self.part_data.DamageRadius, 0, 300)

			if self.part_data.Damage > 0 then
				if self.part_data.DamageType == "heal" then
					if damage_radius > 0 then
						for _, ent in ipairs(ents.FindInSphere(data.HitPos, damage_radius)) do
							if ent ~= ply or self.part_data.CollideWithOwner then
								ent:SetHealth(math.min(ent:Health() + self.part_data.Damage, ent:GetMaxHealth()))
							end
						end
					else
						data.HitEntity:SetHealth(math.min(data.HitEntity:Health() + self.part_data.Damage, data.HitEntity:GetMaxHealth()))
					end
				elseif self.part_data.DamageType == "armor" then
					if damage_radius > 0 then
						for _, ent in ipairs(ents.FindInSphere(data.HitPos, damage_radius)) do
							if ent.SetArmor and ent.Armor then
								if ent ~= ply or self.part_data.CollideWithOwner then
									ent:SetArmor(math.min(ent:Armor() + self.part_data.Damage, ent.GetMaxArmor and ent:GetMaxArmor() or 100))
								end
							end
						end
					elseif data.HitEntity.SetArmor and data.HitEntity.Armor then
						data.HitEntity:SetArmor(math.min(data.HitEntity:Armor() + self.part_data.Damage, data.HitEntity.GetMaxArmor and data.HitEntity:GetMaxArmor() or 100))
					end
				else
					local info = DamageInfo()

					info:SetAttacker(ply)
					info:SetInflictor(self)

					if self.part_data.DamageType == "fire" then
						local ent = data.HitEntity
						if damage_radius > 0 then
							-- this should also use blast damage to find which entities it can damage
							for _, ent in ipairs(ents.FindInSphere(data.HitPos, damage_radius)) do
								if ent ~= self and ent:IsSolid() and hook.Run("CanProperty", ply, "ignite", ent) ~= false and (ent ~= ply or self.part_data.CollideWithOwner) then
									ent:Ignite(math.min(self.part_data.Damage, 5))
								end
							end
						elseif ent:IsSolid() and hook.Run("CanProperty", ply, "ignite", ent) ~= false then
							ent:Ignite(math.min(self.part_data.Damage, 5))
						end
					elseif self.part_data.DamageType == "explosion" then
						info:SetDamageType(damage_types.blast)
						info:SetDamage(math.Clamp(self.part_data.Damage, 0, 100000))
						util.BlastDamageInfo(info, data.HitPos, damage_radius)
					else
						info:SetDamageForce(data.OurOldVelocity)
						info:SetDamagePosition(data.HitPos)
						info:SetDamage(math.min(self.part_data.Damage, 100000))
						info:SetDamageType(damage_types[self.part_data.DamageType] or damage_types.generic)

						if damage_radius > 0 then
							for _, ent in ipairs(ents.FindInSphere(data.HitPos, damage_radius)) do
								if ent ~= ply or self.part_data.CollideWithOwner then
									ent:TakeDamageInfo(info)
								end
							end
						else
							data.HitEntity:TakeDamageInfo(info)
						end
					end
				end
			end

			if self.part_data.RemoveOnCollide then
				timer.Simple(0, function()
					SafeRemoveEntity(self)
				end)
			end
		end

		function ENT:OnRemove()
			if IsValid(self.pac_projectile_owner) then
				local ply = self.pac_projectile_owner
				ply.pac_projectiles = ply.pac_projectiles or {}
				ply.pac_projectiles[self] = nil
			end
		end
	end

	scripted_ents.Register(ENT, ENT.ClassName)
end

if SERVER then
	for key, ent in pairs(ents.FindByClass("pac_projectile")) do
		ent:Remove()
	end

	util.AddNetworkString("pac_projectile")
	util.AddNetworkString("pac_projectile_attach")

	net.Receive("pac_projectile", function(len, ply)
		if not enable:GetBool() then return end

		pace.suppress_prop_spawn = true
		if hook.Run("PlayerSpawnProp", ply, "models/props_junk/popcan01a.mdl") == false then
			pace.suppress_prop_spawn = nil
			return
		end
		pace.suppress_prop_spawn = nil

		local pos = net.ReadVector()
		local ang = net.ReadAngle()
		local part = net.ReadTable()

		local radius_limit = 2000

		if pos:Distance(ply:EyePos()) > radius_limit * ply:GetModelScale() then
			local ok = false

			for _, ent in ipairs(ents.FindInSphere(pos, radius_limit)) do
				if (ent.CPPIGetOwner and ent:CPPIGetOwner() == ply) or ent.projectile_owner == ply or ent:GetOwner() == ply then
					ok = true
					break
				end
			end

			if not ok then
				pos = ply:EyePos()
			end
		end

		local function spawn()
			if not ply:IsValid() then return end

			ply.pac_projectiles = ply.pac_projectiles or {}

			local projectile_count = 0
			for ent in pairs(ply.pac_projectiles) do
				if ent:IsValid() then
					projectile_count = projectile_count + 1
				else
					ply.pac_projectiles[ent] = nil
				end
			end

			if projectile_count > 50 then
				pac.Message("Player ", ply, " has more than 50 projectiles spawned!")
				return
			end

			if part.Maximum > 0 and projectile_count >= part.Maximum then
				return
			end

			local ent = ents.Create("pac_projectile")
			SafeRemoveEntityDelayed(ent,math.Clamp(part.LifeTime, 0, 50))

			ent:SetModel("models/props_junk/popcan01a.mdl")
			ent:SetPos(pos)
			ent:SetAngles(ang)
			ent:Spawn()

			if not part.CollideWithOwner then
				ent:SetOwner(ply)
			end

			ent:SetData(ply, pos, ang, part)

			ent:SetPhysicsAttacker(ply)

			if ent.CPPISetOwner then
				ent:CPPISetOwner(ply)
			end

			net.Start("pac_projectile_attach")
				net.WriteEntity(ply)
				net.WriteInt(ent:EntIndex(), 16)
				net.WriteString(part.UniqueID)
			net.Broadcast()

			ent.pac_projectile_uid = part.UniqueID

			ply.pac_projectiles[ent] = ent

			ent.pac_projectile_owner = ply
		end

		if part.Delay == 0 then
			spawn()
		else
			timer.Simple(part.Delay, spawn)
		end
	end)
end
