diff --git a/code/ai/ai.h b/code/ai/ai.h index b74f7b43527..8cc85a256eb 100644 --- a/code/ai/ai.h +++ b/code/ai/ai.h @@ -21,6 +21,7 @@ #include "parse/sexp.h" #include "physics/physics.h" #include "ship/ship_flags.h" +#include "utils/RandomRange.h" class ship_weapon; class ship_subsys; @@ -142,6 +143,12 @@ typedef struct ai_class { float ai_secondary_range_mult[NUM_SKILL_LEVELS]; bool ai_class_autoscale; //Defaults to true, but can be turned off in order to disable extra scaling of some AI behaviors //based on AI class index + ::util::ParsedRandomFloatRange primary_select_delay[NUM_SKILL_LEVELS]; + ::util::ParsedRandomFloatRange primary_select_delay_on_change[NUM_SKILL_LEVELS]; + ::util::ParsedRandomFloatRange primary_selection_random_factor[NUM_SKILL_LEVELS]; + float primary_selection_oneshot_modifier[NUM_SKILL_LEVELS]; + float primary_selection_status_quo_bias[NUM_SKILL_LEVELS]; + } ai_class; // Submode definitions. @@ -340,6 +347,7 @@ typedef struct ai_info { float prev_dot_to_goal; // dot of fvec to goal last frame, used to see if making progress towards goal. vec3d goal_point; // Used in AIM_SAFETY, AIM_STILL and in circling. vec3d prev_goal_point; // Previous location of goal point, used at least for evading. + bool enemy_shield_is_down; //Values copied from the AI class float ai_accuracy, ai_evasion, ai_courage, ai_patience; @@ -373,6 +381,11 @@ typedef struct ai_info { int ai_chance_to_use_missiles_on_plr; float ai_max_aim_update_delay; float ai_turret_max_aim_update_delay; + ::util::ParsedRandomFloatRange primary_select_delay; + ::util::ParsedRandomFloatRange primary_select_delay_on_change; + ::util::ParsedRandomFloatRange primary_selection_random_factor; + float primary_selection_oneshot_modifier; + float primary_selection_status_quo_bias; flagset ai_profile_flags; //Holds AI_Profiles flags (possibly overriden by AI class) that actually apply to AI ship_subsys* targeted_subsys; // Targeted subobject on current target. NULL if none; diff --git a/code/ai/ai_flags.h b/code/ai/ai_flags.h index 427ec5f8395..41c6d0c8877 100644 --- a/code/ai/ai_flags.h +++ b/code/ai/ai_flags.h @@ -127,6 +127,7 @@ namespace AI { Require_turret_to_have_target_in_fov, Shockwaves_damage_small_ship_subsystems, Smart_afterburner_management, + Configurable_primary_weapon_selection, Smart_primary_weapon_selection, Smart_secondary_weapon_selection, Smart_shield_management, diff --git a/code/ai/ai_profiles.cpp b/code/ai/ai_profiles.cpp index d65ba45ecfc..d817cd294ad 100644 --- a/code/ai/ai_profiles.cpp +++ b/code/ai/ai_profiles.cpp @@ -202,6 +202,18 @@ void parse_ai_profiles_tbl(const char *filename) if (optional_string("$AI In Range Time:")) parse_float_list(profile->in_range_time, NUM_SKILL_LEVELS); + if (optional_string("$Primary select delay:")) { + for (auto& entry : profile->primary_select_delay) { + entry = ::util::ParsedRandomFloatRange::parseRandomRange(); + } + } + + if (optional_string("$Primary select delay on target change:")) { + for (auto& entry : profile->primary_select_delay_on_change) { + entry = ::util::ParsedRandomFloatRange::parseRandomRange(); + } + } + if (optional_string("$AI Always Links Ammo Weapons:")) parse_float_list(profile->link_ammo_levels_always, NUM_SKILL_LEVELS); @@ -382,6 +394,8 @@ void parse_ai_profiles_tbl(const char *filename) set_flag(profile, "$big ships can attack beam turrets on untargeted ships:", AI::Profile_Flags::Big_ships_can_attack_beam_turrets_on_untargeted_ships); + set_flag(profile, "$configurable primary weapon selection:", AI::Profile_Flags::Configurable_primary_weapon_selection); + set_flag(profile, "$smart primary weapon selection:", AI::Profile_Flags::Smart_primary_weapon_selection); set_flag(profile, "$smart secondary weapon selection:", AI::Profile_Flags::Smart_secondary_weapon_selection); @@ -920,6 +934,9 @@ void ai_profile_t::reset() delay_bomb_arm_timer[i] = 0; chance_to_use_missiles_on_plr[i] = 0; player_autoaim_fov[i] = 0; + + primary_select_delay[i] = ::util::UniformFloatRange(5.0f); + primary_select_delay_on_change[i] = ::util::UniformFloatRange(9999.0f); } for (int i = 0; i <= MAX_DETAIL_VALUE; ++i) { diff --git a/code/ai/ai_profiles.h b/code/ai/ai_profiles.h index 9f03bce4782..efcd062a2f4 100644 --- a/code/ai/ai_profiles.h +++ b/code/ai/ai_profiles.h @@ -13,6 +13,7 @@ #include "globalincs/pstypes.h" #include "globalincs/systemvars.h" #include "ai/ai_flags.h" +#include "utils/RandomRange.h" // AI Path types #define AI_PATH_MODE_NORMAL 0 @@ -69,6 +70,8 @@ class ai_profile_t { float stalemate_dist_thresh[NUM_SKILL_LEVELS]; // SUSHI: The maximum distance the AI and target must be within for a stalemate float max_aim_update_delay[NUM_SKILL_LEVELS]; // SUSHI: The maximum delay before the AI updates their aim against small ships float turret_max_aim_update_delay[NUM_SKILL_LEVELS]; // SUSHI: As above, but for turrets updating their aim + ::util::ParsedRandomFloatRange primary_select_delay[NUM_SKILL_LEVELS]; // Time in seconds before next time the AI updates its primary weapon choice + ::util::ParsedRandomFloatRange primary_select_delay_on_change[NUM_SKILL_LEVELS]; // Same, but for special updates taking place when the target ship or subsystem changes, or target's shields change // Multiplicative delay factors for increasing skill levels. float ship_fire_delay_scale_hostile[NUM_SKILL_LEVELS]; diff --git a/code/ai/aicode.cpp b/code/ai/aicode.cpp index 9136144dae1..7a332fa76b2 100644 --- a/code/ai/aicode.cpp +++ b/code/ai/aicode.cpp @@ -677,6 +677,11 @@ void init_ai_class(ai_class *aicp) aicp->ai_evasion[i] = FLT_MIN; aicp->ai_courage[i] = FLT_MIN; aicp->ai_patience[i] = FLT_MIN; + aicp->primary_select_delay[i] = ::util::UniformFloatRange(FLT_MIN); + aicp->primary_select_delay_on_change[i] = ::util::UniformFloatRange(FLT_MIN); + aicp->primary_selection_random_factor[i] = ::util::UniformFloatRange(1.0f); + aicp->primary_selection_oneshot_modifier[i] = 2.0f; + aicp->primary_selection_status_quo_bias[i] = 1.0f; } aicp->ai_profile_flags.reset(); aicp->ai_profile_flags_set.reset(); @@ -796,14 +801,38 @@ void parse_ai_class() if (optional_string("$Autoscale by AI Class Index:")) stuff_boolean(&aicp->ai_class_autoscale); - + //Parse optional values for stuff imported from ai_profiles if (optional_string("$AI Countermeasure Firing Chance:")) parse_float_list(aicp->ai_cmeasure_fire_chance, NUM_SKILL_LEVELS); - + if (optional_string("$AI In Range Time:")) parse_float_list(aicp->ai_in_range_time, NUM_SKILL_LEVELS); + + if (optional_string("$Primary select delay:")) { + for (auto& entry : aicp->primary_select_delay) { + entry = ::util::ParsedRandomFloatRange::parseRandomRange(); + } + } + + if (optional_string("$Primary select delay on target change:")) { + for (auto& entry : aicp->primary_select_delay_on_change) { + entry = ::util::ParsedRandomFloatRange::parseRandomRange(); + } + } + if (optional_string("$Primary selection random factor:")) { + for (auto& entry : aicp->primary_selection_random_factor) { + entry = ::util::ParsedRandomFloatRange::parseRandomRange(); + } + } + + if (optional_string("$Primary selection oneshot modifier:")) + parse_float_list(aicp->primary_selection_oneshot_modifier, NUM_SKILL_LEVELS); + + if (optional_string("$Primary selection status quo bias:")) + parse_float_list(aicp->primary_selection_status_quo_bias, NUM_SKILL_LEVELS); + if (optional_string("$AI Always Links Ammo Weapons:")) parse_float_list(aicp->ai_link_ammo_levels_always, NUM_SKILL_LEVELS); @@ -907,6 +936,8 @@ void parse_ai_class() parse_float_list(aicp->ai_turret_max_aim_update_delay, NUM_SKILL_LEVELS); set_aic_flag(aicp, "$big ships can attack beam turrets on untargeted ships:", AI::Profile_Flags::Big_ships_can_attack_beam_turrets_on_untargeted_ships); + + set_aic_flag(aicp, "$configurable primary weapon selection:", AI::Profile_Flags::Configurable_primary_weapon_selection); set_aic_flag(aicp, "$smart primary weapon selection:", AI::Profile_Flags::Smart_primary_weapon_selection); @@ -1543,6 +1574,10 @@ vec3d ai_get_acc_limit(vec3d* vel_limit, const object* objp) { } +int ai_select_primary_weapon(object *objp, object *other_objp, Weapon::Info_Flags flags); + +int ai_select_primary_weapon_configurable(object *objp, object *other_objp, Weapon::Info_Flags flags, float relevant_shields_left); + // Set aip->target_objnum to objnum // Update aip->previous_target_objnum. // If new target (objnum) is different than old target, reset target_time. @@ -1575,13 +1610,16 @@ int set_target_objnum(ai_info *aip, int objnum) aip->target_signature = (objnum >= 0) ? Objects[objnum].signature : -1; // clear targeted subsystem set_targeted_subsys(aip, NULL, -1); + int candidate_timestamp = timestamp(std::max(fl2i(aip->primary_select_delay_on_change.next() * i2fl(MILLISECONDS_PER_SECOND)), 0)); + // we don't want to bother checking for an early selection if the ai's going to do it sooner anyway + if (timestamp_until(aip->primary_select_timestamp) > timestamp_until(candidate_timestamp)) { + aip->primary_select_timestamp = candidate_timestamp; + } } return aip->target_objnum; } -int ai_select_primary_weapon(object *objp, object *other_objp, Weapon::Info_Flags flags); - /** * Make new_subsys the targeted subsystem of ship *aip. */ @@ -1598,9 +1636,15 @@ ship_subsys *set_targeted_subsys(ai_info *aip, ship_subsys *new_subsys, int pare if (new_subsys->system_info->type == SUBSYSTEM_ENGINE) { if ( aip != Player_ai ) { Assert( aip->shipnum >= 0 ); - ai_select_primary_weapon(&Objects[Ships[aip->shipnum].objnum], &Objects[parent_objnum], Weapon::Info_Flags::Puncture); + ai_select_primary_weapon_configurable(&Objects[Ships[aip->shipnum].objnum], &Objects[parent_objnum], Weapon::Info_Flags::Puncture, -1.0f); ship_primary_changed(&Ships[aip->shipnum]); // AL: maybe send multiplayer information when AI ship changes primaries } + } else { + int candidate_timestamp = timestamp(std::max(fl2i(aip->primary_select_delay_on_change.next() * i2fl(MILLISECONDS_PER_SECOND)), 0)); + // we don't want to bother checking for an early selection if the ai's going to do it sooner anyway + if (timestamp_until(aip->primary_select_timestamp) > timestamp_until(candidate_timestamp)) { + aip->primary_select_timestamp = candidate_timestamp; + } } if ( aip == Player_ai ) { @@ -1615,7 +1659,7 @@ ship_subsys *set_targeted_subsys(ai_info *aip, ship_subsys *new_subsys, int pare } return aip->targeted_subsys; -} +} /** * Called to init the data for single ai object. @@ -5722,26 +5766,8 @@ static int ai_select_primary_weapon_OLD(const object *objp, Weapon::Info_Flags f return swp->current_primary_bank; } -// If: -// flags == Weapon::Info_Flags::Puncture -// Then Select a Puncture weapon. -// Else -// Select Any ol' weapon. -// Returns primary_bank index. -/** - * Etc. Etc. This is like the 4th rewrite of the code here. Special thanks to Bobboau - * for finding the get_shield_strength function. - * - * The AI will now intelligently choose the best weapon to use based on the overall shield - * status of the target. - */ -int ai_select_primary_weapon(object *objp, object *other_objp, Weapon::Info_Flags flags) +std::optional select_primary_setup(ship *shipp, ship *target_shipp, ship_weapon *swp, object *other_objp) { - // Pointer Set Up - ship *shipp = &Ships[objp->instance]; - ship *other_shipp = nullptr; - ship_weapon *swp = &shipp->weapons; - // Debugging if (other_objp==NULL) { @@ -5756,18 +5782,18 @@ int ai_select_primary_weapon(object *objp, object *other_objp, Weapon::Info_Flag if (swp->num_primary_banks <= 0) return -1; - bool other_is_ship = (other_objp->type == OBJ_SHIP); + // if we only have one bank to choose from, just short-circuit + if (swp->num_primary_banks == 1 && swp->current_primary_bank == 0) + return swp->current_primary_bank; - if (other_is_ship) + if (target_shipp) { - other_shipp = &Ships[other_objp->instance]; - //if the good-primary-time sexp has been used, return that weapon immediately //if smart primary weapon selection isn't set, turning this override behavior off might not do anything //however this seems acceptable for (const auto &ppi : Preferred_primary_info) { - if (ppi.subject.matches(shipp) && ppi.target.matches(other_shipp)) + if (ppi.subject.matches(shipp) && ppi.target.matches(target_shipp)) { int weapon_idx = ppi.weapon_index; @@ -5795,6 +5821,37 @@ int ai_select_primary_weapon(object *objp, object *other_objp, Weapon::Info_Flag // we're not prioritizing a specific primary weapon shipp->flags.remove(Ship::Ship_Flags::Force_primary_unlinking); + return std::nullopt; +} + +// If: +// flags == Weapon::Info_Flags::Puncture +// Then Select a Puncture weapon. +// Else +// Select Any ol' weapon. +// Returns primary_bank index. +/** + * Etc. Etc. This is like the 4th rewrite of the code here. Special thanks to Bobboau + * for finding the get_shield_strength function. + * + * The AI will now intelligently choose the best weapon to use based on the overall shield + * status of the target. + */ +int ai_select_primary_weapon(object *objp, object *other_objp, Weapon::Info_Flags flags) +{ + // Pointer Set Up + ship *shipp = &Ships[objp->instance]; + ship *other_shipp = nullptr; + ship_weapon *swp = &shipp->weapons; + + if (other_objp->type == OBJ_SHIP) { + other_shipp = &Ships[other_objp->instance]; + } + + auto early_return_value = select_primary_setup(shipp, other_shipp, swp, other_objp); + if (early_return_value.has_value()) { + return early_return_value.value(); + } //not using the new AI, use the old version of this function instead. if (!(Ai_info[shipp->ai_index].ai_profile_flags[AI::Profile_Flags::Smart_primary_weapon_selection])) @@ -5833,7 +5890,7 @@ int ai_select_primary_weapon(object *objp, object *other_objp, Weapon::Info_Flag float enemy_remaining_shield = get_shield_pct(other_objp); - if ( other_is_ship ) + if ( other_shipp ) { ship_info* other_sip = &Ship_info[other_shipp->ship_info_index]; @@ -5891,7 +5948,7 @@ int ai_select_primary_weapon(object *objp, object *other_objp, Weapon::Info_Flag return -1; } swp->current_primary_bank = i_hullfactor_prev_bank; // Select the best weapon - nprintf(("AI", "%i: Ship %s selecting weapon %s (no shields) vs target %s\n", Framecount, shipp->ship_name, Weapon_info[swp->primary_bank_weapons[i_hullfactor_prev_bank]].name, (other_is_ship ? other_shipp->ship_name : "non-ship") )); + nprintf(("AI", "%i: Ship %s selecting weapon %s (no shields) vs target %s\n", Framecount, shipp->ship_name, Weapon_info[swp->primary_bank_weapons[i_hullfactor_prev_bank]].name, (other_shipp ? other_shipp->ship_name : "non-ship") )); return i_hullfactor_prev_bank; // Return } @@ -5990,6 +6047,223 @@ int ai_select_primary_weapon(object *objp, object *other_objp, Weapon::Info_Flag } } +// More comprehensive version of primary selection, with high modder control. +int ai_select_primary_weapon_configurable(object *objp, object *other_objp, Weapon::Info_Flags flags, float relevant_shields_left = -1.0f) +{ + ship *shipp = &Ships[objp->instance]; + ship *target_shipp = nullptr; + ship_weapon *swp = &shipp->weapons; + ship_info *sinfop = &Ship_info[shipp->ship_info_index]; + + if (other_objp->type == OBJ_SHIP) { + target_shipp = &Ships[other_objp->instance]; + } + + //if we're not using configurable, use the previous version of this function instead. + if (!(Ai_info[shipp->ai_index].ai_profile_flags[AI::Profile_Flags::Configurable_primary_weapon_selection])) + { + return ai_select_primary_weapon(objp, other_objp, flags); + } + + auto early_return_value = select_primary_setup(shipp, target_shipp, swp, other_objp); + if (early_return_value.has_value()) { + return early_return_value.value(); + } + + weapon_info *wip; + shockwave_create_info *sci; + bool has_shockwave; + bool is_beam; + + ship_subsys *target_subsys = nullptr; + if (Ai_info[shipp->ai_index].targeted_subsys) { + target_subsys = Ai_info[shipp->ai_index].targeted_subsys; + } + int relevant_armor_type_idx = -1; + int relevant_ship_type_idx = -1; + int relevant_ship_class_idx = -1; + int relevant_weapon_class_idx = -1; + + if (relevant_shields_left < 0.0f) { + vec3d ship_local_pos = objp->pos; + vm_vec_sub2(&ship_local_pos, &other_objp->pos); + vm_vec_rotate(&ship_local_pos, &ship_local_pos, &other_objp->orient); + int relevant_quadrant = get_quadrant(&ship_local_pos, other_objp); + if (relevant_quadrant >= 0 && relevant_quadrant < sz2i(other_objp->shield_quadrant.size())) { + relevant_shields_left = shield_get_quad(other_objp, relevant_quadrant) - ship_shield_hitpoint_threshold(other_objp, false); + } + } + + SCP_unordered_map weapon_values = {}; + + for (int i = 0; i < swp->num_primary_banks; i++) { + int wip_i = swp->primary_bank_weapons[i]; + if (wip_i < 0) { + continue; + } + wip = &Weapon_info[wip_i]; + sci = &wip->shockwave; + has_shockwave = (sci->inner_rad != 0.0f || sci->outer_rad != 0.0f); + is_beam = wip->wi_flags[Weapon::Info_Flags::Beam]; + float dph = wip->damage; // damage per hit + float damage_scale = -1.0f; + float shockwave_damage = 0.0f; + if (has_shockwave) { + shockwave_damage = sci->damage; + } + + if (target_shipp != nullptr) { + relevant_ship_type_idx = Ship_info[target_shipp->ship_info_index].class_type; + relevant_ship_class_idx = target_shipp->ship_info_index; + if (relevant_shields_left > 0.0f) { + damage_scale = weapon_get_damage_scale(wip, nullptr, other_objp); + dph *= damage_scale; + relevant_armor_type_idx = target_shipp->shield_armor_type_idx; + if (relevant_armor_type_idx >= 0) { + dph = Armor_types[relevant_armor_type_idx].GetDamage(dph, wip->damage_type_idx, 1.0, is_beam); + } + if (!is_beam || Beams_use_damage_factors) { + dph *= wip->shield_factor; + } + } else if (target_subsys != nullptr) { + if (!is_beam || Beams_use_damage_factors) { + if (target_subsys->flags[Ship::Subsystem_Flags::Damage_as_hull]) { + dph *= wip->armor_factor; + } else { + dph *= wip->subsystem_factor; + } + } + relevant_armor_type_idx = target_subsys->armor_type_idx; + if (relevant_armor_type_idx >= 0) { + dph = Armor_types[relevant_armor_type_idx].GetDamage(dph, wip->damage_type_idx, 1.0, is_beam); + } + } else { + if (damage_scale == -1.0f) { + damage_scale = weapon_get_damage_scale(wip, nullptr, other_objp); + } + dph *= damage_scale; + relevant_armor_type_idx = target_shipp->armor_type_idx; + if (relevant_armor_type_idx >= 0) { + dph = Armor_types[relevant_armor_type_idx].GetDamage(dph, wip->damage_type_idx, 1.0, is_beam); + } + if (!is_beam || Beams_use_damage_factors) { + if (wip->wi_flags[Weapon::Info_Flags::Puncture]) { + dph /= 4; + } + dph *= wip->armor_factor; + } + } + if (has_shockwave) { + shockwave_damage = sci->damage; + if (Weapon_shockwaves_respect_huge || sci->speed <= 0.0f) { + if (damage_scale == -1.0f) { + damage_scale = weapon_get_damage_scale(wip, nullptr, other_objp); + } + shockwave_damage *= damage_scale; + } + if (relevant_armor_type_idx >= 0) { + shockwave_damage = Armor_types[relevant_armor_type_idx].GetDamage(shockwave_damage, sci->damage_type_idx, 1.0, is_beam); + } + } + } else if ( other_objp->type == OBJ_WEAPON ) { + weapon *target_weapon = &Weapons[other_objp->instance]; + relevant_armor_type_idx = Weapon_info[target_weapon->weapon_info_index].armor_type_idx; + relevant_weapon_class_idx = target_weapon->weapon_info_index; + dph = Armor_types[relevant_armor_type_idx].GetDamage(dph, wip->damage_type_idx, 1.0, is_beam); + if (has_shockwave) { + shockwave_damage = Armor_types[relevant_armor_type_idx].GetDamage(dph, sci->damage_type_idx, 1.0, is_beam); + if (sci->speed <= 0.0f) { + shockwave_damage *= weapon_get_damage_scale(wip, nullptr, other_objp); + } + } + } else { + if (has_shockwave && sci->speed <= 0.0f) { + shockwave_damage *= weapon_get_damage_scale(wip, nullptr, other_objp); + } + } + + dph += shockwave_damage; + + int shot_count; + FiringPattern firing_pattern; + if (sinfop->flags[Ship::Info_Flags::Dyn_primary_linking]) { + firing_pattern = sinfop->dyn_firing_patterns_allowed[i][swp->dynamic_firing_pattern[i]]; + } else { + firing_pattern = wip->firing_pattern; + } + if (sinfop->flags[Ship::Info_Flags::Dyn_primary_linking]) { + shot_count = wip->cycle_multishot; + } else if (wip->b_info.beam_shots) { + shot_count = wip->shots; + } else if (firing_pattern != FiringPattern::STANDARD) { + shot_count = wip->cycle_multishot; + } else { + shot_count = wip->shots; + } + + ai_info *aip = &Ai_info[shipp->ai_index]; + float effective_dps; + float oneshot_value = 0.0f; + float relevant_hits_left; + if (relevant_shields_left > 0.0f) { + relevant_hits_left = relevant_shields_left; + } else if (target_subsys) { + relevant_hits_left = target_subsys->current_hits; + } else { + relevant_hits_left = other_objp->hull_strength; + } + int burst_shots = wip->burst_shots + 1; + int effective_burst_shots = std::max(burst_shots, fl2i(shipp->weapon_energy / (wip->energy_consumed * burst_shots))); + if (dph * effective_burst_shots >= relevant_hits_left) { + oneshot_value = dph * aip->primary_selection_oneshot_modifier; + } + float fire_rate; + if (wip->burst_shots > 1 && wip->burst_flags[Weapon::Burst_Flags::Random_length]) { + fire_rate = (wip->burst_shots / (wip->fire_wait + wip->burst_delay * (wip->burst_shots - 1)) + (1 / wip->fire_wait)) / 2; + } + else if (wip->burst_shots > 1) { + fire_rate = wip->burst_shots / (wip->fire_wait + wip->burst_delay * (wip->burst_shots - 1)); + } + else { + fire_rate = 1 / wip->fire_wait; + } + // if fire can't be sustained indefinitely given energy consumption, take the average between max fire rate and fire rate when we're drained and have to wait for each shot + if (fire_rate * wip->energy_consumed > (shipp->max_weapon_regen_per_second * sinfop->max_weapon_reserve)) { + fire_rate = (fire_rate + ((shipp->max_weapon_regen_per_second * sinfop->max_weapon_reserve) / wip->energy_consumed)) / 2.0f; + } + int num_slots = model_get(sinfop->model_num)->gun_banks[i].num_slots; + effective_dps = (dph * i2fl(shot_count) * i2fl(num_slots) * burst_shots) * fire_rate; + + float weapon_value = std::max(effective_dps, oneshot_value) * aip->primary_selection_random_factor.next(); + + if (wip->primary_selection_target_flags[PrimarySelectionTargetType::ARMOR].contains(relevant_armor_type_idx)) { + weapon_value *= wip->primary_selection_target_flags[PrimarySelectionTargetType::ARMOR][relevant_armor_type_idx]; + } + if (wip->primary_selection_target_flags[PrimarySelectionTargetType::SHIP_TYPE].contains(relevant_ship_type_idx)) { + weapon_value *= wip->primary_selection_target_flags[PrimarySelectionTargetType::SHIP_TYPE][relevant_ship_type_idx]; + } + if (wip->primary_selection_target_flags[PrimarySelectionTargetType::SHIP_CLASS].contains(relevant_ship_class_idx)) { + weapon_value *= wip->primary_selection_target_flags[PrimarySelectionTargetType::SHIP_CLASS][relevant_ship_class_idx]; + } + if (wip->primary_selection_target_flags[PrimarySelectionTargetType::WEAPON_CLASS].contains(relevant_weapon_class_idx)) { + weapon_value *= wip->primary_selection_target_flags[PrimarySelectionTargetType::WEAPON_CLASS][relevant_weapon_class_idx]; + } + + if (i == swp->current_primary_bank) { + weapon_value *= aip->primary_selection_status_quo_bias; + } + weapon_values.emplace(i, weapon_value); + } + std::pair best_pair = std::make_pair(-1, 0.0f); + for (auto pair : weapon_values) { + if (std::max(pair.second, 0.0f) >= best_pair.second) { + best_pair = pair; + } + } + swp->current_primary_bank = best_pair.first; + return best_pair.first; +} + /** * Maybe link primary weapons. */ @@ -6301,15 +6575,33 @@ int ai_fire_primary_weapon(object *objp) enemy_sip = NULL; } + float relevant_shields_left = -1.0f; + int candidate_timestamp = timestamp(std::max(fl2i(aip->primary_select_delay_on_change.next() * i2fl(MILLISECONDS_PER_SECOND)), 0)); + // we don't want to bother checking for an early selection if the ai's going to do it sooner anyway + if (enemy_objp && (timestamp_until(aip->primary_select_timestamp) > timestamp_until(candidate_timestamp))) { + vec3d ship_local_pos = objp->pos; + vm_vec_sub2(&ship_local_pos, &enemy_objp->pos); + vm_vec_rotate(&ship_local_pos, &ship_local_pos, &enemy_objp->orient); + int relevant_quadrant = get_quadrant(&ship_local_pos, enemy_objp); + if (relevant_quadrant > 0 && relevant_quadrant < sz2i(enemy_objp->shield_quadrant.size())) { + relevant_shields_left = std::max(shield_get_quad(enemy_objp, relevant_quadrant) - ship_shield_hitpoint_threshold(enemy_objp, false), 0.0f); + bool shield_is_down = relevant_shields_left > 0.0f; + if (shield_is_down != aip->enemy_shield_is_down) { + aip->enemy_shield_is_down = shield_is_down; + aip->primary_select_timestamp = candidate_timestamp; + } + } + } + //plieblang - added check for size of Preferred_primaries to force reevaluation if good-primary-time has been used in the meantime if ( (swp->current_primary_bank < 0) || (swp->current_primary_bank >= swp->num_primary_banks) || timestamp_elapsed(aip->primary_select_timestamp)) { Weapon::Info_Flags flags = Weapon::Info_Flags::NUM_VALUES; if ( aip->targeted_subsys != NULL ) { flags = Weapon::Info_Flags::Puncture; } - ai_select_primary_weapon(objp, enemy_objp, flags); + ai_select_primary_weapon_configurable(objp, enemy_objp, flags, relevant_shields_left); ship_primary_changed(shipp); // AL: maybe send multiplayer information when AI ship changes primaries - aip->primary_select_timestamp = timestamp(5 * MILLISECONDS_PER_SECOND); // Maybe change primary weapon five seconds from now. + aip->primary_select_timestamp = timestamp(fl2i(aip->primary_select_delay.next() * i2fl(MILLISECONDS_PER_SECOND))); // Maybe change primary weapon in a while (five seconds by default). } // if the ship has no primary weapon selected, whether because it has no primary banks or because no bank contains a weapon, then there is nothing to fire @@ -15790,6 +16082,7 @@ void init_ai_object(int objnum) aip->goal_check_time = timestamp(0); aip->last_predicted_enemy_pos = near_vec; aip->prev_goal_point = near_vec; + aip->enemy_shield_is_down = false; aip->goal_point = near_vec; aip->time_enemy_in_range = 0.0f; aip->time_enemy_near = 0.0f; @@ -15936,6 +16229,10 @@ void init_aip_from_class_and_profile(ai_info *aip, ai_class *aicp, ai_profile_t aip->ai_secondary_range_mult = aicp->ai_secondary_range_mult[Game_skill_level]; aip->ai_class_autoscale = aicp->ai_class_autoscale; + aip->primary_selection_random_factor = aicp->primary_selection_random_factor[Game_skill_level]; + aip->primary_selection_oneshot_modifier = aicp->primary_selection_oneshot_modifier[Game_skill_level]; + aip->primary_selection_status_quo_bias = aicp->primary_selection_status_quo_bias[Game_skill_level]; + //Apply overrides from ai class to ai profiles values //Only override values which were explicitly set in the AI class aip->ai_cmeasure_fire_chance = (aicp->ai_cmeasure_fire_chance[Game_skill_level] == FLT_MIN) ? @@ -15984,6 +16281,10 @@ void init_aip_from_class_and_profile(ai_info *aip, ai_class *aicp, ai_profile_t profile->max_aim_update_delay[Game_skill_level] : aicp->ai_max_aim_update_delay[Game_skill_level]; aip->ai_turret_max_aim_update_delay = (aicp->ai_turret_max_aim_update_delay[Game_skill_level] == FLT_MIN) ? profile->turret_max_aim_update_delay[Game_skill_level] : aicp->ai_turret_max_aim_update_delay[Game_skill_level]; + aip->primary_select_delay = (aicp->primary_select_delay[Game_skill_level].min() == FLT_MIN && aicp->primary_select_delay[Game_skill_level].max() == FLT_MIN) ? + profile->primary_select_delay[Game_skill_level] : aicp->primary_select_delay[Game_skill_level]; + aip->primary_select_delay_on_change = (aicp->primary_select_delay_on_change[Game_skill_level].min() == FLT_MIN && aicp->primary_select_delay_on_change[Game_skill_level].max() == FLT_MIN) ? + profile->primary_select_delay_on_change[Game_skill_level] : aicp->primary_select_delay_on_change[Game_skill_level]; //Combine AI profile and AI class flags aip->ai_profile_flags = profile->flags | (aicp->ai_profile_flags & aicp->ai_profile_flags_set); diff --git a/code/debris/debris.h b/code/debris/debris.h index 7377f0e7fad..a97b4f99fbd 100644 --- a/code/debris/debris.h +++ b/code/debris/debris.h @@ -15,6 +15,7 @@ #include "globalincs/pstypes.h" #include "globalincs/flagset.h" #include "gamesnd/gamesnd.h" +#include "ship/ship.h" class object; struct CFILE; diff --git a/code/decals/decals.h b/code/decals/decals.h index 4a737930814..f84f0c6953f 100644 --- a/code/decals/decals.h +++ b/code/decals/decals.h @@ -7,6 +7,8 @@ #include "utils/RandomRange.h" #include "utils/reset_on_move.h" +class ship; + namespace decals { class DecalDefinition { diff --git a/code/graphics/openxr.cpp b/code/graphics/openxr.cpp index e878ec7a16f..1a640ba01c6 100644 --- a/code/graphics/openxr.cpp +++ b/code/graphics/openxr.cpp @@ -6,6 +6,7 @@ #include "mod_table/mod_table.h" #include "render/3d.h" #include "starfield/starfield.h" +#include "hud/hudparse.h" std::unique_ptr Stars_XRBuffer; diff --git a/code/lab/dialogs/lab_ui.h b/code/lab/dialogs/lab_ui.h index b70c0e89a81..d133993b4be 100644 --- a/code/lab/dialogs/lab_ui.h +++ b/code/lab/dialogs/lab_ui.h @@ -3,6 +3,7 @@ #include "model/model.h" #include "model/animation/modelanimation.h" #include "species_defs/species_defs.h" +#include "weapon/weapon.h" enum class LabTurretAimType { RANDOM, diff --git a/code/mod_table/mod_table.h b/code/mod_table/mod_table.h index d74e35b0395..f2a386500cb 100644 --- a/code/mod_table/mod_table.h +++ b/code/mod_table/mod_table.h @@ -12,7 +12,9 @@ #include "globalincs/pstypes.h" #include "globalincs/systemvars.h" #include "graphics/2d.h" -#include "hud/hudtarget.h" +#include "globalincs/version.h" + +enum class leadIndicatorBehavior; // Typedef for Overhead View styles typedef enum { diff --git a/code/ship/shield.cpp b/code/ship/shield.cpp index 218dacd32cb..f8e291e4888 100644 --- a/code/ship/shield.cpp +++ b/code/ship/shield.cpp @@ -948,7 +948,7 @@ int get_quadrant(const vec3d *hit_pnt, const object *shipobjp) float closest_dist = FLT_MAX; for (unsigned int i=0; iinstance].shield_points.size(); i++) { - float dist = vm_vec_dist(hit_pnt, &Ships[shipobjp->instance].shield_points.at(i)); + float dist = vm_vec_dist_squared(hit_pnt, &Ships[shipobjp->instance].shield_points.at(i)); if (dist < closest_dist) { closest = i; diff --git a/code/weapon/weapon.h b/code/weapon/weapon.h index ed59726b94f..56a7018a093 100644 --- a/code/weapon/weapon.h +++ b/code/weapon/weapon.h @@ -372,6 +372,15 @@ struct WeaponLaunchCurveData { float target_radius; }; +enum PrimarySelectionTargetType { + ARMOR, + SHIP_TYPE, + SHIP_CLASS, + WEAPON_CLASS, + + MAX, +}; + struct weapon_info; extern SCP_vector Weapon_info; @@ -720,6 +729,8 @@ struct weapon_info animation::ModelAnimationSet animations; + std::array, PrimarySelectionTargetType::MAX> primary_selection_target_flags; + enum class WeaponLaunchCurveOutputs { // outputs FIRE_WAIT_MULT, diff --git a/code/weapon/weapons.cpp b/code/weapon/weapons.cpp index 393b8b5e218..375f479327d 100644 --- a/code/weapon/weapons.cpp +++ b/code/weapon/weapons.cpp @@ -773,6 +773,8 @@ static particle::ParticleEffectHandle convertLegacyPspewBuffer(const pspew_legac hasAnim ? bm_load_either(pspew_buffer.particle_spew_anim.c_str()) : particle::Anim_bitmap_id_smoke)); //Bitmap or Anim } +SCP_unordered_map, PrimarySelectionTargetType::MAX>> primary_selection_target_flags_temp; + /** * Parse the information for a specific ship type. * Return weapon index if successful, otherwise return -1 @@ -2126,6 +2128,34 @@ int parse_weapon(int subtype, bool replace, const char *filename) if (optional_string("$Disallow Support Rearm:")) { stuff_boolean(&wip->disallow_rearm); } + + if (optional_string("$Primary Selection Target Flags:")) { + const int wi_index = static_cast(wip - Weapon_info.data()); + SCP_string type; + SCP_string type_name; + float value; + for (int i = 0; i < PrimarySelectionTargetType::MAX; i++) { + primary_selection_target_flags_temp[i] = {}; + } + while (optional_string("+Condition Type:")) { + stuff_string(type, F_NAME); + required_string("+Target Class:"); + stuff_string(type_name, F_NAME); + required_string("+Value Multiplier:"); + stuff_float(&value); + if (type == "ARMOR") { + primary_selection_target_flags_temp[wi_index][PrimarySelectionTargetType::ARMOR].emplace(type_name, value); + } else if (type == "SHIP TYPE") { + primary_selection_target_flags_temp[wi_index][PrimarySelectionTargetType::SHIP_TYPE].emplace(type_name, value); + } else if (type == "SHIP CLASS") { + primary_selection_target_flags_temp[wi_index][PrimarySelectionTargetType::SHIP_CLASS].emplace(type_name, value); + } else if (type == "WEAPON CLASS") { + primary_selection_target_flags_temp[wi_index][PrimarySelectionTargetType::WEAPON_CLASS].emplace(type_name, value); + } else { + error_display(0, "Invalid primary selection target flag type '%s' in weapon '%s'!", type.c_str(), wip->name); + } + } + } if (optional_string("+Weapon Range:")) { stuff_float(&wip->weapon_range); @@ -4362,6 +4392,34 @@ int parse_weapon(int subtype, bool replace, const char *filename) return w_id; } +void populate_primary_selection_flags() +{ + for (int i = 0; i < sz2i(Weapon_info.size()); i++) { + weapon_info *wip = &Weapon_info[i]; + for (int flavor = 0; flavor < PrimarySelectionTargetType::MAX; flavor++) { + for (auto& [index_name, mult] : primary_selection_target_flags_temp[i][flavor]) { + int index; + switch (flavor) { + case PrimarySelectionTargetType::ARMOR: + index = armor_type_get_idx(index_name.c_str()); + break; + case PrimarySelectionTargetType::SHIP_TYPE: + index = ship_type_name_lookup(index_name.c_str()); + break; + case PrimarySelectionTargetType::SHIP_CLASS: + index = ship_info_lookup(index_name.c_str()); + break; + case PrimarySelectionTargetType::WEAPON_CLASS: + index = weapon_info_lookup(index_name.c_str()); + break; + } + wip->primary_selection_target_flags[flavor].emplace(index, mult); + } + } + } + primary_selection_target_flags_temp.clear(); +} + /** * For all weapons that spawn weapons, given an index at weaponp->spawn_type, * convert the strings in Spawn_names to indices in the Weapon_types array. @@ -5124,7 +5182,7 @@ void weapon_do_post_parse() translate_spawn_types(); } -// Called after ship_init() to resolve proximity ship type/class names into indices. +// Called after ship_init() to resolve proximity ship type/class names into indices, as well as doing the same for primary selection flags. void weapon_post_ship_init() { const int num_weapons = static_cast(Weapon_info.size()); @@ -5157,6 +5215,8 @@ void weapon_post_ship_init() Pending_proximity_type_names.clear(); Pending_proximity_class_names.clear(); + + populate_primary_selection_flags(); } /** @@ -9053,36 +9113,37 @@ void weapon_get_laser_color(color *c, object *objp) */ float weapon_get_damage_scale(const weapon_info *wip, const object *wep, const object *target) { - weapon *wp; - int from_player = 0; float total_scale = 1.0f; float hull_pct; int is_big_damage_ship = 0; - + // Goober5000 - additional sanity (target can be NULL) Assert(wip); - Assert(wep); - + // sanity - if((wip == NULL) || (wep == NULL) || (target == NULL)){ + if((wip == nullptr) || (target == nullptr)){ return 1.0f; } - - // don't scale any damage if its not a weapon - if((wep->type != OBJ_WEAPON) || (wep->instance < 0) || (wep->instance >= MAX_WEAPONS)){ - return 1.0f; - } - wp = &Weapons[wep->instance]; - - // was the weapon fired by the player - from_player = 0; - if((wep->parent >= 0) && (wep->parent < MAX_OBJECTS) && (Objects[wep->parent].flags[Object::Object_Flags::Player_ship])){ - from_player = 1; - } - - // if this is a lockarm weapon, and it was fired unlocked - if((wip->wi_flags[Weapon::Info_Flags::Lockarm]) && !(wp->weapon_flags[Weapon::Weapon_Flags::Locked_when_fired])){ - total_scale *= 0.1f; + + int from_player = 0; + if (wep) { + weapon *wp; + // don't scale any damage if its not a weapon + if((wep->type != OBJ_WEAPON) || (wep->instance < 0) || (wep->instance >= MAX_WEAPONS)){ + return 1.0f; + } + wp = &Weapons[wep->instance]; + + // was the weapon fired by the player + from_player = 0; + if((wep->parent >= 0) && (wep->parent < MAX_OBJECTS) && (Objects[wep->parent].flags[Object::Object_Flags::Player_ship])){ + from_player = 1; + } + + // if this is a lockarm weapon, and it was fired unlocked + if((wip->wi_flags[Weapon::Info_Flags::Lockarm]) && !(wp->weapon_flags[Weapon::Weapon_Flags::Locked_when_fired])){ + total_scale *= 0.1f; + } } // if the hit object was a ship and we're doing damage scaling @@ -10146,6 +10207,10 @@ void weapon_info::reset() // Reset using default constructor this->impact_decal = decals::creation_info(); + for (i = 0; i < PrimarySelectionTargetType::MAX; i++) { + this->primary_selection_target_flags[i] = {}; + } + this->on_create_program = actions::ProgramSet(); }