Files
fs2open.github.com/code/ai/aiturret.cpp
Goober5000andClaude Fable 5.1 04e698c29b fix weapon substitution bank attribution and turret ammo accounting
Follow-up to #7635.

Pass the firing bank explicitly through weapon_create() and beam_fire()
instead of searching the ship's banks by weapon class, which could
attribute substitution patterns to the wrong bank.  Factor the
substitution logic into get_weapon_substitution_tuple() and only apply
linked/dual-fire pairing to ship-level fire, not turret fire.  Skip
substitution on multiplayer clients, since the server already resolved it.

Derive turret bank indices from the weapon slot being fired rather than
the turret's bank cursor, so ammo and substitution state are tracked on
the bank that actually fired.  Previously a single-selection turret could
consume ammo from a bank it never fired, and non-ammo turrets (whose
cursors stay at -1) would have skipped substitution entirely.

Use the Player_ship object flag rather than Player_obj in
ship_secondary_bank_can_dual_fire() so the server and clients agree on
dual fire for remote players, and guard ship_secondary_has_ammo() against
a negative bank index.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-05 19:22:45 -04:00

2958 lines
105 KiB
C++

#include "ai/aibig.h"
#include "ai/aiinternal.h"
#include "asteroid/asteroid.h"
#include "debugconsole/console.h"
#include "freespace.h"
#include "gamesnd/gamesnd.h"
#include "gamesequence/gamesequence.h"
#include "globalincs/linklist.h"
#include "globalincs/systemvars.h"
#include "iff_defs/iff_defs.h"
#include "io/timer.h"
#include "math/staticrand.h"
#include "network/multi.h"
#include "network/multimsgs.h"
#include "object/objectdock.h"
#include "scripting/global_hooks.h"
#include "scripting/scripting.h"
#include "render/3d.h"
#include "ship/ship.h"
#include "ship/shipfx.h"
#include "utils/Random.h"
#include "weapon/beam.h"
#include "weapon/flak.h"
#include "weapon/muzzleflash.h"
#include "weapon/swarm.h"
#include "weapon/weapon.h"
#include "utils/modular_curves.h"
#include <climits>
// How close a turret has to be point at its target before it
// can fire. If the dot of the gun normal and the vector from gun
// to target is greater than this, the turret fires. The smaller
// the sloppier the shooting.
#define AICODE_TURRET_DUMBFIRE_ANGLE (0.8f)
#define AICODE_TURRET_HEATSEEK_ANGLE (0.7f)
#define AICODE_TURRET_MAX_TIME_IN_RANGE (5.0f)
#define BEAM_NEBULA_RANGE_REDUCE_FACTOR (0.8f)
float Lethality_range_const = 2.0f;
DCF(lethality_range, "N for modifying range: 1 / (1+N) at 100")
{
dc_stuff_float(&Lethality_range_const);
}
float Player_lethality_bump[NUM_SKILL_LEVELS] = {
// 0.0f, 5.0f, 10.0f, 25.0f, 40.0f
0.0f, 0.0f, 0.0f, 0.0f, 0.0f
};
const char *Turret_target_order_names[NUM_TURRET_ORDER_TYPES] = {
"Bombs",
"Ships",
"Asteroids",
};
#define EEOF_BIG_ONLY (1<<0) // turret fires only at big and huge ships
#define EEOF_SMALL_ONLY (1<<1) // turret fires only at small ships
#define EEOF_TAGGED_ONLY (1<<2) // turret fires only at tagged ships
#define EEOF_BEAM (1<<3) // turret is a beam
#define EEOF_FLAK (1<<4) // turret is flak
#define EEOF_LASER (1<<5) // turret is a laser
#define EEOF_MISSILE (1<<6) // turret is a missile
const char* Turret_valid_types[NUM_TURRET_TYPES] = {
"Beam",
"Flak",
"Laser",
"Missile",
};
typedef struct eval_enemy_obj_struct {
int turret_parent_objnum = -1; // parent of turret
float weapon_travel_dist = 0.0f; // max targeting range of turret weapon
int enemy_team_mask = 0;
bool weapon_system_ok = false; // is the weapon subsystem of turret ship ok
int eeo_flags = 0;
const vec3d *tpos = nullptr;
const vec3d *tvec = nullptr;
const ship_subsys *turret_subsys = nullptr;
int current_enemy = -1;
float nearest_attacker_dist = 99999.0f; // nearest ship
int nearest_attacker_objnum = -1;
float nearest_homing_bomb_dist = 99999.0f; // nearest homing bomb
int nearest_homing_bomb_objnum = -1;
float nearest_bomb_dist = 99999.0f; // nearest non-homing bomb
int nearest_bomb_objnum = -1;
float nearest_dist = 99999.0f; // nearest ship attacking this turret
int nearest_objnum = -1;
} eval_enemy_obj_struct;
// the current world orientation of the turret matrix, corresponding to its fvec and uvec defined in the model
// is NOT affected by the turret's current aiming
void turret_instance_find_world_orient(matrix* out_mat, int model_instance_num, int submodel_num, const matrix* objorient)
{
auto pmi = model_get_instance(model_instance_num);
auto pm = model_get(pmi->model_num);
vec3d fvec, uvec;
model_instance_local_to_global_dir(&fvec, &pm->submodel[submodel_num].frame_of_reference.vec.fvec, pm, pmi, pm->submodel[submodel_num].parent, objorient);
model_instance_local_to_global_dir(&uvec, &pm->submodel[submodel_num].frame_of_reference.vec.uvec, pm, pmi, pm->submodel[submodel_num].parent, objorient);
vm_vector_2_matrix_norm(out_mat, &fvec, &uvec);
}
/**
* Is object in turret field of view?
*
* @param objp Pointer to object to test
* @param ss Ship turret subsystem to test from
* @param tvec Turret initial vector
* @param tpos Turret initial position
* @param dist Distance from turret to center point of object
*
* @return true if objp is in fov of the specified turret. Otherwise return false.
*/
bool object_in_turret_fov(const object *objp, const ship_subsys *ss, const vec3d *tvec, const vec3d *tpos, float dist)
{
vec3d v2e;
float size_mod;
bool in_fov = false;
if (ss->flags[Ship::Subsystem_Flags::FOV_edge_check]) {
int model_num;
switch (objp->type) {
case OBJ_SHIP:
model_num = Ship_info[Ships[objp->instance].ship_info_index].model_num;
break;
case OBJ_ASTEROID:
model_num = Asteroid_info[Asteroids[objp->instance].asteroid_type].subtypes[Asteroids[objp->instance].asteroid_subtype].model_number;
break;
default:
vm_vec_normalized_dir(&v2e, &objp->pos, tpos);
size_mod = objp->radius / (dist + objp->radius);
in_fov = turret_fov_test(ss, tvec, &v2e, size_mod);
return in_fov;
}
auto pm = model_get(model_num);
for (int i = 0; i < 8; i++) {
vec3d bbox_point;
vm_vec_unrotate(&bbox_point, &pm->bounding_box[i], &objp->orient);
bbox_point += objp->pos;
vm_vec_normalized_dir(&v2e, &bbox_point, tpos);
in_fov = turret_fov_test(ss, tvec, &v2e, -0.2f);
if (in_fov)
return true;
}
}
// if the bbox points method didn't work (or fov_edge_checks isn't on)
// try the normal method
vm_vec_normalized_dir(&v2e, &objp->pos, tpos);
size_mod = objp->radius / (dist + objp->radius);
in_fov = turret_fov_test(ss, tvec, &v2e, size_mod);
return in_fov;
}
/**
* Is bomb headed towards given ship?
*
* @param bomb_objp Bomb specified
* @param ship_objp Ship specified
*
* @return true if bomb_objp is headed towards ship_objp
*/
bool bomb_headed_towards_ship(const object *bomb_objp, const object *ship_objp)
{
float dot;
vec3d bomb_to_ship_vector;
vm_vec_normalized_dir(&bomb_to_ship_vector, &ship_objp->pos, &bomb_objp->pos);
dot = vm_vec_dot(&bomb_objp->orient.vec.fvec, &bomb_to_ship_vector);
return ( dot > 0 );
}
// Set active weapon for turret
//This really isn't needed...but whatever -WMC
/**
* Returns the best weapon on turret for target
*
* @note All non-negative return values are expressed in what I like to call "widx"s.
* @return -1 if unable to find a weapon for the target at all.
*/
int turret_select_best_weapon(const ship_subsys *turret, const object * /*target*/)
{
//TODO: Fill this out with extraodinary gun-picking algorithms
if(turret->weapons.num_primary_banks > 0)
return 0;
else if(turret->weapons.num_secondary_banks > 0)
return MAX_SHIP_PRIMARY_BANKS;
else
return -1;
}
/**
* Returns true if all weapons in swp have the specified flag
*/
bool all_turret_weapons_have_flags(const ship_weapon *swp, flagset<Weapon::Info_Flags> flags)
{
int i;
for(i = 0; i < swp->num_primary_banks; i++)
{
if(!(Weapon_info[swp->primary_bank_weapons[i]].wi_flags & flags).any_set())
return false;
}
for(i = 0; i < swp->num_secondary_banks; i++)
{
if(!(Weapon_info[swp->secondary_bank_weapons[i]].wi_flags & flags).any_set())
return false;
}
return true;
}
bool all_turret_weapons_have_flags(const ship_weapon *swp, Weapon::Info_Flags flags)
{
int i;
for (i = 0; i < swp->num_primary_banks; i++)
{
if (!(Weapon_info[swp->primary_bank_weapons[i]].wi_flags[flags]))
return false;
}
for (i = 0; i < swp->num_secondary_banks; i++)
{
if (!(Weapon_info[swp->secondary_bank_weapons[i]].wi_flags[flags]))
return false;
}
return true;
}
/**
* Returns true if any of the weapons in swp have flags
*
*/
bool turret_weapon_has_flags(const ship_weapon *swp, Weapon::Info_Flags flags)
{
Assert(swp != NULL);
int i = 0;
for(i = 0; i < swp->num_primary_banks; i++)
{
if(swp->primary_bank_weapons[i] >=0) {
if(Weapon_info[swp->primary_bank_weapons[i]].wi_flags[flags])
return true;
}
}
for(i = 0; i < swp->num_secondary_banks; i++)
{
if(swp->secondary_bank_weapons[i] >=0) {
if(Weapon_info[swp->secondary_bank_weapons[i]].wi_flags[flags])
return true;
}
}
return false;
}
/**
* Just gloms all the flags from all the weapons into one variable. More efficient if all you need to do is test for the existence of a flag.
*/
flagset<Weapon::Info_Flags> turret_weapon_aggregate_flags(const ship_weapon *swp)
{
Assert(swp != NULL);
int i = 0;
flagset<Weapon::Info_Flags> flags;
for (i = 0; i < swp->num_primary_banks; i++)
{
if (swp->primary_bank_weapons[i] >= 0) {
flags |= Weapon_info[swp->primary_bank_weapons[i]].wi_flags;
}
}
for (i = 0; i < swp->num_secondary_banks; i++)
{
if (swp->secondary_bank_weapons[i] >= 0) {
flags |= Weapon_info[swp->secondary_bank_weapons[i]].wi_flags;
}
}
return flags;
}
/**
* Returns true if any of the weapons in swp have the subtype specified
*
* @note It might be a little faster to optimize based on WP_LASER should only appear in primaries
* and WP_MISSILE in secondaries. but in the interest of future coding I leave it like this.
*/
bool turret_weapon_has_subtype(const ship_weapon *swp, int subtype)
{
Assert(swp != NULL);
int i = 0;
for(i = 0; i < swp->num_primary_banks; i++)
{
if(swp->primary_bank_weapons[i] >=0) {
if(Weapon_info[swp->primary_bank_weapons[i]].subtype == subtype)
return true;
}
}
for(i = 0; i < swp->num_secondary_banks; i++)
{
if(swp->secondary_bank_weapons[i] >=0) {
if(Weapon_info[swp->secondary_bank_weapons[i]].subtype == subtype)
return true;
}
}
return false;
}
/**
* Use for getting a Weapon_info pointer, given a turret and a turret weapon indice
*
* @return a pointer to the Weapon_info for weapon_num
*/
const weapon_info *get_turret_weapon_wip(const ship_weapon *swp, int weapon_num)
{
Assert(weapon_num < MAX_SHIP_WEAPONS);
Assert(weapon_num >= 0);
int wi_index;
if(weapon_num >= MAX_SHIP_PRIMARY_BANKS)
wi_index = swp->secondary_bank_weapons[weapon_num - MAX_SHIP_PRIMARY_BANKS];
else
wi_index = swp->primary_bank_weapons[weapon_num];
if (wi_index < 0)
return nullptr;
else
return &Weapon_info[wi_index];
}
// similar to the above, since "weapon_num" is a bank index over multiple banks
static std::pair<int, int> get_turret_weapon_bank(int weapon_num)
{
int pbank = -1;
int sbank = -1;
if (weapon_num >= MAX_SHIP_PRIMARY_BANKS)
sbank = weapon_num - MAX_SHIP_PRIMARY_BANKS;
else
pbank = weapon_num;
return { pbank, sbank };
}
int get_turret_weapon_next_fire_stamp(const ship_weapon *swp, int weapon_num)
{
Assert(weapon_num < MAX_SHIP_WEAPONS);
Assert(weapon_num >= 0);
if(weapon_num >= MAX_SHIP_PRIMARY_BANKS)
return swp->next_secondary_fire_stamp[weapon_num - MAX_SHIP_PRIMARY_BANKS];
else
return swp->next_primary_fire_stamp[weapon_num];
}
void set_turret_weapon_next_fire_stamp(ship_weapon *swp, int weapon_num, int delta_ms)
{
Assert(weapon_num < MAX_SHIP_WEAPONS);
Assert(weapon_num >= 0);
if (weapon_num >= MAX_SHIP_PRIMARY_BANKS)
swp->next_secondary_fire_stamp[weapon_num - MAX_SHIP_PRIMARY_BANKS] = timestamp(delta_ms);
else
swp->next_primary_fire_stamp[weapon_num] = timestamp(delta_ms);
}
/**
* Returns the longest-ranged weapon on a turret
*
* @todo This function is kinda slow
*/
float longest_turret_weapon_range(const ship_weapon *swp)
{
float longest_range_so_far = 0.0f;
float weapon_range;
weapon_info *wip;
int i = 0;
for(i = 0; i < swp->num_primary_banks; i++)
{
wip = &Weapon_info[swp->primary_bank_weapons[i]];
if (wip->wi_flags[Weapon::Info_Flags::Local_ssm])
weapon_range = wip->lssm_lock_range;
else
weapon_range = MIN(wip->lifetime * wip->max_speed, wip->weapon_range);
if(weapon_range > longest_range_so_far)
longest_range_so_far = weapon_range;
}
for(i = 0; i < swp->num_secondary_banks; i++)
{
wip = &Weapon_info[swp->secondary_bank_weapons[i]];
if (wip->wi_flags[Weapon::Info_Flags::Local_ssm])
weapon_range = wip->lssm_lock_range;
else
weapon_range = MIN(wip->lifetime * wip->max_speed, wip->weapon_range);
if(weapon_range > longest_range_so_far)
longest_range_so_far = weapon_range;
}
return longest_range_so_far;
}
/**
* Is valid turret target?
*
* @param objp Object that turret is considering as an enemy
* @param turret_parent Object of ship that turret sits on
*
* @return !0 if objp can be considered for a turret target, 0 otherwise
*/
int valid_turret_enemy(object *objp, object *turret_parent)
{
if ( objp == turret_parent ) {
return 0;
}
if ( objp->type == OBJ_ASTEROID ) {
return 1;
}
if ( objp->type == OBJ_SHIP ) {
Assert( objp->instance >= 0 );
ship *shipp;
ship_info *sip;
shipp = &Ships[objp->instance];
sip = &Ship_info[shipp->ship_info_index];
// don't fire at ships with protected bit set!!!
if ( objp->flags[Object::Object_Flags::Protected] ) {
return 0;
}
// don't shoot at ships without collision check
if (!(objp->flags[Object::Object_Flags::Collides]) && !(objp->flags[Object::Object_Flags::Attackable_if_no_collide])) {
return 0;
}
// don't shoot at arriving ships
if (shipp->is_arriving()) {
return 0;
}
// Goober5000 - don't fire at cargo containers (now specified in ship_types)
if ( (sip->class_type >= 0) && !(Ship_types[sip->class_type].flags[Ship::Type_Info_Flags::AI_turrets_attack]) ) {
return 0;
}
return 1;
}
if ( objp->type == OBJ_WEAPON ) {
Assert( objp->instance >= 0 );
weapon *wp = &Weapons[objp->instance];
weapon_info *wip = &Weapon_info[wp->weapon_info_index];
if (wip->subtype == WP_LASER && !(wip->wi_flags[Weapon::Info_Flags::Turret_Interceptable])) { // If the thing can't be shot down, don't try. -MageKing17
return 0;
}
if ( (!((wip->wi_flags[Weapon::Info_Flags::Bomb]) || (wip->wi_flags[Weapon::Info_Flags::Turret_Interceptable])) && !(Ai_info[Ships[turret_parent->instance].ai_index].ai_profile_flags[AI::Profile_Flags::Allow_turrets_target_weapons_freely]) ) ) {
return 0;
}
if ( (wip->wi_flags[Weapon::Info_Flags::Local_ssm]) && (wp->lssm_stage == 3) ) {
return 0;
}
if ( !iff_x_attacks_y(obj_team(turret_parent), wp->team) ) {
return 0;
}
// Mines are only targetable within their configured detection range
if (wip->is_mine()) {
if (vm_vec_dist(&objp->pos, &turret_parent->pos) > wip->mine_targetable_range)
return 0;
}
return 1;
}
return 0;
}
extern int Player_attacking_enabled;
void evaluate_obj_as_target(object *objp, eval_enemy_obj_struct *eeo)
{
object *turret_parent_obj = &Objects[eeo->turret_parent_objnum];
ship *shipp;
auto ss = eeo->turret_subsys;
float dist, dist_comp;
bool turret_has_no_target = false;
// Don't look for bombs when weapon system is not ok
if (objp->type == OBJ_WEAPON && !eeo->weapon_system_ok) {
return;
}
if ( !valid_turret_enemy(objp, turret_parent_obj) ) {
return;
}
#ifndef NDEBUG
if (!Player_attacking_enabled && (objp == Player_obj)) {
return;
}
#endif
if ( objp->type == OBJ_SHIP ) {
shipp = &Ships[objp->instance];
ship_info* sip = &Ship_info[shipp->ship_info_index];
// check on enemy team
if ( !iff_matches_mask(shipp->team, eeo->enemy_team_mask) ) {
return;
}
// check if protected
if (objp->flags[Object::Object_Flags::Protected]) {
return;
}
// check if beam protected
if (eeo->eeo_flags & EEOF_BEAM) {
if (objp->flags[Object::Object_Flags::Beam_protected]) {
return;
}
}
// check if flak protected
if (eeo->eeo_flags & EEOF_FLAK) {
if (objp->flags[Object::Object_Flags::Flak_protected]) {
return;
}
}
// check if laser protected
if (eeo->eeo_flags & EEOF_LASER) {
if (objp->flags[Object::Object_Flags::Laser_protected]) {
return;
}
}
// check if missile protected
if (eeo->eeo_flags & EEOF_MISSILE) {
if (objp->flags[Object::Object_Flags::Missile_protected]) {
return;
}
}
// don't shoot at big ships with huge weapons unless they have the flag
if (eeo->eeo_flags & EEOF_BIG_ONLY) {
if (sip->class_type == -1 || !(Ship_types[sip->class_type].flags[Ship::Type_Info_Flags::Targeted_by_huge_Ignored_by_small_only])) {
return;
}
}
// ^ Note the difference between these checks
// V "small only" EXCLUDES only big ships, "huge" INCLUDES only big ships
// don't shoot at ships ignored by small weapons if this is a small weapon
if (eeo->eeo_flags & EEOF_SMALL_ONLY) {
if (sip->class_type >= 0 && (Ship_types[sip->class_type].flags[Ship::Type_Info_Flags::Targeted_by_huge_Ignored_by_small_only])) {
return;
}
}
// check if turret flagged to only target tagged ships
// Note: retail behaviour was turrets with tagged-only could fire at bombs
// and could fire their spawn weapons
// this check is almost redundant; see the almost identical check in ai_fire_from_turret
// however if this is removed turrets still track targets but don't fire at them (which looks silly)
if (eeo->eeo_flags & EEOF_TAGGED_ONLY) {
if (!ship_is_tagged(objp) &&
( (The_mission.ai_profile->flags[AI::Profile_Flags::Strict_turret_tagged_only_targeting]) ||
( !(objp->type == OBJ_WEAPON) && !(turret_weapon_has_flags(&eeo->turret_subsys->weapons, Weapon::Info_Flags::Spawn))) )) {
return;
}
}
// check if valid target in nebula
if ( !object_is_targetable(objp, &Ships[Objects[eeo->turret_parent_objnum].instance]) ) {
// BYPASS ocassionally for stealth
int try_anyway = FALSE;
if ( is_object_stealth_ship(objp) ) {
float turret_stealth_find_chance = 0.5f;
float speed_mod = -0.1f + vm_vec_mag_quick(&objp->phys_info.vel) / 70.0f;
if (frand() > (turret_stealth_find_chance + speed_mod)) {
try_anyway = TRUE;
}
}
if (!try_anyway) {
return;
}
}
} else {
shipp = NULL;
}
// modify dist for BIG|HUGE, getting closest point on bbox, if not inside
vec3d vec_to_target;
vm_vec_sub(&vec_to_target, &objp->pos, eeo->tpos);
dist = vm_vec_mag_quick(&vec_to_target) - objp->radius;
if (dist < 0.0f) {
dist = 0.0f;
}
dist_comp = dist;
// if weapon has optimum range set then use it
float optimum_range = ss->optimum_range;
if (optimum_range > 0.0f) {
if (dist < optimum_range) {
dist_comp = (2*optimum_range) - dist;
}
}
// if turret has been told to prefer targets from the current direction then do so
float favor_one_side = ss->favor_current_facing;
if (favor_one_side >= 1.0f) {
vm_vec_normalize(&vec_to_target);
float dot_to_target = vm_vec_dot(&ss->turret_last_fire_direction, &vec_to_target);
dot_to_target = 1.0f - (dot_to_target / favor_one_side);
dist_comp *= dot_to_target;
}
// check if object is a bomb attacking the turret parent
// check if bomb is homing on the turret parent ship
bool check_weapon = true;
if ((Ai_info[Ships[turret_parent_obj->instance].ai_index].ai_profile_flags[AI::Profile_Flags::Prevent_targeting_bombs_beyond_range]) && (dist > eeo->weapon_travel_dist)) {
check_weapon = false;
}
if ((objp->type == OBJ_WEAPON) && check_weapon) {
//Maybe restrict the number of turrets attacking this bomb
if (ss->turret_max_bomb_ownage != -1) {
int num_att_turrets = num_turrets_attacking(turret_parent_obj, OBJ_INDEX(objp));
if (num_att_turrets > ss->system_info->turret_max_bomb_ownage) {
return;
}
}
if ( Weapons[objp->instance].homing_object == &Objects[eeo->turret_parent_objnum] ) {
if ( dist_comp < eeo->nearest_homing_bomb_dist ) {
if (!(ss->flags[Ship::Subsystem_Flags::FOV_Required]) && (eeo->current_enemy == -1)) {
turret_has_no_target = true;
}
if ( (turret_has_no_target) || object_in_turret_fov(objp, ss, eeo->tvec, eeo->tpos, dist + objp->radius) ) {
eeo->nearest_homing_bomb_dist = dist_comp;
eeo->nearest_homing_bomb_objnum = OBJ_INDEX(objp);
}
}
// if not homing, check if bomb is flying towards ship
} else if ( bomb_headed_towards_ship(objp, &Objects[eeo->turret_parent_objnum]) ) {
if ( dist_comp < eeo->nearest_bomb_dist ) {
if (!(ss->flags[Ship::Subsystem_Flags::FOV_Required]) && (eeo->current_enemy == -1)) {
turret_has_no_target = true;
}
if ( (turret_has_no_target) || object_in_turret_fov(objp, ss, eeo->tvec, eeo->tpos, dist + objp->radius) ) {
eeo->nearest_bomb_dist = dist_comp;
eeo->nearest_bomb_objnum = OBJ_INDEX(objp);
}
}
}
} // end weapon section
// maybe recalculate dist for big or huge ship
// if (shipp && (Ship_info[shipp->ship_info_index].is_big_or_huge())) {
// fvi_ray_boundingbox(min, max, start, direction, hit);
// dist = vm_vec_dist_quick(hit, tvec);
// }
// check for nearest attcker
if ( (shipp) && (dist < eeo->weapon_travel_dist) ) {
ai_info *aip = &Ai_info[shipp->ai_index];
// modify distance based on number of turrets from my ship attacking enemy (add 10% per turret)
// dist *= (num_enemies_attacking(OBJ_INDEX(objp))+2)/2; // prevents lots of ships from attacking same target
int num_att_turrets = num_turrets_attacking(turret_parent_obj, OBJ_INDEX(objp));
dist_comp *= (1.0f + 0.1f*num_att_turrets);
// return if we're over the cap
// int max_turrets = 3 + Game_skill_level * Game_skill_level;
int max_turrets = The_mission.ai_profile->max_turret_ownage_target[Game_skill_level];
if (objp->flags[Object::Object_Flags::Player_ship]) {
max_turrets = The_mission.ai_profile->max_turret_ownage_player[Game_skill_level];
}
// Apply the per-turret limit for small targets, if there is one and this is a small target
if (ss->turret_max_target_ownage != -1 && (Ship_info[shipp->ship_info_index].is_small_ship())) {
max_turrets = MIN(max_turrets, ss->system_info->turret_max_target_ownage);
}
if (num_att_turrets > max_turrets) {
return;
}
// modify distance based on lethality of objp to my ship
float active_lethality = aip->lethality;
if (objp->flags[Object::Object_Flags::Player_ship]) {
active_lethality += Player_lethality_bump[Game_skill_level];
}
dist_comp /= (1.0f + 0.01f*Lethality_range_const*active_lethality);
// Make level 2 tagged ships more likely to be targeted
if (shipp->level2_tag_left > 0.0f) {
dist_comp *= 0.3f;
}
// check if objp is targeting the turret's ship, or if objp has just hit the turret's ship
if ( aip->target_objnum == eeo->turret_parent_objnum || aip->last_objsig_hit == Objects[eeo->turret_parent_objnum].signature ) {
// A turret will always target a ship that is attacking itself... self-preservation!
if ( aip->targeted_subsys == eeo->turret_subsys ) {
dist_comp *= 0.5f; // highest priority
}
}
// maybe update nearest attacker
if ( dist_comp < eeo->nearest_attacker_dist ) {
if (!(ss->flags[Ship::Subsystem_Flags::FOV_Required]) && (eeo->current_enemy == -1)) {
turret_has_no_target = true;
}
if ( (turret_has_no_target) || object_in_turret_fov(objp, ss, eeo->tvec, eeo->tpos, dist + objp->radius) ) {
// nprintf(("AI", "Nearest enemy = %s, dist = %7.3f, dot = %6.3f, fov = %6.3f\n", Ships[objp->instance].ship_name, dist, vm_vec_dot(&v2e, tvec), tp->turret_fov));
eeo->nearest_attacker_dist = dist_comp;
eeo->nearest_attacker_objnum = OBJ_INDEX(objp);
}
}
} // end ship section
// check if object is an asteroid attacking the turret parent - taylor
if (objp->type == OBJ_ASTEROID) {
if ( eeo->turret_parent_objnum == asteroid_collide_objnum(objp) ) {
// give priority to the closest asteroid *impact* (ms intervals)
dist_comp *= 0.9f + (0.01f * asteroid_time_to_impact(objp));
if (dist_comp < eeo->nearest_dist ) {
if (!(ss->flags[Ship::Subsystem_Flags::FOV_Required]) && (eeo->current_enemy == -1)) {
turret_has_no_target = true;
}
if ( (turret_has_no_target) || object_in_turret_fov(objp, ss, eeo->tvec, eeo->tpos, dist + objp->radius) ) {
eeo->nearest_dist = dist_comp;
eeo->nearest_objnum = OBJ_INDEX(objp);
}
}
}
} // end asteroid selection
}
/**
* Given an object and an enemy team, return the index of the nearest enemy object.
*
* @param turret_parent_objnum Parent objnum for the turret
* @param turret_subsys Pointer to system_info for the turret subsystem
* @param enemy_team_mask OR'ed TEAM_ flags for the enemy of the turret parent ship
* @param tpos Position of turret (world coords)
* @param tvec Forward vector of turret (world coords)
* @param current_enemy Objnum of current turret target
* @param big_only_flag
* @param small_only_flag
* @param tagged_only_flag
* @param beam_flag
* @param flak_flag
* @param laser_flag
* @param missile_flag
*/
int get_nearest_turret_objnum(int turret_parent_objnum, const ship_subsys *turret_subsys, int enemy_team_mask, const vec3d *tpos, const vec3d *tvec, int current_enemy, bool big_only_flag, bool small_only_flag, bool tagged_only_flag, bool beam_flag, bool flak_flag, bool laser_flag, bool missile_flag)
{
eval_enemy_obj_struct eeo;
auto swp = &turret_subsys->weapons;
// list of stuff to go thru
ship_obj *so;
missile_obj *mo;
//wip=&Weapon_info[tp->turret_weapon_type];
//weapon_travel_dist = MIN(wip->lifetime * wip->max_speed, wip->weapon_range);
//if (wip->wi_flags[Weapon::Info_Flags::Local_ssm])
// weapon_travel_dist=wip->lssm_lock_range;
// Set flag based on strength of weapons subsystem. If weapons subsystem is destroyed, don't let turrets fire at bombs
bool weapon_system_ok = !ship_subsystems_blown(&Ships[Objects[turret_parent_objnum].instance], SUBSYSTEM_WEAPONS);
// Initialize eeo struct.
eeo.turret_parent_objnum = turret_parent_objnum;
eeo.weapon_system_ok = weapon_system_ok;
eeo.weapon_travel_dist = longest_turret_weapon_range(swp);
// set flags
eeo.eeo_flags = 0;
if (big_only_flag)
eeo.eeo_flags |= EEOF_BIG_ONLY;
if (small_only_flag)
eeo.eeo_flags |= EEOF_SMALL_ONLY;
if (tagged_only_flag)
eeo.eeo_flags |= EEOF_TAGGED_ONLY;
// flags for weapon types
if (beam_flag)
eeo.eeo_flags |= EEOF_BEAM;
if (flak_flag)
eeo.eeo_flags |= EEOF_FLAK;
if (laser_flag)
eeo.eeo_flags |= EEOF_LASER;
if (missile_flag)
eeo.eeo_flags |= EEOF_MISSILE;
eeo.enemy_team_mask = enemy_team_mask;
eeo.current_enemy = current_enemy;
eeo.tpos = tpos;
eeo.tvec = tvec;
eeo.turret_subsys = turret_subsys;
// here goes the new targeting priority setting
int n_tgt_priorities;
int priority_weapon_idx = -1;
// check for turret itself first
n_tgt_priorities = turret_subsys->num_target_priorities;
// turret had no priorities set for it.. try weapons
if (n_tgt_priorities <= 0) {
if (swp->num_primary_banks > 0)
// first try highest primary slot...
priority_weapon_idx = swp->primary_bank_weapons[0];
else
// ...and then secondary slot
priority_weapon_idx = swp->secondary_bank_weapons[0];
if (priority_weapon_idx > -1)
n_tgt_priorities = Weapon_info[priority_weapon_idx].num_targeting_priorities;
}
if (n_tgt_priorities > 0)
{
for(int i = 0; i < n_tgt_priorities; i++) {
// courtesy of WMC...
ai_target_priority *tt;
if (priority_weapon_idx == -1)
tt = &Ai_tp_list[turret_subsys->target_priority[i]];
else
tt = &Ai_tp_list[Weapon_info[priority_weapon_idx].targeting_priorities[i]];
int n_types = (int)tt->ship_type.size();
int n_s_classes = (int)tt->ship_class.size();
int n_w_classes = (int)tt->weapon_class.size();
bool found_something;
for (auto ptr: list_range(&obj_used_list)) {
if (ptr->flags[Object::Object_Flags::Should_be_dead])
continue;
found_something = false;
if(tt->obj_type > -1 && (ptr->type == tt->obj_type)) {
found_something = true;
}
if( ( n_types > 0 ) && ( ptr->type == OBJ_SHIP ) ) {
for (int j = 0; j < n_types; j++) {
if ( Ship_info[Ships[ptr->instance].ship_info_index].class_type == tt->ship_type[j] ) {
found_something = true;
}
}
}
if( ( n_s_classes > 0 ) && ( ptr->type == OBJ_SHIP ) ) {
for (int j = 0; j < n_s_classes; j++) {
if ( Ships[ptr->instance].ship_info_index == tt->ship_class[j] ) {
found_something = true;
}
}
}
if( ( n_w_classes > 0 ) && ( ptr->type == OBJ_WEAPON ) ) {
for (int j = 0; j < n_w_classes; j++) {
if ( Weapons[ptr->instance].weapon_info_index == tt->weapon_class[j] ) {
found_something = true;
}
}
}
if( (tt->wif_flags.any_set()) && (ptr->type == OBJ_WEAPON) ) {
if( ( (Weapon_info[Weapons[ptr->instance].weapon_info_index].wi_flags & tt->wif_flags ) == tt->wif_flags) ) {
found_something = true;
}
}
if( ( tt->sif_flags.any_set() && (ptr->type == OBJ_SHIP) ) ) {
if( (Ship_info[Ships[ptr->instance].ship_info_index].flags & tt->sif_flags) == tt->sif_flags)
{
found_something = true;
}
}
if ((tt->obj_flags.any_set()) && !((ptr->flags & tt->obj_flags) == tt->obj_flags)) {
found_something = true;
}
if(!(found_something)) {
//we didnt find this object within this priority group
//skip to next without evaluating the object as target
continue;
}
evaluate_obj_as_target(ptr, &eeo);
}
//homing weapon entry...
/*
if ( eeo.nearest_homing_bomb_objnum != -1 ) { // highest priority is an incoming homing bomb
return eeo.nearest_homing_bomb_objnum;
//weapon entry...
} else if ( eeo.nearest_bomb_objnum != -1 ) { // next highest priority is an incoming dumbfire bomb
return eeo.nearest_bomb_objnum;
//ship entry...
} else if ( eeo.nearest_attacker_objnum != -1 ) { // next highest priority is an attacking ship
return eeo.nearest_attacker_objnum;
//something else entry...
} else if ( eeo.nearest_objnum != -1 ) {
return eeo.nearest_objnum;
}
*/
// if we got something...
if ( ( eeo.nearest_homing_bomb_objnum != -1 ) ||
( eeo.nearest_bomb_objnum != -1 ) ||
( eeo.nearest_attacker_objnum != -1 ) ||
( eeo.nearest_objnum != -1 ) )
{
// ...start with homing bombs...
int return_objnum = eeo.nearest_homing_bomb_objnum;
float return_distance = eeo.nearest_homing_bomb_dist;
// ...next test non-homing bombs...
if ( eeo.nearest_bomb_dist < return_distance ) {
return_objnum = eeo.nearest_bomb_objnum;
return_distance = eeo.nearest_bomb_dist;
}
// ...then attackers...
if ( eeo.nearest_attacker_dist < return_distance ) {
return_objnum = eeo.nearest_attacker_objnum;
return_distance = eeo.nearest_attacker_dist;
}
// ...and finally the rest of the lot...
if ( eeo.nearest_dist < return_distance ) {
return_objnum = eeo.nearest_objnum;
}
// ...and return the objnum to the closest target regardless
return return_objnum;
}
}
} else
{
flagset<Weapon::Info_Flags> tmp_flagset;
const Weapon::Info_Flags weapon_flags[] = { Weapon::Info_Flags::Huge, Weapon::Info_Flags::Flak, Weapon::Info_Flags::Homing_aspect, Weapon::Info_Flags::Homing_heat, Weapon::Info_Flags::Homing_javelin, Weapon::Info_Flags::Spawn };
tmp_flagset.set_multiple(std::begin(weapon_flags), std::end(weapon_flags));
for(int i = 0; i < NUM_TURRET_ORDER_TYPES; i++)
{
ai_info *aip = &Ai_info[Ships[Objects[eeo.turret_parent_objnum].instance].ai_index];
switch(turret_subsys->turret_targeting_order[i])
{
case -1:
//Empty priority slot
break;
case 0:
//Return if a bomb is found
//don't fire anti capital ship turrets at bombs.
if ( !((aip->ai_profile_flags[AI::Profile_Flags::Huge_turret_weapons_ignore_bombs]) && big_only_flag) )
{
// Missile_obj_list
for( mo = GET_FIRST(&Missile_obj_list); mo != END_OF_LIST(&Missile_obj_list); mo = GET_NEXT(mo) ) {
auto objp = &Objects[mo->objnum];
if (objp->flags[Object::Object_Flags::Should_be_dead])
continue;
Assert(objp->type == OBJ_WEAPON);
if ((Weapon_info[Weapons[objp->instance].weapon_info_index].wi_flags[Weapon::Info_Flags::Bomb]) || (Weapon_info[Weapons[objp->instance].weapon_info_index].wi_flags[Weapon::Info_Flags::Turret_Interceptable]))
{
evaluate_obj_as_target(objp, &eeo);
}
}
// highest priority
if ( eeo.nearest_homing_bomb_objnum != -1 ) { // highest priority is an incoming homing bomb
return eeo.nearest_homing_bomb_objnum;
} else if ( eeo.nearest_bomb_objnum != -1 ) { // next highest priority is an incoming dumbfire bomb
return eeo.nearest_bomb_objnum;
}
}
break;
case 1:
//Return if a ship is found
// Ship_used_list
for ( so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so) ) {
auto objp = &Objects[so->objnum];
if (objp->flags[Object::Object_Flags::Should_be_dead])
continue;
evaluate_obj_as_target(objp, &eeo);
}
// next highest priority is attacking ship
if ( eeo.nearest_attacker_objnum != -1 ) { // next highest priority is an attacking ship
return eeo.nearest_attacker_objnum;
}
break;
case 2:
//Return if an asteroid is found
// asteroid check - taylor
asteroid_obj *ao;
// don't use turrets that are better for other things:
// - no cap ship beams
// - no flak
// - no heat or aspect missiles
// - no spawn type missiles/bombs
// do use for sure:
// - lasers
// - dumbfire type missiles
// - AAA beams
if ( !all_turret_weapons_have_flags(swp, tmp_flagset) ) {
// Asteroid_obj_list
for ( ao = GET_FIRST(&Asteroid_obj_list); ao != END_OF_LIST(&Asteroid_obj_list); ao = GET_NEXT(ao) ) {
auto objp = &Objects[ao->objnum];
if (objp->flags[Object::Object_Flags::Should_be_dead])
continue;
evaluate_obj_as_target(objp, &eeo);
}
if (eeo.nearest_objnum != -1) {
return eeo.nearest_objnum;
}
}
break;
default:
UNREACHABLE("Invalid target type of %d sent to get_nearest_turret_objnum. Please report to the SCP!", turret_subsys->turret_targeting_order[i]);
}
}
}
return -1;
}
int Use_parent_target = 0;
DCF_BOOL(use_parent_target, Use_parent_target)
/**
* Return objnum if enemy found, else return -1;
*
* @param turret_subsys Pointer to turret subsystem
* @param objnum Parent objnum for the turret
* @param tpos Position of turret (world coords)
* @param tvec Forward vector of turret (world coords)
* @param current_enemy Objnum of current turret target
*/
int find_turret_enemy(const ship_subsys *turret_subsys, int objnum, const vec3d *tpos, const vec3d *tvec, int current_enemy)
{
int enemy_team_mask, enemy_objnum;
ship_info *sip;
enemy_team_mask = iff_get_attackee_mask(obj_team(&Objects[objnum]));
bool big_only_flag = all_turret_weapons_have_flags(&turret_subsys->weapons, Weapon::Info_Flags::Huge);
bool small_only_flag = all_turret_weapons_have_flags(&turret_subsys->weapons, Weapon::Info_Flags::Small_only);
bool tagged_only_flag = all_turret_weapons_have_flags(&turret_subsys->weapons, Weapon::Info_Flags::Tagged_only) || (turret_subsys->weapons.flags[Ship::Weapon_Flags::Tagged_Only]);
bool beam_flag = turret_weapon_has_flags(&turret_subsys->weapons, Weapon::Info_Flags::Beam);
bool flak_flag = turret_weapon_has_flags(&turret_subsys->weapons, Weapon::Info_Flags::Flak);
bool laser_flag = turret_weapon_has_subtype(&turret_subsys->weapons, WP_LASER);
bool missile_flag = turret_weapon_has_subtype(&turret_subsys->weapons, WP_MISSILE);
// If a small ship and target_objnum != -1, use that as goal.
ai_info *aip = &Ai_info[Ships[Objects[objnum].instance].ai_index];
sip = &Ship_info[Ships[Objects[objnum].instance].ship_info_index];
if ((sip->class_type >= 0) && (Ship_types[sip->class_type].flags[Ship::Type_Info_Flags::Turret_tgt_ship_tgt]) && (aip->target_objnum != -1)) {
int target_objnum = aip->target_objnum;
if (Objects[target_objnum].signature == aip->target_signature) {
// The parent's target can be a weapon or an asteroid, so make sure it's a ship before indexing Ships[]
ship* target_shipp = (Objects[target_objnum].type == OBJ_SHIP) ? &Ships[Objects[target_objnum].instance] : nullptr;
if (target_shipp && iff_matches_mask(target_shipp->team, enemy_team_mask)) {
if (!(Objects[target_objnum].flags[Object::Object_Flags::Protected])) { // check this flag as well
// nprintf(("AI", "Frame %i: Object %i resuming goal of object %i\n", AI_FrameCount, objnum,
// target_objnum));
return target_objnum;
}
}
} else {
aip->target_objnum = -1;
aip->target_signature = -1;
}
// Not small or small with target objnum
} else {
// maybe use aip->target_objnum as next target
if ((frand() < 0.8f) && (aip->target_objnum != -1) && Use_parent_target) {
//check if aip->target_objnum is valid target
auto target_flags = Objects[aip->target_objnum].flags;
if ( target_flags[Object::Object_Flags::Protected] ) {
// AL 2-27-98: why is a protected ship being targeted?
set_target_objnum(aip, -1);
return -1;
}
// maybe use ship target_objnum if valid for turret
// check for beam weapon and beam protected, etc.
bool skip = false;
if ( target_flags[Object::Object_Flags::Beam_protected] && beam_flag ) skip = true;
else if ( target_flags[Object::Object_Flags::Flak_protected] && flak_flag ) skip = true;
else if ( target_flags[Object::Object_Flags::Laser_protected] && laser_flag ) skip = true;
else if ( target_flags[Object::Object_Flags::Missile_protected] && missile_flag) skip = true;
if (!skip) {
if ( Objects[aip->target_objnum].type == OBJ_SHIP ) {
ship_info* esip = &Ship_info[Ships[Objects[aip->target_objnum].instance].ship_info_index];
// check for huge weapon and huge ship
if ( !big_only_flag || (esip->class_type >= 0 && Ship_types[esip->class_type].flags[Ship::Type_Info_Flags::Targeted_by_huge_Ignored_by_small_only]) ) {
// check for tagged only and tagged ship
if ( tagged_only_flag && ship_is_tagged(&Objects[aip->target_objnum]) ) {
// select new target if aip->target_objnum is out of field of view
vec3d v2e;
bool in_fov;
vm_vec_normalized_dir(&v2e, &Objects[aip->target_objnum].pos, tpos);
in_fov = turret_fov_test(turret_subsys, tvec, &v2e);
// MODIFY FOR ATTACKING BIG SHIP
// dot += (0.5f * Objects[aip->target_objnum].radius / dist);
if (in_fov) {
return aip->target_objnum;
}
}
}
}
}
}
}
enemy_objnum = get_nearest_turret_objnum(objnum, turret_subsys, enemy_team_mask, tpos, tvec, current_enemy, big_only_flag, small_only_flag, tagged_only_flag, beam_flag, flak_flag, laser_flag, missile_flag);
if ( enemy_objnum >= 0 ) {
Assert( !((Objects[enemy_objnum].flags[Object::Object_Flags::Beam_protected]) && beam_flag) );
Assert( !((Objects[enemy_objnum].flags[Object::Object_Flags::Flak_protected]) && flak_flag) );
Assert( !((Objects[enemy_objnum].flags[Object::Object_Flags::Laser_protected]) && laser_flag) );
Assert( !((Objects[enemy_objnum].flags[Object::Object_Flags::Missile_protected]) && missile_flag) );
Assertion(!Objects[enemy_objnum].flags[Object::Object_Flags::Protected], "find_turret_enemy selected an object of type %d %s that is protected, please report to the SCP!", Objects[enemy_objnum].type, Objects[enemy_objnum].type == OBJ_SHIP ? Ships[Objects[enemy_objnum].instance].ship_name : "");
if ( Objects[enemy_objnum].flags[Object::Object_Flags::Protected] ) {
enemy_objnum = aip->target_objnum;
}
}
return enemy_objnum;
}
/**
* Given an object and a turret on that object, return the global position and forward vector
* of the turret.
*
* @param objp Pointer to object
* @param tp Turret model system on that object
* @param gpos [Output] Global absolute position of gun firing point
* @param gvec [Output] Global vector
*
* @note The gun normal is the unrotated gun normal, (the center of the FOV cone), not
* the actual gun normal given using the current turret heading. But it _is_ rotated into the model's orientation
* in global space.
* @note2 Because of this, both single-part and multi-part turrets are treated the same way; no need to find the multi-part's gun submodel.
*/
void ship_get_global_turret_info(const object *objp, const model_subsystem *tp, vec3d *gpos, vec3d *gvec)
{
auto model_instance_num = Ships[objp->instance].model_instance_num;
if (gpos)
model_instance_local_to_global_point(gpos, &vmd_zero_vector, model_instance_num, tp->subobj_num, &objp->orient, &objp->pos);
if (gvec)
model_instance_local_to_global_dir(gvec, &tp->turret_norm, model_instance_num, tp->subobj_num, &objp->orient, true);
}
/**
* Given an object and a turret on that object, return the actual firing point of the gun and its normal.
*
* @note This uses the current turret angles. We are keeping track of which
* gun to fire next in the ship specific info for this turret subobject. Use this info
* to determine which position to fire from next.
*
* @param objp Pointer to object
* @param ssp Pointer to turret subsystem
* @param gpos Absolute position of gun firing point
* @param avg_origin Use virtual gun pos corresponding to the average of the firing points
* @param gvec Vector from *gpos to *targetp
* @param use_angles Use current angles
* @param targetp Absolute position of target object
*/
void ship_get_global_turret_gun_info(const object *objp, const ship_subsys *ssp, vec3d *gpos, bool avg_origin, vec3d *gvec, bool use_angles, const vec3d *targetp)
{
vec3d *gun_pos;
model_subsystem *tp = ssp->system_info;
polymodel_instance *pmi = model_get_instance(Ships[objp->instance].model_instance_num);
polymodel *pm = model_get(pmi->model_num);
vec3d avg_gun_pos;
if (avg_origin) {
vm_vec_avg_n(&avg_gun_pos, tp->turret_num_firing_points, tp->turret_firing_point);
gun_pos = &avg_gun_pos;
} else {
gun_pos = &tp->turret_firing_point[ssp->turret_next_fire_pos % tp->turret_num_firing_points];
}
model_instance_local_to_global_point(gpos, gun_pos, pm, pmi, tp->turret_gun_sobj, &objp->orient, &objp->pos);
// we might not need to calculate this
if (!gvec)
return;
if (use_angles) {
model_instance_local_to_global_dir(gvec, &tp->turret_norm, pm, pmi, tp->turret_gun_sobj, &objp->orient);
vm_vec_normalize(gvec);
} else {
Assertion(targetp != nullptr, "The targetp parameter must not be null here!");
vm_vec_normalized_dir(gvec, targetp, gpos);
}
}
/**
* Update turret aiming data based on max turret aim update delay
*/
void turret_ai_update_aim(const ai_info *aip, const object *En_Objp, ship_subsys *ss)
{
if (Missiontime >= ss->next_aim_pos_time)
{
ss->last_aim_enemy_pos = En_Objp->pos;
ss->last_aim_enemy_vel = En_Objp->phys_info.vel;
ss->next_aim_pos_time = Missiontime + fl2f(frand_range(0.0f, aip->ai_turret_max_aim_update_delay));
}
else
{
//Update the position based on the velocity (assume no velocity vector change)
vm_vec_scale_add2(&ss->last_aim_enemy_pos, &ss->last_aim_enemy_vel, flFrametime);
}
}
/**
* Sets predicted enemy position. Previously done at the beginning of aifft_rotate_turret.
* If the turret (*ss) has a subsystem targeted, the subsystem is used as the predicted point.
*/
void aifft_update_predicted_enemy_pos(const object *objp, const ship *shipp, ship_subsys *ss, const vec3d *global_gun_pos, const vec3d *global_gun_vec, const object *lep, vec3d *predicted_enemy_pos)
{
if (ss->turret_enemy_objnum != -1) {
model_subsystem *tp = ss->system_info;
vec3d target_vel;
float weapon_system_strength;
//HACK HACK HACK -WMC
//This should use the best weapon variable
//It doesn't, because I haven't implemented code to set it yet -WMC
int best_weapon_tidx = turret_select_best_weapon(ss, lep);
//This turret doesn't have any good weapons
if (best_weapon_tidx < 0)
return;
auto wip = get_turret_weapon_wip(&ss->weapons, best_weapon_tidx);
// weapon_system_strength scales time enemy in range in 0..1. So, the lower this is, the worse the aiming will be.
weapon_system_strength = ship_get_subsystem_strength(shipp, SUBSYSTEM_WEAPONS);
//Update "known" position and velocity of target. Only matters if max_aim_update_delay is set.
turret_ai_update_aim(&Ai_info[shipp->ai_index], &Objects[ss->turret_enemy_objnum], ss);
//Figure out what point on the ship we want to point the gun at, and store the global location
//in enemy_point.
vec3d enemy_point;
if ((ss->targeted_subsys != NULL) && !(ss->flags[Ship::Subsystem_Flags::No_SS_targeting])) {
if (ss->turret_enemy_objnum != -1) {
vm_vec_unrotate(&enemy_point, &ss->targeted_subsys->system_info->pnt, &Objects[ss->turret_enemy_objnum].orient);
vm_vec_add2(&enemy_point, &ss->last_aim_enemy_pos);
}
} else {
if ((lep->type == OBJ_SHIP) && (Ship_info[Ships[lep->instance].ship_info_index].is_big_or_huge())) {
ai_big_pick_attack_point_turret(lep, ss, global_gun_pos, global_gun_vec, &enemy_point, tp->turret_fov, MIN(wip->max_speed * wip->lifetime, wip->weapon_range));
} else {
enemy_point = ss->last_aim_enemy_pos;
}
}
target_vel = ss->last_aim_enemy_vel;
//Try to guess where the enemy will be, and store that spot in predicted_enemy_pos
if (The_mission.ai_profile->flags[AI::Profile_Flags::Use_additive_weapon_velocity]) {
vm_vec_scale_sub2(&target_vel, &objp->phys_info.vel, wip->vel_inherit_amount);
}
if (IS_VEC_NULL(&The_mission.gravity) || wip->gravity_const == 0.0f)
set_predicted_enemy_pos_turret(predicted_enemy_pos, global_gun_pos, objp, &enemy_point, &target_vel, wip->max_speed, ss->turret_time_enemy_in_range * (weapon_system_strength + 1.0f)/2.0f);
else {
vec3d shoot_vec;
vec3d gravity_vec = The_mission.gravity * wip->gravity_const;
if (physics_lead_ballistic_trajectory(global_gun_pos, &enemy_point, &target_vel, wip->max_speed, &gravity_vec, &shoot_vec)) {
*predicted_enemy_pos = *global_gun_pos + shoot_vec * 1000.0f;
}
}
//Mess with the turret's accuracy if the weapon system is damaged.
if (weapon_system_strength < Weapon_SS_Threshold_Turret_Inaccuracy) {
vec3d rand_vec;
static_randvec(Missiontime >> 18, &rand_vec); // Return same random number for two seconds.
// Add to predicted_enemy_pos value in .45 to 1.5x radius of enemy ship, so will often miss, but not by a huge amount.
vm_vec_scale_add2(predicted_enemy_pos, &rand_vec, (1.0f - weapon_system_strength)*1.5f * lep->radius);
}
}
}
/**
* Rotate a turret towards an enemy.
*
* Some obscure model thing only John Slagel knows about.
*
* @return TRUE if caller should use angles in subsequent rotations.
*/
bool aifft_rotate_turret(const object *objp, const ship *shipp, ship_subsys *ss, const vec3d *global_gun_pos, const vec3d *global_gun_vec, const vec3d *predicted_enemy_pos)
{
bool ret_val __UNUSED = false; // to be used in future, see comment @ end of function
auto pmi = model_get_instance(shipp->model_instance_num);
auto pm = model_get(pmi->model_num);
bool in_lab = (gameseq_get_state() == GS_STATE_LAB);
if (!in_lab && ss->turret_enemy_objnum != -1) {
model_subsystem* tp = ss->system_info;
// Get the normalized dir between the turret and the predicted enemy position.
// If the dot product is smaller than or equal to the turret's FOV, try and point the gun at it.
vec3d v2e;
vm_vec_normalized_dir(&v2e, predicted_enemy_pos, global_gun_pos);
bool in_fov;
in_fov = turret_fov_test(ss, global_gun_vec, &v2e);
if (in_fov) {
ret_val = model_rotate_gun(objp, pm, pmi, ss, predicted_enemy_pos);
} else if ((tp->flags[Model::Subsystem_Flags::Turret_reset_idle]) &&(timestamp_elapsed(ss->rotation_timestamp))) {
ret_val = model_rotate_gun(objp, pm, pmi, ss, nullptr);
}
} else if (!in_lab && (ss->system_info->flags[Model::Subsystem_Flags::Turret_reset_idle]) && (timestamp_elapsed(ss->rotation_timestamp))) {
ret_val = model_rotate_gun(objp, pm, pmi, ss, nullptr);
} else if (in_lab) {
// in the lab, if predicted_enemy_pos is zero vec, then go to initial, else try to aim at the target
if (predicted_enemy_pos != nullptr && *predicted_enemy_pos == vmd_zero_vector) {
ret_val = model_rotate_gun(objp, pm, pmi, ss, nullptr);
} else {
ret_val = model_rotate_gun(objp, pm, pmi, ss, predicted_enemy_pos);
}
}
// by default "ret_val" should be set to true for multi-part turrets, and false for single-part turrets
// but we need to keep retail behavior by default, which means always returning false unless a special
// flag is used. the "ret_val" stuff is here is needed/wanted at a later date however.
//return ret_val;
// return false by default (to preserve retail behavior) but allow for a per-subsystem option
// for using the turret normals for firing
if (ss->system_info->flags[Model::Subsystem_Flags::Fire_on_normal])
return true;
return false;
}
/**
* Determine if subsystem *enemy_subsysp is hittable from objp.
*
* @return Dot product of vector from point abs_gunposp to *enemy_subsysp, if hittable
*/
float aifft_compute_turret_dot(const object *objp, const object *enemy_objp, const vec3d *abs_gunposp, const ship_subsys *turret_subsysp, const ship_subsys *enemy_subsysp)
{
float dot_out;
vec3d subobj_pos, vector_out;
vm_vec_unrotate(&subobj_pos, &enemy_subsysp->system_info->pnt, &enemy_objp->orient);
vm_vec_add2(&subobj_pos, &enemy_objp->pos);
if (ship_subsystem_in_sight(enemy_objp, enemy_subsysp, abs_gunposp, &subobj_pos, true, &dot_out, &vector_out)) {
vec3d turret_norm;
model_instance_local_to_global_dir(&turret_norm, &turret_subsysp->system_info->turret_norm, Ships[objp->instance].model_instance_num, turret_subsysp->system_info->subobj_num, &objp->orient, true);
float dot_return = vm_vec_dot(&turret_norm, &vector_out);
if (Ai_info[Ships[objp->instance].ai_index].ai_profile_flags[AI::Profile_Flags::Smart_subsystem_targeting_for_turrets]) {
if (dot_return > turret_subsysp->system_info->turret_fov) {
// target is in sight and in fov
return dot_return;
} else {
// target is in sight but is not in turret's fov
return -1.0f;
}
} else {
// target is in sight and we don't care if its in turret's fov or not
return dot_return;
}
} else
return -1.0f;
}
// NOTE: Do not change this value unless you understand exactly what it means and what it does.
// It refers to how many (non-destroyed) subsystems (and turrets) will be scanned for possible
// targeting, per turret, per frame. A higher value will process more systems at once,
// but it will be much slower to scan though them. It is not necessary to scan all
// non-destroyed subsystem each frame for each turret. Also, "aifft_max_checks" is balanced
// against the original value, be sure to account for this discrepancy with any changes.
#define MAX_AIFFT_TURRETS 60
ship_subsys *aifft_list[MAX_AIFFT_TURRETS];
float aifft_rank[MAX_AIFFT_TURRETS];
int aifft_list_size = 0;
int aifft_max_checks = 5;
DCF(mf, "Adjusts the maximum number of tries an AI may do when trying to pick a subsystem to attack (Default is 5)")
{
dc_stuff_int(&aifft_max_checks);
if (aifft_max_checks <= 0) {
dc_printf("Value must be a non-negative, non-zero integer\n");
dc_printf("aifft_max_checks set to default value of 5\n");
aifft_max_checks = 5;
}
}
/**
* Pick a subsystem to attack on enemy_objp.
* Only pick one if enemy_objp is a big ship or a capital ship.
*
* @return Dot product from turret to subsystem in *dot_out
*/
ship_subsys *aifft_find_turret_subsys(const object *objp, const ship_subsys *ssp, const vec3d *global_gun_pos, const object *enemy_objp, float *dot_out)
{
ship *eshipp, *shipp;
ship_info *esip;
ship_subsys *best_subsysp = NULL;
float dot;
Assert(enemy_objp->type == OBJ_SHIP);
eshipp = &Ships[enemy_objp->instance];
esip = &Ship_info[eshipp->ship_info_index];
shipp = &Ships[objp->instance];
float best_dot = 0.0f;
*dot_out = best_dot;
// Only pick a turret to attack on large ships.
if (!esip->is_big_or_huge())
return best_subsysp;
// Make sure big or huge ship *actually* has subsystems (ie, knossos)
if (esip->n_subsystems == 0) {
return best_subsysp;
}
// first build up a list subsystems to traverse
ship_subsys *pss;
aifft_list_size = 0;
for ( pss = GET_FIRST(&eshipp->subsys_list); pss !=END_OF_LIST(&eshipp->subsys_list); pss = GET_NEXT(pss) ) {
model_subsystem *psub = pss->system_info;
// if we've reached max turrets bail
if(aifft_list_size >= MAX_AIFFT_TURRETS){
break;
}
// Don't process destroyed objects
if ( pss->current_hits <= 0.0f ){
continue;
}
switch (psub->type) {
case SUBSYSTEM_WEAPONS:
aifft_list[aifft_list_size] = pss;
aifft_rank[aifft_list_size++] = 1.4f;
break;
case SUBSYSTEM_TURRET:
aifft_list[aifft_list_size] = pss;
aifft_rank[aifft_list_size++] = 1.2f;
break;
case SUBSYSTEM_SENSORS:
case SUBSYSTEM_ENGINE:
aifft_list[aifft_list_size] = pss;
aifft_rank[aifft_list_size++] = 1.0f;
break;
}
}
// DKA: 6/28/99 all subsystems can be destroyed.
//Assert(aifft_list_size > 0);
if (aifft_list_size == 0) {
return best_subsysp;
}
// determine a stride value so we're not checking too many turrets
int stride = aifft_list_size > aifft_max_checks ? aifft_list_size / aifft_max_checks : 1;
if(stride <= 0){
stride = 1;
}
int offset = Random::next((aifft_list_size % stride) + 1); // returns an offset in [0, size % stride] inclusive
int idx;
float dot_fov_modifier = 0.0f;
if (Ai_info[shipp->ai_index].ai_profile_flags[AI::Profile_Flags::Smart_subsystem_targeting_for_turrets]) {
if (ssp->system_info->turret_fov < 0)
dot_fov_modifier = ssp->system_info->turret_fov;
}
for(idx=offset; idx<aifft_list_size; idx+=stride){
dot = aifft_compute_turret_dot(objp, enemy_objp, global_gun_pos, ssp, aifft_list[idx]);
if ((dot - dot_fov_modifier)* aifft_rank[idx] > best_dot) {
best_dot = (dot - dot_fov_modifier)*aifft_rank[idx];
best_subsysp = aifft_list[idx];
}
}
Assert(best_subsysp != &eshipp->subsys_list);
*dot_out = best_dot;
return best_subsysp;
}
/**
* @return true if the specified target should scan for a new target, otherwise return false
*/
bool turret_should_pick_new_target(ship_subsys *turret)
{
return timestamp_elapsed(turret->turret_next_enemy_check_stamp);
}
/**
* Set the next fire timestamp for a turret, based on weapon type and ai class
*/
void turret_set_next_fire_timestamp(int weapon_num, const weapon_info *wip, ship_subsys *turret, const ai_info *aip, const WeaponLaunchCurveData& launch_curve_data)
{
Assert(weapon_num < MAX_SHIP_WEAPONS);
float wait = 1000.0f;
// we have to add 1 to wip->burst_shots so that the multiplier behaves as expected, then remove 1 from burst_shots to match the zero-indexed burst_counter
int base_burst_shots = (wip->burst_flags[Weapon::Burst_Flags::Num_firepoints_burst_shots] ? turret->system_info->turret_num_firing_points : wip->burst_shots + 1);
float burst_shots_mult = wip->weapon_launch_curves.get_output(weapon_info::WeaponLaunchCurveOutputs::BURST_SHOTS_MULT, launch_curve_data);
int burst_shots = MAX(fl2i(i2fl(base_burst_shots) * burst_shots_mult) - 1, 0);
bool burst = burst_shots > turret->weapons.burst_counter[weapon_num];
if (burst) {
wait *= wip->burst_delay;
wait *= wip->weapon_launch_curves.get_output(weapon_info::WeaponLaunchCurveOutputs::BURST_DELAY_MULT, launch_curve_data);
turret->weapons.burst_counter[weapon_num]++;
} else {
// Random fire delay (DahBlount) used in ship_fire_primary(), added here by wookieejedi to correct oversight
if (wip->max_delay != 0.0f && wip->min_delay != 0.0f) {
wait *= frand_range(wip->min_delay, wip->max_delay);
wait *= wip->weapon_launch_curves.get_output(weapon_info::WeaponLaunchCurveOutputs::FIRE_WAIT_MULT, launch_curve_data);
} else {
wait *= wip->fire_wait;
wait *= wip->weapon_launch_curves.get_output(weapon_info::WeaponLaunchCurveOutputs::FIRE_WAIT_MULT, launch_curve_data);
}
if ((burst_shots > 0) && (wip->burst_flags[Weapon::Burst_Flags::Random_length])) {
turret->weapons.burst_counter[weapon_num] = Random::next(burst_shots);
turret->weapons.burst_seed[weapon_num] = Random::next();
} else {
turret->weapons.burst_counter[weapon_num] = 0;
turret->weapons.burst_seed[weapon_num] = Random::next();
}
}
int *fs_dest;
if(weapon_num < MAX_SHIP_PRIMARY_BANKS)
fs_dest = &turret->weapons.next_primary_fire_stamp[weapon_num];
else
fs_dest = &turret->weapons.next_secondary_fire_stamp[weapon_num - MAX_SHIP_PRIMARY_BANKS];
//Check for the new cooldown flag
if(!((wip->wi_flags[Weapon::Info_Flags::Same_turret_cooldown]) || ((burst_shots > 0) && (wip->burst_flags[Weapon::Burst_Flags::Fast_firing]))))
{
// make side even for team vs. team
if (MULTI_TEAM) {
// flak guns need to fire more rapidly
if (wip->wi_flags[Weapon::Info_Flags::Flak]) {
wait *= aip->ai_ship_fire_delay_scale_friendly * 0.5f;
if (aip->ai_class_autoscale)
{
if (The_mission.ai_profile->flags[AI::Profile_Flags::Adjusted_AI_class_autoscale])
wait += (ai_get_autoscale_index(Num_ai_classes) - ai_get_autoscale_index(aip->ai_class) - 1) * 40.0f;
else
wait += (Num_ai_classes - aip->ai_class - 1) * 40.0f;
}
} else {
wait *= aip->ai_ship_fire_delay_scale_friendly;
if (aip->ai_class_autoscale)
{
if (The_mission.ai_profile->flags[AI::Profile_Flags::Adjusted_AI_class_autoscale])
wait += (ai_get_autoscale_index(Num_ai_classes) - ai_get_autoscale_index(aip->ai_class) - 1) * 100.0f;
else
wait += (Num_ai_classes - aip->ai_class - 1) * 100.0f;
}
}
} else {
// flak guns need to fire more rapidly
if (wip->wi_flags[Weapon::Info_Flags::Flak]) {
if (Player_ship != nullptr && Ships[aip->shipnum].team == Player_ship->team) {
wait *= aip->ai_ship_fire_delay_scale_friendly * 0.5f;
} else {
wait *= aip->ai_ship_fire_delay_scale_hostile * 0.5f;
}
if (aip->ai_class_autoscale)
{
if (The_mission.ai_profile->flags[AI::Profile_Flags::Adjusted_AI_class_autoscale])
wait += (ai_get_autoscale_index(Num_ai_classes) - ai_get_autoscale_index(aip->ai_class) - 1) * 40.0f;
else
wait += (Num_ai_classes - aip->ai_class - 1) * 40.0f;
}
} else if (wip->wi_flags[Weapon::Info_Flags::Huge]) {
// make huge weapons fire independently of team
wait *= aip->ai_ship_fire_delay_scale_friendly;
if (aip->ai_class_autoscale)
{
if (The_mission.ai_profile->flags[AI::Profile_Flags::Adjusted_AI_class_autoscale])
wait += (ai_get_autoscale_index(Num_ai_classes) - ai_get_autoscale_index(aip->ai_class) - 1) * 100.0f;
else
wait += (Num_ai_classes - aip->ai_class - 1) * 100.0f;
}
} else {
// give team friendly an advantage
if (Player_ship != nullptr && Ships[aip->shipnum].team == Player_ship->team) {
wait *= aip->ai_ship_fire_delay_scale_friendly;
} else {
wait *= aip->ai_ship_fire_delay_scale_hostile;
}
if (aip->ai_class_autoscale)
{
if (The_mission.ai_profile->flags[AI::Profile_Flags::Adjusted_AI_class_autoscale])
wait += (ai_get_autoscale_index(Num_ai_classes) - ai_get_autoscale_index(aip->ai_class) - 1) * 100.0f;
else
wait += (Num_ai_classes - aip->ai_class - 1) * 100.0f;
}
}
}
// vary wait time +/- 10%
wait *= frand_range(0.9f, 1.1f);
}
if(turret->rof_scaler != 1.0f && !(burst && turret->system_info->flags[Model::Subsystem_Flags::Burst_ignores_RoF_Mult]))
wait /= get_adjusted_turret_rof(turret);
(*fs_dest) = timestamp((int)wait);
}
/**
* Decide if a turret should launch an aspect seeking missile
*/
bool turret_should_fire_aspect(const ship_subsys *turret, const weapon_info *wip, bool in_sight)
{
if ( in_sight && (turret->turret_time_enemy_in_range >= MIN(wip->min_lock_time,AICODE_TURRET_MAX_TIME_IN_RANGE)) ) {
return true;
}
return false;
}
/**
* Update how long current target has been in this turrets range
*/
void turret_update_enemy_in_range(ship_subsys *turret, float seconds)
{
turret->turret_time_enemy_in_range += seconds;
if ( turret->turret_time_enemy_in_range < 0.0f ) {
turret->turret_time_enemy_in_range = 0.0f;
}
if ( turret->turret_time_enemy_in_range > AICODE_TURRET_MAX_TIME_IN_RANGE ) {
turret->turret_time_enemy_in_range = AICODE_TURRET_MAX_TIME_IN_RANGE;
}
}
/**
* Fire a weapon from a turret
*/
bool turret_fire_weapon(int weapon_num,
ship_subsys *turret,
int parent_objnum,
const WeaponLaunchCurveData& launch_curve_data,
const vec3d *orig_firing_pos,
const vec3d *orig_firing_vec,
const vec3d *predicted_pos = nullptr,
float flak_range_override = 100.0f,
bool play_sound = true)
{
matrix firing_orient;
int weapon_objnum;
ai_info *parent_aip;
ship *parent_ship;
float flak_range = 0.0f;
weapon *wp;
object *objp;
// make sure we actually have a valid weapon
auto wip = get_turret_weapon_wip(&turret->weapons, weapon_num);
if (!wip)
return false;
auto [turret_pbank, turret_sbank] = get_turret_weapon_bank(weapon_num);
bool in_lab = (gameseq_get_state() == GS_STATE_LAB);
if (in_lab) {
predicted_pos = nullptr;
}
bool last_shot_in_salvo = true;
if (turret->system_info->flags[Model::Subsystem_Flags::Turret_salvo])
{
if ((turret->turret_next_fire_pos + 1) == (turret->system_info->turret_num_firing_points))
{
last_shot_in_salvo = true;
}
else
{
last_shot_in_salvo = false;
}
}
//WMC - Limit firing to firestamp
if((!timestamp_elapsed(get_turret_weapon_next_fire_stamp(&turret->weapons, weapon_num))) && last_shot_in_salvo)
return false;
parent_aip = &Ai_info[Ships[Objects[parent_objnum].instance].ai_index];
parent_ship = &Ships[Objects[parent_objnum].instance];
int turret_weapon_class = WEAPON_INFO_INDEX(wip);
#ifndef NDEBUG
// moved here from check_ok_to_fire
if (turret->turret_enemy_objnum >= 0) {
object *tobjp = &Objects[turret->turret_enemy_objnum];
// should not get this far. check if ship is protected from beam and weapon is type beam
if ( (wip->wi_flags[Weapon::Info_Flags::Beam]) && (tobjp->flags[Object::Object_Flags::Beam_protected]) ) {
nprintf(("Warning","Ship %s is trying to fire beam turret at beam protected ship\n", parent_ship->ship_name));
return false;
}
// should not get this far. check if ship is protected from flak and weapon is type flak
else if ( (wip->wi_flags[Weapon::Info_Flags::Flak]) && (tobjp->flags[Object::Object_Flags::Flak_protected]) ) {
nprintf(("Warning","Ship %s is trying to fire flak turret at flak protected ship\n", parent_ship->ship_name));
return false;
}
// should not get this far. check if ship is protected from laser and weapon is type laser
else if ( (wip->subtype == WP_LASER) && (tobjp->flags[Object::Object_Flags::Laser_protected]) ) {
nprintf(("Warning","Ship %s is trying to fire laser turret at laser protected ship\n", parent_ship->ship_name));
return false;
}
// should not get this far. check if ship is protected from missile and weapon is type missile
else if ( (wip->subtype == WP_MISSILE) && (tobjp->flags[Object::Object_Flags::Missile_protected]) ) {
nprintf(("Warning","Ship %s is trying to fire missile turret at missile protected ship\n", parent_ship->ship_name));
return false;
}
}
#endif
if (check_ok_to_fire(parent_objnum, turret->turret_enemy_objnum, wip, -1, orig_firing_pos)) {
auto turret_enemy_objp = (turret->turret_enemy_objnum >= 0) ? &Objects[turret->turret_enemy_objnum] : nullptr;
ship_weapon* swp = &turret->weapons;
turret->turret_last_fire_direction = *orig_firing_vec;
const vec3d *firing_vec = orig_firing_vec;
vec3d firing_vec_buf;
if (turret->turret_inaccuracy > 0.0f) {
vm_vec_random_cone(&firing_vec_buf, firing_vec, turret->turret_inaccuracy);
firing_vec = &firing_vec_buf;
}
vm_vector_2_matrix_norm(&firing_orient, firing_vec, nullptr, nullptr);
// grab and set some burst data before turret_set_next_fire_timestamp wipes it
int old_burst_seed = swp->burst_seed[weapon_num];
int old_burst_counter = swp->burst_counter[weapon_num];
// only used by type 5 beams
if (turret->weapons.burst_counter[weapon_num] == 0) {
swp->per_burst_rot += wip->b_info.t5info.per_burst_rot;
if (swp->per_burst_rot > PI2)
swp->per_burst_rot -= PI2;
else if (swp->per_burst_rot < -PI2)
swp->per_burst_rot += PI2;
}
// set next fire timestamp for the turret
if (last_shot_in_salvo)
turret_set_next_fire_timestamp(weapon_num, wip, turret, parent_aip, launch_curve_data);
// if this weapon is a beam weapon, handle it specially
if (wip->wi_flags[Weapon::Info_Flags::Beam]) {
// if this beam isn't free to fire
if (!in_lab && !(turret->weapons.flags[Ship::Weapon_Flags::Beam_Free])) {
return false;
}
for (int i = 0; i < wip->shots; i++) {
beam_fire_info fire_info;
// stuff beam firing info
memset(&fire_info, 0, sizeof(beam_fire_info));
fire_info.accuracy = 1.0f;
fire_info.beam_info_index = turret_weapon_class;
fire_info.beam_info_override = nullptr;
fire_info.shooter = &Objects[parent_objnum];
fire_info.target = turret_enemy_objp;
if (wip->wi_flags[Weapon::Info_Flags::Antisubsysbeam])
fire_info.target_subsys = turret->targeted_subsys;
else
fire_info.target_subsys = nullptr;
fire_info.turret = turret;
fire_info.bank = turret_pbank;
fire_info.burst_seed = old_burst_seed;
fire_info.fire_method = BFM_TURRET_FIRED;
fire_info.per_burst_rotation = swp->per_burst_rot;
fire_info.burst_index = (old_burst_counter * wip->shots) + i;
// If we're in the lab then force fire
if (in_lab) {
fire_info.fire_method = BFM_TURRET_FORCE_FIRED;
fire_info.bfi_flags |= BFIF_TARGETING_COORDS;
fire_info.target_pos1 = turret->last_aim_enemy_pos;
}
// fire a beam weapon
weapon_objnum = beam_fire(&fire_info);
if (weapon_objnum != -1) {
objp = &Objects[weapon_objnum];
parent_ship->last_fired_turret = turret;
turret->last_fired_weapon_info_index = turret_weapon_class;
turret->turret_last_fired = _timestamp();
if (scripting::hooks::OnTurretFired->isActive()) {
scripting::hooks::OnTurretFired->run(scripting::hooks::WeaponUsedConditions{ parent_ship , turret_enemy_objp, SCP_vector<int>{ turret_weapon_class }, true },
scripting::hook_param_list(
scripting::hook_param("Ship", 'o', &Objects[parent_objnum]),
scripting::hook_param("Beam", 'o', objp),
scripting::hook_param("Target", 'o', turret_enemy_objp)
));
}
}
}
turret->flags.set(Ship::Subsystem_Flags::Has_fired); //set fired flag for scripting -nike
return true;
}
// don't fire swam, but set up swarm info instead
else if ((wip->wi_flags[Weapon::Info_Flags::Swarm]) || (wip->wi_flags[Weapon::Info_Flags::Corkscrew])) {
// swarm and corkscrew are not restricted to missiles, so check whichever bank is actually firing
if (!in_lab && (turret->system_info->flags[Model::Subsystem_Flags::Turret_use_ammo])) {
bool has_ammo = (turret_pbank >= 0)
? swp->primary_bank_ammo[turret_pbank] > 0
: ship_secondary_has_ammo(swp, turret_sbank);
if (!has_ammo)
return false;
}
turret_swarm_set_up_info(parent_objnum, turret, wip, turret->turret_next_fire_pos, turret_pbank, turret_sbank, in_lab);
turret->flags.set(Ship::Subsystem_Flags::Has_fired); //set fired flag for scripting -nike
return true;
}
// now do anything else
else {
float shots_mult = wip->weapon_launch_curves.get_output(weapon_info::WeaponLaunchCurveOutputs::SHOTS_MULT, launch_curve_data);
int shots = fl2i(i2fl(wip->shots) * shots_mult);
// note that wip is reassigned to the created weapon's class inside the loop, so any
// property of the bank's own weapon must be read before we get there
bool no_ammo_needed = (turret_sbank >= 0) && Weapon_info[swp->secondary_bank_weapons[turret_sbank]].wi_flags[Weapon::Info_Flags::SecondaryNoAmmo];
for (int i = 0; i < shots; i++) {
if (!in_lab && turret->system_info->flags[Model::Subsystem_Flags::Turret_use_ammo]) {
if (turret_pbank >= 0) {
int points;
if (turret->system_info->flags[Model::Subsystem_Flags::Turret_salvo]) {
points = turret->system_info->turret_num_firing_points;
} else {
points = 1;
}
if (swp->primary_bank_ammo[turret_pbank] >= points) {
swp->primary_bank_ammo[turret_pbank] -= points;
} else if ((swp->primary_bank_ammo[turret_pbank] >= 0) && !(The_mission.ai_profile->flags[AI::Profile_Flags::Prevent_negative_turret_ammo])) {
// default behavior allowed ammo to be negative
swp->primary_bank_ammo[turret_pbank] -= points;
} else {
return false;
}
}
else if (turret_sbank >= 0) {
if (!no_ammo_needed) {
if (swp->secondary_bank_ammo[turret_sbank] > 0) {
swp->secondary_bank_ammo[turret_sbank]--;
} else if ((swp->secondary_bank_ammo[turret_sbank] == 0) && !(The_mission.ai_profile->flags[AI::Profile_Flags::Prevent_negative_turret_ammo])) {
// default behavior allowed ammo to be negative
swp->secondary_bank_ammo[turret_sbank]--;
} else {
return false;
}
}
}
}
vec3d firing_pos_buf;
const vec3d *firing_pos = &firing_pos_buf;
// zookeeper - Firepoints should cycle normally between shots,
// so we need to get the position info separately for each shot
ship_get_global_turret_gun_info(&Objects[parent_objnum], turret, &firing_pos_buf, false, nullptr, true, nullptr);
weapon_objnum = weapon_create(firing_pos, &firing_orient, turret_weapon_class, parent_objnum, -1, true, false, swp, turret_pbank, turret_sbank, launch_curve_data);
weapon_set_tracking_info(weapon_objnum, parent_objnum, turret->turret_enemy_objnum, 1, turret->targeted_subsys);
//nprintf(("AI", "Turret_time_enemy_in_range = %7.3f\n", ss->turret_time_enemy_in_range));
if (weapon_objnum != -1) {
objp = &Objects[weapon_objnum];
wp = &Weapons[objp->instance];
wip = &Weapon_info[wp->weapon_info_index];
parent_ship->last_fired_turret = turret;
turret->last_fired_weapon_info_index = wp->weapon_info_index;
turret->turret_last_fired = _timestamp();
wp->target_num = turret->turret_enemy_objnum;
// AL 1-6-97: Store pointer to turret subsystem
wp->turret_subsys = turret;
if (scripting::hooks::OnTurretFired->isActive()) {
scripting::hooks::OnTurretFired->run(scripting::hooks::WeaponUsedConditions{ parent_ship , turret_enemy_objp, SCP_vector<int>{ turret_weapon_class }, wip->subtype == WP_LASER },
scripting::hook_param_list(
scripting::hook_param("Ship", 'o', &Objects[parent_objnum]),
scripting::hook_param("Weapon", 'o', objp),
scripting::hook_param("Target", 'o', turret_enemy_objp)
));
}
// if the gun is a flak gun
if (wip->wi_flags[Weapon::Info_Flags::Flak]) {
if(predicted_pos != nullptr)
{
// pick a firing range so that it detonates properly
flak_pick_range(objp, firing_pos, predicted_pos, ship_get_subsystem_strength(parent_ship, SUBSYSTEM_WEAPONS));
// determine what that range was
flak_range = flak_get_range(objp);
}
else
{
flak_set_range(objp, flak_range_override);
}
}
// do mflash if the weapon has it
if (wip->muzzle_effect.isValid()) {
float radius_mult = 1.f;
if (wip->render_type == WRT_LASER) {
radius_mult = wip->weapon_curves.get_output(weapon_info::WeaponCurveOutputs::LASER_RADIUS_MULT, *wp, &wp->modular_curves_instance);
}
//spawn particle effect
auto particleSource = particle::ParticleManager::get()->createSource(wip->muzzle_effect);
particleSource->setHost(std::make_unique<EffectHostTurret>(&Objects[parent_ship->objnum], turret->system_info->turret_gun_sobj, turret->turret_next_fire_pos, false));
particleSource->setTriggerRadius(objp->radius * radius_mult);
particleSource->setTriggerVelocity(vm_vec_mag_quick(&objp->phys_info.vel));
particleSource->finishCreation();
}
// in multiplayer (and the master), then send a turret fired packet.
if ( MULTIPLAYER_MASTER && (weapon_objnum != -1) ) {
int subsys_index;
subsys_index = ship_get_subsys_index(turret);
Assert( subsys_index != -1 );
if(wip->wi_flags[Weapon::Info_Flags::Flak]){
send_flak_fired_packet( parent_objnum, subsys_index, weapon_objnum, flak_range, launch_curve_data.distance_to_target, launch_curve_data.target_radius, launch_curve_data.target_forward_speed );
} else {
send_turret_fired_packet( parent_objnum, subsys_index, weapon_objnum, launch_curve_data.distance_to_target, launch_curve_data.target_radius, launch_curve_data.target_forward_speed );
}
}
if ( play_sound && wip->launch_snd.isValid() ) {
// Don't play turret firing sound if turret sits on player ship... it gets annoying.
if ( parent_objnum != OBJ_INDEX(Player_obj) || (turret->flags[Ship::Subsystem_Flags::Play_sound_for_player]) ) {
snd_play_3d( gamesnd_get_game_sound(wip->launch_snd), firing_pos, &View_position );
}
}
}
turret->turret_next_fire_pos++;
}
turret->turret_next_fire_pos--;
// reset any animations if we need to
// (I'm not sure how accurate this timestamp would be in practice - taylor)
if (turret->turret_animation_position == MA_POS_READY)
turret->turret_animation_done_time = timestamp(100);
}
}
//Not useful -WMC
else if (!(parent_aip->ai_profile_flags[AI::Profile_Flags::Dont_insert_random_turret_fire_delay]) && last_shot_in_salvo)
{
float wait = 1000.0f * frand_range(0.9f, 1.1f);
turret->turret_next_fire_stamp = timestamp((int) wait);
}
turret->flags.set(Ship::Subsystem_Flags::Has_fired); //set has fired flag for scriptng - nuke
//Fire animation stuff
Ship_info[parent_ship->ship_info_index].animations.get(model_get_instance(parent_ship->model_instance_num), animation::ModelAnimationTriggerType::TurretFired, animation::anim_name_from_subsys(turret->system_info))
.start(animation::ModelAnimationDirection::FWD, true);
return true;
}
//void turret_swarm_fire_from_turret(ship_subsys *turret, int parent_objnum, int target_objnum, ship_subsys *target_subsys)
void turret_swarm_fire_from_turret(turret_swarm_info *tsi)
{
int weapon_objnum;
matrix firing_orient;
vec3d turret_pos, turret_fvec;
// parent not alive, quick out.
if (Objects[tsi->parent_objnum].type != OBJ_SHIP) {
return;
}
// if fixed fp make sure to use constant fp
if (tsi->turret->system_info->flags[Model::Subsystem_Flags::Turret_fixed_fp])
tsi->turret->turret_next_fire_pos = tsi->weapon_num;
// change firing point
ship_get_global_turret_gun_info(&Objects[tsi->parent_objnum], tsi->turret, &turret_pos, false, &turret_fvec, true, nullptr);
tsi->turret->turret_next_fire_pos++;
//check if this really is a swarm. If not, how the hell did it get here?
Assert((Weapon_info[tsi->weapon_class].wi_flags[Weapon::Info_Flags::Swarm]) || (Weapon_info[tsi->weapon_class].wi_flags[Weapon::Info_Flags::Corkscrew]));
// *If it's a non-homer, then use the last fire direction instead of turret orientation to fix inaccuracy
// problems with non-homing swarm weapons -Et1
if ( (Weapon_info[tsi->weapon_class].subtype == WP_LASER) || ((The_mission.ai_profile->flags[AI::Profile_Flags::Hack_improve_non_homing_swarm_turret_fire_accuracy])
&& !(Weapon_info[tsi->weapon_class].is_homing())) )
{
turret_fvec = tsi->turret->turret_last_fire_direction;
}
// make firing_orient from turret_fvec -- turret->turret_last_fire_direction
vm_vector_2_matrix_norm(&firing_orient, &turret_fvec, nullptr, nullptr);
auto launch_curve_data = WeaponLaunchCurveData {
tsi->turret->system_info->turret_num_firing_points,
0.f,
0.f,
0.f,
};
// create weapon and homing info
weapon_objnum = weapon_create(&turret_pos, &firing_orient, tsi->weapon_class, tsi->parent_objnum, -1, true, false, &tsi->turret->weapons, tsi->swp_pbank, tsi->swp_sbank, launch_curve_data);
weapon_set_tracking_info(weapon_objnum, tsi->parent_objnum, tsi->target_objnum, 1, tsi->target_subsys);
// do other cool stuff if weapon is created.
if (weapon_objnum > -1) {
Weapons[Objects[weapon_objnum].instance].turret_subsys = tsi->turret;
Weapons[Objects[weapon_objnum].instance].target_num = tsi->turret->turret_enemy_objnum;
Ships[Objects[tsi->parent_objnum].instance].last_fired_turret = tsi->turret;
tsi->turret->last_fired_weapon_info_index = tsi->weapon_class;
tsi->turret->turret_last_fired = _timestamp();
if (scripting::hooks::OnTurretFired->isActive()) {
scripting::hooks::OnTurretFired->run(scripting::hooks::WeaponUsedConditions{
&Ships[Objects[tsi->parent_objnum].instance],
&Objects[tsi->turret->turret_enemy_objnum],
SCP_vector<int>{ tsi->weapon_class }, Weapon_info[tsi->weapon_class].subtype == WP_LASER
},
scripting::hook_param_list(
scripting::hook_param("Ship", 'o', &Objects[tsi->parent_objnum]),
scripting::hook_param("Weapon", 'o', &Objects[weapon_objnum]),
scripting::hook_param("Target", 'o', &Objects[tsi->turret->turret_enemy_objnum])
));
}
// muzzle flash?
if (Weapon_info[tsi->weapon_class].muzzle_effect.isValid()) {
//spawn particle effect
auto particleSource = particle::ParticleManager::get()->createSource(Weapon_info[tsi->weapon_class].muzzle_effect);
particleSource->setHost(std::make_unique<EffectHostTurret>(&Objects[tsi->parent_objnum], tsi->turret->system_info->turret_gun_sobj, tsi->turret->turret_next_fire_pos - 1, false));
particleSource->setTriggerRadius(Objects[weapon_objnum].radius);
particleSource->finishCreation();
}
// maybe sound
if ( Weapon_info[tsi->weapon_class].launch_snd.isValid() ) {
// Don't play turret firing sound if turret sits on player ship... it gets annoying.
if ( tsi->parent_objnum != OBJ_INDEX(Player_obj) || (tsi->turret->flags[Ship::Subsystem_Flags::Play_sound_for_player]) ) {
snd_play_3d( gamesnd_get_game_sound(Weapon_info[tsi->weapon_class].launch_snd), &turret_pos, &View_position );
}
}
// in multiplayer (and the master), then send a turret fired packet.
if ( MULTIPLAYER_MASTER && (weapon_objnum != -1) ) {
int subsys_index;
subsys_index = ship_get_subsys_index(tsi->turret);
Assert( subsys_index != -1 );
send_turret_fired_packet( tsi->parent_objnum, subsys_index, weapon_objnum, 0.f, 0.f, 0.f);
}
}
}
int Num_ai_firing = 0;
int Num_find_turret_enemy = 0;
int Num_turrets_fired = 0;
/**
* Previously called ai_fire_from_turret()
* Given a ship and a turret subsystem, handle all its behavior, mostly targeting and shooting but also
* all turret movement and resetting when idle
*/
extern int Nebula_sec_range;
void ai_turret_execute_behavior(const ship *shipp, ship_subsys *ss)
{
float weapon_firing_range;
float weapon_min_range; // *Weapon minimum firing range -Et1
vec3d v2e;
object *lep; // Last enemy pointer
model_subsystem *tp = ss->system_info;
ship_weapon *swp = &ss->weapons;
vec3d predicted_enemy_pos = vmd_zero_vector;
object *objp;
bool in_lab = (gameseq_get_state() == GS_STATE_LAB);
if (in_lab) {
// Adjust behavior for Lab
ss->turret_enemy_objnum = -1;
lep = nullptr;
}
// Reset the points to target value
ss->points_to_target = -1.0f;
ss->base_rotation_rate_pct = 0.0f;
ss->gun_rotation_rate_pct = 0.0f;
if (!in_lab && !Ai_firing_enabled) {
return;
}
if (ss->current_hits <= 0.0f) {
return;
}
if ( ship_subsys_disrupted(ss) ){ // AL 1/19/98: Make sure turret isn't suffering disruption effects
return;
}
// Check turret free
if (!in_lab && ss->weapons.flags[Ship::Weapon_Flags::Turret_Lock]) {
return;
}
// check if there is any available weapon to fire
int weap_check;
bool valid_weap = false;
for (weap_check = 0; (weap_check < swp->num_primary_banks) && !valid_weap; weap_check++) {
if (swp->primary_bank_weapons[weap_check] >= 0)
valid_weap = true;
}
if (!valid_weap) {
for (weap_check = 0; (weap_check < ss->weapons.num_secondary_banks) && !valid_weap; weap_check++) {
if (ss->weapons.secondary_bank_weapons[weap_check] >= 0)
valid_weap = true;
}
if (!valid_weap)
return;
}
// Monitor number of calls to ai_fire_from_turret
Num_ai_firing++;
// Handle turret animation
if (ss->turret_animation_position == MA_POS_SET) {
if ( timestamp_elapsed(ss->turret_animation_done_time) ) {
ss->turret_animation_position = MA_POS_READY;
// setup a reversal (closing) timestamp at 1 second
// (NOTE: this will probably get changed by other parts of the code (swarming, beams, etc) to be
// a more accurate time, but we need to give it a long enough time for the other parts of the code
// to change the timestamp before it gets acted upon - taylor)
ss->turret_animation_done_time = timestamp(200);
} else {
return;
}
}
// AL 09/14/97: ensure ss->turret_enemy_objnum != -1 before setting lep
if ( (ss->turret_enemy_objnum >= 0 && ss->turret_enemy_objnum < MAX_OBJECTS) && (ss->turret_enemy_sig == Objects[ss->turret_enemy_objnum].signature) )
{
lep = &Objects[ss->turret_enemy_objnum];
}
else
{
ss->turret_enemy_objnum = -1;
lep = nullptr;
ss->flags.remove(Ship::Subsystem_Flags::Forced_target);
ss->flags.remove(Ship::Subsystem_Flags::Forced_subsys_target);
}
Assert((shipp->objnum >= 0) && (shipp->objnum < MAX_OBJECTS));
int parent_objnum = shipp->objnum;
objp = &Objects[shipp->objnum];
Assert(objp->type == OBJ_SHIP);
vec3d global_gun_pos, global_gun_vec;
if (tp->flags[Model::Subsystem_Flags::Turret_distant_firepoint] || Always_use_distant_firepoints) {
//The firing point of this turret is so far away from the its center that we should consider this for firing calculations
//This will do the enemy position prediction based on their relative position and speed to the firing point, not the turret center.
ship_get_global_turret_gun_info(objp, ss, &global_gun_pos, false, &global_gun_vec, true, nullptr);
} else {
// Use the turret info for all guns, not one gun in particular.
ship_get_global_turret_info(objp, tp, &global_gun_pos, &global_gun_vec);
}
if (!in_lab) {
// Update predicted enemy position. This used to be done in aifft_rotate_turret
aifft_update_predicted_enemy_pos(objp, shipp, ss, &global_gun_pos, &global_gun_vec, lep, &predicted_enemy_pos);
} else {
// Lab hijacks this value to use as the target position
predicted_enemy_pos = ss->last_aim_enemy_pos;
}
// Rotate the turret even if time hasn't elapsed, since it needs to turn to face its target.
bool use_angles = aifft_rotate_turret(objp, shipp, ss, &global_gun_pos, &global_gun_vec, &predicted_enemy_pos);
// Multiplayer clients are now able to try to track their targets and also reset to their idle positions.
// But everything after this point is turret firing, so we need to bail here.
if (MULTIPLAYER_CLIENT) {
return;
}
if ((tp->flags[Model::Subsystem_Flags::Fire_on_target]) && (ss->points_to_target >= 0.0f))
{
// value probably needs tweaking... could perhaps be made into table option?
if (ss->points_to_target > 0.010f)
return;
}
if ( !timestamp_elapsed(ss->turret_next_fire_stamp) ) {
return;
}
// Don't try to fire beyond weapon_limit_range
// WMC moved the range check to within the loop, but we can still calculate the enemy distance here
float base_dist_to_enemy = 0.0f;
float dist_to_enemy = 0.0f;
if (lep) {
base_dist_to_enemy = vm_vec_normalized_dir(&v2e, &predicted_enemy_pos, &global_gun_pos);
dist_to_enemy = base_dist_to_enemy;
if (!The_mission.ai_profile->flags[AI::Profile_Flags::Turrets_ignore_target_radius]) {
dist_to_enemy -= lep->radius;
}
base_dist_to_enemy = MAX(0.0f, base_dist_to_enemy);
dist_to_enemy = MAX(0.0f, dist_to_enemy);
} else if (in_lab) {
base_dist_to_enemy = 500.0f;
dist_to_enemy = base_dist_to_enemy;
vm_vec_normalized_dir(&v2e, &predicted_enemy_pos, &global_gun_pos);
}
// count the number of enemies, in case we have a spawning weapon
int num_ships_nearby = num_nearby_fighters(iff_get_attackee_mask(obj_team(objp)), &global_gun_pos, 1500.0f);
// some flags considering there may be different weapon types on this turret
bool we_did_non_spawning_logic = false;
bool tentative_return = false;
float turret_barrel_length = -1.0f;
float target_radius = 0.f;
float target_forward_speed = 0.f;
if (lep != nullptr) {
target_radius = lep->radius;
target_forward_speed = lep->phys_info.fspeed;
}
// grab the data for the launch curve inputs
auto launch_curve_data = WeaponLaunchCurveData {
ss->system_info->turret_num_firing_points,
base_dist_to_enemy,
target_radius,
target_forward_speed,
};
//WMC - go through all valid weapons. Fire spawns if there are any.
int valid_weapons[MAX_SHIP_WEAPONS];
int num_valid = 0;
for (int i = 0; i < MAX_SHIP_WEAPONS; ++i)
{
//WMC - Only fire more than once if we have multiple guns flag set.
if (num_valid > 0 && !(tp->flags[Model::Subsystem_Flags::Use_multiple_guns]))
break;
// figure out which weapon we are firing, if we even have a valid one
weapon_info *wip = nullptr;
if (i < MAX_SHIP_PRIMARY_BANKS)
{
if (i < swp->num_primary_banks && timestamp_elapsed(swp->next_primary_fire_stamp[i]))
wip = &Weapon_info[swp->primary_bank_weapons[i]];
}
else
{
int j = i - MAX_SHIP_PRIMARY_BANKS;
if (j < swp->num_secondary_banks && timestamp_elapsed(swp->next_secondary_fire_stamp[j]))
wip = &Weapon_info[swp->secondary_bank_weapons[j]];
}
if (!wip)
continue;
// Turret spawn weapons are a special case. They fire if there are enough enemies in the
// immediate area (not necessarily in the turret fov).
// However, if this is a 'smart spawn', just use it like a normal weapon
if (!in_lab && (wip->wi_flags[Weapon::Info_Flags::Spawn]) && !(wip->wi_flags[Weapon::Info_Flags::Smart_spawn]) )
{
if (( num_ships_nearby >= 3 ) || ((num_ships_nearby >= 2) && (frand() < 0.1f))) {
turret_fire_weapon(i, ss, parent_objnum, launch_curve_data, &global_gun_pos, &ss->turret_last_fire_direction);
} else {
// Regardless of firing rate, don't check whether should fire for awhile.
if (tp->flags[Model::Subsystem_Flags::Use_multiple_guns])
set_turret_weapon_next_fire_stamp(swp, i, 1000);
else
ss->turret_next_fire_stamp = timestamp(1000);
}
//we're done with this weapon mount
continue;
}
we_did_non_spawning_logic = true; // for the post-loop check
// If beam weapon, check beam free
if (!in_lab && wip->wi_flags[Weapon::Info_Flags::Beam] && !(ss->weapons.flags[Ship::Weapon_Flags::Beam_Free])) {
tentative_return = true;
continue;
}
// the remaining checks in this loop all require a valid lep
if (!in_lab && !lep)
continue;
// Get weapon range
if (wip->wi_flags[Weapon::Info_Flags::Local_ssm])
{
weapon_firing_range = wip->lssm_lock_range;
weapon_min_range = 0.0f;
}
else
{
weapon_firing_range = MIN(wip->lifetime * wip->max_speed, wip->weapon_range);
weapon_min_range = wip->weapon_min_range;
}
// if beam weapon in nebula and target not tagged, decrease firing range
if (wip->wi_flags[Weapon::Info_Flags::Beam]) {
if ( !((shipp->tag_left > 0) || (shipp->level2_tag_left > 0)) ) {
if (Nebula_sec_range) {
weapon_firing_range *= BEAM_NEBULA_RANGE_REDUCE_FACTOR;
}
}
}
// If the barrel of this turret is long, we shouldn't try to fire if the enemy is under the gun.
// Note that this isn't the same thing as min_range, which is unrelated to barrel length.
if (tp->flags[Model::Subsystem_Flags::Turret_distant_firepoint] || Always_use_distant_firepoints) {
if (turret_barrel_length < 0.0f) {
if (tp->turret_gun_sobj >= 0) {
auto pm = model_get(tp->model_num);
auto submodel = &pm->submodel[tp->turret_gun_sobj];
turret_barrel_length = 2 * submodel->rad;
} else {
turret_barrel_length = 0.0f;
}
}
if (dist_to_enemy < turret_barrel_length) continue;
}
// Don't try to fire beyond weapon_limit_range (or within min range)
// In lab we ignore range issues
if (!in_lab && (dist_to_enemy < weapon_min_range || dist_to_enemy > weapon_firing_range)) {
// it's possible another weapon is in range, but if not,
// we will end up selecting a new target
continue;
}
if (!in_lab) {
ship_info* esip = lep->type != OBJ_SHIP ? nullptr : &Ship_info[Ships[lep->instance].ship_info_index];
// If targeted a small ship and have a huge weapon, don't fire. But this shouldn't happen, as a small ship should not get selected.
if ( wip->wi_flags[Weapon::Info_Flags::Huge] ) {
if ( lep->type != OBJ_SHIP ) {
tentative_return = true;
continue;
}
if ( esip->class_type >= 0 && !(Ship_types[esip->class_type].flags[Ship::Type_Info_Flags::Targeted_by_huge_Ignored_by_small_only])) {
tentative_return = true;
continue;
}
}
// Similar check for small weapons
if ( wip->wi_flags[Weapon::Info_Flags::Small_only] ) {
if ( (lep->type == OBJ_SHIP) && esip->class_type >= 0 &&
(Ship_types[esip->class_type].flags[Ship::Type_Info_Flags::Targeted_by_huge_Ignored_by_small_only])) {
tentative_return = true;
continue;
}
}
bool tagged_only = ((wip->wi_flags[Weapon::Info_Flags::Tagged_only]) || (ss->weapons.flags[Ship::Weapon_Flags::Tagged_Only]));
// If targeting protected or beam protected ship, don't fire. Reset enemy objnum
// (note: due to the loop, we need to check all weapons on this turret. if none of them are valid, that's when the enemy objnum is reset)
if (lep->type == OBJ_SHIP) {
// Check if we're targeting a protected ship
if (lep->flags[Object::Object_Flags::Protected]) {
// *none* of the weapons will work on this ship, so just quit the loop
tentative_return = true;
break;
}
// Check if we're targeting a beam protected ship with a beam weapon
if ( (lep->flags[Object::Object_Flags::Beam_protected]) && (wip->wi_flags[Weapon::Info_Flags::Beam]) ) {
tentative_return = true;
continue;
}
// Check if we're targeting a flak protected ship with a flak weapon
else if ( (lep->flags[Object::Object_Flags::Flak_protected]) && (wip->wi_flags[Weapon::Info_Flags::Flak]) ) {
tentative_return = true;
continue;
}
// Check if we're targeting a laser protected ship with a laser weapon
else if ( (lep->flags[Object::Object_Flags::Laser_protected]) && (wip->subtype == WP_LASER) ) {
tentative_return = true;
continue;
}
// Check if we're targeting a missile protected ship with a missile weapon
else if ( (lep->flags[Object::Object_Flags::Missile_protected]) && (wip->subtype == WP_MISSILE) ) {
tentative_return = true;
continue;
}
// Check if weapon or turret is set to tagged-only
// must check here in case turret has multiple weapons and not all are tagged-only
else if (tagged_only && !ship_is_tagged(lep)) {
tentative_return = true;
continue;
}
if (!weapon_target_satisfies_lock_restrictions(wip, lep)) {
tentative_return = true;
continue;
}
}
else {
// check tagged-only for bombs only if the flag is set; see Mantis #3114
if (tagged_only && ((lep->type != OBJ_WEAPON) || The_mission.ai_profile->flags[AI::Profile_Flags::Strict_turret_tagged_only_targeting])) {
tentative_return = true;
continue;
}
}
}
// If we made it this far, we have a valid weapon to fire
valid_weapons[num_valid++] = i;
}
// in the original code, spawning weapons immediately bailed after firing,
// so bail unless we have other weapons that need the rest of the function
if (!we_did_non_spawning_logic)
return;
//none of our guns can hit the enemy, so find a new enemy
if (num_valid == 0) {
if (!ss->flags[Ship::Subsystem_Flags::Forced_target]) {
ss->turret_enemy_objnum = -1;
ss->turret_time_enemy_in_range = 0.0f;
}
// in the original code, we returned where the "tentative return" variable was set,
// so return for real now, since we don't have a reason to remain in the function
if (tentative_return)
return;
}
// if we've got Forced_subsys_target targeted_subsys *really* shouldnt be null but just in case...
if (ss->flags[Ship::Subsystem_Flags::Forced_subsys_target] && ss->targeted_subsys != nullptr) {
if (ss->targeted_subsys->current_hits <= 0.0f) {
ss->flags.remove(Ship::Subsystem_Flags::Forced_subsys_target);
ss->flags.remove(Ship::Subsystem_Flags::Forced_target);
}
}
// Maybe pick a new enemy, unless targeting has been taken over by scripting
if ( turret_should_pick_new_target(ss) && !ss->scripting_target_override && !(ss->flags[Ship::Subsystem_Flags::Forced_target])) {
Num_find_turret_enemy++;
int objnum = find_turret_enemy(ss, parent_objnum, &global_gun_pos, &global_gun_vec, ss->turret_enemy_objnum);
if (objnum >= 0) {
if (ss->turret_enemy_objnum == -1) {
ss->turret_enemy_objnum = objnum;
ss->targeted_subsys = NULL; // Turret has retargeted; reset subsystem - Valathil for Mantis #2652
ss->turret_enemy_sig = Objects[objnum].signature;
// why return?
return;
} else {
ss->turret_enemy_objnum = objnum;
ss->targeted_subsys = NULL; // Turret has retargeted; reset subsystem - Valathil for Mantis #2652
ss->turret_enemy_sig = Objects[objnum].signature;
}
} else {
ss->turret_enemy_objnum = -1;
}
if (ss->turret_enemy_objnum != -1) {
float dot = 1.0f;
lep = &Objects[ss->turret_enemy_objnum];
if (( lep->type == OBJ_SHIP ) && !(ss->flags[Ship::Subsystem_Flags::No_SS_targeting])) {
ss->targeted_subsys = aifft_find_turret_subsys(objp, ss, &global_gun_pos, lep, &dot);
}
// recheck in 2-3 seconds
ss->turret_next_enemy_check_stamp = timestamp((int)((MAX(dot, 0.5f) * The_mission.ai_profile->turret_target_recheck_time) + (The_mission.ai_profile->turret_target_recheck_time / 2.0f)));
} else {
ss->turret_next_enemy_check_stamp = timestamp((int)(The_mission.ai_profile->turret_target_recheck_time * frand_range(0.9f, 1.1f))); // Check every two seconds
}
}
// If still don't have an enemy, return. Or, if enemy is protected, return.
if (ss->turret_enemy_objnum != -1) {
// Don't shoot at ship we're docked with.
if (dock_check_find_docked_object(objp, &Objects[ss->turret_enemy_objnum]))
{
ss->turret_enemy_objnum = -1;
return;
}
// Goober5000 - Also, don't shoot at a ship we're docking with. Volition
// had this in the code originally but messed it up so that it didn't work.
ai_info *aip = &Ai_info[shipp->ai_index];
if ((aip->mode == AIM_DOCK) && (aip->goal_objnum == ss->turret_enemy_objnum))
{
ss->turret_enemy_objnum = -1;
return;
}
if (Objects[ss->turret_enemy_objnum].flags[Object::Object_Flags::Protected]) {
// This can happen if the enemy was selected before it became protected.
ss->turret_enemy_objnum = -1;
return;
}
lep = &Objects[ss->turret_enemy_objnum];
} else {
if (!in_lab) {
if (timestamp_until(ss->turret_next_fire_stamp) < 500) {
ss->turret_next_fire_stamp = timestamp(500);
}
return;
}
}
//This can't happen. See above code -- Except in the Lab as of 4/26/2025
//if ( lep == nullptr ){
// return;
//}
//This can't happen. See above code -- Except in the Lab as of 4/26/2025
//Assert(ss->turret_enemy_objnum != -1);
bool in_fov = turret_fov_test(ss, &global_gun_vec, &v2e);
bool something_was_ok_to_fire = false;
// the only reason predicted_enemy_pos might be zero, while everything else is valid is if
// its a ballistic trajectory with no valid path
if (in_lab || (in_fov && num_valid && predicted_enemy_pos != vmd_zero_vector)) {
// Do salvo thing separately - to prevent messing up things
int number_of_firings;
if (tp->flags[Model::Subsystem_Flags::Turret_salvo])
{
number_of_firings = tp->turret_num_firing_points;
ss->turret_next_fire_pos = 0;
}
else
{
number_of_firings = num_valid;
}
auto sound_played = gamesnd_id();
for (int i = 0; i < number_of_firings; ++i)
{
int valid_index;
if (tp->flags[Model::Subsystem_Flags::Turret_salvo])
valid_index = 0;
else
valid_index = i;
auto wip = get_turret_weapon_wip(&ss->weapons, valid_weapons[valid_index]);
bool play_sound = false;
if (sound_played != wip->launch_snd) {
sound_played = wip->launch_snd;
play_sound = true;
}
// Forces the firing turret to retain the firingpoints of their weapons:
// if the turret has multiple banks, the first bank will only fire from the first firepoint,
// the second bank only from the second firepoint, and so on.
if (tp->flags[Model::Subsystem_Flags::Turret_fixed_fp])
{
int ffp_bank = valid_weapons[valid_index];
if (ffp_bank < MAX_SHIP_PRIMARY_BANKS)
ss->turret_next_fire_pos = ffp_bank;
else
ss->turret_next_fire_pos = ffp_bank - MAX_SHIP_PRIMARY_BANKS + ss->weapons.num_primary_banks;
}
// Ok, the turret is lined up... now line up a particular gun.
bool ok_to_fire = false;
// We're ready to fire... now get down to specifics, like where is the
// actual gun point and normal, not just the one for whole turret.
ship_get_global_turret_gun_info(objp, ss, &global_gun_pos, false, &global_gun_vec, use_angles, &predicted_enemy_pos);
// Fire in the direction the turret is facing, not right at the target regardless of turret dir.
// [Yet this retail comment precedes the calculation of vector-to-enemy...]
dist_to_enemy = vm_vec_normalized_dir(&v2e, &predicted_enemy_pos, &global_gun_pos);
float dot = vm_vec_dot(&v2e, &global_gun_vec);
// (flak jitter moved to after we obtain shoot_vector below)
// Fire if:
// dumbfire and nearly pointing at target.
// heat seeking and target in a fairly wide cone.
// aspect seeking and target is locked.
bool in_sight = false;
if (The_mission.ai_profile->flags[AI::Profile_Flags::Use_only_single_fov_for_turrets]) {
// we have already passed the FOV test of the turret so...
in_sight = true;
} else {
if (wip->wi_flags[Weapon::Info_Flags::Homing_heat]) {
if (dot > AICODE_TURRET_HEATSEEK_ANGLE) {
in_sight = true;
}
} else {
if (dot > AICODE_TURRET_DUMBFIRE_ANGLE) {
in_sight = true;
}
}
}
// if dumbfire (lasers and non-homing missiles)
if ( !(wip->is_homing()) )
{
if ((dist_to_enemy < 75.0f) || in_sight)
{
turret_update_enemy_in_range(ss, 2*wip->fire_wait);
ok_to_fire = true;
}
}
// if heat seekers
else if ( wip->wi_flags[Weapon::Info_Flags::Homing_heat] )
{
if ((dist_to_enemy < 50.0f) || in_sight)
{
turret_update_enemy_in_range(ss, 2*wip->fire_wait);
ok_to_fire = true;
}
}
// if aspect seeker
else if ( wip->wi_flags[Weapon::Info_Flags::Homing_aspect] )
{
if ((dist_to_enemy < 50.0f) || in_sight)
{
turret_update_enemy_in_range(ss, 2*wip->fire_wait);
}
if ( turret_should_fire_aspect(ss, wip, in_sight) )
{
ok_to_fire = true;
}
}
// if javelin heat seeker
else if ( wip->wi_flags[Weapon::Info_Flags::Homing_javelin] )
{
if ((dist_to_enemy < 50.0f) || in_sight)
{
turret_update_enemy_in_range(ss, 2*wip->fire_wait);
}
// Check if turret should fire and enemy's engines are
// in line of sight
if (turret_should_fire_aspect(ss, wip, in_sight) &&
ship_get_closest_subsys_in_sight(&Ships[lep->signature], SUBSYSTEM_ENGINE, &global_gun_pos))
{
ok_to_fire = true;
}
}
// maybe check that the gun vector doesn't intersect with the hull
if ( ok_to_fire && (tp->flags[Model::Subsystem_Flags::Turret_hull_check]) ) {
int model_num = Ship_info[shipp->ship_info_index].model_num;
vec3d end;
vm_vec_scale_add(&end, &global_gun_pos, &global_gun_vec, model_get_radius(model_num));
mc_info hull_check;
hull_check.model_instance_num = shipp->model_instance_num;
hull_check.model_num = model_num;
hull_check.orient = &objp->orient;
hull_check.pos = &objp->pos;
hull_check.p0 = &global_gun_pos;
hull_check.p1 = &end;
hull_check.flags = MC_CHECK_MODEL | MC_CHECK_RAY;
if ( model_collide(&hull_check) ) {
ok_to_fire = false;
}
}
if ( ok_to_fire )
{
// starting animation checks
if (ss->turret_animation_position == MA_POS_NOT_SET) {
//For legacy animations using subtype for turret number
auto animList = Ship_info[shipp->ship_info_index].animations.getAll(model_get_instance(shipp->model_instance_num), animation::ModelAnimationTriggerType::TurretFiring, ss->system_info->subobj_num, true);
//For modern animations using proper triggered-by-subsys name
animList += Ship_info[shipp->ship_info_index].animations.get(model_get_instance(shipp->model_instance_num), animation::ModelAnimationTriggerType::TurretFiring, animation::anim_name_from_subsys(ss->system_info));
if (animList.start(animation::ModelAnimationDirection::FWD)) {
ss->turret_animation_done_time = timestamp(animList.getTime());
ss->turret_animation_position = MA_POS_SET;
}
}
//Wait until the animation is done to actually fire
if (tp->flags[Model::Subsystem_Flags::Turret_anim_wait] && (ss->turret_animation_position != MA_POS_READY))
{
ok_to_fire = false;
}
}
if ( ok_to_fire )
{
// finally!
something_was_ok_to_fire = true;
Num_turrets_fired++;
// (Despite the retail comment above, retail actually fired toward the enemy; v2e = vector-to-enemy)
vec3d shoot_vector = v2e;
if (in_lab || tp->flags[Model::Subsystem_Flags::Fire_on_normal])
shoot_vector = global_gun_vec;
// In the lab the order of operations is a little fuzzy since we're faking targets so always fire along the turret's current vector no matter what if we're a multipart turret
if (in_lab && (ss->system_info->turret_gun_sobj >= 0 && ss->system_info->subobj_num != ss->system_info->turret_gun_sobj)) {
vec3d new_pos;
vec3d new_vec;
ship_get_global_turret_gun_info(objp, ss, &new_pos, false, &new_vec, true, nullptr);
shoot_vector = new_vec;
vm_vec_normalize(&shoot_vector);
global_gun_pos = new_pos;
predicted_enemy_pos = new_pos + new_vec * 500.0f;
}
// if the weapon is a flak gun, add some jitter to its aim so it fires in a "cone"
// to make a cool visual effect and make them less lethal
if (wip->wi_flags[Weapon::Info_Flags::Flak]) {
flak_jitter_aim(&shoot_vector, dist_to_enemy, ship_get_subsystem_strength(shipp, SUBSYSTEM_WEAPONS), wip);
}
turret_fire_weapon(valid_weapons[valid_index], ss, parent_objnum, launch_curve_data, &global_gun_pos, &shoot_vector, &predicted_enemy_pos, 100.0f, play_sound);
}
else
{
turret_update_enemy_in_range(ss, -4*wip->fire_wait);
if (tp->flags[Model::Subsystem_Flags::Use_multiple_guns])
set_turret_weapon_next_fire_stamp(swp, valid_weapons[valid_index], 500);
else
ss->turret_next_fire_stamp = timestamp(500);
// make sure salvo fire mode does not turn into autofire
if ((tp->flags[Model::Subsystem_Flags::Turret_salvo]) && ((i + 1) == number_of_firings)) {
ai_info *parent_aip = &Ai_info[shipp->ai_index];
turret_set_next_fire_timestamp(valid_weapons[valid_index], wip, ss, parent_aip, launch_curve_data);
}
}
// we are done firing, so fire the next weapon from the next firing point
// (This was originally done under the "We're ready to fire..." comment)
ss->turret_next_fire_pos++;
}
if (!something_was_ok_to_fire)
{
// If nothing is OK to fire (lost track of the target?)
// reset the target (so we don't continue to track what we can't hit)
if (tp->flags[Model::Subsystem_Flags::Turret_only_target_if_can_fire] && !(ss->flags[Ship::Subsystem_Flags::Forced_target]))
{
ss->turret_enemy_objnum = -1; // Reset enemy objnum, find a new one next frame.
ss->turret_time_enemy_in_range = 0.0f;
}
}
else
{
//something did fire, get the lowest valid timestamp
// don't do this if we already set a turret timestamp previously in the function
if (timestamp_since(ss->turret_next_fire_stamp) >= 0)
{
int minimum_stamp = -1;
for (int i = 0; i < MAX_SHIP_WEAPONS; ++i)
{
int stamp = (i < MAX_SHIP_PRIMARY_BANKS) ? swp->next_primary_fire_stamp[i] : swp->next_secondary_fire_stamp[i - MAX_SHIP_PRIMARY_BANKS];
// valid timestamps start at 2; stamp must be in the future
if (stamp < 2 || timestamp_since(stamp) >= 0)
continue;
// find minimum
if (minimum_stamp < 0 || timestamp_until(stamp) < timestamp_until(minimum_stamp))
minimum_stamp = stamp;
}
// set turret timestamp, or 100ms for sanity's sake
if (minimum_stamp >= 0)
ss->turret_next_fire_stamp = minimum_stamp;
else
ss->turret_next_fire_stamp = timestamp(100);
}
}
}
else if (!(ss->flags[Ship::Subsystem_Flags::Forced_target]))
{
// Lost him!
ss->turret_enemy_objnum = -1; // Reset enemy objnum, find a new one next frame.
ss->turret_time_enemy_in_range = 0.0f;
}
}
bool turret_std_fov_test(const ship_subsys *ss, const vec3d *gvec, const vec3d *v2e, float size_mod)
{
model_subsystem *tp = ss->system_info;
float dot = vm_vec_dot(v2e, gvec);
if (((dot + size_mod) >= tp->turret_fov) && ((dot - size_mod) <= tp->turret_max_fov))
return true;
return false;
}
bool turret_adv_fov_test(const ship_subsys *ss, const vec3d *gvec, const vec3d *v2e, float size_mod)
{
model_subsystem *tp = ss->system_info;
float dot = vm_vec_dot(v2e, gvec);
if (((dot + size_mod) >= tp->turret_fov) && ((dot - size_mod) <= tp->turret_max_fov)) {
object* objp = &Objects[ss->parent_objnum];
matrix turret_matrix;
turret_instance_find_world_orient(&turret_matrix, Ships[objp->instance].model_instance_num, tp->subobj_num, &objp->orient);
vec3d of_dst;
vm_vec_rotate(&of_dst, v2e, &turret_matrix);
if ((of_dst.xyz.x == 0) && (of_dst.xyz.z == 0))
return true;
else {
of_dst.xyz.y = 0;
if (!IS_VEC_NULL_SQ_SAFE(&of_dst)) {
vm_vec_normalize(&of_dst);
// now we have 2d vector with lenght of 1 that points at the targets direction after being rotated to turrets FOR
if ((of_dst.xyz.z + size_mod) >= tp->turret_base_fov)
return true;
}
}
}
return false;
}
bool turret_fov_test(const ship_subsys *ss, const vec3d *gvec, const vec3d *v2e, float size_mod)
{
bool in_fov = false;
if (ss->system_info->turret_base_fov > -1.0f)
in_fov = turret_adv_fov_test(ss, gvec, v2e, size_mod);
else
in_fov = turret_std_fov_test(ss, gvec, v2e, size_mod);
return in_fov;
}
float get_adjusted_turret_rof(ship_subsys *ss)
{
float tempf = ss->rof_scaler;
// optional reset switch (negative value)
if (tempf < 0) {
ss->rof_scaler = 1.0f;
return 1.0f;
}
if (tempf == 0) {
// special case returning the number of firingpoints
ss->rof_scaler = (float) ss->system_info->turret_num_firing_points;
tempf = ss->rof_scaler;
// safety check to avoid div/0 issues
if (tempf == 0) {
ss->rof_scaler = 1.0f;
return 1.0f;
}
}
return tempf;
}