From adf1187c64ff566e5bb1e2211c28bea51955045f Mon Sep 17 00:00:00 2001 From: Eric Pierce Date: Thu, 15 Oct 2020 18:05:25 -0600 Subject: [PATCH 1/7] Add widget class for data driven sidebar This commit adds a `widget` class for rendering a UI view of some internal value or status variable of an `avatar` (player character). It includes a generic factory wrapper to load widgets from JSON. widget_var for linking avatar attributes to widgets: - focus, move, pain, speed, stamina, stats, HP per bodypart and more widget class to render avatar variables in UI with labels and layout: - uses generic_factory to load JSON from widget types - JSON: id, label, style, var, colors, symbols, width, fill, etc. - show() to render widget value from avatar instance - layout() to arrange widgets and do max width padding/alignment - get_var_value() to return avatar's current "var" value - color_value_string() to render value with colors - supporting: value_color(), value_string(), graph(), num() --- src/widget.cpp | 397 +++++++++++++++++++++++++++++++++++++++++++++++++ src/widget.h | 129 ++++++++++++++++ 2 files changed, 526 insertions(+) create mode 100644 src/widget.cpp create mode 100644 src/widget.h diff --git a/src/widget.cpp b/src/widget.cpp new file mode 100644 index 0000000000000..960a445b5cd44 --- /dev/null +++ b/src/widget.cpp @@ -0,0 +1,397 @@ +#include "widget.h" + +#include "color.h" +#include "generic_factory.h" +#include "json.h" + +// Use generic factory wrappers for widgets to use standardized JSON loading methods +namespace +{ +generic_factory widget_factory( "widgets" ); +} // namespace + +template<> +const widget &string_id::obj() const +{ + return widget_factory.obj( *this ); +} + +template<> +bool string_id::is_valid() const +{ + return widget_factory.is_valid( *this ); +} + +void widget::load_widget( const JsonObject &jo, const std::string &src ) +{ + widget_factory.load( jo, src ); +} + +void widget::reset() +{ + widget_factory.reset(); +} + +// Convert widget "var" enums to string equivalents +namespace io +{ +template<> +std::string enum_to_string( widget_var data ) +{ + switch( data ) { + case widget_var::focus: + return "focus"; + case widget_var::hunger: + return "hunger"; + case widget_var::move: + return "move"; + case widget_var::mood: + return "mood"; + case widget_var::pain: + return "pain"; + case widget_var::sound: + return "sound"; + case widget_var::speed: + return "speed"; + case widget_var::stamina: + return "stamina"; + case widget_var::thirst: + return "thirst"; + case widget_var::fatigue: + return "fatigue"; + case widget_var::mana: + return "mana"; + // Base stats + case widget_var::stat_str: + return "stat_str"; + case widget_var::stat_dex: + return "stat_dex"; + case widget_var::stat_int: + return "stat_int"; + case widget_var::stat_per: + return "stat_per"; + // Bodypart attributes + case widget_var::bp_hp: + return "bp_hp"; + case widget_var::bp_encumb: + return "bp_encumb"; + case widget_var::bp_warmth: + return "bp_warmth"; + case widget_var::last: + break; + } + debugmsg( "Invalid widget_var" ); + abort(); +} + +} // namespace io + +void widget::load( const JsonObject &jo, const std::string & ) +{ + optional( jo, was_loaded, "strings", _strings ); + optional( jo, was_loaded, "width", _width, 1 ); + optional( jo, was_loaded, "symbols", _symbols, "-" ); + optional( jo, was_loaded, "fill", _fill, "bucket" ); + optional( jo, was_loaded, "label", _label, "" ); + optional( jo, was_loaded, "style", _style, "number" ); + optional( jo, was_loaded, "arrange", _arrange, "columns" ); + optional( jo, was_loaded, "var_min", _var_min ); + optional( jo, was_loaded, "var_max", _var_max ); + + if( jo.has_string( "var" ) ) { + _var = io::string_to_enum( jo.get_string( "var" ) ); + } + + if( jo.has_string( "bodypart" ) ) { + _bp_id = bodypart_id( jo.get_string( "bodypart" ) ); + } + + if( jo.has_array( "colors" ) ) { + _colors.clear(); + for( const std::string color_name : jo.get_array( "colors" ) ) { + _colors.emplace_back( get_all_colors().name_to_color( color_name ) ); + } + } + if( jo.has_array( "widgets" ) ) { + _widgets.clear(); + for( const std::string wid : jo.get_array( "widgets" ) ) { + _widgets.emplace_back( widget_id( wid ) ); + } + } +} + +int widget::get_var_max( const avatar &ava ) +{ + // Some vars (like HP) have an inherent maximum, used unless the widget overrides it + int max_val = 1; + // max_val (used only for graphs) is set to a known maximum if the attribute has one; otherwise, + // it is up to the graph widget to set "var_max" so the graph widget can determine a scaling. + switch( _var ) { + case widget_var::stamina: + max_val = ava.get_stamina_max(); + break; + case widget_var::mana: + max_val = ava.magic->max_mana( ava ); + break; + case widget_var::bp_hp: + // HP for body part + max_val = ava.get_part_hp_max( _bp_id ); + break; + case widget_var::bp_warmth: + // From weather.h: Body temperature is measured on a scale of 0u to 10000u, + // where 10u = 0.02C and 5000u is 37C + max_val = 10000; + break; + default: + break; + } + // JSON-defined var_max may override it + if( _var_max > 0 ) { + max_val = _var_max; + } + return max_val; +} + +int widget::get_var_value( const avatar &ava ) +{ + // Numeric value to be rendered in the widget + int value = 0; + + // Each "var" value refers to some attribute, typically of the avatar, that yields a numeric + // value, and can be displayed as a numeric field, a graph, or a series of phrases. + switch( _var ) { + // Vars with a known max val + case widget_var::stamina: + value = ava.get_stamina(); + break; + case widget_var::mana: + value = ava.magic->available_mana(); + break; + case widget_var::bp_hp: + // HP for body part + value = ava.get_part_hp_cur( _bp_id ); + break; + case widget_var::bp_warmth: + // Body part warmth/temperature + value = ava.get_part_temp_cur( _bp_id ); + break; + case widget_var::focus: + value = ava.get_focus(); + break; + case widget_var::speed: + value = ava.get_speed(); + break; + case widget_var::move: + value = ava.movecounter; + break; + case widget_var::pain: + value = ava.get_perceived_pain(); + break; + case widget_var::fatigue: + value = ava.get_fatigue(); + break; + case widget_var::stat_str: + value = ava.get_str(); + break; + case widget_var::stat_dex: + value = ava.get_dex(); + break; + case widget_var::stat_int: + value = ava.get_int(); + break; + case widget_var::stat_per: + value = ava.get_per(); + break; + case widget_var::sound: + value = ava.volume; + break; + case widget_var::bp_encumb: + // Encumbrance for body part + value = ava.get_part_encumbrance_data( _bp_id ).encumbrance; + break; + + // TODO + case widget_var::mood: + // see morale_emotion + case widget_var::hunger: + // see ava.get_hunger_description() + case widget_var::thirst: + // see ava.get_thirst_description() + default: + value = 0; + } + return value; +} + +std::string widget::show( const avatar &ava ) +{ + int value = get_var_value( ava ); + int value_max = get_var_max( ava ); + return color_value_string( value, value_max ); +} + +std::string widget::color_value_string( int value, int value_max ) +{ + if( value_max == 0 ) { + value_max = _var_max; + } + std::string val_string = value_string( value, value_max ); + const nc_color cur_color = value_color( value, value_max ); + if( cur_color == c_unset ) { + return val_string; + } else { + return colorize( val_string, cur_color ); + } +} + +std::string widget::value_string( int value, int value_max ) +{ + std::string ret; + if( _style == "graph" ) { + ret += graph( value, value_max ); + } else if( _style == "phrase" ) { + ret += phrase( value, value_max ); + } else if( _style == "number" ) { + ret += number( value, value_max ); + } else { + ret += "???"; + } + return ret; +} + +nc_color widget::value_color( int value, int value_max ) +{ + if( _colors.empty() ) { + return c_unset; + } + // Scale to value_max + if( value_max > 0 ) { + if( value <= value_max ) { + // Scale value range from [0, 1] to map color range + const double scale = static_cast( value ) / value_max; + const int color_max = _colors.size() - 1; + // Include 0.5f offset to make up for floor piling values up at the bottom + const int color_index = std::floor( scale * color_max + 0.5f ); + return _colors[color_index]; + } else { + return _colors.back(); + } + } + // Assume colors map to 0, 1, 2 ... + if( value < num_colors ) { + return _colors[value]; + } + // Last color as last resort + return _colors.back(); +} + +std::string widget::number( int value, int /* value_max */ ) +{ + return string_format( "%d", value ); +} + +std::string widget::phrase( int value, int /* value_max */ ) +{ + return _strings.at( value ); +} + +std::string widget::graph( int value, int value_max ) +{ + // graph "depth is equal to the number of nonzero symbols + int depth = _symbols.length() - 1; + // Max integer value this graph can show + int max_graph_val = _width * depth; + // Scale value range to current graph resolution (width x depth) + if( value_max > 0 && value_max != max_graph_val ) { + // Scale max source value to max graph value + value = max_graph_val * value / value_max; + } + + // Negative values are not (yet) supported + if( value < 0 ) { + value = 0; + } + // Truncate to maximum value displayable by graph + if( value > max_graph_val ) { + value = max_graph_val; + } + + int quot; + int rem; + + std::string ret; + if( _fill == "bucket" ) { + quot = value / depth; // number of full cells/buckets + rem = value % depth; // partly full next cell, maybe + // Full cells at the front + ret += std::string( quot, _symbols.back() ); + // Any partly-full cells? + if( _width > quot ) { + // Current partly-full cell + ret += _symbols[rem]; + // Any more zero cells at the end + if( _width > quot + 1 ) { + ret += std::string( _width - quot - 1, _symbols[0] ); + } + } + } else if( _fill == "pool" ) { + quot = value / _width; // baseline depth of the pool + rem = value % _width; // number of cells at next depth + // Most-filled cells come first + if( rem > 0 ) { + ret += std::string( rem, _symbols[quot + 1] ); + // Less-filled cells may follow + if( _width > rem ) { + ret += std::string( _width - rem, _symbols[quot] ); + } + } else { + // All cells at the same level + ret += std::string( _width, _symbols[quot] ); + } + } else { + debugmsg( "Unknown widget fill type %s", _fill ); + return ret; + } + return ret; +} + +std::string widget::layout( const avatar &ava, const unsigned int max_width ) +{ + std::string ret; + if( _style == "layout" ) { + // Divide max_width equally among all widgets + int child_width = max_width / _widgets.size(); + int remainder = max_width % _widgets.size(); + for( const widget_id &wid : _widgets ) { + widget cur_child = wid.obj(); + int cur_width = child_width; + // Spread remainder over the first few columns + if( remainder > 0 ) { + cur_width += 1; + remainder -= 1; + } + // Allow 2 spaces of padding after each column, except last column (full-justified) + if( wid != _widgets.back() ) { + ret += string_format( "%s ", cur_child.layout( ava, cur_width - 2 ) ); + } else { + ret += string_format( "%s", cur_child.layout( ava, cur_width ) ); + } + } + } else { + // Get displayed value (colorized) + std::string shown = show( ava ); + // Width used by label, ": " and value, using utf8_width to ignore color tags + unsigned int used_width = _label.length() + 2 + utf8_width( shown, true ); + + // Label first + ret += _label; + // then enough padding to fit max_width + if( used_width < max_width ) { + ret += std::string( max_width - used_width, ' ' ); + } + // then ":" and colorized value + ret += ": " + shown; + } + return ret; +} + diff --git a/src/widget.h b/src/widget.h new file mode 100644 index 0000000000000..f452411471d28 --- /dev/null +++ b/src/widget.h @@ -0,0 +1,129 @@ +#pragma once +#ifndef CATA_SRC_WIDGET_H +#define CATA_SRC_WIDGET_H + +#include +#include + +#include "avatar.h" +//#include "cata_variant.h" +#include "enum_traits.h" +#include "generic_factory.h" +#include "string_id.h" +#include "type_id.h" + +// These are the supported data variables for widgets, defined as enum widget_var. +// widget_var names may be given as the "var" field in widget JSON. +enum class widget_var : int { + focus, // Current focus, integer + move, // Current move counter, integer + pain, // Current perceived pain, integer + sound, // Current sound level, integer + speed, // Current speed, integer + stamina, // Current stamina 0-10000, greater being fuller stamina reserves + fatigue, // Current fatigue, integer + mana, // Current available mana, integer + stat_str, // Base STR (strength) stat, integer + stat_dex, // Base DEX (dexterity) stat, integer + stat_int, // Base INT (intelligence) stat, integer + stat_per, // Base PER (perception) stat, integer + bp_hp, // Current hit points of given "bodypart", integer + bp_encumb, // Current encumbrance of given "bodypart", integer + bp_warmth, // Current warmth of give "bodypart", integer + hunger, // TODO + thirst, // TODO + mood, // TODO + last // END OF ENUMS +}; + +// Use enum_traits for generic iteration over widget_var, and string (de-)serialization. +// Use io::string_to_enum( widget_string ) to convert a string to widget_var. +template<> +struct enum_traits { + static constexpr widget_var last = widget_var::last; +}; + +// Use generic_factory for loading JSON data. +class JsonObject; +template +class generic_factory; + +// A widget is a UI element displaying information from the underlying value of a widget_var. +// It may be loaded from a JSON object having "type": "widget". +class widget +{ + private: + friend class generic_factory; + + widget_id id; + bool was_loaded = false; + + public: + widget() = default; + explicit widget( const widget_id &id ) : id( id ) {} + + // Attributes from JSON + // ---- + // Display style to indicate the value: "numeric", "graph", "phrases" + std::string _style; + // Displayed label in the UI + std::string _label; + // Binding variable enum like stamina, bp_hp or stat_dex + widget_var _var; + // Minimum var value, optional + int _var_min = 0; + // Maximum var value, required for graph widgets + int _var_max = 10; + // Body part variable is linked to + bodypart_id _bp_id; + // Width in characters of widget, not including label + int _width = 0; + // String of symbols for graph widgets, mapped in increasing order like "0123..." + std::string _symbols; + // Graph fill style ("bucket" or "pool") + std::string _fill; + // String values mapped to numeric values or ranges + std::vector _strings; + // Colors mapped to values or ranges + std::vector _colors; + // Child widget ids for layout style + std::vector _widgets; + // Child widget layout arrangement / direction + std::string _arrange; + + // Load JSON data for a widget (uses generic factory widget_factory) + static void load_widget( const JsonObject &jo, const std::string &src ); + void load( const JsonObject &jo, const std::string &src ); + // Reset to defaults using generic widget_factory + static void reset(); + + // Layout this widget within max_width, including child widgets. Calling layout on a regular + // (non-layout style) widget is the same as show(), but will pad with spaces inside the + // label area, so the returned string is equal to max_width. + std::string layout( const avatar &ava, unsigned int max_width = 0 ); + // Display labeled widget, with value (number, graph, or string) from an avatar + std::string show( const avatar &ava ); + + // Evaluate and return the bound "var" associated value for an avatar + int get_var_value( const avatar &ava ); + // Return the maximum "var" value from "var_max", or max for avatar (HP, mana, etc.) + int get_var_max( const avatar &ava ); + + // Return a color-enhanced value_string + std::string color_value_string( int value, int value_max = 0 ); + // Return a string for how a given value will render in the UI + std::string value_string( int value, int value_max = 0 ); + // Return a suitable color for a given value + nc_color value_color( int value, int value_max = 0 ); + + // Return a formatted numeric string + std::string number( int value, int value_max = 0 ); + // Return the phrase mapped to a given value for a "phrase" style + std::string phrase( int value, int value_max = 0 ); + // Return the graph part of this widget, rendered with "bucket" or "pool" fill + std::string graph( int value, int value_max = 0 ); + +}; + +#endif // CATA_SRC_WIDGET_H + From 402cdae2335ebd90312584407b6042b7b8ccb0c5 Mon Sep 17 00:00:00 2001 From: Eric Pierce Date: Thu, 15 Oct 2020 18:06:52 -0600 Subject: [PATCH 2/7] Add widget to type_id, cata_variant, dynamic loader --- src/cata_variant.cpp | 1 + src/cata_variant.h | 7 ++++++- src/init.cpp | 2 ++ src/type_id.h | 3 +++ 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/cata_variant.cpp b/src/cata_variant.cpp index 7eda2a373ed22..b0ae50bdde09f 100644 --- a/src/cata_variant.cpp +++ b/src/cata_variant.cpp @@ -68,6 +68,7 @@ std::string enum_to_string( cata_variant_type type ) case cata_variant_type::trait_id: return "trait_id"; case cata_variant_type::trap_str_id: return "trap_str_id"; case cata_variant_type::tripoint: return "tripoint"; + case cata_variant_type::widget_id: return "widget_id"; // *INDENT-ON* case cata_variant_type::num_types: break; diff --git a/src/cata_variant.h b/src/cata_variant.h index e284b0b01ec3e..fb836057f190a 100644 --- a/src/cata_variant.h +++ b/src/cata_variant.h @@ -68,6 +68,7 @@ enum class cata_variant_type : int { trait_id, trap_str_id, tripoint, + widget_id, num_types, // last }; @@ -174,7 +175,7 @@ struct convert_enum { }; // These are the specializations of convert for each value type. -static_assert( static_cast( cata_variant_type::num_types ) == 33, +static_assert( static_cast( cata_variant_type::num_types ) == 34, "This assert is a reminder to add conversion support for any new types to the " "below specializations" ); @@ -356,6 +357,10 @@ struct convert { } }; +template<> +struct convert : convert_string_id {}; + + } // namespace cata_variant_detail template diff --git a/src/init.cpp b/src/init.cpp index 12a86333ca87a..4c8c5b16df6fd 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -96,6 +96,7 @@ #include "vehicle_group.h" #include "vitamin.h" #include "weather_type.h" +#include "widget.h" #include "worldfactory.h" DynamicDataLoader::DynamicDataLoader() @@ -445,6 +446,7 @@ void DynamicDataLoader::initialize() add( "score", &score::load_score ); add( "achievement", &achievement::load_achievement ); add( "conduct", &achievement::load_achievement ); + add( "widget", &widget::load_widget ); #if defined(TILES) add( "mod_tileset", &load_mod_tileset ); #else diff --git a/src/type_id.h b/src/type_id.h index be4c937605a1d..f4a1bb2665147 100644 --- a/src/type_id.h +++ b/src/type_id.h @@ -219,4 +219,7 @@ using flag_id = string_id; using json_character_flag = string_id; +class widget; +using widget_id = string_id; + #endif // CATA_SRC_TYPE_ID_H From b1bd32dd3158acab8ad961a9ce89b2c10a23396a Mon Sep 17 00:00:00 2001 From: Eric Pierce Date: Thu, 15 Oct 2020 18:07:52 -0600 Subject: [PATCH 3/7] Add widget tests and test data --- data/mods/TEST_DATA/widgets.json | 179 +++++++++++++++++ tests/widget_test.cpp | 322 +++++++++++++++++++++++++++++++ 2 files changed, 501 insertions(+) create mode 100644 data/mods/TEST_DATA/widgets.json create mode 100644 tests/widget_test.cpp diff --git a/data/mods/TEST_DATA/widgets.json b/data/mods/TEST_DATA/widgets.json new file mode 100644 index 0000000000000..c54b494476bc7 --- /dev/null +++ b/data/mods/TEST_DATA/widgets.json @@ -0,0 +1,179 @@ +[ + { + "id": "test_bucket_graph", + "type": "widget", + "style": "graph", + "label": "BUCKET", + "width": 4, + "symbols": "0123", + "fill": "bucket" + }, + { + "id": "test_pool_graph", + "type": "widget", + "style": "graph", + "label": "POOL", + "width": 4, + "symbols": "0123", + "fill": "pool", + "var_max": 240 + }, + { + "id": "test_number_widget", + "type": "widget", + "label": "NUM", + "style": "number" + }, + { + "id": "test_color_number_widget", + "type": "widget", + "label": "COLORNUM", + "style": "number", + "//": "var_min to var_max maps exactly 1:1 with color index", + "var_min": 0, + "var_max": 2, + "colors": [ "c_red", "c_yellow", "c_green" ] + }, + { + "id": "test_color_graph_widget", + "type": "widget", + "label": "COLORGRAPH", + "style": "graph", + "width": 5, + "symbols": "-=#", + "var_min": 0, + "var_max": 10, + "colors": [ "c_red", "c_yellow", "c_light_green", "c_green" ] + }, + { + "id": "test_color_graph_10k_widget", + "type": "widget", + "label": "COLORGRAPH", + "style": "graph", + "width": 10, + "symbols": "-=#", + "fill": "pool", + "var_min": 0, + "var_max": 10000, + "colors": [ "c_red", "c_light_red", "c_yellow", "c_light_green", "c_green" ] + }, + { + "id": "test_focus_num", + "type": "widget", + "label": "FOCUS", + "var": "focus", + "style": "number" + }, + { + "id": "test_mana_num", + "type": "widget", + "label": "MANA", + "var": "mana", + "style": "number" + }, + { + "id": "test_speed_num", + "type": "widget", + "label": "SPEED", + "var": "speed", + "style": "number" + }, + { + "id": "test_stamina_num", + "type": "widget", + "label": "STAMINA", + "var": "stamina", + "style": "number" + }, + { + "id": "test_stamina_graph", + "type": "widget", + "label": "STAMINA", + "var": "stamina", + "style": "graph", + "fill": "pool", + "width": 10, + "symbols": "-=#", + "var_max": 10000 + }, + { + "id": "test_sound_num", + "type": "widget", + "label": "SOUND", + "var": "sound", + "style": "number" + }, + { + "id": "test_move_num", + "type": "widget", + "label": "MOVE", + "var": "move", + "style": "number" + }, + { + "id": "test_str_num", + "type": "widget", + "label": "STR", + "var": "stat_str", + "style": "number" + }, + { + "id": "test_dex_num", + "type": "widget", + "label": "DEX", + "var": "stat_dex", + "style": "number" + }, + { + "id": "test_int_num", + "type": "widget", + "label": "INT", + "var": "stat_int", + "style": "number" + }, + { + "id": "test_per_num", + "type": "widget", + "label": "PER", + "var": "stat_per", + "style": "number" + }, + { + "id": "test_hp_head_graph", + "type": "widget", + "label": "HEAD", + "var": "bp_hp", + "bodypart": "head", + "style": "graph", + "width": 5, + "symbols": ",\\|", + "fill": "bucket" + }, + { + "id": "test_hp_head_num", + "type": "widget", + "label": "HEAD", + "var": "bp_hp", + "bodypart": "head", + "style": "number" + }, + { + "id": "test_stat_panel", + "type": "widget", + "style": "layout", + "widgets": [ "test_str_num", "test_dex_num", "test_int_num", "test_per_num" ] + }, + { + "id": "test_phrase_widget", + "type": "widget", + "label": "PHRASE", + "style": "phrase", + "strings": [ "Zero", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten" ] + }, + { + "id": "test_layout_list", + "type": "widget", + "style": "layout", + "widgets": [ "test_phrase_widget", "test_pool_graph", "test_number_widget" ] + } +] diff --git a/tests/widget_test.cpp b/tests/widget_test.cpp new file mode 100644 index 0000000000000..155a589e8e13e --- /dev/null +++ b/tests/widget_test.cpp @@ -0,0 +1,322 @@ +#include "catch/catch.hpp" + +#include "player_helpers.h" +#include "widget.h" + +// test widgets defined in data/json/sidebar.json and data/mods/TEST_DATA/widgets.json + +TEST_CASE( "widget value strings", "[widget][value][string]" ) +{ + SECTION( "numeric values" ) { + widget focus = widget_id( "test_focus_num" ).obj(); + REQUIRE( focus._style == "number" ); + CHECK( focus.value_string( 0 ) == "0" ); + CHECK( focus.value_string( 50 ) == "50" ); + CHECK( focus.value_string( 100 ) == "100" ); + } + + SECTION( "graph values with bucket fill" ) { + widget head = widget_id( "test_hp_head_graph" ).obj(); + REQUIRE( head._style == "graph" ); + REQUIRE( head._fill == "bucket" ); + // Buckets of width 5 with 2 nonzero symbols can show 10 values + REQUIRE( head._width == 5 ); + // Uses comma instead of period to avoid clang-tidy complaining about ellipses + REQUIRE( head._symbols == ",\\|" ); + // Each integer step cycles the graph symbol by one + CHECK( head.value_string( 0 ) == ",,,,," ); + CHECK( head.value_string( 1 ) == "\\,,,," ); + CHECK( head.value_string( 2 ) == "|,,,," ); + CHECK( head.value_string( 3 ) == "|\\,,," ); + CHECK( head.value_string( 4 ) == "||,,," ); + CHECK( head.value_string( 5 ) == "||\\,," ); + CHECK( head.value_string( 6 ) == "|||,," ); + CHECK( head.value_string( 7 ) == "|||\\," ); + CHECK( head.value_string( 8 ) == "||||," ); + CHECK( head.value_string( 9 ) == "||||\\" ); + CHECK( head.value_string( 10 ) == "|||||" ); + // Above max displayable value, show max + CHECK( head.value_string( 11 ) == "|||||" ); + CHECK( head.value_string( 12 ) == "|||||" ); + // Below minimum, show min + CHECK( head.value_string( -1 ) == ",,,,," ); + CHECK( head.value_string( -2 ) == ",,,,," ); + } + + SECTION( "graph values with pool fill" ) { + widget stamina = widget_id( "test_stamina_graph" ).obj(); + REQUIRE( stamina._style == "graph" ); + REQUIRE( stamina._fill == "pool" ); + // Pool of width 20 with 2 nonzero symbols can show 20 values + REQUIRE( stamina._width == 10 ); + REQUIRE( stamina._symbols == "-=#" ); + // Each integer step increases the graph symbol by one + CHECK( stamina.value_string( 0 ) == "----------" ); + CHECK( stamina.value_string( 1 ) == "=---------" ); + CHECK( stamina.value_string( 2 ) == "==--------" ); + CHECK( stamina.value_string( 3 ) == "===-------" ); + CHECK( stamina.value_string( 4 ) == "====------" ); + CHECK( stamina.value_string( 6 ) == "======----" ); + CHECK( stamina.value_string( 8 ) == "========--" ); + CHECK( stamina.value_string( 10 ) == "==========" ); + CHECK( stamina.value_string( 12 ) == "##========" ); + CHECK( stamina.value_string( 14 ) == "####======" ); + CHECK( stamina.value_string( 16 ) == "######====" ); + CHECK( stamina.value_string( 18 ) == "########==" ); + CHECK( stamina.value_string( 20 ) == "##########" ); + // Above max displayable value, show max + CHECK( stamina.value_string( 21 ) == "##########" ); + CHECK( stamina.value_string( 22 ) == "##########" ); + // Below minimum, show min + CHECK( stamina.value_string( -1 ) == "----------" ); + CHECK( stamina.value_string( -2 ) == "----------" ); + } +} + +TEST_CASE( "widgets", "[widget][graph][color]" ) +{ + SECTION( "phrase widgets" ) { + widget words = widget_id( "test_phrase_widget" ).obj(); + REQUIRE( words._style == "phrase" ); + + CHECK( words.phrase( 0 ) == "Zero" ); + CHECK( words.phrase( 1 ) == "One" ); + CHECK( words.phrase( 2 ) == "Two" ); + CHECK( words.phrase( 3 ) == "Three" ); + CHECK( words.phrase( 4 ) == "Four" ); + CHECK( words.phrase( 5 ) == "Five" ); + CHECK( words.phrase( 6 ) == "Six" ); + CHECK( words.phrase( 7 ) == "Seven" ); + CHECK( words.phrase( 8 ) == "Eight" ); + CHECK( words.phrase( 9 ) == "Nine" ); + CHECK( words.phrase( 10 ) == "Ten" ); + } + + SECTION( "number widget with color" ) { + widget colornum = widget_id( "test_color_number_widget" ).obj(); + REQUIRE( colornum._style == "number" ); + REQUIRE( colornum._colors.size() == 3 ); + REQUIRE( colornum._var_max == 2 ); + + CHECK( colornum.color_value_string( 0 ) == "0" ); + CHECK( colornum.color_value_string( 1 ) == "1" ); + CHECK( colornum.color_value_string( 2 ) == "2" ); + // Beyond var_max, stays at max color + CHECK( colornum.color_value_string( 3 ) == "3" ); + } + + SECTION( "graph widget with color" ) { + widget colornum = widget_id( "test_color_graph_widget" ).obj(); + REQUIRE( colornum._style == "graph" ); + REQUIRE( colornum._colors.size() == 4 ); + REQUIRE( colornum._var_max == 10 ); + + // with +0.5: 2r, 3y, 4lg, 2g + CHECK( colornum.color_value_string( 0 ) == "-----" ); + CHECK( colornum.color_value_string( 1 ) == "=----" ); + CHECK( colornum.color_value_string( 2 ) == "#----" ); + CHECK( colornum.color_value_string( 3 ) == "#=---" ); + CHECK( colornum.color_value_string( 4 ) == "##---" ); + CHECK( colornum.color_value_string( 5 ) == "##=--" ); + CHECK( colornum.color_value_string( 6 ) == "###--" ); + CHECK( colornum.color_value_string( 7 ) == "###=-" ); + CHECK( colornum.color_value_string( 8 ) == "####-" ); + CHECK( colornum.color_value_string( 9 ) == "####=" ); + CHECK( colornum.color_value_string( 10 ) == "#####" ); + // Beyond var_max, stays at max color + CHECK( colornum.color_value_string( 11 ) == "#####" ); + + // Long / large var graph + widget graph10k = widget_id( "test_color_graph_10k_widget" ).obj(); + REQUIRE( graph10k._style == "graph" ); + REQUIRE( graph10k._colors.size() == 5 ); + REQUIRE( graph10k._var_max == 10000 ); + + CHECK( graph10k.color_value_string( 0 ) == "----------" ); + CHECK( graph10k.color_value_string( 2500 ) == "=====-----" ); + CHECK( graph10k.color_value_string( 5000 ) == "==========" ); + CHECK( graph10k.color_value_string( 7500 ) == "#####=====" ); + CHECK( graph10k.color_value_string( 10000 ) == "##########" ); + } + + SECTION( "graph widgets" ) { + SECTION( "bucket fill" ) { + widget wid = widget_id( "test_bucket_graph" ).obj(); + REQUIRE( wid._style == "graph" ); + REQUIRE( wid._fill == "bucket" ); + + CHECK( wid.graph( 0 ) == "0000" ); + CHECK( wid.graph( 1 ) == "1000" ); + CHECK( wid.graph( 2 ) == "2000" ); + CHECK( wid.graph( 3 ) == "3000" ); + CHECK( wid.graph( 4 ) == "3100" ); + CHECK( wid.graph( 5 ) == "3200" ); + CHECK( wid.graph( 6 ) == "3300" ); + CHECK( wid.graph( 7 ) == "3310" ); + CHECK( wid.graph( 8 ) == "3320" ); + CHECK( wid.graph( 9 ) == "3330" ); + CHECK( wid.graph( 10 ) == "3331" ); + CHECK( wid.graph( 11 ) == "3332" ); + CHECK( wid.graph( 12 ) == "3333" ); + } + SECTION( "pool fill" ) { + widget wid = widget_id( "test_pool_graph" ).obj(); + REQUIRE( wid._style == "graph" ); + REQUIRE( wid._fill == "pool" ); + + CHECK( wid.graph( 0 ) == "0000" ); + CHECK( wid.graph( 1 ) == "1000" ); + CHECK( wid.graph( 2 ) == "1100" ); + CHECK( wid.graph( 3 ) == "1110" ); + CHECK( wid.graph( 4 ) == "1111" ); + CHECK( wid.graph( 5 ) == "2111" ); + CHECK( wid.graph( 6 ) == "2211" ); + CHECK( wid.graph( 7 ) == "2221" ); + CHECK( wid.graph( 8 ) == "2222" ); + CHECK( wid.graph( 9 ) == "3222" ); + CHECK( wid.graph( 10 ) == "3322" ); + CHECK( wid.graph( 11 ) == "3332" ); + CHECK( wid.graph( 12 ) == "3333" ); + } + } + + SECTION( "graph hit points" ) { + widget wid = widget_id( "test_hp_head_graph" ).obj(); + REQUIRE( wid._fill == "bucket" ); + + CHECK( wid._label == "HEAD" ); + CHECK( wid.graph( 0 ) == ",,,,," ); + CHECK( wid.graph( 1 ) == "\\,,,," ); + CHECK( wid.graph( 2 ) == "|,,,," ); + CHECK( wid.graph( 3 ) == "|\\,,," ); + CHECK( wid.graph( 4 ) == "||,,," ); + CHECK( wid.graph( 5 ) == "||\\,," ); + CHECK( wid.graph( 6 ) == "|||,," ); + CHECK( wid.graph( 7 ) == "|||\\," ); + CHECK( wid.graph( 8 ) == "||||," ); + CHECK( wid.graph( 9 ) == "||||\\" ); + CHECK( wid.graph( 10 ) == "|||||" ); + } +} + +TEST_CASE( "widgets showing avatar attributes", "[widget][avatar]" ) +{ + avatar &ava = get_avatar(); + clear_avatar(); + + SECTION( "base stats str / dex / int / per" ) { + widget str_w = widget_id( "test_str_num" ).obj(); + widget dex_w = widget_id( "test_dex_num" ).obj(); + widget int_w = widget_id( "test_int_num" ).obj(); + widget per_w = widget_id( "test_per_num" ).obj(); + + ava.str_max = 8; + ava.dex_max = 10; + ava.int_max = 7; + ava.per_max = 13; + + CHECK( str_w.layout( ava ) == "STR: 8" ); + CHECK( dex_w.layout( ava ) == "DEX: 10" ); + CHECK( int_w.layout( ava ) == "INT: 7" ); + CHECK( per_w.layout( ava ) == "PER: 13" ); + } + + SECTION( "stamina" ) { + widget stamina_num_w = widget_id( "test_stamina_num" ).obj(); + widget stamina_graph_w = widget_id( "test_stamina_graph" ).obj(); + REQUIRE( stamina_graph_w._fill == "pool" ); + REQUIRE( stamina_graph_w._symbols == "-=#" ); + + ava.set_stamina( 0 ); + CHECK( stamina_num_w.layout( ava ) == "STAMINA: 0" ); + CHECK( stamina_graph_w.layout( ava ) == "STAMINA: ----------" ); + ava.set_stamina( 2500 ); + CHECK( stamina_num_w.layout( ava ) == "STAMINA: 2500" ); + CHECK( stamina_graph_w.layout( ava ) == "STAMINA: =====-----" ); + ava.set_stamina( 5000 ); + CHECK( stamina_num_w.layout( ava ) == "STAMINA: 5000" ); + CHECK( stamina_graph_w.layout( ava ) == "STAMINA: ==========" ); + ava.set_stamina( 7500 ); + CHECK( stamina_num_w.layout( ava ) == "STAMINA: 7500" ); + CHECK( stamina_graph_w.layout( ava ) == "STAMINA: #####=====" ); + ava.set_stamina( 10000 ); + CHECK( stamina_num_w.layout( ava ) == "STAMINA: 10000" ); + CHECK( stamina_graph_w.layout( ava ) == "STAMINA: ##########" ); + } + + SECTION( "speed pool" ) { + widget speed_w = widget_id( "test_speed_num" ).obj(); + + ava.set_speed_base( 90 ); + CHECK( speed_w.layout( ava ) == "SPEED: 90" ); + ava.set_speed_base( 240 ); + CHECK( speed_w.layout( ava ) == "SPEED: 240" ); + } + + SECTION( "focus pool" ) { + widget focus_w = widget_id( "test_focus_num" ).obj(); + + ava.set_focus( 75 ); + CHECK( focus_w.layout( ava ) == "FOCUS: 75" ); + ava.set_focus( 120 ); + CHECK( focus_w.layout( ava ) == "FOCUS: 120" ); + } + + SECTION( "mana pool" ) { + widget mana_w = widget_id( "test_mana_num" ).obj(); + + ava.magic->set_mana( 150 ); + CHECK( mana_w.layout( ava ) == "MANA: 150" ); + ava.magic->set_mana( 450 ); + CHECK( mana_w.layout( ava ) == "MANA: 450" ); + } + + SECTION( "move counter" ) { + widget move_w = widget_id( "test_move_num" ).obj(); + + ava.movecounter = 80; + CHECK( move_w.layout( ava ) == "MOVE: 80" ); + ava.movecounter = 150; + CHECK( move_w.layout( ava ) == "MOVE: 150" ); + } + + SECTION( "hit points" ) { + bodypart_id head( "head" ); + widget head_num_w = widget_id( "test_hp_head_num" ).obj(); + widget head_graph_w = widget_id( "test_hp_head_graph" ).obj(); + REQUIRE( ava.get_part_hp_max( head ) == 84 ); + REQUIRE( ava.get_part_hp_cur( head ) == 84 ); + + ava.set_part_hp_cur( head, 84 ); + CHECK( head_num_w.layout( ava ) == "HEAD: 84" ); + CHECK( head_graph_w.layout( ava ) == "HEAD: |||||" ); + ava.set_part_hp_cur( head, 42 ); + CHECK( head_num_w.layout( ava ) == "HEAD: 42" ); + CHECK( head_graph_w.layout( ava ) == "HEAD: ||\\,," ); + ava.set_part_hp_cur( head, 17 ); + CHECK( head_num_w.layout( ava ) == "HEAD: 17" ); + CHECK( head_graph_w.layout( ava ) == "HEAD: |,,,," ); + ava.set_part_hp_cur( head, 0 ); + CHECK( head_num_w.layout( ava ) == "HEAD: 0" ); + // NOLINTNEXTLINE(cata-text-style): suppress "unnecessary space" warning before commas + CHECK( head_graph_w.layout( ava ) == "HEAD: ,,,,," ); + } +} + +TEST_CASE( "layout widgets", "[widget][layout]" ) +{ + widget stats_w = widget_id( "test_stat_panel" ).obj(); + + avatar &ava = get_avatar(); + clear_avatar(); + + CHECK( stats_w.layout( ava, 32 ) == + string_format( "STR: 8 DEX: 8 INT: 8 PER : 8" ) ); + CHECK( stats_w.layout( ava, 38 ) == + string_format( "STR : 8 DEX : 8 INT : 8 PER : 8" ) ); + CHECK( stats_w.layout( ava, 40 ) == + string_format( "STR : 8 DEX : 8 INT : 8 PER : 8" ) ); + CHECK( stats_w.layout( ava, 42 ) == + string_format( "STR : 8 DEX : 8 INT : 8 PER : 8" ) ); +} + From 2242aae741f7609fbae5796a06e2c641a239f354 Mon Sep 17 00:00:00 2001 From: Eric Pierce Date: Thu, 15 Oct 2020 18:09:08 -0600 Subject: [PATCH 4/7] Add JSON for sidebar mod Build on the data-driven sidebar `widget` class, defining a collection of numeric and graph display elements, including familiar as well as new informational displays: - HP bar graphs for each body part, in familiar `|||||` form - Classic and extended (10-character) stamina graph - Numeric indicators for sound, focus, pain, moves, str, dex, int, per - Encumbrance bar graphs for each body part - Warmth numbers for each body part - Wide and Narrow root layouts with basic info --- data/json/sidebar.json | 457 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 457 insertions(+) create mode 100644 data/json/sidebar.json diff --git a/data/json/sidebar.json b/data/json/sidebar.json new file mode 100644 index 0000000000000..359e0d8dd6d8f --- /dev/null +++ b/data/json/sidebar.json @@ -0,0 +1,457 @@ +[ + { + "id": "hitpoint_graph", + "type": "widget", + "style": "graph", + "width": 5, + "symbols": ".\\|", + "fill": "bucket", + "colors": [ "c_red", "c_light_red", "c_yellow", "c_light_green", "c_green" ] + }, + { + "id": "encumbrance_graph", + "type": "widget", + "style": "graph", + "var_max": 50, + "width": 5, + "symbols": "-vX", + "fill": "bucket" + }, + { + "id": "warmth_num", + "type": "widget", + "style": "number", + "var_max": 10000, + "colors": [ "c_blue", "c_cyan", "c_light_blue", "c_green", "c_yellow", "c_light_red", "c_red" ] + }, + { + "id": "hp_head_graph", + "type": "widget", + "label": "HEAD", + "var": "bp_hp", + "bodypart": "head", + "copy-from": "hitpoint_graph" + }, + { + "id": "hp_torso_graph", + "type": "widget", + "label": "TORSO", + "var": "bp_hp", + "bodypart": "torso", + "copy-from": "hitpoint_graph" + }, + { + "id": "hp_left_arm_graph", + "type": "widget", + "label": "L ARM", + "var": "bp_hp", + "bodypart": "arm_l", + "copy-from": "hitpoint_graph" + }, + { + "id": "hp_right_arm_graph", + "type": "widget", + "label": "R ARM", + "var": "bp_hp", + "bodypart": "arm_r", + "copy-from": "hitpoint_graph" + }, + { + "id": "hp_left_leg_graph", + "type": "widget", + "label": "L LEG", + "var": "bp_hp", + "bodypart": "leg_l", + "copy-from": "hitpoint_graph" + }, + { + "id": "hp_right_leg_graph", + "type": "widget", + "label": "R LEG", + "var": "bp_hp", + "bodypart": "leg_r", + "copy-from": "hitpoint_graph" + }, + { + "id": "warmth_head_num", + "type": "widget", + "label": "w:H", + "var": "bp_warmth", + "bodypart": "head", + "copy-from": "warmth_num" + }, + { + "id": "warmth_torso_num", + "type": "widget", + "label": "w:T", + "var": "bp_warmth", + "bodypart": "torso", + "copy-from": "warmth_num" + }, + { + "id": "warmth_left_arm_num", + "type": "widget", + "label": "w:LA", + "var": "bp_warmth", + "bodypart": "arm_l", + "copy-from": "warmth_num" + }, + { + "id": "warmth_right_arm_num", + "type": "widget", + "label": "w:RA", + "var": "bp_warmth", + "bodypart": "arm_r", + "copy-from": "warmth_num" + }, + { + "id": "warmth_left_leg_num", + "type": "widget", + "label": "w:LL", + "var": "bp_warmth", + "bodypart": "leg_l", + "copy-from": "warmth_num" + }, + { + "id": "warmth_right_leg_num", + "type": "widget", + "label": "w:RL", + "var": "bp_warmth", + "bodypart": "leg_r", + "copy-from": "warmth_num" + }, + { + "id": "encumbrance_head_graph", + "type": "widget", + "label": "e:H", + "var": "bp_encumb", + "bodypart": "head", + "copy-from": "encumbrance_graph" + }, + { + "id": "encumbrance_torso_graph", + "type": "widget", + "label": "e:T", + "var": "bp_encumb", + "bodypart": "torso", + "copy-from": "encumbrance_graph" + }, + { + "id": "encumbrance_left_arm_graph", + "type": "widget", + "label": "e:LA", + "var": "bp_encumb", + "bodypart": "arm_l", + "copy-from": "encumbrance_graph" + }, + { + "id": "encumbrance_right_arm_graph", + "type": "widget", + "label": "e:RA", + "var": "bp_encumb", + "bodypart": "arm_r", + "copy-from": "encumbrance_graph" + }, + { + "id": "encumbrance_left_leg_graph", + "type": "widget", + "label": "e:LL", + "var": "bp_encumb", + "bodypart": "leg_l", + "copy-from": "encumbrance_graph" + }, + { + "id": "encumbrance_right_leg_graph", + "type": "widget", + "label": "e:RL", + "var": "bp_encumb", + "bodypart": "leg_r", + "copy-from": "encumbrance_graph" + }, + { + "id": "stamina_graph_classic", + "type": "widget", + "label": "Stam", + "var": "stamina", + "var_max": 10000, + "style": "graph", + "width": 5, + "symbols": ".\\|", + "colors": [ "c_red", "c_light_red", "c_yellow", "c_light_green", "c_green" ] + }, + { + "id": "stamina_graph", + "type": "widget", + "label": "Stam", + "var": "stamina", + "var_max": 10000, + "style": "graph", + "width": 10, + "symbols": ".\\|", + "colors": [ "c_red", "c_light_red", "c_yellow", "c_light_green", "c_green" ] + }, + { + "id": "focus_num", + "type": "widget", + "label": "Focus", + "var": "focus", + "style": "number" + }, + { + "id": "speed_num", + "type": "widget", + "label": "Speed", + "var": "speed", + "style": "number" + }, + { + "id": "stamina_num", + "type": "widget", + "label": "Stamina", + "var": "stamina", + "style": "number" + }, + { + "id": "fatigue_num", + "type": "widget", + "label": "Fatigue", + "var": "fatigue", + "style": "number" + }, + { + "id": "fatigue_graph", + "type": "widget", + "label": "Fatig", + "var": "fatigue", + "var_max": 1000, + "style": "graph", + "fill": "bucket", + "width": 5, + "symbols": "-fF", + "colors": [ "c_green", "c_yellow", "c_red" ] + }, + { + "id": "sound_num", + "type": "widget", + "label": "Sound", + "var": "sound", + "style": "number" + }, + { + "id": "move_num", + "type": "widget", + "label": "Move", + "var": "move", + "style": "number" + }, + { + "id": "pain_num", + "type": "widget", + "label": "Pain", + "var": "pain", + "style": "number" + }, + { + "id": "str_num", + "type": "widget", + "label": "Str", + "var": "stat_str", + "style": "number" + }, + { + "id": "dex_num", + "type": "widget", + "label": "Dex", + "var": "stat_dex", + "style": "number" + }, + { + "id": "int_num", + "type": "widget", + "label": "Int", + "var": "stat_int", + "style": "number" + }, + { + "id": "per_num", + "type": "widget", + "label": "Per", + "var": "stat_per", + "style": "number" + }, + { + "id": "hitpoints_top_layout", + "type": "widget", + "style": "layout", + "arrange": "columns", + "widgets": [ "hp_left_arm_graph", "hp_head_graph", "hp_right_arm_graph" ] + }, + { + "id": "hitpoints_bottom_layout", + "type": "widget", + "style": "layout", + "arrange": "columns", + "widgets": [ "hp_left_leg_graph", "hp_torso_graph", "hp_right_leg_graph" ] + }, + { + "id": "hitpoints_head_torso", + "type": "widget", + "style": "layout", + "arrange": "columns", + "widgets": [ "hp_head_graph", "hp_torso_graph" ] + }, + { + "id": "hitpoints_arms", + "type": "widget", + "style": "layout", + "arrange": "columns", + "widgets": [ "hp_left_arm_graph", "hp_right_arm_graph" ] + }, + { + "id": "hitpoints_legs", + "type": "widget", + "style": "layout", + "arrange": "columns", + "widgets": [ "hp_left_leg_graph", "hp_right_leg_graph" ] + }, + { + "id": "encumbrance_top_layout", + "type": "widget", + "style": "layout", + "arrange": "columns", + "widgets": [ "encumbrance_left_arm_graph", "encumbrance_head_graph", "encumbrance_right_arm_graph" ] + }, + { + "id": "encumbrance_bottom_layout", + "type": "widget", + "style": "layout", + "arrange": "columns", + "widgets": [ "encumbrance_left_leg_graph", "encumbrance_torso_graph", "encumbrance_right_leg_graph" ] + }, + { + "id": "warmth_top_layout", + "type": "widget", + "style": "layout", + "arrange": "columns", + "widgets": [ "warmth_left_arm_num", "warmth_head_num", "warmth_right_arm_num" ] + }, + { + "id": "warmth_bottom_layout", + "type": "widget", + "style": "layout", + "arrange": "columns", + "widgets": [ "warmth_left_leg_num", "warmth_torso_num", "warmth_right_leg_num" ] + }, + { + "id": "stamina_fatigue_layout", + "type": "widget", + "style": "layout", + "arrange": "columns", + "widgets": [ "stamina_graph", "fatigue_graph" ] + }, + { + "id": "stamina_speed_layout", + "type": "widget", + "style": "layout", + "arrange": "columns", + "widgets": [ "stamina_graph_classic", "speed_num" ] + }, + { + "id": "focus_move_layout", + "type": "widget", + "style": "layout", + "arrange": "columns", + "widgets": [ "focus_num", "move_num" ] + }, + { + "id": "stamina_fatigue_focus_layout", + "type": "widget", + "style": "layout", + "arrange": "columns", + "widgets": [ "stamina_graph_classic", "fatigue_graph", "focus_num" ] + }, + { + "id": "speed_move_layout", + "type": "widget", + "style": "layout", + "arrange": "columns", + "widgets": [ "speed_num", "move_num" ] + }, + { + "id": "sound_speed_move_layout", + "type": "widget", + "style": "layout", + "arrange": "columns", + "widgets": [ "sound_num", "speed_num", "move_num" ] + }, + { + "id": "sound_fatigue_focus_layout", + "type": "widget", + "style": "layout", + "arrange": "columns", + "widgets": [ "sound_num", "fatigue_graph", "focus_num" ] + }, + { + "id": "stamina_speed_move_layout", + "type": "widget", + "style": "layout", + "arrange": "columns", + "widgets": [ "stamina_graph_classic", "speed_num", "move_num" ] + }, + { + "id": "sound_focus_layout", + "type": "widget", + "style": "layout", + "arrange": "columns", + "widgets": [ "sound_num", "focus_num" ] + }, + { + "id": "stats_layout", + "type": "widget", + "style": "layout", + "arrange": "columns", + "widgets": [ "str_num", "dex_num", "int_num", "per_num" ] + }, + { + "id": "str_dex_layout", + "type": "widget", + "style": "layout", + "arrange": "columns", + "widgets": [ "str_num", "dex_num" ] + }, + { + "id": "int_per_layout", + "type": "widget", + "style": "layout", + "arrange": "columns", + "widgets": [ "int_num", "per_num" ] + }, + { + "id": "root_layout_wide", + "type": "widget", + "style": "layout", + "arrange": "rows", + "widgets": [ + "hitpoints_top_layout", + "hitpoints_bottom_layout", + "sound_fatigue_focus_layout", + "stamina_speed_move_layout", + "stats_layout" + ] + }, + { + "id": "root_layout_narrow", + "type": "widget", + "style": "layout", + "arrange": "rows", + "widgets": [ + "hitpoints_head_torso", + "hitpoints_arms", + "hitpoints_legs", + "stamina_speed_layout", + "focus_move_layout", + "str_dex_layout", + "int_per_layout" + ] + } +] From 91ee83a553096f2fda7b406401f1d80d8fadc095 Mon Sep 17 00:00:00 2001 From: Eric Pierce Date: Thu, 15 Oct 2020 18:09:28 -0600 Subject: [PATCH 5/7] Add custom sidebar demo to main sidebar panel Add a "Custom" sidebar section that can be toggled on for each sidebar layout (classic/labels and narrow/compact), loading from the "root_layout_wide" or "root_layout_narrow" widget. --- src/panels.cpp | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/panels.cpp b/src/panels.cpp index d941844a74602..ae892c6d03c95 100644 --- a/src/panels.cpp +++ b/src/panels.cpp @@ -57,6 +57,7 @@ #include "vpart_position.h" #include "weather.h" #include "weather_type.h" +#include "widget.h" static const trait_id trait_NOPAIN( "NOPAIN" ); static const trait_id trait_SELFAWARE( "SELFAWARE" ); @@ -1927,6 +1928,35 @@ static void draw_overmap_wide( avatar &u, const catacurses::window &w ) wnoutrefresh( w ); } +// Custom moddable sidebar +static void draw_mod_sidebar( avatar &u, const catacurses::window &w, const std::string layout_name, + const int width ) +{ + werase( w ); + + // Render each row of the root layout widget + widget root = widget_id( layout_name ).obj(); + int row_num = 0; + for( const widget_id &row_wid : root._widgets ) { + widget row_widget = row_wid.obj(); + trim_and_print( w, point( 1, row_num ), width - 1, c_light_gray, _( row_widget.layout( u, + width - 1 ) ) ); + row_num++; + } + + wnoutrefresh( w ); +} + +static void draw_mod_sidebar_narrow( avatar &u, const catacurses::window &w ) +{ + draw_mod_sidebar( u, w, "root_layout_narrow", 31 ); +} + +static void draw_mod_sidebar_wide( avatar &u, const catacurses::window &w ) +{ + draw_mod_sidebar( u, w, "root_layout_wide", 43 ); +} + static void draw_veh_compact( const avatar &u, const catacurses::window &w ) { werase( w ); @@ -2317,6 +2347,8 @@ static std::vector initialize_default_classic_panels() 20, 44, false ) ); ret.emplace_back( window_panel( draw_messages_classic, "Log", to_translation( "Log" ), -2, 44, true ) ); + ret.emplace_back( window_panel( draw_mod_sidebar_wide, "Custom", to_translation( "Custom" ), + 8, 44, false ) ); #if defined(TILES) ret.emplace_back( window_panel( draw_mminimap, "Map", to_translation( "Map" ), -1, 44, true, default_render, true ) ); @@ -2356,6 +2388,8 @@ static std::vector initialize_default_compact_panels() 8, 32, true ) ); ret.emplace_back( window_panel( draw_overmap_narrow, "Overmap", to_translation( "Overmap" ), 14, 32, false ) ); + ret.emplace_back( window_panel( draw_mod_sidebar_narrow, "Custom", to_translation( "Custom" ), + 8, 32, false ) ); #if defined(TILES) ret.emplace_back( window_panel( draw_mminimap, "Map", to_translation( "Map" ), -1, 32, true, default_render, true ) ); @@ -2404,6 +2438,8 @@ static std::vector initialize_default_label_narrow_panels() 8, 32, true ) ); ret.emplace_back( window_panel( draw_overmap_narrow, "Overmap", to_translation( "Overmap" ), 14, 32, false ) ); + ret.emplace_back( window_panel( draw_mod_sidebar_narrow, "Custom", to_translation( "Custom" ), + 8, 32, false ) ); #if defined(TILES) ret.emplace_back( window_panel( draw_mminimap, "Map", to_translation( "Map" ), -1, 32, true, default_render, true ) ); @@ -2456,6 +2492,8 @@ static std::vector initialize_default_label_panels() 8, 44, true ) ); ret.emplace_back( window_panel( draw_overmap_wide, "Overmap", to_translation( "Overmap" ), 20, 44, false ) ); + ret.emplace_back( window_panel( draw_mod_sidebar_wide, "Custom", to_translation( "Custom" ), + 8, 44, false ) ); #if defined(TILES) ret.emplace_back( window_panel( draw_mminimap, "Map", to_translation( "Map" ), -1, 44, true, default_render, true ) ); From 37425f784f0cbd881dce2a1763f4986582691b44 Mon Sep 17 00:00:00 2001 From: Eric Pierce Date: Thu, 15 Oct 2020 18:08:35 -0600 Subject: [PATCH 6/7] Add doc/SIDEBAR_MOD.md on data-driven sidebar Overview and technical docs on using the custom sidebar widgets --- doc/SIDEBAR_MOD.md | 320 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 320 insertions(+) create mode 100644 doc/SIDEBAR_MOD.md diff --git a/doc/SIDEBAR_MOD.md b/doc/SIDEBAR_MOD.md new file mode 100644 index 0000000000000..f4100bf2f1f48 --- /dev/null +++ b/doc/SIDEBAR_MOD.md @@ -0,0 +1,320 @@ +# Sidebar Modification + +- [Overview](#overview) +- [About widgets](#about-widgets) +- [Widget variables](#widget-variables) +- [Number widget](#number-widget) +- [Graph widget](#graph-widget) + - [fill](#fill) + - [var_max](#var-max) +- [Layout widget](#layout-widget) + - [Root layouts](#root-layouts) + + +## Overview + +Some parts of the main CDDA sidebar are now moddable, meaning they are data-driven and can be +customized simply by editing JSON files, without recompiling the game. + +You can add the custom sidebar via the Sidebar Options menu `}` by enabling the "Custom" section. + + +## About widgets + +Sidebar UI elements are defined in objects called widgets. A widget can display a variety of player +character attributes in numeric form, or as a bar graph of arbitrary width. A widget can also make a +layout of other widgets. + +Widget instances are defined by JSON data, with the main game sidebar widgets and layouts being in +`data/json/sidebar.json`. You may customize yours by editing this file, or by loading a mod that +adds or modifies widget definitions. + +For example, here is a widget to display the player character's "Focus" attribute as a number: + +```json +{ + "id": "focus_num", + "type": "widget", + "label": "Focus", + "var": "focus", + "style": "number" +} +``` + +All widgets must have a unique "id", and "type": "widget". + +Widgets have the following "style" options: + +- `number`: Display value as a plain integer number +- `graph`: Show a bar graph of the value with colored text characters +- `layout`: Special style; this widget will be a layout container for other widgets + +Non-layout widgets must define a "var" field, with the name of a predefined widget variable. + + +## Widget variables + +The "var" field of a widget tells what variable data gives the widget its value. Valid var names +are given by the `widget_var` enum defined in `widget.h`. In the widget's `show` method, these var +enums determine which avatar method(s) to get their values from. + +Below are a few examples of vars and what they mean. See the `widget_var` list in `widget.h` for the +definitive list of vars. + +- `bp_hp`: hit points of given "bodypart", like "arm_l" or "torso", scale of 0-max HP +- `bp_encumb`: encumbrance given "bodypart", scale of 0-?? +- `bp_warmth`: warmth of given "bodypart", scale of 0-10000 +- `stat_str`, `stat_dex`, `stat_int`, `stat_per`: base character stat values +- `stamina`: 0-10000, greater is fuller stamina reserves +- `fatigue`: 0-1000, greater is more fatigued/tired +- `move`, `pain`, `speed`, `mana`: other numeric avatar attributes + +For example, a widget to show the current STR stat would define this "var": + +```json +{ + "var": "stat_str" +} +``` + +And a widget to show the HP of the right arm would define "var" and "bodypart" like so: + +```json +{ + "var": "bp_hp", + "bodypart": "arm_r" +} +``` + +Plain numeric values can be displayed as-is, up to any maximum. For "graph" widgets, it is useful to +define a "var_max" as a cutoff point; see the "Graph widget" section for more. + + + +## Number widget + +The simplest and usually most compact widget for displaying a value, "style": "number" appears as a +label with an integer number. + +```json +{ + "style": "number", + "label": "Focus" +} +``` + +Result: + +``` +Focus: 100 +``` + +The numeric value comes from the given "var", displayed as a decimal integer. + + +## Graph widget + +The graph shows an arrangement of symbols. It has two important parameters: + +- `width`: how many characters wide is the graph +- `symbols`: single-character strings to map to 0-N + +Given a graph of width 3 with two symbols, "-" and "=": + +```json +{ + "width": 3, + "symbols": "-=" +} +``` + +Each symbol is mapped to a numeric value, starting with 0, so a graph can be represented numerically +by replacing each symbol with its numerical index: + +``` +0: - +1: = + +--- 000 +=-- 100 +==- 110 +=== 111 +``` + +With three symbols, "-", "=", and "#": + + +```json +{ + "width": 3, + "symbols": "-=#" +} +``` + +The numeric values range from 0 to 2: + +``` +0: - +1: = +2: # + +--- 000 +=-- 100 +==- 110 +=== 111 +#== 211 +##= 221 +### 222 +``` + +The simplest possible graph is one character wide, with one symbol. It always shows the same value, +so is not very useful: + +```json +{ + "width": 1, + "symbols": "X" +} +``` + +The simplest *useful* graph is one character wide, with two symbols: + +```json +{ + "width": 1, + "symbols": "XO" +} +``` + +When using more than two sybols, different ways of filling up the graph become possible. This is +specified with the "fill" field. + + +### fill + +With "bucket" fill, positions are filled like a row of buckets, using all symbols in the first +position before beginning to fill the next position. This is like the classic 5-bar HP meter. + +```json +{ + "width": 5, + "symbols": ".\\|", + "fill": "bucket" +} +``` + +Result: + +``` +..... 00000 +\.... 10000 +|.... 20000 +|\... 21000 +||... 22000 +||\.. 22100 +|||.. 22200 +|||\. 22210 +||||. 22220 +||||\ 22221 +||||| 22222 +``` + +Using "pool" fill, positions are filled like a swimming pool, with each symbol filling all positions +before the next symbol appears. + +```json +{ + "width": 5, + "symbols": "-=#", + "fill": "pool" +} +``` + +Result: + +``` +----- 00000 +=---- 10000 +==--- 11000 +===-- 11100 +====- 11110 +===== 11111 +#==== 21111 +##=== 22111 +###== 22211 +####= 22221 +##### 22222 +``` + +The total number of possible graphs is the same in each case, so both have the same resolution. + + +### var_max + +Using "graph" style widgets, usually you should provide a "var_max" value (integer) with the maximum +typical value of "var" that will ever be rendered. + +Some "var" fields such as "stamina", or "hp_bp" (hit points for body part) have a known maximum, but +others like character stats, move speed, or encumbrance have no predefined cap - for these you can +provide an explicit "var_max" that indicates where the top / full point of the graph is. + +This helps the graph widget know whether it needs to show values up to 10000 (like stamina) or only +up to 100 or 200 (like focus). If a var usually varies within a range `[low, high]`, select a +"var_max" greater than `high` to be sure the normal variance is captured in the graph's range. + + +## Layout widget + +Lay out widgets with "style": "layout" widgets, providing a "widgets" list of widget ids or a +"layout" object with row ids mapping to widget id lists. Widgets in the same row will have their +horizontal space split equally if possible. + +The arrangement of widgets is defined by the "arrange" field, which may be "columns" (default) to +array widgets horizontally, or "rows" to arrange them vertically, one widget per row. + +```json +[ + { + "id": "sound_focus_move_layout", + "type": "widget", + "style": "layout", + "arrange": "columns", + "widgets": [ "sound_num", "focus_num", "move_num" ] + }, + { + "id": "stats_layout", + "type": "widget", + "style": "layout", + "arrange": "columns", + "widgets": [ "str_num", "dex_num", "int_num", "per_num" ] + }, + { + "id": "root_layout", + "type": "widget", + "style": "layout", + "arrange": "rows", + "widgets": [ + "sound_focus_move_layout", + "stats_layout" + ] + } +] +``` + +The above might yield: + +``` +Sound: 8 Focus: 105 Move: 120 +Str: 8 Dex: 9 Int: 7 Per: 11 +``` + +### Root layouts + +There are two important "root layout" widgets defined in `data/json/sidebar.json`: + +- Widget id "root_layout_wide" is used for "labels" and "classic" sidebars +- Widget id "root_layout_narrow" is used for "compact" and "labels narrow" sidebars + +Modify or override the root layout widget to define all sub-layouts or child widgets you want to see +in the custom section of your sidebar. + From c12e18892b5f31743e5c2eb359d84caccf8649eb Mon Sep 17 00:00:00 2001 From: Eric Pierce Date: Tue, 10 Aug 2021 16:16:16 -0600 Subject: [PATCH 7/7] Move sidebar.json to new data/json/ui directory --- data/json/{ => ui}/sidebar.json | 0 doc/SIDEBAR_MOD.md | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename data/json/{ => ui}/sidebar.json (100%) diff --git a/data/json/sidebar.json b/data/json/ui/sidebar.json similarity index 100% rename from data/json/sidebar.json rename to data/json/ui/sidebar.json diff --git a/doc/SIDEBAR_MOD.md b/doc/SIDEBAR_MOD.md index f4100bf2f1f48..16f3edca08925 100644 --- a/doc/SIDEBAR_MOD.md +++ b/doc/SIDEBAR_MOD.md @@ -26,7 +26,7 @@ character attributes in numeric form, or as a bar graph of arbitrary width. A wi layout of other widgets. Widget instances are defined by JSON data, with the main game sidebar widgets and layouts being in -`data/json/sidebar.json`. You may customize yours by editing this file, or by loading a mod that +`data/json/ui/sidebar.json`. You may customize yours by editing this file, or by loading a mod that adds or modifies widget definitions. For example, here is a widget to display the player character's "Focus" attribute as a number: @@ -310,7 +310,7 @@ Str: 8 Dex: 9 Int: 7 Per: 11 ### Root layouts -There are two important "root layout" widgets defined in `data/json/sidebar.json`: +There are two important "root layout" widgets defined in `data/json/ui/sidebar.json`: - Widget id "root_layout_wide" is used for "labels" and "classic" sidebars - Widget id "root_layout_narrow" is used for "compact" and "labels narrow" sidebars