|
| 1 | +dofile("Base.rte/Constants.lua") |
| 2 | +require("AI/NativeCrabAI") |
| 3 | + |
| 4 | +function Create(self) |
| 5 | + self.AI = NativeCrabAI:Create(self) |
| 6 | + self.SearchTimer = Timer() |
| 7 | + self.radarRange = 1024 |
| 8 | + self.ValidTargets = {} |
| 9 | + self.Frame = 1 |
| 10 | + |
| 11 | + -- The "AA-Drone Ammo Counter" object tracks ammo across battles and reduce gold cost of the carrier |
| 12 | + if self:HasObject("AA-Drone Ammo Counter") then |
| 13 | + if self.InventorySize == 1 then |
| 14 | + self.Frame = 2 -- One SAM left |
| 15 | + else |
| 16 | + self.Frame = 0 -- Out of ammo |
| 17 | + end |
| 18 | + end |
| 19 | + |
| 20 | + function self.UpdateInertSAM(self) |
| 21 | + if self.SearchTimer:IsPastSimMS(300) then |
| 22 | + local ArmedSAM = CreateAEmitter("Armed SAM", "Base.rte") |
| 23 | + if ArmedSAM then |
| 24 | + self.SAM.ToDelete = true |
| 25 | + self.SAM.HitsMOs = false |
| 26 | + self.SAM.GetsHitByMOs = false |
| 27 | + |
| 28 | + ArmedSAM.Pos = self.SAM.Pos |
| 29 | + ArmedSAM.Vel = self.SAM.Vel |
| 30 | + ArmedSAM.Team = self.SAM.Team |
| 31 | + ArmedSAM.IgnoresTeamHits = true |
| 32 | + ArmedSAM.RotAngle = self.SAM.RotAngle |
| 33 | + ArmedSAM.AngularVel = self.SAM.AngularVel |
| 34 | + |
| 35 | + self.proximityFuze = nil -- Reset the proximity fuze |
| 36 | + |
| 37 | + -- Don't target the center of a craft |
| 38 | + if self.SAM_Target.ClassName == "ACDropShip" then |
| 39 | + if self.SAM_Target.Pos.X > self.SAM.Pos.X then |
| 40 | + self.SAM_Offset = Vector(-self.SAM_Target.Radius, 0) -- Left |
| 41 | + else |
| 42 | + self.SAM_Offset = Vector(self.SAM_Target.Radius, 0) -- Right |
| 43 | + end |
| 44 | + else |
| 45 | + self.SAM_Offset = Vector(0, self.SAM_Target.Radius * 0.5) -- Below |
| 46 | + end |
| 47 | + |
| 48 | + -- Initialize missile velocity history and targeting data |
| 49 | + self.SAM_AimPos = self.SAM_Target.Pos + self.SAM_Target:RotateOffset(self.SAM_Offset) |
| 50 | + self.SAM_LastVel = Vector(self.SAM.Vel.X, self.SAM.Vel.Y) |
| 51 | + |
| 52 | + self.SAM = ArmedSAM |
| 53 | + MovableMan:AddMO(self.SAM) |
| 54 | + |
| 55 | + self.UpdateSAM = self.UpdateArmedSAM -- Use the UpdateArmedSAM-function from now on |
| 56 | + else |
| 57 | + self.SAM = nil |
| 58 | + end |
| 59 | + else |
| 60 | + local angError = math.asin(Vector(0, -1):Cross(self.SAM:RotateOffset(Vector(1, 0)))) -- The angle between missile facing and straight up |
| 61 | + self.SAM.RotAngle = self.SAM.RotAngle + math.min(math.max(angError, -0.02), 0.02) |
| 62 | + self.SAM.AngularVel = self.SAM.AngularVel * 0.95 |
| 63 | + end |
| 64 | + end |
| 65 | + |
| 66 | + function self.UpdateArmedSAM(self) |
| 67 | + -- Find the velocity vector that will take the missile to the target |
| 68 | + local FutureVel = self.SAM.Vel + (self.SAM.Vel - self.SAM_LastVel) * 10 |
| 69 | + local OptimalVel = SceneMan:ShortestDistance(self.SAM.Pos, self.SAM_AimPos, false) |
| 70 | + local angError = math.asin(OptimalVel.Normalized:Cross(FutureVel.Normalized)) -- The angle between FutureVel and OptimalVel |
| 71 | + |
| 72 | + -- Gradually turn towards the optimal velocity vector |
| 73 | + self.SAM.RotAngle = self.SAM.RotAngle + math.min(math.max(angError, -0.04), 0.04) |
| 74 | + |
| 75 | + -- Gradually return the thruster to the starting position if the missile is facing the target |
| 76 | + if math.abs(angError) < 0.15 then |
| 77 | + self.SAM.EmitAngle = self.SAM.EmitAngle * 0.8 + math.pi * 0.2 |
| 78 | + else |
| 79 | + self.SAM.EmitAngle = math.max(math.min(self.SAM.EmitAngle + angError * 0.1, 4.14), 2.14) -- Vector thrust |
| 80 | + end |
| 81 | + |
| 82 | + -- Detonate the missile when appropriate |
| 83 | + local range = SceneMan:ShortestDistance( |
| 84 | + self.SAM.Pos, |
| 85 | + self.SAM_Target.Pos + self.SAM_Target:RotateOffset(self.SAM_Offset), |
| 86 | + false |
| 87 | + ).Magnitude |
| 88 | + if self.proximityFuze then |
| 89 | + if range < 30 then |
| 90 | + self.SAM:GibThis() -- The target is close enough; detonate |
| 91 | + elseif math.abs(angError) > 1.5 and range > self.proximityFuze then -- The missile is moving away from the target: detonate |
| 92 | + self.SAM:GibThis() |
| 93 | + else |
| 94 | + self.proximityFuze = range |
| 95 | + end |
| 96 | + elseif range < 120 then -- The target is close: arm the proximity fuze |
| 97 | + self.proximityFuze = range |
| 98 | + end |
| 99 | + |
| 100 | + self.SAM.AngularVel = self.SAM.AngularVel * 0.96 + (self.SAM.EmitAngle - math.pi) * 0.05 -- The vector thrust will cause the SAM to rotate |
| 101 | + self.SAM_LastVel = self.SAM_LastVel * 0.6 + self.SAM.Vel * 0.4 -- Used to calculate the acceleration of the missile |
| 102 | + self.SAM_AimPos = self.SAM_AimPos * 0.6 |
| 103 | + + ( |
| 104 | + self.SAM_Target.Pos |
| 105 | + + self.SAM_Target:RotateOffset(self.SAM_Offset) |
| 106 | + + self.SAM_Target.Vel * math.min(range / 50, 20) |
| 107 | + ) |
| 108 | + * 0.4 -- Filter the AimPos to reduce noise |
| 109 | + end |
| 110 | +end |
| 111 | + |
| 112 | +function Update(self) |
| 113 | + if self.SAM then -- Check if any old missile is alive |
| 114 | + if MovableMan:ValidMO(self.SAM) then |
| 115 | + if self.SAM_Target and MovableMan:ValidMO(self.SAM_Target) then |
| 116 | + self.UpdateSAM(self) |
| 117 | + else |
| 118 | + -- The target is not valid any more: replace the missile with an intert one |
| 119 | + if self.SAM.PresetName == "Armed SAM" then |
| 120 | + local InertSAM = CreateAEmitter("Inert SAM", "Base.rte") |
| 121 | + if InertSAM then |
| 122 | + self.SAM.ToDelete = true |
| 123 | + self.SAM.HitsMOs = false |
| 124 | + self.SAM.GetsHitByMOs = false |
| 125 | + |
| 126 | + InertSAM.Pos = self.SAM.Pos |
| 127 | + InertSAM.Vel = self.SAM.Vel |
| 128 | + InertSAM.RotAngle = self.SAM.RotAngle |
| 129 | + InertSAM.AngularVel = self.SAM.AngularVel |
| 130 | + InertSAM.Team = self.Team |
| 131 | + InertSAM.IgnoresTeamHits = true |
| 132 | + MovableMan:AddMO(InertSAM) |
| 133 | + end |
| 134 | + end |
| 135 | + |
| 136 | + self.SAM = nil |
| 137 | + self.SAM_Target = nil |
| 138 | + end |
| 139 | + else |
| 140 | + self.SAM = nil |
| 141 | + end |
| 142 | + elseif self.Frame > 0 and self.Vel.Largest < 12 then -- we have SAMs left |
| 143 | + if #self.ValidTargets < 1 then -- Find valid targets |
| 144 | + if self.SearchTimer:IsPastSimMS(100) then |
| 145 | + self.SearchTimer:Reset() -- Only search a few times/sec to reduce calculations per update |
| 146 | + |
| 147 | + -- Only look for targets if there are no obstacles above us |
| 148 | + local Trace = self:RotateOffset(Vector(0, -200)) |
| 149 | + if not SceneMan:CastStrengthRay(self.Pos, Trace, 5, Vector(), 9, -1, true) then -- Terrain str 5 |
| 150 | + local obstructed = false |
| 151 | + local ID = SceneMan:CastMORay(self.AboveHUDPos, Trace, self.ID, self.IgnoresWhichTeam, 0, true, 15) |
| 152 | + if ID < rte.NoMOID then |
| 153 | + local MO = MovableMan:GetMOFromID(ID) |
| 154 | + if ID ~= MO.RootID then |
| 155 | + MO = MovableMan:GetMOFromID(MO.RootID) |
| 156 | + end |
| 157 | + |
| 158 | + if MO.Team == self.Team then |
| 159 | + obstructed = true -- The MO above us is on our team: don't shoot |
| 160 | + end |
| 161 | + end |
| 162 | + |
| 163 | + if not obstructed then |
| 164 | + local Dist, range, angle |
| 165 | + for Act in MovableMan.Actors do |
| 166 | + if Act.Team ~= self.Team and not Act:IsDead() and Act:HasObjectInGroup("Craft") then |
| 167 | + Dist = SceneMan:ShortestDistance(self.Pos, Act.Pos + Act.Vel * 9, false) |
| 168 | + if |
| 169 | + Act.Vel.Y > 0 or ((Dist.X > 0 and Act.Vel.X < -5) or (Dist.X < 0 and Act.Vel.X > 5)) |
| 170 | + then -- Only shoot at craft moving down, or moving towards us |
| 171 | + range = Dist.Magnitude - Act.Radius |
| 172 | + if range < self.radarRange then -- Shoot at enemy craft within radarRange pixels |
| 173 | + angle = math.abs( |
| 174 | + math.asin(Dist.Normalized:Cross(self:RotateOffset(Vector(0, -1)))) |
| 175 | + ) |
| 176 | + if angle < 1.7 then -- Search in a ~200 degree arc above us |
| 177 | + table.insert( |
| 178 | + self.ValidTargets, |
| 179 | + { |
| 180 | + Actor = Act, |
| 181 | + priority = angle / 3 |
| 182 | + + range / 300 |
| 183 | + + (3 - Act.Health / 100) |
| 184 | + - math.abs(Act.AngularVel), |
| 185 | + } |
| 186 | + ) -- prioritize close, damaged targets that are straight above us that does not spin |
| 187 | + end |
| 188 | + end |
| 189 | + end |
| 190 | + end |
| 191 | + end |
| 192 | + end |
| 193 | + end |
| 194 | + end |
| 195 | + |
| 196 | + -- Sort the targets in ascending order |
| 197 | + if #self.ValidTargets > 1 then |
| 198 | + table.sort(self.ValidTargets, function(A, B) |
| 199 | + return A.priority > B.priority |
| 200 | + end) |
| 201 | + end |
| 202 | + |
| 203 | + -- Store brain location |
| 204 | + local GmActiv = ActivityMan:GetActivity() |
| 205 | + for player = Activity.PLAYER_1, Activity.MAXPLAYERCOUNT - 1 do |
| 206 | + if GmActiv:PlayerActive(player) and GmActiv:GetTeamOfPlayer(player) == self.Team then |
| 207 | + local Brain = GmActiv:GetPlayerBrain(player) |
| 208 | + if Brain and MovableMan:IsActor(Brain) then |
| 209 | + self.MyBrainPos = Vector(Brain.Pos.X, Brain.Pos.Y) |
| 210 | + end |
| 211 | + |
| 212 | + break |
| 213 | + end |
| 214 | + end |
| 215 | + else -- Check if the missile have a clear line of sight to any of the selected targets |
| 216 | + local NewTarget = table.remove(self.ValidTargets).Actor -- Only check one target to reduce calculations per update |
| 217 | + if NewTarget and MovableMan:ValidMO(NewTarget) and not NewTarget:IsDead() then |
| 218 | + local Trace = SceneMan:ShortestDistance(self.AboveHUDPos, NewTarget.Pos, false) |
| 219 | + -- Don't shoot at targets that are out of reach |
| 220 | + if Trace.Magnitude < self.radarRange then |
| 221 | + -- Don't shoot at targets that are very close to the brain |
| 222 | + if |
| 223 | + SceneMan:ShortestDistance(self.MyBrainPos or self.Pos, NewTarget.Pos, false).Largest |
| 224 | + - NewTarget.Radius |
| 225 | + > 120 |
| 226 | + then |
| 227 | + -- First do a very inexact scan of half the distance to the target for friendly dropships and terrain |
| 228 | + if |
| 229 | + SceneMan:CastObstacleRay( |
| 230 | + self.AboveHUDPos, |
| 231 | + Trace * 0.5, |
| 232 | + Vector(), |
| 233 | + Vector(), |
| 234 | + self.ID, |
| 235 | + self.IgnoresWhichTeam, |
| 236 | + 0, |
| 237 | + 25 |
| 238 | + ) < 0 |
| 239 | + then |
| 240 | + -- If nothing was found, do a more exact scan for terrain all the way to the target |
| 241 | + if not SceneMan:CastStrengthRay(self.AboveHUDPos, Trace, 5, Vector(), 9, -1, true) then -- Terrain str 5 |
| 242 | + self.SearchTimer:Reset() |
| 243 | + |
| 244 | + -- Spawn the SAM |
| 245 | + self.SAM = CreateAEmitter("Inert SAM", "Base.rte") |
| 246 | + if self.SAM then |
| 247 | + local SpawnOffset = Vector(0, -15) |
| 248 | + if self.Frame < 2 then |
| 249 | + self.Frame = 2 -- Remove the left SAM |
| 250 | + SpawnOffset.X = -10 |
| 251 | + else |
| 252 | + self.Frame = 0 -- Remove the right SAM |
| 253 | + SpawnOffset.X = 10 |
| 254 | + end |
| 255 | + |
| 256 | + self.SAM.Team = self.Team |
| 257 | + self.SAM.RotAngle = self.RotAngle + 1.571 |
| 258 | + self.SAM.AngularVel = self.AngularVel |
| 259 | + self.SAM.Pos = self.Pos + self:RotateOffset(SpawnOffset) |
| 260 | + self.SAM.Vel = self.Vel + self:RotateOffset(Vector(0, -17)) |
| 261 | + self.SAM.IgnoresTeamHits = true |
| 262 | + self.SAM:TriggerBurst() |
| 263 | + MovableMan:AddMO(self.SAM) |
| 264 | + |
| 265 | + self.armedSAM = false |
| 266 | + self.SAM_Target = NewTarget |
| 267 | + |
| 268 | + -- Call this function to update the missile |
| 269 | + self.UpdateSAM = self.UpdateInertSAM |
| 270 | + end |
| 271 | + |
| 272 | + -- Add an invisible object to the inventory to track ammo |
| 273 | + local AmmoCounter = CreateMOSRotating("AA-Drone Ammo Counter", "Base.rte") |
| 274 | + if AmmoCounter then |
| 275 | + self:AddInventoryItem(AmmoCounter) |
| 276 | + end |
| 277 | + end |
| 278 | + end |
| 279 | + end |
| 280 | + end |
| 281 | + end |
| 282 | + end |
| 283 | + end |
| 284 | +end |
| 285 | + |
| 286 | +function UpdateAI(self) |
| 287 | + self.AI:Update(self) |
| 288 | +end |
| 289 | + |
| 290 | +function Destroy(self) |
| 291 | + self.AI:Destroy(self) |
| 292 | +end |
0 commit comments