From 7837fe30176f20bd6b3fe9cfc50324b99a825f61 Mon Sep 17 00:00:00 2001 From: Kestrellius <902X@comcast.net> Date: Sat, 8 Aug 2026 10:28:03 -0700 Subject: [PATCH 1/4] add destroy turret type orders --- code/ai/ai.h | 8 +- code/ai/aicode.cpp | 34 +++-- code/ai/aigoals.cpp | 133 +++++++++++++----- code/ai/aigoals.h | 2 + code/missioneditor/missionsave.cpp | 8 ++ code/parse/sexp.cpp | 45 ++++++ code/parse/sexp.h | 2 + code/scripting/api/objs/enums.cpp | 2 + code/scripting/api/objs/enums.h | 2 + code/scripting/api/objs/order.cpp | 10 +- code/scripting/api/objs/ship.cpp | 24 +++- code/ship/ship.cpp | 79 ++++++++++- code/ship/ship.h | 6 +- fred2/management.cpp | 52 +++---- qtfred/src/mission/Editor.cpp | 50 +++---- .../ShipEditor/ShipGoalsDialogModel.cpp | 4 + qtfred/src/ui/util/ErrorChecker.cpp | 3 + 17 files changed, 357 insertions(+), 107 deletions(-) diff --git a/code/ai/ai.h b/code/ai/ai.h index b74f7b43527..1ed24be3294 100644 --- a/code/ai/ai.h +++ b/code/ai/ai.h @@ -528,7 +528,7 @@ const char *ai_get_goal_target_name(const char *name, int *index); void ai_clear_goal_target_names(); extern void init_ai_system(void); -extern void ai_attack_object(object *attacker, object *attacked, int ship_info_index = -1, int class_type = -1); +extern void ai_attack_object(object *attacker, object *attacked, int ship_info_index = -1, int class_type = -1, int turret_wip_index = -1); extern void ai_evade_object(object *evader, object *evaded); extern void ai_ignore_object(object *ignorer, object *ignored, int ignore_new); extern void ai_ignore_wing(object *ignorer, int wingnum); @@ -544,7 +544,7 @@ extern void init_ai_object(int objnum); extern void ai_init(void); // Call this one to parse ai.tbl. extern void ai_level_init(void); // Call before each level to reset AI -extern int ai_set_attack_subsystem(object *objp, int subnum); +extern int ai_set_attack_subsystem(object *objp, int subnum, int wip_index = -1); extern int ai_issue_rearm_request(object *requester_objp); // Object requests rearm/repair. extern int ai_abort_rearm_request(object *requester_objp); // Object aborts rearm/repair. extern void ai_do_repair_frame(object *objp, ai_info *aip, float frametime); // Repair a ship object, player or AI. @@ -597,7 +597,7 @@ extern int ai_maybe_fire_afterburner(object *objp, ai_info *aip); extern void set_predicted_enemy_pos(vec3d *predicted_enemy_pos, object *pobjp, vec3d *enemy_pos, vec3d *enemy_vel, ai_info *aip); extern int is_instructor(object *objp); -extern int find_enemy(int objnum, float range, int max_attackers, int ship_info_index = -1, int class_type = -1); +extern int find_enemy(int objnum, float range, int max_attackers, int ship_info_index = -1, int class_type = -1, int turret_wip_index = -1); float ai_get_weapon_speed(const ship_weapon *swp); void set_predicted_enemy_pos_turret(vec3d *predicted_enemy_pos, const vec3d *gun_pos, const object *pobjp, const vec3d *enemy_pos, const vec3d *enemy_vel, float weapon_speed, float time_enemy_in_range); @@ -618,7 +618,7 @@ extern float dock_orient_and_approach(object *docker_objp, int docker_index, obj void ai_set_mode_warp_out(object *objp, ai_info *aip); // prototyped by Goober5000 -int get_nearest_objnum(int objnum, int enemy_team_mask, int enemy_wing, float range, int max_attackers, int ship_info_index, int class_type = -1); +int get_nearest_objnum(int objnum, int enemy_team_mask, int enemy_wing, float range, int max_attackers, int ship_info_index, int class_type = -1, int turret_wip_index = -1); // moved to header file by Goober5000 void ai_announce_ship_dying(object *dying_objp); diff --git a/code/ai/aicode.cpp b/code/ai/aicode.cpp index 9136144dae1..675de881a14 100644 --- a/code/ai/aicode.cpp +++ b/code/ai/aicode.cpp @@ -2210,6 +2210,7 @@ typedef struct eval_nearest_objnum { int enemy_team_mask; int enemy_ship_info_index; int enemy_class_type; + int enemy_turret_wip_index; int enemy_wing; float range; int max_attackers; @@ -2247,6 +2248,10 @@ void evaluate_object_as_nearest_objnum(eval_nearest_objnum *eno) if ((eno->enemy_class_type >= 0) && (Ship_info[shipp->ship_info_index].class_type != eno->enemy_class_type)) return; + // If only supposed to attack turrets carrying a certain weapon, don't attack ships without any such live turrets. + if ((eno->enemy_turret_wip_index >= 0) && (ship_get_turret_type_aggregate_hits(shipp, eno->enemy_turret_wip_index) <= 0.0f)) + return; + // Don't keep firing at a ship that is in its death throes. if (shipp->flags[Ship::Ship_Flags::Dying]) return; @@ -2337,7 +2342,7 @@ void evaluate_object_as_nearest_objnum(eval_nearest_objnum *eno) * @param ship_info_index If >=0, the enemy object must be of the specified ship class * @param class_type If >=0, the enemy object must be of the specified ship type */ -int get_nearest_objnum(int objnum, int enemy_team_mask, int enemy_wing, float range, int max_attackers, int ship_info_index, int class_type) +int get_nearest_objnum(int objnum, int enemy_team_mask, int enemy_wing, float range, int max_attackers, int ship_info_index, int class_type, int turret_wip_index) { object *danger_weapon_objp; ai_info *aip; @@ -2348,6 +2353,7 @@ int get_nearest_objnum(int objnum, int enemy_team_mask, int enemy_wing, float ra eno.enemy_team_mask = enemy_team_mask; eno.enemy_ship_info_index = ship_info_index; eno.enemy_class_type = class_type; + eno.enemy_turret_wip_index = turret_wip_index; eno.enemy_wing = enemy_wing; eno.max_attackers = max_attackers; eno.objnum = objnum; @@ -2503,7 +2509,7 @@ int get_enemy_timestamp() * @param ship_info_index If specified, restrict the search to enemies with this ship class * @param class_type If specified, restrict the search to enemies with this ship type */ -int find_enemy(int objnum, float range, int max_attackers, int ship_info_index, int class_type) +int find_enemy(int objnum, float range, int max_attackers, int ship_info_index, int class_type, int turret_wip_index) { int enemy_team_mask; @@ -2529,8 +2535,10 @@ int find_enemy(int objnum, float range, int max_attackers, int ship_info_index, if (ship_info_index < 0 || ship_info_index == target_shipp->ship_info_index) { if (class_type < 0 || (target_shipp->ship_info_index >= 0 && class_type == Ship_info[target_shipp->ship_info_index].class_type)) { - if (!(Objects[target_objnum].flags[Object::Object_Flags::Protected])) { - return target_objnum; + if (turret_wip_index < 0 || (ship_get_turret_type_aggregate_hits(target_shipp, turret_wip_index) > 0.0f)) { + if (!(Objects[target_objnum].flags[Object::Object_Flags::Protected])) { + return target_objnum; + } } } } @@ -2541,7 +2549,7 @@ int find_enemy(int objnum, float range, int max_attackers, int ship_info_index, } } - return get_nearest_objnum(objnum, enemy_team_mask, aip->enemy_wing, range, max_attackers, ship_info_index, class_type); + return get_nearest_objnum(objnum, enemy_team_mask, aip->enemy_wing, range, max_attackers, ship_info_index, class_type, turret_wip_index); } else { aip->target_objnum = -1; aip->target_signature = -1; @@ -2582,7 +2590,7 @@ void force_avoid_player_check(object *objp, ai_info *aip) * If attacked == NULL, then attack any enemy object. * Attack point *rel_pos on object. This is for supporting attacking subsystems. */ -void ai_attack_object(object* attacker, object* attacked, int ship_info_index, int class_type) +void ai_attack_object(object* attacker, object* attacked, int ship_info_index, int class_type, int turret_wip_index) { int temp; ai_info* aip; @@ -2615,7 +2623,7 @@ void ai_attack_object(object* attacker, object* attacked, int ship_info_index, i if (attacked == nullptr) { aip->choose_enemy_timestamp = timestamp(0); // nebula safe - set_target_objnum(aip, find_enemy(OBJ_INDEX(attacker), 99999.9f, 4, ship_info_index, class_type)); + set_target_objnum(aip, find_enemy(OBJ_INDEX(attacker), 99999.9f, 4, ship_info_index, class_type, turret_wip_index)); } else { // check if we can see attacked in nebula if (aip->target_objnum != OBJ_INDEX(attacked)) { @@ -8168,7 +8176,7 @@ void ai_chase_ga(ai_info *aip, ship_info *sip) // Make object *objp attack subsystem with ID = subnum. // Return true if found a subsystem to attack, else return false. // Note, can fail if subsystem exists, but has no hits. -int ai_set_attack_subsystem(object *objp, int subnum) +int ai_set_attack_subsystem(object *objp, int subnum, int wip_index) { int temp; ship *shipp, *attacker_shipp; @@ -8195,7 +8203,9 @@ int ai_set_attack_subsystem(object *objp, int subnum) shipp = &Ships[attacked_objp->instance]; // need to get our target's ship pointer!!! // When subnum is >= 0, it indicates a specific subsystem. Otherwise it is a subsystem type. See ai_submode - if (subnum >= 0) + if (wip_index >= 0) { + ssp = ship_find_first_subsys(shipp, SUBSYSTEM_TURRET, &objp->pos, wip_index); + } else if (subnum >= 0) ssp = ship_get_indexed_subsys(shipp, subnum); else ssp = ship_find_first_subsys(shipp, -subnum, &objp->pos); @@ -15526,18 +15536,22 @@ void ai_frame(int objnum) Assert(En_objp->type == OBJ_SHIP); if ( aip->targeted_subsys->current_hits <= 0.0f ) { int subsys_type; + int turret_wip_index = -1; if ( aip->goals[0].ai_mode == AI_GOAL_DISABLE_SHIP || aip->goals[0].ai_mode == AI_GOAL_DISABLE_SHIP_TACTICAL ) { subsys_type = SUBSYSTEM_ENGINE; } else if ( aip->goals[0].ai_mode == AI_GOAL_DISARM_SHIP || aip->goals[0].ai_mode == AI_GOAL_DISARM_SHIP_TACTICAL ) { subsys_type = SUBSYSTEM_TURRET; + } else if (aip->goals[0].ai_mode == AI_GOAL_DESTROY_TURRET_TYPE || aip->goals[0].ai_mode == AI_GOAL_DESTROY_TURRET_TYPE_ON_SHIP) { + subsys_type = SUBSYSTEM_TURRET; + turret_wip_index = aip->goals[0].int_data; } else { subsys_type = -1; } if ( subsys_type != -1 ) { ship_subsys *new_subsys; - new_subsys = ship_return_next_subsys(&Ships[En_objp->instance], subsys_type, &Pl_objp->pos); + new_subsys = ship_return_next_subsys(&Ships[En_objp->instance], subsys_type, &Pl_objp->pos, turret_wip_index); if ( new_subsys != NULL ) { set_targeted_subsys(aip, new_subsys, aip->target_objnum); } else { diff --git a/code/ai/aigoals.cpp b/code/ai/aigoals.cpp index 99d1aa6d4cd..551207e14f8 100644 --- a/code/ai/aigoals.cpp +++ b/code/ai/aigoals.cpp @@ -87,35 +87,37 @@ char Ai_dock_names[MAX_AI_DOCK_NAMES][NAME_LENGTH]; // Used in objecttypes.tbl to define custom ship types ai_goal_list Ai_goal_names[] = { - { "Attack ship", AI_GOAL_CHASE, 0 }, - { "Dock", AI_GOAL_DOCK, 0 }, - { "Waypoints", AI_GOAL_WAYPOINTS, 0 }, - { "Waypoints once", AI_GOAL_WAYPOINTS_ONCE, 0 }, - { "Depart", AI_GOAL_WARP, 0 }, - { "Attack subsys", AI_GOAL_DESTROY_SUBSYSTEM, 0 }, - { "Form on wing", AI_GOAL_FORM_ON_WING, 0 }, - { "Undock", AI_GOAL_UNDOCK, 0 }, - { "Attack wing", AI_GOAL_CHASE_WING, 0 }, - { "Guard ship", AI_GOAL_GUARD, 0 }, - { "Disable ship", AI_GOAL_DISABLE_SHIP, 0 }, - { "Disable ship (tactical)",AI_GOAL_DISABLE_SHIP_TACTICAL, 0 }, - { "Disarm ship", AI_GOAL_DISARM_SHIP, 0 }, - { "Disarm ship (tactical)", AI_GOAL_DISARM_SHIP_TACTICAL, 0 }, - { "Attack any", AI_GOAL_CHASE_ANY, 0 }, - { "Ignore ship", AI_GOAL_IGNORE, 0 }, - { "Ignore ship (new)", AI_GOAL_IGNORE_NEW, 0 }, - { "Guard wing", AI_GOAL_GUARD_WING, 0 }, - { "Evade ship", AI_GOAL_EVADE_SHIP, 0 }, - { "Stay near ship", AI_GOAL_STAY_NEAR_SHIP, 0 }, - { "Keep safe dist", AI_GOAL_KEEP_SAFE_DISTANCE, 0 }, - { "Rearm ship", AI_GOAL_REARM_REPAIR, 0 }, - { "Stay still", AI_GOAL_STAY_STILL, 0 }, - { "Play dead", AI_GOAL_PLAY_DEAD, 0 }, - { "Play dead (persistent)", AI_GOAL_PLAY_DEAD_PERSISTENT, 0 }, - { "Attack weapon", AI_GOAL_CHASE_WEAPON, 0 }, - { "Fly to ship", AI_GOAL_FLY_TO_SHIP, 0 }, - { "Attack ship class", AI_GOAL_CHASE_SHIP_CLASS, 0 }, - { "Attack ship type", AI_GOAL_CHASE_SHIP_TYPE, 0 }, + { "Attack ship", AI_GOAL_CHASE, 0 }, + { "Dock", AI_GOAL_DOCK, 0 }, + { "Waypoints", AI_GOAL_WAYPOINTS, 0 }, + { "Waypoints once", AI_GOAL_WAYPOINTS_ONCE, 0 }, + { "Depart", AI_GOAL_WARP, 0 }, + { "Attack subsys", AI_GOAL_DESTROY_SUBSYSTEM, 0 }, + { "Form on wing", AI_GOAL_FORM_ON_WING, 0 }, + { "Undock", AI_GOAL_UNDOCK, 0 }, + { "Attack wing", AI_GOAL_CHASE_WING, 0 }, + { "Guard ship", AI_GOAL_GUARD, 0 }, + { "Disable ship", AI_GOAL_DISABLE_SHIP, 0 }, + { "Disable ship (tactical)", AI_GOAL_DISABLE_SHIP_TACTICAL, 0 }, + { "Disarm ship", AI_GOAL_DISARM_SHIP, 0 }, + { "Disarm ship (tactical)", AI_GOAL_DISARM_SHIP_TACTICAL, 0 }, + { "Attack any", AI_GOAL_CHASE_ANY, 0 }, + { "Ignore ship", AI_GOAL_IGNORE, 0 }, + { "Ignore ship (new)", AI_GOAL_IGNORE_NEW, 0 }, + { "Guard wing", AI_GOAL_GUARD_WING, 0 }, + { "Evade ship", AI_GOAL_EVADE_SHIP, 0 }, + { "Stay near ship", AI_GOAL_STAY_NEAR_SHIP, 0 }, + { "Keep safe dist", AI_GOAL_KEEP_SAFE_DISTANCE, 0 }, + { "Rearm ship", AI_GOAL_REARM_REPAIR, 0 }, + { "Stay still", AI_GOAL_STAY_STILL, 0 }, + { "Play dead", AI_GOAL_PLAY_DEAD, 0 }, + { "Play dead (persistent)", AI_GOAL_PLAY_DEAD_PERSISTENT, 0 }, + { "Attack weapon", AI_GOAL_CHASE_WEAPON, 0 }, + { "Fly to ship", AI_GOAL_FLY_TO_SHIP, 0 }, + { "Attack ship class", AI_GOAL_CHASE_SHIP_CLASS, 0 }, + { "Attack ship type", AI_GOAL_CHASE_SHIP_TYPE, 0 }, + { "Attack turret type", AI_GOAL_DESTROY_TURRET_TYPE, 0 }, + { "Attack turret type on ship",AI_GOAL_DESTROY_TURRET_TYPE_ON_SHIP,0 }, }; int Num_ai_goals = sizeof(Ai_goal_names) / sizeof(ai_goal_list); @@ -131,6 +133,8 @@ const char *Ai_goal_text(ai_goal_mode goal, int submode) case AI_GOAL_CHASE_WING: case AI_GOAL_CHASE_SHIP_CLASS: case AI_GOAL_CHASE_SHIP_TYPE: + case AI_GOAL_DESTROY_TURRET_TYPE: + case AI_GOAL_DESTROY_TURRET_TYPE_ON_SHIP: return XSTR( "attack ", 474); case AI_GOAL_DOCK: return XSTR( "dock ", 475); @@ -1045,6 +1049,17 @@ void ai_add_goal_sub_sexp( int sexp, ai_goal_type type, ai_info *aip, ai_goal *a aigp->ai_submode = -SUBSYSTEM_TURRET; aigp->priority = eval_num(CDDR(node), priority_is_nan, priority_is_nan_forever); break; + + case OP_AI_DESTROY_TURRET_TYPE: + aigp->ai_mode = AI_GOAL_DESTROY_TURRET_TYPE; + aigp->int_data = weapon_info_lookup(ai_get_goal_target_name( CTEXT(CDR(node)), &dummy )); + break; + + case OP_AI_DESTROY_TURRET_TYPE_ON_SHIP: + aigp->ai_mode = AI_GOAL_DESTROY_TURRET_TYPE_ON_SHIP; + aigp->int_data = weapon_info_lookup(ai_get_goal_target_name( CTEXT(CDR(node)), &dummy )); + aigp->target_name = ai_get_goal_target_name( CTEXT(CDDR(node)), &aigp->target_name_index ); + break; case OP_AI_WARP_OUT: aigp->ai_mode = AI_GOAL_WARP; @@ -1313,7 +1328,8 @@ void ai_add_goal_sub_sexp( int sexp, ai_goal_type type, ai_info *aip, ai_goal *a if (op == OP_AI_GUARD || op == OP_AI_GUARD_WING || op == OP_AI_WAYPOINTS || - op == OP_AI_WAYPOINTS_ONCE) { + op == OP_AI_WAYPOINTS_ONCE || + op == OP_AI_DESTROY_TURRET_TYPE) { if (is_sexp_true(CDDDR(node))) aigp->flags.set(AI::Goal_Flags::Afterburn_hard); } @@ -1326,7 +1342,8 @@ void ai_add_goal_sub_sexp( int sexp, ai_goal_type type, ai_info *aip, ai_goal *a op == OP_AI_DISARM_SHIP || op == OP_AI_DISARM_SHIP_TACTICAL || op == OP_AI_STAY_NEAR_SHIP || - op == OP_AI_FLY_TO_SHIP) { + op == OP_AI_FLY_TO_SHIP || + op == OP_AI_DESTROY_TURRET_TYPE_ON_SHIP) { if (is_sexp_true(CDDDDR(node))) aigp->flags.set(AI::Goal_Flags::Afterburn_hard); } @@ -1538,6 +1555,14 @@ int ai_remove_goal_sexp_sub( int sexp, ai_goal* aigp, bool &remove_more ) priority = eval_priority_et_seq(CDDR(node)); goalmode = AI_GOAL_REARM_REPAIR; break; + case OP_AI_DESTROY_TURRET_TYPE: + priority = eval_priority_et_seq(CDDR(node)); + goalmode = AI_GOAL_REARM_REPAIR; + break; + case OP_AI_DESTROY_TURRET_TYPE_ON_SHIP: + priority = eval_priority_et_seq(CDDDR(node)); + goalmode = AI_GOAL_REARM_REPAIR; + break; default: const ai_mode_lua* luaAIMode = ai_lua_find_mode(op); if(luaAIMode != nullptr){ @@ -1836,6 +1861,19 @@ ai_achievability ai_mission_goal_achievable( int objnum, ai_goal *aigp ) } return ai_achievability::NOT_KNOWN; } + // similarly for attacking all turrets with a certain weapon class + if (aigp->ai_mode == AI_GOAL_DESTROY_TURRET_TYPE) { + for (auto so : list_range(&Ship_obj_list)) { + auto type_objp = &Objects[so->objnum]; + if (type_objp->type != OBJ_SHIP || type_objp->flags[Object::Object_Flags::Should_be_dead] || aigp->int_data < 0 || aigp->int_data >= sz2i(Weapon_info.size())) + continue; + ship *potential_target_shipp = &Ships[type_objp->instance]; + if (ship_get_turret_type_aggregate_hits(potential_target_shipp, aigp->int_data) > 0.0f) { + return ai_achievability::ACHIEVABLE; + } + } + return ai_achievability::NOT_KNOWN; + } return_val = ai_achievability::SATISFIED; @@ -1912,6 +1950,19 @@ ai_achievability ai_mission_goal_achievable( int objnum, ai_goal *aigp ) } break; } + case AI_GOAL_DESTROY_TURRET_TYPE_ON_SHIP: + { + // shipnum could be -1 depending on if the ship hasn't arrived or died. only look for subsystem + // destroyed when shipnum is valid + + // can't determine the status of this goal if ship not valid + if (!target_ship_entry || !target_ship_entry->has_shipp() || aigp->int_data < 0 || aigp->int_data >= sz2i(Weapon_info.size())) { + status = 0; + } else { + status = (ship_get_turret_type_aggregate_hits(target_ship_entry->shipp(), aigp->int_data) <= 0.0f) ? 1 : 0; + } + break; + } // to guard or ignore a ship, the goal cannot continue if the ship being guarded is either destroyed // or has departed. @@ -2049,7 +2100,7 @@ ai_achievability ai_mission_goal_achievable( int objnum, ai_goal *aigp ) } // Goober5000 - before doing anything else, check if this is a disarm goal for an arrived ship... - if ((status == SHIP_STATUS_ARRIVED) && (aigp->ai_mode == AI_GOAL_DISARM_SHIP || aigp->ai_mode == AI_GOAL_DISARM_SHIP_TACTICAL)) + if ((status == SHIP_STATUS_ARRIVED) && (aigp->ai_mode == AI_GOAL_DISARM_SHIP || aigp->ai_mode == AI_GOAL_DISARM_SHIP_TACTICAL || aigp->ai_mode == AI_GOAL_DESTROY_TURRET_TYPE_ON_SHIP)) { if (target_ship_entry && target_ship_entry->has_shipp()) { // if the ship has no turrets, we can't disarm it! @@ -2212,6 +2263,7 @@ ai_achievability ai_mission_goal_achievable( int objnum, ai_goal *aigp ) case AI_GOAL_EVADE_SHIP: case AI_GOAL_STAY_NEAR_SHIP: case AI_GOAL_FLY_TO_SHIP: + case AI_GOAL_DESTROY_TURRET_TYPE_ON_SHIP: { if ( status == SHIP_STATUS_ARRIVED ) return ai_achievability::ACHIEVABLE; @@ -2676,6 +2728,15 @@ void ai_process_mission_orders( int objnum, ai_info *aip ) break; } + case AI_GOAL_DESTROY_TURRET_TYPE_ON_SHIP: { + Assert(current_goal_target_ship && current_goal_target_ship->has_objp()); + Assertion(current_goal->int_data >= 0, "The target of AI_GOAL_DESTROY_TURRET_TYPE_ON_SHIP must refer to a valid weapon class!"); + other_obj = current_goal_target_ship->objp(); + ai_attack_object( objp, other_obj); + ai_set_attack_subsystem( objp, 0, current_goal->int_data ); + break; + } + case AI_GOAL_CHASE_WING: wingnum = wing_name_lookup( current_goal->target_name ); Assertion( wingnum >= 0, "The target of AI_GOAL_CHASE_WING must refer to a valid wing!" ); @@ -2686,6 +2747,12 @@ void ai_process_mission_orders( int objnum, ai_info *aip ) ai_attack_object( objp, nullptr); break; + case AI_GOAL_DESTROY_TURRET_TYPE: + Assertion(current_goal->int_data >= 0, "The target of AI_GOAL_DESTROY_TURRET_TYPE must refer to a valid weapon class!"); + ai_attack_object( objp, nullptr, -1, -1, current_goal->int_data); + ai_set_attack_subsystem( objp, 0, current_goal->int_data ); + break; + // chase-ship-class is chase-any but restricted to a subset of ships case AI_GOAL_CHASE_SHIP_CLASS: { @@ -2796,6 +2863,7 @@ void ai_update_goal_references(ai_goal *goals, sexp_ref_type type, const char *o case AI_GOAL_DISABLE_SHIP_TACTICAL: case AI_GOAL_DISARM_SHIP: case AI_GOAL_DISARM_SHIP_TACTICAL: + case AI_GOAL_DESTROY_TURRET_TYPE_ON_SHIP: case AI_GOAL_IGNORE: case AI_GOAL_IGNORE_NEW: case AI_GOAL_EVADE_SHIP: @@ -2876,6 +2944,7 @@ bool query_referenced_in_ai_goals(ai_goal *goals, sexp_ref_type type, const char case AI_GOAL_IGNORE_NEW: case AI_GOAL_EVADE_SHIP: case AI_GOAL_STAY_NEAR_SHIP: + case AI_GOAL_DESTROY_TURRET_TYPE_ON_SHIP: flag = 1; } break; diff --git a/code/ai/aigoals.h b/code/ai/aigoals.h index d6a449d4c3b..89156094ac2 100644 --- a/code/ai/aigoals.h +++ b/code/ai/aigoals.h @@ -85,6 +85,8 @@ enum ai_goal_mode : uint8_t AI_GOAL_LUA, AI_GOAL_DISARM_SHIP_TACTICAL, AI_GOAL_DISABLE_SHIP_TACTICAL, + AI_GOAL_DESTROY_TURRET_TYPE, + AI_GOAL_DESTROY_TURRET_TYPE_ON_SHIP, AI_GOAL_NUM_VALUES }; diff --git a/code/missioneditor/missionsave.cpp b/code/missioneditor/missionsave.cpp index 72201544116..47a3141939f 100644 --- a/code/missioneditor/missionsave.cpp +++ b/code/missioneditor/missionsave.cpp @@ -675,6 +675,10 @@ void Fred_mission_save::save_ai_goals(ai_goal* goalp, int ship) str = "ai-chase-ship-type"; break; + case AI_GOAL_DESTROY_TURRET_TYPE: + str = "ai-destroy-turret-type"; + break; + case AI_GOAL_GUARD: str = "ai-guard"; break; @@ -699,6 +703,10 @@ void Fred_mission_save::save_ai_goals(ai_goal* goalp, int ship) str = "ai-disarm-ship-tactical"; break; + case AI_GOAL_DESTROY_TURRET_TYPE_ON_SHIP: + str = "ai-destroy-turret-type-on-ship"; + break; + case AI_GOAL_IGNORE: str = "ai-ignore"; break; diff --git a/code/parse/sexp.cpp b/code/parse/sexp.cpp index 0610b11be0c..a276848b35d 100644 --- a/code/parse/sexp.cpp +++ b/code/parse/sexp.cpp @@ -886,6 +886,8 @@ SCP_vector Operators = { { "ai-stay-still", OP_AI_STAY_STILL, 2, 3, SEXP_GOAL_OPERATOR, }, { "ai-play-dead", OP_AI_PLAY_DEAD, 1, 1, SEXP_GOAL_OPERATOR, }, { "ai-play-dead-persistent", OP_AI_PLAY_DEAD_PERSISTENT, 1, 1, SEXP_GOAL_OPERATOR, }, + { "ai-destroy-turret-type", OP_AI_DESTROY_TURRET_TYPE, 2, 3, SEXP_GOAL_OPERATOR, }, + { "ai-destroy-turret-type-on-ship", OP_AI_DESTROY_TURRET_TYPE_ON_SHIP, 3, 4, SEXP_GOAL_OPERATOR, }, { "goals", OP_GOALS_ID, 1, INT_MAX, SEXP_ACTION_OPERATOR, }, @@ -947,6 +949,8 @@ sexp_ai_goal_link Sexp_ai_goal_links[] = { { AI_GOAL_FORM_ON_WING, OP_AI_FORM_ON_WING }, { AI_GOAL_FLY_TO_SHIP, OP_AI_FLY_TO_SHIP }, { AI_GOAL_REARM_REPAIR, OP_AI_REARM_REPAIR }, + { AI_GOAL_DESTROY_TURRET_TYPE, OP_AI_DESTROY_TURRET_TYPE }, + { AI_GOAL_DESTROY_TURRET_TYPE, OP_AI_DESTROY_TURRET_TYPE_ON_SHIP }, }; SCP_vector Dynamic_enums; @@ -32113,6 +32117,8 @@ int query_operator_return_type(int op) case OP_AI_FORM_ON_WING: case OP_AI_FLY_TO_SHIP: case OP_AI_REARM_REPAIR: + case OP_AI_DESTROY_TURRET_TYPE: + case OP_AI_DESTROY_TURRET_TYPE_ON_SHIP: return OPR_AI_GOAL; case OP_ANY_OF: @@ -33130,6 +33136,24 @@ int query_operator_argument_type(int op_index, int argnum) else return OPF_BOOL; + case OP_AI_DESTROY_TURRET_TYPE: + if (argnum == 0) + return OPF_WEAPON_NAME; + else if (argnum == 1) + return OPF_POSITIVE; + else + return OPF_BOOL; + + case OP_AI_DESTROY_TURRET_TYPE_ON_SHIP: + if (argnum == 0) + return OPF_WEAPON_NAME; + else if (argnum == 1) + return OPF_SHIP; + else if (argnum == 2) + return OPF_POSITIVE; + else + return OPF_BOOL; + case OP_AI_GUARD: if (argnum == 0) return OPF_SHIP_WING; @@ -37346,6 +37370,8 @@ int get_category(int op_id) case OP_AI_PLAY_DEAD_PERSISTENT: case OP_AI_FLY_TO_SHIP: case OP_AI_REARM_REPAIR: + case OP_AI_DESTROY_TURRET_TYPE: + case OP_AI_DESTROY_TURRET_TYPE_ON_SHIP: return OP_CATEGORY_AI; case OP_GOALS_ID: @@ -39969,6 +39995,25 @@ SCP_vector Sexp_help = { "\t4 (optional):\tWhether to afterburn as hard as possible to the target; defaults to false." }, + { OP_AI_DESTROY_TURRET_TYPE, "Ai-destroy-turret-type (Ship/wing goal)\r\n" + "\tThis AI goal causes a ship/wing to destroy all enemy turrets in the mission that" + "are carrying a specified weapon." + "Takes 2 arguments...\r\n" + "\t1:\tName of weapon (primary or secondary) that should be attacked\r\n" + "\t2:\tGoal priority (number between 0 and 200. Player orders have a priority of 90-100).\r\n" + "\t3 (optional):\tWhether to afterburn as hard as possible to the target; defaults to false." + }, + + { OP_AI_DESTROY_TURRET_TYPE_ON_SHIP, "Ai-destroy-turret-type-on-ship (Ship/wing goal)\r\n" + "\tThis AI goal causes a ship/wing to destroy all turrets on a specified ship that" + "are carrying a specified weapon." + "Takes 2 arguments...\r\n" + "\t1:\tName of weapon (primary or secondary) that should be attacked\r\n" + "\t2:\tName of ship to target\r\n" + "\t3:\tGoal priority (number between 0 and 200. Player orders have a priority of 90-100).\r\n" + "\t4 (optional):\tWhether to afterburn as hard as possible to the target; defaults to false." + }, + { OP_AI_GUARD, "Ai-guard (Ship goal)\r\n" "\tCauses the specified ship to guard a ship from other ships not on the same team.\r\n\r\n" "Takes 2 arguments...\r\n" diff --git a/code/parse/sexp.h b/code/parse/sexp.h index 0434915fd41..5fbc7cf90dc 100644 --- a/code/parse/sexp.h +++ b/code/parse/sexp.h @@ -971,6 +971,8 @@ enum : int { OP_AI_REARM_REPAIR, // Goober5000 OP_AI_DISABLE_SHIP_TACTICAL, // Goober5000 OP_AI_DISARM_SHIP_TACTICAL, // Goober5000 + OP_AI_DESTROY_TURRET_TYPE, + OP_AI_DESTROY_TURRET_TYPE_ON_SHIP, // OP_CATEGORY_UNLISTED diff --git a/code/scripting/api/objs/enums.cpp b/code/scripting/api/objs/enums.cpp index 7aab3151e35..82d26cc5a80 100644 --- a/code/scripting/api/objs/enums.cpp +++ b/code/scripting/api/objs/enums.cpp @@ -52,6 +52,8 @@ const lua_enum_def_list Enumerations[] = { {"ORDER_UNDOCK", LE_ORDER_UNDOCK, true}, {"ORDER_WAYPOINTS", LE_ORDER_WAYPOINTS, true}, {"ORDER_WAYPOINTS_ONCE", LE_ORDER_WAYPOINTS_ONCE, true}, + {"ORDER_DESTROY_TURRET_TYPE", LE_ORDER_DESTROY_TURRET_TYPE, true}, + {"ORDER_DESTROY_TURRET_TYPE_ON_SHIP", LE_ORDER_DESTROY_TURRET_TYPE_ON_SHIP, true}, {"ORDER_LUA", LE_ORDER_LUA, true}, {"PARTICLE_DEBUG", LE_PARTICLE_DEBUG, true}, {"PARTICLE_BITMAP", LE_PARTICLE_BITMAP, true}, diff --git a/code/scripting/api/objs/enums.h b/code/scripting/api/objs/enums.h index c603b9172cc..8319456912e 100644 --- a/code/scripting/api/objs/enums.h +++ b/code/scripting/api/objs/enums.h @@ -51,6 +51,8 @@ enum lua_enum : int32_t { LE_ORDER_UNDOCK, LE_ORDER_WAYPOINTS, LE_ORDER_WAYPOINTS_ONCE, + LE_ORDER_DESTROY_TURRET_TYPE, + LE_ORDER_DESTROY_TURRET_TYPE_ON_SHIP, LE_ORDER_LUA, LE_PARTICLE_DEBUG, LE_PARTICLE_BITMAP, diff --git a/code/scripting/api/objs/order.cpp b/code/scripting/api/objs/order.cpp index 9176413b0f6..fab968bb7ff 100644 --- a/code/scripting/api/objs/order.cpp +++ b/code/scripting/api/objs/order.cpp @@ -177,6 +177,12 @@ ADE_FUNC(getType, l_Order, NULL, "Gets the type of the order.", "enumeration", " case AI_GOAL_CHASE_SHIP_TYPE: eh_idx = LE_ORDER_ATTACK_SHIP_TYPE; break; + case AI_GOAL_DESTROY_TURRET_TYPE: + eh_idx = LE_ORDER_DESTROY_TURRET_TYPE; + break; + case AI_GOAL_DESTROY_TURRET_TYPE_ON_SHIP: + eh_idx = LE_ORDER_DESTROY_TURRET_TYPE_ON_SHIP; + break; case AI_GOAL_LUA: eh_idx = LE_ORDER_LUA; break; @@ -233,6 +239,7 @@ ADE_VIRTVAR(Target, l_Order, "object", "Target of the order. Value may also be a case AI_GOAL_STAY_NEAR_SHIP: case AI_GOAL_FLY_TO_SHIP: case AI_GOAL_STAY_STILL: + case AI_GOAL_DESTROY_TURRET_TYPE_ON_SHIP: if ((newh->objp()->type == OBJ_SHIP) && stricmp(Ships[newh->objp()->instance].ship_name, ohp->aigp->target_name)) { ohp->aigp->target_name = ai_get_goal_target_name(Ships[newh->objp()->instance].ship_name, &ohp->aigp->target_name_index); ohp->aigp->time = (ohp->odx == 0) ? Missiontime : 0; @@ -364,7 +371,8 @@ ADE_VIRTVAR(Target, l_Order, "object", "Target of the order. Value may also be a case AI_GOAL_STAY_STILL: case AI_GOAL_REARM_REPAIR: case AI_GOAL_DOCK: - case AI_GOAL_UNDOCK: { + case AI_GOAL_UNDOCK: + case AI_GOAL_DESTROY_TURRET_TYPE_ON_SHIP: { auto target_entry = ship_registry_get(ohp->aigp->target_name); if (target_entry && target_entry->has_shipp()) { shipnum = target_entry->shipnum; diff --git a/code/scripting/api/objs/ship.cpp b/code/scripting/api/objs/ship.cpp index c366431d624..63b849c0858 100644 --- a/code/scripting/api/objs/ship.cpp +++ b/code/scripting/api/objs/ship.cpp @@ -1705,16 +1705,17 @@ ADE_FUNC(clearOrders, l_Ship, NULL, "Clears a ship's orders list", "boolean", "T return ADE_RETURN_TRUE; } -ADE_FUNC(giveOrder, l_Ship, "enumeration Order /* ORDER_* */, [object Target=nil, subsystem TargetSubsystem=nil, number Priority=1.0, shipclass TargetShipclass=nil, shiptype TargetShiptype=nil]", "Uses the goal code to execute orders. NOTE: This function uses a scale from 0.0-1.0 (up to 2.0) rather than the usual 0-100 (up to 200)", "boolean", "True if order was given, otherwise false or nil") +ADE_FUNC(giveOrder, l_Ship, "enumeration Order /* ORDER_* */, [object Target=nil, subsystem TargetSubsystem=nil, number Priority=1.0, shipclass TargetShipclass=nil, shiptype TargetShiptype=nil, weaponclass TargetWeaponclass=nil]", "Uses the goal code to execute orders. NOTE: This function uses a scale from 0.0-1.0 (up to 2.0) rather than the usual 0-100 (up to 200)", "boolean", "True if order was given, otherwise false or nil") { object_h *objh = NULL; enum_h *eh = NULL; float priority = 1.0f; // default to PLAYER_PRIORITY_SHIP int sclass = -1; int stype = -1; + int wip_index = -1; object_h *tgh = NULL; ship_subsys_h *tgsh = NULL; - int n_args = ade_get_args(L, "oo|oofoo", l_Object.GetPtr(&objh), l_Enum.GetPtr(&eh), l_Object.GetPtr(&tgh), l_Subsystem.GetPtr(&tgsh), &priority, l_Shipclass.Get(&sclass), l_Shiptype.Get(&stype)); + int n_args = ade_get_args(L, "oo|oofooo", l_Object.GetPtr(&objh), l_Enum.GetPtr(&eh), l_Object.GetPtr(&tgh), l_Subsystem.GetPtr(&tgsh), &priority, l_Shipclass.Get(&sclass), l_Shiptype.Get(&stype), l_Weaponclass.Get(&wip_index)); if (n_args < 2) return ADE_RETURN_NIL; bool omitted_priority = (n_args < 5); @@ -1980,6 +1981,25 @@ ADE_FUNC(giveOrder, l_Ship, "enumeration Order /* ORDER_* */, [object Target=nil } break; } + case LE_ORDER_DESTROY_TURRET_TYPE: + { + if (wip_index >= 0) + { + ai_mode = AI_GOAL_DESTROY_TURRET_TYPE; + int_data = wip_index; + } + break; + } + case LE_ORDER_DESTROY_TURRET_TYPE_ON_SHIP: + { + if (wip_index >= 0 && tgh_valid && tgh->objp()->type == OBJ_SHIP) + { + ai_mode = AI_GOAL_DESTROY_TURRET_TYPE_ON_SHIP; + ai_shipname = Ships[tgh->objp()->instance].ship_name; + int_data = wip_index; + } + break; + } default: return ade_set_error(L, "b", false); } diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index eee23123886..79e6961e9d1 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -15458,6 +15458,67 @@ int ship_find_num_turrets(object *objp) return n; } +bool turret_has_weapon(ship_subsys *ssp, int wi_index) +{ + ship_weapon *swp = &ssp->weapons; + for ( auto& i : swp->primary_bank_weapons ) { + if (i == wi_index) { + return true; + } + } + for ( auto& i : swp->secondary_bank_weapons ) { + if (i == wi_index) { + return true; + } + } + + ssp = GET_NEXT( ssp ); + + return false; +} + +float ship_get_turret_type_aggregate_hits(ship *shipp, int wi_index) +{ + float strength = 0.0f; + ship_subsys *ssp; + + // For a dying ship, all subsystem strengths are zero. + if (shipp->flags[Ship::Ship_Flags::Dying]) + return 0.0f; + + if ( shipp->subsys_info[SUBSYSTEM_TURRET].aggregate_current_hits <= 0.0f ) { + return 0.0f; + } + + ssp = GET_FIRST(&shipp->subsys_list); + while ( ssp != END_OF_LIST( &shipp->subsys_list ) ) { + ship_weapon *swp = &ssp->weapons; + bool weapon_found = false; + for ( auto& i : swp->primary_bank_weapons ) { + if (weapon_found) { + break; + } + if (i == wi_index) { + weapon_found = true; + strength += ssp->current_hits; + } + } + for ( auto& i : swp->secondary_bank_weapons ) { + if (weapon_found) { + break; + } + if (i == wi_index) { + weapon_found = true; + strength += ssp->current_hits; + } + } + + ssp = GET_NEXT( ssp ); + } + + return strength; +} + //WMC static void ship_set_eye( object *obj, int eye_index) { @@ -15537,7 +15598,7 @@ void object_get_eye(vec3d *eye_pos, matrix *eye_orient, const object *obj, bool // // returns: pointer to subsystem if one found, NULL otherwise #define MAX_SUBSYS_ATTACKERS 3 -ship_subsys *ship_get_best_subsys_to_attack(ship *sp, int subsys_type, const vec3d *attacker_pos) +ship_subsys *ship_get_best_subsys_to_attack(ship *sp, int subsys_type, const vec3d *attacker_pos, int wip_index = -1) { ship_subsys *ss; ship_subsys *best_in_sight_subsys, *lowest_attacker_subsys, *ss_return; @@ -15548,7 +15609,7 @@ ship_subsys *ship_get_best_subsys_to_attack(ship *sp, int subsys_type, const vec ss_return = best_in_sight_subsys = lowest_attacker_subsys = NULL; for (ss = GET_FIRST(&sp->subsys_list); ss != END_OF_LIST(&sp->subsys_list); ss = GET_NEXT(ss) ) { - if ( (ss->system_info->type == subsys_type) && (ss->current_hits > 0) ) { + if ( (ss->system_info->type == subsys_type) && (ss->current_hits > 0) && ( wip_index < 0 || turret_has_weapon(ss, wip_index)) ) { // get world pos of subsystem vm_vec_unrotate(&gsubpos, &ss->system_info->pnt, &Objects[sp->objnum].orient); @@ -15600,7 +15661,7 @@ ship_subsys *ship_get_best_subsys_to_attack(ship *sp, int subsys_type, const vec * to select the best subsystem to attack of that type (using line-of-sight) * and based on the number of ships already attacking the subsystem */ -ship_subsys *ship_find_first_subsys(ship *sp, int subsys_type, const vec3d *attacker_pos) +ship_subsys *ship_find_first_subsys(ship *sp, int subsys_type, const vec3d *attacker_pos, int wip_index) { Assertion(subsys_type > SUBSYSTEM_NONE && subsys_type < SUBSYSTEM_MAX, "Subsys_type %d must refer to a valid subsystem type!", subsys_type); @@ -15610,7 +15671,7 @@ ship_subsys *ship_find_first_subsys(ship *sp, int subsys_type, const vec3d *atta return nullptr; if ( attacker_pos != nullptr ) { - return ship_get_best_subsys_to_attack(sp, subsys_type, attacker_pos); + return ship_get_best_subsys_to_attack(sp, subsys_type, attacker_pos, wip_index); } else { // next, scan the list of subsystems and search for the first subsystem of the particular // type which has > 0 hits remaining. @@ -17433,7 +17494,7 @@ bool ship_subsystem_in_sight(const object *objp, const ship_subsys *subsys, cons * Find a subsystem matching 'type' inside the ship, and that is not destroyed. * @return If cannot find one, return NULL. */ -ship_subsys *ship_return_next_subsys(ship *shipp, int type, vec3d *attacker_pos) +ship_subsys *ship_return_next_subsys(ship *shipp, int type, vec3d *attacker_pos, int turret_wip_index) { ship_subsys *ssp; @@ -17444,7 +17505,7 @@ ship_subsys *ship_return_next_subsys(ship *shipp, int type, vec3d *attacker_pos) return NULL; // loop through all the subsystems, if we find a match that has some strength, return it - ssp = ship_get_best_subsys_to_attack(shipp, type, attacker_pos); + ssp = ship_get_best_subsys_to_attack(shipp, type, attacker_pos, turret_wip_index); return ssp; } @@ -17729,7 +17790,8 @@ static const char* ship_get_ai_target_display_name(int goal, const char* name) case AI_GOAL_EVADE_SHIP: case AI_GOAL_REARM_REPAIR: case AI_GOAL_FLY_TO_SHIP: - case AI_GOAL_DESTROY_SUBSYSTEM: { + case AI_GOAL_DESTROY_SUBSYSTEM: + case AI_GOAL_DESTROY_TURRET_TYPE_ON_SHIP: { auto ship = ship_name_lookup(name); if (ship < 0) { return name; @@ -17744,6 +17806,7 @@ static const char* ship_get_ai_target_display_name(int goal, const char* name) case AI_GOAL_GUARD_WING: case AI_GOAL_WAYPOINTS: case AI_GOAL_WAYPOINTS_ONCE: + case AI_GOAL_DESTROY_TURRET_TYPE: default: return name; } @@ -17819,6 +17882,7 @@ SCP_string ship_return_orders(ship* sp) case AI_GOAL_EVADE_SHIP: case AI_GOAL_REARM_REPAIR: case AI_GOAL_FLY_TO_SHIP: + case AI_GOAL_DESTROY_TURRET_TYPE_ON_SHIP: if (aigp->target_name) { outbuf += target_name; } else { @@ -17840,6 +17904,7 @@ SCP_string ship_return_orders(ship* sp) case AI_GOAL_WAYPOINTS: case AI_GOAL_WAYPOINTS_ONCE: + case AI_GOAL_DESTROY_TURRET_TYPE: case AI_GOAL_LUA: // don't do anything, all info is in order_text break; diff --git a/code/ship/ship.h b/code/ship/ship.h index 1d0c40112e1..ec3a9a6bf60 100644 --- a/code/ship/ship.h +++ b/code/ship/ship.h @@ -1901,13 +1901,15 @@ extern int ship_find_num_turrets(object *objp); extern void compute_slew_matrix(matrix *orient, angles *a); extern void object_get_eye(vec3d *eye_pos, matrix *eye_orient, const object *obj, bool do_slew = true, bool local_pos = false, bool local_orient = false); -extern ship_subsys *ship_find_first_subsys(ship *sp, int subsys_type, const vec3d *attacker_pos = nullptr); +extern ship_subsys *ship_find_first_subsys(ship *sp, int subsys_type, const vec3d *attacker_pos = nullptr, int wip_index = -1); extern ship_subsys *ship_get_indexed_subsys(ship *sp, int index); // returns index'th subsystem of this ship extern int ship_find_subsys(const ship *sp, const char *ss_name); // returns numerical index in linked list of subsystems extern int ship_get_subsys_index(const ship_subsys *subsys); extern bool ship_subsystems_blown(const ship *shipp, int type, bool skip_dying_check = false); extern float ship_get_subsystem_strength(const ship *shipp, int type, bool skip_dying_check = false, bool no_minimum_engine_str = false); +extern bool turret_get_weapon(ship_subsys *ssp, int wi_index); +extern float ship_get_turret_type_aggregate_hits(ship *shipp, int wi_index); extern ship_subsys *ship_get_subsys(const ship *shipp, const char *subsys_name); extern int ship_get_num_subsys(const ship *shipp); extern ship_subsys *ship_get_closest_subsys_in_sight(const ship *sp, int subsys_type, const vec3d *attacker_pos); @@ -1958,7 +1960,7 @@ bool ship_can_bay_depart(ship *sp); // checks to see if a ship has a departure int ship_return_subsys_path_normal(const ship *sp, const ship_subsys *ss, const vec3d *gsubpos, vec3d *norm); bool ship_subsystem_in_sight(const object *objp, const ship_subsys *subsys, const vec3d *eye_pos, const vec3d *subsys_pos, bool do_facing_check = true, float *dot_out = nullptr, vec3d *vec_out = nullptr); -ship_subsys *ship_return_next_subsys(ship *shipp, int type, vec3d *attacker_pos); +ship_subsys *ship_return_next_subsys(ship *shipp, int type, vec3d *attacker_pos, int turret_wip_index = -1); // defines and definition for function to get a random ship of a particular team (any ship, // any ship but player ships, or only players) diff --git a/fred2/management.cpp b/fred2/management.cpp index f0e65be50da..efdbda24bba 100644 --- a/fred2/management.cpp +++ b/fred2/management.cpp @@ -123,31 +123,33 @@ CCriticalSection CS_cur_object_index; // NOTE: Certain goals (Form On Wing, Rearm, Chase Weapon, Fly To Ship) aren't listed here. This may or may not be intentional, // but if they are added in the future, it will be necessary to verify correct functionality in the various FRED dialog functions. ai_goal_list Ai_goal_list[] = { - { "Waypoints", AI_GOAL_WAYPOINTS, 0 }, - { "Waypoints once", AI_GOAL_WAYPOINTS_ONCE, 0 }, - { "Attack", AI_GOAL_CHASE, 0 }, - { "Attack", AI_GOAL_CHASE_WING, 0 }, // duplicate needed because we can no longer use bitwise operators - { "Attack any ship", AI_GOAL_CHASE_ANY, 0 }, - { "Attack ship class", AI_GOAL_CHASE_SHIP_CLASS, 0 }, - { "Attack ship type", AI_GOAL_CHASE_SHIP_TYPE, 0 }, - { "Guard", AI_GOAL_GUARD, 0 }, - { "Guard", AI_GOAL_GUARD_WING, 0 }, // duplicate needed because we can no longer use bitwise operators - { "Disable ship", AI_GOAL_DISABLE_SHIP, 0 }, - { "Disable ship (tactical)",AI_GOAL_DISABLE_SHIP_TACTICAL, 0 }, - { "Disarm ship", AI_GOAL_DISARM_SHIP, 0 }, - { "Disarm ship (tactical)", AI_GOAL_DISARM_SHIP_TACTICAL, 0 }, - { "Destroy subsystem", AI_GOAL_DESTROY_SUBSYSTEM, 0 }, - { "Dock", AI_GOAL_DOCK, 0 }, - { "Undock", AI_GOAL_UNDOCK, 0 }, - { "Warp", AI_GOAL_WARP, 0 }, - { "Evade ship", AI_GOAL_EVADE_SHIP, 0 }, - { "Ignore ship", AI_GOAL_IGNORE, 0 }, - { "Ignore ship (new)", AI_GOAL_IGNORE_NEW, 0 }, - { "Stay near ship", AI_GOAL_STAY_NEAR_SHIP, 0 }, - { "Keep safe distance", AI_GOAL_KEEP_SAFE_DISTANCE, 0 }, - { "Stay still", AI_GOAL_STAY_STILL, 0 }, - { "Play dead", AI_GOAL_PLAY_DEAD, 0 }, - { "Play dead (persistent)", AI_GOAL_PLAY_DEAD_PERSISTENT, 0 } + { "Waypoints", AI_GOAL_WAYPOINTS, 0 }, + { "Waypoints once", AI_GOAL_WAYPOINTS_ONCE, 0 }, + { "Attack", AI_GOAL_CHASE, 0 }, + { "Attack", AI_GOAL_CHASE_WING, 0 }, // duplicate needed because we can no longer use bitwise operators + { "Attack any ship", AI_GOAL_CHASE_ANY, 0 }, + { "Attack ship class", AI_GOAL_CHASE_SHIP_CLASS, 0 }, + { "Attack ship type", AI_GOAL_CHASE_SHIP_TYPE, 0 }, + { "Guard", AI_GOAL_GUARD, 0 }, + { "Guard", AI_GOAL_GUARD_WING, 0 }, // duplicate needed because we can no longer use bitwise operators + { "Disable ship", AI_GOAL_DISABLE_SHIP, 0 }, + { "Disable ship (tactical)", AI_GOAL_DISABLE_SHIP_TACTICAL, 0 }, + { "Disarm ship", AI_GOAL_DISARM_SHIP, 0 }, + { "Disarm ship (tactical)", AI_GOAL_DISARM_SHIP_TACTICAL, 0 }, + { "Destroy subsystem", AI_GOAL_DESTROY_SUBSYSTEM, 0 }, + { "Dock", AI_GOAL_DOCK, 0 }, + { "Undock", AI_GOAL_UNDOCK, 0 }, + { "Warp", AI_GOAL_WARP, 0 }, + { "Evade ship", AI_GOAL_EVADE_SHIP, 0 }, + { "Ignore ship", AI_GOAL_IGNORE, 0 }, + { "Ignore ship (new)", AI_GOAL_IGNORE_NEW, 0 }, + { "Stay near ship", AI_GOAL_STAY_NEAR_SHIP, 0 }, + { "Keep safe distance", AI_GOAL_KEEP_SAFE_DISTANCE, 0 }, + { "Stay still", AI_GOAL_STAY_STILL, 0 }, + { "Play dead", AI_GOAL_PLAY_DEAD, 0 }, + { "Play dead (persistent)", AI_GOAL_PLAY_DEAD_PERSISTENT, 0 } + { "Destroy turret type", AI_GOAL_DESTROY_TURRET_TYPE, 0 }, + { "Destroy turret type on ship",AI_GOAL_DESTROY_TURRET_TYPE_ON_SHIP, 0 } }; int Ai_goal_list_size = sizeof(Ai_goal_list) / sizeof(ai_goal_list); diff --git a/qtfred/src/mission/Editor.cpp b/qtfred/src/mission/Editor.cpp index b4f147c9d1e..b67fb3242d8 100644 --- a/qtfred/src/mission/Editor.cpp +++ b/qtfred/src/mission/Editor.cpp @@ -76,30 +76,32 @@ std::pair query_referenced_in_ai_goals(sexp_ref_type type, const // NOTE: Certain goals (Form On Wing, Rearm, Chase Weapon, Fly To Ship) aren't listed here. This may or may not be intentional, // but if they are added in the future, it will be necessary to verify correct functionality in the various FRED dialog functions. ai_goal_list Ai_goal_list[] = { - { "Waypoints", AI_GOAL_WAYPOINTS, 0 }, - { "Waypoints once", AI_GOAL_WAYPOINTS_ONCE, 0 }, - { "Attack", AI_GOAL_CHASE, 0 }, - { "Attack", AI_GOAL_CHASE_WING, 0 }, // duplicate needed because we can no longer use bitwise operators - { "Attack any ship", AI_GOAL_CHASE_ANY, 0 }, - { "Attack ship class", AI_GOAL_CHASE_SHIP_CLASS, 0 }, - { "Guard", AI_GOAL_GUARD, 0 }, - { "Guard", AI_GOAL_GUARD_WING, 0 }, // duplicate needed because we can no longer use bitwise operators - { "Disable ship", AI_GOAL_DISABLE_SHIP, 0 }, - { "Disable ship (tactical)",AI_GOAL_DISABLE_SHIP_TACTICAL, 0 }, - { "Disarm ship", AI_GOAL_DISARM_SHIP, 0 }, - { "Disarm ship (tactical)", AI_GOAL_DISARM_SHIP_TACTICAL, 0 }, - { "Destroy subsystem", AI_GOAL_DESTROY_SUBSYSTEM, 0 }, - { "Dock", AI_GOAL_DOCK, 0 }, - { "Undock", AI_GOAL_UNDOCK, 0 }, - { "Warp", AI_GOAL_WARP, 0 }, - { "Evade ship", AI_GOAL_EVADE_SHIP, 0 }, - { "Ignore ship", AI_GOAL_IGNORE, 0 }, - { "Ignore ship (new)", AI_GOAL_IGNORE_NEW, 0 }, - { "Stay near ship", AI_GOAL_STAY_NEAR_SHIP, 0 }, - { "Keep safe distance", AI_GOAL_KEEP_SAFE_DISTANCE, 0 }, - { "Stay still", AI_GOAL_STAY_STILL, 0 }, - { "Play dead", AI_GOAL_PLAY_DEAD, 0 }, - { "Play dead (persistent)", AI_GOAL_PLAY_DEAD_PERSISTENT, 0 } + { "Waypoints", AI_GOAL_WAYPOINTS, 0 }, + { "Waypoints once", AI_GOAL_WAYPOINTS_ONCE, 0 }, + { "Attack", AI_GOAL_CHASE, 0 }, + { "Attack", AI_GOAL_CHASE_WING, 0 }, // duplicate needed because we can no longer use bitwise operators + { "Attack any ship", AI_GOAL_CHASE_ANY, 0 }, + { "Attack ship class", AI_GOAL_CHASE_SHIP_CLASS, 0 }, + { "Guard", AI_GOAL_GUARD, 0 }, + { "Guard", AI_GOAL_GUARD_WING, 0 }, // duplicate needed because we can no longer use bitwise operators + { "Disable ship", AI_GOAL_DISABLE_SHIP, 0 }, + { "Disable ship (tactical)", AI_GOAL_DISABLE_SHIP_TACTICAL, 0 }, + { "Disarm ship", AI_GOAL_DISARM_SHIP, 0 }, + { "Disarm ship (tactical)", AI_GOAL_DISARM_SHIP_TACTICAL, 0 }, + { "Destroy subsystem", AI_GOAL_DESTROY_SUBSYSTEM, 0 }, + { "Dock", AI_GOAL_DOCK, 0 }, + { "Undock", AI_GOAL_UNDOCK, 0 }, + { "Warp", AI_GOAL_WARP, 0 }, + { "Evade ship", AI_GOAL_EVADE_SHIP, 0 }, + { "Ignore ship", AI_GOAL_IGNORE, 0 }, + { "Ignore ship (new)", AI_GOAL_IGNORE_NEW, 0 }, + { "Stay near ship", AI_GOAL_STAY_NEAR_SHIP, 0 }, + { "Keep safe distance", AI_GOAL_KEEP_SAFE_DISTANCE, 0 }, + { "Stay still", AI_GOAL_STAY_STILL, 0 }, + { "Play dead", AI_GOAL_PLAY_DEAD, 0 }, + { "Play dead (persistent)", AI_GOAL_PLAY_DEAD_PERSISTENT, 0 } + { "Destroy turret type", AI_GOAL_DESTROY_TURRET_TYPE, 0 }, + { "Destroy turret type on ship",AI_GOAL_DESTROY_TURRET_TYPE_ON_SHIP, 0 } }; } diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipGoalsDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipGoalsDialogModel.cpp index dab14a652dd..c7244d471f4 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipGoalsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipGoalsDialogModel.cpp @@ -171,6 +171,8 @@ void ShipGoalsDialogModel::updateItem(int item) case AI_GOAL_FORM_ON_WING: case AI_GOAL_STAY_STILL: case AI_GOAL_CHASE_SHIP_CLASS: + case AI_GOAL_DESTROY_TURRET_TYPE: + case AI_GOAL_DESTROY_TURRET_TYPE_ON_SHIP: break; case AI_GOAL_DESTROY_SUBSYSTEM: { @@ -531,6 +533,8 @@ void ShipGoalsDialogModel::initialize(ai_goal* goals) case AI_GOAL_EVADE_SHIP: case AI_GOAL_STAY_NEAR_SHIP: case AI_GOAL_FORM_ON_WING: + case AI_GOAL_DESTROY_TURRET_TYPE: + case AI_GOAL_DESTROY_TURRET_TYPE_ON_SHIP: break; case AI_GOAL_WAYPOINTS: diff --git a/qtfred/src/ui/util/ErrorChecker.cpp b/qtfred/src/ui/util/ErrorChecker.cpp index e1ab1e68bd0..55b9a76304e 100644 --- a/qtfred/src/ui/util/ErrorChecker.cpp +++ b/qtfred/src/ui/util/ErrorChecker.cpp @@ -1293,6 +1293,7 @@ int ErrorChecker::checkInitialOrders(ai_goal* goals, int ship, int wing) { case AI_GOAL_PLAY_DEAD: case AI_GOAL_PLAY_DEAD_PERSISTENT: case AI_GOAL_WARP: + case AI_GOAL_DESTROY_TURRET_TYPE: flag = 0; break; @@ -1320,6 +1321,7 @@ int ErrorChecker::checkInitialOrders(ai_goal* goals, int ship, int wing) { case AI_GOAL_FORM_ON_WING: case AI_GOAL_IGNORE: case AI_GOAL_IGNORE_NEW: + case AI_GOAL_DESTROY_TURRET_TYPE_ON_SHIP: flag = 2; break; @@ -1501,6 +1503,7 @@ int ErrorChecker::checkInitialOrders(ai_goal* goals, int ship, int wing) { case AI_GOAL_DISARM_SHIP_TACTICAL: case AI_GOAL_DISABLE_SHIP: case AI_GOAL_DISABLE_SHIP_TACTICAL: + case AI_GOAL_DESTROY_TURRET_TYPE_ON_SHIP: if (team == team2) potential("Initial orders error for %s \"%s\"\n\n%s assigned to attack same team", entity, source, ship >= 0 ? "Ship" : "Wing"); From 24c8449944aa34f5b66aa2fd53f67f8a1f1520f1 Mon Sep 17 00:00:00 2001 From: Kestrellius <902X@comcast.net> Date: Sun, 9 Aug 2026 07:54:53 -0700 Subject: [PATCH 2/4] bugfixing --- qtfred/src/mission/Editor.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qtfred/src/mission/Editor.cpp b/qtfred/src/mission/Editor.cpp index b67fb3242d8..c7e419a9307 100644 --- a/qtfred/src/mission/Editor.cpp +++ b/qtfred/src/mission/Editor.cpp @@ -99,9 +99,9 @@ ai_goal_list Ai_goal_list[] = { { "Keep safe distance", AI_GOAL_KEEP_SAFE_DISTANCE, 0 }, { "Stay still", AI_GOAL_STAY_STILL, 0 }, { "Play dead", AI_GOAL_PLAY_DEAD, 0 }, - { "Play dead (persistent)", AI_GOAL_PLAY_DEAD_PERSISTENT, 0 } + { "Play dead (persistent)", AI_GOAL_PLAY_DEAD_PERSISTENT, 0 }, { "Destroy turret type", AI_GOAL_DESTROY_TURRET_TYPE, 0 }, - { "Destroy turret type on ship",AI_GOAL_DESTROY_TURRET_TYPE_ON_SHIP, 0 } + { "Destroy turret type on ship",AI_GOAL_DESTROY_TURRET_TYPE_ON_SHIP, 0 }, }; } From d4c91b1e4f7de2b8bcafa07cc7ca91c3ad14a4c6 Mon Sep 17 00:00:00 2001 From: Kestrellius <902X@comcast.net> Date: Sun, 9 Aug 2026 08:19:11 -0700 Subject: [PATCH 3/4] bugfixing --- fred2/management.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fred2/management.cpp b/fred2/management.cpp index efdbda24bba..221748d67ce 100644 --- a/fred2/management.cpp +++ b/fred2/management.cpp @@ -147,9 +147,9 @@ ai_goal_list Ai_goal_list[] = { { "Keep safe distance", AI_GOAL_KEEP_SAFE_DISTANCE, 0 }, { "Stay still", AI_GOAL_STAY_STILL, 0 }, { "Play dead", AI_GOAL_PLAY_DEAD, 0 }, - { "Play dead (persistent)", AI_GOAL_PLAY_DEAD_PERSISTENT, 0 } + { "Play dead (persistent)", AI_GOAL_PLAY_DEAD_PERSISTENT, 0 }, { "Destroy turret type", AI_GOAL_DESTROY_TURRET_TYPE, 0 }, - { "Destroy turret type on ship",AI_GOAL_DESTROY_TURRET_TYPE_ON_SHIP, 0 } + { "Destroy turret type on ship",AI_GOAL_DESTROY_TURRET_TYPE_ON_SHIP, 0 }, }; int Ai_goal_list_size = sizeof(Ai_goal_list) / sizeof(ai_goal_list); From 8cb163b7cd3ebd4e49eac5aa806a3c2ed222490f Mon Sep 17 00:00:00 2001 From: Kestrellius <902X@comcast.net> Date: Mon, 17 Aug 2026 14:53:28 -0700 Subject: [PATCH 4/4] address feedback --- code/ai/aigoals.cpp | 37 ++++++++++++++++++++++++++++++------- code/parse/sexp.cpp | 6 +++--- code/ship/ship.h | 2 +- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/code/ai/aigoals.cpp b/code/ai/aigoals.cpp index 551207e14f8..3f287a9c0bc 100644 --- a/code/ai/aigoals.cpp +++ b/code/ai/aigoals.cpp @@ -1557,11 +1557,11 @@ int ai_remove_goal_sexp_sub( int sexp, ai_goal* aigp, bool &remove_more ) break; case OP_AI_DESTROY_TURRET_TYPE: priority = eval_priority_et_seq(CDDR(node)); - goalmode = AI_GOAL_REARM_REPAIR; + goalmode = AI_GOAL_DESTROY_TURRET_TYPE; break; case OP_AI_DESTROY_TURRET_TYPE_ON_SHIP: priority = eval_priority_et_seq(CDDDR(node)); - goalmode = AI_GOAL_REARM_REPAIR; + goalmode = AI_GOAL_DESTROY_TURRET_TYPE_ON_SHIP; break; default: const ai_mode_lua* luaAIMode = ai_lua_find_mode(op); @@ -1865,7 +1865,7 @@ ai_achievability ai_mission_goal_achievable( int objnum, ai_goal *aigp ) if (aigp->ai_mode == AI_GOAL_DESTROY_TURRET_TYPE) { for (auto so : list_range(&Ship_obj_list)) { auto type_objp = &Objects[so->objnum]; - if (type_objp->type != OBJ_SHIP || type_objp->flags[Object::Object_Flags::Should_be_dead] || aigp->int_data < 0 || aigp->int_data >= sz2i(Weapon_info.size())) + if (type_objp->type != OBJ_SHIP || type_objp->flags[Object::Object_Flags::Should_be_dead] || !Weapon_info.in_bounds(aigp->int_data)) continue; ship *potential_target_shipp = &Ships[type_objp->instance]; if (ship_get_turret_type_aggregate_hits(potential_target_shipp, aigp->int_data) > 0.0f) { @@ -1956,7 +1956,7 @@ ai_achievability ai_mission_goal_achievable( int objnum, ai_goal *aigp ) // destroyed when shipnum is valid // can't determine the status of this goal if ship not valid - if (!target_ship_entry || !target_ship_entry->has_shipp() || aigp->int_data < 0 || aigp->int_data >= sz2i(Weapon_info.size())) { + if (!target_ship_entry || !target_ship_entry->has_shipp() || !Weapon_info.in_bounds(aigp->int_data)) { status = 0; } else { status = (ship_get_turret_type_aggregate_hits(target_ship_entry->shipp(), aigp->int_data) <= 0.0f) ? 1 : 0; @@ -2100,7 +2100,7 @@ ai_achievability ai_mission_goal_achievable( int objnum, ai_goal *aigp ) } // Goober5000 - before doing anything else, check if this is a disarm goal for an arrived ship... - if ((status == SHIP_STATUS_ARRIVED) && (aigp->ai_mode == AI_GOAL_DISARM_SHIP || aigp->ai_mode == AI_GOAL_DISARM_SHIP_TACTICAL || aigp->ai_mode == AI_GOAL_DESTROY_TURRET_TYPE_ON_SHIP)) + if ((status == SHIP_STATUS_ARRIVED) && (aigp->ai_mode == AI_GOAL_DISARM_SHIP || aigp->ai_mode == AI_GOAL_DISARM_SHIP_TACTICAL)) { if (target_ship_entry && target_ship_entry->has_shipp()) { // if the ship has no turrets, we can't disarm it! @@ -2112,6 +2112,29 @@ ai_achievability ai_mission_goal_achievable( int objnum, ai_goal *aigp ) } } + if ((status == SHIP_STATUS_ARRIVED) && aigp->ai_mode == AI_GOAL_DESTROY_TURRET_TYPE_ON_SHIP) { + if (target_ship_entry && target_ship_entry->has_shipp()) { + // if the ship has no turrets of the relevant type, we can't destroy them! + ship_subsys *ssp = GET_FIRST(&target_ship_entry->shipp()->subsys_list); + bool weapon_found = false; + if (target_ship_entry->shipp()->subsys_info[SUBSYSTEM_TURRET].type_count > 0) { + while ( ssp != END_OF_LIST( &shipp->subsys_list ) ) { + if (turret_has_weapon(ssp, aigp->int_data)) { + weapon_found = true; + break; + } + ssp = GET_NEXT( ssp ); + } + } + if (!weapon_found) { + return ai_achievability::NOT_ACHIEVABLE; + } + } else { + Assertion(false, "Target name %s is not an arrived ship!", aigp->target_name); + return ai_achievability::NOT_ACHIEVABLE; // force this goal to be invalid + } + } + // if the goal is an ignore/disable/disarm goal, then // Goober5000 - see note at PURGE_GOALS_ALL_SHIPS... this is bizarre if ((status == SHIP_STATUS_ARRIVED) && !(aigp->flags[AI::Goal_Flags::Goals_purged])) @@ -2733,7 +2756,7 @@ void ai_process_mission_orders( int objnum, ai_info *aip ) Assertion(current_goal->int_data >= 0, "The target of AI_GOAL_DESTROY_TURRET_TYPE_ON_SHIP must refer to a valid weapon class!"); other_obj = current_goal_target_ship->objp(); ai_attack_object( objp, other_obj); - ai_set_attack_subsystem( objp, 0, current_goal->int_data ); + ai_set_attack_subsystem( objp, SUBSYSTEM_TURRET, current_goal->int_data ); break; } @@ -2750,7 +2773,7 @@ void ai_process_mission_orders( int objnum, ai_info *aip ) case AI_GOAL_DESTROY_TURRET_TYPE: Assertion(current_goal->int_data >= 0, "The target of AI_GOAL_DESTROY_TURRET_TYPE must refer to a valid weapon class!"); ai_attack_object( objp, nullptr, -1, -1, current_goal->int_data); - ai_set_attack_subsystem( objp, 0, current_goal->int_data ); + ai_set_attack_subsystem( objp, SUBSYSTEM_TURRET, current_goal->int_data ); break; // chase-ship-class is chase-any but restricted to a subset of ships diff --git a/code/parse/sexp.cpp b/code/parse/sexp.cpp index a276848b35d..699f2f84ab8 100644 --- a/code/parse/sexp.cpp +++ b/code/parse/sexp.cpp @@ -950,7 +950,7 @@ sexp_ai_goal_link Sexp_ai_goal_links[] = { { AI_GOAL_FLY_TO_SHIP, OP_AI_FLY_TO_SHIP }, { AI_GOAL_REARM_REPAIR, OP_AI_REARM_REPAIR }, { AI_GOAL_DESTROY_TURRET_TYPE, OP_AI_DESTROY_TURRET_TYPE }, - { AI_GOAL_DESTROY_TURRET_TYPE, OP_AI_DESTROY_TURRET_TYPE_ON_SHIP }, + { AI_GOAL_DESTROY_TURRET_TYPE_ON_SHIP, OP_AI_DESTROY_TURRET_TYPE_ON_SHIP }, }; SCP_vector Dynamic_enums; @@ -39996,7 +39996,7 @@ SCP_vector Sexp_help = { }, { OP_AI_DESTROY_TURRET_TYPE, "Ai-destroy-turret-type (Ship/wing goal)\r\n" - "\tThis AI goal causes a ship/wing to destroy all enemy turrets in the mission that" + "\tThis AI goal causes a ship/wing to destroy all enemy turrets in the mission that " "are carrying a specified weapon." "Takes 2 arguments...\r\n" "\t1:\tName of weapon (primary or secondary) that should be attacked\r\n" @@ -40005,7 +40005,7 @@ SCP_vector Sexp_help = { }, { OP_AI_DESTROY_TURRET_TYPE_ON_SHIP, "Ai-destroy-turret-type-on-ship (Ship/wing goal)\r\n" - "\tThis AI goal causes a ship/wing to destroy all turrets on a specified ship that" + "\tThis AI goal causes a ship/wing to destroy all turrets on a specified ship that " "are carrying a specified weapon." "Takes 2 arguments...\r\n" "\t1:\tName of weapon (primary or secondary) that should be attacked\r\n" diff --git a/code/ship/ship.h b/code/ship/ship.h index ec3a9a6bf60..6ab8f45eed9 100644 --- a/code/ship/ship.h +++ b/code/ship/ship.h @@ -1908,7 +1908,7 @@ extern int ship_get_subsys_index(const ship_subsys *subsys); extern bool ship_subsystems_blown(const ship *shipp, int type, bool skip_dying_check = false); extern float ship_get_subsystem_strength(const ship *shipp, int type, bool skip_dying_check = false, bool no_minimum_engine_str = false); -extern bool turret_get_weapon(ship_subsys *ssp, int wi_index); +extern bool turret_has_weapon(ship_subsys *ssp, int wi_index); extern float ship_get_turret_type_aggregate_hits(ship *shipp, int wi_index); extern ship_subsys *ship_get_subsys(const ship *shipp, const char *subsys_name); extern int ship_get_num_subsys(const ship *shipp);