diff --git a/data/raw/keybindings.json b/data/raw/keybindings.json index 6fd1ea00fbc14..658177c4f3b72 100644 --- a/data/raw/keybindings.json +++ b/data/raw/keybindings.json @@ -2456,7 +2456,7 @@ }, { "type": "keybinding", - "name": "List all items around the player", + "name": "List nearby items, monsters, terrain and furniture", "category": "DEFAULTMODE", "id": "listitems", "bindings": [ { "input_method": "keyboard_char", "key": "V" }, { "input_method": "keyboard_code", "key": "v", "mod": [ "shift" ] } ] @@ -3248,21 +3248,21 @@ { "type": "keybinding", "id": "SCROLL_ITEM_INFO_UP", - "category": "LIST_ITEMS", + "category": "LIST_SURROUNDINGS", "name": "Scroll item info up", "bindings": [ { "input_method": "keyboard_char", "key": "<" }, { "input_method": "keyboard_code", "key": ",", "mod": [ "shift" ] } ] }, { "type": "keybinding", "id": "SCROLL_ITEM_INFO_DOWN", - "category": "LIST_ITEMS", + "category": "LIST_SURROUNDINGS", "name": "Scroll item info down", "bindings": [ { "input_method": "keyboard_char", "key": ">" }, { "input_method": "keyboard_code", "key": ".", "mod": [ "shift" ] } ] }, { "type": "keybinding", "id": "COMPARE", - "category": "LIST_ITEMS", + "category": "LIST_SURROUNDINGS", "name": "Compare", "bindings": [ { "input_method": "keyboard_char", "key": "I" }, @@ -3275,7 +3275,7 @@ { "type": "keybinding", "id": "EXAMINE", - "category": "LIST_ITEMS", + "category": "LIST_SURROUNDINGS", "name": "Examine", "bindings": [ { "input_method": "keyboard_any", "key": "e" }, @@ -3286,7 +3286,7 @@ { "type": "keybinding", "id": "PRIORITY_INCREASE", - "category": "LIST_ITEMS", + "category": "LIST_SURROUNDINGS", "name": "Increase priority", "bindings": [ { "input_method": "keyboard_any", "key": "=" }, @@ -3297,14 +3297,14 @@ { "type": "keybinding", "id": "PRIORITY_DECREASE", - "category": "LIST_ITEMS", + "category": "LIST_SURROUNDINGS", "name": "Decrease priority", "bindings": [ { "input_method": "keyboard_any", "key": "-" }, { "input_method": "keyboard_code", "key": "KEYPAD_MINUS" } ] }, { "type": "keybinding", "id": "SORT", - "category": "LIST_ITEMS", + "category": "LIST_SURROUNDINGS", "name": "Change sort order", "bindings": [ { "input_method": "keyboard_any", "key": "s" }, @@ -3315,28 +3315,28 @@ { "type": "keybinding", "id": "SAFEMODE_BLACKLIST_ADD", - "category": "LIST_MONSTERS", + "category": "LIST_SURROUNDINGS", "name": "Add to safe mode blacklist", "bindings": [ { "input_method": "keyboard_any", "key": "a" } ] }, { "type": "keybinding", "id": "SAFEMODE_BLACKLIST_REMOVE", - "category": "LIST_MONSTERS", + "category": "LIST_SURROUNDINGS", "name": "Remove from safe mode blacklist", "bindings": [ { "input_method": "keyboard_any", "key": "r" } ] }, { "type": "keybinding", "id": "look", - "category": "LIST_MONSTERS", + "category": "LIST_SURROUNDINGS", "name": "Look around", "bindings": [ { "input_method": "keyboard_any", "key": "x" } ] }, { "type": "keybinding", "id": "fire", - "category": "LIST_MONSTERS", + "category": "LIST_SURROUNDINGS", "name": { "ctxt": "verb", "str": "Fire" }, "bindings": [ { "input_method": "keyboard_any", "key": "f" } ] }, @@ -3344,7 +3344,7 @@ "type": "keybinding", "id": "LIST_ITEMS", "category": "LOOK", - "name": "List items and monsters", + "name": "List nearby items, monsters, terrain and furniture", "bindings": [ { "input_method": "keyboard_char", "key": "V" }, { "input_method": "keyboard_code", "key": "v", "mod": [ "shift" ] } ] }, { diff --git a/src/game.cpp b/src/game.cpp index 9f390fd5d1d1c..9c88420045dbf 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -186,6 +186,7 @@ #include "stats_tracker.h" #include "string_formatter.h" #include "string_input_popup.h" +#include "surroundings_menu.h" #include "talker.h" #include "text_snippets.h" #include "tileray.h" @@ -7617,7 +7618,7 @@ look_around_result game::look_around( blink = !blink; } if( action == "LIST_ITEMS" ) { - list_items_monsters(); + list_surroundings(); } else if( action == "TOGGLE_FAST_SCROLL" ) { fast_scroll = !fast_scroll; } else if( action == "map" ) { @@ -8101,7 +8102,7 @@ void game::reset_item_list_state( const catacurses::window &window, int height, } } -void game::list_items_monsters() +void game::list_surroundings() { // Search whole reality bubble because each function internally verifies // the visibility of the items / monsters in question. @@ -8136,19 +8137,27 @@ void game::list_items_monsters() } temp_exit_fullscreen(); - game::vmenu_ret ret; - while( true ) { - ret = uistate.vmenu_show_items ? list_items( items ) : list_monsters( mons ); - if( ret == game::vmenu_ret::CHANGE_TAB ) { - uistate.vmenu_show_items = !uistate.vmenu_show_items; - } else { - break; - } - } - - if( ret == game::vmenu_ret::FIRE ) { - avatar_action::fire_wielded_weapon( u ); - } + std::optional path_start = u.pos(); + std::optional path_end = std::nullopt; + surroundings_menu vmenu( u, m, path_end, 55 ); + shared_ptr_fast trail_cb = create_trail_callback( path_start, path_end, true ); + add_draw_callback( trail_cb ); + vmenu.execute(); + //} else { + // game::vmenu_ret ret; + // while( true ) { + // ret = uistate.vmenu_show_items ? list_items( items ) : list_monsters( mons ); + // if( ret == game::vmenu_ret::CHANGE_TAB ) { + // uistate.vmenu_show_items = !uistate.vmenu_show_items; + // } else { + // break; + // } + // } + + // if( ret == game::vmenu_ret::FIRE ) { + // avatar_action::fire_wielded_weapon( u ); + // } + //} reenter_fullscreen(); } diff --git a/src/game.h b/src/game.h index c1aae626bc269..d8ec4ce410362 100644 --- a/src/game.h +++ b/src/game.h @@ -889,7 +889,7 @@ class game const vproto_id &id, const point_abs_omt &origin, int min_distance, int max_distance, const std::vector &omt_search_types = {} ); // V Menu Functions and helpers: - void list_items_monsters(); // Called when you invoke the `V`-menu + void list_surroundings(); // Called when you invoke the `V`-menu enum class vmenu_ret : int { CHANGE_TAB, diff --git a/src/handle_action.cpp b/src/handle_action.cpp index b8c798bd4f74e..d83e889df48ad 100644 --- a/src/handle_action.cpp +++ b/src/handle_action.cpp @@ -2451,7 +2451,7 @@ bool game::do_regular_action( action_id &act, avatar &player_character, break; case ACTION_LIST_ITEMS: - list_items_monsters(); + list_surroundings(); break; case ACTION_ZONES: diff --git a/src/map_entity_stack.cpp b/src/map_entity_stack.cpp new file mode 100644 index 0000000000000..0a88cc497a2a3 --- /dev/null +++ b/src/map_entity_stack.cpp @@ -0,0 +1,253 @@ +#include "map_entity_stack.h" + +#include "creature.h" +#include "game.h" +#include "item.h" +#include "item_category.h" +#include "item_search.h" +#include "mapdata.h" + +static const trait_id trait_INATTENTIVE( "INATTENTIVE" ); + +template +const T *map_entity_stack::get_selected_entity() const +{ + if( !entities.empty() ) { + // todo: check index + return entities[selected_index].entity; + } + return nullptr; +} + +template +std::optional map_entity_stack::get_selected_pos() const +{ + if( !entities.empty() ) { + // todo: check index + return entities[selected_index].pos; + } + return std::nullopt; +} + +template +int map_entity_stack::get_selected_count() const +{ + if( !entities.empty() ) { + // todo: check index + return entities[selected_index].count; + } + return 0; +} + +template +void map_entity_stack::select_next() +{ + if( entities.empty() ) { + return; + } + + selected_index++; + + if( selected_index >= static_cast( entities.size() ) ) { + selected_index = 0; + } +} + +template +void map_entity_stack::select_prev() +{ + if( entities.empty() ) { + return; + } + + if( selected_index <= 0 ) { + selected_index = entities.size(); + } + + selected_index--; +} + +template +int map_entity_stack::get_selected_index() const +{ + return selected_index; +} + +template +map_entity_stack::map_entity_stack() : totalcount( 0 ) +{ + entities.emplace_back(); +} + +template +map_entity_stack::map_entity_stack( const T *const entity, const tripoint_rel_ms &pos, + const int count ) : totalcount( count ) +{ + entities.emplace_back( pos, 1, entity ); +} + +template +void map_entity_stack::add_at_pos( const T *const entity, const tripoint_rel_ms &pos, + const int count ) +{ + if( entities.empty() || entities.back().pos != pos ) { + entities.emplace_back( pos, 1, entity ); + } else { + entities.back().count++; + } + + totalcount += count; +} + +template +const std::string map_entity_stack::get_category() const +{ + return std::string(); +} + +template<> +const std::string map_entity_stack::get_category() const +{ + const item *it = get_selected_entity(); + if( it ) { + return it->get_category_of_contents().name_header(); + } + + return std::string(); +} + +template<> +const std::string map_entity_stack::get_category() const +{ + const Creature *mon = get_selected_entity(); + if( mon ) { + const Character &you = get_player_character(); + if( you.has_trait( trait_INATTENTIVE ) ) { + return _( "Unknown" ); + } + return Creature::get_attitude_ui_data( mon->attitude_to( you ) ).first.translated(); + } + + return std::string(); +} + +template<> +const std::string map_entity_stack::get_category() const +{ + const map_data_common_t *tf = get_selected_entity(); + if( tf ) { + if( tf->is_terrain() ) { + return "TERRAIN"; + } else { + return "FURNITURE"; + } + } + + return std::string(); +} + +template +bool map_entity_stack::compare( const map_entity_stack &, bool ) const +{ + return false; +} + +template<> +bool map_entity_stack::compare( const map_entity_stack &rhs, bool use_category ) const +{ + bool compare_dist = rl_dist( tripoint_rel_ms(), entities[0].pos ) < + rl_dist( tripoint_rel_ms(), rhs.entities[0].pos ); + + if( !use_category ) { + return compare_dist; + } + + const item_category &lhs_cat = get_selected_entity()->get_category_of_contents(); + const item_category &rhs_cat = rhs.get_selected_entity()->get_category_of_contents(); + + if( lhs_cat == rhs_cat ) { + return compare_dist; + } + + return lhs_cat < rhs_cat; +} + +template<> +bool map_entity_stack::compare( const map_entity_stack &rhs, + bool use_category ) const +{ + bool compare_dist = rl_dist( tripoint_rel_ms(), entities[0].pos ) < + rl_dist( tripoint_rel_ms(), rhs.entities[0].pos ); + + if( !use_category ) { + return compare_dist; + } + + // maybe pass you as a parameter to avoid including game.h? + Character &you = get_player_character(); + const Creature::Attitude att_lhs = get_selected_entity()->attitude_to( you ); + const Creature::Attitude att_rhs = rhs.get_selected_entity()->attitude_to( you ); + + if( att_lhs == att_rhs ) { + return compare_dist; + } + + return att_lhs < att_rhs; +} + +template<> +bool map_entity_stack::compare( const map_entity_stack &rhs, + bool use_category ) const +{ + bool compare_dist = rl_dist( tripoint_rel_ms(), entities[0].pos ) < + rl_dist( tripoint_rel_ms(), rhs.entities[0].pos ); + + if( !use_category ) { + return compare_dist; + } + + const bool &lhs_cat = get_selected_entity()->is_terrain(); + const bool &rhs_cat = rhs.get_selected_entity()->is_terrain(); + + if( lhs_cat == rhs_cat ) { + return compare_dist; + } + + return rhs_cat; +} + +//returns the first non priority items. +template +int list_filter_high_priority( std::vector> &, const std::string & ) +{ + return 0; +} + +template +int list_filter_low_priority( std::vector> &stack, const int start, + const std::string &priorities ) +{ + // todo: actually use it and specialization (only used for items) + // TODO:optimize if necessary + std::vector> tempstack; + const auto filter_fn = item_filter_from_string( priorities ); + for( auto it = stack.begin() + start; it != stack.end(); ) { + if( !priorities.empty() && it->example != nullptr && filter_fn( *it->example ) ) { + tempstack.push_back( *it ); + it = stack.erase( it ); + } else { + it++; + } + } + + int id = stack.size(); + for( map_item_stack &elem : tempstack ) { + stack.push_back( elem ); + } + return id; +} + +// explicit template instantiation +template class map_entity_stack; +template class map_entity_stack; +template class map_entity_stack; diff --git a/src/map_entity_stack.h b/src/map_entity_stack.h new file mode 100644 index 0000000000000..cd0de3085a406 --- /dev/null +++ b/src/map_entity_stack.h @@ -0,0 +1,59 @@ +#pragma once +#ifndef CATA_SRC_MAP_ENTITY_STACK_H +#define CATA_SRC_MAP_ENTITY_STACK_H + +#include "coordinates.h" +#include "point.h" + +template +class map_entity_stack +{ + private: + class entity_group + { + public: + tripoint_rel_ms pos; + int count; + const T *entity; + + //only expected to be used for things like lists and vectors + entity_group() : count( 0 ), entity( nullptr ) {}; + entity_group( const tripoint_rel_ms &p, int arg_count, const T *entity ) : + pos( p ), count( arg_count ), entity( entity ) {}; + }; + + int selected_index = 0; + public: + std::vector entities; + int totalcount; + + const T *get_selected_entity() const; + std::optional get_selected_pos() const; + int get_selected_count() const; + int get_selected_index() const; + + void select_next(); + void select_prev(); + + //only expected to be used for things like lists and vectors + map_entity_stack(); + map_entity_stack( const T *entity, const tripoint_rel_ms &pos, int count = 1 ); + + // This adds to an existing entity group if the last current + // entity group is the same position and otherwise creates and + // adds to a new entity group. Note that it does not search + // through all older entity groups for a match. + void add_at_pos( const T *entity, const tripoint_rel_ms &pos, int count = 1 ); + + bool compare( const map_entity_stack &rhs, bool use_category = false ) const; + const std::string get_category() const; +}; + +template +int list_filter_high_priority( std::vector> &stack, + const std::string &priorities ); +template +int list_filter_low_priority( std::vector> &stack, int start, + const std::string &priorities ); + +#endif // CATA_SRC_MAP_ENTITY_STACK_H diff --git a/src/mapdata.cpp b/src/mapdata.cpp index 473f0962e13c6..0f751f9d05f4f 100644 --- a/src/mapdata.cpp +++ b/src/mapdata.cpp @@ -1019,6 +1019,16 @@ void map_data_common_t::load( const JsonObject &jo, const std::string &src ) } } +bool map_data_common_t::is_terrain() const +{ + return false; +} + +bool ter_t::is_terrain() const +{ + return true; +} + bool ter_t::is_null() const { return id == ter_str_id::NULL_ID(); diff --git a/src/mapdata.h b/src/mapdata.h index d3500d7fba1d0..b11a65f4ea97a 100644 --- a/src/mapdata.h +++ b/src/mapdata.h @@ -593,6 +593,8 @@ struct map_data_common_t { has_flag( ter_furn_flag::TFLAG_FLAMMABLE_HARD ); } + virtual bool is_terrain() const; + virtual void load( const JsonObject &jo, const std::string & ); virtual void check() const {}; }; @@ -631,6 +633,7 @@ struct ter_t : map_data_common_t { static size_t count(); bool is_null() const; + bool is_terrain() const override; std::vector extended_description() const override; diff --git a/src/ranged.h b/src/ranged.h index 3a44503f2883c..d75bc54308a80 100644 --- a/src/ranged.h +++ b/src/ranged.h @@ -6,6 +6,7 @@ #include #include "creature.h" + #include "point.h" class aim_activity_actor; diff --git a/src/surroundings_menu.cpp b/src/surroundings_menu.cpp new file mode 100644 index 0000000000000..806785f1d1fd2 --- /dev/null +++ b/src/surroundings_menu.cpp @@ -0,0 +1,1149 @@ +#include "surroundings_menu.h" + +#include "game.h" +#include "game_inventory.h" +#include "item_search.h" +#include "messages.h" +#include "monster.h" +#include "npc.h" +#include "options.h" +#include "safemode_ui.h" +#include "ui_extended_description.h" +#include "ui_manager.h" + +static const trait_id trait_INATTENTIVE( "INATTENTIVE" ); + +static surroundings_menu_tab_enum &operator++( surroundings_menu_tab_enum &c ) +{ + c = static_cast( static_cast( c ) + 1 ); + if( c == surroundings_menu_tab_enum::num_tabs ) { + c = static_cast( 0 ); + } + return c; +} + +static surroundings_menu_tab_enum &operator--( surroundings_menu_tab_enum &c ) +{ + if( c == static_cast( 0 ) ) { + c = surroundings_menu_tab_enum::num_tabs; + } + c = static_cast( static_cast( c ) - 1 ); + return c; +} + +//static std::string list_items_filter_history_help() +//{ +// return colorize(_("UP: history, CTRL-U: clear line, ESC: abort, ENTER: save"), c_green); +//} + +tab_data::tab_data( const std::string &title ) : title( title ) +{ + ctxt = input_context( "LIST_SURROUNDINGS" ); + ctxt.register_action( "NEXT_TAB" ); + ctxt.register_action( "PREV_TAB" ); + ctxt.register_action( "HELP_KEYBINDINGS" ); + ctxt.register_action( "QUIT" ); + ctxt.register_action( "UP" ); + ctxt.register_action( "DOWN" ); + ctxt.register_action( "LEFT" ); + ctxt.register_action( "RIGHT" ); + ctxt.register_action( "look" ); + ctxt.register_action( "zoom_in" ); + ctxt.register_action( "zoom_out" ); + ctxt.register_action( "PAGE_UP" ); + ctxt.register_action( "PAGE_DOWN" ); + ctxt.register_action( "SCROLL_ITEM_INFO_UP" ); + ctxt.register_action( "SCROLL_ITEM_INFO_DOWN" ); + ctxt.register_action( "FILTER" ); + ctxt.register_action( "RESET_FILTER" ); + ctxt.register_action( "SORT" ); + ctxt.register_action( "TRAVEL_TO" ); + + // arbitrary timeout to fix imgui input weirdness + ctxt.set_timeout( 10 ); +} + +item_tab_data::item_tab_data( const Character &you, map &m, + const std::string &title ) : tab_data( title ) +{ + ctxt.register_action( "EXAMINE" ); + ctxt.register_action( "COMPARE" ); + ctxt.register_action( "PRIORITY_INCREASE" ); + ctxt.register_action( "PRIORITY_DECREASE" ); + + find_nearby_items( you, m ); + filtered_list = item_list; + selected_entry = filtered_list.front(); + + hotkey_list = { + { "EXAMINE", ctxt.get_button_text( "EXAMINE" ) }, + { "COMPARE", ctxt.get_button_text( "COMPARE" ) }, + { "FILTER", ctxt.get_button_text( "FILTER" ) }, + { "RESET_FILTER", ctxt.get_button_text( "RESET_FILTER" ) }, + { "PRIORITY_INCREASE", ctxt.get_button_text( "PRIORITY_INCREASE" ) }, + { "PRIORITY_DECREASE", ctxt.get_button_text( "PRIORITY_DECREASE" ) }, + { "TRAVEL_TO", ctxt.get_button_text( "TRAVEL_TO" ) } + }; +} + +void item_tab_data::add_item_recursive( std::vector &item_order, const item *it, + const tripoint_rel_ms &relative_pos ) +{ + const std::string name = it->tname(); + + if( std::find( item_order.begin(), item_order.end(), name ) == item_order.end() ) { + item_order.push_back( name ); + + items[name] = map_entity_stack( it, relative_pos, it->count() ); + } else { + items[name].add_at_pos( it, relative_pos, it->count() ); + } + + for( const item *content : it->all_known_contents() ) { + add_item_recursive( item_order, content, relative_pos ); + } +} + +void item_tab_data::find_nearby_items( const Character &you, map &m ) +{ + std::vector item_order; + + if( you.is_blind() ) { + return; + } + + tripoint_bub_ms pos = you.pos_bub(); + + for( tripoint_bub_ms &points_p_it : closest_points_first( pos, radius ) ) { + if( points_p_it.y() >= pos.y() - radius && points_p_it.y() <= pos.y() + radius && + you.sees( points_p_it ) && m.sees_some_items( points_p_it.raw(), you ) ) { + + for( item &elem : m.i_at( points_p_it ) ) { + const tripoint_rel_ms relative_pos = points_p_it - pos; + add_item_recursive( item_order, &elem, relative_pos ); + } + } + } + + for( const std::string &elem : item_order ) { + // todo: remove this extra sorting when closest_points_first supports circular distances + std::sort( items[elem].entities.begin(), items[elem].entities.end(), + []( const auto & lhs, const auto & rhs ) { + return rl_dist( tripoint_rel_ms(), lhs.pos ) < rl_dist( tripoint_rel_ms(), rhs.pos ); + } ); + item_list.push_back( &items[elem] ); + } + + // todo: remove this extra sorting when closest_points_first supports circular distances + std::sort( item_list.begin(), item_list.end(), []( const map_entity_stack *lhs, + const map_entity_stack *rhs ) { + return lhs->compare( *rhs ); + } ); +} + +void item_tab_data::apply_filter( std::string filter ) +{ + this->filter = filter; + + filtered_list.clear(); + + auto z = item_filter_from_string( filter ); + std::copy_if( item_list.begin(), item_list.end(), + std::back_inserter( filtered_list ), [z]( const map_entity_stack *a ) { + return z( *a->get_selected_entity() ); + } ); +} + +int item_tab_data::get_max_entity_width() +{ + int max_width = 0; + for( auto &it : item_list ) { + max_width = std::max( max_width, + utf8_width( remove_color_tags( it->get_selected_entity()->display_name() ) ) ); + } + return max_width; +} + +monster_tab_data::monster_tab_data( const Character &you, + const std::string &title ) : tab_data( title ) +{ + ctxt.register_action( "fire" ); + ctxt.register_action( "SAFEMODE_BLACKLIST_ADD" ); + ctxt.register_action( "SAFEMODE_BLACKLIST_REMOVE" ); + + find_nearby_monsters( you ); + filtered_list = monster_list; + selected_entry = filtered_list.front(); + + hotkey_list = { + { "fire", ctxt.get_button_text( "fire" ) }, + { "SAFEMODE_BLACKLIST_ADD", ctxt.get_button_text( "SAFEMODE_BLACKLIST_ADD" ) }, + { "SAFEMODE_BLACKLIST_REMOVE", ctxt.get_button_text( "SAFEMODE_BLACKLIST_REMOVE" ) }, + { "FILTER", ctxt.get_button_text( "FILTER" ) }, + { "RESET_FILTER", ctxt.get_button_text( "RESET_FILTER" ) }, + { "TRAVEL_TO", ctxt.get_button_text( "TRAVEL_TO" ) } + }; +} + +void monster_tab_data::find_nearby_monsters( const Character &you ) +{ + std::vector mons = you.get_visible_creatures( radius ); + + for( Creature *mon : mons ) { + tripoint_rel_ms pos = mon->pos_bub() - you.pos_bub(); + map_entity_stack mstack( mon, pos ); + monsters.push_back( mstack ); + } + for( map_entity_stack &mon : monsters ) { + // no stack internal sorting because monsters currently don't stack + monster_list.push_back( &mon ); + } + + std::sort( monster_list.begin(), monster_list.end(), []( const map_entity_stack *lhs, + const map_entity_stack *rhs ) { + return lhs->compare( *rhs ); + } ); +} + +void monster_tab_data::apply_filter( std::string filter ) +{ + this->filter = filter; + + filtered_list.clear(); + + // todo: filter_from_string + // for now just matching creature name + // auto z = item_filter_from_string( filter ); + std::copy_if( monster_list.begin(), monster_list.end(), + std::back_inserter( filtered_list ), [filter]( const map_entity_stack *a ) { + return lcmatch( remove_color_tags( a->get_selected_entity()->disp_name() ), filter ); + } ); +} + +int monster_tab_data::get_max_entity_width() +{ + int max_width = 0; + for( auto &it : monster_list ) { + max_width = std::max( max_width, + utf8_width( remove_color_tags( it->get_selected_entity()->disp_name() ) ) ); + } + return max_width; +} + +terfurn_tab_data::terfurn_tab_data( const Character &you, map &m, + const std::string &title ) : tab_data( title ) +{ + find_nearby_terfurn( you, m ); + filtered_list = terfurn_list; + selected_entry = filtered_list.front(); + + hotkey_list = { + { "FILTER", ctxt.get_button_text( "FILTER" ) }, + { "RESET_FILTER", ctxt.get_button_text( "RESET_FILTER" ) }, + { "TRAVEL_TO", ctxt.get_button_text( "TRAVEL_TO" ) } + }; +} + +void terfurn_tab_data::add_terfurn( std::vector &item_order, + const map_data_common_t *terfurn, const tripoint_rel_ms &relative_pos ) +{ + const std::string name = terfurn->name(); + + if( std::find( item_order.begin(), item_order.end(), name ) == item_order.end() ) { + item_order.push_back( name ); + + terfurns[name] = map_entity_stack( terfurn, relative_pos ); + } else { + terfurns[name].add_at_pos( terfurn, relative_pos ); + } +} + +void terfurn_tab_data::find_nearby_terfurn( const Character &you, map &m ) +{ + std::vector item_order; + + if( you.is_blind() ) { + return; + } + + tripoint_bub_ms pos = you.pos_bub(); + + for( tripoint_bub_ms &points_p_it : closest_points_first( pos, radius ) ) { + if( points_p_it.y() >= pos.y() - radius && points_p_it.y() <= pos.y() + radius && + you.sees( points_p_it ) ) { + const tripoint_rel_ms relative_pos = points_p_it - pos; + + ter_id ter = m.ter( points_p_it ); + add_terfurn( item_order, &*ter, relative_pos ); + + if( m.has_furn( points_p_it ) ) { + furn_id furn = m.furn( points_p_it ); + add_terfurn( item_order, &*furn, relative_pos ); + } + } + } + + for( const std::string &elem : item_order ) { + // todo: remove this extra sorting when closest_points_first supports circular distances + std::sort( terfurns[elem].entities.begin(), terfurns[elem].entities.end(), + []( const auto & lhs, const auto & rhs ) { + return rl_dist( tripoint_rel_ms(), lhs.pos ) < rl_dist( tripoint_rel_ms(), rhs.pos ); + } ); + terfurn_list.push_back( &terfurns[elem] ); + } + + // todo: remove this extra sorting when closest_points_first supports circular distances + std::sort( terfurn_list.begin(), + terfurn_list.end(), []( const map_entity_stack *lhs, + const map_entity_stack *rhs ) { + return lhs->compare( *rhs ); + } ); +} + +void terfurn_tab_data::apply_filter( std::string filter ) +{ + this->filter = filter; + + filtered_list.clear(); + + // todo: filter_from_string + // for now just matching creature name + //auto z = item_filter_from_string( filter ); + std::copy_if( terfurn_list.begin(), terfurn_list.end(), + std::back_inserter( filtered_list ), [filter]( const map_entity_stack *a ) { + return lcmatch( remove_color_tags( a->get_selected_entity()->name() ), filter ); + } ); +} + +int terfurn_tab_data::get_max_entity_width() +{ + int max_width = 0; + for( auto &it : terfurn_list ) { + max_width = std::max( max_width, + utf8_width( remove_color_tags( it->get_selected_entity()->name() ) ) ); + } + return max_width; +} + +surroundings_menu::surroundings_menu( avatar &you, map &m, std::optional &path_end, + int min_width ) : + cataimgui::window( "SURROUNDINGS", ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoNavInputs ), + you( you ), + path_end( path_end ), + stored_view_offset( you.view_offset ), + info_height( std::min( 25, TERMY / 2 ) ), + item_data( you, m ), + monster_data( you ), + terfurn_data( you, m ) +{ + // todo: add distance width, calculate correct creature text width + int wanted_width = std::max( { item_data.get_max_entity_width(), + monster_data.get_max_entity_width(), + terfurn_data.get_max_entity_width() } ); + width = clamp( wanted_width, min_width, TERMX / 3 ); +} + +cataimgui::bounds surroundings_menu::get_bounds() +{ + info_height = std::min( 25, TERMY / 2 ); + return { static_cast( str_width_to_pixels( TERMX - width ) ), 0.f, static_cast( str_width_to_pixels( width ) ), static_cast( str_height_to_pixels( TERMY ) ) }; +} + +void surroundings_menu::draw_controls() +{ + if( ImGui::BeginTabBar( "surroundings tabs" ) ) { + draw_item_tab(); + draw_monster_tab(); + draw_terfurn_tab(); + ImGui::EndTabBar(); + } +} + +static std::string get_distance_string( tripoint_rel_ms p1, tripoint_rel_ms p2 ) +{ + const int dist = rl_dist( p1, p2 ); + + std::string text = string_format( "%*d %s", 2, dist, + direction_name_short( direction_from( p1, p2 ) ) ); + + return text; +} + +// todo: get ImGui::SeparatorText to draw on the entire row of a table +void surroundings_menu::draw_category_separator( const std::string &category, + std::string &last_category, int target_col ) +{ + if( !category.empty() && ( category != last_category ) ) { + last_category = category; + do { + ImGui::TableNextColumn(); + target_col--; + } while( target_col >= 0 ); + cataimgui::draw_colored_text( category, c_magenta ); + + } + ImGui::TableNextRow(); +} + +void surroundings_menu::draw_item_tab() +{ + ImGuiTabItemFlags_ flags = ImGuiTabItemFlags_None; + if( switch_tab == surroundings_menu_tab_enum::items ) { + flags = ImGuiTabItemFlags_SetSelected; + switch_tab = surroundings_menu_tab_enum::num_tabs; + } + if( ImGui::BeginTabItem( item_data.title.c_str(), nullptr, flags ) ) { + selected_tab = surroundings_menu_tab_enum::items; + + // list of nearby items + float list_height = str_height_to_pixels( TERMY - info_height ) - get_hotkey_buttons_height( + item_data.hotkey_list ) - magic_number_other_elements_height; + if( ImGui::BeginTable( "items", 2, ImGuiTableFlags_ScrollY, + ImVec2( 0.0f, list_height ) ) ) { + ImGui::TableSetupColumn( "it_name", ImGuiTableColumnFlags_NoDirectResize_, + str_width_to_pixels( width - dist_width - 3 ) ); + ImGui::TableSetupColumn( "it_distance", ImGuiTableColumnFlags_NoDirectResize_, + str_width_to_pixels( dist_width ) ); + if( !item_data.selected_entry || + std::find( item_data.filtered_list.begin(), item_data.filtered_list.end(), + item_data.selected_entry ) == item_data.filtered_list.end() ) { + item_data.selected_entry = item_data.filtered_list.front(); + } + std::string last_category; + int entry_no = 0; + for( map_entity_stack *it : item_data.filtered_list ) { + if( item_data.draw_categories ) { + draw_category_separator( it->get_category(), last_category, 0 ); + } + bool is_selected = it == item_data.selected_entry; + ImGui::TableNextColumn(); + ImGui::PushID( entry_no++ ); + ImGui::Selectable( "", &is_selected, + ImGuiSelectableFlags_AllowItemOverlap | ImGuiSelectableFlags_SpanAllColumns, ImVec2( 0, 0 ) ); + if( is_selected ) { + if( auto_scroll ) { + ImGui::SetScrollHereY(); + auto_scroll = false; + } + item_data.selected_entry = it; + } + ImGui::SameLine( 0, 0 ); + ImGui::PopID(); + const item *itm = it->get_selected_entity(); + std::string text; + if( it->entities.size() > 1 ) { + text = string_format( "[%d/%d] (%d) ", it->get_selected_index() + 1, it->entities.size(), + it->totalcount ); + } + + const int count = it->get_selected_count(); + if( count > 1 ) { + text += string_format( "%d ", count ); + } + + text += itm->display_name( count ); + nc_color color = itm->color_in_inventory(); + cataimgui::draw_colored_text( text, color ); + + ImGui::TableNextColumn(); + std::string dist_text = get_distance_string( tripoint_rel_ms(), *it->get_selected_pos() ); + cataimgui::draw_colored_text( dist_text, c_light_gray ); + } + ImGui::EndTable(); + } + + draw_hotkey_buttons( item_data.hotkey_list ); + + const item *selected_it = item_data.selected_entry->get_selected_entity(); + if( ImGui::BeginChild( "info", ImVec2( 0.0f, str_height_to_pixels( info_height ) ) ) ) { + // using table so we get a (pre-styled) header + if( ImGui::BeginTable( "iteminfo", 1, ImGuiTableFlags_None, ImVec2( 0.0f, 0.0f ) ) ) { + ImGui::TableSetupColumn( remove_color_tags( selected_it->display_name() ).c_str() ); + ImGui::TableHeadersRow(); + ImGui::TableNextColumn(); + // embedded info for selected item + std::vector selected_info; + std::vector dummy_info; + selected_it->info( true, selected_info ); + item_info_data dummy( "", "", selected_info, dummy_info ); + dummy.without_getch = true; + dummy.without_border = true; + draw_item_info_imgui( *this, dummy, width, info_scroll ); + ImGui::EndTable(); + } + } + ImGui::EndChild(); + ImGui::EndTabItem(); + } +} + +void surroundings_menu::draw_monster_tab() +{ + ImGuiTabItemFlags_ flags = ImGuiTabItemFlags_None; + if( switch_tab == surroundings_menu_tab_enum::monsters ) { + flags = ImGuiTabItemFlags_SetSelected; + switch_tab = surroundings_menu_tab_enum::num_tabs; + } + if( ImGui::BeginTabItem( monster_data.title.c_str(), nullptr, flags ) ) { + selected_tab = surroundings_menu_tab_enum::monsters; + + // list of nearby monsters + float list_height = str_height_to_pixels( TERMY - info_height ) - get_hotkey_buttons_height( + monster_data.hotkey_list ) - magic_number_other_elements_height; + if( ImGui::BeginTable( "monsters", 5, ImGuiTableFlags_ScrollY, + ImVec2( 0.0f, list_height ) ) ) { + ImGui::TableSetupColumn( "mon_sees", ImGuiTableColumnFlags_NoDirectResize_, + str_width_to_pixels( 1 ) ); + ImGui::TableSetupColumn( "mon_name", ImGuiTableColumnFlags_NoDirectResize_, + str_width_to_pixels( width - dist_width - 26 ) ); + ImGui::TableSetupColumn( "mon_health", ImGuiTableColumnFlags_NoDirectResize_, + str_width_to_pixels( 5 ) ); + ImGui::TableSetupColumn( "mon_attitude", ImGuiTableColumnFlags_NoDirectResize_, + str_width_to_pixels( 18 ) ); + ImGui::TableSetupColumn( "mon_distance", ImGuiTableColumnFlags_NoDirectResize_, + str_width_to_pixels( dist_width ) ); + if( !monster_data.selected_entry || + std::find( monster_data.filtered_list.begin(), monster_data.filtered_list.end(), + monster_data.selected_entry ) == monster_data.filtered_list.end() ) { + monster_data.selected_entry = monster_data.filtered_list.front(); + } + std::string last_category; + int entry_no = 0; + for( map_entity_stack *it : monster_data.filtered_list ) { + if( monster_data.draw_categories ) { + draw_category_separator( it->get_category(), last_category, 1 ); + } + bool is_selected = it == monster_data.selected_entry; + ImGui::TableNextColumn(); + ImGui::PushID( entry_no++ ); + ImGui::Selectable( "", &is_selected, + ImGuiSelectableFlags_AllowItemOverlap | ImGuiSelectableFlags_SpanAllColumns, ImVec2( 0, 0 ) ); + if( is_selected ) { + if( auto_scroll ) { + ImGui::SetScrollHereY(); + auto_scroll = false; + } + monster_data.selected_entry = it; + } + ImGui::SameLine( 0, 0 ); + ImGui::PopID(); + const Creature *mon = it->get_selected_entity(); + const Character &you = get_player_character(); + const bool inattentive = you.has_trait( trait_INATTENTIVE ); + bool sees = mon->sees( you ) && !inattentive; + cataimgui::draw_colored_text( sees ? "!" : " ", c_yellow ); + + ImGui::TableNextColumn(); + std::string mon_name; + if( mon->is_monster() ) { + mon_name = mon->as_monster()->name(); + } else { + mon_name = mon->disp_name(); + } + nc_color mon_color = mon->basic_symbol_color(); + cataimgui::draw_colored_text( mon_name, mon_color ); + if( is_selected ) { + ImGui::SetScrollHereY(); + } + + ImGui::TableNextColumn(); + nc_color hp_color = c_white; + std::string hp_text; + + if( mon->is_monster() ) { + mon->as_monster()->get_HP_Bar( hp_color, hp_text ); + } else { + std::tie( hp_text, hp_color ) = get_hp_bar( mon->as_npc()->hp_percentage(), 100 ); + } + const int bar_max_width = 5; + const int bar_width = utf8_width( hp_text ); + if( bar_width < bar_max_width ) { + hp_text += ""; + for( int i = bar_max_width - bar_width; i > 0; i-- ) { + hp_text += "."; + } + hp_text += ""; + } + cataimgui::draw_colored_text( hp_text, hp_color ); + + ImGui::TableNextColumn(); + nc_color att_color; + std::string att_text; + + if( inattentive ) { + att_text = _( "Unknown" ); + att_color = c_yellow; + } else if( mon->is_monster() ) { + const std::pair att = mon->as_monster()->get_attitude(); + att_text = att.first; + att_color = att.second; + } else if( mon->is_npc() ) { + att_text = npc_attitude_name( mon->as_npc()->get_attitude() ); + att_color = mon->symbol_color(); + } + cataimgui::draw_colored_text( att_text, att_color ); + + ImGui::TableNextColumn(); + std::string dist_text = get_distance_string( tripoint_rel_ms(), *it->get_selected_pos() ); + cataimgui::draw_colored_text( dist_text, c_light_gray ); + } + ImGui::EndTable(); + } + + draw_hotkey_buttons( monster_data.hotkey_list ); + + // embedded info for selected monster + if( ImGui::BeginChild( "info", ImVec2( 0.0f, str_height_to_pixels( info_height ) ) ) ) { + draw_extended_description( + monster_data.selected_entry->get_selected_entity()->extended_description(), + str_width_to_pixels( width ), info_scroll ); + } + ImGui::EndChild(); + ImGui::EndTabItem(); + } +} + +void surroundings_menu::draw_terfurn_tab() +{ + ImGuiTabItemFlags_ flags = ImGuiTabItemFlags_None; + if( switch_tab == surroundings_menu_tab_enum::terfurn ) { + flags = ImGuiTabItemFlags_SetSelected; + switch_tab = surroundings_menu_tab_enum::num_tabs; + } + + if( ImGui::BeginTabItem( terfurn_data.title.c_str(), nullptr, flags ) ) { + selected_tab = surroundings_menu_tab_enum::terfurn; + + float list_height = str_height_to_pixels( TERMY - info_height ) - get_hotkey_buttons_height( + terfurn_data.hotkey_list ) - magic_number_other_elements_height; + if( ImGui::BeginTable( "terfurn", 2, ImGuiTableFlags_ScrollY, + ImVec2( 0.0f, list_height ) ) ) { + ImGui::TableSetupColumn( "tf_name", ImGuiTableColumnFlags_NoDirectResize_, + str_width_to_pixels( width - dist_width - 3 ) ); + ImGui::TableSetupColumn( "tf_distance", ImGuiTableColumnFlags_NoDirectResize_, + str_width_to_pixels( dist_width ) ); + if( !terfurn_data.selected_entry || + std::find( terfurn_data.filtered_list.begin(), terfurn_data.filtered_list.end(), + terfurn_data.selected_entry ) == terfurn_data.filtered_list.end() ) { + terfurn_data.selected_entry = terfurn_data.filtered_list.front(); + } + std::string last_category; + int entry_no = 0; + for( map_entity_stack *it : terfurn_data.filtered_list ) { + if( terfurn_data.draw_categories ) { + draw_category_separator( it->get_category(), last_category, 0 ); + } + bool is_selected = it == terfurn_data.selected_entry; + ImGui::TableNextColumn(); + ImGui::PushID( entry_no++ ); + ImGui::Selectable( "", &is_selected, + ImGuiSelectableFlags_AllowItemOverlap | ImGuiSelectableFlags_SpanAllColumns, ImVec2( 0, 0 ) ); + if( is_selected ) { + if( auto_scroll ) { + ImGui::SetScrollHereY(); + auto_scroll = false; + } + terfurn_data.selected_entry = it; + } + ImGui::SameLine( 0, 0 ); + ImGui::PopID(); + const map_data_common_t *terfurn = it->get_selected_entity(); + std::string text; + if( it->entities.size() > 1 ) { + text = string_format( "[%d/%d] ", it->get_selected_index() + 1, it->entities.size() ); + } + text += terfurn->name(); + nc_color color = terfurn->color(); + cataimgui::draw_colored_text( text, color ); + + ImGui::TableNextColumn(); + std::string dist_text = get_distance_string( tripoint_rel_ms(), *it->get_selected_pos() ); + cataimgui::draw_colored_text( dist_text, c_light_gray ); + } + ImGui::EndTable(); + } + + draw_hotkey_buttons( terfurn_data.hotkey_list ); + + // embedded info for selected terrain/furniture + if( ImGui::BeginChild( "info", ImVec2( 0.0f, str_height_to_pixels( info_height ) ) ) ) { + draw_extended_description( + terfurn_data.selected_entry->get_selected_entity()->extended_description(), + str_width_to_pixels( width ), info_scroll ); + } + ImGui::EndChild(); + ImGui::EndTabItem(); + } +} + +void surroundings_menu::draw_examine_info() +{ + switch( selected_tab ) { + case surroundings_menu_tab_enum::items: { + std::vector selected_info; + std::vector dummy_info; + const item *selected_item = item_data.selected_entry->get_selected_entity(); + selected_item->info( true, selected_info ); + item_info_data dummy( selected_item->tname(), selected_item->type_name(), selected_info, + dummy_info ); + dummy.handle_scrolling = true; + dummy.arrow_scrolling = true; + iteminfo_window info( dummy, point_zero, width - 5, TERMY ); + info.execute(); + } + break; + case surroundings_menu_tab_enum::monsters: + break; + case surroundings_menu_tab_enum::terfurn: + break; + default: + debugmsg( "invalid tab selected" ); + break; + } +} + +void surroundings_menu::execute() +{ + while( true ) { + update_path_end(); + g->invalidate_main_ui_adaptor(); + + ui_manager::redraw(); + + std::string action; + if( has_button_action() ) { + action = get_button_action(); + } else { + action = get_selected_data()->ctxt.handle_input(); + } + + if( action == "UP" ) { + select_prev(); + } else if( action == "DOWN" ) { + select_next(); + } else if( action == "PAGE_UP" ) { + select_page_up(); + } else if( action == "PAGE_DOWN" ) { + select_page_down(); + } else if( action == "SCROLL_ITEM_INFO_UP" ) { + info_scroll = cataimgui::scroll::page_up; + } else if( action == "SCROLL_ITEM_INFO_DOWN" ) { + info_scroll = cataimgui::scroll::page_down; + } else if( action == "NEXT_TAB" ) { + switch_tab = selected_tab; + ++switch_tab; + } else if( action == "PREV_TAB" ) { + switch_tab = selected_tab; + --switch_tab; + } else if( action == "fire" ) { + // todo: maybe refactor to not close the menu here, obsoletes the whole enum + //return surroundings_menu_ret::fire; + } else if( action == "QUIT" ) { + return; + } else if( action == "TRAVEL_TO" && path_end ) { + you.view_offset = stored_view_offset; + if( !you.sees( *path_end ) ) { + add_msg( _( "You can't see that destination." ) ); + } + // try finding route to the tile, or one tile away from it + const std::optional> try_route = + g->safe_route_to( you, tripoint_bub_ms( *path_end ), 1, []( const std::string & msg ) { + popup( msg ); + } ); + if( try_route.has_value() ) { + you.set_destination( *try_route ); + break; + } + } else if( action == "EXAMINE" ) { + draw_examine_info(); + } else if( action == "COMPARE" ) { + game_menus::inv::compare( path_end ); + } else if( action == "SORT" ) { + change_selected_tab_sorting(); + } else if( action == "FILTER" ) { + get_filter(); + } else if( action == "RESET_FILTER" ) { + reset_filter(); + } else { + handle_list_input( action ); + } + } +} + +void surroundings_menu::get_filter() +{ + switch( selected_tab ) { + case surroundings_menu_tab_enum::items: + break; + case surroundings_menu_tab_enum::monsters: + break; + case surroundings_menu_tab_enum::terfurn: + break; + default: + debugmsg( "invalid tab selected" ); + break; + } +} + +void surroundings_menu::reset_filter() +{ + switch( selected_tab ) { + case surroundings_menu_tab_enum::items: + item_data.reset_filter(); + break; + case surroundings_menu_tab_enum::monsters: + monster_data.reset_filter(); + break; + case surroundings_menu_tab_enum::terfurn: + terfurn_data.reset_filter(); + break; + default: + debugmsg( "invalid tab selected" ); + break; + } +} + +//else if( action == "PRIORITY_INCREASE" ) +//{ +// get_string_input( _( "High Priority:" ), width, list_item_upvote, "list_item_priority" ); +//} else if( action == "PRIORITY_DECREASE" ) +//{ +// get_string_input( _( "Low Priority:" ), width, list_item_downvote, "list_item_downvote" ); +//} else if( action == "EXAMINE" ) { +// catacurses::window temp_info = catacurses::newwin(TERMY, width - 5, point_zero); +// int scroll = 0; +// draw_info(temp_info, scroll, true); + +//} + +void surroundings_menu::update_path_end() +{ + std::optional p = get_selected_pos(); + + if( p ) { + path_end = you.pos() + p->raw(); + } else { + path_end = std::nullopt; + } +} + +void surroundings_menu::toggle_safemode_entry( const map_entity_stack *mstack ) +{ + safemode &sm = get_safemode(); + if( mstack && !sm.empty() ) { + const Creature *mon = mstack->get_selected_entity(); + if( mon ) { + std::string mon_name = "human"; + if( mon->is_monster() ) { + mon_name = mon->as_monster()->name(); + } + + if( sm.has_rule( mon_name, Creature::Attitude::ANY ) ) { + sm.remove_rule( mon_name, Creature::Attitude::ANY ); + } else { + sm.add_rule( mon_name, Creature::Attitude::ANY, get_option( "SAFEMODEPROXIMITY" ), + rule_state::BLACKLISTED ); + } + } + } +} + +std::optional surroundings_menu::get_selected_pos() +{ + switch( selected_tab ) { + case surroundings_menu_tab_enum::items: + return item_data.selected_entry->get_selected_pos(); + case surroundings_menu_tab_enum::monsters: + return monster_data.selected_entry->get_selected_pos(); + case surroundings_menu_tab_enum::terfurn: + return terfurn_data.selected_entry->get_selected_pos(); + default: + debugmsg( "invalid tab selected" ); + return std::nullopt; + } +} + +void surroundings_menu::handle_list_input( const std::string &action ) +{ + if( action == "LEFT" ) { + select_prev_internal(); + } else if( action == "RIGHT" ) { + select_next_internal(); + } else if( action == "look" ) { + hide_ui = true; + g->look_around(); + hide_ui = false; + } else if( action == "zoom_in" ) { + g->zoom_in(); + g->mark_main_ui_adaptor_resize(); + } else if( action == "zoom_out" ) { + g->zoom_out(); + g->mark_main_ui_adaptor_resize(); + } else if( action == "SAFEMODE_BLACKLIST_TOGGLE" && + selected_tab == surroundings_menu_tab_enum::monsters ) { + toggle_safemode_entry( monster_data.selected_entry ); + } +} + +tab_data *surroundings_menu::get_selected_data() +{ + switch( selected_tab ) { + case surroundings_menu_tab_enum::items: + return &item_data; + case surroundings_menu_tab_enum::monsters: + return &monster_data; + case surroundings_menu_tab_enum::terfurn: + return &terfurn_data; + default: + debugmsg( "invalid tab selected, defaulting to items" ); + return &item_data; + } +} + +template +static void select_prev_generic( T &data ) +{ + auto it = std::find( data.filtered_list.begin(), data.filtered_list.end(), data.selected_entry ); + if( it == data.filtered_list.begin() ) { + data.selected_entry = *data.filtered_list.rbegin(); + } else if( it == data.filtered_list.end() ) { + data.selected_entry = *data.filtered_list.begin(); + } else { + data.selected_entry = *--it; + } +} + +template +static void select_next_generic( T &data ) +{ + auto it = std::find( data.filtered_list.begin(), data.filtered_list.end(), data.selected_entry ); + if( it == data.filtered_list.end() ) { + data.selected_entry = *data.filtered_list.begin(); + } else if( ++it == data.filtered_list.end() ) { + data.selected_entry = *data.filtered_list.begin(); + } else { + data.selected_entry = *it; + } +} + +void surroundings_menu::select_prev() +{ + auto_scroll = true; + switch( selected_tab ) { + case surroundings_menu_tab_enum::items: + select_prev_generic( item_data ); + break; + case surroundings_menu_tab_enum::monsters: + select_prev_generic( monster_data ); + break; + case surroundings_menu_tab_enum::terfurn: + select_prev_generic( terfurn_data ); + break; + default: + debugmsg( "invalid tab selected" ); + } +} + +void surroundings_menu::select_next() +{ + auto_scroll = true; + switch( selected_tab ) { + case surroundings_menu_tab_enum::items: + select_next_generic( item_data ); + break; + case surroundings_menu_tab_enum::monsters: + select_next_generic( monster_data ); + break; + case surroundings_menu_tab_enum::terfurn: + select_next_generic( terfurn_data ); + break; + default: + debugmsg( "invalid tab selected" ); + } +} + +template +void surroundings_menu::select_page_up_generic( T &data ) +{ + auto it = std::find( data.filtered_list.begin(), data.filtered_list.end(), data.selected_entry ); + if( it == data.filtered_list.begin() ) { + data.selected_entry = *data.filtered_list.rbegin(); + } else if( it == data.filtered_list.end() || + std::distance( data.filtered_list.begin(), it ) <= page_scroll ) { + data.selected_entry = *data.filtered_list.begin(); + } else { + std::advance( it, -page_scroll ); + data.selected_entry = *it; + } +} + +template +void surroundings_menu::select_page_down_generic( T &data ) +{ + auto it = std::find( data.filtered_list.begin(), data.filtered_list.end(), data.selected_entry ); + if( it == data.filtered_list.end() || it == data.filtered_list.end() - 1 ) { + data.selected_entry = *data.filtered_list.begin(); + } else if( std::distance( it, data.filtered_list.end() ) <= page_scroll ) { + data.selected_entry = *data.filtered_list.rbegin(); + } else { + std::advance( it, page_scroll ); + data.selected_entry = *it; + } +} + +void surroundings_menu::select_page_up() +{ + auto_scroll = true; + switch( selected_tab ) { + case surroundings_menu_tab_enum::items: + select_page_up_generic( item_data ); + break; + case surroundings_menu_tab_enum::monsters: + select_page_up_generic( monster_data ); + break; + case surroundings_menu_tab_enum::terfurn: + select_page_up_generic( terfurn_data ); + break; + default: + debugmsg( "invalid tab selected" ); + } +} + +void surroundings_menu::select_page_down() +{ + auto_scroll = true; + switch( selected_tab ) { + case surroundings_menu_tab_enum::items: + select_page_down_generic( item_data ); + break; + case surroundings_menu_tab_enum::monsters: + select_page_down_generic( monster_data ); + break; + case surroundings_menu_tab_enum::terfurn: + select_page_down_generic( terfurn_data ); + break; + default: + debugmsg( "invalid tab selected" ); + } +} + +void surroundings_menu::select_prev_internal() +{ + switch( selected_tab ) { + case surroundings_menu_tab_enum::items: + item_data.selected_entry->select_prev(); + break; + case surroundings_menu_tab_enum::monsters: + monster_data.selected_entry->select_prev(); + break; + case surroundings_menu_tab_enum::terfurn: + terfurn_data.selected_entry->select_prev(); + break; + default: + debugmsg( "invalid tab selected" ); + } +} + +void surroundings_menu::select_next_internal() +{ + switch( selected_tab ) { + case surroundings_menu_tab_enum::items: + item_data.selected_entry->select_next(); + break; + case surroundings_menu_tab_enum::monsters: + monster_data.selected_entry->select_next(); + break; + case surroundings_menu_tab_enum::terfurn: + terfurn_data.selected_entry->select_next(); + break; + default: + debugmsg( "invalid tab selected" ); + } +} + +void surroundings_menu::change_selected_tab_sorting() +{ + auto_scroll = true; + switch( selected_tab ) { + case surroundings_menu_tab_enum::items: + item_data.draw_categories = !item_data.draw_categories; + std::sort( item_data.filtered_list.begin(), item_data.filtered_list.end(), + [&]( const map_entity_stack *lhs, const map_entity_stack *rhs ) { + return lhs->compare( *rhs, item_data.draw_categories ); + } ); + break; + case surroundings_menu_tab_enum::monsters: + monster_data.draw_categories = !monster_data.draw_categories; + std::sort( monster_data.filtered_list.begin(), monster_data.filtered_list.end(), + [&]( const map_entity_stack *lhs, const map_entity_stack *rhs ) { + return lhs->compare( *rhs, monster_data.draw_categories ); + } ); + break; + case surroundings_menu_tab_enum::terfurn: + terfurn_data.draw_categories = !terfurn_data.draw_categories; + std::sort( terfurn_data.filtered_list.begin(), terfurn_data.filtered_list.end(), + [&]( const map_entity_stack *lhs, + const map_entity_stack *rhs ) { + return lhs->compare( *rhs, terfurn_data.draw_categories ); + } ); + break; + default: + debugmsg( "invalid tab selected" ); + } +} + +void surroundings_menu::draw_hotkey_buttons( std::unordered_map + &buttons ) +{ + float pos = 0; + float posy = ImGui::GetCursorPosY(); + float line_height = str_height_to_pixels( 1 ) + ImGui::GetStyle().FramePadding.y + + ImGui::GetStyle().ItemSpacing.y; + for( std::pair &button : buttons ) { + pos += ImGui::GetStyle().FramePadding.x; + + float next_pos = get_text_width( button.second ) + ImGui::GetStyle().FramePadding.x + + ImGui::GetStyle().ItemSpacing.x; + + if( pos + next_pos > str_width_to_pixels( width ) ) { + posy += line_height; + pos = ImGui::GetStyle().FramePadding.x; + } + + ImGui::SetCursorPosX( pos ); + ImGui::SetCursorPosY( posy ); + action_button( button.first, button.second ); + + pos += next_pos; + } +} + +float surroundings_menu::get_hotkey_buttons_height( std::unordered_map + &buttons ) +{ + if( buttons.empty() ) { + return 0; + } + + int pos = 0; + int lines = 1; + for( std::pair &button : buttons ) { + pos += ImGui::GetStyle().FramePadding.x; + + float next_pos = get_text_width( button.second ) + ImGui::GetStyle().FramePadding.x + + ImGui::GetStyle().ItemSpacing.x; + + if( pos + next_pos > str_width_to_pixels( width ) ) { + lines++; + pos = ImGui::GetStyle().FramePadding.x; + } + + pos += next_pos; + } + + float line_height = str_height_to_pixels( 1 ) + ImGui::GetStyle().FramePadding.y + + ImGui::GetStyle().ItemSpacing.y; + + return lines * line_height; +} diff --git a/src/surroundings_menu.h b/src/surroundings_menu.h new file mode 100644 index 0000000000000..16a32e66cc6d5 --- /dev/null +++ b/src/surroundings_menu.h @@ -0,0 +1,196 @@ +#pragma once +#ifndef CATA_SRC_SURROUNDINGS_MENU_H +#define CATA_SRC_SURROUNDINGS_MENU_H + +#include "avatar.h" +#include "cata_imgui.h" +#include "map.h" +#include "map_entity_stack.h" +#include "ui_iteminfo.h" + +enum class surroundings_menu_ret : int { + quit = 0, + fire, +}; + +enum class surroundings_menu_tab_enum : int { + items = 0, + monsters, + terfurn, + num_tabs +}; + +class tab_data +{ + protected: + explicit tab_data( const std::string &title ); + public: + virtual ~tab_data() = default; + input_context ctxt; + std::string title; + + bool draw_categories = false; + std::unordered_map hotkey_list; + + virtual size_t get_filtered_list_size() = 0; + virtual void reset_filter() = 0; + virtual void apply_filter( std::string filter ) = 0; + virtual int get_max_entity_width() = 0; + + protected: + const int radius = 60; + std::string filter; +}; + +class item_tab_data : public tab_data +{ + public: + item_tab_data( const Character &you, map &m, const std::string &title = _( "Items" ) ); + std::vector*> item_list; + std::vector*> filtered_list; + map_entity_stack *selected_entry; + + size_t get_filtered_list_size() override { + return filtered_list.size(); + } + + void reset_filter() override { + filter = std::string(); + filtered_list = item_list; + } + + void apply_filter( std::string filter ) override; + int get_max_entity_width() override; + + private: + std::map> items; + void find_nearby_items( const Character &you, map &m ); + void add_item_recursive( std::vector &item_order, const item *it, + const tripoint_rel_ms &relative_pos ); +}; + +class monster_tab_data : public tab_data +{ + public: + explicit monster_tab_data( const Character &you, const std::string &title = _( "Monsters" ) ); + std::vector*> monster_list; + std::vector*> filtered_list; + map_entity_stack *selected_entry; + + size_t get_filtered_list_size() override { + return filtered_list.size(); + } + + void reset_filter() override { + filter = std::string(); + filtered_list = monster_list; + } + + void apply_filter( std::string filter ) override; + int get_max_entity_width() override; + + private: + // could be turned into a map to allow monster grouping + std::vector> monsters; + void find_nearby_monsters( const Character &you ); +}; + +class terfurn_tab_data : public tab_data +{ + public: + terfurn_tab_data( const Character &you, map &m, + const std::string &title = _( "Terrain/Furniture" ) ); + std::vector*> terfurn_list; + std::vector*> filtered_list; + map_entity_stack *selected_entry; + + size_t get_filtered_list_size() override { + return filtered_list.size(); + } + + void reset_filter() override { + filter = std::string(); + filtered_list = terfurn_list; + } + + void apply_filter( std::string filter ) override; + int get_max_entity_width() override; + + private: + std::map> terfurns; + void add_terfurn( std::vector &item_order, const map_data_common_t *terfurn, + const tripoint_rel_ms &relative_pos ); + void find_nearby_terfurn( const Character &you, map &m ); +}; + +class surroundings_menu : public cataimgui::window +{ + public: + surroundings_menu( avatar &you, map &m, std::optional &path_end, int width ); + void execute(); + + protected: + void draw_controls() override; + cataimgui::bounds get_bounds() override; + + private: + void draw_item_tab(); + void draw_monster_tab(); + void draw_terfurn_tab(); + void draw_examine_info(); + void draw_category_separator( const std::string &category, std::string &last_category, + int target_col ); + void draw_hotkey_buttons( std::unordered_map &buttons ); + float get_hotkey_buttons_height( std::unordered_map &buttons ); + + void toggle_safemode_entry( const map_entity_stack *mstack ); + void update_path_end(); + void handle_list_input( const std::string &action ); + std::optional get_selected_pos(); + /*template + const T *get_selected_element() const;*/ + + tab_data *get_selected_data(); + void select_prev(); + void select_next(); + + template + void select_page_up_generic( T &data ); + template + void select_page_down_generic( T &data ); + void select_page_up(); + void select_page_down(); + + void select_prev_internal(); + void select_next_internal(); + + void change_selected_tab_sorting(); + void get_filter(); + void reset_filter(); + + surroundings_menu_tab_enum selected_tab = surroundings_menu_tab_enum::items; + surroundings_menu_tab_enum switch_tab = surroundings_menu_tab_enum::num_tabs; + // auto scrolling when navigating by keyboard + bool auto_scroll = false; + + avatar &you; + std::optional &path_end; + const tripoint_rel_ms stored_view_offset; + + cataimgui::scroll info_scroll = cataimgui::scroll::none; + bool hide_ui; + + int width; + int info_height; + float magic_number_other_elements_height = ImGui::GetTextLineHeight() * 3 + + ImGui::GetStyle().FramePadding.y * 2; + + item_tab_data item_data; + monster_tab_data monster_data; + terfurn_tab_data terfurn_data; + + const int dist_width = 7; + const int page_scroll = 10; +}; + +#endif // CATA_SRC_SURROUNDINGS_MENU_H diff --git a/src/ui_extended_description.cpp b/src/ui_extended_description.cpp index fb7f5b5517b02..7033527229152 100644 --- a/src/ui_extended_description.cpp +++ b/src/ui_extended_description.cpp @@ -60,8 +60,11 @@ static void draw_graffiti_text( const tripoint_bub_ms &p ) } } -void draw_extended_description( const std::vector &description, const uint64_t width ) +void draw_extended_description( const std::vector &description, const uint64_t width, + cataimgui::scroll &s ) { + cataimgui::set_scroll( s ); + for( const std::string &s : description ) { if( s == "--" ) { ImGui::Separator(); @@ -87,6 +90,10 @@ extended_description_window::extended_description_window( tripoint_bub_ms &p ) : ctxt.register_action( "CONFIRM" ); ctxt.register_action( "QUIT" ); ctxt.register_action( "HELP_KEYBINDINGS" ); + ctxt.register_action( "UP" ); + ctxt.register_action( "DOWN" ); + ctxt.register_action( "PAGE_UP" ); + ctxt.register_action( "PAGE_DOWN" ); ctxt.set_timeout( 10 ); const Creature *critter = seen_critter( p ); @@ -140,7 +147,7 @@ void extended_description_window::draw_creature_tab() if( ImGui::BeginTabItem( title.c_str(), nullptr, flags ) ) { cur_target = description_target::creature; if( !creature_description.empty() ) { - draw_extended_description( creature_description, str_width_to_pixels( TERMX ) ); + draw_extended_description( creature_description, str_width_to_pixels( TERMX ), info_scroll ); } else { cataimgui::draw_colored_text( _( "You do not see any creature here." ), c_light_gray ); } @@ -159,7 +166,7 @@ void extended_description_window::draw_furniture_tab() if( ImGui::BeginTabItem( title.c_str(), nullptr, flags ) ) { cur_target = description_target::furniture; if( !furniture_description.empty() ) { - draw_extended_description( furniture_description, str_width_to_pixels( TERMX ) ); + draw_extended_description( furniture_description, str_width_to_pixels( TERMX ), info_scroll ); } else { cataimgui::draw_colored_text( _( "You do not see any furniture here." ), c_light_gray ); } @@ -179,7 +186,7 @@ void extended_description_window::draw_terrain_tab() if( ImGui::BeginTabItem( title.c_str(), nullptr, flags ) ) { cur_target = description_target::terrain; if( !terrain_description.empty() ) { - draw_extended_description( terrain_description, str_width_to_pixels( TERMX ) ); + draw_extended_description( terrain_description, str_width_to_pixels( TERMX ), info_scroll ); } else { cataimgui::draw_colored_text( _( "You can't see the terrain here." ), c_light_gray ); } @@ -199,7 +206,7 @@ void extended_description_window::draw_vehicle_tab() if( ImGui::BeginTabItem( title.c_str(), nullptr, flags ) ) { cur_target = description_target::vehicle; if( !veh_app_description.empty() ) { - draw_extended_description( veh_app_description, str_width_to_pixels( TERMX ) ); + draw_extended_description( veh_app_description, str_width_to_pixels( TERMX ), info_scroll ); } else { cataimgui::draw_colored_text( _( "You can't see vehicles or appliances here." ), c_light_gray ); } @@ -223,6 +230,14 @@ void extended_description_window::show() } else if( action == "PREV_TAB" ) { switch_target = cur_target; --switch_target; + } else if( action == "UP" ) { + info_scroll = cataimgui::scroll::line_up; + } else if( action == "DOWN" ) { + info_scroll = cataimgui::scroll::line_down; + } else if( action == "PAGE_UP" ) { + info_scroll = cataimgui::scroll::page_up; + } else if( action == "PAGE_DOWN" ) { + info_scroll = cataimgui::scroll::page_down; } else if( action == "CREATURE" ) { switch_target = description_target::creature; } else if( action == "FURNITURE" ) { diff --git a/src/ui_extended_description.h b/src/ui_extended_description.h index 99c1b5001f028..04957bbe6b17c 100644 --- a/src/ui_extended_description.h +++ b/src/ui_extended_description.h @@ -16,7 +16,8 @@ enum class description_target : int { num_targets }; -void draw_extended_description( const std::vector &description, uint64_t width ); +void draw_extended_description( const std::vector &description, uint64_t width, + cataimgui::scroll &s ); class extended_description_window : public cataimgui::window { @@ -45,6 +46,8 @@ class extended_description_window : public cataimgui::window std::vector furniture_description; std::vector terrain_description; std::vector veh_app_description; + + cataimgui::scroll info_scroll = cataimgui::scroll::none; }; #endif // CATA_SRC_UI_EXTENDED_DESCRIPTION_H