Skip to content

Commit 2fbb322

Browse files
fix: stop turrets targeting through followers (#8547)
Assisted-by: openai/gpt-5.4 on opencode Co-authored-by: chatgpt-codex-connector[bot] <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com>
1 parent 6d97137 commit 2fbb322

File tree

2 files changed

+124
-37
lines changed

2 files changed

+124
-37
lines changed

src/creature_functions.cpp

Lines changed: 57 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
#include <vector>
1+
#include <algorithm>
22
#include <set>
3+
#include <vector>
34

45
#include "creature_functions.h"
56
#include "avatar.h"
@@ -50,31 +51,52 @@ auto auto_find_hostile_target(
5051
// iff safety margin (degrees). less accuracy, more paranoia
5152
units::angle iff_hangle = units::from_degrees( 15 + option.area );
5253
float best_target_rating = -1.0f; // bigger is better
53-
units::angle u_angle = {}; // player angle relative to turret
5454
int boo_hoo = 0; // how many targets were passed due to IFF. Tragically.
5555
bool self_area_iff = false; // Need to check if the target is near the vehicle we're a part of
56-
bool area_iff = false; // Need to check distance from target to player
57-
bool angle_iff = true; // Need to check if player is in a cone between us and target
58-
int pldist = rl_dist( creature.pos(), u.pos() );
5956
map &here = get_map();
6057
vehicle *in_veh = creature.is_fake()
6158
? veh_pointer_or_null( here.veh_at( creature.pos() ) ) : nullptr;
62-
// Skip IFF for adjacent player if weapon is safe (bullets/rockets protected by ballistics).
63-
// Always apply IFF for weapons with dangerous trails (lasers) even when adjacent.
64-
const bool apply_iff = pldist < iff_dist && ( option.trail || pldist > 1 ) && creature.sees( u );
65-
if( apply_iff ) {
66-
area_iff = option.area > 0;
67-
angle_iff = true;
68-
// Player inside vehicle won't be hit by shots from the roof,
69-
// so we can fire "through" them just fine.
70-
const optional_vpart_position vp = here.veh_at( u.pos() );
59+
60+
struct iff_guard_creature {
61+
const Creature *critter = nullptr;
62+
int dist = 0;
63+
bool area_iff = false;
64+
bool angle_iff = true;
65+
units::angle angle = {};
66+
units::angle iff_hangle = {};
67+
};
68+
69+
auto protected_creatures = std::vector<iff_guard_creature> {};
70+
for( Creature *const critter : g->get_creatures_if( [&]( const Creature & other ) {
71+
return &other != &creature && creature.attitude_to( other ) == Attitude::A_FRIENDLY;
72+
} ) ) {
73+
const auto critter_dist = rl_dist( creature.pos(), critter->pos() );
74+
// Skip IFF for adjacent friendlies if weapon is safe (bullets/rockets protected by ballistics).
75+
// Always apply IFF for weapons with dangerous trails (lasers) even when adjacent.
76+
if( critter_dist >= iff_dist || ( !option.trail && critter_dist <= 1 ) ||
77+
!creature.sees( *critter ) ) {
78+
continue;
79+
}
80+
81+
auto guard = iff_guard_creature{
82+
.critter = critter,
83+
.dist = critter_dist,
84+
.area_iff = option.area > 0,
85+
.angle = coord_to_angle( creature.pos(), critter->pos() ),
86+
.iff_hangle = iff_hangle,
87+
};
88+
89+
// Occupants inside the same vehicle are safe from the turret's direct line of fire,
90+
// but still need AoE protection.
91+
const optional_vpart_position vp = here.veh_at( critter->pos() );
7192
if( in_veh && veh_pointer_or_null( vp ) == in_veh && vp->is_inside() ) {
72-
angle_iff = false; // No angle IFF, but possibly area IFF
73-
} else if( pldist < 3 ) {
93+
guard.angle_iff = false;
94+
} else if( critter_dist < 3 ) {
7495
// granularity increases with proximity
75-
iff_hangle = ( pldist == 2 ? 30_degrees : 60_degrees );
96+
guard.iff_hangle = critter_dist == 2 ? 30_degrees : 60_degrees;
7697
}
77-
u_angle = coord_to_angle( creature.pos(), u.pos() );
98+
99+
protected_creatures.push_back( guard );
78100
}
79101

80102
if( option.area > 0 && in_veh != nullptr ) {
@@ -141,34 +163,33 @@ auto auto_find_hostile_target(
141163
// No shooting stuff on vehicle we're a part of
142164
continue;
143165
}
144-
if( area_iff && rl_dist( u.pos(), m->pos() ) <= option.area ) {
145-
// Player in AoE
146-
boo_hoo++;
147-
continue;
148-
}
149-
// Hostility check can be expensive, but we need to inform the player of boo_hoo
150-
// only when the target is actually "hostile enough"
151-
bool maybe_boo = false;
152-
if( angle_iff ) {
153-
units::angle tangle = coord_to_angle( creature.pos(), m->pos() );
154-
units::angle diff = units::fabs( u_angle - tangle );
155-
// Player is in the angle and not too far behind the target
156-
if( ( diff + iff_hangle > 360_degrees || diff < iff_hangle ) &&
157-
( dist * 3 / 2 + 6 > pldist ) ) {
158-
maybe_boo = true;
166+
const auto target_angle = coord_to_angle( creature.pos(), m->pos() );
167+
const auto blocked_by_friendly = std::ranges::any_of( protected_creatures,
168+
[&]( const iff_guard_creature & guard ) {
169+
if( guard.area_iff && rl_dist( guard.critter->pos(), m->pos() ) <= option.area ) {
170+
return true;
159171
}
160-
}
161-
if( !maybe_boo && ( ( mon_rating + hostile_adj ) / dist <= best_target_rating ) ) {
172+
if( !guard.angle_iff ) {
173+
return false;
174+
}
175+
176+
const auto diff = units::fabs( guard.angle - target_angle );
177+
return ( diff + guard.iff_hangle > 360_degrees || diff < guard.iff_hangle ) &&
178+
( dist * 3 / 2 + 6 > guard.dist );
179+
} );
180+
if( !blocked_by_friendly && ( ( mon_rating + hostile_adj ) / dist <= best_target_rating ) ) {
162181
// "Would we skip the target even if it was hostile?"
163182
// Helps avoid (possibly expensive) attitude calculation
164183
continue;
165184
}
166185
if( m->attitude_to( u ) == Attitude::A_HOSTILE ) {
167186
target_rating = ( mon_rating + hostile_adj ) / dist;
168-
if( maybe_boo ) {
187+
if( blocked_by_friendly ) {
169188
boo_hoo++;
170189
continue;
171190
}
191+
} else if( blocked_by_friendly ) {
192+
continue;
172193
}
173194
if( target_rating <= best_target_rating || target_rating <= 0 ) {
174195
continue; // Handle this late so that boo_hoo++ can happen

tests/vehicle_turrets_test.cpp

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,24 @@
22

33
#include <algorithm>
44
#include <map>
5-
#include <memory>
65
#include <ranges>
76
#include <utility>
87
#include <vector>
98

109
#include "ammo.h"
1110
#include "avatar.h"
1211
#include "calendar.h"
12+
#include "creature_functions.h"
13+
#include "faction.h"
1314
#include "game.h"
1415
#include "item.h"
1516
#include "itype.h"
1617
#include "map.h"
18+
#include "map_helpers.h"
19+
#include "monster.h"
20+
#include "npc.h"
1721
#include "point.h"
22+
#include "player_helpers.h"
1823
#include "state_helpers.h"
1924
#include "string_id.h"
2025
#include "type_id.h"
@@ -154,3 +159,64 @@ TEST_CASE( "vehicle_turret_autoloader_integral_magazine", "[vehicle][gun][turret
154159
}
155160
REQUIRE( gun.ammo_remaining() == ammo_capacity );
156161
}
162+
163+
TEST_CASE( "vehicle_turret_iff_protects_followers_in_line_of_fire", "[vehicle][turret][npc][iff]" )
164+
{
165+
clear_all_state();
166+
build_test_map( ter_id( "t_dirt" ) );
167+
map &here = get_map();
168+
set_time( calendar::turn_zero + 12_hours );
169+
170+
const auto shooter_pos = tripoint( 60, 60, 0 );
171+
avatar &shooter = get_avatar();
172+
shooter.setpos( shooter_pos );
173+
shooter.set_body();
174+
175+
const auto follower_pos = shooter_pos + point( 3, 0 );
176+
npc &follower = spawn_npc( follower_pos.xy(), "thug" );
177+
follower.set_fac( faction_id( "your_followers" ) );
178+
follower.set_attitude( NPCATT_FOLLOW );
179+
REQUIRE( follower.is_player_ally() );
180+
REQUIRE( shooter.attitude_to( follower ) == Attitude::A_FRIENDLY );
181+
182+
const auto hostile_pos = shooter_pos + point( 8, 0 );
183+
monster &hostile = spawn_test_monster( "mon_zombie_tough", hostile_pos );
184+
here.invalidate_map_cache( shooter_pos.z );
185+
here.build_map_cache( shooter_pos.z, true );
186+
REQUIRE( shooter.sees( hostile ) );
187+
188+
const auto target = creature_functions::auto_find_hostile_target(
189+
shooter, { .range = 20, .trail = false, .area = 0 } );
190+
REQUIRE_FALSE( target.has_value() );
191+
CHECK( target.error() == 1 );
192+
}
193+
194+
TEST_CASE( "vehicle_turret_iff_allows_clear_shots", "[vehicle][turret][npc][iff]" )
195+
{
196+
clear_all_state();
197+
build_test_map( ter_id( "t_dirt" ) );
198+
map &here = get_map();
199+
set_time( calendar::turn_zero + 12_hours );
200+
201+
const auto shooter_pos = tripoint( 60, 60, 0 );
202+
avatar &shooter = get_avatar();
203+
shooter.setpos( shooter_pos );
204+
shooter.set_body();
205+
206+
const auto follower_pos = shooter_pos + point( 0, 5 );
207+
npc &follower = spawn_npc( follower_pos.xy(), "thug" );
208+
follower.set_fac( faction_id( "your_followers" ) );
209+
follower.set_attitude( NPCATT_FOLLOW );
210+
REQUIRE( follower.is_player_ally() );
211+
212+
const auto hostile_pos = shooter_pos + point( 8, 0 );
213+
monster &hostile = spawn_test_monster( "mon_zombie_tough", hostile_pos );
214+
here.invalidate_map_cache( shooter_pos.z );
215+
here.build_map_cache( shooter_pos.z, true );
216+
REQUIRE( shooter.sees( hostile ) );
217+
218+
const auto target = creature_functions::auto_find_hostile_target(
219+
shooter, { .range = 20, .trail = false, .area = 0 } );
220+
REQUIRE( target.has_value() );
221+
CHECK( &target->get() == &hostile );
222+
}

0 commit comments

Comments
 (0)