Skip to content

Commit

Permalink
Merge pull request CleverRaven#70359 from mqrause/reload_inv_selector
Browse files Browse the repository at this point in the history
Migrate reload ammo selection menu to inventory_selector
  • Loading branch information
Maleclypse authored Jan 18, 2024
2 parents ea12ac6 + e441c35 commit f2c8c58
Show file tree
Hide file tree
Showing 11 changed files with 381 additions and 337 deletions.
10 changes: 10 additions & 0 deletions src/avatar.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1454,6 +1454,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<std::string, use_function> &use_methods = used->type->use_methods;
Expand Down
3 changes: 3 additions & 0 deletions src/avatar.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<advanced_inv_listitem> get_AIM_inventory( const advanced_inventory_pane &pane,
advanced_inv_area &square );
Expand Down
8 changes: 2 additions & 6 deletions src/character.h
Original file line number Diff line number Diff line change
Expand Up @@ -1825,12 +1825,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<item::reload_option> 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();
Expand Down
312 changes: 0 additions & 312 deletions src/character_ammo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -111,317 +110,6 @@ bool Character::list_ammo( const item_location &base, std::vector<item::reload_o
return ammo_match_found;
}

item::reload_option Character::select_ammo( const item_location &base,
std::vector<item::reload_option> 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<std::string> 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<std::string> 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<std::string> 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<std::string> &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<int>( dam.total_damage() ),
static_cast<int>( 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<int>( 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<item::reload_option> &opts;
const std::function<std::string( int )> draw_row;
int last_key;
const int default_to;
const bool can_partial_reload;

reload_callback( std::vector<item::reload_option> &_opts,
std::function<std::string( int )> _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<int>( 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<size_t>( 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<item::reload_option> 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<ammotype> 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 ) ) {
Expand Down
Loading

0 comments on commit f2c8c58

Please sign in to comment.