/* Starshatter OpenSource Distribution Copyright (c) 1997-2004, Destroyer Studios LLC. All Rights Reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name "Destroyer Studios" nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. SUBSYSTEM: Stars.exe FILE: TacticalAI.cpp AUTHOR: John DiCamillo OVERVIEW ======== Generic Ship Tactical Level AI class */ #include "MemDebug.h" #include "TacticalAI.h" #include "ShipAI.h" #include "CarrierAI.h" #include "Ship.h" #include "ShipDesign.h" #include "Element.h" #include "Instruction.h" #include "RadioMessage.h" #include "RadioTraffic.h" #include "Contact.h" #include "WeaponGroup.h" #include "Drive.h" #include "Hangar.h" #include "Sim.h" #include "Shot.h" #include "Drone.h" #include "StarSystem.h" #include "Game.h" #include "Random.h" // +----------------------------------------------------------------------+ static int exec_time_seed = 0; // +----------------------------------------------------------------------+ TacticalAI::TacticalAI(ShipAI* ai) : ship(0), ship_ai(0), carrier_ai(0), navpt(0), orders(0), action(0), threat_level(0), support_level(1), directed_tgtid(0) { if (ai) { ship_ai = ai; ship = ai->GetShip(); Sim* sim = Sim::GetSim(); if (ship && ship->GetHangar() && ship->GetCommandAILevel() > 0 && ship != sim->GetPlayerShip()) carrier_ai = new(__FILE__,__LINE__) CarrierAI(ship, ship_ai->GetAILevel()); } agression = 0; roe = FLEXIBLE; element_index = 1; exec_time = exec_time_seed; exec_time_seed += 17; } TacticalAI::~TacticalAI() { delete carrier_ai; } // +--------------------------------------------------------------------+ void TacticalAI::ExecFrame(double secs) { const int exec_period = 1000; if (!ship || !ship_ai) return; navpt = ship->GetNextNavPoint(); orders = ship->GetRadioOrders(); if ((int) Game::GameTime() - exec_time > exec_period) { element_index = ship->GetElementIndex(); CheckOrders(); SelectTarget(); FindThreat(); FindSupport(); if (element_index > 1) { int formation = 0; if (orders && orders->Formation() >= 0) formation = orders->Formation(); else if (navpt) formation = navpt->Formation(); FindFormationSlot(formation); } ship_ai->SetNavPoint(navpt); if (carrier_ai) carrier_ai->ExecFrame(secs); exec_time += exec_period; } } // +--------------------------------------------------------------------+ void TacticalAI::CheckOrders() { directed_tgtid = 0; if (CheckShipOrders()) return; if (CheckFlightPlan()) return; if (CheckObjectives()) return; } // +--------------------------------------------------------------------+ bool TacticalAI::CheckShipOrders() { return ProcessOrders(); } // +--------------------------------------------------------------------+ bool TacticalAI::CheckObjectives() { bool processed = false; Ship* ward = 0; Element* elem = ship->GetElement(); if (elem) { Instruction* obj = elem->GetTargetObjective(); if (obj) { ship_ai->ClearPatrol(); if (obj->Action()) { switch (obj->Action()) { case Instruction::INTERCEPT: case Instruction::STRIKE: case Instruction::ASSAULT: { SimObject* tgt = obj->GetTarget(); if (tgt && tgt->Type() == SimObject::SIM_SHIP) { roe = DIRECTED; SelectTargetDirected((Ship*) tgt); } } break; case Instruction::DEFEND: case Instruction::ESCORT: { SimObject* tgt = obj->GetTarget(); if (tgt && tgt->Type() == SimObject::SIM_SHIP) { roe = DEFENSIVE; ward = (Ship*) tgt; } } break; default: break; } } orders = obj; processed = true; } } ship_ai->SetWard(ward); return processed; } // +--------------------------------------------------------------------+ bool TacticalAI::ProcessOrders() { if (ship_ai) ship_ai->ClearPatrol(); if (orders && orders->EMCON() > 0) { int desired_emcon = orders->EMCON(); if (ship_ai && (ship_ai->GetThreat() || ship_ai->GetThreatMissile())) desired_emcon = 3; if (ship->GetEMCON() != desired_emcon) ship->SetEMCON(desired_emcon); } if (orders && orders->Action()) { switch (orders->Action()) { case RadioMessage::ATTACK: case RadioMessage::BRACKET: case RadioMessage::IDENTIFY: { bool tgt_ok = false; SimObject* tgt = orders->GetTarget(); if (tgt && tgt->Type() == SimObject::SIM_SHIP) { Ship* tgt_ship = (Ship*) tgt; if (CanTarget(tgt_ship)) { roe = DIRECTED; SelectTargetDirected((Ship*) tgt); ship_ai->SetBracket(orders->Action() == RadioMessage::BRACKET); ship_ai->SetIdentify(orders->Action() == RadioMessage::IDENTIFY); ship_ai->SetNavPoint(0); tgt_ok = true; } } if (!tgt_ok) ClearRadioOrders(); } break; case RadioMessage::ESCORT: case RadioMessage::COVER_ME: { SimObject* tgt = orders->GetTarget(); if (tgt && tgt->Type() == SimObject::SIM_SHIP) { roe = DEFENSIVE; ship_ai->SetWard((Ship*) tgt); ship_ai->SetNavPoint(0); } else { ClearRadioOrders(); } } break; case RadioMessage::WEP_FREE: roe = AGRESSIVE; ship_ai->DropTarget(0.1); break; case RadioMessage::WEP_HOLD: case RadioMessage::FORM_UP: roe = NONE; ship_ai->DropTarget(5); break; case RadioMessage::MOVE_PATROL: roe = SELF_DEFENSIVE; ship_ai->SetPatrol(orders->Location()); ship_ai->SetNavPoint(0); ship_ai->DropTarget(Random(5, 10)); break; case RadioMessage::RTB: case RadioMessage::DOCK_WITH: roe = NONE; ship_ai->DropTarget(10); if (!ship->GetInbound()) { RadioMessage* msg = 0; Ship* controller = ship->GetController(); if (orders->Action() == RadioMessage::DOCK_WITH && orders->GetTarget()) { controller = (Ship*) orders->GetTarget(); } if (!controller) { Element* elem = ship->GetElement(); if (elem && elem->GetCommander()) { Element* cmdr = elem->GetCommander(); controller = cmdr->GetShip(1); } } if (controller && controller->GetHangar() && controller->GetHangar()->CanStow(ship)) { SimRegion* self_rgn = ship->GetRegion(); SimRegion* rtb_rgn = controller->GetRegion(); if (self_rgn == rtb_rgn) { double range = Point(controller->Location() - ship->Location()).length(); if (range < 50e3) { msg = new(__FILE__,__LINE__) RadioMessage(controller, ship, RadioMessage::CALL_INBOUND); RadioTraffic::Transmit(msg); } } } else { ship->ClearRadioOrders(); } ship_ai->SetNavPoint(0); } break; case RadioMessage::QUANTUM_TO: case RadioMessage::FARCAST_TO: roe = NONE; ship_ai->DropTarget(10); break; } action = orders->Action(); return true; } // if we had an action before, this must be a "cancel orders" else if (action) { ClearRadioOrders(); } return false; } void TacticalAI::ClearRadioOrders() { action = 0; roe = FLEXIBLE; if (ship_ai) ship_ai->DropTarget(0.1); if (ship) ship->ClearRadioOrders(); } // +--------------------------------------------------------------------+ bool TacticalAI::CheckFlightPlan() { Ship* ward = 0; // Find next Instruction: navpt = ship->GetNextNavPoint(); roe = FLEXIBLE; if (navpt) { switch (navpt->Action()) { case Instruction::LAUNCH: case Instruction::DOCK: case Instruction::RTB: roe = NONE; break; case Instruction::VECTOR: roe = SELF_DEFENSIVE; break; case Instruction::DEFEND: case Instruction::ESCORT: roe = DEFENSIVE; break; case Instruction::INTERCEPT: roe = DIRECTED; break; case Instruction::RECON: case Instruction::STRIKE: case Instruction::ASSAULT: roe = DIRECTED; break; case Instruction::PATROL: case Instruction::SWEEP: roe = FLEXIBLE; break; default: break; } if (roe == DEFENSIVE) { SimObject* tgt = navpt->GetTarget(); if (tgt && tgt->Type() == SimObject::SIM_SHIP) ward = (Ship*) tgt; } if (navpt->EMCON() > 0) { int desired_emcon = navpt->EMCON(); if (ship_ai && (ship_ai->GetThreat() || ship_ai->GetThreatMissile())) desired_emcon = 3; if (ship->GetEMCON() != desired_emcon) ship->SetEMCON(desired_emcon); } } if (ship_ai) ship_ai->SetWard(ward); return (navpt != 0); } // +--------------------------------------------------------------------+ void TacticalAI::SelectTarget() { if (!ship) { roe = NONE; return; } // unarmed vessels should never engage an enemy: if (ship->Weapons().size() < 1) roe = NONE; SimObject* target = ship_ai->GetTarget(); SimObject* ward = ship_ai->GetWard(); // if not allowed to engage, drop and return: if (roe == NONE) { if (target) ship_ai->DropTarget(); return; } // if we have abandoned our ward, drop and return: if (ward && roe != AGRESSIVE) { double d = (ward->Location() - ship->Location()).length(); double safe_zone = 50e3; if (target) { if (ship->IsStarship()) safe_zone = 100e3; if (d > safe_zone) { ship_ai->DropTarget(); return; } } else { if (d > safe_zone) { return; } } } // already have a target, keep it: if (target) { if (target->Life()) { CheckTarget(); // frigates need to be ready to abandon ship-type targets // in favor of drone-type targets, others should just go // with what they have: if (ship->Class() != Ship::CORVETTE && ship->Class() != Ship::FRIGATE) return; // in case the check decided to drop the target: target = ship_ai->GetTarget(); } // if the old target is dead, forget it: else { ship_ai->DropTarget(); target = 0; } } // if not allowed to acquire, forget it: if (ship_ai->DropTime() > 0) return; if (roe == DIRECTED) { if (target && target->Type() == SimObject::SIM_SHIP) SelectTargetDirected((Ship*) target); else if (navpt && navpt->GetTarget() && navpt->GetTarget()->Type() == SimObject::SIM_SHIP) SelectTargetDirected((Ship*) navpt->GetTarget()); else SelectTargetDirected(); } else { SelectTargetOpportunity(); // don't switch one ship target for another... if (ship->Class() == Ship::CORVETTE || ship->Class() == Ship::FRIGATE) { SimObject* potential_target = ship_ai->GetTarget(); if (target && potential_target && target != potential_target) { if (target->Type() == SimObject::SIM_SHIP && potential_target->Type() == SimObject::SIM_SHIP) { ship_ai->SetTarget(target); } } } } } // +--------------------------------------------------------------------+ void TacticalAI::SelectTargetDirected(Ship* tgt) { Ship* potential_target = tgt; // try to target one of the element's objectives // (if it shows up in the contact list) if (!tgt) { Element* elem = ship->GetElement(); if (elem) { Instruction* objective = elem->GetTargetObjective(); if (objective) { SimObject* obj_sim_obj = objective->GetTarget(); Ship* obj_tgt = 0; if (obj_sim_obj && obj_sim_obj->Type() == SimObject::SIM_SHIP) obj_tgt = (Ship*) obj_sim_obj; if (obj_tgt) { ListIter contact = ship->ContactList(); while (++contact && !potential_target) { Ship* test = contact->GetShip(); if (obj_tgt == test) { potential_target = test; } } } } } } if (!CanTarget(potential_target)) potential_target = 0; ship_ai->SetTarget(potential_target); if (tgt && tgt == ship_ai->GetTarget()) directed_tgtid = tgt->Identity(); else directed_tgtid = 0; } // +--------------------------------------------------------------------+ bool TacticalAI::CanTarget(Ship* tgt) { bool result = false; if (tgt && !tgt->InTransition()) { if (tgt->IsRogue() || tgt->GetIFF() != ship->GetIFF()) result = true; } return result; } // +--------------------------------------------------------------------+ void TacticalAI::SelectTargetOpportunity() { // NON-COMBATANTS do not pick targets of opportunity: if (ship->GetIFF() == 0) return; SimObject* potential_target = 0; // pick the closest combatant ship with a different IFF code: double target_dist = ship->Design()->commit_range; SimObject* ward = ship_ai->GetWard(); // FRIGATES are primarily anti-air platforms, but may // also attack smaller starships: if (ship->Class() == Ship::CORVETTE || ship->Class() == Ship::FRIGATE) { Ship* current_ship_target = 0; Shot* current_shot_target = 0; // if we are escorting a larger warship, it is good to attack // the same target as our ward: if (ward) { Ship* s = (Ship*) ward; if (s->Class() > ship->Class()) { SimObject* obj = s->GetTarget(); if (obj && obj->Type() == SimObject::SIM_SHIP) { current_ship_target = (Ship*) obj; target_dist = (ship->Location() - obj->Location()).length(); } } } ListIter contact = ship->ContactList(); while (++contact) { Ship* c_ship = contact->GetShip(); Shot* c_shot = contact->GetShot(); if (!c_ship && !c_shot) continue; int c_iff = contact->GetIFF(ship); bool rogue = c_ship && c_ship->IsRogue(); bool tgt_ok = c_iff > 0 && c_iff != ship->GetIFF() && c_iff < 1000; if (rogue || tgt_ok) { if (c_ship && c_ship != ship && !c_ship->InTransition()) { if (c_ship->Class() < Ship::DESTROYER || (c_ship->Class() >= Ship::MINE && c_ship->Class() <= Ship::DEFSAT)) { // found an enemy, check distance: double dist = (ship->Location() - c_ship->Location()).length(); if (dist < 0.75 * target_dist && (!current_ship_target || c_ship->Class() <= current_ship_target->Class())) { current_ship_target = c_ship; target_dist = dist; } } } else if (c_shot) { // found an enemy shot, is there enough time to engage? if (c_shot->GetEta() < 3) continue; // found an enemy shot, check distance: double dist = (ship->Location() - c_shot->Location()).length(); if (!current_shot_target) { current_shot_target = c_shot; target_dist = dist; } // is this shot a better target than the one we've found? else { Ship* ward = ship_ai->GetWard(); if ((c_shot->IsTracking(ward) || c_shot->IsTracking(ship)) && (!current_shot_target->IsTracking(ward) || !current_shot_target->IsTracking(ship))) { current_shot_target = c_shot; target_dist = dist; } else if (dist < target_dist) { current_shot_target = c_shot; target_dist = dist; } } } } } if (current_shot_target) potential_target = current_shot_target; else potential_target = current_ship_target; } // ALL OTHER SHIP CLASSES ignore fighters and only engage // other starships: else { List ward_threats; ListIter contact = ship->ContactList(); while (++contact) { Ship* c_ship = contact->GetShip(); if (!c_ship) continue; int c_iff = contact->GetIFF(ship); bool rogue = c_ship->IsRogue(); bool tgt_ok = c_ship != ship && c_iff > 0 && c_iff != ship->GetIFF() && !c_ship->InTransition(); if (rogue || tgt_ok) { if (c_ship->IsStarship() || c_ship->IsStatic()) { // found an enemy, check distance: double dist = (ship->Location() - c_ship->Location()).length(); if (dist < 0.75 * target_dist) { potential_target = c_ship; target_dist = dist; } if (ward && c_ship->IsTracking(ward)) { ward_threats.append(c_ship); } } } } // if this ship is protecting a ward, // prefer targets that are threatening that ward: if (potential_target && ward_threats.size() && !ward_threats.contains((Ship*)potential_target)) { target_dist *= 2; ListIter iter = ward_threats; while (++iter) { Ship* threat = iter.value(); double dist = (ward->Location() - threat->Location()).length(); if (dist < target_dist) { potential_target = threat; target_dist = dist; } } } } if (ship->Class() != Ship::CARRIER && ship->Class() != Ship::SWACS) ship_ai->SetTarget(potential_target); } // +--------------------------------------------------------------------+ void TacticalAI::CheckTarget() { SimObject* tgt = ship_ai->GetTarget(); if (!tgt) return; if (tgt->GetRegion() != ship->GetRegion()) { ship_ai->DropTarget(); return; } if (tgt->Type() == SimObject::SIM_SHIP) { Ship* target = (Ship*) tgt; // has the target joined our side? if (target->GetIFF() == ship->GetIFF() && !target->IsRogue()) { ship_ai->DropTarget(); return; } // is the target already jumping/breaking/dying? if (target->InTransition()) { ship_ai->DropTarget(); return; } // have we been ordered to pursue the target? if (directed_tgtid) { if (directed_tgtid != target->Identity()) { ship_ai->DropTarget(); } return; } // can we catch the target? if (target->Design()->vlimit <= ship->Design()->vlimit || ship->Velocity().length() <= ship->Design()->vlimit) return; // is the target now out of range? WeaponDesign* wep_dsn = ship->GetPrimaryDesign(); if (!wep_dsn) return; // compute the "give up" range: double drop_range = 3 * wep_dsn->max_range; if (drop_range > 0.75 * ship->Design()->commit_range) drop_range = 0.75 * ship->Design()->commit_range; double range = Point(target->Location() - ship->Location()).length(); if (range < drop_range) return; // is the target closing or separating? Point delta = (target->Location() + target->Velocity()) - (ship->Location() + ship->Velocity()); if (delta.length() < range) return; ship_ai->DropTarget(); } else if (tgt->Type() == SimObject::SIM_DRONE) { Drone* drone = (Drone*) tgt; // is the target still a threat? if (drone->GetEta() < 1 || drone->GetTarget() == 0) ship_ai->DropTarget(); } } // +--------------------------------------------------------------------+ void TacticalAI::FindThreat() { // pick the closest contact on Threat Warning System: Ship* threat = 0; Shot* threat_missile = 0; Ship* rumor = 0; double threat_dist = 1e9; const DWORD THREAT_REACTION_TIME = 1000; // 1 second ListIter iter = ship->ContactList(); while (++iter) { Contact* contact = iter.value(); if (contact->Threat(ship) && (Game::GameTime() - contact->AcquisitionTime()) > THREAT_REACTION_TIME) { if (contact->GetShot()) { threat_missile = contact->GetShot(); rumor = (Ship*) threat_missile->Owner(); } else { double rng = contact->Range(ship); Ship* c_ship = contact->GetShip(); if (c_ship && !c_ship->InTransition() && c_ship->Class() != Ship::FREIGHTER && c_ship->Class() != Ship::FARCASTER) { if (c_ship->GetTarget() == ship) { if (!threat || c_ship->Class() > threat->Class()) { threat = c_ship; threat_dist = 0; } } else if (rng < threat_dist) { threat = c_ship; threat_dist = rng; } } } } } if (rumor && !rumor->InTransition()) { iter.reset(); while (++iter) { if (iter->GetShip() == rumor) { rumor = 0; ship_ai->ClearRumor(); break; } } } else { rumor = 0; ship_ai->ClearRumor(); } ship_ai->SetRumor(rumor); ship_ai->SetThreat(threat); ship_ai->SetThreatMissile(threat_missile); } // +--------------------------------------------------------------------+ void TacticalAI::FindSupport() { if (!ship_ai->GetThreat()) { ship_ai->SetSupport(0); return; } // pick the biggest friendly contact in the sector: Ship* support = 0; double support_dist = 1e9; ListIter contact = ship->ContactList(); while (++contact) { if (contact->GetShip() && contact->GetIFF(ship) == ship->GetIFF()) { Ship* c_ship = contact->GetShip(); if (c_ship != ship && c_ship->Class() >= ship->Class() && !c_ship->InTransition()) { if (!support || c_ship->Class() > support->Class()) support = c_ship; } } } ship_ai->SetSupport(support); } // +--------------------------------------------------------------------+ void TacticalAI::FindFormationSlot(int formation) { // find the formation delta: int s = element_index - 1; Point delta(10*s, 0, 10*s); // diamond: if (formation == Instruction::DIAMOND) { switch (element_index) { case 2: delta = Point( 10, 0, -12); break; case 3: delta = Point(-10, 0, -12); break; case 4: delta = Point( 0, 0, -24); break; } } // spread: if (formation == Instruction::SPREAD) { switch (element_index) { case 2: delta = Point( 15, 0, 0); break; case 3: delta = Point(-15, 0, 0); break; case 4: delta = Point(-30, 0, 0); break; } } // box: if (formation == Instruction::BOX) { switch (element_index) { case 2: delta = Point(15, 0, 0); break; case 3: delta = Point( 0, -1, -15); break; case 4: delta = Point(15, -1, -15); break; } } // trail: if (formation == Instruction::TRAIL) { delta = Point(0, 0, -15*s); } ship_ai->SetFormationDelta(delta * ship->Radius() * 2); }