diff --git a/src/character.cpp b/src/character.cpp index 2be49df116a9b..8ab8c1b147d65 100644 --- a/src/character.cpp +++ b/src/character.cpp @@ -1012,6 +1012,31 @@ std::vector Character::find_ammo( const item &obj, bool empty, in return res; } +std::vector Character::find_reloadables() +{ + std::vector reloadables; + + visit_items( [this, &reloadables]( item * node ) { + if( node->is_holster() ) { + return VisitResponse::NEXT; + } + bool reloadable = false; + if( node->is_gun() && !node->magazine_compatible().empty() ) { + reloadable = node->magazine_current() == nullptr || + node->ammo_remaining() < node->ammo_capacity(); + } else { + reloadable = ( node->is_magazine() || node->is_bandolier() || + ( node->is_gun() && node->magazine_integral() ) ) && + node->ammo_remaining() < node->ammo_capacity(); + } + if( reloadable ) { + reloadables.push_back( item_location( *this, node ) ); + } + return VisitResponse::SKIP; + } ); + return reloadables; +} + units::mass Character::weight_carried() const { return weight_carried_with_tweaks( {} ); diff --git a/src/character.h b/src/character.h index 4a51c26908e85..e29bf4fbc985c 100644 --- a/src/character.h +++ b/src/character.h @@ -561,6 +561,10 @@ class Character : public Creature, public visitable */ std::vector find_ammo( const item &obj, bool empty = true, int radius = 1 ) const; + /** + * Searches for weapons and magazines that can be reloaded. + */ + std::vector find_reloadables(); /** * Counts ammo and UPS charges (lower of) for a given gun on the character. */ diff --git a/src/game.cpp b/src/game.cpp index 82e026fd9dafc..8ca25e9d7a96f 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -9310,7 +9310,7 @@ void game::reload( int pos, bool prompt ) reload( loc, prompt ); } -void game::reload( item_location &loc, bool prompt ) +void game::reload( item_location &loc, bool prompt, bool empty ) { item *it = loc.get_item(); bool use_loc = true; @@ -9377,7 +9377,7 @@ void game::reload( item_location &loc, bool prompt ) item::reload_option opt = u.ammo_location && it->can_reload_with( u.ammo_location->typeId() ) ? item::reload_option( &u, it, it, u.ammo_location.clone() ) : - u.select_ammo( *it, prompt ); + u.select_ammo( *it, prompt, empty ); if( opt ) { u.assign_activity( activity_id( "ACT_RELOAD" ), opt.moves(), opt.qty() ); @@ -9392,39 +9392,76 @@ void game::reload( item_location &loc, bool prompt ) refresh_all(); } -void game::reload() -{ - // general reload item menu will popup if: - // - user is unarmed; - // - weapon wielded can't be reloaded (bows can, they just reload before shooting automatically) - // - weapon wielded reloads before shooting (like bows) - if( !u.is_armed() || !u.can_reload( u.weapon ) || u.weapon.has_flag( "RELOAD_AND_SHOOT" ) ) { - vehicle *veh = veh_pointer_or_null( m.veh_at( u.pos() ) ); - turret_data turret; - if( veh && ( turret = veh->turret_query( u.pos() ) ) && turret.can_reload() ) { - item::reload_option opt = g->u.select_ammo( *turret.base(), true ); - if( opt ) { - g->u.assign_activity( activity_id( "ACT_RELOAD" ), opt.moves(), opt.qty() ); - g->u.activity.targets.emplace_back( turret.base() ); - g->u.activity.targets.push_back( std::move( opt.ammo ) ); - } - return; +void game::reload( bool try_everything ) +{ + // As a special streamlined activity, hitting reload repeatedly should: + // Reload wielded gun + // First reload a magazine if necessary. + // Then load said magazine into gun. + // Reload magazines that are compatible with the current gun. + // Reload other guns in inventory. + // Reload misc magazines in inventory. + std::vector reloadables = u.find_reloadables(); + std::sort( reloadables.begin(), reloadables.end(), + [this]( const item_location & a, const item_location & b ) { + const item *ap = a.get_item(); + const item *bp = b.get_item(); + // Current wielded weapon comes first. + if( this->u.is_wielding( *ap ) ) { + return true; } - - item_location item_loc = inv_map_splice( [&]( const item & it ) { - return u.rate_action_reload( it ) == HINT_GOOD; - }, _( "Reload item" ), 1, _( "You have nothing to reload." ) ); - - if( !item_loc ) { - add_msg( _( "Never mind." ) ); + if( this->u.is_wielding( *bp ) ) { + return false; + } + // Second sort by afiliation with wielded gun + const std::set compatible_magazines = this->u.weapon.magazine_compatible(); + const bool mag_ap = compatible_magazines.count( ap->typeId() ) > 0; + const bool mag_bp = compatible_magazines.count( bp->typeId() ) > 0; + if( mag_ap != mag_bp ) { + return mag_ap; + } + // Third sort by gun vs magazine, + if( ap->is_gun() != bp->is_gun() ) { + return ap->is_gun(); + } + // Finally sort by speed to reload. + return ( ap->get_reload_time() * ( ap->ammo_capacity() - ap->ammo_remaining() ) ) < + ( bp->get_reload_time() * ( bp->ammo_capacity() - bp->ammo_remaining() ) ); + } ); + for( item_location &candidate : reloadables ) { + std::vector ammo_list; + u.list_ammo( *candidate.get_item(), ammo_list, false ); + if( !ammo_list.empty() ) { + reload( candidate, false, false ); return; } + } + // Just for testing, bail out here to avoid unwanted side effects. + if( !try_everything ) { + return; + } + // If we make it here and haven't found anything to reload, start looking elsewhere. + vehicle *veh = veh_pointer_or_null( m.veh_at( u.pos() ) ); + turret_data turret; + if( veh && ( turret = veh->turret_query( u.pos() ) ) && turret.can_reload() ) { + item::reload_option opt = g->u.select_ammo( *turret.base(), true ); + if( opt ) { + g->u.assign_activity( activity_id( "ACT_RELOAD" ), opt.moves(), opt.qty() ); + g->u.activity.targets.emplace_back( turret.base() ); + g->u.activity.targets.push_back( std::move( opt.ammo ) ); + } + return; + } + item_location item_loc = inv_map_splice( [&]( const item & it ) { + return u.rate_action_reload( it ) == HINT_GOOD; + }, _( "Reload item" ), 1, _( "You have nothing to reload." ) ); - reload( item_loc ); - - } else { - reload( -1 ); + if( !item_loc ) { + add_msg( _( "Never mind." ) ); + return; } + + reload( item_loc ); } // Unload a container, gun, or tool diff --git a/src/game.h b/src/game.h index 379fae210791e..02a151c74867a 100644 --- a/src/game.h +++ b/src/game.h @@ -965,12 +965,12 @@ class game void use_item( int pos = INT_MIN ); // Use item; also tries E,R,W 'a' void change_side( int pos = INT_MIN ); // Change the side on which an item is worn 'c' - void reload(); // Reload a wielded gun/tool 'r' void reload( int pos, bool prompt = false ); - void reload( item_location &loc, bool prompt = false ); + void reload( item_location &loc, bool prompt = false, bool empty = true ); void mend( int pos = INT_MIN ); void autoattack(); public: + void reload( bool try_everything = true ); // Reload a wielded gun/tool 'r' // Places the player at the specified point; hurts feet, lists items etc. void place_player( const tripoint &dest ); void place_player_overmap( const tripoint &om_dest ); diff --git a/src/item.cpp b/src/item.cpp index 8ab16558d8176..54a63c09679e3 100644 --- a/src/item.cpp +++ b/src/item.cpp @@ -4388,11 +4388,11 @@ bool item::is_firearm() const int item::get_reload_time() const { - if( !is_gun() ) { + if( !is_gun() && !is_magazine() ) { return 0; } - int reload_time = type->gun->reload_time; + int reload_time = is_gun() ? type->gun->reload_time : type->magazine->reload_time; for( const auto mod : gunmods() ) { reload_time = static_cast( reload_time * ( 100 + mod->type->gunmod->reload_modifier ) / 100 ); } diff --git a/src/player.cpp b/src/player.cpp index cccf534ab45ea..6948445e92703 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -7372,11 +7372,9 @@ void player::rooted() item::reload_option player::select_ammo( const item &base, std::vector opts ) const { - using reload_option = item::reload_option; - if( opts.empty() ) { add_msg_if_player( m_info, _( "Never mind." ) ); - return reload_option(); + return item::reload_option(); } uilist menu; @@ -7389,7 +7387,7 @@ item::reload_option player::select_ammo( const item &base, // Construct item names std::vector names; std::transform( opts.begin(), opts.end(), - std::back_inserter( names ), [&]( const reload_option & e ) { + std::back_inserter( names ), [&]( const item::reload_option & e ) { if( e.ammo->is_magazine() && e.ammo->ammo_data() ) { if( e.ammo->ammo_current() == "battery" ) { // This battery ammo is not a real object that can be recovered but pseudo-object that represents charge @@ -7414,7 +7412,7 @@ item::reload_option player::select_ammo( const item &base, // Get location descriptions std::vector where; std::transform( opts.begin(), opts.end(), - std::back_inserter( where ), []( const reload_option & e ) { + std::back_inserter( where ), []( const item::reload_option & e ) { bool is_ammo_container = e.ammo->is_ammo_container(); if( is_ammo_container || e.ammo->is_container() ) { if( is_ammo_container && g->u.is_worn( *e.ammo ) ) { @@ -7586,7 +7584,7 @@ item::reload_option player::select_ammo( const item &base, menu.query(); if( menu.ret < 0 || static_cast( menu.ret ) >= opts.size() ) { add_msg_if_player( m_info, _( "Never mind." ) ); - return reload_option(); + return item::reload_option(); } const item_location &sel = opts[ menu.ret ].ammo; @@ -7595,11 +7593,9 @@ item::reload_option player::select_ammo( const item &base, return std::move( opts[ menu.ret ] ); } -item::reload_option player::select_ammo( const item &base, bool prompt ) const +bool player::list_ammo( const item &base, std::vector &ammo_list, + bool empty ) const { - using reload_option = item::reload_option; - std::vector ammo_list; - auto opts = base.gunmods(); opts.push_back( &base ); @@ -7615,7 +7611,7 @@ item::reload_option player::select_ammo( const item &base, bool prompt ) const bool ammo_match_found = false; for( const auto e : opts ) { - for( item_location &ammo : find_ammo( *e ) ) { + for( item_location &ammo : find_ammo( *e, empty ) ) { // don't try to unload frozen liquids if( ammo->is_watertight_container() && ammo->contents_made_of( SOLID ) ) { continue; @@ -7634,6 +7630,13 @@ item::reload_option player::select_ammo( const item &base, bool prompt ) const } } } + return ammo_match_found; +} + +item::reload_option player::select_ammo( const item &base, bool prompt, bool empty ) const +{ + std::vector ammo_list; + bool ammo_match_found = list_ammo( base, ammo_list, empty ); if( ammo_list.empty() ) { if( !base.is_magazine() && !base.magazine_integral() && !base.magazine_current() ) { @@ -7654,20 +7657,20 @@ item::reload_option player::select_ammo( const item &base, bool prompt ) const add_msg_if_player( m_info, _( "You don't have any %s to reload your %s!" ), name.c_str(), base.tname() ); } - return reload_option(); + return item::reload_option(); } // sort in order of move cost (ascending), then remaining ammo (descending) with empty magazines always last - std::stable_sort( ammo_list.begin(), ammo_list.end(), []( const reload_option & lhs, - const reload_option & rhs ) { + std::stable_sort( ammo_list.begin(), ammo_list.end(), []( const item::reload_option & lhs, + const item::reload_option & rhs ) { return lhs.ammo->ammo_remaining() > rhs.ammo->ammo_remaining(); } ); - std::stable_sort( ammo_list.begin(), ammo_list.end(), []( const reload_option & lhs, - const reload_option & rhs ) { + std::stable_sort( ammo_list.begin(), ammo_list.end(), []( const item::reload_option & lhs, + const item::reload_option & rhs ) { return lhs.moves() < rhs.moves(); } ); - std::stable_sort( ammo_list.begin(), ammo_list.end(), []( const reload_option & lhs, - const reload_option & rhs ) { + std::stable_sort( ammo_list.begin(), ammo_list.end(), []( const item::reload_option & lhs, + const item::reload_option & rhs ) { return ( lhs.ammo->ammo_remaining() != 0 ) > ( rhs.ammo->ammo_remaining() != 0 ); } ); diff --git a/src/player.h b/src/player.h index d3989de7ca67d..702e69867ada1 100644 --- a/src/player.h +++ b/src/player.h @@ -894,12 +894,17 @@ class player : public Character void rooted_message() const; void rooted(); int get_lift_assist() const; + + bool list_ammo( const item &base, std::vector &ammo_list, + bool empty = true ) const; /** * Select suitable ammo with which to reload the item * @param base Item to select ammo for * @param prompt force display of the menu even if only one choice + * @param empty allow selection of empty magazines */ - item::reload_option select_ammo( const item &base, bool prompt = false ) const; + item::reload_option select_ammo( const item &base, bool prompt = false, + bool empty = true ) const; /** Select ammo from the provided options */ item::reload_option select_ammo( const item &base, std::vector opts ) const; diff --git a/tests/player_helpers.cpp b/tests/player_helpers.cpp index 12ac14c593728..33f0c0e46bcc6 100644 --- a/tests/player_helpers.cpp +++ b/tests/player_helpers.cpp @@ -63,3 +63,13 @@ void clear_player() const tripoint spot( 60, 60, 0 ); dummy.setpos( spot ); } + +void process_activity( player &dummy ) +{ + do { + dummy.moves += dummy.get_speed(); + while( dummy.moves > 0 && dummy.activity ) { + dummy.activity.do_turn( dummy ); + } + } while( dummy.activity ); +} diff --git a/tests/player_helpers.h b/tests/player_helpers.h index 96155b83d93ab..f9afdaf7ca642 100644 --- a/tests/player_helpers.h +++ b/tests/player_helpers.h @@ -2,6 +2,8 @@ #ifndef PLAYER_HELPERS_H #define PLAYER_HELPERS_H +#include "player.h" + #include struct itype; @@ -9,5 +11,6 @@ struct itype; int get_remaining_charges( const std::string &tool_id ); bool player_has_item_of_type( const std::string & ); void clear_player(); +void process_activity( player &dummy ); #endif diff --git a/tests/reloading_test.cpp b/tests/reloading_test.cpp index b026eb554524f..bda9306a59ca5 100644 --- a/tests/reloading_test.cpp +++ b/tests/reloading_test.cpp @@ -5,15 +5,15 @@ #include "itype.h" #include "player.h" +#include "player_helpers.h" + TEST_CASE( "reload_gun_with_integral_magazine", "[reload],[gun]" ) { player &dummy = g->u; - // Remove first worn item until there are none left. - std::list temp; - while( dummy.takeoff( dummy.i_at( -2 ), &temp ) ); - - dummy.remove_weapon(); + clear_player(); + // Make sure the player doesn't drop anything :P + dummy.wear_item( item( "backpack", 0 ) ); item &ammo = dummy.i_add( item( "40sw", 0, item::default_charges_tag{} ) ); item &gun = dummy.i_add( item( "sw_610", 0, item::default_charges_tag{} ) ); @@ -33,11 +33,9 @@ TEST_CASE( "reload_gun_with_integral_magazine_using_speedloader", "[reload],[gun { player &dummy = g->u; - // Remove first worn item until there are none left. - std::list temp; - while( dummy.takeoff( dummy.i_at( -2 ), &temp ) ); - - dummy.remove_weapon(); + clear_player(); + // Make sure the player doesn't drop anything :P + dummy.wear_item( item( "backpack", 0 ) ); item &ammo = dummy.i_add( item( "38_special", 0, item::default_charges_tag{} ) ); item &speedloader = dummy.i_add( item( "38_speedloader", 0, false ) ); @@ -70,14 +68,10 @@ TEST_CASE( "reload_gun_with_swappable_magazine", "[reload],[gun]" ) { player &dummy = g->u; - // Remove first worn item until there are none left. - std::list temp; - while( dummy.takeoff( dummy.i_at( -2 ), &temp ) ); + clear_player(); // Make sure the player doesn't drop anything :P dummy.wear_item( item( "backpack", 0 ) ); - dummy.remove_weapon(); - item &ammo = dummy.i_add( item( "9mm", 0, item::default_charges_tag{} ) ); const cata::optional &ammo_type = ammo.type->ammo; REQUIRE( ammo_type ); @@ -117,3 +111,157 @@ TEST_CASE( "reload_gun_with_swappable_magazine", "[reload],[gun]" ) CHECK( gun_success ); REQUIRE( gun.ammo_remaining() == gun.ammo_capacity() ); } + +void reload_a_revolver( player &dummy, item &gun, item &ammo ) +{ + while( gun.ammo_remaining() < gun.ammo_capacity() ) { + g->reload( false ); + REQUIRE( dummy.activity ); + process_activity( dummy ); + CHECK( gun.ammo_remaining() > 0 ); + CHECK( gun.ammo_current() == ammo.type->get_id() ); + } +} + +TEST_CASE( "automatic_reloading_action", "[reload],[gun]" ) +{ + player &dummy = g->u; + + clear_player(); + // Make sure the player doesn't drop anything :P + dummy.wear_item( item( "backpack", 0 ) ); + + GIVEN( "an unarmed player" ) { + REQUIRE( !dummy.is_armed() ); + WHEN( "the player triggers auto reload" ) { + g->reload( false ); + THEN( "No activity is generated" ) { + CHECK( !dummy.activity ); + } + } + } + + GIVEN( "a player armed with a revolver and ammo for it" ) { + item &ammo = dummy.i_add( item( "40sw", 0, item::default_charges_tag{} ) ); + REQUIRE( ammo.is_ammo() ); + + dummy.weapon = item( "sw_610", 0, 0 ); + REQUIRE( dummy.weapon.ammo_remaining() == 0 ); + REQUIRE( dummy.weapon.can_reload_with( ammo.type->get_id() ) ); + + WHEN( "the player triggers auto reload until the revolver is full" ) { + reload_a_revolver( dummy, dummy.weapon, ammo ); + WHEN( "the player triggers auto reload again" ) { + g->reload( false ); + THEN( "no activity is generated" ) { + CHECK( !dummy.activity ); + } + } + } + GIVEN( "the player has another gun with ammo" ) { + item &gun2 = dummy.i_add( item( "sw_610", 0, 0 ) ); + REQUIRE( gun2.ammo_remaining() == 0 ); + REQUIRE( gun2.can_reload_with( ammo.type->get_id() ) ); + WHEN( "the player triggers auto reload until the first revolver is full" ) { + reload_a_revolver( dummy, dummy.weapon, ammo ); + WHEN( "the player triggers auto reload until the second revolver is full" ) { + reload_a_revolver( dummy, gun2, ammo ); + WHEN( "the player triggers auto reload again" ) { + g->reload( false ); + THEN( "no activity is generated" ) { + CHECK( !dummy.activity ); + } + } + } + } + } + } + + GIVEN( "a player wielding an unloaded gun, carrying an unloaded magazine, and carrying ammo for the magazine" ) { + item &ammo = dummy.i_add( item( "9mm", 0, 50 ) ); + const cata::optional &ammo_type = ammo.type->ammo; + REQUIRE( ammo_type ); + + item &mag = dummy.i_add( item( "glockmag", 0, 0 ) ); + const cata::optional &magazine_type = mag.type->magazine; + REQUIRE( magazine_type ); + REQUIRE( ammo_type->type.count( magazine_type->type ) != 0 ); + REQUIRE( mag.ammo_remaining() == 0 ); + + dummy.weapon = item( "glock_19", 0, 0 ); + REQUIRE( dummy.weapon.ammo_remaining() == 0 ); + + WHEN( "the player triggers auto reload" ) { + g->reload( false ); + REQUIRE( dummy.activity ); + process_activity( dummy ); + + THEN( "the associated magazine is reloaded" ) { + CHECK( mag.ammo_remaining() > 0 ); + CHECK( mag.contents.front().type == ammo.type ); + } + WHEN( "the player triggers auto reload again" ) { + g->reload( false ); + REQUIRE( dummy.activity ); + process_activity( dummy ); + + THEN( "The magazine is loaded into the gun" ) { + CHECK( dummy.weapon.ammo_remaining() > 0 ); + } + WHEN( "the player triggers auto reload again" ) { + g->reload( false ); + THEN( "No activity is generated" ) { + CHECK( !dummy.activity ); + } + } + } + } + GIVEN( "the player also has an extended magazine" ) { + item &mag2 = dummy.i_add( item( "glockbigmag", 0, 0 ) ); + const cata::optional &magazine_type2 = mag2.type->magazine; + REQUIRE( magazine_type2 ); + REQUIRE( ammo_type->type.count( magazine_type2->type ) != 0 ); + REQUIRE( mag2.ammo_remaining() == 0 ); + + WHEN( "the player triggers auto reload" ) { + g->reload( false ); + REQUIRE( dummy.activity ); + process_activity( dummy ); + + THEN( "the associated magazine is reloaded" ) { + CHECK( mag.ammo_remaining() > 0 ); + CHECK( mag.contents.front().type == ammo.type ); + } + WHEN( "the player triggers auto reload again" ) { + g->reload( false ); + REQUIRE( dummy.activity ); + process_activity( dummy ); + + THEN( "The magazine is loaded into the gun" ) { + CHECK( dummy.weapon.ammo_remaining() > 0 ); + } + WHEN( "the player triggers auto reload again" ) { + g->reload( false ); + REQUIRE( dummy.activity ); + process_activity( dummy ); + + THEN( "the second associated magazine is reloaded" ) { + CHECK( mag2.ammo_remaining() > 0 ); + CHECK( mag2.contents.front().type == ammo.type ); + } + WHEN( "the player triggers auto reload again" ) { + g->reload( false ); + THEN( "No activity is generated" ) { + CHECK( !dummy.activity ); + } + } + } + } + } + GIVEN( "the player also has another gun with ammo" ) { + item &mag2 = dummy.i_add( item( "glockbigmag", 0, 0 ) ); + const cata::optional &magazine_type2 = mag2.type->magazine; + } + } + } +}