diff --git a/data/json/monsters/defense_bot.json b/data/json/monsters/defense_bot.json index 6aebf8d46573f..3176e3f5d221e 100644 --- a/data/json/monsters/defense_bot.json +++ b/data/json/monsters/defense_bot.json @@ -118,6 +118,7 @@ "ranges": [ [ 0, 30, "DEFAULT" ] ], "require_targeting_npc": true, "require_targeting_monster": true, + "target_moving_vehicles": true, "laser_lock": false, "targeting_cost": 200, "targeting_timeout_extend": -10, @@ -175,6 +176,7 @@ "ranges": [ [ 0, 20, "DEFAULT" ] ], "require_targeting_npc": true, "require_targeting_monster": true, + "target_moving_vehicles": true, "laser_lock": false, "targeting_cost": 200, "targeting_timeout_extend": -10, diff --git a/data/json/monsters/drones.json b/data/json/monsters/drones.json index ca2e45992c073..1d87599258c42 100644 --- a/data/json/monsters/drones.json +++ b/data/json/monsters/drones.json @@ -165,6 +165,7 @@ "ranges": [ [ 0, 50, "DEFAULT" ] ], "require_targeting_npc": true, "require_targeting_monster": true, + "target_moving_vehicles": true, "laser_lock": true, "targeting_cost": 700, "targeting_timeout_extend": -10, diff --git a/data/json/monsters/feral_humans.json b/data/json/monsters/feral_humans.json index a43a539d4a341..b82bd72cbf6fa 100644 --- a/data/json/monsters/feral_humans.json +++ b/data/json/monsters/feral_humans.json @@ -678,6 +678,7 @@ "fake_per": 10, "ranges": [ [ 2, 35, "DEFAULT" ] ], "require_targeting_player": false, + "target_moving_vehicles": true, "description": "The mad militiaman fires their rifle!" } ], @@ -776,6 +777,7 @@ "//": "(ferals don't care about max-effective range.)", "ranges": [ [ 2, 25, "DEFAULT" ] ], "require_targeting_player": false, + "target_moving_vehicles": true, "description": "The feral biker fires their shotgun!" } ], @@ -999,6 +1001,7 @@ "fake_per": 10, "ranges": [ [ 2, 14, "DEFAULT" ] ], "require_targeting_player": false, + "target_moving_vehicles": true, "description": "The feral officer fires their M18!" } ], diff --git a/data/json/monsters/mi-go.json b/data/json/monsters/mi-go.json index c3208f5c72f2b..ae7be8d8624f8 100644 --- a/data/json/monsters/mi-go.json +++ b/data/json/monsters/mi-go.json @@ -373,6 +373,7 @@ "fake_dex": 8, "fake_per": 8, "require_targeting_player": false, + "target_moving_vehicles": true, "description": "The mi-go scout fires its weapon!", "ranges": [ [ 2, 30, "DEFAULT" ] ] } diff --git a/data/json/monsters/robofac_robots.json b/data/json/monsters/robofac_robots.json index 530e8448cdea9..17eebdc3e884b 100644 --- a/data/json/monsters/robofac_robots.json +++ b/data/json/monsters/robofac_robots.json @@ -25,6 +25,7 @@ "cooldown": 1, "gun_type": "v29_turret", "fake_skills": [ [ "gun", 4 ], [ "pistol", 4 ] ], + "target_moving_vehicles": true, "ranges": [ [ 0, 30, "DEFAULT" ] ] } ], diff --git a/data/json/monsters/turrets.json b/data/json/monsters/turrets.json index c379f52d2f3b1..79f4fe9fa60ec 100644 --- a/data/json/monsters/turrets.json +++ b/data/json/monsters/turrets.json @@ -115,6 +115,7 @@ "ranges": [ [ 0, 40, "AUTO" ], [ 41, 110, "DEFAULT" ] ], "require_targeting_npc": true, "require_targeting_monster": true, + "target_moving_vehicles": true, "laser_lock": false, "targeting_cost": 200, "targeting_timeout_extend": -10, @@ -175,6 +176,7 @@ "ranges": [ [ 0, 36, "DEFAULT" ] ], "require_targeting_npc": true, "require_targeting_monster": true, + "target_moving_vehicles": true, "laser_lock": false, "targeting_cost": 200, "targeting_timeout_extend": -10, @@ -235,6 +237,7 @@ "ranges": [ [ 0, 60, "DEFAULT" ] ], "require_targeting_npc": true, "require_targeting_monster": true, + "target_moving_vehicles": true, "laser_lock": false, "targeting_cost": 200, "targeting_timeout_extend": -10, diff --git a/data/json/monsters/yrax.json b/data/json/monsters/yrax.json index 6a32e9abdded6..99b848ba24bfb 100644 --- a/data/json/monsters/yrax.json +++ b/data/json/monsters/yrax.json @@ -40,6 +40,7 @@ "ranges": [ [ 0, 25, "DEFAULT" ] ], "require_targeting_npc": true, "require_targeting_monster": true, + "target_moving_vehicles": true, "laser_lock": true, "targeting_sound": "a fast sequence of clicking tones.", "targeting_volume": 50, diff --git a/data/json/monsters/zed-animal.json b/data/json/monsters/zed-animal.json index 3d17fe58fce01..fb1ced1d779c7 100644 --- a/data/json/monsters/zed-animal.json +++ b/data/json/monsters/zed-animal.json @@ -536,6 +536,7 @@ "fake_dex": 4, "fake_per": 4, "require_targeting_player": false, + "target_moving_vehicles": true, "description": "The vicious vicuña spits a glob of acid!", "ranges": [ [ 2, 10, "DEFAULT" ] ] }, diff --git a/data/json/monsters/zed_acid.json b/data/json/monsters/zed_acid.json index cb4025efc448b..b2bdf58f79a36 100644 --- a/data/json/monsters/zed_acid.json +++ b/data/json/monsters/zed_acid.json @@ -92,6 +92,7 @@ "fake_dex": 8, "fake_per": 8, "require_targeting_player": false, + "target_moving_vehicles": true, "description": "The corrosive zombie spits a glob of acid!", "ranges": [ [ 2, 10, "DEFAULT" ] ] }, diff --git a/data/json/monsters/zed_soldiers.json b/data/json/monsters/zed_soldiers.json index 16060f0c6fe4e..ce851273ae55e 100644 --- a/data/json/monsters/zed_soldiers.json +++ b/data/json/monsters/zed_soldiers.json @@ -143,6 +143,7 @@ "fake_dex": 8, "fake_per": 8, "require_targeting_player": false, + "target_moving_vehicles": true, "description": "The %s launches a corrosive dart!", "ranges": [ [ 2, 20, "DEFAULT" ] ] } @@ -180,6 +181,7 @@ "fake_dex": 8, "fake_per": 8, "require_targeting_player": false, + "target_moving_vehicles": true, "description": "The %s launches a hail of corrosive darts!", "ranges": [ [ 2, 8, "DEFAULT" ] ] } diff --git a/data/mods/DinoMod/monsters/dinosaur_CBM.json b/data/mods/DinoMod/monsters/dinosaur_CBM.json index 176865c9d2b68..9c2d9a6a95b21 100644 --- a/data/mods/DinoMod/monsters/dinosaur_CBM.json +++ b/data/mods/DinoMod/monsters/dinosaur_CBM.json @@ -38,6 +38,7 @@ "ranges": [ [ 0, 30, "DEFAULT" ] ], "require_targeting_npc": true, "require_targeting_monster": true, + "target_moving_vehicles": true, "laser_lock": false, "targeting_cost": 1, "targeting_timeout_extend": -1, diff --git a/data/mods/DinoMod/monsters/zed-dinosaur_CBM.json b/data/mods/DinoMod/monsters/zed-dinosaur_CBM.json index dce863c157659..7ce21414932d3 100644 --- a/data/mods/DinoMod/monsters/zed-dinosaur_CBM.json +++ b/data/mods/DinoMod/monsters/zed-dinosaur_CBM.json @@ -41,6 +41,7 @@ "ranges": [ [ 0, 30, "DEFAULT" ] ], "require_targeting_npc": true, "require_targeting_monster": true, + "target_moving_vehicles": true, "laser_lock": false, "targeting_cost": 1, "targeting_timeout_extend": -1, diff --git a/data/mods/Xedra_Evolved/monsters/bloodsuckers.json b/data/mods/Xedra_Evolved/monsters/bloodsuckers.json index fa053ddfde429..fa384dd3cab67 100644 --- a/data/mods/Xedra_Evolved/monsters/bloodsuckers.json +++ b/data/mods/Xedra_Evolved/monsters/bloodsuckers.json @@ -186,6 +186,7 @@ "fake_per": 10, "ranges": [ [ 2, 14, "DEFAULT" ] ], "require_targeting_player": false, + "target_moving_vehicles": true, "description": "The renfield fires their Glock!" } ], @@ -253,6 +254,7 @@ "//": "(ferals don't care about max-effective range.)", "ranges": [ [ 2, 25, "DEFAULT" ] ], "require_targeting_player": false, + "target_moving_vehicles": true, "description": "The renfield fires their shotgun!" } ], @@ -318,6 +320,7 @@ "no_ammo_sound": "hiss!", "ranges": [ [ 0, 10, "DEFAULT" ] ], "require_targeting_player": false, + "target_moving_vehicles": true, "description": "The renfield shoots their flamethrower!" } ], diff --git a/data/mods/Xedra_Evolved/monsters/exodii.json b/data/mods/Xedra_Evolved/monsters/exodii.json index 427082797fec9..66c43191c9858 100644 --- a/data/mods/Xedra_Evolved/monsters/exodii.json +++ b/data/mods/Xedra_Evolved/monsters/exodii.json @@ -52,6 +52,7 @@ "ranges": [ [ 4, 41, "DEFAULT" ] ], "require_targeting_npc": true, "require_targeting_monster": true, + "target_moving_vehicles": true, "laser_lock": false, "targeting_cost": 400, "targeting_timeout_extend": -10, diff --git a/doc/MONSTER_SPECIAL_ATTACKS.md b/doc/MONSTER_SPECIAL_ATTACKS.md index f5de75211e6ee..efa4055ae541a 100644 --- a/doc/MONSTER_SPECIAL_ATTACKS.md +++ b/doc/MONSTER_SPECIAL_ATTACKS.md @@ -250,6 +250,7 @@ The monster fires a gun at a target. If the monster is friendly, it will avoid | `require_targeting_player` | If true, the monster will need to "target" the player, wasting `targeting_cost` moves, putting the attack on cooldown and making warning sounds, unless it attacked something that needs to be targeted recently. Gives "grace period" to player. | | `require_targeting_npc` | As above, but with NPCs. | | `require_targeting_monster` | As above, but with monsters. | +| 'target_moving_vehicles' | If true, the monster will "target" moving vehicles even if it cannot see the player. | `targeting_timeout` | Targeting status will be applied for this many turns. Note that targeting applies to turret, not targets. | | `targeting_timeout_extend` | Successfully attacking will extend the targeting for this many turns. Can be negative. | | `targeting_cost` | Move cost of targeting the player. Only applied if attacking the player and didn't target player within last 5 turns. | diff --git a/src/map.cpp b/src/map.cpp index 726839e0eb471..64d75795fc5c6 100644 --- a/src/map.cpp +++ b/src/map.cpp @@ -1122,6 +1122,41 @@ bool map::deregister_vehicle_zone( zone_data &zone ) const return false; } +std::set map::get_moving_vehicle_targets( const Creature &z, int max_range ) +{ + const tripoint_bub_ms zpos( z.pos() ); + std::set priority; + std::set visible; + for( wrapped_vehicle &v : get_vehicles() ) { + if( !v.v->is_moving() ) { + continue; + } + if( !fov_3d && v.pos.z != zpos.z() ) { + continue; + } + if( rl_dist( zpos, tripoint_bub_ms( v.pos ) ) > max_range + 40 ) { + continue; // coarse distance filter, 40 = ~24 * sqrt(2) - rough max diameter of a vehicle + } + for( const vpart_reference &vpr : v.v->get_all_parts() ) { + const tripoint_bub_ms vppos = static_cast( vpr.pos() ); + if( rl_dist( zpos, vppos ) > max_range ) { + continue; + } + if( !z.sees( vppos ) ) { + continue; + } + if( vpr.has_feature( VPFLAG_CONTROLS ) || + vpr.has_feature( VPFLAG_ENGINE ) || + vpr.has_feature( VPFLAG_WHEEL ) ) { + priority.emplace( vppos ); + } else { + visible.emplace( vppos ); + } + } + } + return !priority.empty() ? priority : visible; +} + // 3D vehicle functions VehicleList map::get_vehicles( const tripoint &start, const tripoint &end ) diff --git a/src/map.h b/src/map.h index 959d02196b47a..e98630af5c1c4 100644 --- a/src/map.h +++ b/src/map.h @@ -675,6 +675,10 @@ class map std::vector get_vehicle_zones( int zlev ); void register_vehicle_zone( vehicle *, int zlev ); bool deregister_vehicle_zone( zone_data &zone ) const; + // returns a list of tripoints which contain parts from moving vehicles within \p max_range + // distance from \p source position, if any parts are CONTROLS, ENGINE or WHEELS returns a + // list of tripoints with exclusively such parts instead. Used for monster gun actor targeting. + std::set get_moving_vehicle_targets( const Creature &source, int max_range ); // Removes vehicle from map and returns it in unique_ptr std::unique_ptr detach_vehicle( vehicle *veh ); diff --git a/src/mattack_actors.cpp b/src/mattack_actors.cpp index 80c7ac83936aa..025aef2691fe3 100644 --- a/src/mattack_actors.cpp +++ b/src/mattack_actors.cpp @@ -34,6 +34,7 @@ #include "translations.h" #include "viewer.h" + static const efftype_id effect_badpoison( "badpoison" ); static const efftype_id effect_bite( "bite" ); static const efftype_id effect_grabbed( "grabbed" ); @@ -809,6 +810,8 @@ void gun_actor::load_internal( const JsonObject &obj, const std::string & ) laser_lock = obj.get_bool( "laser_lock", false ); + obj.read( "target_moving_vehicles", target_moving_vehicles ); + obj.read( "require_sunlight", require_sunlight ); } @@ -831,6 +834,8 @@ int gun_actor::get_max_range() const bool gun_actor::call( monster &z ) const { Creature *target; + tripoint aim_at; + bool untargeted = false; if( z.friendly ) { int max_range = get_max_range(); @@ -847,20 +852,35 @@ bool gun_actor::call( monster &z ) const } return false; } - + aim_at = target->pos(); } else { target = z.attack_target(); + aim_at = target ? target->pos() : tripoint_zero; if( !target || !z.sees( *target ) || ( !target->is_monster() && !z.aggro_character ) ) { - return false; + if( !target_moving_vehicles ) { + return false; + } + untargeted = true; // no living targets, try to find moving car parts + const std::set moving_veh_parts = get_map() + .get_moving_vehicle_targets( z, get_max_range() ); + if( moving_veh_parts.empty() ) { + return false; + } + aim_at = random_entry( moving_veh_parts, tripoint_bub_ms() ).raw(); } } - int dist = rl_dist( z.pos(), target->pos() ); - add_msg_debug( debugmode::DF_MATTACK, "Target %s at range %d", target->disp_name(), dist ); + const int dist = rl_dist( z.pos(), aim_at ); + if( target ) { + add_msg_debug( debugmode::DF_MATTACK, "Target %s at range %d", target->disp_name(), dist ); + } else { + add_msg_debug( debugmode::DF_MATTACK, "Shooting at vehicle at range %d", dist ); + } + for( const auto &e : ranges ) { if( dist >= e.first.first && dist <= e.first.second ) { - if( try_target( z, *target ) ) { - shoot( z, target->pos(), e.second ); + if( untargeted || try_target( z, *target ) ) { + shoot( z, aim_at, e.second ); } return true; } diff --git a/src/mattack_actors.h b/src/mattack_actors.h index dcae6c97f6b02..bbc21c5cb5c4e 100644 --- a/src/mattack_actors.h +++ b/src/mattack_actors.h @@ -193,6 +193,8 @@ class gun_actor : public mattack_actor /** Number of moves required for each attack */ int move_cost = 150; + /** Should moving vehicles be targeted */ + bool target_moving_vehicles = false; /*@{*/ /** Turrets may need to expend moves targeting before firing on certain targets */