// Script work by fellen (https://steamcommunity.com/profiles/76561198055313095). class UnholyAssault.HeatSeek { static VisionMask = MASK_BLOCKLOS|CONTENTS_IGNORE_NODRAW_OPAQUE static TickInterval = 0.015 projectile = null team = TEAM_UNASSIGNED speed = 1100.0 turn_power = 360.0 activate_time = -1.0 function constructor(handle, deg_per_sec = 360.0, delay = 0.0, speed_mod = 1.0, spread = 0.0) { projectile = handle team = projectile.GetTeam() local velocity = projectile.GetAbsVelocity() speed = velocity.Length() * speed_mod projectile.SetAbsVelocity(velocity * speed_mod) turn_power = deg_per_sec * TickInterval activate_time = Time() + delay - 0.1 if (spread) { local Ang = projectile.GetLocalAngles() // Force maximum spread angle but only upward. local ang = projectile.GetLocalAngles() ang.x += RandomInt(0, 1) ? 0.0 : -spread ang.y += RandomInt(0, 1) ? spread : -spread projectile.SetLocalAngles(ang) local vec = ang.Forward() projectile.SetAbsVelocity(vec * speed) } NetProps.SetPropBool(projectile, "m_bForcePurgeFixedupStrings", true) projectile.ValidateScriptScope() local scope = projectile.GetScriptScope() scope.HeatSeek <- this scope.HeatSeekThink <- HeatSeekThink.bindenv(this) UnholyAssault.AddContextThink(projectile, "HeatSeekThink") } function HeatSeekThink() { if (activate_time > Time()) return -1.0 local target = SelectTarget() if (target) TurnTo(target) return -1.0 } function SelectTarget() { local target = null local min_dist = FLT_MAX // Find nearest player to the projectile. foreach (player in UnholyAssault.PLAYERS) { if (!IsThreat(player)) continue if (!IsTargetVisible(player)) continue local player_dist = (projectile.GetCenter() - player.GetCenter()).LengthSqr() if (min_dist < player_dist) continue target = player min_dist = player_dist } return target } // Partial replication of CTFBotVision::IsIgnored(). function IsThreat(target) { if (!target.IsAlive()) return false if (target.GetTeam() == team) return false // Set currently to only target players, more effective than targeting buildings etc. if (!target.IsPlayer()) return false if (target.InCond(TF_COND_BURNING) || target.InCond(TF_COND_URINE) || target.InCond(TF_COND_STEALTHED_BLINK) || target.InCond(TF_COND_BLEEDING)) return true // m_Shared.GetPercentInvisible() is not exposed to us. if (target.IsStealthed()) return false // This condition is separated as it only reveals if the player is not cloaked. if (target.InCond(TF_COND_DISGUISING)) return true if (target.GetDisguiseTeam() == team) return false return true } function IsTargetVisible(target) { local trace = { start = projectile.GetCenter(), end = target.GetOrigin(), mask = VisionMask, ignore = projectile } TraceLineEx(trace) return !trace.hit } function TurnTo(target) { local current_vec = projectile.GetForwardVector() local desired_vec = target.GetCenter() - projectile.GetCenter() desired_vec.Norm() local lerp_vec = Lerp(current_vec, desired_vec, turn_power) local approach_ang = VectorToAngles(lerp_vec) projectile.SetLocalAngles(approach_ang) projectile.SetAbsVelocity(lerp_vec * speed) } function Lerp(start, end, factor) { local delta = end - start if (delta.Norm() < factor) return end return start + delta * factor } // Convert vector to angles without roll. function VectorToAngles(vec) { local angles = QAngle() if (vec.x == 0.0 && vec.y == 0.0) { angles.y = 0.0 if (vec.z > 0.0) angles.x = 270.0 else angles.x = 90.0 } else { angles.x = atan2(-vec.z, vec.Length2D()) * 180.0 / PI if (angles.x < 0.0) angles.x += 360.0 angles.y = atan2(vec.y, vec.x) * 180.0 / PI if (angles.y < 0.0) angles.y += 360.0 } return angles } }