diff --git a/data/json/flags.json b/data/json/flags.json
index 8293735e87f0b..9b2e6b95251e8 100644
--- a/data/json/flags.json
+++ b/data/json/flags.json
@@ -1,80 +1,85 @@
[
+ {
+ "id": "DEBUG_ONLY",
+ "type": "json_flag",
+ "info": "This is not intended to be visible during normal gameplay. Please file a bug report unless you are seeing this in the debug menu."
+ },
{
"id": "ABLATIVE_CHAINMAIL_ARMS",
"type": "json_flag",
- "info": "You can use this armor with chainmail with out encumbrance penalty",
+ "info": "You can use this armor with chainmail without encumbrance penalty.",
"restriction": "Item must be a chainmail compatible armor piece"
},
{
"id": "ABLATIVE_CHAINMAIL_ELBOWS",
"type": "json_flag",
- "info": "You can use this armor with chainmail with out encumbrance penalty",
+ "info": "You can use this armor with chainmail without encumbrance penalty.",
"restriction": "Item must be a chainmail compatible armor piece"
},
{
"id": "ABLATIVE_CHAINMAIL_LEGS",
"type": "json_flag",
- "info": "You can use this armor with chainmail with out encumbrance penalty",
+ "info": "You can use this armor with chainmail without encumbrance penalty.",
"restriction": "Item must be a chainmail compatible armor piece"
},
{
"id": "ABLATIVE_CHAINMAIL_KNEES",
"type": "json_flag",
- "info": "You can use this armor with chainmail with out encumbrance penalty",
+ "info": "You can use this armor with chainmail without encumbrance penalty.",
"restriction": "Item must be a chainmail compatible armor piece"
},
{
"id": "ABLATIVE_CHAINMAIL_TORSO",
"type": "json_flag",
- "info": "You can use this armor with chainmail with out encumbrance penalty",
+ "info": "You can use this armor with chainmail without encumbrance penalty.",
"restriction": "Item must be a chainmail compatible armor piece"
},
{
"id": "ABLATIVE_LARGE",
"type": "json_flag",
- "info": "This plate will fit in large armor pockets",
+ "info": "This plate will fit in large armor pockets.",
"restriction": "Item must be a large ablative plate"
},
{
"id": "ABLATIVE_MANTLE",
"type": "json_flag",
- "info": "This will hook to a Hub 01 proprietary mantle connector",
+ "info": "This will hook to a Hub 01 proprietary mantle connector.",
"restriction": "Item must be an armored mantle"
},
{
"id": "STAR_PLATE",
"type": "json_flag",
- "info": "This can be supported by the ryūsei battle kit",
+ "info": "This can be supported by the ryūsei battle kit.",
"restriction": "Item must be resonance plate"
},
{
"id": "STAR_SHOULDER",
"type": "json_flag",
- "info": "This can be supported by the ryūsei battle kit",
+ "info": "This can be supported by the ryūsei battle kit.",
"restriction": "Item must be resonance shoulder"
},
{
"id": "STAR_SKIRT",
"type": "json_flag",
- "info": "This can be supported by the ryūsei battle kit",
+ "info": "This can be supported by the ryūsei battle kit.",
"restriction": "Item must be resonance skirt"
},
{
"id": "ABLATIVE_MEDIUM",
"type": "json_flag",
- "info": "This plate will fit in medium armor pockets",
+ "info": "This plate will fit in medium armor pockets.",
"restriction": "Item must be a medium ablative plate"
},
{
"id": "ABLATIVE_SKIRT",
"type": "json_flag",
- "info": "This will hook to a Hub 01 proprietary skirt connector",
+ "info": "This will hook to a Hub 01 proprietary skirt connector.",
"restriction": "Item must be an armored skirt"
},
{
"id": "ABLATIVE_HELMET",
"type": "json_flag",
- "info": "This will hook to a Hub 01 proprietary helmet connector",
+ "info": "This will hook to a Hub 01 proprietary helmet connector.",
"restriction": "Item must be an armored helmet"
},
{
@@ -406,13 +411,13 @@
"id": "HEAD_STRAP_MOUNT",
"type": "json_flag",
"info": "This item is attachable to straps and mountings on some masks and hoods.",
- "restriction": "Item must be some kind of item that can be attached to straps on masks and hoods."
+ "restriction": "Item must be some kind of item that can be attached to straps on masks and hoods"
},
{
"id": "WRIST_MOUNT_ATTACHMENT",
"type": "json_flag",
"info": "This item is attachable to a proprietary wrist mount.",
- "restriction": "Item must be some kind of item that can be attached to a proprietary wrist mount."
+ "restriction": "Item must be some kind of item that can be attached to a proprietary wrist mount"
},
{
"id": "EXTRA_PLATING",
diff --git a/data/json/items/fake.json b/data/json/items/fake.json
index 1d2860909d489..385a191b48b68 100644
--- a/data/json/items/fake.json
+++ b/data/json/items/fake.json
@@ -3,14 +3,22 @@
"abstract": "fake_item",
"type": "GENERIC",
"name": { "str": "fake item" },
- "description": "Dummy item. If you see this, then something went wrong.",
+ "description": "Dummy item.",
"//": "Include ZERO_WEIGHT flag to avoid errors when instantiating pseudo items.",
- "flags": [ "ZERO_WEIGHT" ],
+ "flags": [ "ZERO_WEIGHT", "TRADER_AVOID", "DEBUG_ONLY" ],
"weight": "1 g",
"volume": "1 ml",
"symbol": "@",
"color": "red"
},
+ {
+ "id": "fake_crafting_tool",
+ "copy-from": "fake_item",
+ "type": "TOOL",
+ "name": { "str": "fake crafting tool" },
+ "description": "Used for practice recipes and the like.",
+ "extend": { "flags": [ "ALLOWS_REMOTE_USE", "PSEUDO" ] }
+ },
{
"id": "fake_arcfurnace",
"copy-from": "fake_item",
@@ -195,7 +203,6 @@
{
"abstract": "fake_appliance_tool",
"name": { "str": "fake appliance tool" },
- "description": "Dummy item. If you see this, then something went wrong.",
"type": "TOOL",
"copy-from": "fake_item",
"ammo": [ "battery" ],
@@ -279,82 +286,74 @@
"id": "pseudo_exercise_machine",
"type": "TOOL",
"name": { "str": "exercise machine" },
- "description": "This is a crafting_pseudo_item if you have it something is wrong.",
+ "copy-from": "fake_crafting_tool",
"material": [ "steel" ],
"symbol": "T",
- "color": "dark_gray",
- "flags": [ "ALLOWS_REMOTE_USE", "PSEUDO" ]
+ "color": "dark_gray"
},
{
"id": "pseudo_punching_bag",
"type": "TOOL",
"name": { "str": "heavy punching bag" },
- "description": "This is a crafting_pseudo_item if you have it something is wrong.",
+ "copy-from": "fake_crafting_tool",
"symbol": "0",
- "color": "dark_gray",
- "flags": [ "ALLOWS_REMOTE_USE", "PSEUDO" ]
+ "color": "dark_gray"
},
{
"id": "pseudo_ergometer_mechanical",
"type": "TOOL",
"name": { "str": "mechanical ergometer" },
- "description": "This is a crafting_pseudo_item if you have it something is wrong.",
+ "copy-from": "fake_crafting_tool",
"symbol": "5",
- "color": "dark_gray",
- "flags": [ "ALLOWS_REMOTE_USE", "PSEUDO" ]
+ "color": "dark_gray"
},
{
"id": "pseudo_treadmill_mechanical",
"type": "TOOL",
"name": { "str": "gravity treadmill" },
- "description": "This is a crafting_pseudo_item if you have it something is wrong.",
+ "copy-from": "fake_crafting_tool",
"symbol": "L",
- "color": "dark_gray",
- "flags": [ "ALLOWS_REMOTE_USE", "PSEUDO" ]
+ "color": "dark_gray"
},
{
"id": "pseudo_training_dummy_light",
"type": "TOOL",
"name": { "str": "training dummy", "str_pl": "training dummies" },
- "description": "This is a crafting_pseudo_item if you have it something is wrong.",
+ "copy-from": "fake_crafting_tool",
"symbol": "@",
- "color": "brown",
- "flags": [ "ALLOWS_REMOTE_USE", "PSEUDO" ]
+ "color": "brown"
},
{
"id": "pseudo_training_dummy_heavy",
"type": "TOOL",
"name": { "str": "armored training dummy", "str_pl": "armored training dummies" },
- "description": "This is a crafting_pseudo_item if you have it something is wrong.",
+ "copy-from": "fake_crafting_tool",
"symbol": "@",
- "color": "brown",
- "flags": [ "ALLOWS_REMOTE_USE", "PSEUDO" ]
+ "color": "brown"
},
{
"id": "pseudo_archery_target_box",
"type": "TOOL",
"name": { "str": "box archery target" },
- "description": "This is a crafting_pseudo_item if you have it something is wrong.",
+ "copy-from": "fake_crafting_tool",
"symbol": "@",
- "color": "brown",
- "flags": [ "ALLOWS_REMOTE_USE", "PSEUDO" ]
+ "color": "brown"
},
{
"id": "pseudo_archery_target_bale",
"type": "TOOL",
"name": { "str": "bale archery target" },
- "description": "This is a crafting_pseudo_item if you have it something is wrong.",
+ "copy-from": "fake_crafting_tool",
"symbol": "@",
- "color": "yellow",
- "flags": [ "ALLOWS_REMOTE_USE", "PSEUDO" ]
+ "color": "yellow"
},
{
"id": "pseudo_magazine",
"type": "MAGAZINE",
"category": "spare_parts",
"name": { "str": "pseudo magazine" },
- "description": "Pseudo magazine for use in dirty vehicle hacks. If you see this, then something went wrong",
- "flags": [ "PSEUDO", "ZERO_WEIGHT", "NO_SALVAGE", "NO_UNLOAD", "NO_RELOAD" ],
+ "description": "Pseudo magazine for use in dirty vehicle hacks.",
+ "flags": [ "PSEUDO", "ZERO_WEIGHT", "NO_SALVAGE", "NO_UNLOAD", "NO_RELOAD", "DEBUG_ONLY" ],
"ammo_type": [ "battery" ],
"capacity": 1000000,
"pocket_data": [
@@ -371,53 +370,40 @@
},
{
"id": "fake_lift_light",
- "copy-from": "fake_item",
+ "copy-from": "fake_crafting_tool",
"type": "TOOL",
"name": { "str": "light hoist" },
- "flags": [ "ALLOWS_REMOTE_USE", "PSEUDO" ],
"qualities": [ [ "LIFT", 2 ], [ "ROPE", 2 ], [ "SUSPENDING", 2 ] ]
},
{
"id": "fake_lift_medium",
- "copy-from": "fake_item",
+ "copy-from": "fake_crafting_tool",
"type": "TOOL",
"name": { "str": "sturdy hoist" },
- "flags": [ "ALLOWS_REMOTE_USE", "PSEUDO" ],
"qualities": [ [ "LIFT", 8 ], [ "ROPE", 2 ], [ "SUSPENDING", 2 ] ]
},
{
"id": "fake_lift_heavy",
- "copy-from": "fake_item",
+ "copy-from": "fake_crafting_tool",
"type": "TOOL",
"name": { "str": "heavy-duty hoist" },
- "flags": [ "ALLOWS_REMOTE_USE", "PSEUDO" ],
"qualities": [ [ "LIFT", 40 ], [ "ROPE", 2 ], [ "SUSPENDING", 2 ] ]
},
{
"id": "pseudo_app_oxygen_concentrator",
"type": "TOOL",
+ "name": { "str": "oxygen concentrator" },
+ "copy-from": "fake_crafting_tool",
"ammo": [ "battery" ],
- "flags": [ "ALLOWS_REMOTE_USE", "PSEUDO", "ZERO_WEIGHT" ],
- "pocket_data": [ { "pocket_type": "MAGAZINE_WELL", "default_magazine": "pseudo_battery" } ],
- "description": "Dummy item. If you see this, then something went wrong.",
- "weight": "1 g",
- "volume": "1 ml",
- "symbol": "@",
- "color": "red",
- "name": { "str": "oxygen concentrator" }
+ "pocket_data": [ { "pocket_type": "MAGAZINE_WELL", "default_magazine": "pseudo_battery" } ]
},
{
"id": "pseudo_app_hd_compressor_unit",
"type": "TOOL",
+ "name": { "str": "high pressure compressor unit" },
+ "copy-from": "fake_crafting_tool",
"ammo": [ "battery" ],
- "flags": [ "ALLOWS_REMOTE_USE", "PSEUDO", "ZERO_WEIGHT" ],
"pocket_data": [ { "pocket_type": "MAGAZINE_WELL", "default_magazine": "pseudo_battery" } ],
- "description": "Dummy item. If you see this, then something went wrong.",
- "weight": "1 g",
- "volume": "1 ml",
- "symbol": "@",
- "color": "red",
- "name": { "str": "high pressure compressor unit" },
"charges_per_use": 100,
"charged_qualities": [ [ "PRESSURIZATION", 10 ] ]
}
diff --git a/data/json/items/tool/cooking.json b/data/json/items/tool/cooking.json
index 1c2db2a3a5bcc..fffe36e8247e6 100644
--- a/data/json/items/tool/cooking.json
+++ b/data/json/items/tool/cooking.json
@@ -37,23 +37,6 @@
"price_postapoc": 150,
"qualities": [ [ "CUT", 2 ], [ "CUT_FINE", 1 ], [ "BUTCHER", 37 ] ]
},
- {
- "id": "butter_churn",
- "type": "TOOL",
- "name": { "str": "pseudo butter churn" },
- "description": "This is a crafting_pseudo_item if you have it something is wrong.",
- "weight": "6464 g",
- "volume": "9 L",
- "price": 20000,
- "price_postapoc": 100,
- "to_hit": -2,
- "material": [ "wood" ],
- "qualities": [ [ "CHURN", 1 ] ],
- "symbol": "H",
- "color": "light_cyan",
- "flags": [ "ALLOWS_REMOTE_USE" ],
- "melee_damage": { "bash": 9 }
- },
{
"id": "screw_press",
"type": "TOOL",
diff --git a/data/json/items/tool/pseudo.json b/data/json/items/tool/pseudo.json
index 062ed794a07ca..91529bcd8e2fb 100644
--- a/data/json/items/tool/pseudo.json
+++ b/data/json/items/tool/pseudo.json
@@ -4,71 +4,70 @@
"type": "TOOL",
"name": { "str": "butchery tree pseudo item" },
"description": "This is a pseudo item to represent the support of a tree or butchery rack without a hook.",
- "weight": "1 g",
- "volume": "1 ml",
+ "copy-from": "fake_item",
"symbol": "[",
"color": "dark_gray",
- "qualities": [ [ "SUSPENDING", 2 ] ],
- "flags": [ "TRADER_AVOID", "ZERO_WEIGHT" ]
+ "qualities": [ [ "SUSPENDING", 2 ] ]
},
{
"id": "fake_butcher_rack",
"type": "TOOL",
"name": { "str": "fake butcher rack (pseudo item)", "str_pl": "fake butcher racks (pseudo items)" },
"description": "This is a pseudo item to represent the support of a tree or butchery rack with a hook.",
- "weight": "1 g",
- "volume": "1 ml",
+ "copy-from": "fake_item",
"symbol": "[",
"color": "dark_gray",
- "qualities": [ [ "SUSPENDING", 2 ], [ "ROPE", 2 ] ],
- "flags": [ "TRADER_AVOID", "ZERO_WEIGHT" ]
+ "qualities": [ [ "SUSPENDING", 2 ], [ "ROPE", 2 ] ]
},
{
"id": "medium_surface_pseudo",
"type": "TOOL",
"name": { "str": "medium surface pseudo item" },
"description": "This is a pseudo item to represent a surface large enough to butcher a medium animal on.",
- "weight": "1 g",
- "volume": "1 ml",
+ "copy-from": "fake_item",
"symbol": "[",
"color": "dark_gray",
- "qualities": [ [ "SURFACE", 2 ] ],
- "flags": [ "TRADER_AVOID", "ZERO_WEIGHT" ]
+ "qualities": [ [ "SURFACE", 2 ] ]
},
{
"id": "large_surface_pseudo",
"type": "TOOL",
"name": { "str": "large surface pseudo item" },
"description": "This is a pseudo item to represent a surface large enough to butcher a large animal on.",
- "weight": "1 g",
- "volume": "1 ml",
+ "copy-from": "fake_item",
"symbol": "[",
"color": "dark_gray",
- "qualities": [ [ "SURFACE", 3 ] ],
- "flags": [ "TRADER_AVOID", "ZERO_WEIGHT" ]
+ "qualities": [ [ "SURFACE", 3 ] ]
},
{
"id": "fake_stove",
"type": "TOOL",
- "copy-from": "fake_item",
"name": { "str": "basecamp stove" },
"description": "A fake stove used for basecamps.",
+ "copy-from": "fake_item",
"sub": "hotplate",
"ammo": [ "tinder" ],
"pocket_data": [ { "pocket_type": "MAGAZINE", "rigid": true, "ammo_restriction": { "tinder": 50000 } } ],
- "charge_factor": 10,
- "flags": [ "ZERO_WEIGHT" ]
+ "charge_factor": 10
},
{
"id": "brick_oven_pseudo",
"type": "TOOL",
"name": { "str": "brick oven" },
- "description": "This is a pseudo item to represent t_brick_oven. If you see it, it's a bug.",
- "weight": "1 g",
- "volume": "1 ml",
+ "description": "This is a pseudo item to represent t_brick_oven.",
+ "copy-from": "fake_item",
"symbol": "[",
"color": "dark_gray",
- "qualities": [ [ "OVEN", 2 ] ],
- "flags": [ "TRADER_AVOID", "ZERO_WEIGHT" ]
+ "qualities": [ [ "OVEN", 2 ] ]
+ },
+ {
+ "id": "butter_churn",
+ "type": "TOOL",
+ "name": { "str": "pseudo butter churn" },
+ "copy-from": "fake_item",
+ "qualities": [ [ "CHURN", 1 ] ],
+ "symbol": "H",
+ "color": "light_cyan",
+ "extend": { "flags": [ "ALLOWS_REMOTE_USE" ] }
}
]
diff --git a/data/json/items/toolmod.json b/data/json/items/toolmod.json
index 9731a8b265b1e..cad247db6a1b2 100644
--- a/data/json/items/toolmod.json
+++ b/data/json/items/toolmod.json
@@ -49,9 +49,9 @@
"type": "TOOLMOD",
"category": "spare_parts",
"name": { "str": "pseudo magazine mod" },
- "description": "If you see this - you see a bug.",
+ "description": "For containing pseudo magazines, obviously.",
"color": "light_green",
- "flags": [ "PSEUDO" ],
+ "flags": [ "PSEUDO", "DEBUG_ONLY" ],
"pocket_mods": [ { "pocket_type": "MAGAZINE_WELL", "default_magazine": "pseudo_magazine", "item_restriction": [ "pseudo_magazine" ] } ]
},
{
diff --git a/src/avatar.cpp b/src/avatar.cpp
index 8f23fbd6a04ba..83ae79c35cdf0 100644
--- a/src/avatar.cpp
+++ b/src/avatar.cpp
@@ -1452,6 +1452,16 @@ bool avatar::wield( item &target, const int obtain_cost )
return true;
}
+item::reload_option avatar::select_ammo( const item_location &base, bool prompt,
+ bool empty )
+{
+ if( !base ) {
+ return item::reload_option();
+ }
+
+ return game_menus::inv::select_ammo( *this, base, prompt, empty );
+}
+
bool avatar::invoke_item( item *used, const tripoint &pt, int pre_obtain_moves )
{
const std::map &use_methods = used->type->use_methods;
diff --git a/src/avatar.h b/src/avatar.h
index 7ba2d5a388075..e110a6de2ed15 100644
--- a/src/avatar.h
+++ b/src/avatar.h
@@ -307,6 +307,9 @@ class avatar : public Character
bool wield( item &target ) override;
bool wield( item &target, int obtain_cost );
+ item::reload_option select_ammo( const item_location &base, bool prompt = false,
+ bool empty = true ) override;
+
/** gets the inventory from the avatar that is interactible via advanced inventory management */
std::vector get_AIM_inventory( const advanced_inventory_pane &pane,
advanced_inv_area &square );
diff --git a/src/character.h b/src/character.h
index 609c771c84d7b..53d16afef47be 100644
--- a/src/character.h
+++ b/src/character.h
@@ -1819,12 +1819,8 @@ class Character : public Creature, public visitable
* @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_location &base, bool prompt = false,
- bool empty = true ) const;
-
- /** Select ammo from the provided options */
- item::reload_option select_ammo( const item_location &base, std::vector opts,
- const std::string &name_override = std::string() ) const;
+ virtual item::reload_option select_ammo( const item_location &base, bool prompt = false,
+ bool empty = true ) = 0;
void process_items();
void leak_items();
diff --git a/src/character_ammo.cpp b/src/character_ammo.cpp
index 295505c3e4e48..79001def8a8f7 100644
--- a/src/character_ammo.cpp
+++ b/src/character_ammo.cpp
@@ -10,7 +10,6 @@
#include "output.h"
static const character_modifier_id character_modifier_reloading_move_mod( "reloading_move_mod" );
-static const itype_id itype_battery( "battery" );
static const skill_id skill_gun( "gun" );
int Character::ammo_count_for( const item_location &gun ) const
@@ -111,317 +110,6 @@ bool Character::list_ammo( const item_location &base, std::vector opts, const std::string &name_override ) const
-{
- if( opts.empty() ) {
- add_msg_if_player( m_info, _( "Never mind." ) );
- return item::reload_option();
- }
-
- std::string name = name_override.empty() ? base->tname() : name_override;
- uilist menu;
- menu.text = string_format( base->is_watertight_container() ? _( "Refill %s" ) :
- base->has_flag( flag_RELOAD_AND_SHOOT ) ? _( "Select ammo for %s" ) : _( "Reload %s" ),
- name );
-
- // Construct item names
- std::vector names;
- std::transform( opts.begin(), opts.end(),
- std::back_inserter( names ), [&]( const item::reload_option & e ) {
- if( e.ammo->is_magazine() && e.ammo->ammo_data() ) {
- if( e.ammo->ammo_current() == itype_battery ) {
- // This battery ammo is not a real object that can be recovered but pseudo-object that represents charge
- //~ battery storage (charges)
- return string_format( pgettext( "magazine", "%1$s (%2$d)" ), e.ammo->type_name(),
- e.ammo->ammo_remaining() );
- } else {
- //~ magazine with ammo (count)
- return string_format( pgettext( "magazine", "%1$s with %2$s (%3$d)" ), e.ammo->type_name(),
- e.ammo->ammo_data()->nname( e.ammo->ammo_remaining() ), e.ammo->ammo_remaining() );
- }
- } else if( e.ammo->is_watertight_container() ||
- ( e.ammo->is_ammo_container() && is_worn( *e.ammo ) ) ) {
- // worn ammo containers should be named by their ammo contents with their location also updated below
- return e.ammo->first_ammo().display_name();
-
- } else {
- return ( ammo_location && ammo_location == e.ammo ? "* " : "" ) + e.ammo->display_name();
- }
- } );
-
- // Get location descriptions
- std::vector where;
- std::transform( opts.begin(), opts.end(),
- std::back_inserter( where ), [this]( const item::reload_option & e ) {
- bool is_ammo_container = e.ammo->is_ammo_container();
- Character &player_character = get_player_character();
- if( is_ammo_container || e.ammo->is_container() ) {
- if( is_ammo_container && is_worn( *e.ammo ) ) {
- return e.ammo->type_name();
- }
- return string_format( _( "%s, %s" ), e.ammo->type_name(), e.ammo.describe( &player_character ) );
- }
- return e.ammo.describe( &player_character );
- } );
- // Get destination names
- std::vector destination;
- std::transform( opts.begin(), opts.end(),
- std::back_inserter( destination ), [&]( const item::reload_option & e ) {
- name = name_override.empty() ? e.target->tname( 1, false, 0, false ) :
- name_override;
- if( ( e.target->is_gunmod() || e.target->is_magazine() ) && e.target.has_parent() ) {
- return string_format( _( "%s in %s" ), name, e.target.parent_item()->tname( 1, false, 0, false ) );
- } else {
- return name;
- }
- } );
- // Pads elements to match longest member and return length
- auto pad = []( std::vector &vec, int n, int t ) -> int {
- for( const auto &e : vec )
- {
- n = std::max( n, utf8_width( e, true ) + t );
- }
- for( auto &e : vec )
- {
- e += std::string( n - utf8_width( e, true ), ' ' );
- }
- return n;
- };
-
- // Pad the first column including 4 trailing spaces
- int w = pad( names, utf8_width( menu.text, true ), 6 );
- menu.text.insert( 0, 2, ' ' ); // add space for UI hotkeys
- menu.text += std::string( w + 2 - utf8_width( menu.text, true ), ' ' );
-
- // Pad the location similarly (excludes leading "| " and trailing " ")
- w = pad( where, utf8_width( _( "| Location " ) ) - 3, 6 );
- menu.text += _( "| Location " );
- menu.text += std::string( w + 3 - utf8_width( _( "| Location " ) ), ' ' );
-
- // Pad the names of target
- w = pad( destination, utf8_width( _( "| Destination " ) ) - 3, 6 );
- menu.text += _( "| Destination " );
- menu.text += std::string( w + 3 - utf8_width( _( "| Destination " ) ), ' ' );
-
- menu.text += _( "| Amount " );
- menu.text += _( "| Moves " );
-
- // We only show ammo statistics for guns and magazines
- if( ( base->is_gun() || base->is_magazine() ) && !base->is_tool() ) {
- menu.text += _( "| Damage | Pierce " );
- }
-
- auto draw_row = [&]( int idx ) {
- const item::reload_option &sel = opts[ idx ];
- std::string row = string_format( "%s| %s | %s |", names[ idx ], where[ idx ], destination[ idx ] );
- row += string_format( ( sel.ammo->is_ammo() ||
- sel.ammo->is_ammo_container() ) ? " %-7d |" : " |", sel.qty() );
- row += string_format( " %-7d ", sel.moves() );
-
- if( ( base->is_gun() || base->is_magazine() ) && !base->is_tool() ) {
- const itype *ammo = sel.ammo->is_ammo_container() ? sel.ammo->first_ammo().ammo_data() :
- sel.ammo->ammo_data();
- if( ammo ) {
- const damage_instance &dam = ammo->ammo->damage;
- row += string_format( "| %-7d | %-7d", static_cast( dam.total_damage() ),
- static_cast( dam.empty() ? 0.0f : ( *dam.begin() ).res_pen ) );
- } else {
- row += "| | ";
- }
- }
- return row;
- };
-
- const ammotype base_ammotype( base->ammo_default().str() );
- itype_id last = uistate.lastreload[ base_ammotype ];
- // We keep the last key so that pressing the key twice (for example, r-r for reload)
- // will always pick the first option on the list.
- int last_key = inp_mngr.get_previously_pressed_key();
- bool last_key_bound = false;
- // This is the entry that has out default
- int default_to = 0;
-
- // If last_key is RETURN, don't use that to override hotkey
- if( last_key == '\n' ) {
- last_key_bound = true;
- default_to = -1;
- }
-
- for( int i = 0; i < static_cast( opts.size() ); ++i ) {
- const item &ammo = opts[ i ].ammo->is_ammo_container() ? opts[ i ].ammo->first_ammo() :
- *opts[ i ].ammo;
-
- char hotkey = -1;
- if( has_item( ammo ) ) {
- // if ammo in player possession and either it or any container has a valid invlet use this
- if( ammo.invlet ) {
- hotkey = ammo.invlet;
- } else {
- for( const item *obj : parents( ammo ) ) {
- if( obj->invlet ) {
- hotkey = obj->invlet;
- break;
- }
- }
- }
- }
- if( last == ammo.typeId() ) {
- if( !last_key_bound && hotkey == -1 ) {
- // If this is the first occurrence of the most recently used type of ammo and the hotkey
- // was not already set above then set it to the keypress that opened this prompt
- hotkey = last_key;
- last_key_bound = true;
- }
- if( !last_key_bound ) {
- // Pressing the last key defaults to the first entry of compatible type
- default_to = i;
- last_key_bound = true;
- }
- }
- if( hotkey == last_key ) {
- last_key_bound = true;
- // Prevent the default from being used: key is bound to something already
- default_to = -1;
- }
-
- menu.addentry( i, true, hotkey, draw_row( i ) );
- }
-
- struct reload_callback : public uilist_callback {
- public:
- std::vector &opts;
- const std::function draw_row;
- int last_key;
- const int default_to;
- const bool can_partial_reload;
-
- reload_callback( std::vector &_opts,
- std::function _draw_row,
- int _last_key, int _default_to, bool _can_partial_reload ) :
- opts( _opts ), draw_row( std::move( _draw_row ) ),
- last_key( _last_key ), default_to( _default_to ),
- can_partial_reload( _can_partial_reload )
- {}
-
- bool key( const input_context &, const input_event &event, int idx, uilist *menu ) override {
- int cur_key = event.get_first_input();
- if( default_to != -1 && cur_key == last_key ) {
- // Select the first entry on the list
- menu->ret = default_to;
- return true;
- }
- if( idx < 0 || idx >= static_cast( opts.size() ) ) {
- return false;
- }
- auto &sel = opts[ idx ];
- switch( cur_key ) {
- case KEY_LEFT:
- if( can_partial_reload ) {
- sel.qty( sel.qty() - 1 );
- menu->entries[ idx ].txt = draw_row( idx );
- }
- return true;
-
- case KEY_RIGHT:
- if( can_partial_reload ) {
- sel.qty( sel.qty() + 1 );
- menu->entries[ idx ].txt = draw_row( idx );
- }
- return true;
- }
- return false;
- }
- } cb( opts, draw_row, last_key, default_to, !base->has_flag( flag_RELOAD_ONE ) );
- menu.callback = &cb;
-
- menu.query();
- if( menu.ret < 0 || static_cast( menu.ret ) >= opts.size() ) {
- add_msg_if_player( m_info, _( "Never mind." ) );
- return item::reload_option();
- }
-
- const item_location &sel = opts[ menu.ret ].ammo;
- uistate.lastreload[ base_ammotype ] = sel->is_ammo_container() ?
- // get first item in all magazine pockets
- sel->first_ammo().typeId() :
- sel->typeId();
- return opts[ menu.ret ];
-}
-
-item::reload_option Character::select_ammo( const item_location &base, bool prompt,
- bool empty ) const
-{
- if( !base ) {
- return item::reload_option();
- }
-
- std::vector ammo_list;
- bool ammo_match_found = list_ammo( base, ammo_list, empty );
-
- if( ammo_list.empty() ) {
- if( !is_npc() ) {
- if( !base->magazine_integral() && !base->magazine_current() ) {
- add_msg_if_player( m_info, _( "You need a compatible magazine to reload the %s!" ),
- base->tname() );
-
- } else if( ammo_match_found ) {
- add_msg_if_player( m_info, _( "You can't reload anything with the ammo you have on hand." ) );
- } else {
- std::string name;
- if( base->ammo_data() ) {
- name = base->ammo_data()->nname( 1 );
- } else if( base->is_watertight_container() ) {
- name = base->is_container_empty() ? "liquid" : base->legacy_front().tname();
- } else {
- const std::set types_of_ammo = base->ammo_types();
- name = enumerate_as_string( types_of_ammo.begin(),
- types_of_ammo.end(), []( const ammotype & at ) {
- return at->name();
- }, enumeration_conjunction::none );
- }
- if( base->is_magazine_full() ) {
- add_msg_if_player( m_info, _( "The %s is already full!" ),
- base->tname() );
- } else {
- add_msg_if_player( m_info, _( "You don't have any %s to reload your %s!" ),
- name, base->tname() );
- }
- }
- }
- 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 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 item::reload_option & lhs,
- const item::reload_option & rhs ) {
- return lhs.moves() < rhs.moves();
- } );
- 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 );
- } );
-
- if( is_npc() ) {
- if( ammo_list[0].ammo.get_item()->ammo_remaining() > 0 ) {
- return ammo_list[0];
- } else {
- return item::reload_option();
- }
- }
-
- if( !prompt && ammo_list.size() == 1 ) {
- // unconditionally suppress the prompt if there's only one option
- return ammo_list[ 0 ];
- }
-
- return select_ammo( base, std::move( ammo_list ) );
-}
-
int Character::item_reload_cost( const item &it, const item &ammo, int qty ) const
{
if( ammo.is_ammo() || ammo.is_frozen_liquid() || ammo.made_of_from_type( phase_id::LIQUID ) ) {
diff --git a/src/game_inventory.cpp b/src/game_inventory.cpp
index ecc851bea297f..3dc2903a8e281 100644
--- a/src/game_inventory.cpp
+++ b/src/game_inventory.cpp
@@ -2769,3 +2769,179 @@ std::pair game_menus::inv::unload( Character &you )
return inv_s.execute();
}
+
+class select_ammo_inventory_preset : public inventory_selector_preset
+{
+ public:
+ select_ammo_inventory_preset( Character &you, const item_location &target,
+ bool empty ) : you( you ),
+ target( target ), empty( empty ) {
+ _indent_entries = false;
+ _collate_entries = true;
+
+ append_cell( [&you]( const item_location & loc ) {
+ bool is_ammo_container = loc->is_ammo_container();
+ Character &player_character = get_player_character();
+ if( is_ammo_container || loc->is_container() ) {
+ if( is_ammo_container && you.is_worn( *loc ) ) {
+ return loc->type_name();
+ }
+ return string_format( _( "%s, %s" ), loc->type_name(), loc.describe( &player_character ) );
+ }
+ return loc.describe( &player_character );
+ }, _( "LOCATION" ) );
+
+ append_cell( [&you, target]( const item_location & loc ) {
+ for( const item_location &opt : get_possible_reload_targets( target ) ) {
+ if( opt->can_reload_with( *loc, true ) ) {
+ if( opt == target ) {
+ return std::string();
+ }
+ return string_format( _( "%s, %s" ), opt->type_name(), opt.describe( &you ) );
+ }
+ }
+ return std::string();
+ }, _( "DESTINATION" ) );
+
+ append_cell( []( const inventory_entry & entry ) {
+ if( entry.any_item()->is_ammo() ) {
+ return std::to_string( entry.chosen_count );
+ }
+ return std::string();
+ }, _( "AMOUNT" ) );
+
+ append_cell( [&you, &target]( const item_location & loc ) {
+ item::reload_option opt( &you, target, loc );
+ return std::to_string( opt.moves() );
+ }, _( "MOVES" ) );
+
+ append_cell( []( const item_location & loc ) {
+ const itype *ammo = loc->is_ammo_container() ? loc->first_ammo().ammo_data() :
+ loc->ammo_data();
+ if( ammo ) {
+ const damage_instance &dam = ammo->ammo->damage;
+ return std::to_string( static_cast( dam.total_damage() ) );
+ }
+ return std::string();
+ }, _( "DAMAGE" ) );
+
+ append_cell( []( const item_location & loc ) {
+ const itype *ammo = loc->is_ammo_container() ? loc->first_ammo().ammo_data() :
+ loc->ammo_data();
+ if( ammo ) {
+ const damage_instance &dam = ammo->ammo->damage;
+ return std::to_string( static_cast( dam.empty() ? 0.0f : ( *dam.begin() ).res_pen ) );
+ }
+ return std::string();
+ }, _( "PIERCE" ) );
+ }
+
+ bool is_shown( const item_location &loc ) const override {
+ // todo: allow to reload a magazine/magazine well from a container pocket on the same item
+ if( loc.parent_item() == target ) {
+ return false;
+ }
+
+ if( loc->made_of( phase_id::LIQUID ) && loc.where() == item_location::type::map ) {
+ map &here = get_map();
+ if( !here.has_flag_ter_or_furn( ter_furn_flag::TFLAG_LIQUIDCONT, loc.pos_bub() ) ) {
+ return false;
+ }
+ }
+
+ if( loc->is_frozen_liquid() ) {
+ return false;
+ }
+
+ if( !empty && loc->is_magazine() && !loc->ammo_remaining() ) {
+ return false;
+ }
+
+ std::vector opts = get_possible_reload_targets( target );
+
+ for( item_location &p : opts ) {
+ if( ( loc->has_flag( flag_SPEEDLOADER ) && p->allows_speedloader( loc->typeId() ) &&
+ loc->ammo_remaining() > 1 && p->ammo_remaining() < 1 ) && p->can_reload_with( *loc, true ) ) {
+ return true;
+ }
+
+ if( p->can_reload_with( *loc, true ) ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ // sort in order of move cost (ascending), then remaining ammo (descending) with empty magazines always last
+ bool sort_compare( const inventory_entry &lhs, const inventory_entry &rhs ) const override {
+ item_location left = lhs.any_item();
+ item_location right = rhs.any_item();
+
+ if( left->ammo_remaining() == 0 || right->ammo_remaining() == 0 ) {
+ return ( left->ammo_remaining() != 0 ) > ( right->ammo_remaining() != 0 );
+ }
+
+ if( left.obtain_cost( you ) != right.obtain_cost( you ) ) {
+ return left.obtain_cost( you ) < right.obtain_cost( you );
+ }
+
+ if( left->ammo_remaining() != right->ammo_remaining() ) {
+ return left->ammo_remaining() > right->ammo_remaining();
+ }
+
+ return inventory_selector_preset::sort_compare( lhs, rhs );
+ }
+
+ private:
+ Character &you;
+ const item_location target;
+ bool empty;
+};
+
+item::reload_option game_menus::inv::select_ammo( Character &you, const item_location &loc,
+ bool prompt, bool empty )
+{
+ const select_ammo_inventory_preset preset( you, loc, empty );
+ ammo_inventory_selector inv_s( you, loc, preset );
+
+ inv_s.set_title( string_format( loc->is_watertight_container() ? _( "Refill %s" ) :
+ loc->has_flag( flag_RELOAD_AND_SHOOT ) ? _( "Select ammo for %s" ) : _( "Reload %s" ),
+ loc->display_name() ) );
+ inv_s.set_hint( _( "Choose ammo to reload" ) );
+ inv_s.set_display_stats( false );
+
+ inv_s.clear_items();
+ inv_s.add_character_items( you );
+ inv_s.add_nearby_items( 1 );
+ inv_s.set_all_entries_chosen_count();
+
+ if( inv_s.empty() ) {
+ popup( _( "You have nothing to reload." ), PF_GET_KEY );
+ return item::reload_option();
+ }
+
+ drop_location selected;
+ if( !prompt && inv_s.item_entry_count() == 1 ) {
+ selected = inv_s.get_only_choice();
+ } else {
+ selected = inv_s.execute();
+ }
+
+ if( !selected.first ) {
+ return item::reload_option();
+ }
+
+ item_location target_loc;
+ for( const item_location &opt : get_possible_reload_targets( loc ) ) {
+ if( opt->can_reload_with( *selected.first, true ) ) {
+ target_loc = opt;
+ break;
+ }
+ }
+
+ item::reload_option opt( &you, target_loc, selected.first );
+ opt.qty( selected.second );
+
+ return opt;
+}
diff --git a/src/game_inventory.h b/src/game_inventory.h
index 99fd91a408162..310a4f70c511c 100644
--- a/src/game_inventory.h
+++ b/src/game_inventory.h
@@ -157,6 +157,8 @@ item_location sterilize_cbm( Character &you );
item_location change_sprite( Character &you );
/** Unload item menu **/
std::pair unload( Character &you );
+item::reload_option select_ammo( Character &you, const item_location &loc, bool prompt = false,
+ bool empty = true );
/*@}*/
} // namespace inv
diff --git a/src/iexamine.cpp b/src/iexamine.cpp
index f9867cf21c149..e2e738dc81ae6 100644
--- a/src/iexamine.cpp
+++ b/src/iexamine.cpp
@@ -4449,25 +4449,11 @@ static void reload_furniture( Character &you, const tripoint &examp, bool allow_
// maybe at some point we need a pseudo item_location or something
// but for now this should at least work as intended
item_location pseudo_loc( map_cursor( examp ), &pseudo );
- std::vector ammo_list;
- for( item_location &ammo : you.find_ammo( pseudo, false, PICKUP_RANGE ) ) {
- // Only allow the same type to reload if partially loaded.
- if( ( amount_in_furn > 0 || !use_ammotype ) && ammo_itypeID != ammo.get_item()->typeId() ) {
- continue;
- }
- if( pseudo.can_reload_with( *ammo, true ) ) {
- ammo_list.emplace_back( &you, pseudo_loc, std::move( ammo ) );
- }
- }
- if( ammo_list.empty() ) {
- //~ Reloading or restocking a piece of furniture, for example a forge.
- add_msg( m_info, _( "You need some %1$s to reload this %2$s." ), ammo->nname( 2 ),
- f.name() );
- return;
- }
+ // used to only allow one type of ammo, changed with move to inventory_selector
+ // todo: use furniture name instead of pseudo item name
+ item::reload_option opt = game_menus::inv::select_ammo( you, pseudo_loc );
- item::reload_option opt = you.select_ammo( pseudo_loc, std::move( ammo_list ), f.name() );
if( !opt ) {
return;
}
diff --git a/src/inventory_ui.cpp b/src/inventory_ui.cpp
index 1c443618fa851..75585ebbe1723 100644
--- a/src/inventory_ui.cpp
+++ b/src/inventory_ui.cpp
@@ -2282,14 +2282,14 @@ void inventory_selector::rearrange_columns( size_t client_width )
const item_location prev_selection = prev_entry.is_item() ?
prev_entry.any_item() : item_location::nowhere;
bool const front_only = prev_entry.is_collation_entry();
- if( is_overflown( client_width ) && !own_gear_column.empty() ) {
+ if( ( is_overflown( client_width ) || force_single_column ) && !own_gear_column.empty() ) {
if( own_inv_column.empty() ) {
own_inv_column.set_indent_entries_override( own_gear_column.indent_entries() );
}
own_gear_column.move_entries_to( own_inv_column );
own_inv_column.reset_width( {} );
}
- if( is_overflown( client_width ) && !map_column.empty() ) {
+ if( ( is_overflown( client_width ) || force_single_column ) && !map_column.empty() ) {
if( own_inv_column.empty() ) {
own_inv_column.set_indent_entries_override( map_column.indent_entries() );
}
@@ -2873,6 +2873,33 @@ bool inventory_selector::has_available_choices() const
} );
}
+uint64_t inventory_selector::item_entry_count() const
+{
+ uint64_t count = 0;
+ for( const inventory_column *col : columns ) {
+ count += col->get_entries( return_item, true ).size();
+ }
+
+ return count;
+}
+
+drop_location inventory_selector::get_only_choice() const
+{
+ if( item_entry_count() != 1 ) {
+ debugmsg( "inventory_selector::get_only_choice called with more that one choice" );
+ return drop_location();
+ }
+
+ for( const inventory_column *col : columns ) {
+ const std::vector ent = col->get_entries( return_item, true );
+ if( !ent.empty() ) {
+ return { ent.front()->any_item(), ent.front()->get_available_count() };
+ }
+ }
+
+ return drop_location();
+}
+
inventory_input inventory_selector::get_input()
{
std::string const &action = ctxt.handle_input();
@@ -3295,6 +3322,108 @@ inventory_selector::stats container_inventory_selector::get_raw_stats() const
loc->get_used_holsters(), loc->get_total_holsters() );
}
+ammo_inventory_selector::ammo_inventory_selector( Character &you,
+ const item_location &reload_loc, const inventory_selector_preset &preset ) :
+ inventory_selector( you, preset ), reload_loc( reload_loc )
+{
+ ctxt.register_action( "INCREASE_COUNT" );
+ ctxt.register_action( "DECREASE_COUNT" );
+
+ force_single_column = true;
+}
+
+std::vector get_possible_reload_targets( const item_location &target )
+{
+ std::vector opts;
+ opts.emplace_back( target );
+
+ if( target->magazine_current() ) {
+ opts.emplace_back( target, const_cast- ( target->magazine_current() ) );
+ }
+
+ for( const item *mod : target->gunmods() ) {
+ item_location mod_loc( target, const_cast
- ( mod ) );
+ opts.emplace_back( mod_loc );
+ if( mod->magazine_current() ) {
+ opts.emplace_back( mod_loc, const_cast
- ( mod->magazine_current() ) );
+ }
+ }
+
+ return opts;
+}
+
+// todo: this should happen when the entries are created, but that's a different refactoring
+void ammo_inventory_selector::set_all_entries_chosen_count()
+{
+ for( inventory_column *col : columns ) {
+ for( inventory_entry *entry : col->get_entries( return_item, true ) ) {
+ for( const item_location &loc : get_possible_reload_targets( reload_loc ) ) {
+ if( loc->can_reload_with( *entry->any_item(), true ) ) {
+ item::reload_option tmp_opt( &u, loc, entry->any_item() );
+ tmp_opt.qty( entry->get_available_count() );
+ entry->chosen_count = tmp_opt.qty();
+ break;
+ }
+ }
+ }
+ }
+}
+
+void ammo_inventory_selector::mod_chosen_count( inventory_entry &entry, int value )
+{
+ for( const item_location &loc : get_possible_reload_targets( reload_loc ) ) {
+ if( loc->can_reload_with( *entry.any_item(), true ) ) {
+ item::reload_option tmp_opt( &u, loc, entry.any_item() );
+ tmp_opt.qty( entry.chosen_count + value );
+ entry.chosen_count = tmp_opt.qty();
+ break;
+ }
+ }
+
+ entry.make_entry_cell_cache( preset );
+ on_change( entry );
+}
+
+drop_location ammo_inventory_selector::execute()
+{
+ shared_ptr_fast ui = create_or_get_ui_adaptor();
+ debug_print_timer( tp_start );
+ while( true ) {
+ ui_manager::redraw();
+ const inventory_input input = get_input();
+
+ if( input.entry != nullptr ) {
+ if( input.action == "MOUSE_MOVE" ) {
+ if( highlight( input.entry->any_item() ) ) {
+ ui_manager::redraw();
+ }
+ } else if( input.action == "ANY_INPUT" || input.action == "SELECT" ) {
+ return { input.entry->any_item(), static_cast( input.entry->chosen_count ) };
+ } else {
+ if( highlight( input.entry->any_item() ) ) {
+ ui_manager::redraw();
+ }
+ on_input( input );
+ }
+ } else if( input.action == "QUIT" ) {
+ return drop_location();
+ } else if( input.action == "CONFIRM" ) {
+ const inventory_entry &highlighted = get_active_column().get_highlighted();
+ if( highlighted && highlighted.is_selectable() ) {
+ return { highlighted.any_item(), static_cast( highlighted.chosen_count ) };
+ }
+ } else if( input.action == "INCREASE_COUNT" ) {
+ inventory_entry &highlighted = get_active_column().get_highlighted();
+ mod_chosen_count( highlighted, 1 );
+ } else if( input.action == "DECREASE_COUNT" ) {
+ inventory_entry &highlighted = get_active_column().get_highlighted();
+ mod_chosen_count( highlighted, -1 );
+ } else {
+ on_input( input );
+ }
+ }
+}
+
void inventory_selector::action_examine( const item_location &sitem )
{
// Code below pulled from the action_examine function in advanced_inv.cpp
diff --git a/src/inventory_ui.h b/src/inventory_ui.h
index 53e88c976239e..bbe8ca99bcce4 100644
--- a/src/inventory_ui.h
+++ b/src/inventory_ui.h
@@ -642,6 +642,8 @@ class inventory_selector
bool empty() const;
/** @return true when there are enabled entries to select. */
bool has_available_choices() const;
+ uint64_t item_entry_count() const;
+ drop_location get_only_choice() const;
/** Apply filter string to all columns */
void set_filter( const std::string &str );
@@ -783,6 +785,7 @@ class inventory_selector
// NOLINTNEXTLINE(cata-use-named-point-constants)
point _fixed_origin{ -1, -1 }, _fixed_size{ -1, -1 };
bool _categorize_map_items = false;
+ bool force_single_column = false;
private:
// These functions are called from resizing/redraw callbacks of ui_adaptor
@@ -945,6 +948,20 @@ class container_inventory_selector : public inventory_pick_selector
item_location loc;
};
+std::vector get_possible_reload_targets( const item_location &target );
+class ammo_inventory_selector : public inventory_selector
+{
+ public:
+ explicit ammo_inventory_selector( Character &you, const item_location &reload_loc,
+ const inventory_selector_preset &preset = default_preset );
+
+ drop_location execute();
+ void set_all_entries_chosen_count();
+ private:
+ void mod_chosen_count( inventory_entry &entry, int val );
+ const item_location reload_loc;
+};
+
class inventory_multiselector : public inventory_selector
{
public:
diff --git a/src/npc.h b/src/npc.h
index a4ccaa4d2e17f..bdd3651c53c63 100644
--- a/src/npc.h
+++ b/src/npc.h
@@ -1158,6 +1158,8 @@ class npc : public Character
/** Finds ammo the NPC could use to reload a given object */
item_location find_usable_ammo( const item_location &weap );
item_location find_usable_ammo( const item_location &weap ) const;
+ item::reload_option select_ammo( const item_location &base, bool prompt = false,
+ bool empty = true ) override;
bool dispose_item( item_location &&obj, const std::string &prompt = std::string() ) override;
diff --git a/src/npcmove.cpp b/src/npcmove.cpp
index 1c3bc9892a0ab..81b696a8e5a8e 100644
--- a/src/npcmove.cpp
+++ b/src/npcmove.cpp
@@ -13,6 +13,7 @@
#include "active_item_cache.h"
#include "activity_handlers.h"
+#include "ammo.h"
#include "avatar.h"
#include "basecamp.h"
#include "bionics.h"
@@ -2158,6 +2159,40 @@ item_location npc::find_usable_ammo( const item_location &weap ) const
return const_cast( this )->find_usable_ammo( weap );
}
+item::reload_option npc::select_ammo( const item_location &base, bool, bool empty )
+{
+ if( !base ) {
+ return item::reload_option();
+ }
+
+ std::vector ammo_list;
+ list_ammo( base, ammo_list, empty );
+
+ if( ammo_list.empty() ) {
+ 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 item::reload_option & lhs,
+ const item::reload_option & rhs ) {
+ if( lhs.ammo->ammo_remaining() == 0 || rhs.ammo->ammo_remaining() == 0 ) {
+ return ( lhs.ammo->ammo_remaining() != 0 ) > ( rhs.ammo->ammo_remaining() != 0 );
+ }
+
+ if( lhs.moves() != rhs.moves() ) {
+ return lhs.moves() < rhs.moves();
+ }
+
+ return lhs.ammo->ammo_remaining() > rhs.ammo->ammo_remaining();
+ } );
+
+ if( ammo_list[0].ammo.get_item()->ammo_remaining() > 0 ) {
+ return ammo_list[0];
+ } else {
+ return item::reload_option();
+ }
+}
+
void npc::activate_combat_cbms()
{
for( const bionic_id &cbm_id : defense_cbms ) {