Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions code/ai/ai_flags.h
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ namespace AI {
Smart_shield_management,
Smart_subsystem_targeting_for_turrets,
Strict_turret_tagged_only_targeting,
Ships_intercept_mines, // all AI-controlled ships autonomously engage hostile mines within mine_targetable_range of themselves
Support_dont_add_primaries, //Prevents support ship from equipping new primary as requested in https://scp.indiegames.us/mantis/view.php?id=3198
Turrets_ignore_target_radius,
Use_actual_primary_range,
Expand Down
2 changes: 2 additions & 0 deletions code/ai/ai_profiles.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,8 @@ void parse_ai_profiles_tbl(const char *filename)

set_flag(profile, "$cancel future waves of any wing launched from an exited ship:", AI::Profile_Flags::Cancel_future_waves_of_any_wing_launched_from_an_exited_ship);

set_flag(profile, "$ships intercept mines:", AI::Profile_Flags::Ships_intercept_mines);


// end of options ----------------------------------------

Expand Down
103 changes: 101 additions & 2 deletions code/ai/aicode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6484,6 +6484,11 @@ bool ai_select_secondary_weapon(object *objp, ship_weapon *swp, flagset<Weapon::
ignore_mask.set(Weapon::Info_Flags::Bomber_plus);
}

// Ignore mines in normal combat... they must be explicitly prioritized via SEXP
if (!(prio1[Weapon::Info_Flags::Mine] || prio2[Weapon::Info_Flags::Mine])) {
ignore_mask.set(Weapon::Info_Flags::Mine);
}

#ifndef NDEBUG
for (i=0; i<MAX_WEAPON_TYPES; i++) {
weapon_id_list[i] = -1;
Expand Down Expand Up @@ -7447,6 +7452,14 @@ void attack_set_accel(ai_info *aip, ship_info *sip, float dist_to_enemy, float d
if (wip != nullptr && wip->optimum_range > 0)
optimal_range = wip->optimum_range;

// Standoff for mines: keep at least 1.2x the target mine's proximity radius
// so we shoot it from outside its detonation zone.
if (En_objp->type == OBJ_WEAPON) {
weapon_info *target_wip = &Weapon_info[Weapons[En_objp->instance].weapon_info_index];
if (target_wip->is_mine() && target_wip->proximity_radius > 0.0f)
optimal_range = MAX(optimal_range, target_wip->proximity_radius * 1.2f);
}

if (dist_to_enemy > optimal_range + vm_vec_mag_quick(&En_objp->phys_info.vel) * dot_from_enemy + Pl_objp->phys_info.speed * speed_ratio) {
if (dist_to_enemy > optimal_range + 600.0f) {
if (ai_willing_to_afterburn_hard(aip)) {
Expand Down Expand Up @@ -10497,8 +10510,59 @@ void maybe_update_guard_object(object *hit_objp, object *hitter_objp)
}
}

// Scan missile list looking for bombs homing on guarded_objp
// return 1 if bomb is found (and targeted by guarding_objp), otherwise return 0
// Find the closest hostile mine threatening 'against_objp', ranked by distance from 'from_objp'.
// If 'against_objp' is null, defaults to 'from_objp' (i.e. find mines threatening the searcher itself).
// imminent_only: also requires the mine to be within proximity_radius * 1.5 of against_objp;
// used to preempt a current target when the mine is about to detonate.
static object *ai_find_nearby_mine_threat(object *from_objp, object *against_objp = nullptr, bool imminent_only = false)
{
if (against_objp == nullptr)
against_objp = from_objp;

object *closest_mine = nullptr;
float closest_dist_from = std::numeric_limits<float>::max();

for (missile_obj *mo = GET_NEXT(&Missile_obj_list); mo != END_OF_LIST(&Missile_obj_list); mo = GET_NEXT(mo)) {
Assert(mo->objnum >= 0 && mo->objnum < MAX_OBJECTS);
object *mine_objp = &Objects[mo->objnum];
if (mine_objp->flags[Object::Object_Flags::Should_be_dead])
continue;

weapon *wp = &Weapons[mine_objp->instance];
weapon_info *wip = &Weapon_info[wp->weapon_info_index];

if (!wip->is_mine())
continue;

// Only engage destructible mines
if (!wip->wi_flags[Weapon::Info_Flags::Fighter_Interceptable])
continue;

// Only engage mines hostile to the threatened ship
if (!iff_x_attacks_y(wp->team, obj_team(against_objp)))
continue;

float dist_against = vm_vec_dist(&mine_objp->pos, &against_objp->pos);

// Mine must be within its own targetable range of the threatened ship
if (dist_against > wip->mine_targetable_range)
continue;

// Imminent gate: only consider mines about to detonate on the threatened ship.
// proximity_radius is always > 0 for mines (parser enforces).
if (imminent_only && dist_against > wip->proximity_radius * 1.5f)
continue;

float dist_from = (from_objp == against_objp) ? dist_against : vm_vec_dist(&mine_objp->pos, &from_objp->pos);
if (dist_from < closest_dist_from) {
closest_dist_from = dist_from;
closest_mine = mine_objp;
}
}

return closest_mine;
}

int ai_guard_find_nearby_bomb(object *guarding_objp, object *guarded_objp)
{
missile_obj *mo;
Expand Down Expand Up @@ -10544,6 +10608,13 @@ int ai_guard_find_nearby_bomb(object *guarding_objp, object *guarded_objp)
return 1;
}

// No homing threat found... check for mines threatening the guarded ship.
// Mines are stationary so they won't home; handle them as a lower priority threat.
if (object *closest_mine_objp = ai_find_nearby_mine_threat(guarding_objp, guarded_objp)) {
guard_object_was_hit(guarding_objp, closest_mine_objp);
return 1;
}

return 0;
}

Expand Down Expand Up @@ -10654,6 +10725,13 @@ void ai_guard_find_nearby_object()
if ( (aip->target_objnum == -1) && asteroid_count() ) {
ai_guard_find_nearby_asteroid(Pl_objp, guardobjp);
}

// if still not attacking anything and flag is set, engage mines near the guarding ship itself
if (aip->target_objnum == -1 && The_mission.ai_profile->flags[AI::Profile_Flags::Ships_intercept_mines]) {
object *mine_objp = ai_find_nearby_mine_threat(Pl_objp);
if (mine_objp)
guard_object_was_hit(Pl_objp, mine_objp);
}
}
}

Expand Down Expand Up @@ -15283,6 +15361,19 @@ void ai_frame(int objnum)

ai_maybe_depart(Pl_objp);

// Imminent-mine override: even if we have a current target, swap to a mine
// that is about to detonate on us (within proximity_radius * 1.5).
if (The_mission.ai_profile->flags[AI::Profile_Flags::Ships_intercept_mines]
&& aip->mode != AIM_GUARD && aip->mode != AIM_EVADE_WEAPON
&& Ship_info[shipp->ship_info_index].class_type > -1
&& Ship_types[Ship_info[shipp->ship_info_index].class_type].flags[Ship::Type_Info_Flags::AI_auto_attacks]) {
object *imminent_mine = ai_find_nearby_mine_threat(Pl_objp, nullptr, true);
if (imminent_mine != nullptr && (target_objnum < 0 || &Objects[target_objnum] != imminent_mine)) {
aip->aspect_locked_time = 0.0f;
target_objnum = set_target_objnum(aip, OBJ_INDEX(imminent_mine));
}
}

// Find an enemy if don't already have one.
En_objp = NULL;
if ( ai_need_new_target(Pl_objp, target_objnum) ) {
Expand All @@ -15302,6 +15393,14 @@ void ai_frame(int objnum)
{
En_objp = &Objects[target_objnum];
}
} else if (aip->mode != AIM_GUARD && The_mission.ai_profile->flags[AI::Profile_Flags::Ships_intercept_mines]) {
// No enemy found so check for nearby hostile mines within their targetable range
object *mine_objp = ai_find_nearby_mine_threat(Pl_objp);
if (mine_objp) {
target_objnum = set_target_objnum(aip, OBJ_INDEX(mine_objp));
if (target_objnum >= 0)
En_objp = &Objects[target_objnum];
}
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions code/ai/aiturret.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,12 @@ int valid_turret_enemy(object *objp, object *turret_parent)
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;
}

Expand Down
50 changes: 24 additions & 26 deletions code/hud/hudtarget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,18 @@ int hud_target_invalid_awacs(object *objp)
return 0;
}

// Returns true if the weapon object can currently be targeted from source_objp.
// For mines, uses range-based detection (mine_targetable_range). For other weapons, uses flags.
static bool weapon_is_targetable_from(object *source_objp, object *objp)
{
weapon_info *wip = &Weapon_info[Weapons[objp->instance].weapon_info_index];
if (wip->is_mine()) {
Assertion(source_objp != nullptr, "weapon_is_targetable_from called with null source_objp");
return vm_vec_dist(&source_objp->pos, &objp->pos) <= wip->mine_targetable_range;
}
return wip->wi_flags[Weapon::Info_Flags::Can_be_targeted] || wip->wi_flags[Weapon::Info_Flags::Bomb];
}

ship_subsys *advance_subsys(ship_subsys *cur, int next_flag)
{
if (next_flag) {
Expand Down Expand Up @@ -1187,9 +1199,8 @@ void hud_target_common(int team_mask, int next_flag)
continue;

if (A->type == OBJ_WEAPON) {
if ( !(Weapon_info[Weapons[A->instance].weapon_info_index].wi_flags[Weapon::Info_Flags::Can_be_targeted]) )
if ( !(Weapon_info[Weapons[A->instance].weapon_info_index].wi_flags[Weapon::Info_Flags::Bomb]) )
continue;
if (!weapon_is_targetable_from(Player_obj, A))
continue;

if (Weapons[A->instance].lssm_stage == 3)
continue;
Expand Down Expand Up @@ -1503,10 +1514,9 @@ void hud_target_hostile_bomb_or_bomber(object* source_obj, int next_flag, bool t
continue;

weapon* wp = &Weapons[A->instance];
weapon_info* wip = &Weapon_info[wp->weapon_info_index];

// only allow targeting of bombs
if (!(wip->wi_flags[Weapon::Info_Flags::Can_be_targeted]) && !(wip->wi_flags[Weapon::Info_Flags::Bomb]))
// only allow targeting of bombs/targetable weapons/mines in range
if (!weapon_is_targetable_from(source_obj, A))
continue;

if (wp->lssm_stage == 3)
Expand Down Expand Up @@ -2498,11 +2508,8 @@ void hud_target_in_reticle_new()
}

if ( A->type == OBJ_WEAPON ) {
if ( !(Weapon_info[Weapons[A->instance].weapon_info_index].wi_flags[Weapon::Info_Flags::Can_be_targeted]) ) {
if ( !(Weapon_info[Weapons[A->instance].weapon_info_index].wi_flags[Weapon::Info_Flags::Bomb]) ){
continue;
}
}
if (!weapon_is_targetable_from(Player_obj, A))
continue;
if (Weapons[A->instance].lssm_stage==3){
continue;
}
Expand Down Expand Up @@ -2607,11 +2614,8 @@ void hud_target_in_reticle_old()
}

if ( A->type == OBJ_WEAPON ) {
if ( !(Weapon_info[Weapons[A->instance].weapon_info_index].wi_flags[Weapon::Info_Flags::Can_be_targeted]) ){
if ( !(Weapon_info[Weapons[A->instance].weapon_info_index].wi_flags[Weapon::Info_Flags::Bomb]) ){
continue;
}
}
if (!weapon_is_targetable_from(Player_obj, A))
continue;

if (Weapons[A->instance].lssm_stage==3){
continue;
Expand Down Expand Up @@ -4188,11 +4192,8 @@ void HudGaugeLeadIndicator::renderLeadCurrentTarget(bool config)

// only allow bombs to have lead indicator displayed
if ( targetp->type == OBJ_WEAPON ) {
if ( !(Weapon_info[Weapons[targetp->instance].weapon_info_index].wi_flags[Weapon::Info_Flags::Can_be_targeted]) ) {
if ( !(Weapon_info[Weapons[targetp->instance].weapon_info_index].wi_flags[Weapon::Info_Flags::Bomb]) ) {
return;
}
}
if (!weapon_is_targetable_from(Player_obj, targetp))
return;
}

// If the target is out of range, then draw the correct frame for the lead indicator
Expand Down Expand Up @@ -4363,11 +4364,8 @@ void HudGaugeLeadIndicator::renderLeadQuick(vec3d *target_world_pos, object *tar

// only allow bombs to have lead indicator displayed
if ( targetp->type == OBJ_WEAPON ) {
if ( !(Weapon_info[Weapons[targetp->instance].weapon_info_index].wi_flags[Weapon::Info_Flags::Can_be_targeted]) ) {
if ( !(Weapon_info[Weapons[targetp->instance].weapon_info_index].wi_flags[Weapon::Info_Flags::Bomb]) ) {
return;
}
}
if (!weapon_is_targetable_from(Player_obj, targetp))
return;
}

// If the target is out of range, then draw the correct frame for the lead indicator
Expand Down
41 changes: 36 additions & 5 deletions code/radar/radarsetup.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ void radar_plot_object( object *objp )
vec3d pos, tempv;
float awacs_level, dist, max_radar_dist;
vec3d world_pos = objp->pos;
bool mine_in_targetable_range = true; // for mines, computed below after distance check; non-mines unaffected

// don't process anything here. Somehow, a jumpnode object caused this function
// to get entered on server side.
Expand Down Expand Up @@ -236,25 +237,40 @@ void radar_plot_object( object *objp )

case OBJ_WEAPON:
{
weapon_info *wip = &Weapon_info[Weapons[objp->instance].weapon_info_index];

if (wip->is_mine()) {
// if explicitly hidden, return
if (wip->wi_flags[Weapon::Info_Flags::Dont_show_on_radar])
return;

// if we don't attack the mine, return
if ( !wip->wi_flags[Weapon::Info_Flags::Show_friendly] && !iff_x_attacks_y(Player_ship->team, obj_team(objp)) )
return;

// Mine range-based detection... visibility determined after distance is calculated below
break;
}

// if not a bomb, return
if ( !(Weapon_info[Weapons[objp->instance].weapon_info_index].wi_flags[Weapon::Info_Flags::Shown_on_radar]) )
if ( !(Weapon_info[Weapons[objp->instance].weapon_info_index].wi_flags[Weapon::Info_Flags::Bomb]) )
if ( !(wip->wi_flags[Weapon::Info_Flags::Shown_on_radar]) )
if ( !(wip->wi_flags[Weapon::Info_Flags::Bomb]) )
return;

// if explicitly hidden, return
if (Weapon_info[Weapons[objp->instance].weapon_info_index].wi_flags[Weapon::Info_Flags::Dont_show_on_radar])
if (wip->wi_flags[Weapon::Info_Flags::Dont_show_on_radar])
return;

// if we don't attack the bomb, return
if ( (!(Weapon_info[Weapons[objp->instance].weapon_info_index].wi_flags[Weapon::Info_Flags::Show_friendly])) && (!iff_x_attacks_y(Player_ship->team, obj_team(objp))))
if ( !wip->wi_flags[Weapon::Info_Flags::Show_friendly] && !iff_x_attacks_y(Player_ship->team, obj_team(objp)) )
return;

// if a local ssm is in subspace, return
if (Weapons[objp->instance].lssm_stage == 3)
return;

// if corkscrew missile use last frame pos for pos
if ( (Weapon_info[Weapons[objp->instance].weapon_info_index].wi_flags[Weapon::Info_Flags::Corkscrew]) )
if (wip->wi_flags[Weapon::Info_Flags::Corkscrew])
world_pos = objp->last_pos;

break;
Expand All @@ -281,6 +297,17 @@ void radar_plot_object( object *objp )
return;
}

// Mine range-based visibility: determine state from mine-specific ranges
if (objp->type == OBJ_WEAPON) {
weapon_info *wip = &Weapon_info[Weapons[objp->instance].weapon_info_index];
if (wip->is_mine()) {
mine_in_targetable_range = (dist <= wip->mine_targetable_range);
bool in_sensors = (dist <= wip->mine_sensors_range);
if (!mine_in_targetable_range && !in_sensors)
return; // beyond all detection ranges
}
}

// determine the range within which the radar blip is bright
if (timestamp_elapsed(Radar_calc_bright_dist_timer))
{
Expand Down Expand Up @@ -337,6 +364,10 @@ void radar_plot_object( object *objp )

// see if blip should be drawn distorted
// also determine if alternate image was defined for this ship
// Mines outside their targetable range (but inside sensors range, by the earlier gate) get a distorted blip
if (!mine_in_targetable_range)
b->flags |= BLIP_DRAW_DISTORTED;

if (objp->type == OBJ_SHIP)
{
// ships specifically hidden from sensors
Expand Down
26 changes: 26 additions & 0 deletions code/scripting/api/objs/weapon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,32 @@ ADE_FUNC(getSubmodelAnimationTime, l_Weapon, "string type, string triggeredBy",
return ade_set_args(L, "f", time_s);
}

ADE_FUNC(isMine, l_Weapon, nullptr, "Returns whether this weapon is a mine.", "boolean", "True if the weapon is a mine type, false otherwise or if handle is invalid")
{
object_h *oh = nullptr;
if (!ade_get_args(L, "o", l_Weapon.GetPtr(&oh)))
return ade_set_error(L, "b", false);

if (!oh->isValid())
return ade_set_error(L, "b", false);

const weapon_info *wip = &Weapon_info[Weapons[oh->objp()->instance].weapon_info_index];
return ade_set_args(L, "b", wip->is_mine());
}

ADE_FUNC(detonate, l_Weapon, nullptr, "Forces this weapon to detonate immediately.", "boolean", "True if successful, false if handle is invalid")
{
object_h *oh = nullptr;
if (!ade_get_args(L, "o", l_Weapon.GetPtr(&oh)))
return ade_set_error(L, "b", false);

if (!oh->isValid())
return ade_set_error(L, "b", false);

weapon_detonate(oh->objp());
return ade_set_args(L, "b", true);
}

ADE_FUNC(vanish, l_Weapon, nullptr, "Vanishes this weapon from the mission.", "boolean", "True if the deletion was successful, false otherwise.")
{

Expand Down
Loading
Loading