AddCSLuaFile() ENT.Type = "anim" ENT.Base = "base_entity" ENT.RenderGroup = RENDERGROUP_TRANSLUCENT ENT.PrintName = "Base Projectile" ENT.Category = "" ENT.Spawnable = false ENT.Model = "" local smokeimages = {"particle/smokesprites_0001", "particle/smokesprites_0002", "particle/smokesprites_0003", "particle/smokesprites_0004", "particle/smokesprites_0005", "particle/smokesprites_0006", "particle/smokesprites_0007", "particle/smokesprites_0008", "particle/smokesprites_0009", "particle/smokesprites_0010", "particle/smokesprites_0011", "particle/smokesprites_0012", "particle/smokesprites_0013", "particle/smokesprites_0014", "particle/smokesprites_0015", "particle/smokesprites_0016"} local function GetSmokeImage() return smokeimages[math.random(#smokeimages)] end ENT.Material = false // custom material ENT.IsRocket = false // projectile has a booster and will not drop. ENT.Sticky = false // projectile sticks on impact ENT.InstantFuse = true // projectile is armed immediately after firing. ENT.TimeFuse = false // projectile will arm after this amount of time ENT.RemoteFuse = false // allow this projectile to be triggered by remote detonator. ENT.ImpactFuse = false // projectile explodes on impact. ENT.StickyFuse = false // projectile becomes timed after sticking. ENT.SafetyFuse = 0 // impact fuse hitting too early will not detonate ENT.RemoveOnImpact = false ENT.ExplodeOnImpact = false ENT.ExplodeOnDamage = false // projectile explodes when it takes damage. ENT.ExplodeUnderwater = false // projectile explodes when it enters water ENT.Defusable = false // press E on the projectile to defuse it ENT.DefuseOnDamage = false ENT.ImpactDamage = 50 ENT.ImpactDamageType = DMG_CRUSH + DMG_CLUB ENT.Delay = 0 // after being triggered and this amount of time has passed, the projectile will explode. ENT.SoundHint = false // Emit a sound hint so NPCs can react ENT.SoundHintDelay = 0 // Delay after spawn ENT.SoundHintRadius = 328 ENT.SoundHintDuration = 1 ENT.Armed = false ENT.SmokeTrail = false // leaves trail of smoke ENT.FlareColor = nil ENT.FlareSizeMin = 200 ENT.FlareSizeMax = 250 // Guided projectile related ENT.SteerDelay = 0 // Delay before steering logic kicks in ENT.SteerSpeed = 60 // Turn rate in degrees per second ENT.SteerBrake = 0 // Amount of speed to slow down by when turning ENT.SeekerAngle = 180 // Angle difference (degrees) above which projectile loses target ENT.SeekerExplodeRange = 256 // Distance to the target below which the missile will immediately explode ENT.SeekerExplodeSnapPosition = true // When exploding on a seeked target, teleport to the entity's position for more damage ENT.SeekerExplodeAngle = 180 // Angle tolerance (degrees) below which detonation can happen ENT.TopAttack = false // This missile will attack from the top ENT.TopAttackHeight = 512 // Distance above target to top attack ENT.TopAttackDistance = 128 // Distance from target to stop top attacking ENT.RocketLifetime = 30 // Rocket will cut after this time ENT.MinSpeed = 0 ENT.MaxSpeed = 0 ENT.Acceleration = 0 ENT.LeadTarget = false // account for target's velocity and distance ENT.SuperSteerTime = 0 // Amount of time where turn rate is boosted ENT.SuperSteerSpeed = 100 // Boosted turn rate in degrees per seconds ENT.NoReacquire = true // If target is lost, it cannot be tracked anymore ENT.FlareRedirectChance = 0 ENT.LockOnEntity = NULL ENT.TargetPos = nil ENT.AudioLoop = nil ENT.BounceSounds = nil ENT.CollisionSphere = nil ENT.GunshipWorkaround = true // Tell LVS to not ricochet us ENT.DisableBallistics = true ENT.TopAttacking = false ENT.StartSuperSteerTime = 0 function ENT:SetupDataTables() self:NetworkVar("Entity", 0, "Weapon") end function ENT:Initialize() if SERVER then self:SetModel(self.Model) self:SetMaterial(self.Material or "") if self.CollisionSphere then self:PhysicsInitSphere(self.CollisionSphere) else self:PhysicsInit(SOLID_VPHYSICS) end self:SetMoveType(MOVETYPE_VPHYSICS) self:SetSolid(SOLID_VPHYSICS) self:SetCollisionGroup(COLLISION_GROUP_PROJECTILE) if self.Defusable then self:SetUseType(SIMPLE_USE) end local phys = self:GetPhysicsObject() if !phys:IsValid() then self:Remove() return end phys:EnableDrag(false) phys:SetDragCoefficient(0) phys:SetBuoyancyRatio(0) phys:Wake() if self.IsRocket then phys:EnableGravity(false) end if self.TopAttack then self.TopAttacking = true end self:SwitchTarget(self.LockOnEntity) end self.StartSuperSteerTime = CurTime() self.SpawnTime = CurTime() self.NextFlareRedirectTime = 0 self.NPCDamage = IsValid(self:GetOwner()) and self:GetOwner():IsNPC() and !TacRP.ConVars["npc_equality"]:GetBool() if self.AudioLoop then self.LoopSound = CreateSound(self, self.AudioLoop) self.LoopSound:Play() end if self.InstantFuse then self.ArmTime = CurTime() self.Armed = true end self:OnInitialize() end function ENT:OnRemove() if self.LoopSound then self.LoopSound:Stop() end end function ENT:OnTakeDamage(dmg) if self.Detonated then return end // self:TakePhysicsDamage(dmg) if self.ExplodeOnDamage then if IsValid(self:GetOwner()) and IsValid(dmg:GetAttacker()) then self:SetOwner(dmg:GetAttacker()) else self.Attacker = dmg:GetAttacker() or self.Attacker end self:PreDetonate() elseif self.DefuseOnDamage and dmg:GetDamageType() != DMG_BLAST then self:EmitSound("physics/plastic/plastic_box_break" .. math.random(1, 2) .. ".wav", 70, math.Rand(95, 105)) local fx = EffectData() fx:SetOrigin(self:GetPos()) fx:SetNormal(self:GetAngles():Forward()) fx:SetAngles(self:GetAngles()) util.Effect("ManhackSparks", fx) self.Detonated = true self:Remove() end end function ENT:PhysicsCollide(data, collider) if IsValid(data.HitEntity) and data.HitEntity:GetClass() == "func_breakable_surf" then self:FireBullets({ Attacker = self:GetOwner(), Inflictor = self, Damage = 0, Distance = 32, Tracer = 0, Src = self:GetPos(), Dir = data.OurOldVelocity:GetNormalized(), }) local pos, ang, vel = self:GetPos(), self:GetAngles(), data.OurOldVelocity self:SetAngles(ang) self:SetPos(pos) self:GetPhysicsObject():SetVelocityInstantaneous(vel * 0.5) return end if (self.SafetyFuse or 0) > 0 and self.SpawnTime + self.SafetyFuse > CurTime() then self:SafetyImpact(data, collider) self:Remove() return elseif self.ImpactFuse then if !self.Armed then self.ArmTime = CurTime() self.Armed = true if self:Impact(data, collider) then return end end if self.Delay == 0 or self.ExplodeOnImpact then self:PreDetonate(data.HitEntity) end elseif self.ImpactDamage > 0 and (self.NextImpactDamage or 0) < CurTime() and data.Speed >= 10 and IsValid(data.HitEntity) and (engine.ActiveGamemode() != "terrortown" or !data.HitEntity:IsPlayer()) then local dmg = DamageInfo() dmg:SetAttacker(IsValid(self:GetOwner()) and self:GetOwner() or self.Attacker) dmg:SetInflictor(self) dmg:SetDamage(self.ImpactDamage) dmg:SetDamageType(self.ImpactDamageType) dmg:SetDamageForce(data.OurOldVelocity) dmg:SetDamagePosition(data.HitPos) data.HitEntity:TakeDamageInfo(dmg) self.NextImpactDamage = CurTime() + 0.05 elseif !self.ImpactFuse then self:Impact(data, collider) end if self.Sticky then self:SetCollisionGroup(COLLISION_GROUP_DEBRIS) self:SetPos(data.HitPos) self:SetAngles((-data.HitNormal):Angle()) if data.HitEntity:IsWorld() or data.HitEntity:GetSolid() == SOLID_BSP then self:SetMoveType(MOVETYPE_NONE) self:SetPos(data.HitPos) else self:SetPos(data.HitPos) self:SetParent(data.HitEntity) end self:EmitSound("TacRP/weapons/plant_bomb.wav", 65) self.Attacker = self:GetOwner() self:SetOwner(NULL) if self.StickyFuse and !self.Armed then self.ArmTime = CurTime() self.Armed = true end self:Stuck() else if !self.Bounced then self.Bounced = true local dot = data.HitNormal:Dot(Vector(0, 0, 1)) if dot < 0 then self:GetPhysicsObject():SetVelocityInstantaneous(data.OurNewVelocity * (1 + dot * 0.5)) end end end if data.DeltaTime < 0.1 then return end if !self.BounceSounds then return end local s = table.Random(self.BounceSounds) self:EmitSound(s) end function ENT:OnThink() end function ENT:OnInitialize() end function ENT:DoSmokeTrail() if CLIENT and self.SmokeTrail then local emitter = ParticleEmitter(self:GetPos()) local smoke = emitter:Add(GetSmokeImage(), self:GetPos()) smoke:SetStartAlpha(50) smoke:SetEndAlpha(0) smoke:SetStartSize(10) smoke:SetEndSize(math.Rand(50, 75)) smoke:SetRoll(math.Rand(-180, 180)) smoke:SetRollDelta(math.Rand(-1, 1)) smoke:SetPos(self:GetPos()) smoke:SetVelocity(-self:GetAngles():Forward() * 400 + (VectorRand() * 10)) smoke:SetColor(200, 200, 200) smoke:SetLighting(true) smoke:SetDieTime(math.Rand(0.75, 1.25)) smoke:SetGravity(Vector(0, 0, 0)) emitter:Finish() end end function ENT:PhysicsUpdate(phys) if self.SpawnTime + self.RocketLifetime < CurTime() then return end if self.TargetPos and (self.SteerDelay + self.SpawnTime) <= CurTime() then local v = phys:GetVelocity() local steer_amount = self.SteerSpeed * FrameTime() if self.SuperSteerTime + self.StartSuperSteerTime > CurTime() then steer_amount = self.SuperSteerSpeed * FrameTime() end local dir = (self.TargetPos - self:GetPos()):GetNormalized() local diff = math.deg(math.acos(dir:Dot(self:GetForward()))) local turn_deg = math.min(diff, steer_amount) local newang = self:GetAngles() newang:RotateAroundAxis(dir:Cross(self:GetForward()), -turn_deg) local brake = turn_deg / steer_amount * self.SteerBrake self:SetAngles(Angle(newang.p, newang.y, 0)) phys:SetVelocityInstantaneous(self:GetForward() * math.Clamp(v:Length() + (self.Acceleration - brake) * FrameTime(), self.MinSpeed, self.MaxSpeed)) elseif self.Acceleration > 0 then phys:SetVelocityInstantaneous(self:GetForward() * math.Clamp(phys:GetVelocity():Length() + self.Acceleration * FrameTime(), self.MinSpeed, self.MaxSpeed)) end end local gunship = {["npc_combinegunship"] = true, ["npc_combinedropship"] = true, ["npc_helicopter"] = true} function ENT:DoTracking() local target = self.LockOnEntity if IsValid(target) then if self.TopAttack and self.TopAttacking then local xyvector = (target:WorldSpaceCenter() - self:GetPos()) xyvector.z = 0 local dist = xyvector:Length() self.TargetPos = target:WorldSpaceCenter() + Vector(0, 0, self.TopAttackHeight) if self.LeadTarget then local dist2 = (self.TargetPos - self:GetPos()):Length() local time = dist2 / self:GetVelocity():Length() self.TargetPos = self.TargetPos + (target:GetVelocity() * time) end if dist <= self.TopAttackDistance then self.TopAttacking = false self.StartSuperSteerTime = CurTime() end else local dir = (target:WorldSpaceCenter() - self:GetPos()):GetNormalized() local diff = math.deg(math.acos(dir:Dot(self:GetForward()))) if diff <= self.SeekerAngle or self.SuperSteerTime + self.StartSuperSteerTime > CurTime() then self.TargetPos = target:WorldSpaceCenter() local dist = (self.TargetPos - self:GetPos()):Length() if self.LeadTarget then local time = dist / self:GetVelocity():Length() self.TargetPos = self.TargetPos + (target:GetVelocity() * time) end if self.FlareRedirectChance > 0 and self.NextFlareRedirectTime <= CurTime() and !TacRP.FlareEntities[target:GetClass()] then local flares = ents.FindInSphere(self:GetPos(), 2048) for k, v in pairs(flares) do if (TacRP.FlareEntities[v:GetClass()] or v.IsTacRPFlare) and math.Rand(0, 1) <= self.FlareRedirectChance then self:SwitchTarget(v) break end end self.NextFlareRedirectTime = CurTime() + 0.5 end if self.SeekerExplodeRange > 0 and diff <= self.SeekerExplodeAngle and self.SteerDelay + self.SpawnTime <= CurTime() and dist < self.SeekerExplodeRange then local tr = util.TraceLine({ start = self:GetPos(), endpos = target:GetPos(), filter = self, mask = MASK_SOLID, }) if self.SeekerExplodeSnapPosition then self:SetPos(tr.HitPos) end self:PreDetonate(target) end elseif self.NoReacquire then self.LockOnEntity = nil self.TargetPos = nil end end elseif (!IsValid(target) and self.NoReacquire) or target.UnTrackable then self.LockOnEntity = nil self.TargetPos = nil end if self.GunshipWorkaround and (self.GunshipCheck or 0 < CurTime()) then self.GunshipCheck = CurTime() + 0.33 local tr = util.TraceLine({ start = self:GetPos(), endpos = self:GetPos() + (self:GetVelocity() * 6 * engine.TickInterval()), filter = self, mask = MASK_SHOT }) if IsValid(tr.Entity) and gunship[tr.Entity:GetClass()] then self:SetPos(tr.HitPos) self:PreDetonate(tr.Entity) end end end function ENT:Think() if !IsValid(self) or self:GetNoDraw() then return end if !self.SpawnTime then self.SpawnTime = CurTime() end if SERVER and self.SoundHint and CurTime() >= self.SpawnTime + self.SoundHintDelay then self.SoundHint = false // only once sound.EmitHint(SOUND_DANGER, self:GetPos(), self.SoundHintRadius, self.SoundHintDuration, self) end if !self.Armed and isnumber(self.TimeFuse) and self.SpawnTime + self.TimeFuse < CurTime() then self.ArmTime = CurTime() self.Armed = true end if self.Armed and self.ArmTime + self.Delay < CurTime() then self:PreDetonate() end if self.ExplodeUnderwater and self:WaterLevel() > 0 then self:PreDetonate() end if SERVER and self.SpawnTime + self.RocketLifetime > CurTime() then self:DoTracking() end self:DoSmokeTrail() self:OnThink() self:NextThink(CurTime()) return true end function ENT:Use(ply) if !self.Defusable then return end self:EmitSound("TacRP/weapons/rifle_jingle-1.wav") if self.PickupAmmo then ply:GiveAmmo(1, self.PickupAmmo, true) end self:Remove() end function ENT:RemoteDetonate() self:EmitSound("TacRP/weapons/c4/relay_switch-1.wav") self.ArmTime = CurTime() self.Armed = true end function ENT:PreDetonate(ent) if CLIENT then return end if !self.Detonated then self.Detonated = true if !IsValid(self.Attacker) and !IsValid(self:GetOwner()) then self.Attacker = game.GetWorld() end self:Detonate(ent) end end function ENT:Detonate(ent) // fill this in :) end function ENT:Impact(data, collider) end function ENT:SafetyImpact(data, collider) local attacker = self.Attacker or self:GetOwner() local ang = data.OurOldVelocity:Angle() local fx = EffectData() fx:SetOrigin(data.HitPos) fx:SetNormal(-ang:Forward()) fx:SetAngles(-ang) util.Effect("ManhackSparks", fx) if IsValid(data.HitEntity) then local dmginfo = DamageInfo() dmginfo:SetAttacker(attacker) dmginfo:SetInflictor(self) dmginfo:SetDamageType(self.ImpactDamageType) dmginfo:SetDamage(self.ImpactDamage * (self.NPCDamage and 0.25 or 1)) dmginfo:SetDamageForce(data.OurOldVelocity * 20) dmginfo:SetDamagePosition(data.HitPos) data.HitEntity:TakeDamageInfo(dmginfo) end self:EmitSound("weapons/rpg/shotdown.wav", 80) if self:GetModel() == "models/weapons/tacint/rocket_deployed.mdl" then for i = 1, 4 do local prop = ents.Create("prop_physics") prop:SetPos(self:GetPos()) prop:SetAngles(self:GetAngles()) prop:SetModel("models/weapons/tacint/rpg7_shrapnel_p" .. i .. ".mdl") prop:Spawn() prop:GetPhysicsObject():SetVelocityInstantaneous(data.OurNewVelocity * 0.5 + VectorRand() * 75) prop:SetCollisionGroup(COLLISION_GROUP_DEBRIS) SafeRemoveEntityDelayed(prop, 3) end end end function ENT:ImpactTraceAttack(ent, damage, pen) if !IsValid(ent) then return end if ent.LVS then // LVS only does its penetration logic on FireBullets, so we must fire a bullet to trigger it self:SetCollisionGroup(COLLISION_GROUP_DEBRIS) // The projectile blocks the penetration decal?! self:FireBullets({ Attacker = self.Attacker or self:GetOwner(), Damage = damage, Tracer = 0, Src = self:GetPos(), Dir = self:GetForward(), HullSize = 16, Distance = 128, IgnoreEntity = self, Callback = function(atk, btr, dmginfo) dmginfo:SetDamageType(DMG_AIRBOAT + DMG_SNIPER) // LVS wants this dmginfo:SetDamageForce(self:GetForward() * pen) // penetration strength end, }) else // This is way more consistent because the damage always lands local tr = util.TraceHull({ start = self:GetPos(), endpos = self:GetPos() + self:GetForward() * 256, filter = ent, whitelist = true, ignoreworld = true, mask = MASK_ALL, mins = Vector( -8, -8, -8 ), maxs = Vector( 8, 8, 8 ), }) local dmginfo = DamageInfo() dmginfo:SetAttacker(self.Attacker or self:GetOwner()) dmginfo:SetInflictor(self) dmginfo:SetDamagePosition(self:GetPos()) dmginfo:SetDamageForce(self:GetForward() * pen) dmginfo:SetDamageType(DMG_AIRBOAT + DMG_SNIPER) dmginfo:SetDamage(damage) ent:DispatchTraceAttack(dmginfo, tr, self:GetForward()) end end function ENT:Stuck() end function ENT:DrawTranslucent() self:Draw() end local mat = Material("effects/ar2_altfire1b") function ENT:Draw() if self:GetOwner() == LocalPlayer() and (self.SpawnTime + 0.05) > CurTime() then return end self:DrawModel() if self.FlareColor then local mult = self.SafetyFuse and math.Clamp((CurTime() - (self.SpawnTime + self.SafetyFuse)) / self.SafetyFuse, 0.1, 1) or 1 render.SetMaterial(mat) render.DrawSprite(self:GetPos() + (self:GetAngles():Forward() * -16), mult * math.Rand(self.FlareSizeMin, self.FlareSizeMax), mult * math.Rand(self.FlareSizeMin, self.FlareSizeMax), self.FlareColor) end end function ENT:SwitchTarget(target) if IsValid(self.LockOnEntity) then if isfunction(self.LockOnEntity.OnLaserLock) then self.LockOnEntity:OnLaserLock(false) end end self.LockOnEntity = target if IsValid(self.LockOnEntity) then if isfunction(self.LockOnEntity.OnLaserLock) then self.LockOnEntity:OnLaserLock(true) end end end hook.Add("EntityTakeDamage", "tacrp_proj_collision", function(ent, dmginfo) if IsValid(dmginfo:GetInflictor()) and scripted_ents.IsBasedOn(dmginfo:GetInflictor():GetClass(), "tacrp_proj_base") and dmginfo:GetDamageType() == DMG_CRUSH then dmginfo:SetDamage(0) return true end end)