void AI::Attack(Ship &ship, Command &command, const Ship &target) { Point d = target.Position() - ship.Position(); // First, figure out what your shortest-range weapon is. double shortestRange = 4000.; for(const Armament::Weapon &weapon : ship.Weapons()) { const Outfit *outfit = weapon.GetOutfit(); if(outfit && !weapon.IsAntiMissile()) shortestRange = min(outfit->Range(), shortestRange); } // Deploy any fighters you are carrying. if(!ship.IsYours()) command |= Command::DEPLOY; // If this ship only has long-range weapons, it should keep its distance // instead of trying to close with the target ship. if(shortestRange > 1000. && d.Length() < .5 * shortestRange) { command.SetTurn(TurnToward(ship, -d)); if(ship.Facing().Unit().Dot(d) <= 0.) command |= Command::FORWARD; return; } // First of all, aim in the direction that will hit this target. command.SetTurn(TurnToward(ship, TargetAim(ship))); // Calculate this ship's "turning radius; that is, the smallest circle it // can make while at full speed. double stepsInFullTurn = 360. / ship.TurnRate(); double circumference = stepsInFullTurn * ship.Velocity().Length(); double diameter = max(200., circumference / PI); // This isn't perfect, but it works well enough. if((ship.Facing().Unit().Dot(d) >= 0. && d.Length() > diameter) || (ship.Velocity().Dot(d) < 0. && ship.Facing().Unit().Dot(d.Unit()) >= .9)) command |= Command::FORWARD; }
void Engine::CalculateStep() { FrameTimer loadTimer; // Clear the list of objects to draw. draw[calcTickTock].Clear(step); radar[calcTickTock].Clear(); if(!player.GetSystem()) return; // Now, all the ships must decide what they are doing next. ai.Step(ships, player); const Ship *flagship = player.Flagship(); bool wasHyperspacing = (flagship && flagship->IsEnteringHyperspace()); // Now, move all the ships. We must finish moving all of them before any of // them fire, or their turrets will be targeting where a given ship was // instead of where it is now. This is also where ships get deleted, and // where they may create explosions if they are dying. for(auto it = ships.begin(); it != ships.end(); ) { int hyperspaceType = (*it)->HyperspaceType(); bool wasHere = (flagship && (*it)->GetSystem() == flagship->GetSystem()); bool wasHyperspacing = (*it)->IsHyperspacing(); // Give the ship the list of effects so that it can draw explosions, // ion sparks, jump drive flashes, etc. if(!(*it)->Move(effects, flotsam)) { // If Move() returns false, it means the ship should be removed from // play. That may be because it was destroyed, because it is an // ordinary ship that has been out of system for long enough to be // "forgotten," or because it is a fighter that just docked with its // mothership. Report it destroyed if that's really what happened: if((*it)->IsDestroyed()) eventQueue.emplace_back(nullptr, *it, ShipEvent::DESTROY); it = ships.erase(it); } else { if(&**it != flagship) { // Did this ship just begin hyperspacing? if(wasHere && !wasHyperspacing && (*it)->IsHyperspacing()) Audio::Play( Audio::Get(hyperspaceType >= 200 ? "jump out" : "hyperdrive out"), (*it)->Position()); // Did this ship just jump into the player's system? if(!wasHere && flagship && (*it)->GetSystem() == flagship->GetSystem()) Audio::Play( Audio::Get(hyperspaceType >= 200 ? "jump in" : "hyperdrive in"), (*it)->Position()); } // Boarding: bool autoPlunder = !(*it)->GetGovernment()->IsPlayer(); shared_ptr<Ship> victim = (*it)->Board(autoPlunder); if(victim) eventQueue.emplace_back(*it, victim, (*it)->GetGovernment()->IsEnemy(victim->GetGovernment()) ? ShipEvent::BOARD : ShipEvent::ASSIST); ++it; } } if(!wasHyperspacing && flagship && flagship->IsEnteringHyperspace()) Audio::Play(Audio::Get(flagship->HyperspaceType() >= 200 ? "jump drive" : "hyperdrive")); if(flagship && player.GetSystem() != flagship->GetSystem()) { // Wormhole travel: if(!wasHyperspacing) for(const auto &it : player.GetSystem()->Objects()) if(it.GetPlanet() && it.GetPlanet()->IsWormhole() && it.GetPlanet()->WormholeDestination(player.GetSystem()) == flagship->GetSystem()) player.Visit(it.GetPlanet()); doFlash = Preferences::Has("Show hyperspace flash"); player.SetSystem(flagship->GetSystem()); EnterSystem(); } // Draw the planets. Point newCenter = center; Point newCenterVelocity; if(flagship) { newCenter = flagship->Position(); newCenterVelocity = flagship->Velocity(); } else doClick = false; draw[calcTickTock].SetCenter(newCenter, newCenterVelocity); radar[calcTickTock].SetCenter(newCenter); for(const StellarObject &object : player.GetSystem()->Objects()) if(object.HasSprite()) { // Don't apply motion blur to very large planets and stars. if(object.Width() >= 280.) draw[calcTickTock].AddUnblurred(object); else draw[calcTickTock].Add(object); double r = max(2., object.Radius() * .03 + .5); radar[calcTickTock].Add(RadarType(object), object.Position(), r, r - 1.); if(object.GetPlanet()) object.GetPlanet()->DeployDefense(ships); Point position = object.Position() - newCenter; if(doClick && object.GetPlanet() && (clickPoint - position).Length() < object.Radius()) { if(&object == player.Flagship()->GetTargetPlanet()) { if(!object.GetPlanet()->CanLand()) Messages::Add("The authorities on " + object.GetPlanet()->Name() + " refuse to let you land."); else { clickCommands |= Command::LAND; Messages::Add("Landing on " + object.GetPlanet()->Name() + "."); } } else player.Flagship()->SetTargetPlanet(&object); } } // Add all neighboring systems to the radar. const System *targetSystem = flagship ? flagship->GetTargetSystem() : nullptr; const vector<const System *> &links = (flagship && flagship->Attributes().Get("jump drive")) ? player.GetSystem()->Neighbors() : player.GetSystem()->Links(); for(const System *system : links) radar[calcTickTock].AddPointer( (system == targetSystem) ? Radar::SPECIAL : Radar::INACTIVE, system->Position() - player.GetSystem()->Position()); // Now that the planets have been drawn, we can draw the asteroids on top // of them. This could be done later, as long as it is done before the // collision detection. asteroids.Step(); asteroids.Draw(draw[calcTickTock], newCenter); // Move existing projectiles. Do this before ships fire, which will create // new projectiles, since those should just stay where they are created for // this turn. This is also where projectiles get deleted, which may also // result in a "die" effect or a sub-munition being created. We could not // move the projectiles before this because some of them are homing and need // to know the current positions of the ships. list<Projectile> newProjectiles; for(auto it = projectiles.begin(); it != projectiles.end(); ) { if(!it->Move(effects)) { it->MakeSubmunitions(newProjectiles); it = projectiles.erase(it); } else ++it; } projectiles.splice(projectiles.end(), newProjectiles); // Move the flotsam, which should be drawn underneath the ships. for(auto it = flotsam.begin(); it != flotsam.end(); ) { if(!it->Move(effects)) { it = flotsam.erase(it); continue; } Ship *collector = nullptr; for(const shared_ptr<Ship> &ship : ships) { if(ship->GetSystem() != player.GetSystem() || ship->CannotAct()) continue; if(ship.get() == it->Source() || ship->Cargo().Free() < it->UnitSize()) continue; const Mask &mask = ship->GetMask(step); if(mask.Contains(it->Position() - ship->Position(), ship->Facing())) { collector = ship.get(); break; } } if(collector) { string name; if(collector->IsYours()) { if(collector->GetParent()) name = "Your ship \"" + collector->Name() + "\" picked up "; else name = "You picked up "; } string commodity; int amount = 0; if(it->OutfitType()) { amount = collector->Cargo().Add(it->OutfitType(), it->Count()); if(!name.empty()) { if(it->OutfitType()->Get("installable") < 0.) commodity = it->OutfitType()->Name(); else Messages::Add(name + Format::Number(amount) + " " + it->OutfitType()->Name() + (amount == 1 ? "." : "s.")); } } else { amount = collector->Cargo().Add(it->CommodityType(), it->Count()); if(!name.empty()) commodity = it->CommodityType(); } if(!commodity.empty()) Messages::Add(name + (amount == 1 ? "a ton" : Format::Number(amount) + " tons") + " of " + Format::LowerCase(commodity) + "."); it = flotsam.erase(it); continue; } // Draw this flotsam. draw[calcTickTock].Add(*it); ++it; } // Keep track of the relative strength of each government in this system. Do // not add more ships to make a winning team even stronger. This is mostly // to avoid having the player get mobbed by pirates, say, if they hang out // in one system for too long. map<const Government *, int64_t> strength; // Now, ships fire new projectiles, which includes launching fighters. If an // anti-missile system is ready to fire, it does not actually fire unless a // missile is detected in range during collision detection, below. vector<Ship *> hasAntiMissile; double clickRange = 50.; const Ship *previousTarget = nullptr; const Ship *clickTarget = nullptr; if(player.Flagship() && player.Flagship()->GetTargetShip()) previousTarget = &*player.Flagship()->GetTargetShip(); bool showFlagship = false; bool hasHostiles = false; for(shared_ptr<Ship> &ship : ships) if(ship->GetSystem() == player.GetSystem()) { strength[ship->GetGovernment()] += ship->Cost(); // Note: if a ship "fires" a fighter, that fighter was already in // existence and under the control of the same AI as the ship, but // its system was null to mark that it was not active. ship->Launch(ships); if(ship->Fire(projectiles, effects)) hasAntiMissile.push_back(ship.get()); int scan = ship->Scan(); if(scan) { shared_ptr<Ship> target = ship->GetTargetShip(); if(target && target->IsTargetable()) eventQueue.emplace_back(ship, target, scan); } // This is a good opportunity to draw all the ships in system. if(!ship->HasSprite()) continue; // Draw the flagship separately, on top of everything else. if(ship.get() != flagship) { AddSprites(*ship); if(ship->IsThrusting()) { for(const auto &it : ship->Attributes().FlareSounds()) if(it.second > 0) Audio::Play(it.first, ship->Position()); } } else showFlagship = true; // Do not show cloaked ships on the radar, except the player's ships. bool isPlayer = ship->GetGovernment()->IsPlayer(); if(ship->Cloaking() == 1. && !isPlayer) continue; if(doClick && &*ship != player.Flagship() && ship->IsTargetable()) { Point position = ship->Position() - newCenter; const Mask &mask = ship->GetMask(step); double range = mask.Range(clickPoint - position, ship->Facing()); if(range <= clickRange) { clickRange = range; clickTarget = ship.get(); player.Flagship()->SetTargetShip(ship); // If we've found an enemy within the click zone, favor // targeting it rather than any other ship. Otherwise, keep // checking for hits because another ship might be an enemy. if(!range && ship->GetGovernment()->IsEnemy()) doClick = false; } } double size = sqrt(ship->Width() + ship->Height()) * .14 + .5; bool isYourTarget = (flagship && ship == flagship->GetTargetShip()); int type = RadarType(*ship); hasHostiles |= (type == Radar::HOSTILE); radar[calcTickTock].Add(isYourTarget ? Radar::SPECIAL : type, ship->Position(), size); } if(flagship && showFlagship) { AddSprites(*flagship); if(flagship->IsThrusting()) { for(const auto &it : flagship->Attributes().FlareSounds()) if(it.second > 0) Audio::Play(it.first); } } if(clickTarget && clickTarget == previousTarget) clickCommands |= Command::BOARD; if(alarmTime) --alarmTime; else if(hasHostiles && !hadHostiles) { if(Preferences::Has("Warning siren")) Audio::Play(Audio::Get("alarm")); alarmTime = 180; hadHostiles = true; } else if(!hasHostiles) hadHostiles = false; // Collision detection: if(grudgeTime) --grudgeTime; for(Projectile &projectile : projectiles) { // The asteroids can collide with projectiles, the same as any other // object. If the asteroid turns out to be closer than the ship, it // shields the ship (unless the projectile has a blast radius). Point hitVelocity; double closestHit = 0.; shared_ptr<Ship> hit; const Government *gov = projectile.GetGovernment(); // If this "projectile" is a ship explosion, it always explodes. if(gov) { closestHit = asteroids.Collide(projectile, step, &hitVelocity); // Projectiles can only collide with ships that are in the current // system and are not landing, and that are hostile to this projectile. for(shared_ptr<Ship> &ship : ships) if(ship->GetSystem() == player.GetSystem() && !ship->IsLanding() && ship->Cloaking() < 1.) { if(ship.get() != projectile.Target() && !gov->IsEnemy(ship->GetGovernment())) continue; // This returns a value of 0 if the projectile has a trigger // radius and the ship is within it. double range = projectile.CheckCollision(*ship, step); if(range < closestHit) { closestHit = range; hit = ship; hitVelocity = ship->Velocity(); } } } if(closestHit < 1.) { // Create the explosion the given distance along the projectile's // motion path for this step. projectile.Explode(effects, closestHit, hitVelocity); // If this projectile has a blast radius, find all ships within its // radius. Otherwise, only one is damaged. if(projectile.HasBlastRadius()) { // Even friendly ships can be hit by the blast. for(shared_ptr<Ship> &ship : ships) if(ship->GetSystem() == player.GetSystem() && ship->Zoom() == 1.) if(projectile.InBlastRadius(*ship, step, closestHit)) { int eventType = ship->TakeDamage(projectile, ship != hit); if(eventType) eventQueue.emplace_back( projectile.GetGovernment(), ship, eventType); } } else if(hit) { int eventType = hit->TakeDamage(projectile); if(eventType) eventQueue.emplace_back( projectile.GetGovernment(), hit, eventType); } if(hit) DoGrudge(hit, projectile.GetGovernment()); } else if(projectile.MissileStrength()) { bool isEnemy = projectile.GetGovernment() && projectile.GetGovernment()->IsEnemy(); radar[calcTickTock].Add( isEnemy ? Radar::SPECIAL : Radar::INACTIVE, projectile.Position(), 1.); // If the projectile did not hit anything, give the anti-missile // systems a chance to shoot it down. for(Ship *ship : hasAntiMissile) if(ship == projectile.Target() || gov->IsEnemy(ship->GetGovernment()) || ship->GetGovernment()->IsEnemy(gov)) if(ship->FireAntiMissile(projectile, effects)) { projectile.Kill(); break; } } else if(projectile.HasBlastRadius()) radar[calcTickTock].Add(Radar::SPECIAL, projectile.Position(), 1.8); // Now, we can draw the projectile. The motion blur should be reduced // depending on how much motion blur is in the sprite itself: double innateVelocity = 2. * projectile.GetWeapon().Velocity(); Point relativeVelocity = projectile.Velocity() - projectile.Unit() * innateVelocity; draw[calcTickTock].AddProjectile(projectile, relativeVelocity, closestHit); } // Finally, draw all the effects, and then move them (because their motion // is not dependent on anything else, and this way we do all the work on // them in a single place. for(auto it = effects.begin(); it != effects.end(); ) { draw[calcTickTock].AddUnblurred(*it); if(!it->Move()) it = effects.erase(it); else ++it; } // Add incoming ships. for(const System::FleetProbability &fleet : player.GetSystem()->Fleets()) if(!Random::Int(fleet.Period())) { const Government *gov = fleet.Get()->GetGovernment(); if(!gov) continue; int64_t enemyStrength = 0; for(const auto &it : strength) if(gov->IsEnemy(it.first)) enemyStrength += it.second; if(enemyStrength && strength[gov] > 2 * enemyStrength) continue; fleet.Get()->Enter(*player.GetSystem(), ships); } if(!Random::Int(36000) && !player.GetSystem()->Links().empty()) { // Loop through all persons once to see if there are any who can enter // this system. int sum = 0; for(const auto &it : GameData::Persons()) sum += it.second.Frequency(player.GetSystem()); if(sum) { // Adjustment factor: special persons will appear once every ten // minutes, but much less frequently if the game only specifies a // few of them. This way, they will become more common as I add // more, without needing to change the 10-minute constant above. sum = Random::Int(sum + 1000); for(const auto &it : GameData::Persons()) { const Person &person = it.second; sum -= person.Frequency(player.GetSystem()); if(sum < 0) { shared_ptr<Ship> ship = person.GetShip(); ship->Recharge(); ship->SetName(it.first); ship->SetGovernment(person.GetGovernment()); ship->SetPersonality(person.GetPersonality()); ship->SetHail(person.GetHail()); Fleet::Enter(*player.GetSystem(), *ship); ships.push_front(ship); break; } } } } // Occasionally have some ship hail you. if(!Random::Int(600) && !ships.empty()) { shared_ptr<Ship> source; unsigned i = Random::Int(ships.size()); for(const shared_ptr<Ship> &it : ships) if(!i--) { source = it; break; } if(source->GetGovernment() && !source->GetGovernment()->IsPlayer() && !source->IsDisabled() && source->Crew()) { string message = source->GetHail(); if(!message.empty() && source->GetSystem() == player.GetSystem()) Messages::Add(source->GetGovernment()->GetName() + " ship \"" + source->Name() + "\": " + message); } } // A mouse click should only be active for a single step. doClick = false; // Keep track of how much of the CPU time we are using. loadSum += loadTimer.Time(); if(++loadCount == 60) { load = loadSum; loadSum = 0.; loadCount = 0; } }
// Pick a new target for the given ship. shared_ptr<Ship> AI::FindTarget(const Ship &ship, const list<shared_ptr<Ship>> &ships) const { // If this ship has no government, it has no enemies. shared_ptr<Ship> target; const Government *gov = ship.GetGovernment(); if(!gov) return target; bool isPlayerEscort = ship.IsYours(); if(isPlayerEscort) { shared_ptr<Ship> locked = sharedTarget.lock(); if(locked && locked->GetSystem() == ship.GetSystem() && !locked->IsDisabled()) return locked; } // If this ship is not armed, do not make it fight. double minRange = numeric_limits<double>::infinity(); double maxRange = 0.; for(const Armament::Weapon &weapon : ship.Weapons()) if(weapon.GetOutfit() && !weapon.IsAntiMissile()) { minRange = min(minRange, weapon.GetOutfit()->Range()); maxRange = max(maxRange, weapon.GetOutfit()->Range()); } if(!maxRange) return target; shared_ptr<Ship> oldTarget = ship.GetTargetShip(); if(oldTarget && !oldTarget->IsTargetable()) oldTarget.reset(); shared_ptr<Ship> parentTarget; if(ship.GetParent()) parentTarget = ship.GetParent()->GetTargetShip(); if(parentTarget && !parentTarget->IsTargetable()) parentTarget.reset(); // Find the closest enemy ship (if there is one). If this ship is "heroic," // it will attack any ship in system. Otherwise, if all its weapons have a // range higher than 2000, it will engage ships up to 50% beyond its range. // If a ship has short range weapons and is not heroic, it will engage any // ship that is within 3000 of it. const Personality &person = ship.GetPersonality(); double closest = person.IsHeroic() ? numeric_limits<double>::infinity() : (minRange > 1000.) ? maxRange * 1.5 : 4000.; const System *system = ship.GetSystem(); bool isDisabled = false; for(const auto &it : ships) if(it->GetSystem() == system && it->IsTargetable() && gov->IsEnemy(it->GetGovernment())) { if(person.IsNemesis() && !it->GetGovernment()->IsPlayer()) continue; double range = it->Position().Distance(ship.Position()); // Preferentially focus on your previous target or your parent ship's // target if they are nearby. if(it == oldTarget || it == parentTarget) range -= 500.; // If your personality it to disable ships rather than destroy them, // never target disabled ships. if(it->IsDisabled() && !person.Plunders() && (person.Disables() || (!person.IsNemesis() && it != oldTarget))) continue; if(!person.Plunders()) range += 5000. * it->IsDisabled(); else { bool hasBoarded = Has(ship, it, ShipEvent::BOARD); // Don't plunder unless there are no "live" enemies nearby. range += 2000. * (2 * it->IsDisabled() - !hasBoarded); } // Focus on nearly dead ships. range += 500. * (it->Shields() + it->Hull()); if(range < closest) { closest = range; target = it; isDisabled = it->IsDisabled(); } } bool cargoScan = ship.Attributes().Get("cargo scan"); bool outfitScan = ship.Attributes().Get("outfit scan"); if(!target && (cargoScan || outfitScan) && !isPlayerEscort) { closest = numeric_limits<double>::infinity(); for(const auto &it : ships) if(it->GetSystem() == system && it->GetGovernment() != gov && it->IsTargetable()) { if((cargoScan && !Has(ship, it, ShipEvent::SCAN_CARGO)) || (outfitScan && !Has(ship, it, ShipEvent::SCAN_OUTFITS))) { double range = it->Position().Distance(ship.Position()); if(range < closest) { closest = range; target = it; } } } } // Run away if your target is not disabled and you are badly damaged. if(!isDisabled && (ship.GetPersonality().IsFleeing() || (ship.Shields() + ship.Hull() < 1. && !ship.GetPersonality().IsHeroic()))) target.reset(); return target; }
void AI::MoveIndependent(Ship &ship, Command &command) const { if(ship.Position().Length() >= 10000.) { MoveTo(ship, command, Point(), 40., .8); return; } shared_ptr<const Ship> target = ship.GetTargetShip(); if(target && (ship.GetGovernment()->IsEnemy(target->GetGovernment()) || (ship.IsYours() && target == sharedTarget.lock()))) { bool shouldBoard = ship.Cargo().Free() && ship.GetPersonality().Plunders(); bool hasBoarded = Has(ship, target, ShipEvent::BOARD); if(shouldBoard && target->IsDisabled() && !hasBoarded) { if(ship.IsBoarding()) return; MoveTo(ship, command, target->Position(), 40., .8); command |= Command::BOARD; } else Attack(ship, command, *target); return; } else if(target) { bool cargoScan = ship.Attributes().Get("cargo scan"); bool outfitScan = ship.Attributes().Get("outfit scan"); if((!cargoScan || Has(ship, target, ShipEvent::SCAN_CARGO)) && (!outfitScan || Has(ship, target, ShipEvent::SCAN_OUTFITS))) target.reset(); else { CircleAround(ship, command, *target); if(!ship.GetGovernment()->IsPlayer()) command |= Command::SCAN; } return; } // If this ship is moving independently because it has a target, not because // it has no parent, don't let it make travel plans. if(ship.GetParent() && !ship.GetPersonality().IsStaying()) return; if(!ship.GetTargetSystem() && !ship.GetTargetPlanet() && !ship.GetPersonality().IsStaying()) { int jumps = ship.JumpsRemaining(); // Each destination system has an average priority of 10. // If you only have one jump left, landing should be high priority. int planetWeight = jumps ? (1 + 40 / jumps) : 1; vector<int> systemWeights; int totalWeight = 0; const vector<const System *> &links = ship.Attributes().Get("jump drive") ? ship.GetSystem()->Neighbors() : ship.GetSystem()->Links(); if(jumps) { for(const System *link : links) { // Prefer systems in the direction we're facing. Point direction = link->Position() - ship.GetSystem()->Position(); int weight = static_cast<int>( 11. + 10. * ship.Facing().Unit().Dot(direction.Unit())); systemWeights.push_back(weight); totalWeight += weight; } } int systemTotalWeight = totalWeight; // Anywhere you can land that has a port has the same weight. Ships will // not land anywhere without a port. vector<const StellarObject *> planets; for(const StellarObject &object : ship.GetSystem()->Objects()) if(object.GetPlanet() && object.GetPlanet()->HasSpaceport() && object.GetPlanet()->CanLand(ship)) { planets.push_back(&object); totalWeight += planetWeight; } if(!totalWeight) return; int choice = Random::Int(totalWeight); if(choice < systemTotalWeight) { for(unsigned i = 0; i < systemWeights.size(); ++i) { choice -= systemWeights[i]; if(choice < 0) { ship.SetTargetSystem(links[i]); break; } } } else { choice = (choice - systemTotalWeight) / planetWeight; ship.SetTargetPlanet(planets[choice]); } } if(ship.GetTargetSystem()) { PrepareForHyperspace(ship, command); bool mustWait = false; for(const weak_ptr<const Ship> &escort : ship.GetEscorts()) { shared_ptr<const Ship> locked = escort.lock(); mustWait = locked && locked->CanBeCarried(); } if(!mustWait) command |= Command::JUMP; } else if(ship.GetTargetPlanet()) { MoveToPlanet(ship, command); if(!ship.GetPersonality().IsStaying()) command |= Command::LAND; else if(ship.Position().Distance(ship.GetTargetPlanet()->Position()) < 100.) ship.SetTargetPlanet(nullptr); } else if(ship.GetPersonality().IsStaying() && ship.GetSystem()->Objects().size()) { unsigned i = Random::Int(ship.GetSystem()->Objects().size()); ship.SetTargetPlanet(&ship.GetSystem()->Objects()[i]); } }
// Fire whichever of the given ship's weapons can hit a hostile target. Command AI::AutoFire(const Ship &ship, const list<shared_ptr<Ship>> &ships, bool secondary) const { Command command; int index = -1; // Special case: your target is not your enemy. Do not fire, because you do // not want to risk damaging that target. The only time a ship other than // the player will target a friendly ship is if the player has asked a ship // for assistance. shared_ptr<Ship> currentTarget = ship.GetTargetShip(); const Government *gov = ship.GetGovernment(); bool isSharingTarget = ship.IsYours() && currentTarget == sharedTarget.lock(); bool currentIsEnemy = currentTarget && currentTarget->GetGovernment()->IsEnemy(gov) && currentTarget->GetSystem() == ship.GetSystem(); if(currentTarget && !(currentIsEnemy || isSharingTarget)) currentTarget.reset(); // Only fire on disabled targets if you don't want to plunder them. bool spareDisabled = (ship.GetPersonality().Disables() || ship.GetPersonality().Plunders()); // Find the longest range of any of your non-homing weapons. double maxRange = 0.; for(const Armament::Weapon &weapon : ship.Weapons()) if(weapon.IsReady() && !weapon.IsHoming() && (secondary || !weapon.GetOutfit()->Icon())) maxRange = max(maxRange, weapon.GetOutfit()->Range()); // Extend the weapon range slightly to account for velocity differences. maxRange *= 1.5; // Find all enemy ships within range of at least one weapon. vector<shared_ptr<const Ship>> enemies; if(currentTarget) enemies.push_back(currentTarget); for(auto target : ships) if(target->IsTargetable() && gov->IsEnemy(target->GetGovernment()) && target->Velocity().Length() < 20. && target->GetSystem() == ship.GetSystem() && target->Position().Distance(ship.Position()) < maxRange && target != currentTarget) enemies.push_back(target); for(const Armament::Weapon &weapon : ship.Weapons()) { ++index; // Skip weapons that are not ready to fire. Also skip homing weapons if // no target is selected, and secondary weapons if only firing primaries. if(!weapon.IsReady() || (!currentTarget && weapon.IsHoming())) continue; if(!secondary && weapon.GetOutfit()->Icon()) continue; // Special case: if the weapon uses fuel, be careful not to spend so much // fuel that you cannot leave the system if necessary. if(weapon.GetOutfit()->FiringFuel()) { double fuel = ship.Fuel() * ship.Attributes().Get("fuel capacity"); fuel -= weapon.GetOutfit()->FiringFuel(); // If the ship is not ever leaving this system, it does not need to // reserve any fuel. bool isStaying = ship.GetPersonality().IsStaying(); if(!secondary || fuel < (isStaying ? 0. : ship.JumpFuel())) continue; } // Figure out where this weapon will fire from, but add some randomness // depending on how accurate this ship's pilot is. Point start = ship.Position() + ship.Facing().Rotate(weapon.GetPoint()); start += ship.GetPersonality().Confusion(); const Outfit *outfit = weapon.GetOutfit(); double vp = outfit->Velocity(); double lifetime = outfit->TotalLifetime(); if(currentTarget && (weapon.IsHoming() || weapon.IsTurret())) { bool hasBoarded = Has(ship, currentTarget, ShipEvent::BOARD); if(currentTarget->IsDisabled() && spareDisabled && !hasBoarded) continue; Point p = currentTarget->Position() - start; Point v = currentTarget->Velocity() - ship.Velocity(); // By the time this action is performed, the ships will have moved // forward one time step. p += v; if(p.Length() < outfit->BlastRadius()) continue; double steps = Armament::RendevousTime(p, v, vp); if(steps == steps && steps <= lifetime) { command.SetFire(index); continue; } } // Don't fire homing weapons with no target. if(weapon.IsHoming()) continue; for(const shared_ptr<const Ship> &target : enemies) { if(!target->IsTargetable() || target->Velocity().Length() > 20. || target->GetSystem() != ship.GetSystem()) continue; // Don't shoot ships we want to plunder. bool hasBoarded = Has(ship, target, ShipEvent::BOARD); if(target->IsDisabled() && spareDisabled && !hasBoarded) continue; Point p = target->Position() - start; Point v = target->Velocity() - ship.Velocity(); // By the time this action is performed, the ships will have moved // forward one time step. p += v; // Get the vector the weapon will travel along. v = (ship.Facing() + weapon.GetAngle()).Unit() * vp - v; // Extrapolate over the lifetime of the projectile. v *= lifetime; const Mask &mask = target->GetSprite().GetMask(step); if(mask.Collide(-p, v, target->Facing()) < 1.) { command.SetFire(index); break; } } } return command; }