Skip to content

Commit 31f77e5

Browse files
authored
Mines weapon type & weapon proximity triggers (#7461)
* new mine weapon type * some fixes and cleanup * fix some parsing issues * make sure mines get plotted * make mine spawned objects can get a correct target * some cleanup and additional features * chasing mines * some Lua tweaks * generalize proximity detonations * minor tweaks * collapse namespace * address feedback
1 parent d17bb56 commit 31f77e5

15 files changed

Lines changed: 716 additions & 51 deletions

code/ai/ai_flags.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ namespace AI {
132132
Smart_shield_management,
133133
Smart_subsystem_targeting_for_turrets,
134134
Strict_turret_tagged_only_targeting,
135+
Ships_intercept_mines, // all AI-controlled ships autonomously engage hostile mines within mine_targetable_range of themselves
135136
Support_dont_add_primaries, //Prevents support ship from equipping new primary as requested in https://scp.indiegames.us/mantis/view.php?id=3198
136137
Turrets_ignore_target_radius,
137138
Use_actual_primary_range,

code/ai/ai_profiles.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,8 @@ void parse_ai_profiles_tbl(const char *filename)
745745

746746
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);
747747

748+
set_flag(profile, "$ships intercept mines:", AI::Profile_Flags::Ships_intercept_mines);
749+
748750

749751
// end of options ----------------------------------------
750752

code/ai/aicode.cpp

Lines changed: 116 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6506,6 +6506,11 @@ bool ai_select_secondary_weapon(object *objp, ship_weapon *swp, flagset<Weapon::
65066506
ignore_mask.set(Weapon::Info_Flags::Bomber_plus);
65076507
}
65086508

6509+
// Ignore mines in normal combat... they must be explicitly prioritized via SEXP
6510+
if (!(prio1[Weapon::Info_Flags::Mine] || prio2[Weapon::Info_Flags::Mine])) {
6511+
ignore_mask.set(Weapon::Info_Flags::Mine);
6512+
}
6513+
65096514
#ifndef NDEBUG
65106515
for (i=0; i<MAX_WEAPON_TYPES; i++) {
65116516
weapon_id_list[i] = -1;
@@ -7469,6 +7474,21 @@ void attack_set_accel(ai_info *aip, ship_info *sip, float dist_to_enemy, float d
74697474
if (wip != nullptr && wip->optimum_range > 0)
74707475
optimal_range = wip->optimum_range;
74717476

7477+
// Standoff for mines: keep at least 1.2x the target mine's proximity radius
7478+
// so we shoot it from outside its detonation zone.
7479+
if (En_objp->type == OBJ_WEAPON) {
7480+
weapon_info *target_wip = &Weapon_info[Weapons[En_objp->instance].weapon_info_index];
7481+
if (target_wip->is_mine() && target_wip->proximity_radius > 0.0f) {
7482+
float standoff = target_wip->proximity_radius * 1.2f;
7483+
// Don't stand off farther than our weapon can actually reach, or we'd hold
7484+
// position outside firing range and never destroy the mine. If the weapon
7485+
// can't outrange the detonation zone, close in and accept the risk.
7486+
if (wip != nullptr)
7487+
standoff = MIN(standoff, MIN(wip->max_speed * wip->lifetime, wip->weapon_range));
7488+
optimal_range = MAX(optimal_range, standoff);
7489+
}
7490+
}
7491+
74727492
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) {
74737493
if (dist_to_enemy > optimal_range + 600.0f) {
74747494
if (ai_willing_to_afterburn_hard(aip)) {
@@ -10519,10 +10539,65 @@ void maybe_update_guard_object(object *hit_objp, object *hitter_objp)
1051910539
}
1052010540
}
1052110541

10522-
// Scan missile list looking for bombs homing on guarded_objp
10523-
// return 1 if bomb is found (and targeted by guarding_objp), otherwise return 0
10542+
// Find the closest hostile mine threatening 'against_objp', ranked by distance from 'from_objp'.
10543+
// If 'against_objp' is null, defaults to 'from_objp' (i.e. find mines threatening the searcher itself).
10544+
// imminent_only: also requires the mine to be within proximity_radius * 1.5 of against_objp;
10545+
// used to preempt a current target when the mine is about to detonate.
10546+
static object *ai_find_nearby_mine_threat(object *from_objp, object *against_objp = nullptr, bool imminent_only = false)
10547+
{
10548+
if (against_objp == nullptr)
10549+
against_objp = from_objp;
10550+
10551+
object *closest_mine = nullptr;
10552+
float closest_dist_from = std::numeric_limits<float>::max();
10553+
10554+
for (const auto *mo : list_range(&Missile_obj_list)) {
10555+
Assert(mo->objnum >= 0 && mo->objnum < MAX_OBJECTS);
10556+
object *mine_objp = &Objects[mo->objnum];
10557+
if (mine_objp->flags[Object::Object_Flags::Should_be_dead])
10558+
continue;
10559+
10560+
weapon *wp = &Weapons[mine_objp->instance];
10561+
weapon_info *wip = &Weapon_info[wp->weapon_info_index];
10562+
10563+
if (!wip->is_mine())
10564+
continue;
10565+
10566+
// Only engage destructible mines
10567+
if (!wip->wi_flags[Weapon::Info_Flags::Fighter_Interceptable])
10568+
continue;
10569+
10570+
// Only engage mines hostile to the threatened ship
10571+
if (!iff_x_attacks_y(wp->team, obj_team(against_objp)))
10572+
continue;
10573+
10574+
float dist_against = vm_vec_dist(&mine_objp->pos, &against_objp->pos);
10575+
10576+
// Mine must be within its own targetable range of the threatened ship
10577+
if (dist_against > wip->mine_targetable_range)
10578+
continue;
10579+
10580+
// Imminent gate: only consider mines about to detonate on the threatened ship.
10581+
// proximity_radius is always > 0 for mines (parser enforces).
10582+
if (imminent_only && dist_against > wip->proximity_radius * 1.5f)
10583+
continue;
10584+
10585+
float dist_from = (from_objp == against_objp) ? dist_against : vm_vec_dist(&mine_objp->pos, &from_objp->pos);
10586+
if (dist_from < closest_dist_from) {
10587+
closest_dist_from = dist_from;
10588+
closest_mine = mine_objp;
10589+
}
10590+
}
10591+
10592+
return closest_mine;
10593+
}
10594+
10595+
// Scan the missile list for a threat to guarded_objp and, if one is found, target it with guarding_objp.
10596+
// Bombs homing on guarded_objp take priority; if none are found, fall back to the closest hostile mine
10597+
// threatening guarded_objp (mines are stationary so they can't home, hence the lower priority).
10598+
// Returns 1 if a threat was found and targeted, otherwise 0.
1052410599
int ai_guard_find_nearby_bomb(object *guarding_objp, object *guarded_objp)
10525-
{
10600+
{
1052610601
missile_obj *mo;
1052710602
object *bomb_objp, *closest_bomb_objp=NULL;
1052810603
float dist, dist_to_guarding_obj,closest_dist_to_guarding_obj=999999.0f;
@@ -10566,6 +10641,13 @@ int ai_guard_find_nearby_bomb(object *guarding_objp, object *guarded_objp)
1056610641
return 1;
1056710642
}
1056810643

10644+
// No homing threat found... check for mines threatening the guarded ship.
10645+
// Mines are stationary so they won't home; handle them as a lower priority threat.
10646+
if (object *closest_mine_objp = ai_find_nearby_mine_threat(guarding_objp, guarded_objp)) {
10647+
guard_object_was_hit(guarding_objp, closest_mine_objp);
10648+
return 1;
10649+
}
10650+
1056910651
return 0;
1057010652
}
1057110653

@@ -10676,6 +10758,13 @@ void ai_guard_find_nearby_object()
1067610758
if ( (aip->target_objnum == -1) && asteroid_count() ) {
1067710759
ai_guard_find_nearby_asteroid(Pl_objp, guardobjp);
1067810760
}
10761+
10762+
// if still not attacking anything and flag is set, engage mines near the guarding ship itself
10763+
if (aip->target_objnum == -1 && The_mission.ai_profile->flags[AI::Profile_Flags::Ships_intercept_mines]) {
10764+
object *mine_objp = ai_find_nearby_mine_threat(Pl_objp);
10765+
if (mine_objp)
10766+
guard_object_was_hit(Pl_objp, mine_objp);
10767+
}
1067910768
}
1068010769
}
1068110770

@@ -15305,6 +15394,22 @@ void ai_frame(int objnum)
1530515394

1530615395
ai_maybe_depart(Pl_objp);
1530715396

15397+
// Imminent-mine override: even if we have a current target, swap to a mine
15398+
// that is about to detonate on us (within proximity_radius * 1.5).
15399+
// Skip departing ships (AIM_WARP_OUT/AIM_BAY_DEPART): ai_maybe_depart() ran just above and may
15400+
// have committed this ship to leaving, so we must not divert it back to a mine here.
15401+
if (The_mission.ai_profile->flags[AI::Profile_Flags::Ships_intercept_mines]
15402+
&& aip->mode != AIM_GUARD && aip->mode != AIM_EVADE_WEAPON
15403+
&& aip->mode != AIM_WARP_OUT && aip->mode != AIM_BAY_DEPART
15404+
&& Ship_info[shipp->ship_info_index].class_type > -1
15405+
&& Ship_types[Ship_info[shipp->ship_info_index].class_type].flags[Ship::Type_Info_Flags::AI_auto_attacks]) {
15406+
object *imminent_mine = ai_find_nearby_mine_threat(Pl_objp, nullptr, true);
15407+
if (imminent_mine != nullptr && (target_objnum < 0 || &Objects[target_objnum] != imminent_mine)) {
15408+
aip->aspect_locked_time = 0.0f;
15409+
target_objnum = set_target_objnum(aip, OBJ_INDEX(imminent_mine));
15410+
}
15411+
}
15412+
1530815413
// Find an enemy if don't already have one.
1530915414
En_objp = NULL;
1531015415
if ( ai_need_new_target(Pl_objp, target_objnum) ) {
@@ -15324,6 +15429,14 @@ void ai_frame(int objnum)
1532415429
{
1532515430
En_objp = &Objects[target_objnum];
1532615431
}
15432+
} else if (aip->mode != AIM_GUARD && The_mission.ai_profile->flags[AI::Profile_Flags::Ships_intercept_mines]) {
15433+
// No enemy found so check for nearby hostile mines within their targetable range
15434+
object *mine_objp = ai_find_nearby_mine_threat(Pl_objp);
15435+
if (mine_objp) {
15436+
target_objnum = set_target_objnum(aip, OBJ_INDEX(mine_objp));
15437+
if (target_objnum >= 0)
15438+
En_objp = &Objects[target_objnum];
15439+
}
1532715440
}
1532815441
}
1532915442
}

code/ai/aiturret.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,12 @@ int valid_turret_enemy(object *objp, object *turret_parent)
479479
return 0;
480480
}
481481

482+
// Mines are only targetable within their configured detection range
483+
if (wip->is_mine()) {
484+
if (vm_vec_dist(&objp->pos, &turret_parent->pos) > wip->mine_targetable_range)
485+
return 0;
486+
}
487+
482488
return 1;
483489
}
484490

code/hud/hudtarget.cpp

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,18 @@ int hud_target_invalid_awacs(object *objp)
478478
return 0;
479479
}
480480

481+
// Returns true if the weapon object can currently be targeted from source_objp.
482+
// For mines, uses range-based detection (mine_targetable_range). For other weapons, uses flags.
483+
static bool weapon_is_targetable_from(object *source_objp, object *objp)
484+
{
485+
weapon_info *wip = &Weapon_info[Weapons[objp->instance].weapon_info_index];
486+
if (wip->is_mine()) {
487+
Assertion(source_objp != nullptr, "weapon_is_targetable_from called with null source_objp");
488+
return vm_vec_dist(&source_objp->pos, &objp->pos) <= wip->mine_targetable_range;
489+
}
490+
return wip->wi_flags[Weapon::Info_Flags::Can_be_targeted] || wip->wi_flags[Weapon::Info_Flags::Bomb];
491+
}
492+
481493
ship_subsys *advance_subsys(ship_subsys *cur, int next_flag)
482494
{
483495
if (next_flag) {
@@ -1187,9 +1199,8 @@ void hud_target_common(int team_mask, int next_flag)
11871199
continue;
11881200

11891201
if (A->type == OBJ_WEAPON) {
1190-
if ( !(Weapon_info[Weapons[A->instance].weapon_info_index].wi_flags[Weapon::Info_Flags::Can_be_targeted]) )
1191-
if ( !(Weapon_info[Weapons[A->instance].weapon_info_index].wi_flags[Weapon::Info_Flags::Bomb]) )
1192-
continue;
1202+
if (!weapon_is_targetable_from(Player_obj, A))
1203+
continue;
11931204

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

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

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

15121522
if (wp->lssm_stage == 3)
@@ -2498,11 +2508,8 @@ void hud_target_in_reticle_new()
24982508
}
24992509

25002510
if ( A->type == OBJ_WEAPON ) {
2501-
if ( !(Weapon_info[Weapons[A->instance].weapon_info_index].wi_flags[Weapon::Info_Flags::Can_be_targeted]) ) {
2502-
if ( !(Weapon_info[Weapons[A->instance].weapon_info_index].wi_flags[Weapon::Info_Flags::Bomb]) ){
2503-
continue;
2504-
}
2505-
}
2511+
if (!weapon_is_targetable_from(Player_obj, A))
2512+
continue;
25062513
if (Weapons[A->instance].lssm_stage==3){
25072514
continue;
25082515
}
@@ -2607,11 +2614,8 @@ void hud_target_in_reticle_old()
26072614
}
26082615

26092616
if ( A->type == OBJ_WEAPON ) {
2610-
if ( !(Weapon_info[Weapons[A->instance].weapon_info_index].wi_flags[Weapon::Info_Flags::Can_be_targeted]) ){
2611-
if ( !(Weapon_info[Weapons[A->instance].weapon_info_index].wi_flags[Weapon::Info_Flags::Bomb]) ){
2612-
continue;
2613-
}
2614-
}
2617+
if (!weapon_is_targetable_from(Player_obj, A))
2618+
continue;
26152619

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

41894193
// only allow bombs to have lead indicator displayed
41904194
if ( targetp->type == OBJ_WEAPON ) {
4191-
if ( !(Weapon_info[Weapons[targetp->instance].weapon_info_index].wi_flags[Weapon::Info_Flags::Can_be_targeted]) ) {
4192-
if ( !(Weapon_info[Weapons[targetp->instance].weapon_info_index].wi_flags[Weapon::Info_Flags::Bomb]) ) {
4193-
return;
4194-
}
4195-
}
4195+
if (!weapon_is_targetable_from(Player_obj, targetp))
4196+
return;
41964197
}
41974198

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

43644365
// only allow bombs to have lead indicator displayed
43654366
if ( targetp->type == OBJ_WEAPON ) {
4366-
if ( !(Weapon_info[Weapons[targetp->instance].weapon_info_index].wi_flags[Weapon::Info_Flags::Can_be_targeted]) ) {
4367-
if ( !(Weapon_info[Weapons[targetp->instance].weapon_info_index].wi_flags[Weapon::Info_Flags::Bomb]) ) {
4368-
return;
4369-
}
4370-
}
4367+
if (!weapon_is_targetable_from(Player_obj, targetp))
4368+
return;
43714369
}
43724370

43734371
// If the target is out of range, then draw the correct frame for the lead indicator

code/radar/radarsetup.cpp

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ void radar_plot_object( object *objp )
177177
vec3d pos, tempv;
178178
float awacs_level, dist, max_radar_dist;
179179
vec3d world_pos = objp->pos;
180+
bool mine_in_targetable_range = true; // for mines, computed below after distance check; non-mines unaffected
180181

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

237238
case OBJ_WEAPON:
238239
{
240+
weapon_info *wip = &Weapon_info[Weapons[objp->instance].weapon_info_index];
241+
242+
if (wip->is_mine()) {
243+
// if explicitly hidden, return
244+
if (wip->wi_flags[Weapon::Info_Flags::Dont_show_on_radar])
245+
return;
246+
247+
// if we don't attack the mine, return
248+
if ( !wip->wi_flags[Weapon::Info_Flags::Show_friendly] && !iff_x_attacks_y(Player_ship->team, obj_team(objp)) )
249+
return;
250+
251+
// Mine range-based detection... visibility determined after distance is calculated below
252+
break;
253+
}
254+
239255
// if not a bomb, return
240-
if ( !(Weapon_info[Weapons[objp->instance].weapon_info_index].wi_flags[Weapon::Info_Flags::Shown_on_radar]) )
241-
if ( !(Weapon_info[Weapons[objp->instance].weapon_info_index].wi_flags[Weapon::Info_Flags::Bomb]) )
256+
if ( !(wip->wi_flags[Weapon::Info_Flags::Shown_on_radar]) )
257+
if ( !(wip->wi_flags[Weapon::Info_Flags::Bomb]) )
242258
return;
243259

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

248264
// if we don't attack the bomb, return
249-
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))))
265+
if ( !wip->wi_flags[Weapon::Info_Flags::Show_friendly] && !iff_x_attacks_y(Player_ship->team, obj_team(objp)) )
250266
return;
251267

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

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

260276
break;
@@ -281,6 +297,18 @@ void radar_plot_object( object *objp )
281297
return;
282298
}
283299

300+
// Mine range-based visibility: sensors_range is the outer detection envelope. The parser
301+
// guarantees sensors_range >= targetable_range, so anything within targetable range is
302+
// necessarily within sensors range; targetable range only governs distorted-vs-clear below.
303+
if (objp->type == OBJ_WEAPON) {
304+
weapon_info *wip = &Weapon_info[Weapons[objp->instance].weapon_info_index];
305+
if (wip->is_mine()) {
306+
if (dist > wip->mine_sensors_range)
307+
return; // beyond detection
308+
mine_in_targetable_range = (dist <= wip->mine_targetable_range);
309+
}
310+
}
311+
284312
// determine the range within which the radar blip is bright
285313
if (timestamp_elapsed(Radar_calc_bright_dist_timer))
286314
{
@@ -337,6 +365,10 @@ void radar_plot_object( object *objp )
337365

338366
// see if blip should be drawn distorted
339367
// also determine if alternate image was defined for this ship
368+
// Mines outside their targetable range (but inside sensors range, by the earlier gate) get a distorted blip
369+
if (!mine_in_targetable_range)
370+
b->flags |= BLIP_DRAW_DISTORTED;
371+
340372
if (objp->type == OBJ_SHIP)
341373
{
342374
// ships specifically hidden from sensors

0 commit comments

Comments
 (0)