From acf0edc3d2fb44a6bb63b53654fbd7533009a846 Mon Sep 17 00:00:00 2001 From: hop311 Date: Sun, 2 Feb 2025 00:11:08 +0000 Subject: [PATCH] Add trade menu control script --- extension/deps/openvic-simulation | 2 +- extension/doc_classes/GUILineChart.xml | 10 + extension/doc_classes/GUINode.xml | 91 +++ extension/doc_classes/MenuSingleton.xml | 24 + .../classes/GUILineChart.cpp | 6 + .../classes/GUILineChart.hpp | 3 +- .../src/openvic-extension/classes/GUINode.cpp | 4 + .../src/openvic-extension/classes/GUINode.hpp | 19 + .../singletons/MenuSingleton.cpp | 10 + .../singletons/MenuSingleton.hpp | 10 + .../singletons/TradeMenu.cpp | 323 ++++++++++ .../NationManagementScreen/TradeMenu.gd | 587 +++++++++++++++++- 12 files changed, 1070 insertions(+), 19 deletions(-) create mode 100644 extension/src/openvic-extension/singletons/TradeMenu.cpp diff --git a/extension/deps/openvic-simulation b/extension/deps/openvic-simulation index 05baea57..91d7b0a9 160000 --- a/extension/deps/openvic-simulation +++ b/extension/deps/openvic-simulation @@ -1 +1 @@ -Subproject commit 05baea57e45e8bfc09869f296330908d2269ddb6 +Subproject commit 91d7b0a9c7319a2d144efcec4d846bb5fdaa4c37 diff --git a/extension/doc_classes/GUILineChart.xml b/extension/doc_classes/GUILineChart.xml index b8e3a34a..26e1eec3 100644 --- a/extension/doc_classes/GUILineChart.xml +++ b/extension/doc_classes/GUILineChart.xml @@ -29,6 +29,16 @@ + + + + + + + + + + diff --git a/extension/doc_classes/GUINode.xml b/extension/doc_classes/GUINode.xml index 87af2f88..2e19e0d8 100644 --- a/extension/doc_classes/GUINode.xml +++ b/extension/doc_classes/GUINode.xml @@ -55,6 +55,13 @@ + + + + + + + @@ -67,6 +74,13 @@ + + + + + + + @@ -79,6 +93,13 @@ + + + + + + + @@ -91,6 +112,13 @@ + + + + + + + @@ -103,6 +131,13 @@ + + + + + + + @@ -115,6 +150,13 @@ + + + + + + + @@ -127,6 +169,13 @@ + + + + + + + @@ -139,6 +188,13 @@ + + + + + + + @@ -151,6 +207,13 @@ + + + + + + + @@ -170,6 +233,13 @@ + + + + + + + @@ -182,6 +252,13 @@ + + + + + + + @@ -194,6 +271,13 @@ + + + + + + + @@ -206,6 +290,13 @@ + + + + + + + diff --git a/extension/doc_classes/MenuSingleton.xml b/extension/doc_classes/MenuSingleton.xml index c4e85637..b96d465d 100644 --- a/extension/doc_classes/MenuSingleton.xml +++ b/extension/doc_classes/MenuSingleton.xml @@ -170,6 +170,22 @@ + + + + + + + + + + + + + + + + @@ -337,6 +353,14 @@ + + + + + + + + diff --git a/extension/src/openvic-extension/classes/GUILineChart.cpp b/extension/src/openvic-extension/classes/GUILineChart.cpp index 31bb6e47..c0b5a020 100644 --- a/extension/src/openvic-extension/classes/GUILineChart.cpp +++ b/extension/src/openvic-extension/classes/GUILineChart.cpp @@ -14,6 +14,9 @@ void GUILineChart::_bind_methods() { OV_BIND_METHOD(GUILineChart::clear); OV_BIND_METHOD(GUILineChart::clear_lines); + OV_BIND_METHOD(GUILineChart::get_min_value); + OV_BIND_METHOD(GUILineChart::get_max_value); + OV_BIND_METHOD(GUILineChart::set_gfx_line_chart_name, { "new_gfx_line_chart_name" }); OV_BIND_METHOD(GUILineChart::get_gfx_line_chart_name); @@ -118,6 +121,9 @@ Error GUILineChart::set_gradient_line(PackedFloat32Array const& line_values, flo } } + min_value = central_value - min_value_range; + max_value = central_value + min_value_range; + if (min_value_range == 0.0f) { min_value_range = 1.0f; } else { diff --git a/extension/src/openvic-extension/classes/GUILineChart.hpp b/extension/src/openvic-extension/classes/GUILineChart.hpp index 697679ae..10163d63 100644 --- a/extension/src/openvic-extension/classes/GUILineChart.hpp +++ b/extension/src/openvic-extension/classes/GUILineChart.hpp @@ -12,7 +12,8 @@ namespace OpenVic { GFX::LineChart const* gfx_line_chart = nullptr; int32_t point_count = 0; - float min_value = 0.0f, max_value = 0.0f; + float PROPERTY(min_value, 0.0f); + float PROPERTY(max_value, 0.0f); protected: static void _bind_methods(); diff --git a/extension/src/openvic-extension/classes/GUINode.cpp b/extension/src/openvic-extension/classes/GUINode.cpp index e1e50fb5..63de6afa 100644 --- a/extension/src/openvic-extension/classes/GUINode.cpp +++ b/extension/src/openvic-extension/classes/GUINode.cpp @@ -67,6 +67,7 @@ void GUINode::_bind_methods() { #define GET_BINDINGS(type, name) \ OV_BIND_SMETHOD(get_##name##_from_node, { "node" }); \ + OV_BIND_SMETHOD(get_##name##_from_node_and_path, { "node", "path" }); \ OV_BIND_METHOD(GUINode::get_##name##_from_nodepath, { "path" }); APPLY_TO_CHILD_TYPES(GET_BINDINGS) @@ -139,6 +140,9 @@ static T* _cast_node(Node* node) { type* GUINode::get_##name##_from_node(Node* node) { \ return _cast_node(node); \ } \ + type* GUINode::get_##name##_from_node_and_path(Node* node, NodePath const& path) { \ + return _cast_node(node->get_node_internal(path)); \ + } \ type* GUINode::get_##name##_from_nodepath(NodePath const& path) const { \ return _cast_node(get_node_internal(path)); \ } diff --git a/extension/src/openvic-extension/classes/GUINode.hpp b/extension/src/openvic-extension/classes/GUINode.hpp index a701f1ef..538749f9 100644 --- a/extension/src/openvic-extension/classes/GUINode.hpp +++ b/extension/src/openvic-extension/classes/GUINode.hpp @@ -66,6 +66,25 @@ namespace OpenVic { static godot::LineEdit* get_line_edit_from_node(godot::Node* node); static GUILineChart* get_gui_line_chart_from_node(godot::Node* node); + // These expect a non-null node! + static GUIIconButton* get_gui_icon_button_from_node_and_path(godot::Node* node, godot::NodePath const& path); + static GUIMaskedFlagButton* get_gui_masked_flag_button_from_node_and_path( + godot::Node* node, godot::NodePath const& path + ); + static GUILabel* get_gui_label_from_node_and_path(godot::Node* node, godot::NodePath const& path); + static godot::Panel* get_panel_from_node_and_path(godot::Node* node, godot::NodePath const& path); + static GUIProgressBar* get_gui_progress_bar_from_node_and_path(godot::Node* node, godot::NodePath const& path); + static GUIIcon* get_gui_icon_from_node_and_path(godot::Node* node, godot::NodePath const& path); + static GUIMaskedFlag* get_gui_masked_flag_from_node_and_path(godot::Node* node, godot::NodePath const& path); + static GUIPieChart* get_gui_pie_chart_from_node_and_path(godot::Node* node, godot::NodePath const& path); + static GUIOverlappingElementsBox* get_gui_overlapping_elements_box_from_node_and_path( + godot::Node* node, godot::NodePath const& path + ); + static GUIScrollbar* get_gui_scrollbar_from_node_and_path(godot::Node* node, godot::NodePath const& path); + static GUIListBox* get_gui_listbox_from_node_and_path(godot::Node* node, godot::NodePath const& path); + static godot::LineEdit* get_line_edit_from_node_and_path(godot::Node* node, godot::NodePath const& path); + static GUILineChart* get_gui_line_chart_from_node_and_path(godot::Node* node, godot::NodePath const& path); + GUIIconButton* get_gui_icon_button_from_nodepath(godot::NodePath const& path) const; GUIMaskedFlagButton* get_gui_masked_flag_button_from_nodepath(godot::NodePath const& path) const; GUILabel* get_gui_label_from_nodepath(godot::NodePath const& path) const; diff --git a/extension/src/openvic-extension/singletons/MenuSingleton.cpp b/extension/src/openvic-extension/singletons/MenuSingleton.cpp index 1d70b9ab..20460979 100644 --- a/extension/src/openvic-extension/singletons/MenuSingleton.cpp +++ b/extension/src/openvic-extension/singletons/MenuSingleton.cpp @@ -404,6 +404,16 @@ void MenuSingleton::_bind_methods() { BIND_ENUM_CONSTANT(SORT_SIZE_CHANGE); BIND_ENUM_CONSTANT(SORT_LITERACY); + /* TRADE MENU */ + OV_BIND_METHOD(MenuSingleton::get_trade_menu_good_categories_info); + OV_BIND_METHOD(MenuSingleton::get_trade_menu_trade_details_info, { "trade_detail_good_index" }); + OV_BIND_METHOD(MenuSingleton::get_trade_menu_tables_info); + + BIND_ENUM_CONSTANT(TRADE_SETTING_NONE); + BIND_ENUM_CONSTANT(TRADE_SETTING_AUTOMATED); + BIND_ENUM_CONSTANT(TRADE_SETTING_BUYING); + BIND_ENUM_CONSTANT(TRADE_SETTING_SELLING); + /* MILITARY MENU */ OV_BIND_METHOD(MenuSingleton::get_military_menu_info, { "leader_sort_key", "sort_leaders_descending", diff --git a/extension/src/openvic-extension/singletons/MenuSingleton.hpp b/extension/src/openvic-extension/singletons/MenuSingleton.hpp index fb5b1d4d..2623b2e8 100644 --- a/extension/src/openvic-extension/singletons/MenuSingleton.hpp +++ b/extension/src/openvic-extension/singletons/MenuSingleton.hpp @@ -96,6 +96,10 @@ namespace OpenVic { std::vector pops, filtered_pops; }; + enum TradeSettingBit { + TRADE_SETTING_NONE = 0, TRADE_SETTING_AUTOMATED = 1, TRADE_SETTING_BUYING = 2, TRADE_SETTING_SELLING = 4 + }; + enum LeaderSortKey { LEADER_SORT_NONE, LEADER_SORT_PRESTIGE, LEADER_SORT_TYPE, LEADER_SORT_NAME, LEADER_SORT_ASSIGNMENT, MAX_LEADER_SORT_KEY @@ -233,6 +237,11 @@ namespace OpenVic { /* Array of GFXPieChartTexture::godot_pie_chart_data_t. */ godot::TypedArray get_population_menu_distribution_info() const; + /* TRADE MENU */ + godot::Dictionary get_trade_menu_good_categories_info() const; + godot::Dictionary get_trade_menu_trade_details_info(int32_t trade_detail_good_index) const; + godot::Dictionary get_trade_menu_tables_info() const; + /* MILITARY MENU */ godot::Dictionary make_leader_dict(LeaderBase const& leader); template @@ -257,5 +266,6 @@ namespace OpenVic { VARIANT_ENUM_CAST(OpenVic::MenuSingleton::ProvinceListEntry); VARIANT_ENUM_CAST(OpenVic::MenuSingleton::PopSortKey); +VARIANT_ENUM_CAST(OpenVic::MenuSingleton::TradeSettingBit); VARIANT_ENUM_CAST(OpenVic::MenuSingleton::LeaderSortKey); VARIANT_ENUM_CAST(OpenVic::MenuSingleton::UnitGroupSortKey); diff --git a/extension/src/openvic-extension/singletons/TradeMenu.cpp b/extension/src/openvic-extension/singletons/TradeMenu.cpp new file mode 100644 index 00000000..8a65d7fb --- /dev/null +++ b/extension/src/openvic-extension/singletons/TradeMenu.cpp @@ -0,0 +1,323 @@ +#include "MenuSingleton.hpp" + +#include + +#include "openvic-extension/classes/GUILabel.hpp" +#include "openvic-extension/singletons/GameSingleton.hpp" +#include "openvic-extension/utility/Utilities.hpp" + +using namespace OpenVic; +using namespace godot; + +/* TRADE MENU */ + +Dictionary MenuSingleton::get_trade_menu_good_categories_info() const { + static const StringName good_index_key = "good_index"; + static const StringName current_price_key = "current_price"; + static const StringName price_change_key = "price_change"; + static const StringName demand_tooltip_key = "demand_tooltip"; + static const StringName trade_settings_key = "trade_settings"; + + GameSingleton const& game_singleton = *GameSingleton::get_singleton(); + InstanceManager const* instance_manager = game_singleton.get_instance_manager(); + ERR_FAIL_NULL_V(instance_manager, {}); + + GoodInstanceManager const& good_instance_manager = instance_manager->get_good_instance_manager(); + GoodDefinitionManager const& good_definition_manager = good_instance_manager.get_good_definition_manager(); + + CountryInstance const* country = game_singleton.get_viewed_country(); + + Dictionary ret; + + for (GoodCategory const& good_category : good_definition_manager.get_good_categories()) { + TypedArray array; + + for (GoodDefinition const* good_definition : good_category.get_good_definitions()) { + GoodInstance const& good_instance = good_instance_manager.get_good_instance_from_definition(*good_definition); + + if (!good_instance.get_is_available() || !good_definition->get_is_tradeable()) { + continue; + } + + Dictionary good_dict; + + good_dict[good_index_key] = static_cast(good_definition->get_index()); + good_dict[current_price_key] = good_instance.get_price().to_float(); + good_dict[price_change_key] = good_instance.get_price_change_yesterday().to_float(); + + { + static const StringName in_demand_localisation_key = "TRADE_IN_DEMAND"; + static const StringName not_in_demand_localisation_key = "TRADE_NOT_IN_DEMAND"; + static const StringName supply_localisation_key = "SUPPLY"; + static const StringName demand_localisation_key = "DEMAND"; + static const StringName actual_bought_localisation_key = "ACTUAL_BOUGHT"; + static const String val_replace_key = "$VAL$"; + + const fixed_point_t supply = good_instance.get_total_supply_yesterday(); + const fixed_point_t demand = good_instance.get_total_demand_yesterday(); + + good_dict[demand_tooltip_key] = tr( + demand > supply ? in_demand_localisation_key : not_in_demand_localisation_key + ) + get_tooltip_separator() + tr(supply_localisation_key).replace( + val_replace_key, Utilities::fixed_point_to_string_dp(supply, 3) + ) + "\n" + tr(demand_localisation_key).replace( + val_replace_key, Utilities::fixed_point_to_string_dp(demand, 3) + ) + "\n" + tr(actual_bought_localisation_key).replace( + val_replace_key, Utilities::fixed_point_to_string_dp(good_instance.get_quantity_traded_yesterday(), 3) + ); + } + + if (country != nullptr) { + CountryInstance::good_data_t const& good_data = country->get_goods_data()[good_instance]; + + // Trade settings: + // - 1 bit: automated (1) or not (0) + // - 2 bit: buying (1) or not (0) + // - 4 bit: selling (1) or not (0) + // The automated bit can be 0 or 1, regardless of the buying and selling bits' values. + // The buying and selling bits can both be 0 or 1 and 0, but never both 1. + + int32_t trade_settings = TRADE_SETTING_NONE; + + if (good_data.is_selling) { + if (good_data.stockpile_amount > good_data.stockpile_cutoff) { + trade_settings = TRADE_SETTING_SELLING; + } + } else { + if (good_data.stockpile_amount < good_data.stockpile_cutoff) { + trade_settings = TRADE_SETTING_BUYING; + } + } + + if (good_data.is_automated) { + trade_settings |= TRADE_SETTING_AUTOMATED; + } + + good_dict[trade_settings_key] = trade_settings; + } + + array.push_back(good_dict); + } + + ret[Utilities::std_to_godot_string(good_category.get_identifier())] = std::move(array); + } + + return ret; +} + +Dictionary MenuSingleton::get_trade_menu_trade_details_info(int32_t trade_detail_good_index) const { + static const StringName trade_detail_good_name_key = "trade_detail_good_name"; + static const StringName trade_detail_good_price_key = "trade_detail_good_price"; + static const StringName trade_detail_good_base_price_key = "trade_detail_good_base_price"; + static const StringName trade_detail_price_history_key = "trade_detail_price_history"; + static const StringName trade_detail_is_automated_key = "trade_detail_is_automated"; + static const StringName trade_detail_is_selling_key = "trade_detail_is_selling"; // or buying (false) + static const StringName trade_detail_slider_value_key = "trade_detail_slider_value"; // linear slider value + static const StringName trade_detail_slider_amount_key = "trade_detail_slider_amount"; // exponential good amount + static const StringName trade_detail_government_needs_key = "trade_detail_government_needs"; + static const StringName trade_detail_army_needs_key = "trade_detail_army_needs"; + static const StringName trade_detail_navy_needs_key = "trade_detail_navy_needs"; + static const StringName trade_detail_production_needs_key = "trade_detail_production_needs"; + static const StringName trade_detail_overseas_needs_key = "trade_detail_overseas_needs"; + static const StringName trade_detail_factory_needs_key = "trade_detail_factory_needs"; + static const StringName trade_detail_pop_needs_key = "trade_detail_pop_needs"; + static const StringName trade_detail_available_key = "trade_detail_available"; + + GameSingleton const& game_singleton = *GameSingleton::get_singleton(); + InstanceManager const* instance_manager = game_singleton.get_instance_manager(); + ERR_FAIL_NULL_V(instance_manager, {}); + + GoodInstance const* good_instance = + instance_manager->get_good_instance_manager().get_good_instance_by_index(trade_detail_good_index); + ERR_FAIL_NULL_V(good_instance, {}); + + CountryInstance const* country = game_singleton.get_viewed_country(); + + Dictionary ret; + + ret[trade_detail_good_name_key] = Utilities::std_to_godot_string(good_instance->get_identifier()); + ret[trade_detail_good_price_key] = good_instance->get_price().to_float(); + ret[trade_detail_good_base_price_key] = good_instance->get_good_definition().get_base_price().to_float(); + { + ValueHistory const& good_price_history = good_instance->get_price_history(); + + PackedFloat32Array price_history; + + if (price_history.resize(good_price_history.size()) == OK) { + for (size_t idx = 0; idx < good_price_history.size(); ++idx) { + price_history[idx] = good_price_history[idx].to_float(); + } + + ret[trade_detail_price_history_key] = std::move(price_history); + } else { + UtilityFunctions::push_error( + "Failed to resize price history array to the correct size (", + static_cast(good_price_history.size()), ")" + ); + } + } + + if (unlikely(country == nullptr)) { + return ret; + } + + CountryInstance::good_data_t const& good_data = country->get_goods_data()[*good_instance]; + + ret[trade_detail_is_automated_key] = good_data.is_automated; + ret[trade_detail_is_selling_key] = good_data.is_selling; + // TODO - use exponential formula! + ret[trade_detail_slider_value_key] = (good_data.stockpile_cutoff / 2000).to_int32_t(); + ret[trade_detail_slider_amount_key] = good_data.stockpile_cutoff.to_float(); + ret[trade_detail_government_needs_key] = good_data.government_needs.to_float(); + ret[trade_detail_army_needs_key] = good_data.army_needs.to_float(); + ret[trade_detail_navy_needs_key] = good_data.navy_needs.to_float(); + ret[trade_detail_production_needs_key] = good_data.production_needs.to_float(); + ret[trade_detail_overseas_needs_key] = good_data.overseas_needs.to_float(); + ret[trade_detail_factory_needs_key] = good_data.factory_needs.to_float(); + ret[trade_detail_pop_needs_key] = good_data.pop_needs.to_float(); + ret[trade_detail_available_key] = good_data.available_amount.to_float(); + + return ret; +} + +Dictionary MenuSingleton::get_trade_menu_tables_info() const { + static const StringName good_producers_tooltips_key = "good_producers_tooltips"; + static const StringName good_trading_yesterday_tooltips_key = "good_trading_yesterday_tooltips"; + static const StringName government_needs_key = "government_needs"; + static const StringName factory_needs_key = "factory_needs"; + static const StringName pop_needs_key = "pop_needs"; + static const StringName market_activity_key = "market_activity"; + static const StringName stockpile_key = "stockpile"; + static const StringName common_market_key = "common_market"; + + GameSingleton const& game_singleton = *GameSingleton::get_singleton(); + InstanceManager const* instance_manager = game_singleton.get_instance_manager(); + ERR_FAIL_NULL_V(instance_manager, {}); + GoodInstanceManager const& good_instance_manager = instance_manager->get_good_instance_manager(); + + CountryInstance const* country = game_singleton.get_viewed_country(); + + Dictionary ret; + + // This needs an entry for every good, even untradeable and unavailable ones, so we can look entries up with good indices + PackedStringArray good_producers_tooltips; + // TODO - replace test code with actual top producers + CountryInstanceManager const& country_instance_manager = instance_manager->get_country_instance_manager(); + for (GoodInstance const& good : good_instance_manager.get_good_instances()) { + static const StringName top_producers_localisation_key = "TRADE_TOP_PRODUCERS"; + + String tooltip = tr(Utilities::std_to_godot_string(good.get_identifier())) + get_tooltip_separator() + + tr(top_producers_localisation_key); + + for (size_t index = 0; index < 5; ++index) { + CountryInstance const* country = country_instance_manager.get_country_instance_by_index(index + 1); + + ERR_CONTINUE(country == nullptr); + + static const String top_producer_template_string = "\n" + GUILabel::get_flag_marker() + "%s %s: %s"; + + tooltip += vformat( + top_producer_template_string, + Utilities::std_to_godot_string(country->get_identifier()), + _get_country_name(*country), + Utilities::fixed_point_to_string_dp(fixed_point_t::parse(1000) / static_cast(index + 1), 2) + ); + } + + good_producers_tooltips.push_back(tooltip); + } + ret[good_producers_tooltips_key] = std::move(good_producers_tooltips); + + if (unlikely(country == nullptr)) { + return ret; + } + + PackedStringArray good_trading_yesterday_tooltips; + PackedVector2Array government_needs; + PackedVector2Array factory_needs; + PackedVector2Array pop_needs; + PackedVector3Array market_activity; + PackedVector3Array stockpile; + PackedVector4Array common_market; + + for (auto const& [good, good_data] : country->get_goods_data()) { + if (!good.get_is_available() || !good.get_good_definition().get_is_tradeable()) { + continue; + } + + static const StringName stockpile_bought_localisation_key = "TRADE_STOCKPILE_BUY"; + static const StringName stockpile_sold_localisation_key = "TRADE_STOCKPILE_SOLD"; + static const String money_replace_key = "$MONEY$"; + static const String units_replace_key = "$UNITS$"; + + fixed_point_t stockpile_change_yesterday = good_data.stockpile_change_yesterday; + String tooltip; + + if (stockpile_change_yesterday > 0) { + tooltip = tr(stockpile_bought_localisation_key); + } else { + tooltip = tr(stockpile_sold_localisation_key); + stockpile_change_yesterday = -stockpile_change_yesterday; + } + + good_trading_yesterday_tooltips.push_back( + tooltip.replace( + money_replace_key, Utilities::fixed_point_to_string_dp(stockpile_change_yesterday, 2) + ).replace( + units_replace_key, Utilities::fixed_point_to_string_dp(stockpile_change_yesterday * good.get_price(), 2) + ) + ); + + const float good_index = good.get_good_definition().get_index(); + + if (good_data.government_needs != fixed_point_t::_0()) { + government_needs.push_back({ + good_index, + good_data.government_needs.to_float() + }); + } + + if (good_data.factory_needs != fixed_point_t::_0()) { + factory_needs.push_back({ + good_index, + good_data.factory_needs.to_float() + }); + } + + if (good_data.pop_needs != fixed_point_t::_0()) { + pop_needs.push_back({ + good_index, good_data.pop_needs.to_float() + }); + } + + market_activity.push_back({ + good_index, + good_data.exported_amount.abs().to_float(), + (good_data.exported_amount * good.get_price()).to_float() + }); + + stockpile.push_back({ + good_index, + good_data.stockpile_amount.to_float(), + good_data.stockpile_change_yesterday.to_float() + }); + + // TODO - replace with actual common market data + common_market.push_back({ + good_index, + good_index * 100, + -good_index, + good_index * 10 + }); + } + + ret[good_trading_yesterday_tooltips_key] = std::move(good_trading_yesterday_tooltips); + ret[government_needs_key] = std::move(government_needs); + ret[factory_needs_key] = std::move(factory_needs); + ret[pop_needs_key] = std::move(pop_needs); + ret[market_activity_key] = std::move(market_activity); + ret[stockpile_key] = std::move(stockpile); + ret[common_market_key] = std::move(common_market); + + return ret; +} diff --git a/game/src/Game/GameSession/NationManagementScreen/TradeMenu.gd b/game/src/Game/GameSession/NationManagementScreen/TradeMenu.gd index 34931ba4..a5aa33cc 100644 --- a/game/src/Game/GameSession/NationManagementScreen/TradeMenu.gd +++ b/game/src/Game/GameSession/NationManagementScreen/TradeMenu.gd @@ -4,12 +4,72 @@ var _active : bool = false const _screen : NationManagement.Screen = NationManagement.Screen.TRADE +const _gui_file : String = "country_trade" + +var _trade_detail_good_index : int = 0 + +# Trade details +var _trade_detail_good_icon : GUIIcon +var _trade_detail_good_name_label : GUILabel +var _trade_detail_good_price_label : GUILabel +var _trade_detail_good_price_line_chart : GUILineChart +var _trade_detail_good_price_low_label : GUILabel +var _trade_detail_good_price_high_label : GUILabel +var _trade_detail_good_chart_time_label : GUILabel +var _trade_detail_automate_checkbox : GUIIconButton +var _trade_detail_buy_sell_stockpile_checkbox : GUIIconButton +var _trade_detail_buy_sell_stockpile_label : GUILabel +var _trade_detail_stockpile_slider_description_label : GUILabel +var _trade_detail_stockpile_slider_scrollbar : GUIScrollbar +var _trade_detail_stockpile_slider_amount_label : GUILabel +var _trade_detail_confirm_trade_button : GUIIconButton +var _trade_detail_government_good_needs_label : GUILabel +var _trade_detail_factory_good_needs_label : GUILabel +var _trade_detail_pop_good_needs_label : GUILabel +var _trade_detail_good_available_label : GUILabel + +# Goods tables +enum Table { + GOVERNMENT_NEEDS, + FACTORY_NEEDS, + POP_NEEDS, + MARKET_ACTIVITY, + STOCKPILE, + COMMON_MARKET +} +const TABLE_NAMES : PackedStringArray = [ + "government_needs", "factory_needs", "pop_needs", "market_activity", "stockpile", "common_market" +] +const TABLE_ENTRY_NAMES : PackedStringArray = [ + "goods_needs_entry", "goods_needs_entry", "goods_needs_entry", "market_activity_entry", "stockpile_entry", "common_market_entry" +] +# Nested Array contains only NodePaths +const TABLE_ITEM_PATHS : Array[Array] = [ + [^"./goods_type", ^"./value"], [^"./goods_type", ^"./value"], [^"./goods_type", ^"./value"], + [^"./goods_type", ^"./activity", ^"./cost"], + [^"./goods_type", ^"./value", ^"./change"], + [^"./goods_type", ^"./total", ^"./produce_change", ^"./exported"] +] + +var _table_listboxes : Array[GUIListBox] + +const TABLE_UNSORTED : int = 255 +const TABLE_COLUMN_KEYS : Array[StringName] = [&"COLUMN_0", &"COLUMN_1", &"COLUMN_2", &"COLUMN_3"] +var _table_sort_columns : PackedByteArray + +const SORT_DESCENDING : int = 0 +const SORT_ASCENDING : int = 1 +var _table_sort_directions : PackedByteArray + +# Good entries +var _goods_entry_offset : Vector2 + func _ready() -> void: GameSingleton.gamestate_updated.connect(_update_info) Events.NationManagementScreens.update_active_nation_management_screen.connect(_on_update_active_nation_management_screen) - add_gui_element("country_trade", "country_trade") + add_gui_element(_gui_file, "country_trade") set_click_mask_from_nodepaths([^"./country_trade/main_bg"]) @@ -17,21 +77,141 @@ func _ready() -> void: if close_button: close_button.pressed.connect(Events.NationManagementScreens.close_nation_management_screen.bind(_screen)) - var good_price_line_chart : GUILineChart = get_gui_line_chart_from_nodepath(^"./country_trade/trade_details/price_linechart") - - if good_price_line_chart: - # TEST COLOURED LINES - var colours : PackedColorArray = [ - Color.RED, Color.GREEN, Color.BLUE, Color.MAGENTA, - Color.CYAN, Color.ORANGE, Color.CRIMSON, Color.FOREST_GREEN - ] - for n : int in colours.size(): - const point_count : int = 36 - var values : PackedFloat32Array - for x : int in point_count: - values.push_back(1000 * sin((float(x) / (point_count - 1) + float(n) / (colours.size() * 2)) * 4 * PI) + 4000) - good_price_line_chart.add_coloured_line(values, colours[n]) - good_price_line_chart.scale_coloured_lines() + # Goods tables + _table_listboxes.push_back(get_gui_listbox_from_nodepath(^"./country_trade/government_needs_list")) + _table_sort_columns.push_back(TABLE_UNSORTED) + _table_sort_directions.push_back(SORT_DESCENDING) + var sort_government_needs_by_good_button : GUIIconButton = get_gui_icon_button_from_nodepath(^"./country_trade/needs_government_sort_by_goods") + if sort_government_needs_by_good_button: + sort_government_needs_by_good_button.pressed.connect(_change_table_sorting.bind(Table.GOVERNMENT_NEEDS, 0)) + var sort_government_needs_by_need_button : GUIIconButton = get_gui_icon_button_from_nodepath(^"./country_trade/needs_government_sort_by_value") + if sort_government_needs_by_need_button: + sort_government_needs_by_need_button.pressed.connect(_change_table_sorting.bind(Table.GOVERNMENT_NEEDS, 1)) + + _table_listboxes.push_back(get_gui_listbox_from_nodepath(^"./country_trade/factory_needs_list")) + _table_sort_columns.push_back(TABLE_UNSORTED) + _table_sort_directions.push_back(SORT_DESCENDING) + var sort_factory_needs_by_good_button : GUIIconButton = get_gui_icon_button_from_nodepath(^"./country_trade/needs_factories_sort_by_goods") + if sort_factory_needs_by_good_button: + sort_factory_needs_by_good_button.pressed.connect(_change_table_sorting.bind(Table.FACTORY_NEEDS, 0)) + var sort_factory_needs_by_need_button : GUIIconButton = get_gui_icon_button_from_nodepath(^"./country_trade/needs_factories_sort_by_value") + if sort_factory_needs_by_need_button: + sort_factory_needs_by_need_button.pressed.connect(_change_table_sorting.bind(Table.FACTORY_NEEDS, 1)) + + _table_listboxes.push_back(get_gui_listbox_from_nodepath(^"./country_trade/pop_needs_list")) + _table_sort_columns.push_back(TABLE_UNSORTED) + _table_sort_directions.push_back(SORT_DESCENDING) + var sort_pop_needs_by_good_button : GUIIconButton = get_gui_icon_button_from_nodepath(^"./country_trade/needs_pops_sort_by_goods") + if sort_pop_needs_by_good_button: + sort_pop_needs_by_good_button.pressed.connect(_change_table_sorting.bind(Table.POP_NEEDS, 0)) + var sort_pop_needs_by_need_button : GUIIconButton = get_gui_icon_button_from_nodepath(^"./country_trade/needs_pops_sort_by_value") + if sort_pop_needs_by_need_button: + sort_pop_needs_by_need_button.pressed.connect(_change_table_sorting.bind(Table.POP_NEEDS, 1)) + + _table_listboxes.push_back(get_gui_listbox_from_nodepath(^"./country_trade/market_activity_list")) + _table_sort_columns.push_back(TABLE_UNSORTED) + _table_sort_directions.push_back(SORT_DESCENDING) + var sort_market_activity_by_good_button : GUIIconButton = get_gui_icon_button_from_nodepath(^"./country_trade/market_activity_sort_by_goods") + if sort_market_activity_by_good_button: + sort_market_activity_by_good_button.pressed.connect(_change_table_sorting.bind(Table.MARKET_ACTIVITY, 0)) + var sort_market_activity_by_activity_button : GUIIconButton = get_gui_icon_button_from_nodepath(^"./country_trade/market_activity_sort_by_activity") + if sort_market_activity_by_activity_button: + sort_market_activity_by_activity_button.pressed.connect(_change_table_sorting.bind(Table.MARKET_ACTIVITY, 1)) + var sort_market_activity_by_cost_button : GUIIconButton = get_gui_icon_button_from_nodepath(^"./country_trade/market_activity_sort_by_cost") + if sort_market_activity_by_cost_button: + sort_market_activity_by_cost_button.pressed.connect(_change_table_sorting.bind(Table.MARKET_ACTIVITY, 2)) + + _table_listboxes.push_back(get_gui_listbox_from_nodepath(^"./country_trade/stockpile_list")) + _table_sort_columns.push_back(TABLE_UNSORTED) + _table_sort_directions.push_back(SORT_DESCENDING) + var sort_stockpile_by_good_button : GUIIconButton = get_gui_icon_button_from_nodepath(^"./country_trade/stockpile_sort_by_goods") + if sort_stockpile_by_good_button: + sort_stockpile_by_good_button.pressed.connect(_change_table_sorting.bind(Table.STOCKPILE, 0)) + var sort_stockpile_by_stock_button : GUIIconButton = get_gui_icon_button_from_nodepath(^"./country_trade/stockpile_sort_by_value") + if sort_stockpile_by_stock_button: + sort_stockpile_by_stock_button.pressed.connect(_change_table_sorting.bind(Table.STOCKPILE, 1)) + var sort_stockpile_by_increase_button : GUIIconButton = get_gui_icon_button_from_nodepath(^"./country_trade/stockpile_sort_by_change") + if sort_stockpile_by_increase_button: + sort_stockpile_by_increase_button.pressed.connect(_change_table_sorting.bind(Table.STOCKPILE, 2)) + + _table_listboxes.push_back(get_gui_listbox_from_nodepath(^"./country_trade/common_market_list")) + _table_sort_columns.push_back(TABLE_UNSORTED) + _table_sort_directions.push_back(SORT_DESCENDING) + var sort_common_market_by_good_button : GUIIconButton = get_gui_icon_button_from_nodepath(^"./country_trade/common_market_sort_by_goods") + if sort_common_market_by_good_button: + sort_common_market_by_good_button.pressed.connect(_change_table_sorting.bind(Table.COMMON_MARKET, 0)) + var sort_common_market_by_available_button : GUIIconButton = get_gui_icon_button_from_nodepath(^"./country_trade/common_market_sort_by_produced") + if sort_common_market_by_available_button: + sort_common_market_by_available_button.pressed.connect(_change_table_sorting.bind(Table.COMMON_MARKET, 1)) + var sort_common_market_by_increase_button : GUIIconButton = get_gui_icon_button_from_nodepath(^"./country_trade/common_market_sort_by_diff") + if sort_common_market_by_increase_button: + sort_common_market_by_increase_button.pressed.connect(_change_table_sorting.bind(Table.COMMON_MARKET, 2)) + var sort_common_market_by_exported_button : GUIIconButton = get_gui_icon_button_from_nodepath(^"./country_trade/common_market_sort_by_exported") + if sort_common_market_by_exported_button: + sort_common_market_by_exported_button.pressed.connect(_change_table_sorting.bind(Table.COMMON_MARKET, 3)) + + # Trade details + _trade_detail_good_icon = get_gui_icon_from_nodepath(^"./country_trade/trade_details/goods_icon") + _trade_detail_good_name_label = get_gui_label_from_nodepath(^"./country_trade/trade_details/goods_title") + _trade_detail_good_price_label = get_gui_label_from_nodepath(^"./country_trade/trade_details/goods_price") + if _trade_detail_good_price_label: + _trade_detail_good_price_label.set_auto_translate(false) + _trade_detail_good_price_line_chart = get_gui_line_chart_from_nodepath(^"./country_trade/trade_details/price_linechart") + _trade_detail_good_price_low_label = get_gui_label_from_nodepath(^"./country_trade/trade_details/price_chart_low") + if _trade_detail_good_price_low_label: + _trade_detail_good_price_low_label.set_auto_translate(false) + _trade_detail_good_price_high_label = get_gui_label_from_nodepath(^"./country_trade/trade_details/price_chart_high") + if _trade_detail_good_price_high_label: + _trade_detail_good_price_high_label.set_auto_translate(false) + _trade_detail_good_chart_time_label = get_gui_label_from_nodepath(^"./country_trade/trade_details/price_chart_time") + if _trade_detail_good_chart_time_label: + _trade_detail_good_chart_time_label.set_text("PRICE_HISTORY_TIME_RANGE") + var trade_detail_automate_label : GUILabel = get_gui_label_from_nodepath(^"./country_trade/trade_details/automate_label") + if trade_detail_automate_label: + trade_detail_automate_label.set_tooltip_string("AUTOMATE_TRADE_CHECK") + _trade_detail_automate_checkbox = get_gui_icon_button_from_nodepath(^"./country_trade/trade_details/automate") + if _trade_detail_automate_checkbox: + _trade_detail_automate_checkbox.set_tooltip_string("AUTOMATE_TRADE_CHECK") + _trade_detail_buy_sell_stockpile_checkbox = get_gui_icon_button_from_nodepath(^"./country_trade/trade_details/sell_stockpile") + _trade_detail_buy_sell_stockpile_label = get_gui_label_from_nodepath(^"./country_trade/trade_details/sell_stockpile_label") + _trade_detail_stockpile_slider_description_label = get_gui_label_from_nodepath(^"./country_trade/trade_details/sell_slidier_desc") + _trade_detail_stockpile_slider_scrollbar = get_gui_scrollbar_from_nodepath(^"./country_trade/trade_details/sell_slider") + _trade_detail_stockpile_slider_amount_label = get_gui_label_from_nodepath(^"./country_trade/trade_details/slider_value") + if _trade_detail_stockpile_slider_amount_label: + _trade_detail_stockpile_slider_amount_label.set_auto_translate(false) + _trade_detail_confirm_trade_button = get_gui_icon_button_from_nodepath(^"./country_trade/trade_details/confirm_trade") + if _trade_detail_confirm_trade_button: + _trade_detail_confirm_trade_button.pressed.connect( + func() -> void: + # TODO - implement button functionality + print("Confirm trade!") + ) + var good_details_button : GUIIconButton = get_gui_icon_button_from_nodepath(^"./country_trade/trade_details/goods_details") + if good_details_button: + good_details_button.pressed.connect( + func() -> void: + # TODO - open trade details menu + print("Open details menu for good index ", _trade_detail_good_index) + ) + + _trade_detail_government_good_needs_label = get_gui_label_from_nodepath(^"./country_trade/trade_details/goods_need_gov_desc") + if _trade_detail_government_good_needs_label: + _trade_detail_government_good_needs_label.set_text("GOV_NEED_DETAIL") + _trade_detail_factory_good_needs_label = get_gui_label_from_nodepath(^"./country_trade/trade_details/goods_need_factory_desc") + if _trade_detail_factory_good_needs_label: + _trade_detail_factory_good_needs_label.set_text("FACTORY_NEED_DETAIL") + _trade_detail_pop_good_needs_label = get_gui_label_from_nodepath(^"./country_trade/trade_details/produced_detail_desc") # names switched in GUI file + if _trade_detail_pop_good_needs_label: + _trade_detail_pop_good_needs_label.set_text("POP_NEED_DETAIL") + _trade_detail_good_available_label = get_gui_label_from_nodepath(^"./country_trade/trade_details/goods_need_pop_desc") # names switched in GUI file + if _trade_detail_good_available_label: + _trade_detail_good_available_label.set_text("PRODUCED_DETAIL_REMOVE") + + # Good entries + _goods_entry_offset = get_gui_position(_gui_file, "goods_entry_offset") + if _goods_entry_offset.x == 0.0: + push_error("TradeMenu: Goods entry x offset is zero, setting to 1 to avoid divide by 0 errors") + _goods_entry_offset.x = 1.0 _update_info() @@ -46,7 +226,380 @@ func _on_update_active_nation_management_screen(active_screen : NationManagement func _update_info() -> void: if _active: - # TODO - update UI state + + _update_trade_details() + + const good_producers_tooltips_key : StringName = &"good_producers_tooltips" + const good_trading_yesterday_tooltips_key : StringName = &"good_trading_yesterday_tooltips" + const government_needs_key : StringName = &"government_needs" + const factory_needs_key : StringName = &"factory_needs" + const pop_needs_key : StringName = &"pop_needs" + const market_activity_key : StringName = &"market_activity" + const stockpile_key : StringName = &"stockpile" + const common_market_key : StringName = &"common_market" + + var trade_info : Dictionary = MenuSingleton.get_trade_menu_tables_info() + + var good_producers_tooltips : PackedStringArray = trade_info.get(good_producers_tooltips_key, [] as PackedStringArray) + + _generate_listbox(Table.GOVERNMENT_NEEDS, trade_info.get(government_needs_key, [] as PackedVector2Array), good_producers_tooltips) + _generate_listbox(Table.FACTORY_NEEDS, trade_info.get(factory_needs_key, [] as PackedVector2Array), good_producers_tooltips) + _generate_listbox(Table.POP_NEEDS, trade_info.get(pop_needs_key, [] as PackedVector2Array), good_producers_tooltips) + _generate_listbox(Table.MARKET_ACTIVITY, trade_info.get(market_activity_key, [] as PackedVector3Array), good_producers_tooltips) + _generate_listbox(Table.STOCKPILE, trade_info.get(stockpile_key, [] as PackedVector3Array), trade_info.get(good_trading_yesterday_tooltips_key, [] as PackedStringArray)) + _generate_listbox(Table.COMMON_MARKET, trade_info.get(common_market_key, [] as PackedVector4Array), good_producers_tooltips) + + _generate_good_entries(good_producers_tooltips) + show() else: hide() + +func _update_trade_details(new_trade_detail_good_index : int = -1) -> void: + # If the desired good is already selected, do nothing (current index will never be negative, so -1 forces a refresh) + if _trade_detail_good_index == new_trade_detail_good_index: + return + + # If the new index isn't negative, update the current index to match it (newly selected good) + if new_trade_detail_good_index >= 0: + _trade_detail_good_index = new_trade_detail_good_index + + # Trade details + const trade_detail_good_name_key : StringName = &"trade_detail_good_name" + const trade_detail_good_price_key : StringName = &"trade_detail_good_price" + const trade_detail_good_base_price_key : StringName = &"trade_detail_good_base_price" + const trade_detail_price_history_key : StringName = &"trade_detail_price_history" + const trade_detail_is_automated_key : StringName = &"trade_detail_is_automated" + const trade_detail_is_selling_key : StringName = &"trade_detail_is_selling" # or buying (false) + const trade_detail_slider_value_key : StringName = &"trade_detail_slider_value" # linear slider value + const trade_detail_slider_amount_key : StringName = &"trade_detail_slider_amount" # exponential good amount + const trade_detail_government_needs_key : StringName = &"trade_detail_government_needs" + const trade_detail_army_needs_key : StringName = &"trade_detail_army_needs" + const trade_detail_navy_needs_key : StringName = &"trade_detail_navy_needs" + const trade_detail_production_needs_key : StringName = &"trade_detail_production_needs" + const trade_detail_overseas_needs_key : StringName = &"trade_detail_overseas_needs" + const trade_detail_factory_needs_key : StringName = &"trade_detail_factory_needs" + const trade_detail_pop_needs_key : StringName = &"trade_detail_pop_needs" + const trade_detail_available_key : StringName = &"trade_detail_available" + + var trade_info : Dictionary = MenuSingleton.get_trade_menu_trade_details_info(_trade_detail_good_index) + + var trade_detail_good_name : String = trade_info.get(trade_detail_good_name_key, "") + + if _trade_detail_good_icon: + _trade_detail_good_icon.set_icon_index(_trade_detail_good_index + 2) + _trade_detail_good_icon.set_tooltip_string(trade_detail_good_name) + + if _trade_detail_good_name_label: + _trade_detail_good_name_label.set_text(trade_detail_good_name) + + if _trade_detail_good_price_label: + _trade_detail_good_price_label.set_text(GUINode.float_to_string_dp(trade_info.get(trade_detail_good_price_key, 0), 3) + "¤") + + var price_history : PackedFloat32Array = trade_info.get(trade_detail_price_history_key, [] as PackedFloat32Array) + if price_history.size() == 1: + # We cannot draw a line with just one point + push_error("TradeMenu: Price history has only one point: ", price_history) + price_history.clear() + var base_price : float = trade_info.get(trade_detail_good_base_price_key, 0) + var price_low : float = base_price + var price_high : float = base_price + + if _trade_detail_good_price_line_chart: + if price_history.is_empty(): + _trade_detail_good_price_line_chart.clear_lines() + else: + _trade_detail_good_price_line_chart.set_gradient_line(price_history, base_price, 1.0) + price_low = _trade_detail_good_price_line_chart.get_min_value() + price_high = _trade_detail_good_price_line_chart.get_max_value() + + if _trade_detail_good_price_low_label: + _trade_detail_good_price_low_label.set_text(GUINode.float_to_string_dp(price_low, 1) + "¤") + + if _trade_detail_good_price_high_label: + _trade_detail_good_price_high_label.set_text(GUINode.float_to_string_dp(price_high, 1) + "¤") + + if _trade_detail_good_chart_time_label: + _trade_detail_good_chart_time_label.add_substitution("MONTHS", str(price_history.size())) + + var is_automated : bool = trade_info.get(trade_detail_is_automated_key, false) + var is_selling : bool = trade_info.get(trade_detail_is_selling_key, false) + + if _trade_detail_automate_checkbox: + # Investigate whether set_pressed_no_signal can/should be used here + _trade_detail_automate_checkbox.set_pressed(is_automated) + + if _trade_detail_buy_sell_stockpile_checkbox: + # Investigate whether set_pressed_no_signal can/should be used here + _trade_detail_buy_sell_stockpile_checkbox.set_pressed(is_selling) + + if _trade_detail_buy_sell_stockpile_label: + _trade_detail_buy_sell_stockpile_label.set_text("SELL" if is_selling else "BUY") + + if _trade_detail_stockpile_slider_description_label: + _trade_detail_stockpile_slider_description_label.set_text("MINIMUM_STOCKPILE_TARGET" if is_selling else "MAXIMUM_STOCKPILE_TARGET") + + if _trade_detail_stockpile_slider_scrollbar: + _trade_detail_stockpile_slider_scrollbar.set_value(trade_info.get(trade_detail_slider_value_key, 0), false) + + if _trade_detail_stockpile_slider_amount_label: + var slider_amount : float = trade_info.get(trade_detail_slider_amount_key, 0) + _trade_detail_stockpile_slider_amount_label.set_text(GUINode.float_to_string_dp(slider_amount, 3 if slider_amount < 10.0 else 2)) + + if _trade_detail_confirm_trade_button: + _trade_detail_confirm_trade_button.set_disabled(is_automated) + _trade_detail_confirm_trade_button.set_tooltip_string("TRADE_DISABLED_AUTOMATE" if is_automated else "TRADE_CONFIRM_DESC") + + if _trade_detail_government_good_needs_label: + _trade_detail_government_good_needs_label.add_substitution("VAL", GUINode.float_to_string_dp(trade_info.get(trade_detail_government_needs_key, 0), 2)) + var government_needs_tooltip : String + var army_needs : float = trade_info.get(trade_detail_army_needs_key, 0) + if army_needs > 0: + government_needs_tooltip = tr(&"TRADE_SUPPLY_NEED_A").replace("$VAL$", GUINode.float_to_string_dp(army_needs, 2)) + var navy_needs : float = trade_info.get(trade_detail_navy_needs_key, 0) + if navy_needs > 0: + government_needs_tooltip += tr(&"TRADE_SUPPLY_NEED_N").replace("$VAL$", GUINode.float_to_string_dp(navy_needs, 2)) + var production_needs : float = trade_info.get(trade_detail_production_needs_key, 0) + if production_needs > 0: + government_needs_tooltip += tr(&"TRADE_TEMP_PROD_NEED").replace("$VAL$", GUINode.float_to_string_dp(production_needs, 2)) + var overseas_needs : float = trade_info.get(trade_detail_overseas_needs_key, 0) + if overseas_needs > 0: + government_needs_tooltip += tr(&"TRADE_OVERSEAS_NEED").replace("$VAL$", GUINode.float_to_string_dp(overseas_needs, 2)) + if not government_needs_tooltip.is_empty(): + government_needs_tooltip = government_needs_tooltip.trim_suffix("\n") + _trade_detail_government_good_needs_label.set_tooltip_string(government_needs_tooltip) + + if _trade_detail_factory_good_needs_label: + _trade_detail_factory_good_needs_label.add_substitution("VAL", GUINode.float_to_string_dp(trade_info.get(trade_detail_factory_needs_key, 0), 2)) + + if _trade_detail_pop_good_needs_label: + _trade_detail_pop_good_needs_label.add_substitution("VAL", GUINode.float_to_string_dp(trade_info.get(trade_detail_pop_needs_key, 0), 2)) + + if _trade_detail_good_available_label: + _trade_detail_good_available_label.add_substitution("VAL", GUINode.float_to_string_dp(trade_info.get(trade_detail_available_key, 0), 2)) + +func _change_table_sorting(table : Table, column : int) -> void: + if _table_sort_columns[table] != column: + _table_sort_columns[table] = column + _table_sort_directions[table] = SORT_DESCENDING + else: + _table_sort_directions[table] = SORT_ASCENDING if _table_sort_directions[table] == SORT_DESCENDING else SORT_DESCENDING + + print("Sorting table ", TABLE_NAMES[table], " by column ", column, " ", "ascending" if _table_sort_directions[table] == SORT_ASCENDING else "descending") + + _sort_table(table) + +func _sort_table(table : Table) -> void: + var column : int = _table_sort_columns[table] + if column == TABLE_UNSORTED: + return + + var listbox : GUIListBox = _table_listboxes[table] + var sort_key : StringName = TABLE_COLUMN_KEYS[column] + var descending : bool = _table_sort_directions[table] == SORT_DESCENDING + + var items : Array[Node] = listbox.get_children() + + for child : Node in items: + listbox.remove_child(child) + + items.sort_custom( + (func(a : Node, b : Node) -> bool: return a.get_meta(sort_key) > b.get_meta(sort_key)) + if descending else + (func(a : Node, b : Node) -> bool: return a.get_meta(sort_key) < b.get_meta(sort_key)) + ) + + for child : Node in items: + listbox.add_child(child) + +func _float_to_string_suffixed_dp(value : float, decimals : int) -> String: + if value < 1000: + return GUINode.float_to_string_dp(value, decimals) + else: + return GUINode.float_to_string_dp(value / 1000, decimals) + "k" + +func _float_to_string_suffixed_dp_dynamic(value : float) -> String: + if value < 2: + return GUINode.float_to_string_dp(value, 3) + elif value < 100: + return GUINode.float_to_string_dp(value, 2) + else: + return _float_to_string_suffixed_dp(value, 1) + +# data is either a PackedVector2Array, PackedVector3Array or PackedVector4Array +func _generate_listbox(table : Table, data : Variant, good_tooltips : PackedStringArray) -> void: + var listbox : GUIListBox = _table_listboxes[table] + if not listbox: + return + + var entry_name : String = TABLE_ENTRY_NAMES[table] + + listbox.clear_children(data.size()) + while listbox.get_child_count() < data.size(): + var entry : Panel = GUINode.generate_gui_element(_gui_file, entry_name) + if not entry: + break + listbox.add_child(entry) + + var item_paths : Array = TABLE_ITEM_PATHS[table] + + for index in min(listbox.get_child_count(), data.size()): + var entry : Panel = listbox.get_child(index) + if not entry: + break + + # entry_data is either a Vector2, Vector3 or Vector4 + var entry_data : Variant = data[index] + var good_index : int = int(entry_data.x) + var good_tooltip : String = good_tooltips[good_index] if good_index < good_tooltips.size() else "" + + var good_button : GUIIconButton = GUINode.get_gui_icon_button_from_node_and_path(entry, item_paths[0]) + if good_button: + good_button.set_icon_index(good_index + 2) + good_button.set_tooltip_string(good_tooltip) + good_button.pressed.connect( + func() -> void: + _update_trade_details(entry.get_meta(TABLE_COLUMN_KEYS[0])) + ) + entry.set_meta(TABLE_COLUMN_KEYS[0], good_index) + + var set_tooltips : bool = table == Table.STOCKPILE + + var second_column : GUILabel = GUINode.get_gui_label_from_node_and_path(entry, item_paths[1]) + if second_column: + second_column.set_auto_translate(false) + second_column.set_text( + _float_to_string_suffixed_dp_dynamic(entry_data.y) + if table <= Table.POP_NEEDS + else _float_to_string_suffixed_dp(abs(entry_data.y), 1) + if table == Table.MARKET_ACTIVITY + else _float_to_string_suffixed_dp(entry_data.y, 2) + ) + if set_tooltips: + second_column.set_tooltip_string(good_tooltip) + entry.set_meta(TABLE_COLUMN_KEYS[1], entry_data.y) + + if item_paths.size() < 3: + continue + + var third_column : GUILabel = GUINode.get_gui_label_from_node_and_path(entry, item_paths[2]) + if third_column: + third_column.set_auto_translate(false) + third_column.set_text( + _float_to_string_suffixed_dp(entry_data.z, 0) + if table == Table.MARKET_ACTIVITY + else ("+" if entry_data.z >= 0 else "") + GUINode.float_to_string_dp(entry_data.z, 2) + if table == Table.STOCKPILE + else "(%s)" % GUINode.float_to_string_dp(entry_data.z, 0) + ) + if set_tooltips: + third_column.set_tooltip_string(good_tooltip) + entry.set_meta(TABLE_COLUMN_KEYS[2], entry_data.z) + + if item_paths.size() < 4: + continue + + var fourth_column : GUILabel = GUINode.get_gui_label_from_node_and_path(entry, item_paths[3]) + if fourth_column: + fourth_column.set_auto_translate(false) + fourth_column.set_text(_float_to_string_suffixed_dp(entry_data.w, 1)) + if set_tooltips: + fourth_column.set_tooltip_string(good_tooltip) + entry.set_meta(TABLE_COLUMN_KEYS[3], entry_data.w) + + _sort_table(table) + +func _generate_good_entries(good_tooltips : PackedStringArray) -> void: + var good_categories_info : Dictionary = MenuSingleton.get_trade_menu_good_categories_info() + + for good_category : String in good_categories_info: + var good_category_panel : Panel = get_panel_from_nodepath("./country_trade/group_%s" % good_category) + if not good_category_panel: + continue + + # Fall back to 1 if panel width is too small to avoid division by zero + var max_items_per_row : int = max(floor(good_category_panel.get_size().x / _goods_entry_offset.x), 1) + + var good_category_goods : Array[Dictionary] = good_categories_info[good_category] + + var child_count : int = good_category_panel.get_child_count() + + while child_count < good_category_goods.size(): + var good_entry_panel : Panel = generate_gui_element(_gui_file, "goods_entry") + if not good_entry_panel: + break + good_entry_panel.set_position( + _goods_entry_offset * Vector2(child_count % max_items_per_row, child_count / max_items_per_row) + ) + good_category_panel.add_child(good_entry_panel) + child_count += 1 + + while child_count > good_category_goods.size(): + child_count -= 1 + good_category_panel.remove_child(good_category_panel.get_child(child_count)) + + for category_index : int in min(child_count, good_category_goods.size()): + var good_entry_panel : Panel = GUINode.get_panel_from_node(good_category_panel.get_child(category_index)) + if not good_entry_panel: + continue + + const good_index_key : StringName = &"good_index" + const current_price_key : StringName = &"current_price" + const price_change_key : StringName = &"price_change" + const demand_tooltip_key : StringName = &"demand_tooltip" + const trade_settings_key : StringName = &"trade_settings" + + var good_dict : Dictionary = good_category_goods[category_index] + + var good_index : int = good_dict.get(good_index_key, 0) + + var entry_button : GUIIconButton = get_gui_icon_button_from_node_and_path(good_entry_panel, ^"./entry_button") + if entry_button: + # Connecting this way ensures the Callable is always the same, preventing errors when good_index changes, + # while still allowing updates by changing the meta value rather than the Callable + entry_button.pressed.connect( + func() -> void: + _update_trade_details(good_entry_panel.get_meta(TABLE_COLUMN_KEYS[0])) + ) + good_entry_panel.set_meta(TABLE_COLUMN_KEYS[0], good_index) + entry_button.set_tooltip_string(good_dict.get(demand_tooltip_key, "")) + + var good_button : GUIIconButton = get_gui_icon_button_from_node_and_path(good_entry_panel, ^"./goods_type") + if good_button: + good_button.set_icon_index(good_index + 2) + good_button.set_tooltip_string(good_tooltips[good_index] if good_index < good_tooltips.size() else "") + good_button.pressed.connect(func() -> void: print("Good button pressed with index ", good_index)) + + var price_label : GUILabel = get_gui_label_from_node_and_path(good_entry_panel, ^"./price") + if price_label: + # TODO - change colour of text if price is very high or very low!!! + price_label.set_auto_translate(false) + price_label.set_text(GUINode.float_to_string_dp(good_dict.get(current_price_key, 0), 1) + "¤") + + var trend_icon : GUIIcon = get_gui_icon_from_node_and_path(good_entry_panel, ^"./trend_indicator") + if trend_icon: + var price_change : float = good_dict.get(price_change_key, 0) + trend_icon.set_icon_index(1 if price_change > 0 else 3 if price_change < 0 else 2) + trend_icon.set_tooltip_string( + tr(&"TRADE_PRICE_TREND").replace("$VALUE$", GUINode.float_to_string_dp(price_change, 4)) + if price_change != 0.0 else "TRADE_PRICE_TREND_UNCHANGED" + ) + + var trade_settings : int = good_dict.get(trade_settings_key, 0) + + var automated_icon : GUIIcon = get_gui_icon_from_node_and_path(good_entry_panel, ^"./automation_indicator") + if automated_icon: + automated_icon.set_visible(trade_settings & MenuSingleton.TradeSettingBit.TRADE_SETTING_AUTOMATED) + + var buy_sell_icon : GUIIcon = get_gui_icon_from_node_and_path(good_entry_panel, ^"./selling_indicator") + if buy_sell_icon: + if trade_settings & MenuSingleton.TradeSettingBit.TRADE_SETTING_BUYING: + buy_sell_icon.set_icon_index(2) + buy_sell_icon.set_tooltip_string("TRADE_BUYING") + buy_sell_icon.show() + elif trade_settings & MenuSingleton.TradeSettingBit.TRADE_SETTING_SELLING: + buy_sell_icon.set_icon_index(3) + buy_sell_icon.set_tooltip_string("TRADE_SELLING") + buy_sell_icon.show() + else: + buy_sell_icon.hide()