From c79bf36ced0319572c561b82a2a9158ffa3bc94b Mon Sep 17 00:00:00 2001 From: hop311 Date: Wed, 20 Mar 2024 22:51:08 +0000 Subject: Implement Population Menu --- extension/deps/openvic-simulation | 2 +- .../openvic-extension/singletons/GameSingleton.cpp | 7 + .../openvic-extension/singletons/MenuSingleton.cpp | 69 +- .../openvic-extension/singletons/MenuSingleton.hpp | 96 +++ .../singletons/PopulationMenu.cpp | 712 +++++++++++++++++++++ .../NationManagementScreen/PopulationMenu.gd | 574 ++++++++++++++++- game/src/Game/GameSession/ProvinceOverviewPanel.gd | 9 + 7 files changed, 1464 insertions(+), 5 deletions(-) create mode 100644 extension/src/openvic-extension/singletons/PopulationMenu.cpp diff --git a/extension/deps/openvic-simulation b/extension/deps/openvic-simulation index c11d262..020ada6 160000 --- a/extension/deps/openvic-simulation +++ b/extension/deps/openvic-simulation @@ -1 +1 @@ -Subproject commit c11d262a4d2c987c8cf8e0d4b24929cbe56bb289 +Subproject commit 020ada6c8f0f1bf4486fd9e76ec29673044794d6 diff --git a/extension/src/openvic-extension/singletons/GameSingleton.cpp b/extension/src/openvic-extension/singletons/GameSingleton.cpp index 459b2c8..563f1cb 100644 --- a/extension/src/openvic-extension/singletons/GameSingleton.cpp +++ b/extension/src/openvic-extension/singletons/GameSingleton.cpp @@ -9,6 +9,7 @@ #include "openvic-extension/singletons/AssetManager.hpp" #include "openvic-extension/singletons/LoadLocalisation.hpp" +#include "openvic-extension/singletons/MenuSingleton.hpp" #include "openvic-extension/utility/ClassBindings.hpp" #include "openvic-extension/utility/Utilities.hpp" @@ -115,6 +116,7 @@ Error GameSingleton::setup_game(int32_t bookmark_index) { Bookmark const* bookmark = game_manager.get_history_manager().get_bookmark_manager().get_bookmark_by_index(bookmark_index); ERR_FAIL_NULL_V_MSG(bookmark, FAILED, vformat("Failed to get bookmark with index: %d", bookmark_index)); bool ret = game_manager.load_bookmark(bookmark); + for (Province& province : game_manager.get_map().get_provinces()) { province.set_crime( game_manager.get_crime_manager().get_crime_modifier_by_index( @@ -122,6 +124,11 @@ Error GameSingleton::setup_game(int32_t bookmark_index) { ) ); } + + MenuSingleton* menu_singleton = MenuSingleton::get_singleton(); + ERR_FAIL_NULL_V(menu_singleton, FAILED); + menu_singleton->_population_menu_update_provinces(); + return ERR(ret); } diff --git a/extension/src/openvic-extension/singletons/MenuSingleton.cpp b/extension/src/openvic-extension/singletons/MenuSingleton.cpp index 52fb6af..02fcd54 100644 --- a/extension/src/openvic-extension/singletons/MenuSingleton.cpp +++ b/extension/src/openvic-extension/singletons/MenuSingleton.cpp @@ -1,7 +1,5 @@ #include "MenuSingleton.hpp" -#include - #include #include "openvic-extension/classes/GFXPieChartTexture.hpp" @@ -15,6 +13,19 @@ using namespace OpenVic; using OpenVic::Utilities::std_to_godot_string; using OpenVic::Utilities::std_view_to_godot_string; +StringName const& MenuSingleton::_signal_population_menu_province_list_changed() { + static const StringName signal_population_menu_province_list_changed = "population_menu_province_list_changed"; + return signal_population_menu_province_list_changed; +} +StringName const& MenuSingleton::_signal_population_menu_province_list_selected_changed() { + static const StringName signal_population_menu_province_list_selected_changed = "population_menu_province_list_selected_changed"; + return signal_population_menu_province_list_selected_changed; +} +StringName const& MenuSingleton::_signal_population_menu_pops_changed() { + static const StringName signal_population_menu_pops_changed = "population_menu_pops_changed"; + return signal_population_menu_pops_changed; +} + void MenuSingleton::_bind_methods() { /* PROVINCE OVERVIEW PANEL */ OV_BIND_METHOD(MenuSingleton::get_province_info_from_index, { "index" }); @@ -35,6 +46,60 @@ void MenuSingleton::_bind_methods() { OV_BIND_METHOD(MenuSingleton::can_increase_speed); OV_BIND_METHOD(MenuSingleton::can_decrease_speed); OV_BIND_METHOD(MenuSingleton::get_longform_date); + + /* POPULATION MENU */ + OV_BIND_METHOD(MenuSingleton::get_population_menu_province_list_row_count); + OV_BIND_METHOD(MenuSingleton::get_population_menu_province_list_rows, { "start", "count" }); + OV_BIND_METHOD( + MenuSingleton::population_menu_select_province_list_entry, { "select_index", "set_scroll_index" }, DEFVAL(false) + ); + OV_BIND_METHOD(MenuSingleton::population_menu_select_province, { "province_index" }); + OV_BIND_METHOD(MenuSingleton::population_menu_toggle_expanded, { "toggle_index", "emit_selected_changed" }, DEFVAL(true)); + + OV_BIND_METHOD(MenuSingleton::population_menu_select_sort_key, { "sort_key" }); + OV_BIND_METHOD(MenuSingleton::get_population_menu_pop_rows, { "start", "count" }); + OV_BIND_METHOD(MenuSingleton::get_population_menu_pop_row_count); + + OV_BIND_METHOD(MenuSingleton::get_population_menu_pop_filter_setup_info); + OV_BIND_METHOD(MenuSingleton::get_population_menu_pop_filter_info); + OV_BIND_METHOD(MenuSingleton::population_menu_toggle_pop_filter, { "filter_index" }); + OV_BIND_METHOD(MenuSingleton::population_menu_select_all_pop_filters); + OV_BIND_METHOD(MenuSingleton::population_menu_deselect_all_pop_filters); + + OV_BIND_METHOD(MenuSingleton::get_population_menu_distribution_setup_info); + OV_BIND_METHOD(MenuSingleton::get_population_menu_distribution_info); + + ADD_SIGNAL(MethodInfo(_signal_population_menu_province_list_changed())); + ADD_SIGNAL( + MethodInfo(_signal_population_menu_province_list_selected_changed(), PropertyInfo(Variant::INT, "scroll_index")) + ); + ADD_SIGNAL(MethodInfo(_signal_population_menu_pops_changed())); + + using enum population_menu_t::ProvinceListEntry; + BIND_ENUM_CONSTANT(LIST_ENTRY_NONE); + BIND_ENUM_CONSTANT(LIST_ENTRY_COUNTRY); + BIND_ENUM_CONSTANT(LIST_ENTRY_STATE); + BIND_ENUM_CONSTANT(LIST_ENTRY_PROVINCE); + + using enum population_menu_t::PopSortKey; + BIND_ENUM_CONSTANT(NONE); + BIND_ENUM_CONSTANT(SORT_SIZE); + BIND_ENUM_CONSTANT(SORT_TYPE); + BIND_ENUM_CONSTANT(SORT_CULTURE); + BIND_ENUM_CONSTANT(SORT_RELIGION); + BIND_ENUM_CONSTANT(SORT_LOCATION); + BIND_ENUM_CONSTANT(SORT_MILITANCY); + BIND_ENUM_CONSTANT(SORT_CONSCIOUSNESS); + BIND_ENUM_CONSTANT(SORT_IDEOLOGY); + BIND_ENUM_CONSTANT(SORT_ISSUES); + BIND_ENUM_CONSTANT(SORT_UNEMPLOYMENT); + BIND_ENUM_CONSTANT(SORT_CASH); + BIND_ENUM_CONSTANT(SORT_LIFE_NEEDS); + BIND_ENUM_CONSTANT(SORT_EVERYDAY_NEEDS); + BIND_ENUM_CONSTANT(SORT_LUXURY_NEEDS); + BIND_ENUM_CONSTANT(SORT_REBEL_FACTION); + BIND_ENUM_CONSTANT(SORT_SIZE_CHANGE); + BIND_ENUM_CONSTANT(SORT_LITERACY); } MenuSingleton* MenuSingleton::get_singleton() { diff --git a/extension/src/openvic-extension/singletons/MenuSingleton.hpp b/extension/src/openvic-extension/singletons/MenuSingleton.hpp index 6bd90c5..fd1b6c5 100644 --- a/extension/src/openvic-extension/singletons/MenuSingleton.hpp +++ b/extension/src/openvic-extension/singletons/MenuSingleton.hpp @@ -2,10 +2,12 @@ #include +#include #include namespace OpenVic { struct GameManager; + struct Region; class MenuSingleton : public godot::Object { GDCLASS(MenuSingleton, godot::Object) @@ -14,6 +16,70 @@ namespace OpenVic { GameManager* game_manager; + public: + struct population_menu_t { + enum ProvinceListEntry { + LIST_ENTRY_NONE, LIST_ENTRY_COUNTRY, LIST_ENTRY_STATE, LIST_ENTRY_PROVINCE + }; + + struct country_entry_t { + Country const& country; + bool selected = true; + }; + + struct state_entry_t { + // TODO - change to State + Region const& state; + bool selected = true, expanded = false; + }; + + struct province_entry_t { + Province const& province; + bool selected = true; + }; + + using province_list_entry_t = std::variant; + + std::vector province_list_entries; + int32_t visible_province_list_entries = 0; + + struct pop_filter_t { + Pop::pop_size_t count, promotion_demotion_change; + bool selected; + }; + ordered_map pop_filters; + + static constexpr int32_t DISTRIBUTION_COUNT = 6; + /* Distributions: + * - Workforce (PopType) + * - Religion + * - Ideology + * - Nationality (Culture) + * - Issues + * - Vote */ + std::array, DISTRIBUTION_COUNT> distributions; + + enum PopSortKey { + NONE, SORT_SIZE, SORT_TYPE, SORT_CULTURE, SORT_RELIGION, SORT_LOCATION, SORT_MILITANCY, SORT_CONSCIOUSNESS, + SORT_IDEOLOGY, SORT_ISSUES, SORT_UNEMPLOYMENT, SORT_CASH, SORT_LIFE_NEEDS, SORT_EVERYDAY_NEEDS, + SORT_LUXURY_NEEDS, SORT_REBEL_FACTION, SORT_SIZE_CHANGE, SORT_LITERACY, MAX_SORT_KEY + } sort_key = NONE; + bool sort_descending = true; + + std::vector pops, filtered_pops; + }; + + private: + population_menu_t population_menu; + + /* Emitted when the number of visible province list rows changes (list generated or state entry expanded).*/ + static godot::StringName const& _signal_population_menu_province_list_changed(); + /* Emitted when the state of visible province list rows changes (selection changes). Provides an integer argument + * which, if not negative, the province list scroll index should be updated to. */ + static godot::StringName const& _signal_population_menu_province_list_selected_changed(); + /* Emitted when the selected/filtered collection of pops changes. */ + static godot::StringName const& _signal_population_menu_pops_changed(); + protected: static void _bind_methods(); @@ -44,5 +110,35 @@ namespace OpenVic { bool can_increase_speed() const; bool can_decrease_speed() const; godot::String get_longform_date() const; + + /* POPULATION MENU */ + void _population_menu_update_provinces(); + int32_t get_population_menu_province_list_row_count() const; + godot::TypedArray get_population_menu_province_list_rows(int32_t start, int32_t count) const; + godot::Error population_menu_select_province_list_entry(int32_t select_index, bool set_scroll_index = false); + godot::Error population_menu_select_province(int32_t province_index); + godot::Error population_menu_toggle_expanded(int32_t toggle_index, bool emit_selected_changed = true); + + void _population_menu_update_pops(); + void _population_menu_update_filtered_pops(); + using sort_func_t = std::function; + sort_func_t _get_population_menu_sort_func(population_menu_t::PopSortKey sort_key) const; + void _population_menu_sort_pops(); + godot::Error population_menu_select_sort_key(population_menu_t::PopSortKey sort_key); + godot::TypedArray get_population_menu_pop_rows(int32_t start, int32_t count) const; + int32_t get_population_menu_pop_row_count() const; + + godot::PackedInt32Array get_population_menu_pop_filter_setup_info(); + godot::TypedArray get_population_menu_pop_filter_info() const; + godot::Error population_menu_toggle_pop_filter(int32_t filter_index); + void population_menu_select_all_pop_filters(); + void population_menu_deselect_all_pop_filters(); + + godot::PackedStringArray get_population_menu_distribution_setup_info() const; + /* Array of GFXPieChartTexture::godot_pie_chart_data_t. */ + godot::TypedArray get_population_menu_distribution_info() const; }; } + +VARIANT_ENUM_CAST(OpenVic::MenuSingleton::population_menu_t::ProvinceListEntry); +VARIANT_ENUM_CAST(OpenVic::MenuSingleton::population_menu_t::PopSortKey); diff --git a/extension/src/openvic-extension/singletons/PopulationMenu.cpp b/extension/src/openvic-extension/singletons/PopulationMenu.cpp new file mode 100644 index 0000000..b256e1f --- /dev/null +++ b/extension/src/openvic-extension/singletons/PopulationMenu.cpp @@ -0,0 +1,712 @@ +#include "MenuSingleton.hpp" + +#include + +#include + +#include "openvic-extension/classes/GFXPieChartTexture.hpp" +#include "openvic-extension/utility/Utilities.hpp" + +using namespace godot; +using namespace OpenVic; + +using OpenVic::Utilities::std_view_to_godot_string; + +/* POPULATION MENU */ + +void MenuSingleton::_population_menu_update_provinces() { + ERR_FAIL_NULL(game_manager); + + population_menu.province_list_entries.clear(); + population_menu.visible_province_list_entries = 0; + + for (Country const* country : { + // Example country + game_manager->get_country_manager().get_country_by_identifier("ENG") + }) { + ERR_CONTINUE(country == nullptr); + + population_menu.province_list_entries.emplace_back(population_menu_t::country_entry_t { *country }); + population_menu.visible_province_list_entries++; + + // TODO - change to State + for (Region const& state : game_manager->get_map().get_regions()) { + + population_menu.province_list_entries.emplace_back(population_menu_t::state_entry_t { state }); + population_menu.visible_province_list_entries++; + + for (Province const* province : state.get_provinces()) { + population_menu.province_list_entries.emplace_back(population_menu_t::province_entry_t { *province }); + } + } + } + + population_menu.sort_key = population_menu_t::NONE; + + emit_signal(_signal_population_menu_province_list_changed()); + + // TODO - may need to emit population_menu_province_list_selected_changed if _update_info cannot be guaranteed + + _population_menu_update_pops(); +} + +int32_t MenuSingleton::get_population_menu_province_list_row_count() const { + return population_menu.visible_province_list_entries; +} + +TypedArray MenuSingleton::get_population_menu_province_list_rows(int32_t start, int32_t count) const { + if (population_menu.province_list_entries.empty()) { + return {}; + } + + ERR_FAIL_INDEX_V_MSG( + start, population_menu.visible_province_list_entries, {}, + vformat("Invalid start for population menu province list rows: %d", start) + ); + ERR_FAIL_COND_V_MSG(count <= 0, {}, vformat("Invalid count for population menu province list rows: %d", count)); + + static const StringName type_key = "type"; + static const StringName index_key = "index"; + static const StringName name_key = "name"; + static const StringName size_key = "size"; + static const StringName change_key = "change"; + static const StringName selected_key = "selected"; + /* State-only keys */ + static const StringName expanded_key = "expanded"; + static const StringName colonial_status_key = "colony"; + // TODO - national focus + + struct entry_visitor_t { + + int32_t& start_counter; + int32_t& count_counter; + + /* This is the index among all entries, not just visible ones unlike start and count. */ + int32_t index = 0; + + bool is_expanded = true; + + TypedArray array {}; + + /* Overloads return false if count_counter reaches 0 and the function should return, + * otherwise true indicating the province list loop should continue. */ + + bool operator()(population_menu_t::country_entry_t const& country_entry) { + if (start_counter-- <= 0) { + Dictionary country_dict; + + country_dict[type_key] = population_menu_t::LIST_ENTRY_COUNTRY; + country_dict[index_key] = index; + country_dict[name_key] = std_view_to_godot_string(country_entry.country.get_identifier()); + country_dict[size_key] = 0; + country_dict[change_key] = 0; + country_dict[selected_key] = country_entry.selected; + + array.push_back(country_dict); + + return --count_counter > 0; + } + + return true; + } + + bool operator()(population_menu_t::state_entry_t const& state_entry) { + is_expanded = state_entry.expanded; + + if (start_counter-- <= 0) { + Dictionary state_dict; + + state_dict[type_key] = population_menu_t::LIST_ENTRY_STATE; + state_dict[index_key] = index; + state_dict[name_key] = std_view_to_godot_string(state_entry.state.get_identifier()); + state_dict[size_key] = 0; + state_dict[change_key] = 0; + state_dict[selected_key] = state_entry.selected; + state_dict[expanded_key] = state_entry.expanded; + state_dict[colonial_status_key] = false; + + array.push_back(state_dict); + + return --count_counter > 0; + } + + return true; + } + + bool operator()(population_menu_t::province_entry_t const& province_entry) { + if (is_expanded && start_counter-- <= 0) { + Dictionary province_dict; + + province_dict[type_key] = population_menu_t::LIST_ENTRY_PROVINCE; + province_dict[index_key] = index; + province_dict[name_key] = std_view_to_godot_string(province_entry.province.get_identifier()); + province_dict[size_key] = province_entry.province.get_total_population(); + province_dict[change_key] = 0; + province_dict[selected_key] = province_entry.selected; + + array.push_back(province_dict); + + return --count_counter > 0; + } + + return true; + } + } entry_visitor { start, count }; + + while (entry_visitor.index < population_menu.province_list_entries.size() + && std::visit(entry_visitor, population_menu.province_list_entries[entry_visitor.index])) { + entry_visitor.index++; + } + + return entry_visitor.array; +} + +Error MenuSingleton::population_menu_select_province_list_entry(int32_t select_index, bool set_scroll_index) { + ERR_FAIL_INDEX_V(select_index, population_menu.province_list_entries.size(), FAILED); + + struct entry_visitor { + + const int32_t _select_index; + + int32_t index = 0, visible_index = 0; + bool is_expanded = true; + + int32_t selected_visible_index = -1; + + using enum population_menu_t::ProvinceListEntry; + population_menu_t::ProvinceListEntry select_level = LIST_ENTRY_NONE; + + void operator()(population_menu_t::country_entry_t& country_entry) { + if (index == _select_index) { + select_level = LIST_ENTRY_COUNTRY; + + country_entry.selected = true; + + selected_visible_index = visible_index; + } else { + select_level = LIST_ENTRY_NONE; + + country_entry.selected = false; + } + + visible_index++; + } + + void operator()(population_menu_t::state_entry_t& state_entry) { + if (select_level == LIST_ENTRY_COUNTRY) { + state_entry.selected = true; + } else if (index == _select_index) { + select_level = LIST_ENTRY_STATE; + + state_entry.selected = true; + + selected_visible_index = visible_index; + } else { + select_level = LIST_ENTRY_NONE; + state_entry.selected = false; + } + + visible_index++; + + is_expanded = state_entry.expanded; + } + + void operator()(population_menu_t::province_entry_t& province_entry) { + if (select_level == LIST_ENTRY_COUNTRY || select_level == LIST_ENTRY_STATE) { + province_entry.selected = true; + } else if (index == _select_index) { + province_entry.selected = true; + + selected_visible_index = visible_index; + } else { + province_entry.selected = false; + } + + if (is_expanded) { + visible_index++; + } + } + + } entry_visitor { select_index }; + + while (entry_visitor.index < population_menu.province_list_entries.size()) { + std::visit(entry_visitor, population_menu.province_list_entries[entry_visitor.index]); + entry_visitor.index++; + } + + emit_signal( + _signal_population_menu_province_list_selected_changed(), + set_scroll_index ? entry_visitor.selected_visible_index : -1 + ); + + _population_menu_update_pops(); + + return OK; +} + +Error MenuSingleton::population_menu_select_province(int32_t province_index) { + ERR_FAIL_NULL_V(game_manager, FAILED); + + ERR_FAIL_COND_V(province_index <= 0 || province_index > game_manager->get_map().get_province_count(), FAILED); + + struct entry_visitor_t { + + MenuSingleton& menu_singleton; + + const int32_t _province_index; + + int32_t index = 0; + + int32_t state_entry_to_expand = -1; + + bool ret = true; + + /* Overloads return false if the province entry is found and the loop can stop, true otherwise. */ + + bool operator()(population_menu_t::country_entry_t& country_entry) { + return true; + } + + bool operator()(population_menu_t::state_entry_t& state_entry) { + if (state_entry.expanded) { + state_entry_to_expand = -1; + } else { + state_entry_to_expand = index; + } + return true; + } + + bool operator()(population_menu_t::province_entry_t& province_entry) { + if (province_entry.province.get_index() == _province_index) { + + if (state_entry_to_expand >= 0) { + ret &= menu_singleton.population_menu_toggle_expanded(state_entry_to_expand, false) == OK; + } + + ret &= menu_singleton.population_menu_select_province_list_entry(index, true) == OK; + + return false; + } + return true; + } + + } entry_visitor { *this, province_index }; + + while (entry_visitor.index < population_menu.province_list_entries.size() + && std::visit(entry_visitor, population_menu.province_list_entries[entry_visitor.index])) { + entry_visitor.index++; + } + + ERR_FAIL_COND_V_MSG( + entry_visitor.index >= population_menu.province_list_entries.size(), FAILED, + vformat("Cannot select province index %d - not found in population menu province list!", province_index) + ); + + return ERR(entry_visitor.ret); +} + +Error MenuSingleton::population_menu_toggle_expanded(int32_t toggle_index, bool emit_selected_changed) { + ERR_FAIL_INDEX_V(toggle_index, population_menu.province_list_entries.size(), FAILED); + + population_menu_t::state_entry_t* state_entry = + std::get_if(&population_menu.province_list_entries[toggle_index]); + + ERR_FAIL_NULL_V_MSG(state_entry, FAILED, vformat("Cannot toggle expansion of a non-state entry! (%d)", toggle_index)); + + int32_t provinces = 0; + + while (++toggle_index < population_menu.province_list_entries.size() + && std::holds_alternative(population_menu.province_list_entries[toggle_index])) { + provinces++; + } + + if (state_entry->expanded) { + state_entry->expanded = false; + population_menu.visible_province_list_entries -= provinces; + } else { + state_entry->expanded = true; + population_menu.visible_province_list_entries += provinces; + } + + emit_signal(_signal_population_menu_province_list_changed()); + + if (emit_selected_changed) { + emit_signal(_signal_population_menu_province_list_selected_changed(), -1); + } + + return OK; +} + +void MenuSingleton::_population_menu_update_pops() { + for (auto [pop_type, filter] : mutable_iterator(population_menu.pop_filters)) { + filter.count = 0; + filter.promotion_demotion_change = 0; + } + + population_menu.pops.clear(); + + for (int32_t index = 0; index < population_menu.province_list_entries.size(); index++) { + population_menu_t::province_entry_t const* province_entry = + std::get_if(&population_menu.province_list_entries[index]); + + if (province_entry != nullptr && province_entry->selected) { + for (Pop const& pop : province_entry->province.get_pops()) { + population_menu.pops.push_back(&pop); + population_menu_t::pop_filter_t& filter = population_menu.pop_filters[&pop.get_type()]; + filter.count += pop.get_size(); + // TODO - set filter.promotion_demotion_change + } + } + } + + _population_menu_update_filtered_pops(); +} + +void MenuSingleton::_population_menu_update_filtered_pops() { + population_menu.filtered_pops.clear(); + + for (fixed_point_map_t& distribution : population_menu.distributions) { + distribution.clear(); + } + + for (Pop const* pop : population_menu.pops) { + if (population_menu.pop_filters[&pop->get_type()].selected) { + population_menu.filtered_pops.push_back(pop); + } + } + + for (Pop const* pop : population_menu.filtered_pops) { + population_menu.distributions[0][&pop->get_type()] += pop->get_size(); + population_menu.distributions[1][&pop->get_religion()] += pop->get_size(); + population_menu.distributions[2] += + cast_map(pop->get_ideologies() * static_cast(pop->get_size())); + population_menu.distributions[3][&pop->get_culture()] += pop->get_size(); + population_menu.distributions[4] += + cast_map(pop->get_issues() * static_cast(pop->get_size())); + population_menu.distributions[5] += + cast_map(pop->get_votes() * static_cast(pop->get_size())); + } + + for (fixed_point_map_t& distribution : population_menu.distributions) { + normalise_fixed_point_map(distribution); + } + + _population_menu_sort_pops(); +} + +MenuSingleton::sort_func_t MenuSingleton::_get_population_menu_sort_func(population_menu_t::PopSortKey sort_key) const { + using enum population_menu_t::PopSortKey; + switch (sort_key) { + case SORT_SIZE: + return [](Pop const* a, Pop const* b) -> bool { + return a->get_size() < b->get_size(); + }; + case SORT_TYPE: + return [this](Pop const* a, Pop const* b) -> bool { + return tr(std_view_to_godot_string(a->get_type().get_identifier())) + < tr(std_view_to_godot_string(b->get_type().get_identifier())); + }; + case SORT_CULTURE: + return [this](Pop const* a, Pop const* b) -> bool { + return tr(std_view_to_godot_string(a->get_culture().get_identifier())) + < tr(std_view_to_godot_string(b->get_culture().get_identifier())); + }; + case SORT_RELIGION: + return [this](Pop const* a, Pop const* b) -> bool { + return tr(std_view_to_godot_string(a->get_religion().get_identifier())) + < tr(std_view_to_godot_string(b->get_religion().get_identifier())); + }; + case SORT_LOCATION: + return [this](Pop const* a, Pop const* b) -> bool { + return tr(a->get_location() != nullptr ? std_view_to_godot_string(a->get_location()->get_identifier()) : String {}) + < tr(b->get_location() != nullptr ? std_view_to_godot_string(b->get_location()->get_identifier()) : String {}); + }; + case SORT_MILITANCY: + return [](Pop const* a, Pop const* b) -> bool { + return a->get_militancy() < b->get_militancy(); + }; + case SORT_CONSCIOUSNESS: + return [](Pop const* a, Pop const* b) -> bool { + return a->get_consciousness() < b->get_consciousness(); + }; + case SORT_IDEOLOGY: + return [](Pop const* a, Pop const* b) -> bool { + return sorted_fixed_map_less_than(a->get_ideologies(), b->get_ideologies()); + }; + case SORT_ISSUES: + return [](Pop const* a, Pop const* b) -> bool { + return sorted_fixed_map_less_than(a->get_issues(), b->get_issues()); + }; + case SORT_UNEMPLOYMENT: + return [](Pop const* a, Pop const* b) -> bool { + return a->get_unemployment() < b->get_unemployment(); + }; + case SORT_CASH: + return [](Pop const* a, Pop const* b) -> bool { + return a->get_cash() < b->get_cash(); + }; + case SORT_LIFE_NEEDS: + return [](Pop const* a, Pop const* b) -> bool { + return a->get_life_needs_fulfilled() < b->get_life_needs_fulfilled(); + }; + case SORT_EVERYDAY_NEEDS: + return [](Pop const* a, Pop const* b) -> bool { + return a->get_everyday_needs_fulfilled() < b->get_everyday_needs_fulfilled(); + }; + case SORT_LUXURY_NEEDS: + return [](Pop const* a, Pop const* b) -> bool { + return a->get_luxury_needs_fulfilled() < b->get_luxury_needs_fulfilled(); + }; + case SORT_REBEL_FACTION: + return [](Pop const* a, Pop const* b) -> bool { return false; }; // TODO - implement + case SORT_SIZE_CHANGE: + return [](Pop const* a, Pop const* b) -> bool { + return a->get_total_change() < b->get_total_change(); + }; + case SORT_LITERACY: + return [](Pop const* a, Pop const* b) -> bool { + return a->get_literacy() < b->get_literacy(); + }; + default: + UtilityFunctions::push_error("Invalid population menu sort key: ", sort_key); + return [](Pop const* a, Pop const* b) -> bool { return false; }; + } +} + +void MenuSingleton::_population_menu_sort_pops() { + if (population_menu.sort_key != population_menu_t::NONE) { + const sort_func_t base_sort_func = _get_population_menu_sort_func(population_menu.sort_key); + + const sort_func_t sort_func = population_menu.sort_descending + ? base_sort_func + : [base_sort_func](Pop const* a, Pop const* b) { return base_sort_func(b, a); }; + + std::sort(population_menu.filtered_pops.begin(), population_menu.filtered_pops.end(), sort_func); + } + + emit_signal(_signal_population_menu_pops_changed()); +} + +Error MenuSingleton::population_menu_select_sort_key(population_menu_t::PopSortKey sort_key) { + using enum population_menu_t::PopSortKey; + /* sort_key must be cast here to avoid causing clang to segfault during compilation. */ + ERR_FAIL_INDEX_V_MSG( + static_cast(sort_key), static_cast(MAX_SORT_KEY), FAILED, + vformat("Invalid population menu sort key: %d (must be under %d)", sort_key, MAX_SORT_KEY) + ); + + if (sort_key == population_menu.sort_key) { + /* Re-selecting the current sort key reverses sort order. */ + population_menu.sort_descending = !population_menu.sort_descending; + } else { + /* Selecting a new sort key switches sorting to that key, preserving the existing sort order. */ + population_menu.sort_key = sort_key; + } + + _population_menu_sort_pops(); + + return OK; +} + +TypedArray MenuSingleton::get_population_menu_pop_rows(int32_t start, int32_t count) const { + if (population_menu.filtered_pops.empty()) { + return {}; + } + ERR_FAIL_INDEX_V_MSG( + start, population_menu.filtered_pops.size(), {}, vformat("Invalid start for population menu pop rows: %d", start) + ); + ERR_FAIL_COND_V_MSG(count <= 0, {}, vformat("Invalid count for population menu pop rows: %d", count)); + + if (start + count > population_menu.filtered_pops.size()) { + count = population_menu.filtered_pops.size() - start; + } + + static const StringName pop_size_key = "size"; + + static const StringName pop_type_icon_key = "pop_type_icon"; + // TODO - pop type name + // TODO - promotions (target pop type and count) + // TODO - demotions (target pop type and count) + // TODO - good being produced (artisans, farmers, labourers, slaves) + // TODO - military unit and army (soldiers) + + static const StringName pop_culture_key = "culture"; + // TODO - cultural assimilation (primary/accepted, or number, target culture, and conditional weights breakdown) + + static const StringName pop_religion_icon_key = "religion_icon"; + // TODO - religion name + // TODO - religious conversion (accepted, or number, target religion, and conditional weights breakdown) + + static const StringName pop_location_key = "location"; + // TODO - internal, external and colonial migration + + static const StringName pop_militancy_key = "militancy"; + // TODO - monthly militancy change and modifier breakdown + + static const StringName pop_consciousness_key = "consciousness"; + // TODO - monthly consciousness change and modifier breakdown + + static const StringName pop_ideology_key = "ideology"; + + static const StringName pop_issues_key = "issues"; + + static const StringName pop_unemployment_key = "unemployment"; + + static const StringName pop_cash_key = "cash"; + // TODO - daily income, needs, salary and savings + + static const StringName pop_life_needs_key = "life_needs"; + static const StringName pop_everyday_needs_key = "everyday_needs"; + static const StringName pop_luxury_needs_key = "luxury_needs"; + // TODO - goods not available on market or goods not affordale + price (for all 3 needs types) + + // TODO - rebel faction icon and name/description + + static const StringName pop_size_change_key = "size_change"; + // TODO - size change breakdown + + static const StringName pop_literacy_key = "literacy"; + // TODO - monthly change + + TypedArray array; + + for (int32_t idx = start; idx < start + count; ++idx) { + Pop const* pop = population_menu.filtered_pops[idx]; + Dictionary pop_dict; + + pop_dict[pop_size_key] = pop->get_size(); + pop_dict[pop_type_icon_key] = pop->get_type().get_sprite(); + pop_dict[pop_culture_key] = std_view_to_godot_string(pop->get_culture().get_identifier()); + pop_dict[pop_religion_icon_key] = pop->get_religion().get_icon(); + pop_dict[pop_location_key] = + pop->get_location() != nullptr ? std_view_to_godot_string(pop->get_location()->get_identifier()) : String {}; + pop_dict[pop_militancy_key] = pop->get_militancy().to_float(); + pop_dict[pop_consciousness_key] = pop->get_consciousness().to_float(); + pop_dict[pop_ideology_key] = GFXPieChartTexture::distribution_to_slices_array(pop->get_ideologies()); + pop_dict[pop_issues_key] = GFXPieChartTexture::distribution_to_slices_array(pop->get_issues()); + pop_dict[pop_unemployment_key] = pop->get_unemployment().to_float(); + pop_dict[pop_cash_key] = pop->get_cash().to_float(); + pop_dict[pop_life_needs_key] = pop->get_life_needs_fulfilled().to_float(); + pop_dict[pop_everyday_needs_key] = pop->get_everyday_needs_fulfilled().to_float(); + pop_dict[pop_luxury_needs_key] = pop->get_luxury_needs_fulfilled().to_float(); + pop_dict[pop_size_change_key] = pop->get_total_change(); + pop_dict[pop_literacy_key] = pop->get_literacy().to_float(); + + array.push_back(pop_dict); + } + + return array; +} + +int32_t MenuSingleton::get_population_menu_pop_row_count() const { + return population_menu.filtered_pops.size(); +} + +PackedInt32Array MenuSingleton::get_population_menu_pop_filter_setup_info() { + ERR_FAIL_NULL_V(game_manager, {}); + + if (population_menu.pop_filters.empty()) { + for (PopType const& pop_type : game_manager->get_pop_manager().get_pop_types()) { + population_menu.pop_filters.emplace(&pop_type, population_menu_t::pop_filter_t { 0, 0, true }); + } + } + ERR_FAIL_COND_V_MSG(population_menu.pop_filters.empty(), {}, "Failed to generate population menu pop filters!"); + + PackedInt32Array array; + + for (auto const& [pop_type, filter] : population_menu.pop_filters) { + array.push_back(pop_type->get_sprite()); + } + + return array; +} + +TypedArray MenuSingleton::get_population_menu_pop_filter_info() const { + static const StringName pop_filter_count_key = "count"; + static const StringName pop_filter_change_key = "change"; + static const StringName pop_filter_selected_key = "selected"; + + TypedArray array; + + for (auto const& [pop_type, filter] : population_menu.pop_filters) { + Dictionary filter_dict; + filter_dict[pop_filter_count_key] = filter.count; + filter_dict[pop_filter_change_key] = filter.promotion_demotion_change; + filter_dict[pop_filter_selected_key] = filter.selected; + array.push_back(filter_dict); + } + + return array; +} + +Error MenuSingleton::population_menu_toggle_pop_filter(int32_t index) { + ERR_FAIL_COND_V_MSG( + index < 0 || index >= population_menu.pop_filters.size(), FAILED, vformat("Invalid pop filter index: %d", index) + ); + + population_menu_t::pop_filter_t& filter = mutable_iterator(population_menu.pop_filters).begin()[index].second; + filter.selected = !filter.selected; + + _population_menu_update_filtered_pops(); + + return OK; +} + +void MenuSingleton::population_menu_select_all_pop_filters() { + bool changed = false; + + for (auto [pop_type, filter] : mutable_iterator(population_menu.pop_filters)) { + if (!filter.selected) { + filter.selected = true; + changed = true; + } + } + + if (changed) { + _population_menu_update_filtered_pops(); + } +} + +void MenuSingleton::population_menu_deselect_all_pop_filters() { + bool changed = false; + for (auto [pop_type, filter] : mutable_iterator(population_menu.pop_filters)) { + if (filter.selected) { + filter.selected = false; + changed = true; + } + } + if (changed) { + _population_menu_update_filtered_pops(); + } +} + +PackedStringArray MenuSingleton::get_population_menu_distribution_setup_info() const { + static const PackedStringArray distribution_names = []() -> PackedStringArray { + PackedStringArray array; + + for (char const* name : std::array { + /* Workforce (PopType) */ "WORKFORCE_DISTTITLE", + /* Religion */ "RELIGION_DISTTITLE", + /* Ideology */ "IDEOLOGY_DISTTITLE", + /* Nationality (Culture) */ "NATIONALITY_DISTTITLE", + /* Issues */ "DOMINANT_ISSUES_DISTTITLE", + /* Vote */ "ELECTORATE_DISTTITLE" + }) { + array.push_back(name); + } + + return array; + }(); + + return distribution_names; +} + +TypedArray MenuSingleton::get_population_menu_distribution_info() const { + TypedArray array; + + for (fixed_point_map_t const& distribution : population_menu.distributions) { + array.push_back(GFXPieChartTexture::distribution_to_slices_array(distribution)); + } + + return array; +} diff --git a/game/src/Game/GameSession/NationManagementScreen/PopulationMenu.gd b/game/src/Game/GameSession/NationManagementScreen/PopulationMenu.gd index 29bd56b..5de2d25 100644 --- a/game/src/Game/GameSession/NationManagementScreen/PopulationMenu.gd +++ b/game/src/Game/GameSession/NationManagementScreen/PopulationMenu.gd @@ -4,19 +4,371 @@ var _active : bool = false const _screen : NationManagement.Screen = NationManagement.Screen.POPULATION +const _scene_name : String = "country_pops" + +var _pop_screen_panel : Panel + +var _province_listbox : GUIListBox +var _province_list_scroll_index : int = 0 +var _province_list_types : Array[MenuSingleton.ProvinceListEntry] +var _province_list_indices : PackedInt32Array +var _province_list_panels : Array[Panel] +var _province_list_button_icons : Array[GFXSpriteTexture] +var _province_list_name_labels : Array[Label] +var _province_list_size_labels : Array[Label] +var _province_list_growth_icons : Array[GFXSpriteTexture] +var _province_list_colony_buttons : Array[Button] +var _province_list_national_focus_icons : Array[GFXSpriteTexture] +var _province_list_expand_icons : Array[GFXSpriteTexture] + +var _pop_filter_buttons : Array[Button] +var _pop_filter_icons : Array[GFXSpriteTexture] +var _pop_filter_selected_icons : Array[GFXButtonStateTexture] +var _pop_filter_hover_icons : Array[GFXButtonStateTexture] + +var _distribution_charts : Array[GFXPieChartTexture] +var _distribution_lists : Array[GUIListBox] + +var _pop_list_scrollbar : GUIScrollbar +var _pop_list_scroll_index : int = 0 + +var _pop_list_rows : Array[Panel] +var _pop_list_size_labels : Array[Label] +var _pop_list_type_buttons : Array[Button] +var _pop_list_type_icons : Array[GFXSpriteTexture] +var _pop_list_producing_icons : Array[GFXSpriteTexture] +var _pop_list_culture_labels : Array[Label] +var _pop_list_religion_icons : Array[GFXSpriteTexture] +var _pop_list_location_labels : Array[Label] +var _pop_list_militancy_labels : Array[Label] +var _pop_list_consciousness_labels : Array[Label] +var _pop_list_ideology_charts : Array[GFXPieChartTexture] +var _pop_list_issues_charts : Array[GFXPieChartTexture] +var _pop_list_unemployment_progressbars : Array[TextureProgressBar] +var _pop_list_cash_labels : Array[Label] +var _pop_list_life_needs_progressbars : Array[TextureProgressBar] +var _pop_list_everyday_needs_progressbars : Array[TextureProgressBar] +var _pop_list_luxury_needs_progressbars : Array[TextureProgressBar] +var _pop_list_rebel_icons : Array[GFXSpriteTexture] +var _pop_list_social_movement_icons : Array[GFXSpriteTexture] +var _pop_list_political_movement_icons : Array[GFXSpriteTexture] +var _pop_list_national_movement_flags : Array[GFXMaskedFlagTexture] +var _pop_list_size_change_icons : Array[GFXSpriteTexture] +var _pop_list_literacy_labels : Array[Label] + func _ready() -> void: GameSingleton.gamestate_updated.connect(_update_info) + MenuSingleton.population_menu_province_list_changed.connect(_setup_province_list) + MenuSingleton.population_menu_province_list_selected_changed.connect(_update_province_list) + MenuSingleton.population_menu_pops_changed.connect(_update_pops) Events.NationManagementScreens.update_active_nation_management_screen.connect(_on_update_active_nation_management_screen) - add_gui_element("country_pops", "country_pop") + add_gui_element(_scene_name, "country_pop") var close_button : Button = get_button_from_nodepath(^"./country_pop/close_button") if close_button: close_button.pressed.connect(Events.NationManagementScreens.close_nation_management_screen.bind(_screen)) + _pop_screen_panel = get_panel_from_nodepath(^"./country_pop") + + # province list is set up via the population_menu_provinces_changed signal + _setup_sort_buttons() + _setup_pop_filter_buttons() + _setup_distribution_windows() + _setup_pop_list() + _update_info() +func _generate_province_list_row(index : int, type : MenuSingleton.ProvinceListEntry) -> Error: + while _province_list_types.size() <= index: + _province_list_types.push_back(MenuSingleton.LIST_ENTRY_NONE) + _province_list_indices.push_back(-1) + _province_list_panels.push_back(null) + _province_list_button_icons.push_back(null) + _province_list_name_labels.push_back(null) + _province_list_size_labels.push_back(null) + _province_list_growth_icons.push_back(null) + _province_list_colony_buttons.push_back(null) + _province_list_national_focus_icons.push_back(null) + _province_list_expand_icons.push_back(null) + + if _province_list_types[index] == type: + return OK + + if _province_list_panels[index]: + _province_listbox.remove_child(_province_list_panels[index]) + + _province_list_types[index] = MenuSingleton.LIST_ENTRY_NONE + _province_list_indices[index] = -1 + _province_list_panels[index] = null + _province_list_button_icons[index] = null + _province_list_name_labels[index] = null + _province_list_size_labels[index] = null + _province_list_growth_icons[index] = null + _province_list_colony_buttons[index] = null + _province_list_national_focus_icons[index] = null + _province_list_expand_icons[index] = null + + if type == MenuSingleton.LIST_ENTRY_NONE: + return OK + + const gui_element_names : Dictionary = { + MenuSingleton.LIST_ENTRY_COUNTRY: "poplistitem_country", + MenuSingleton.LIST_ENTRY_STATE: "poplistitem_state", + MenuSingleton.LIST_ENTRY_PROVINCE: "poplistitem_province" + } + + var entry_panel : Panel = GUINode.generate_gui_element(_scene_name, gui_element_names[type]) + + if not entry_panel: + return FAILED + + _province_list_types[index] = type + + _province_list_panels[index] = entry_panel + + var base_button : Button = GUINode.get_button_from_node(entry_panel.get_node(^"./poplistbutton")) + if base_button: + base_button.pressed.connect( + func() -> void: MenuSingleton.population_menu_select_province_list_entry(_province_list_indices[index]) + ) + _province_list_button_icons[index] = GUINode.get_gfx_sprite_texture_from_node(base_button) + + _province_list_name_labels[index] = GUINode.get_label_from_node(entry_panel.get_node(^"./poplist_name")) + + _province_list_size_labels[index] = GUINode.get_label_from_node(entry_panel.get_node(^"./poplist_numpops")) + + _province_list_growth_icons[index] = GUINode.get_gfx_sprite_texture_from_node(entry_panel.get_node(^"./growth_indicator")) + + if type == MenuSingleton.LIST_ENTRY_STATE: + _province_list_colony_buttons[index] = GUINode.get_button_from_node(entry_panel.get_node(^"./colonial_state_icon")) + + var national_focus_button : Button = GUINode.get_button_from_node(entry_panel.get_node(^"./state_focus")) + if national_focus_button: + # TODO - connect national focus button to national focus selection submenu + _province_list_national_focus_icons[index] = GUINode.get_gfx_sprite_texture_from_node(national_focus_button) + + var expand_button : Button = GUINode.get_button_from_node(entry_panel.get_node(^"./expand")) + if expand_button: + expand_button.pressed.connect( + func() -> void: MenuSingleton.population_menu_toggle_expanded(_province_list_indices[index]) + ) + _province_list_expand_icons[index] = GUINode.get_gfx_sprite_texture_from_node(expand_button) + + _province_listbox.add_child(entry_panel) + _province_listbox.move_child(entry_panel, index) + + return OK + +func _setup_province_list() -> void: + if not _province_listbox: + _province_listbox = get_gui_listbox_from_nodepath(^"./country_pop/pop_province_list") + + if not _province_listbox: + return + _province_listbox.scroll_index_changed.connect(_update_province_list) + + if _province_list_panels.size() < 1 or not _province_list_panels[0]: + if _generate_province_list_row(0, MenuSingleton.LIST_ENTRY_COUNTRY) != OK or _province_list_panels.size() < 1 or not _province_list_panels[0]: + push_error("Failed to generate country row in population menu province list to determine row height!") + return + + _province_listbox.set_fixed(MenuSingleton.get_population_menu_province_list_row_count(), _province_list_panels[0].size.y, false) + +func _setup_sort_buttons() -> void: + # button_path : NodePath, clear_text : bool, sort_key : GameSingleton.PopSortKey + const sort_button_info : Array[Array] = [ + [^"./country_pop/sortby_size_button", false, MenuSingleton.SORT_SIZE], + [^"./country_pop/sortby_type_button", false, MenuSingleton.SORT_TYPE], + [^"./country_pop/sortby_nationality_button", false, MenuSingleton.SORT_CULTURE], + [^"./country_pop/sortby_religion_button", false, MenuSingleton.SORT_RELIGION], + [^"./country_pop/sortby_location_button", false, MenuSingleton.SORT_LOCATION], + [^"./country_pop/sortby_mil_button", true, MenuSingleton.SORT_MILITANCY], + [^"./country_pop/sortby_con_button", true, MenuSingleton.SORT_CONSCIOUSNESS], + [^"./country_pop/sortby_ideology_button", true, MenuSingleton.SORT_IDEOLOGY], + [^"./country_pop/sortby_issues_button", true, MenuSingleton.SORT_ISSUES], + [^"./country_pop/sortby_unemployment_button", true, MenuSingleton.SORT_UNEMPLOYMENT], + [^"./country_pop/sortby_cash_button", true, MenuSingleton.SORT_CASH], + [^"./country_pop/sortby_subsistence_button", true, MenuSingleton.SORT_LIFE_NEEDS], + [^"./country_pop/sortby_eve_button", true, MenuSingleton.SORT_EVERYDAY_NEEDS], + [^"./country_pop/sortby_luxury_button", true, MenuSingleton.SORT_LUXURY_NEEDS], + [^"./country_pop/sortby_revoltrisk_button", true, MenuSingleton.SORT_REBEL_FACTION], + [^"./country_pop/sortby_change_button", true, MenuSingleton.SORT_SIZE_CHANGE], + [^"./country_pop/sortby_literacy_button", true, MenuSingleton.SORT_LITERACY] + ] + + for button_info : Array in sort_button_info: + var sort_button : Button = get_button_from_nodepath(button_info[0]) + if sort_button: + if button_info[1]: + sort_button.set_text("") + sort_button.pressed.connect(MenuSingleton.population_menu_select_sort_key.bind(button_info[2])) + +func _setup_pop_filter_buttons() -> void: + if not _pop_screen_panel: + push_error("Cannot set up pop filter buttons without pop screen to add them to") + return + + var pop_filter_sprite_indices : PackedInt32Array = MenuSingleton.get_population_menu_pop_filter_setup_info() + + var pop_filter_start : Vector2 = GUINode.get_gui_position(_scene_name, "popfilter_start") + var pop_filter_step : Vector2 = GUINode.get_gui_position(_scene_name, "popfilter_offset") + + for index : int in pop_filter_sprite_indices.size(): + var pop_filter_button : Button = GUINode.get_button_from_node(GUINode.generate_gui_element(_scene_name, "pop_filter_button")) + var pop_filter_icon : GFXSpriteTexture = null + var pop_filter_selected_icon : GFXButtonStateTexture = null + var pop_filter_hover_icon : GFXButtonStateTexture = null + + if pop_filter_button: + _pop_screen_panel.add_child(pop_filter_button) + pop_filter_button.set_position(pop_filter_start + pop_filter_step * index) + pop_filter_button.pressed.connect(MenuSingleton.population_menu_toggle_pop_filter.bind(index)) + pop_filter_icon = GUINode.get_gfx_sprite_texture_from_node(pop_filter_button) + + if pop_filter_icon: + pop_filter_icon.set_icon_index(pop_filter_sprite_indices[index]) + pop_filter_selected_icon = pop_filter_icon.get_button_state_texture(GFXButtonStateTexture.SELECTED) + pop_filter_hover_icon = pop_filter_icon.get_button_state_texture(GFXButtonStateTexture.HOVER) + + _pop_filter_buttons.push_back(pop_filter_button) + _pop_filter_icons.push_back(pop_filter_icon) + _pop_filter_selected_icons.push_back(pop_filter_selected_icon) + _pop_filter_hover_icons.push_back(pop_filter_hover_icon) + + var select_all_button : Button = get_button_from_nodepath(^"./country_pop/popfilter_ALL") + if select_all_button: + select_all_button.pressed.connect(MenuSingleton.population_menu_select_all_pop_filters) + + var deselect_all_button : Button = get_button_from_nodepath(^"./country_pop/popfilter_DESELECT_ALL") + if deselect_all_button: + deselect_all_button.pressed.connect(MenuSingleton.population_menu_deselect_all_pop_filters) + +func _setup_distribution_windows() -> void: + if not _pop_screen_panel: + push_error("Cannot set up distribution windows without pop screen to add them to") + return + + const columns : int = 3 + + var distribution_names : PackedStringArray = MenuSingleton.get_population_menu_distribution_setup_info() + + var distribution_start : Vector2 = GUINode.get_gui_position(_scene_name, "popdistribution_start") + var distribution_step : Vector2 = GUINode.get_gui_position(_scene_name, "popdistribution_offset") + + for index : int in distribution_names.size(): + var distribution_panel : Panel = GUINode.generate_gui_element(_scene_name, "distribution_window") + var distribution_chart : GFXPieChartTexture = null + var distribution_list : GUIListBox = null + + if distribution_panel: + _pop_screen_panel.add_child(distribution_panel) + distribution_panel.set_position(distribution_start + distribution_step * Vector2(index % columns, index / columns)) + + var name_label : Label = GUINode.get_label_from_node(distribution_panel.get_node(^"./item_name")) + if name_label: + name_label.text = distribution_names[index] + + distribution_chart = GUINode.get_gfx_pie_chart_texture_from_node(distribution_panel.get_node(^"./chart")) + distribution_list = GUINode.get_gui_listbox_from_node(distribution_panel.get_node(^"./member_names")) + + _distribution_charts.push_back(distribution_chart) + _distribution_lists.push_back(distribution_list) + +func _setup_pop_list() -> void: + _pop_list_scrollbar = get_gui_scrollbar_from_nodepath(^"./country_pop/external_scroll_slider") + + var pop_list_panel : Panel = get_panel_from_nodepath(^"./country_pop/pop_list") + if not pop_list_panel: + return + + if _pop_list_scrollbar: + _pop_list_scrollbar.value_changed.connect( + func (value : int) -> void: + _pop_list_scroll_index = value + _update_pop_list() + ) + + pop_list_panel.gui_input.connect( + func (event : InputEvent) -> void: + if event is InputEventMouseButton: + if event.is_pressed(): + if event.get_button_index() == MOUSE_BUTTON_WHEEL_UP: + _pop_list_scrollbar.decrement_value() + elif event.get_button_index() == MOUSE_BUTTON_WHEEL_DOWN: + _pop_list_scrollbar.increment_value() + ) + + var height : float = 0.0 + while height < pop_list_panel.size.y: + var pop_row_panel : Panel = GUINode.generate_gui_element(_scene_name, "popinfomember_popview") + if not pop_row_panel: + break + + pop_list_panel.add_child(pop_row_panel) + pop_row_panel.set_position(Vector2(0, height)) + height += pop_row_panel.size.y + _pop_list_rows.push_back(pop_row_panel) + + _pop_list_size_labels.push_back(GUINode.get_label_from_node(pop_row_panel.get_node(^"./pop_size"))) + + var pop_type_button : Button = GUINode.get_button_from_node(pop_row_panel.get_node(^"./pop_type")) + # TODO - open pop details menu on pop type button press + _pop_list_type_buttons.push_back(pop_type_button) + + _pop_list_type_icons.push_back(GUINode.get_gfx_sprite_texture_from_node(pop_type_button)) + + _pop_list_producing_icons.push_back(GUINode.get_gfx_sprite_texture_from_node(pop_row_panel.get_node(^"./pop_producing_icon"))) + + var culture_label : Label = GUINode.get_label_from_node(pop_row_panel.get_node(^"./pop_nation")) + if culture_label: + culture_label.set_text_overrun_behavior(TextServer.OVERRUN_TRIM_ELLIPSIS) + _pop_list_culture_labels.push_back(culture_label) + + _pop_list_religion_icons.push_back(GUINode.get_gfx_sprite_texture_from_node(pop_row_panel.get_node(^"./pop_religion"))) + + var location_label : Label = GUINode.get_label_from_node(pop_row_panel.get_node(^"./pop_location")) + if location_label: + location_label.set_text_overrun_behavior(TextServer.OVERRUN_TRIM_ELLIPSIS) + _pop_list_location_labels.push_back(location_label) + + _pop_list_militancy_labels.push_back(GUINode.get_label_from_node(pop_row_panel.get_node(^"./pop_mil"))) + + _pop_list_consciousness_labels.push_back(GUINode.get_label_from_node(pop_row_panel.get_node(^"./pop_con"))) + + _pop_list_ideology_charts.push_back(GUINode.get_gfx_pie_chart_texture_from_node(pop_row_panel.get_node(^"./pop_ideology"))) + + _pop_list_issues_charts.push_back(GUINode.get_gfx_pie_chart_texture_from_node(pop_row_panel.get_node(^"./pop_issues"))) + + _pop_list_unemployment_progressbars.push_back(GUINode.get_progress_bar_from_node(pop_row_panel.get_node(^"./pop_unemployment_bar"))) + + _pop_list_cash_labels.push_back(GUINode.get_label_from_node(pop_row_panel.get_node(^"./pop_cash"))) + + var pop_list_life_needs_progressbar : TextureProgressBar = GUINode.get_progress_bar_from_node(pop_row_panel.get_node(^"./lifeneed_progress")) + if pop_list_life_needs_progressbar: + pop_list_life_needs_progressbar.position += Vector2(1, 0) + _pop_list_life_needs_progressbars.push_back(pop_list_life_needs_progressbar) + + var pop_list_everyday_needs_progressbar : TextureProgressBar = GUINode.get_progress_bar_from_node(pop_row_panel.get_node(^"./eveneed_progress")) + if pop_list_everyday_needs_progressbar: + pop_list_everyday_needs_progressbar.position += Vector2(1, 0) + _pop_list_everyday_needs_progressbars.push_back(pop_list_everyday_needs_progressbar) + + _pop_list_luxury_needs_progressbars.push_back(GUINode.get_progress_bar_from_node(pop_row_panel.get_node(^"./luxneed_progress"))) + + _pop_list_rebel_icons.push_back(GUINode.get_gfx_sprite_texture_from_node(pop_row_panel.get_node(^"./pop_revolt"))) + + _pop_list_social_movement_icons.push_back(GUINode.get_gfx_sprite_texture_from_node(pop_row_panel.get_node(^"./pop_movement_social"))) + + _pop_list_political_movement_icons.push_back(GUINode.get_gfx_sprite_texture_from_node(pop_row_panel.get_node(^"./pop_movement_political"))) + + _pop_list_national_movement_flags.push_back(GUINode.get_gfx_masked_flag_texture_from_node(pop_row_panel.get_node(^"./pop_movement_flag"))) + + _pop_list_size_change_icons.push_back(GUINode.get_gfx_sprite_texture_from_node(pop_row_panel.get_node(^"./growth_indicator"))) + + _pop_list_literacy_labels.push_back(GUINode.get_label_from_node(pop_row_panel.get_node(^"./pop_literacy"))) + func _notification(what : int) -> void: match what: NOTIFICATION_TRANSLATION_CHANGED: @@ -28,7 +380,225 @@ func _on_update_active_nation_management_screen(active_screen : NationManagement func _update_info() -> void: if _active: - # TODO - update UI state + # Province list + _update_province_list() + + # Pop filter buttons, Distributions, Pop list + _update_pops() + show() else: hide() + +func get_growth_icon_index(size_change : int) -> int: + return 1 + int(size_change <= 0) + int(size_change < 0) + +func _update_province_list(scroll_index : int = -1) -> void: + if not _province_listbox: + return + + if scroll_index >= 0: + _province_listbox.set_scroll_index(scroll_index, false) + + _province_list_scroll_index = _province_listbox.get_scroll_index() + + var province_list_info_list : Array[Dictionary] = MenuSingleton.get_population_menu_province_list_rows(_province_list_scroll_index, _province_listbox.get_fixed_visible_items()) + + for index : int in province_list_info_list.size(): + const type_key : StringName = &"type" + const index_key : StringName = &"index" + const name_key : StringName = &"name" + const size_key : StringName = &"size" + const change_key : StringName = &"change" + const selected_key : StringName = &"selected" + const expanded_key : StringName = &"expanded" + const colony_key : StringName = &"colony" + + var province_list_info : Dictionary = province_list_info_list[index] + + var type : MenuSingleton.ProvinceListEntry = province_list_info[type_key] + + if _generate_province_list_row(index, type) != OK: + continue + + if type == MenuSingleton.LIST_ENTRY_NONE or type != _province_list_types[index]: + continue + + _province_list_indices[index] = province_list_info[index_key] + + if _province_list_button_icons[index]: + _province_list_button_icons[index].set_icon_index(1 + int(province_list_info[selected_key])) + + if _province_list_name_labels[index]: + _province_list_name_labels[index].set_text( + GUINode.format_province_name(province_list_info[name_key]) if type == MenuSingleton.LIST_ENTRY_PROVINCE + else province_list_info[name_key] + ) + + if _province_list_size_labels[index]: + _province_list_size_labels[index].set_text(GUINode.int_to_formatted_string(province_list_info[size_key])) + + if _province_list_growth_icons[index]: + _province_list_growth_icons[index].set_icon_index(get_growth_icon_index(province_list_info[change_key])) + + if type == MenuSingleton.LIST_ENTRY_STATE: + if _province_list_colony_buttons[index]: + _province_list_colony_buttons[index].set_visible(province_list_info[colony_key]) + + if _province_list_expand_icons[index]: + _province_list_expand_icons[index].set_icon_index(1 + int(province_list_info[expanded_key])) + + # TODO - set _province_list_national_focus_icons[index] + + # Clear any excess rows + for index : int in range(province_list_info_list.size(), _province_list_types.size()): + _generate_province_list_row(index, MenuSingleton.LIST_ENTRY_NONE) + +func _update_pops() -> void: + _update_pop_filters() + _update_distributions() + _update_pop_list() + +func _update_pop_filters() -> void: + var pop_filter_info_list : Array[Dictionary] = MenuSingleton.get_population_menu_pop_filter_info() + + for index : int in pop_filter_info_list.size(): + const pop_filter_count_key : StringName = &"count" + const pop_filter_change_key : StringName = &"change" + const pop_filter_selected_key : StringName = &"selected" + + var pop_filter_info : Dictionary = pop_filter_info_list[index] + + var pop_filter_button : Button = _pop_filter_buttons[index] + if not pop_filter_button: + continue + pop_filter_button.disabled = pop_filter_info[pop_filter_count_key] <= 0 + + const normal_theme : StringName = &"normal" + const hover_theme : StringName = &"hover" + + if pop_filter_info[pop_filter_selected_key] or pop_filter_button.disabled: + pop_filter_button.get_theme_stylebox(normal_theme).set_texture(_pop_filter_icons[index]) + pop_filter_button.get_theme_stylebox(hover_theme).set_texture(_pop_filter_hover_icons[index]) + else: + pop_filter_button.get_theme_stylebox(normal_theme).set_texture(_pop_filter_selected_icons[index]) + pop_filter_button.get_theme_stylebox(hover_theme).set_texture(_pop_filter_selected_icons[index]) + # TODO - size and promotion/demotion change tooltip + +func _update_distributions(): + const slice_identifier_key : StringName = &"identifier" + const slice_colour_key : StringName = &"colour" + const slice_weight_key : StringName = &"weight" + + var distribution_info_list : Array[Array] = MenuSingleton.get_population_menu_distribution_info() + + for distribution_index : int in distribution_info_list.size(): + var distribution_info : Array[Dictionary] = distribution_info_list[distribution_index] + + if _distribution_charts[distribution_index]: + _distribution_charts[distribution_index].set_slices_array(distribution_info) + + if _distribution_lists[distribution_index]: + distribution_info.sort_custom(func(a : Dictionary, b : Dictionary) -> bool: return a[slice_weight_key] > b[slice_weight_key]) + + var list : GUIListBox = _distribution_lists[distribution_index] + + list.clear_children(distribution_info.size()) + + while list.get_child_count() < distribution_info.size(): + var child : Panel = GUINode.generate_gui_element(_scene_name, "pop_legend_item") + if not child: + break + child.set_mouse_filter(Control.MOUSE_FILTER_IGNORE) + list.add_child(child) + + for list_index in min(list.get_child_count(), distribution_info.size()): + + var child : Panel = list.get_child(list_index) + + var distribution_row : Dictionary = distribution_info[list_index] + + var colour_icon_rect : TextureRect = GUINode.get_texture_rect_from_node(child.get_node(^"./legend_color")) + if colour_icon_rect: + colour_icon_rect.set_modulate(distribution_row[slice_colour_key]) + + var identifier_label : Label = GUINode.get_label_from_node(child.get_node(^"./legend_title")) + if identifier_label: + identifier_label.set_text_overrun_behavior(TextServer.OVERRUN_TRIM_ELLIPSIS) + identifier_label.set_text(distribution_row[slice_identifier_key]) + + var weight_label : Label = GUINode.get_label_from_node(child.get_node(^"./legend_value")) + if weight_label: + weight_label.set_text("%s%%" % GUINode.float_to_formatted_string(distribution_row[slice_weight_key] * 100.0, 1)) + +func _update_pop_list() -> void: + if _pop_list_scrollbar: + var max_scroll_index : int = MenuSingleton.get_population_menu_pop_row_count() - _pop_list_rows.size() + if max_scroll_index > 0: + _pop_list_scrollbar.set_limits(0, max_scroll_index) + _pop_list_scrollbar.show() + else: + _pop_list_scrollbar.set_limits(0, 0) + _pop_list_scrollbar.hide() + + var pop_rows = MenuSingleton.get_population_menu_pop_rows(_pop_list_scroll_index, _pop_list_rows.size()) + + for index : int in _pop_list_rows.size(): + if not _pop_list_rows[index]: + continue + if index < pop_rows.size(): + const pop_size_key : StringName = &"size" + const pop_type_icon_key : StringName = &"pop_type_icon" + const pop_culture_key : StringName = &"culture" + const pop_religion_icon_key : StringName = &"religion_icon" + const pop_location_key : StringName = &"location" + const pop_militancy_key : StringName = &"militancy" + const pop_consciousness_key : StringName = &"consciousness" + const pop_ideology_key : StringName = &"ideology" + const pop_issues_key : StringName = &"issues" + const pop_unemployment_key : StringName = &"unemployment" + const pop_cash_key : StringName = &"cash" + const pop_life_needs_key : StringName = &"life_needs" + const pop_everyday_needs_key : StringName = &"everyday_needs" + const pop_luxury_needs_key : StringName = &"luxury_needs" + const pop_size_change_key : StringName = &"size_change" + const pop_literacy_key : StringName = &"literacy" + + var pop_row : Dictionary = pop_rows[index] + + if _pop_list_size_labels[index]: + _pop_list_size_labels[index].set_text(GUINode.int_to_formatted_string(pop_row[pop_size_key])) + if _pop_list_type_icons[index]: + _pop_list_type_icons[index].set_icon_index(pop_row[pop_type_icon_key]) + if _pop_list_culture_labels[index]: + _pop_list_culture_labels[index].set_text(pop_row[pop_culture_key]) + if _pop_list_religion_icons[index]: + _pop_list_religion_icons[index].set_icon_index(pop_row[pop_religion_icon_key]) + if _pop_list_location_labels[index]: + _pop_list_location_labels[index].set_text(GUINode.format_province_name(pop_row[pop_location_key])) + if _pop_list_militancy_labels[index]: + _pop_list_militancy_labels[index].set_text(GUINode.float_to_formatted_string(pop_row[pop_militancy_key], 2)) + if _pop_list_consciousness_labels[index]: + _pop_list_consciousness_labels[index].set_text(GUINode.float_to_formatted_string(pop_row[pop_consciousness_key], 2)) + if _pop_list_ideology_charts[index]: + _pop_list_ideology_charts[index].set_slices_array(pop_row[pop_ideology_key]) + if _pop_list_issues_charts[index]: + _pop_list_issues_charts[index].set_slices_array(pop_row[pop_issues_key]) + if _pop_list_unemployment_progressbars[index]: + _pop_list_unemployment_progressbars[index].set_value_no_signal(pop_row[pop_unemployment_key]) + if _pop_list_cash_labels[index]: + _pop_list_cash_labels[index].set_text(GUINode.float_to_formatted_string(pop_row[pop_cash_key], 2)) + if _pop_list_life_needs_progressbars[index]: + _pop_list_life_needs_progressbars[index].set_value_no_signal(pop_row[pop_life_needs_key]) + if _pop_list_everyday_needs_progressbars[index]: + _pop_list_everyday_needs_progressbars[index].set_value_no_signal(pop_row[pop_everyday_needs_key]) + if _pop_list_luxury_needs_progressbars[index]: + _pop_list_luxury_needs_progressbars[index].set_value_no_signal(pop_row[pop_luxury_needs_key]) + if _pop_list_size_change_icons[index]: + _pop_list_size_change_icons[index].set_icon_index(get_growth_icon_index(pop_row[pop_size_change_key])) + if _pop_list_literacy_labels[index]: + _pop_list_literacy_labels[index].set_text("%s%%" % GUINode.float_to_formatted_string(pop_row[pop_literacy_key], 2)) + + _pop_list_rows[index].show() + else: + _pop_list_rows[index].hide() diff --git a/game/src/Game/GameSession/ProvinceOverviewPanel.gd b/game/src/Game/GameSession/ProvinceOverviewPanel.gd index 731d02c..13e7111 100644 --- a/game/src/Game/GameSession/ProvinceOverviewPanel.gd +++ b/game/src/Game/GameSession/ProvinceOverviewPanel.gd @@ -166,6 +166,15 @@ func _ready() -> void: _pop_types_piechart = get_gfx_pie_chart_texture_from_nodepath(^"./province_view/province_statistics/workforce_chart") _pop_ideologies_piechart = get_gfx_pie_chart_texture_from_nodepath(^"./province_view/province_statistics/ideology_chart") _pop_cultures_piechart = get_gfx_pie_chart_texture_from_nodepath(^"./province_view/province_statistics/culture_chart") + var population_menu_button : Button = get_button_from_nodepath(^"./province_view/province_statistics/open_popscreen") + if population_menu_button: + population_menu_button.pressed.connect( + func() -> void: + pass + MenuSingleton.population_menu_select_province(_selected_index) + _on_close_button_pressed() + Events.NationManagementScreens.open_nation_management_screen(NationManagement.Screen.POPULATION) + ) _supply_limit_label = get_label_from_nodepath(^"./province_view/province_statistics/supply_limit_label") _cores_overlapping_elements_box = get_gui_overlapping_elements_box_from_nodepath(^"./province_view/province_statistics/core_icons") if _cores_overlapping_elements_box and _cores_overlapping_elements_box.set_gui_child_element_name("province_interface", "province_core") != OK: -- cgit v1.2.3-56-ga3b1