diff options
26 files changed, 2090 insertions, 206 deletions
@@ -8,6 +8,9 @@ For detailed instructions, view the Contributor Quickstart Guide [here](docs/con * [Godot 4.2.1](https://github.com/godotengine/godot/releases/tag/4.2.1-stable) * [scons](https://scons.org/) +> [!WARNING] +> If you are using Arch Linux, do not use the Arch repo package, it is known to break under some GDExtensions, use the official release file. + See [System Requirements](docs/contribution/system-requirements.md). ## Repo Setup diff --git a/extension/deps/openvic-simulation b/extension/deps/openvic-simulation -Subproject c11d262a4d2c987c8cf8e0d4b24929cbe56bb28 +Subproject e286cfef29d7c431ba33cd77283e838e6fba05d diff --git a/extension/src/openvic-extension/classes/GFXMaskedFlagTexture.cpp b/extension/src/openvic-extension/classes/GFXMaskedFlagTexture.cpp index 2c315a9..51a4235 100644 --- a/extension/src/openvic-extension/classes/GFXMaskedFlagTexture.cpp +++ b/extension/src/openvic-extension/classes/GFXMaskedFlagTexture.cpp @@ -34,7 +34,7 @@ Error GFXMaskedFlagTexture::_generate_combined_image() { set_region({ {}, button_image->get_size() }); } - if (mask_image.is_valid() && flag_image.is_valid()) { + if (mask_image.is_valid() && flag_image.is_valid() && flag_image_rect.has_area()) { const Vector2i centre_translation = (mask_image->get_size() - button_image->get_size()) / 2; for (Vector2i combined_image_point { 0, 0 }; combined_image_point.y < button_image->get_height(); ++combined_image_point.y) { for (combined_image_point.x = 0; combined_image_point.x < button_image->get_width(); ++combined_image_point.x) { @@ -46,10 +46,14 @@ Error GFXMaskedFlagTexture::_generate_combined_image() { 0 <= mask_image_point.y && mask_image_point.y < mask_image->get_height() ) { const Color mask_image_colour = mask_image->get_pixelv(mask_image_point); + // Rescale from mask_image to flag_image coordinates. - const Vector2i flag_image_point = mask_image_point * flag_image->get_size() / mask_image->get_size(); + const Vector2i flag_image_point = + flag_image_rect.position + mask_image_point * flag_image_rect.size / mask_image->get_size(); + Color flag_image_colour = flag_image->get_pixelv(flag_image_point); flag_image_colour.a = mask_image_colour.a; + button_image->set_pixelv(combined_image_point, flag_image_colour.blend(overlay_image_colour)); } else { button_image->set_pixelv(combined_image_point, overlay_image_colour); @@ -157,12 +161,12 @@ Error GFXMaskedFlagTexture::set_flag_country_and_type(Country const* new_flag_co GameSingleton* game_singleton = GameSingleton::get_singleton(); ERR_FAIL_NULL_V(game_singleton, FAILED); - const Ref<Image> new_flag_image = game_singleton->get_flag_image(new_flag_country, new_flag_type); - ERR_FAIL_NULL_V(new_flag_image, FAILED); + flag_image_rect = game_singleton->get_flag_sheet_rect(new_flag_country->get_index(), new_flag_type); + ERR_FAIL_COND_V(!flag_image_rect.has_area(), FAILED); flag_country = new_flag_country; flag_type = new_flag_type; - flag_image = new_flag_image; + flag_image = game_singleton->get_flag_sheet_image(); } else { // TODO - use REB flag as default/error flag flag_country = nullptr; diff --git a/extension/src/openvic-extension/classes/GFXMaskedFlagTexture.hpp b/extension/src/openvic-extension/classes/GFXMaskedFlagTexture.hpp index 1e85dd8..9290d5c 100644 --- a/extension/src/openvic-extension/classes/GFXMaskedFlagTexture.hpp +++ b/extension/src/openvic-extension/classes/GFXMaskedFlagTexture.hpp @@ -1,7 +1,7 @@ #pragma once #include <openvic-simulation/country/Country.hpp> -#include <openvic-simulation/interface/GFX.hpp> +#include <openvic-simulation/interface/GFXSprite.hpp> #include "openvic-extension/classes/GFXButtonStateTexture.hpp" @@ -14,6 +14,7 @@ namespace OpenVic { godot::StringName PROPERTY(flag_type); godot::Ref<godot::Image> overlay_image, mask_image, flag_image; + godot::Rect2i flag_image_rect; godot::Ref<godot::ImageTexture> combined_texture; godot::Error _generate_combined_image(); diff --git a/extension/src/openvic-extension/classes/GFXPieChartTexture.hpp b/extension/src/openvic-extension/classes/GFXPieChartTexture.hpp index abeca1e..8683b10 100644 --- a/extension/src/openvic-extension/classes/GFXPieChartTexture.hpp +++ b/extension/src/openvic-extension/classes/GFXPieChartTexture.hpp @@ -2,7 +2,7 @@ #include <godot_cpp/classes/image_texture.hpp> -#include <openvic-simulation/interface/GFX.hpp> +#include <openvic-simulation/interface/GFXSprite.hpp> #include "openvic-extension/utility/Utilities.hpp" @@ -54,12 +54,14 @@ namespace OpenVic { return lhs.first < rhs.first; }); godot_pie_chart_data_t array; - for (auto const& [key, val] : sorted_dist) { + ERR_FAIL_COND_V(array.resize(sorted_dist.size()) != OK, {}); + for (size_t idx = 0; idx < array.size(); ++idx) { + auto const& [key, val] = sorted_dist[idx]; Dictionary sub_dict; sub_dict[_slice_identifier_key()] = Utilities::std_view_to_godot_string(key->get_identifier()); sub_dict[_slice_colour_key()] = Utilities::to_godot_color(key->get_colour()); sub_dict[_slice_weight_key()] = val.to_float(); - array.push_back(sub_dict); + array[idx] = std::move(sub_dict); } return array; } diff --git a/extension/src/openvic-extension/classes/GFXSpriteTexture.hpp b/extension/src/openvic-extension/classes/GFXSpriteTexture.hpp index 34ec405..4e93e62 100644 --- a/extension/src/openvic-extension/classes/GFXSpriteTexture.hpp +++ b/extension/src/openvic-extension/classes/GFXSpriteTexture.hpp @@ -2,7 +2,7 @@ #include <godot_cpp/classes/atlas_texture.hpp> -#include <openvic-simulation/interface/GFX.hpp> +#include <openvic-simulation/interface/GFXSprite.hpp> #include "openvic-extension/classes/GFXButtonStateTexture.hpp" diff --git a/extension/src/openvic-extension/classes/MapMesh.cpp b/extension/src/openvic-extension/classes/MapMesh.cpp index a557105..8db3b84 100644 --- a/extension/src/openvic-extension/classes/MapMesh.cpp +++ b/extension/src/openvic-extension/classes/MapMesh.cpp @@ -92,7 +92,7 @@ bool MapMesh::is_valid_uv_coord(godot::Vector2 const& uv) const { Array MapMesh::_create_mesh_array() const { Array arr; - arr.resize(Mesh::ARRAY_MAX); + ERR_FAIL_COND_V(arr.resize(Mesh::ARRAY_MAX) != OK, {}); const int32_t vertex_count = (subdivide_w + 2) * (subdivide_d + 2); const int32_t indice_count = (subdivide_w + 1) * (subdivide_d + 1) * 6; @@ -103,11 +103,11 @@ Array MapMesh::_create_mesh_array() const { PackedVector2Array uvs; PackedInt32Array indices; - points.resize(vertex_count); - normals.resize(vertex_count); - tangents.resize(vertex_count * 4); - uvs.resize(vertex_count); - indices.resize(indice_count); + ERR_FAIL_COND_V(points.resize(vertex_count) != OK, {}); + ERR_FAIL_COND_V(normals.resize(vertex_count) != OK, {}); + ERR_FAIL_COND_V(tangents.resize(vertex_count * 4) != OK, {}); + ERR_FAIL_COND_V(uvs.resize(vertex_count) != OK, {}); + ERR_FAIL_COND_V(indices.resize(indice_count) != OK, {}); static const Vector3 normal { 0.0f, 1.0f, 0.0f }; const Size2 uv_size { 1.0f + 2.0f * repeat_proportion, 1.0f }; @@ -153,11 +153,11 @@ Array MapMesh::_create_mesh_array() const { thisrow = point_index; } - arr[Mesh::ARRAY_VERTEX] = points; - arr[Mesh::ARRAY_NORMAL] = normals; - arr[Mesh::ARRAY_TANGENT] = tangents; - arr[Mesh::ARRAY_TEX_UV] = uvs; - arr[Mesh::ARRAY_INDEX] = indices; + arr[Mesh::ARRAY_VERTEX] = std::move(points); + arr[Mesh::ARRAY_NORMAL] = std::move(normals); + arr[Mesh::ARRAY_TANGENT] = std::move(tangents); + arr[Mesh::ARRAY_TEX_UV] = std::move(uvs); + arr[Mesh::ARRAY_INDEX] = std::move(indices); return arr; } diff --git a/extension/src/openvic-extension/singletons/AssetManager.cpp b/extension/src/openvic-extension/singletons/AssetManager.cpp index 083d934..6646c8b 100644 --- a/extension/src/openvic-extension/singletons/AssetManager.cpp +++ b/extension/src/openvic-extension/singletons/AssetManager.cpp @@ -13,9 +13,14 @@ using OpenVic::Utilities::godot_to_std_string; using OpenVic::Utilities::std_to_godot_string; void AssetManager::_bind_methods() { - OV_BIND_METHOD(AssetManager::get_image, { "path", "cache", "flip_y" }, DEFVAL(true), DEFVAL(false)); - OV_BIND_METHOD(AssetManager::get_texture, { "path", "flip_y" }, DEFVAL(false)); + OV_BIND_METHOD(AssetManager::get_image, { "path", "load_flags" }, DEFVAL(LOAD_FLAG_CACHE_IMAGE)); + OV_BIND_METHOD(AssetManager::get_texture, { "path", "load_flags" }, DEFVAL(LOAD_FLAG_CACHE_TEXTURE)); OV_BIND_METHOD(AssetManager::get_font, { "name" }); + + BIND_ENUM_CONSTANT(LOAD_FLAG_NONE); + BIND_ENUM_CONSTANT(LOAD_FLAG_CACHE_IMAGE); + BIND_ENUM_CONSTANT(LOAD_FLAG_CACHE_TEXTURE); + BIND_ENUM_CONSTANT(LOAD_FLAG_FLIP_Y); } AssetManager* AssetManager::get_singleton() { @@ -32,55 +37,95 @@ AssetManager::~AssetManager() { _singleton = nullptr; } -Ref<Image> AssetManager::_load_image(StringName const& path) { +Ref<Image> AssetManager::_load_image(StringName const& path, bool flip_y) { GameSingleton* game_singleton = GameSingleton::get_singleton(); ERR_FAIL_NULL_V(game_singleton, nullptr); + const String lookedup_path = std_to_godot_string(game_singleton->get_dataloader().lookup_image_file(godot_to_std_string(path)).string()); ERR_FAIL_COND_V_MSG(lookedup_path.is_empty(), nullptr, vformat("Failed to look up image: %s", path)); + const Ref<Image> image = Utilities::load_godot_image(lookedup_path); ERR_FAIL_COND_V_MSG( - image.is_null() || image->is_empty(), nullptr, vformat("Failed to load image: %s (looked up: %s)", path, lookedup_path) + image.is_null() || image->is_empty(), nullptr, + vformat("Failed to load image: %s (looked up: %s)", path, lookedup_path) ); - return image; -} -AssetManager::image_asset_t* AssetManager::_get_image_asset(StringName const& path, bool flip_y) { - image_asset_map_t::iterator it = image_assets.find(path); - if (it != image_assets.end()) { - return &it.value(); - } - const Ref<Image> image = _load_image(path); - ERR_FAIL_NULL_V(image, nullptr); if (flip_y) { image->flip_y(); } - return &image_assets.emplace(std::move(path), AssetManager::image_asset_t { image, nullptr }).first.value(); + + return image; } -Ref<Image> AssetManager::get_image(StringName const& path, bool cache, bool flip_y) { - if (cache) { - image_asset_t const* asset = _get_image_asset(path, flip_y); - ERR_FAIL_NULL_V(asset, nullptr); - return asset->image; +Ref<Image> AssetManager::get_image(StringName const& path, LoadFlags load_flags) { + /* Check for an existing image entry indicating a previous load attempt, whether successful or not. */ + const image_asset_map_t::iterator it = image_assets.find(path); + if (it != image_assets.end()) { + std::optional<Ref<Image>> const& cached_image = it->second.image; + + if (cached_image.has_value()) { + ERR_FAIL_NULL_V_MSG(*cached_image, nullptr, vformat("Failed to load image previously: %s", path)); + + return *cached_image; + } + } + + /* No load attempt has been made yet, so we try now. */ + const Ref<Image> image = _load_image(path, load_flags & LOAD_FLAG_FLIP_Y); + + if (image.is_valid()) { + if (load_flags & LOAD_FLAG_CACHE_IMAGE) { + image_assets[path].image = image; + } + + return image; } else { - return _load_image(path); + /* Mark both image and texture as failures, regardless of cache flags, in case of future load/creation attempts. */ + image_assets[path] = { nullptr, nullptr }; + + ERR_FAIL_V_MSG(nullptr, vformat("Failed to load image: %s", path)); } } -Ref<ImageTexture> AssetManager::get_texture(StringName const& path, bool flip_y) { - image_asset_t* asset = _get_image_asset(path, flip_y); - ERR_FAIL_NULL_V(asset, nullptr); - if (asset->texture.is_null()) { - asset->texture = ImageTexture::create_from_image(asset->image); - ERR_FAIL_NULL_V_MSG(asset->texture, nullptr, vformat("Failed to turn image into texture: %s", path)); +Ref<ImageTexture> AssetManager::get_texture(StringName const& path, LoadFlags load_flags) { + /* Check for an existing texture entry indicating a previous creation attempt, whether successful or not. */ + const image_asset_map_t::const_iterator it = image_assets.find(path); + if (it != image_assets.end()) { + std::optional<Ref<ImageTexture>> const& cached_texture = it->second.texture; + + if (cached_texture.has_value()) { + ERR_FAIL_NULL_V_MSG(*cached_texture, nullptr, vformat("Failed to create texture previously: %s", path)); + + return *cached_texture; + } + } + + /* No creation attempt has yet been made, so we try now starting by finding the corresponding image. */ + const Ref<Image> image = get_image(path, load_flags); + ERR_FAIL_NULL_V_MSG(image, nullptr, vformat("Failed to load image for texture: %s", path)); + + const Ref<ImageTexture> texture = ImageTexture::create_from_image(image); + + if (texture.is_valid()) { + if (load_flags & LOAD_FLAG_CACHE_TEXTURE) { + image_assets[path].texture = texture; + } + + return texture; + } else { + /* Mark texture as a failure, regardless of cache flags, in case of future creation attempts. */ + image_assets[path].texture = nullptr; + + ERR_FAIL_V_MSG(nullptr, vformat("Failed to create texture: %s", path)); } - return asset->texture; } Ref<Font> AssetManager::get_font(StringName const& name) { const font_map_t::const_iterator it = fonts.find(name); if (it != fonts.end()) { + ERR_FAIL_NULL_V_MSG(it->second, nullptr, vformat("Failed to load font previously: %s", name)); + return it->second; } @@ -89,18 +134,35 @@ Ref<Font> AssetManager::get_font(StringName const& name) { static const String image_ext = ".tga"; const StringName image_path = font_dir + name + image_ext; - const Ref<Image> image = get_image(image_path); - ERR_FAIL_NULL_V_MSG(image, nullptr, vformat("Failed to load font image %s for the font named %s", image_path, name)); + const Ref<Image> image = get_image(image_path, LOAD_FLAG_NONE); + if (image.is_null()) { + fonts.emplace(name, nullptr); + + ERR_FAIL_V_MSG(nullptr, vformat("Failed to load font image %s for the font named %s", image_path, name)); + } + GameSingleton* game_singleton = GameSingleton::get_singleton(); ERR_FAIL_NULL_V(game_singleton, nullptr); + const String font_path = font_dir + name + font_ext; const String lookedup_font_path = std_to_godot_string(game_singleton->get_dataloader().lookup_file(godot_to_std_string(font_path)).string()); + if (lookedup_font_path.is_empty()) { + fonts.emplace(name, nullptr); + + ERR_FAIL_V_MSG(nullptr, vformat("Failed to look up font: %s", font_path)); + } + const Ref<Font> font = Utilities::load_godot_font(lookedup_font_path, image); - ERR_FAIL_NULL_V_MSG( - font, nullptr, - vformat("Failed to load font file %s (looked up: %s) for the font named %s", font_path, lookedup_font_path, name) - ); - fonts.emplace(std::move(name), font); + if (font.is_null()) { + fonts.emplace(name, nullptr); + + ERR_FAIL_V_MSG( + nullptr, + vformat("Failed to load font file %s (looked up: %s) for the font named %s", font_path, lookedup_font_path, name) + ); + } + + fonts.emplace(name, font); return font; } diff --git a/extension/src/openvic-extension/singletons/AssetManager.hpp b/extension/src/openvic-extension/singletons/AssetManager.hpp index 0416e5b..0856d05 100644 --- a/extension/src/openvic-extension/singletons/AssetManager.hpp +++ b/extension/src/openvic-extension/singletons/AssetManager.hpp @@ -5,7 +5,7 @@ #include <godot_cpp/classes/image_texture.hpp> #include <godot_cpp/core/class_db.hpp> -#include <openvic-simulation/interface/GFX.hpp> +#include <openvic-simulation/interface/GFXSprite.hpp> namespace OpenVic { class AssetManager : public godot::Object { @@ -13,9 +13,22 @@ namespace OpenVic { static inline AssetManager* _singleton = nullptr; + public: + enum LoadFlags { + LOAD_FLAG_NONE = 0, + LOAD_FLAG_CACHE_IMAGE = 1 << 0, + LOAD_FLAG_CACHE_TEXTURE = 1 << 1, + LOAD_FLAG_FLIP_Y = 1 << 2 + }; + + constexpr friend LoadFlags operator|(LoadFlags lhs, LoadFlags rhs) { + return static_cast<LoadFlags>(static_cast<int>(lhs) | static_cast<int>(rhs)); + } + + private: struct image_asset_t { - godot::Ref<godot::Image> image; - godot::Ref<godot::ImageTexture> texture; + std::optional<godot::Ref<godot::Image>> image; + std::optional<godot::Ref<godot::ImageTexture>> texture; }; /* deque_ordered_map to avoid the need to reallocate. */ using image_asset_map_t = deque_ordered_map<godot::StringName, image_asset_t>; @@ -24,8 +37,7 @@ namespace OpenVic { image_asset_map_t image_assets; font_map_t fonts; - static godot::Ref<godot::Image> _load_image(godot::StringName const& path); - image_asset_t* _get_image_asset(godot::StringName const& path, bool flip_y); + static godot::Ref<godot::Image> _load_image(godot::StringName const& path, bool flip_y); protected: static void _bind_methods(); @@ -37,16 +49,27 @@ namespace OpenVic { ~AssetManager(); /* Search for and load an image at the specified path relative to the game defines, first checking the AssetManager's - * image cache (if cache is true) in case it has already been loaded, and returning nullptr if image loading fails. */ - godot::Ref<godot::Image> get_image(godot::StringName const& path, bool cache = true, bool flip_y = false); + * image cache in case it has already been loaded, and returning nullptr if image loading fails. If the cache image + * load flag is set then the loaded image will be stored in the AssetManager's image cache for future access; if the + * flip y load flag is set then the image will be flipped vertically before being returned (if the image is already + * in the cache then no flipping will occur, regardless of whether it was orginally flipped or not). */ + godot::Ref<godot::Image> get_image(godot::StringName const& path, LoadFlags load_flags = LOAD_FLAG_CACHE_IMAGE); - /* Create a texture from an image found at the specified path relative to the game defines, fist checking - * AssetManager's texture cache in case it has already been loaded, and returning nullptr if image loading - * or texture creation fails. */ - godot::Ref<godot::ImageTexture> get_texture(godot::StringName const& path, bool flip_y = false); + /* Create a texture from an image found at the specified path relative to the game defines, fist checking the + * AssetManager's texture cache in case it has already been loaded, and returning nullptr if image loading or texture + * creation fails. If the cache image load flag is set then the loaded image will be stored in the AssetManager's + * image cache for future access; if the cache texture load flag is set then the created texture will be stored in the + * AssetManager's texture cache for future access; if the flip y load flag is set then the image will be flipped + * vertically before being used to create the texture (if the image is already in the cache then no flipping will + * occur, regardless of whether it was orginally flipped or not). */ + godot::Ref<godot::ImageTexture> get_texture( + godot::StringName const& path, LoadFlags load_flags = LOAD_FLAG_CACHE_TEXTURE + ); /* Search for and load a font with the specified name from the game defines' font directory, first checking the * AssetManager's font cache in case it has already been loaded, and returning nullptr if font loading fails. */ godot::Ref<godot::Font> get_font(godot::StringName const& name); }; } + +VARIANT_ENUM_CAST(OpenVic::AssetManager::LoadFlags); diff --git a/extension/src/openvic-extension/singletons/GameSingleton.cpp b/extension/src/openvic-extension/singletons/GameSingleton.cpp index 459b2c8..838542d 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" @@ -50,12 +51,17 @@ void GameSingleton::_bind_methods() { OV_BIND_METHOD(GameSingleton::get_map_width); OV_BIND_METHOD(GameSingleton::get_map_height); + OV_BIND_METHOD(GameSingleton::get_map_dims); OV_BIND_METHOD(GameSingleton::get_map_aspect_ratio); OV_BIND_METHOD(GameSingleton::get_terrain_texture); + OV_BIND_METHOD(GameSingleton::get_flag_dims); + OV_BIND_METHOD(GameSingleton::get_flag_sheet_texture); OV_BIND_METHOD(GameSingleton::get_province_shape_image_subdivisions); OV_BIND_METHOD(GameSingleton::get_province_shape_texture); OV_BIND_METHOD(GameSingleton::get_province_colour_texture); + OV_BIND_METHOD(GameSingleton::get_province_names); + OV_BIND_METHOD(GameSingleton::get_mapmode_count); OV_BIND_METHOD(GameSingleton::get_mapmode_identifier); OV_BIND_METHOD(GameSingleton::set_mapmode, { "identifier" }); @@ -115,6 +121,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,13 +129,17 @@ 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); } int32_t GameSingleton::get_province_index_from_uv_coords(Vector2 const& coords) const { - const size_t x_mod_w = UtilityFunctions::fposmod(coords.x, 1.0f) * get_map_width(); - const size_t y_mod_h = UtilityFunctions::fposmod(coords.y, 1.0f) * get_map_height(); - return game_manager.get_map().get_province_index_at(x_mod_w, y_mod_h); + const Vector2 pos = coords.posmod(1.0f) * get_map_dims(); + return game_manager.get_map().get_province_index_at(Utilities::from_godot_ivec2(pos)); } int32_t GameSingleton::get_map_width() const { @@ -139,6 +150,10 @@ int32_t GameSingleton::get_map_height() const { return game_manager.get_map().get_height(); } +Vector2i GameSingleton::get_map_dims() const { + return Utilities::to_godot_ivec2(game_manager.get_map().get_dims()); +} + float GameSingleton::get_map_aspect_ratio() const { return static_cast<float>(get_map_width()) / static_cast<float>(get_map_height()); } @@ -147,19 +162,36 @@ Ref<Texture2DArray> GameSingleton::get_terrain_texture() const { return terrain_texture; } -Ref<Image> GameSingleton::get_flag_image(Country const* country, StringName const& flag_type) const { - ERR_FAIL_NULL_V(country, nullptr); - const typename decltype(flag_image_map)::const_iterator it = flag_image_map.find(country); +Ref<Image> GameSingleton::get_flag_sheet_image() const { + return flag_sheet_image; +} + +Ref<ImageTexture> GameSingleton::get_flag_sheet_texture() const { + return flag_sheet_texture; +} + +int32_t GameSingleton::get_flag_sheet_index(int32_t country_index, godot::StringName const& flag_type) const { ERR_FAIL_COND_V_MSG( - it == flag_image_map.end(), nullptr, - vformat("Failed to find flags for country: %s", std_view_to_godot_string(country->get_identifier())) + country_index < 0 || country_index >= game_manager.get_country_manager().get_country_count(), -1, + vformat("Invalid country index: %d", country_index) ); - const typename decltype(it->second)::const_iterator it2 = it->second.find(flag_type); + + const typename decltype(flag_type_index_map)::const_iterator it = flag_type_index_map.find(flag_type); + ERR_FAIL_COND_V_MSG(it == flag_type_index_map.end(), -1, vformat("Invalid flag type %s", flag_type)); + + return flag_type_index_map.size() * country_index + it->second; +} + +Rect2i GameSingleton::get_flag_sheet_rect(int32_t flag_index) const { ERR_FAIL_COND_V_MSG( - it2 == it->second.end(), nullptr, - vformat("Failed to find %s flag for country: %s", flag_type, std_view_to_godot_string(country->get_identifier())) + flag_index < 0 || flag_index >= flag_sheet_count, {}, vformat("Invalid flag sheet index: %d", flag_index) ); - return it2->second; + + return { Vector2i { flag_index % flag_sheet_dims.x, flag_index / flag_sheet_dims.x } * flag_dims, flag_dims }; +} + +Rect2i GameSingleton::get_flag_sheet_rect(int32_t country_index, godot::StringName const& flag_type) const { + return get_flag_sheet_rect(get_flag_sheet_index(country_index, flag_type)); } Vector2i GameSingleton::get_province_shape_image_subdivisions() const { @@ -185,11 +217,11 @@ Error GameSingleton::_update_colour_image() { static constexpr int32_t colour_image_width = PROVINCE_INDEX_SQRT * sizeof(Mapmode::base_stripe_t) / sizeof(colour_argb_t); /* Province count + null province, rounded up to next multiple of PROVINCE_INDEX_SQRT. * Rearranged from: (map.get_province_count() + 1) + (PROVINCE_INDEX_SQRT - 1) */ - static const int32_t colour_image_height = (map.get_province_count() + PROVINCE_INDEX_SQRT) / PROVINCE_INDEX_SQRT; + const int32_t colour_image_height = (map.get_province_count() + PROVINCE_INDEX_SQRT) / PROVINCE_INDEX_SQRT; static PackedByteArray colour_data_array; - static const int64_t colour_data_array_size = colour_image_width * colour_image_height * sizeof(colour_argb_t); - colour_data_array.resize(colour_data_array_size); + const int64_t colour_data_array_size = colour_image_width * colour_image_height * sizeof(colour_argb_t); + ERR_FAIL_COND_V(colour_data_array.resize(colour_data_array_size) != OK, FAILED); Error err = OK; if (!map.generate_mapmode_colours(mapmode_index, colour_data_array.ptrw())) { @@ -213,6 +245,39 @@ Error GameSingleton::_update_colour_image() { return err; } +TypedArray<Dictionary> GameSingleton::get_province_names() const { + static const StringName identifier_key = "identifier"; + static const StringName position_key = "position"; + static const StringName rotation_key = "rotation"; + static const StringName scale_key = "scale"; + + TypedArray<Dictionary> ret; + ERR_FAIL_COND_V(ret.resize(game_manager.get_map().get_province_count()) != OK, {}); + + for (int32_t index = 0; index < game_manager.get_map().get_province_count(); ++index) { + Province const& province = game_manager.get_map().get_provinces()[index]; + + Dictionary province_dict; + + province_dict[identifier_key] = std_view_to_godot_string(province.get_identifier()); + province_dict[position_key] = Utilities::to_godot_fvec2(province.get_text_position()) / get_map_dims(); + + const float rotation = province.get_text_rotation().to_float(); + if (rotation != 0.0f) { + province_dict[rotation_key] = rotation; + } + + const float scale = province.get_text_scale().to_float(); + if (scale != 1.0f) { + province_dict[scale_key] = scale; + } + + ret[index] = std::move(province_dict); + } + + return ret; +} + int32_t GameSingleton::get_mapmode_count() const { return game_manager.get_map().get_mapmode_count(); } @@ -275,19 +340,25 @@ Error GameSingleton::_load_map_images() { } Map::shape_pixel_t const* province_shape_data = game_manager.get_map().get_province_shape_image().data(); + const Vector2i divided_dims = province_dims / image_subdivisions; + const int64_t subdivision_width = divided_dims.x * sizeof(Map::shape_pixel_t); + const int64_t subdivision_size = subdivision_width * divided_dims.y; + TypedArray<Image> province_shape_images; - province_shape_images.resize(image_subdivisions.x * image_subdivisions.y); + ERR_FAIL_COND_V(province_shape_images.resize(image_subdivisions.x * image_subdivisions.y) != OK, FAILED); + + PackedByteArray index_data_array; + ERR_FAIL_COND_V(index_data_array.resize(subdivision_size) != OK, FAILED); + for (int32_t v = 0; v < image_subdivisions.y; ++v) { for (int32_t u = 0; u < image_subdivisions.x; ++u) { - PackedByteArray index_data_array; - index_data_array.resize(divided_dims.x * divided_dims.y * sizeof(Map::shape_pixel_t)); for (int32_t y = 0; y < divided_dims.y; ++y) { memcpy( - index_data_array.ptrw() + y * divided_dims.x * sizeof(Map::shape_pixel_t), + index_data_array.ptrw() + y * subdivision_width, province_shape_data + (v * divided_dims.y + y) * province_dims.x + u * divided_dims.x, - divided_dims.x * sizeof(Map::shape_pixel_t) + subdivision_width ); } @@ -322,7 +393,7 @@ Error GameSingleton::_load_terrain_variants() { AssetManager* asset_manager = AssetManager::get_singleton(); ERR_FAIL_NULL_V(asset_manager, FAILED); // Load the terrain texture sheet and prepare to slice it up - Ref<Image> terrain_sheet = asset_manager->get_image(terrain_texturesheet_path); + Ref<Image> terrain_sheet = asset_manager->get_image(terrain_texturesheet_path, AssetManager::LOAD_FLAG_NONE); ERR_FAIL_NULL_V_MSG(terrain_sheet, FAILED, vformat("Failed to load terrain texture sheet: %s", terrain_texturesheet_path)); static constexpr int32_t SHEET_DIMS = 8, SHEET_SIZE = SHEET_DIMS * SHEET_DIMS; @@ -337,6 +408,7 @@ Error GameSingleton::_load_terrain_variants() { const int32_t slice_size = sheet_width / SHEET_DIMS; TypedArray<Image> terrain_images; + ERR_FAIL_COND_V(terrain_images.resize(SHEET_SIZE + 1) != OK, FAILED); { /* This is a placeholder image so that we don't have to branch to avoid looking up terrain index 0 (water). * It should never appear in game, and so is bright red to to make it obvious if it slips through. */ @@ -344,72 +416,136 @@ Error GameSingleton::_load_terrain_variants() { { 1.0f, 0.0f, 0.0f }, slice_size, slice_size, terrain_sheet->get_format() ); ERR_FAIL_NULL_V_EDMSG(water_image, FAILED, "Failed to create water terrain image"); - terrain_images.append(water_image); + terrain_images[0] = water_image; } - Error err = OK; + for (int32_t idx = 0; idx < SHEET_SIZE; ++idx) { const Rect2i slice { idx % SHEET_DIMS * slice_size, idx / SHEET_DIMS * slice_size, slice_size, slice_size }; const Ref<Image> terrain_image = terrain_sheet->get_region(slice); - if (terrain_image.is_null() || terrain_image->is_empty()) { - UtilityFunctions::push_error( - "Failed to extract terrain texture slice ", slice, " from ", terrain_texturesheet_path - ); - err = FAILED; - } - terrain_images.append(terrain_image); + + ERR_FAIL_COND_V_MSG(terrain_image.is_null() || terrain_image->is_empty(), FAILED, vformat( + "Failed to extract terrain texture slice %s from %s", slice, terrain_texturesheet_path + )); + + terrain_images[idx + 1] = terrain_image; } terrain_texture.instantiate(); ERR_FAIL_COND_V_MSG( terrain_texture->create_from_images(terrain_images) != OK, FAILED, "Failed to create terrain texture array!" ); - return err; + return OK; } -Error GameSingleton::_load_flag_images() { - ERR_FAIL_COND_V_MSG(!flag_image_map.empty(), FAILED, "Flag images have already been loaded!"); +const Vector2i GameSingleton::flag_dims { 128, 64 }; + +Error GameSingleton::_load_flag_sheet() { + ERR_FAIL_COND_V_MSG( + flag_sheet_image.is_valid() || flag_sheet_texture.is_valid(), FAILED, + "Flag sheet image and/or texture has already been generated!" + ); GovernmentTypeManager const& government_type_manager = game_manager.get_politics_manager().get_government_type_manager(); ERR_FAIL_COND_V_MSG( - !government_type_manager.government_types_are_locked(), FAILED, - "Cannot load flag images before government types are locked!" + government_type_manager.get_flag_types().empty() || !government_type_manager.government_types_are_locked(), FAILED, + "Cannot load flag images if flag types are empty or government types are not locked!" ); CountryManager const& country_manager = game_manager.get_country_manager(); ERR_FAIL_COND_V_MSG( - !country_manager.countries_are_locked(), FAILED, "Cannot load flag images before countries are locked!" + country_manager.countries_empty() || !country_manager.countries_are_locked(), FAILED, + "Cannot load flag images if countries are empty or not locked!" ); AssetManager* asset_manager = AssetManager::get_singleton(); ERR_FAIL_NULL_V(asset_manager, FAILED); - static const String flag_directory = "gfx/flags/"; - static const String flag_separator = "_"; - static const String flag_extension = ".tga"; - - std::vector<StringName> flag_types; + /* Generate flag type - index lookup map */ + flag_type_index_map.clear(); for (std::string const& type : government_type_manager.get_flag_types()) { - flag_types.emplace_back(std_to_godot_string_name(type)); + flag_type_index_map.emplace(std_to_godot_string_name(type), static_cast<int32_t>(flag_type_index_map.size())); } - flag_image_map.reserve(country_manager.get_countries().size()); + flag_sheet_count = country_manager.get_countries().size() * flag_type_index_map.size(); + + std::vector<Ref<Image>> flag_images; + flag_images.reserve(flag_sheet_count); + + static constexpr Image::Format flag_format = Image::FORMAT_RGB8; Error ret = OK; for (Country const& country : country_manager.get_countries()) { - ordered_map<StringName, Ref<Image>>& flag_images = flag_image_map[&country]; - flag_images.reserve(flag_types.size()); const String country_name = std_view_to_godot_string(country.get_identifier()); - for (StringName const& flag_type : flag_types) { + + for (auto const& [flag_type, flag_type_index] : flag_type_index_map) { + static const String flag_directory = "gfx/flags/"; + static const String flag_separator = "_"; + static const String flag_extension = ".tga"; + const StringName flag_path = flag_directory + country_name + (flag_type.is_empty() ? "" : flag_separator + flag_type) + flag_extension; - const Ref<Image> flag_image = asset_manager->get_image(flag_path); + + /* Do not cache flag image, they should be freed after the flag sheet has been generated. */ + const Ref<Image> flag_image = asset_manager->get_image(flag_path, AssetManager::LOAD_FLAG_NONE); + if (flag_image.is_valid()) { - flag_images.emplace(flag_type, flag_image); + + if (flag_image->get_format() != flag_format) { + flag_image->convert(flag_format); + } + + if (flag_image->get_size() != flag_dims) { + if (flag_image->get_width() > flag_dims.x || flag_image->get_height() > flag_dims.y) { + UtilityFunctions::push_warning( + "Flag image ", flag_path, " (", flag_image->get_size(), ") is larger than the sheet flag size (", + flag_dims, ")" + ); + } + + flag_image->resize(flag_dims.x, flag_dims.y, Image::INTERPOLATE_NEAREST); + } } else { UtilityFunctions::push_error("Failed to load flag image: ", flag_path); ret = FAILED; } + + /* Add flag_image to the vector even if it's null to ensure each flag has the right index. */ + flag_images.push_back(flag_image); } } + + ERR_FAIL_COND_V(flag_images.size() != flag_sheet_count, FAILED); + + /* Calculate the width that will make the sheet as close to a square as possible (taking flag dimensions into account.) */ + flag_sheet_dims.x = (fixed_point_t { static_cast<int32_t>(flag_images.size()) } * flag_dims.y / flag_dims.x).sqrt().ceil(); + + /* Calculated corresponding height (rounded up). */ + flag_sheet_dims.y = (static_cast<int32_t>(flag_images.size()) + flag_sheet_dims.x - 1 ) / flag_sheet_dims.x; + + const Vector2i sheet_dims = flag_sheet_dims * flag_dims; + + flag_sheet_image = Image::create(sheet_dims.x, sheet_dims.y, false, flag_format); + ERR_FAIL_NULL_V_MSG(flag_sheet_image, FAILED, "Failed to create flag sheet image!"); + + static const Rect2i flag_rect { { 0, 0 }, flag_dims }; + + /* Fill the flag sheet with the flag images. */ + for (int32_t index = 0; index < flag_images.size(); ++index) { + Ref<Image> const& flag_image = flag_images[index]; + + const Vector2i sheet_pos = Vector2i { index % flag_sheet_dims.x, index / flag_sheet_dims.x } * flag_dims; + + if (flag_image.is_valid()) { + flag_sheet_image->blit_rect(flag_image, flag_rect, sheet_pos); + } else { + static const Color error_colour { 1.0f, 0.0f, 1.0f, 1.0f }; /* Magenta */ + + flag_sheet_image->fill_rect({ sheet_pos, flag_dims }, error_colour); + } + } + + flag_sheet_texture = ImageTexture::create_from_image(flag_sheet_image); + ERR_FAIL_NULL_V_MSG(flag_sheet_texture, FAILED, "Failed to create flag sheet texture!"); + return ret; } @@ -433,8 +569,8 @@ Error GameSingleton::load_defines_compatibility_mode(PackedStringArray const& fi UtilityFunctions::push_error("Failed to load terrain variants!"); err = FAILED; } - if (_load_flag_images() != OK) { - UtilityFunctions::push_error("Failed to load flag textures!"); + if (_load_flag_sheet() != OK) { + UtilityFunctions::push_error("Failed to load flag sheet!"); err = FAILED; } if (_load_map_images() != OK) { diff --git a/extension/src/openvic-extension/singletons/GameSingleton.hpp b/extension/src/openvic-extension/singletons/GameSingleton.hpp index e84e366..f2b88ac 100644 --- a/extension/src/openvic-extension/singletons/GameSingleton.hpp +++ b/extension/src/openvic-extension/singletons/GameSingleton.hpp @@ -22,7 +22,13 @@ namespace OpenVic { godot::Ref<godot::ImageTexture> province_colour_texture; Mapmode::index_t mapmode_index = 0; godot::Ref<godot::Texture2DArray> terrain_texture; - ordered_map<Country const*, ordered_map<godot::StringName, godot::Ref<godot::Image>>> flag_image_map; + + static const godot::Vector2i PROPERTY(flag_dims); /* The size in pixels of an individual flag. */ + int32_t flag_sheet_count = 0; /* The number of flags in the flag sheet. */ + godot::Vector2i flag_sheet_dims; /* The size of the flag sheet in flags, rather than pixels. */ + godot::Ref<godot::Image> flag_sheet_image; + godot::Ref<godot::ImageTexture> flag_sheet_texture; + ordered_map<godot::StringName, int32_t> flag_type_index_map; static godot::StringName const& _signal_gamestate_updated(); static godot::StringName const& _signal_province_selected(); @@ -30,7 +36,7 @@ namespace OpenVic { godot::Error _load_map_images(); godot::Error _load_terrain_variants(); - godot::Error _load_flag_images(); + godot::Error _load_flag_sheet(); /* Generate the province_colour_texture from the current mapmode. */ godot::Error _update_colour_image(); @@ -61,14 +67,20 @@ namespace OpenVic { int32_t get_map_width() const; int32_t get_map_height() const; + godot::Vector2i get_map_dims() const; float get_map_aspect_ratio() const; /* The cosmetic terrain textures stored in a Texture2DArray. */ godot::Ref<godot::Texture2DArray> get_terrain_texture() const; - /* The flag image corresponding to the requested country / flag_type - * combination, or nullptr if no such flag can be found. */ - godot::Ref<godot::Image> get_flag_image(Country const* country, godot::StringName const& flag_type) const; + godot::Ref<godot::Image> get_flag_sheet_image() const; + godot::Ref<godot::ImageTexture> get_flag_sheet_texture() const; + + /* The index of the flag in the flag sheet corresponding to the requested country / flag_type + * combination, or -1 if no such flag can be found. */ + int32_t get_flag_sheet_index(int32_t country_index, godot::StringName const& flag_type) const; + godot::Rect2i get_flag_sheet_rect(int32_t flag_index) const; + godot::Rect2i get_flag_sheet_rect(int32_t country_index, godot::StringName const& flag_type) const; /* Number of (vertical, horizontal) subdivisions the province shape image * was split into when making the province_shape_texture to ensure no @@ -83,6 +95,8 @@ namespace OpenVic { /* The base and stripe colours for each province. */ godot::Ref<godot::ImageTexture> get_province_colour_texture() const; + godot::TypedArray<godot::Dictionary> get_province_names() const; + int32_t get_mapmode_count() const; godot::String get_mapmode_identifier(int32_t index) const; godot::Error set_mapmode(godot::String const& identifier); diff --git a/extension/src/openvic-extension/singletons/LoadLocalisation.cpp b/extension/src/openvic-extension/singletons/LoadLocalisation.cpp index 8860105..16ebe57 100644 --- a/extension/src/openvic-extension/singletons/LoadLocalisation.cpp +++ b/extension/src/openvic-extension/singletons/LoadLocalisation.cpp @@ -111,7 +111,7 @@ Error LoadLocalisation::load_localisation_dir(String const& dir_path) const { ERR_FAIL_COND_V_MSG( !DirAccess::dir_exists_absolute(dir_path), FAILED, vformat("Localisation directory does not exist: %s", dir_path) ); - PackedStringArray const dirs = DirAccess::get_directories_at(dir_path); + const PackedStringArray dirs = DirAccess::get_directories_at(dir_path); ERR_FAIL_COND_V_MSG( dirs.size() < 1, FAILED, vformat("Localisation directory does not contain any sub-directories: %s", dir_path) ); diff --git a/extension/src/openvic-extension/singletons/MenuSingleton.cpp b/extension/src/openvic-extension/singletons/MenuSingleton.cpp index 52fb6af..993549c 100644 --- a/extension/src/openvic-extension/singletons/MenuSingleton.cpp +++ b/extension/src/openvic-extension/singletons/MenuSingleton.cpp @@ -15,6 +15,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 +48,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() { @@ -143,11 +210,17 @@ Dictionary MenuSingleton::get_province_info_from_index(int32_t index) const { std::vector<Country const*> const& cores = province->get_cores(); if (!cores.empty()) { PackedStringArray cores_array; - cores_array.resize(cores.size()); - for (size_t idx = 0; idx < cores.size(); ++idx) { - cores_array[idx] = std_view_to_godot_string(cores[idx]->get_identifier()); + if (cores_array.resize(cores.size()) == OK) { + for (size_t idx = 0; idx < cores.size(); ++idx) { + cores_array[idx] = std_view_to_godot_string(cores[idx]->get_identifier()); + } + ret[province_info_cores_key] = std::move(cores_array); + } else { + UtilityFunctions::push_error( + "Failed to resize cores array to the correct size (", static_cast<int64_t>(cores.size()), ") for province ", + std_view_to_godot_string(province->get_identifier()) + ); } - ret[province_info_cores_key] = cores_array; } static const StringName building_info_level_key = "level"; @@ -161,20 +234,26 @@ Dictionary MenuSingleton::get_province_info_from_index(int32_t index) const { /* This system relies on the province buildings all being present in the right order. It will have to * be changed if we want to support variable combinations and permutations of province buildings. */ TypedArray<Dictionary> buildings_array; - buildings_array.resize(buildings.size()); - for (size_t idx = 0; idx < buildings.size(); ++idx) { - BuildingInstance const& building = buildings[idx]; - - Dictionary building_dict; - building_dict[building_info_level_key] = static_cast<int32_t>(building.get_level()); - building_dict[building_info_expansion_state_key] = static_cast<int32_t>(building.get_expansion_state()); - building_dict[building_info_start_date_key] = std_to_godot_string(building.get_start_date().to_string()); - building_dict[building_info_end_date_key] = std_to_godot_string(building.get_end_date().to_string()); - building_dict[building_info_expansion_progress_key] = building.get_expansion_progress(); - - buildings_array[idx] = building_dict; + if (buildings_array.resize(buildings.size()) == OK) { + for (size_t idx = 0; idx < buildings.size(); ++idx) { + BuildingInstance const& building = buildings[idx]; + + Dictionary building_dict; + building_dict[building_info_level_key] = static_cast<int32_t>(building.get_level()); + building_dict[building_info_expansion_state_key] = static_cast<int32_t>(building.get_expansion_state()); + building_dict[building_info_start_date_key] = std_to_godot_string(building.get_start_date().to_string()); + building_dict[building_info_end_date_key] = std_to_godot_string(building.get_end_date().to_string()); + building_dict[building_info_expansion_progress_key] = building.get_expansion_progress(); + + buildings_array[idx] = std::move(building_dict); + } + ret[province_info_buildings_key] = std::move(buildings_array); + } else { + UtilityFunctions::push_error( + "Failed to resize buildings array to the correct size (", static_cast<int64_t>(buildings.size()), + ") for province ", std_view_to_godot_string(province->get_identifier()) + ); } - ret[province_info_buildings_key] = buildings_array; } return ret; } 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 <godot_cpp/classes/image.hpp> +#include <openvic-simulation/pop/Pop.hpp> #include <openvic-simulation/types/OrderedContainers.hpp> 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<country_entry_t, state_entry_t, province_entry_t>; + + std::vector<province_list_entry_t> 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<PopType const*, pop_filter_t> pop_filters; + + static constexpr int32_t DISTRIBUTION_COUNT = 6; + /* Distributions: + * - Workforce (PopType) + * - Religion + * - Ideology + * - Nationality (Culture) + * - Issues + * - Vote */ + std::array<fixed_point_map_t<HasIdentifierAndColour const*>, 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<Pop const*> 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<godot::Dictionary> 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<bool(Pop const*, Pop const*)>; + 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<godot::Dictionary> 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<godot::Dictionary> 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<godot::Array> 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..a598ceb --- /dev/null +++ b/extension/src/openvic-extension/singletons/PopulationMenu.cpp @@ -0,0 +1,729 @@ +#include "MenuSingleton.hpp" + +#include <godot_cpp/variant/utility_functions.hpp> + +#include <openvic-simulation/GameManager.hpp> + +#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<Dictionary> MenuSingleton::get_population_menu_province_list_rows(int32_t start, int32_t count) const { + // TODO - remove when country population is used instead of total map population + ERR_FAIL_NULL_V(game_manager, {}); + + 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; + + // TODO - remove when country population is used instead of total map population + const Pop::pop_size_t total_map_population; + + /* This is the index among all entries, not just visible ones unlike start and count. */ + int32_t index = 0; + + bool is_expanded = true; + + TypedArray<Dictionary> 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] = total_map_population; + 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] = state_entry.state.calculate_total_population(); + 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, game_manager->get_map().get_total_map_population() }; + + 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_t::state_entry_t>(&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_t::province_entry_t>(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_t::province_entry_t>(&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<HasIdentifierAndColour const*>& 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<HasIdentifierAndColour>(pop->get_ideologies() * static_cast<int32_t>(pop->get_size())); + population_menu.distributions[3][&pop->get_culture()] += pop->get_size(); + population_menu.distributions[4] += + cast_map<HasIdentifierAndColour>(pop->get_issues() * static_cast<int32_t>(pop->get_size())); + population_menu.distributions[5] += + cast_map<HasIdentifierAndColour>(pop->get_votes() * static_cast<int32_t>(pop->get_size())); + } + + for (fixed_point_map_t<HasIdentifierAndColour const*>& 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<int32_t>(sort_key), static_cast<int32_t>(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<Dictionary> 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<Dictionary> array; + ERR_FAIL_COND_V(array.resize(count) != OK, {}); + + for (int32_t idx = 0; idx < count; ++idx) { + Pop const* pop = population_menu.filtered_pops[start + 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[idx] = std::move(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; + ERR_FAIL_COND_V(array.resize(population_menu.pop_filters.size()) != OK, {}); + + for (int32_t idx = 0; idx < array.size(); ++idx) { + array[idx] = population_menu.pop_filters.data()[idx].first->get_sprite(); + } + + return array; +} + +TypedArray<Dictionary> 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<Dictionary> array; + ERR_FAIL_COND_V(array.resize(population_menu.pop_filters.size()) != OK, {}); + + for (int32_t idx = 0; idx < array.size(); ++idx) { + population_menu_t::pop_filter_t const& filter = population_menu.pop_filters.data()[idx].second; + + 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[idx] = std::move(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 { + constexpr std::array<char const*, population_menu_t::DISTRIBUTION_COUNT> NAMES { + /* Workforce (PopType) */ "WORKFORCE_DISTTITLE", + /* Religion */ "RELIGION_DISTTITLE", + /* Ideology */ "IDEOLOGY_DISTTITLE", + /* Nationality (Culture) */ "NATIONALITY_DISTTITLE", + /* Issues */ "DOMINANT_ISSUES_DISTTITLE", + /* Vote */ "ELECTORATE_DISTTITLE" + }; + + PackedStringArray array; + ERR_FAIL_COND_V(array.resize(NAMES.size()) != OK, {}); + + for (int32_t idx = 0; idx < array.size(); ++idx) { + array[idx] = NAMES[idx]; + } + + return array; + }(); + + return distribution_names; +} + +TypedArray<Array> MenuSingleton::get_population_menu_distribution_info() const { + TypedArray<Array> array; + ERR_FAIL_COND_V(array.resize(population_menu.distributions.size()) != OK, {}); + + for (int32_t idx = 0; idx < array.size(); ++idx) { + array[idx] = GFXPieChartTexture::distribution_to_slices_array(population_menu.distributions[idx]); + } + + return array; +} diff --git a/extension/src/openvic-extension/utility/UITools.cpp b/extension/src/openvic-extension/utility/UITools.cpp index 4af2b74..510c6da 100644 --- a/extension/src/openvic-extension/utility/UITools.cpp +++ b/extension/src/openvic-extension/utility/UITools.cpp @@ -187,10 +187,12 @@ static bool generate_icon(generate_gui_args_t&& args) { GFX::ProgressBar const* progress_bar = icon.get_sprite()->cast_to<GFX::ProgressBar>(); + using enum AssetManager::LoadFlags; + Ref<ImageTexture> back_texture; if (!progress_bar->get_back_texture_file().empty()) { const StringName back_texture_file = std_view_to_godot_string_name(progress_bar->get_back_texture_file()); - back_texture = args.asset_manager.get_texture(back_texture_file, true); + back_texture = args.asset_manager.get_texture(back_texture_file, LOAD_FLAG_CACHE_TEXTURE | LOAD_FLAG_FLIP_Y); if (back_texture.is_null()) { UtilityFunctions::push_error( "Failed to load progress bar sprite back texture ", back_texture_file, " for GUI icon ", icon_name @@ -221,11 +223,14 @@ static bool generate_icon(generate_gui_args_t&& args) { Ref<ImageTexture> progress_texture; if (!progress_bar->get_progress_texture_file().empty()) { - const StringName progress_texture_file = std_view_to_godot_string_name(progress_bar->get_progress_texture_file()); - progress_texture = args.asset_manager.get_texture(progress_texture_file, true); + const StringName progress_texture_file = + std_view_to_godot_string_name(progress_bar->get_progress_texture_file()); + progress_texture = + args.asset_manager.get_texture(progress_texture_file, LOAD_FLAG_CACHE_TEXTURE | LOAD_FLAG_FLIP_Y); if (progress_texture.is_null()) { UtilityFunctions::push_error( - "Failed to load progress bar sprite progress texture ", progress_texture_file, " for GUI icon ", icon_name + "Failed to load progress bar sprite progress texture ", progress_texture_file, " for GUI icon ", + icon_name ); ret = false; } @@ -237,7 +242,8 @@ static bool generate_icon(generate_gui_args_t&& args) { ); if (progress_texture.is_null()) { UtilityFunctions::push_error( - "Failed to generate progress bar sprite ", progress_colour, " progress texture for GUI icon ", icon_name + "Failed to generate progress bar sprite ", progress_colour, " progress texture for GUI icon ", + icon_name ); ret = false; } @@ -252,7 +258,9 @@ static bool generate_icon(generate_gui_args_t&& args) { } // TODO - work out why progress bar is missing bottom border pixel (e.g. province building expansion bar) - godot_progress_bar->set_custom_minimum_size(Utilities::to_godot_fvec2(static_cast<fvec2_t>(progress_bar->get_size()))); + godot_progress_bar->set_custom_minimum_size( + Utilities::to_godot_fvec2(static_cast<fvec2_t>(progress_bar->get_size())) + ); args.result = godot_progress_bar; } else if (icon.get_sprite()->is_type<GFX::PieChart>()) { @@ -286,7 +294,9 @@ static bool generate_icon(generate_gui_args_t&& args) { const float rotation = icon.get_rotation(); if (rotation != 0.0f) { args.result->set_position( - args.result->get_position() - args.result->get_custom_minimum_size().height * Vector2 { sin(rotation), cos(rotation) - 1.0f } + args.result->get_position() - args.result->get_custom_minimum_size().height * Vector2 { + sin(rotation), cos(rotation) - 1.0f + } ); args.result->set_rotation(-rotation); } diff --git a/extension/src/openvic-extension/utility/UITools.hpp b/extension/src/openvic-extension/utility/UITools.hpp index 6092853..566318a 100644 --- a/extension/src/openvic-extension/utility/UITools.hpp +++ b/extension/src/openvic-extension/utility/UITools.hpp @@ -2,7 +2,7 @@ #include <godot_cpp/classes/control.hpp> -#include <openvic-simulation/interface/GFX.hpp> +#include <openvic-simulation/interface/GFXSprite.hpp> #include <openvic-simulation/interface/GUI.hpp> namespace OpenVic::UITools { diff --git a/extension/src/openvic-extension/utility/Utilities.cpp b/extension/src/openvic-extension/utility/Utilities.cpp index 7450212..4389e95 100644 --- a/extension/src/openvic-extension/utility/Utilities.cpp +++ b/extension/src/openvic-extension/utility/Utilities.cpp @@ -80,7 +80,8 @@ static Ref<Image> load_dds_image(String const& path) { ); PackedByteArray pixels; - pixels.resize(size); + ERR_FAIL_COND_V(pixels.resize(size) != OK, nullptr); + /* Index offset used to control whether we are reading */ const size_t rb_idx = 2 * needs_bgr_to_rgb; uint8_t const* ptr = static_cast<uint8_t const*>(texture.data()); diff --git a/extension/src/openvic-extension/utility/Utilities.hpp b/extension/src/openvic-extension/utility/Utilities.hpp index f39be3e..f7a0d67 100644 --- a/extension/src/openvic-extension/utility/Utilities.hpp +++ b/extension/src/openvic-extension/utility/Utilities.hpp @@ -41,11 +41,15 @@ namespace OpenVic::Utilities { return { colour.redf(), colour.greenf(), colour.bluef(), colour.alphaf() }; } - _FORCE_INLINE_ godot::Vector2i to_godot_ivec2(ivec2_t vec) { + _FORCE_INLINE_ godot::Vector2i to_godot_ivec2(ivec2_t const& vec) { return { vec.x, vec.y }; } - _FORCE_INLINE_ godot::Vector2 to_godot_fvec2(fvec2_t vec) { + _FORCE_INLINE_ godot::Vector2 to_godot_fvec2(fvec2_t const& vec) { + return { vec.x, vec.y }; + } + + _FORCE_INLINE_ ivec2_t from_godot_ivec2(godot::Vector2i const& vec) { return { vec.x, vec.y }; } diff --git a/game/project.godot b/game/project.godot index 9d005b0..ac627a3 100644 --- a/game/project.godot +++ b/game/project.godot @@ -58,6 +58,10 @@ window/per_pixel_transparency/allowed=true enabled=PackedStringArray("res://addons/keychain/plugin.cfg", "res://addons/openvic-plugin/plugin.cfg") +[filesystem] + +import/blender/enabled=false + [gui] theme/custom="res://assets/graphics/theme/default_theme.tres" diff --git a/game/src/Game/GameSession/MapText.gd b/game/src/Game/GameSession/MapText.gd new file mode 100644 index 0000000..22eba10 --- /dev/null +++ b/game/src/Game/GameSession/MapText.gd @@ -0,0 +1,50 @@ +class_name MapText +extends Node3D + +@export var _map_view : MapView + +var _province_name_font : Font + +const _province_name_scale : float = 1.0 / 48.0 + +func _ready() -> void: + _province_name_font = AssetManager.get_font(&"mapfont_56") + +func _clear_children() -> void: + var child_count : int = get_child_count() + while child_count > 0: + child_count -= 1 + remove_child(get_child(child_count)) + +func generate_map_names() -> void: + _clear_children() + + for dict : Dictionary in GameSingleton.get_province_names(): + _add_province_name(dict) + +func _add_province_name(dict : Dictionary) -> void: + const identifier_key : StringName = &"identifier" + const position_key : StringName = &"position" + const rotation_key : StringName = &"rotation" + const scale_key : StringName = &"scale" + + var label : Label3D = Label3D.new() + + label.set_draw_flag(Label3D.FLAG_DOUBLE_SIDED, false) + label.set_modulate(Color.BLACK) + label.set_outline_size(0) + label.set_font(_province_name_font) + label.set_vertical_alignment(VERTICAL_ALIGNMENT_BOTTOM) + + var identifier : String = dict[identifier_key] + label.set_name(identifier) + label.set_text(GUINode.format_province_name(identifier)) + + label.set_position(_map_view._map_to_world_coords(dict[position_key]) + Vector3(0, 0.001, 0)) + + label.rotate_x(-PI / 2) + label.rotate_y(dict.get(rotation_key, 0.0)) + + label.scale *= dict.get(scale_key, 1.0) * _province_name_scale + + add_child(label) diff --git a/game/src/Game/GameSession/MapView.gd b/game/src/Game/GameSession/MapView.gd index f522bcb..2ab7c34 100644 --- a/game/src/Game/GameSession/MapView.gd +++ b/game/src/Game/GameSession/MapView.gd @@ -1,6 +1,9 @@ +class_name MapView extends Node3D signal map_view_camera_changed(near_left : Vector2, far_left : Vector2, far_right : Vector2, near_right : Vector2) +signal parchment_view_changed(is_parchment_view : bool) +signal detailed_view_changed(is_detailed_view : bool) const _action_north : StringName = &"map_north" const _action_east : StringName = &"map_east" @@ -22,9 +25,9 @@ var _drag_active : bool = false var _mouse_over_viewport : bool = true var _window_in_focus : bool = true -@export var _zoom_target_min : float = 0.15 +@export var _zoom_target_min : float = 0.10 @export var _zoom_target_max : float = 5.0 -@export var _zoom_target_step : float = (_zoom_target_max - _zoom_target_min) / 64.0 +@export var _zoom_target_step : float = (_zoom_target_max - _zoom_target_min) / 40.0 @export var _zoom_epsilon : float = _zoom_target_step * 0.005 @export var _zoom_speed : float = 5.0 # _zoom_target's starting value is ignored as it is updated to the camera's height by _ready, @@ -35,8 +38,13 @@ var _zoom_target : float = _zoom_target_max: const _zoom_position_multiplier = 3.14159 # Horizontal movement coefficient during zoom var _zoom_position : Vector2 -# Display the detailed terrain map below this height, and the parchment map above it +# Display the parchment map above this height @export var _zoom_parchment_threshold : float = _zoom_target_min + (_zoom_target_max - _zoom_target_min) / 4 +# Display details like models and province names below this height +@export var _zoom_detailed_threshold : float = _zoom_parchment_threshold / 2 + +var _is_parchment_view : bool = false +var _is_detailed_view : bool = false @export var _map_mesh_instance : MeshInstance3D var _map_mesh : MapMesh @@ -50,6 +58,8 @@ var _mouse_pos_viewport : Vector2 = Vector2(0.5, 0.5) var _mouse_pos_map : Vector2 = Vector2(0.5, 0.5) var _viewport_dims : Vector2 = Vector2(1, 1) +@export var _map_text : MapText + # ??? Strange Godot/GDExtension Bug ??? # Upon first opening a clone of this repo with the Godot Editor, # if GameSingleton.get_province_index_image is called before MapMesh @@ -61,7 +71,12 @@ func _ready() -> void: if not _camera: push_error("MapView's _camera variable hasn't been set!") return + + # Start just under the parchment threshold + _camera.position.y = _zoom_parchment_threshold - _zoom_target_step _zoom_target = _camera.position.y + _update_view_states(true) + if not _map_mesh_instance: push_error("MapView's _map_mesh_instance variable hasn't been set!") return @@ -105,6 +120,8 @@ func _ready() -> void: scaled_dims.z *= 2.0 (_map_background_instance.mesh as PlaneMesh).set_size(Vector2(scaled_dims.x, scaled_dims.z)) + _map_text.generate_map_names() + func _notification(what : int) -> void: match what: NOTIFICATION_WM_MOUSE_ENTER: # Mouse inside window @@ -119,6 +136,10 @@ func _notification(what : int) -> void: func _world_to_map_coords(pos : Vector3) -> Vector2: return (Vector2(pos.x, pos.z) - _map_mesh_corner) / _map_mesh_dims +func _map_to_world_coords(pos : Vector2) -> Vector3: + pos = pos * _map_mesh_dims + _map_mesh_corner + return Vector3(pos.x, 0, pos.y) + func _viewport_to_map_coords(pos_viewport : Vector2) -> Vector2: var ray_origin := _camera.project_ray_origin(pos_viewport) var ray_normal := _camera.project_ray_normal(pos_viewport) @@ -149,12 +170,13 @@ func _on_province_selected(index : int) -> void: # REQUIREMENTS # * SS-31 func _unhandled_input(event : InputEvent) -> void: - if _mouse_over_viewport and event.is_action_pressed(_action_click): - # Check if the mouse is outside of bounds - if _map_mesh.is_valid_uv_coord(_mouse_pos_map): - GameSingleton.set_selected_province(GameSingleton.get_province_index_from_uv_coords(_mouse_pos_map)) - else: - print("Clicked outside the map!") + if event.is_action_pressed(_action_click): + if _mouse_over_viewport: + # Check if the mouse is outside of bounds + if _map_mesh.is_valid_uv_coord(_mouse_pos_map): + GameSingleton.set_selected_province(GameSingleton.get_province_index_from_uv_coords(_mouse_pos_map)) + else: + print("Clicked outside the map!") elif event.is_action_pressed(_action_drag): if _drag_active: push_warning("Drag being activated while already active!") @@ -221,6 +243,17 @@ func _clamp_over_map() -> void: _camera.position.x = _map_mesh_corner.x + fposmod(_camera.position.x - _map_mesh_corner.x, _map_mesh_dims.x) _camera.position.z = clamp(_camera.position.z, _map_mesh_corner.y, _map_mesh_corner.y + _map_mesh_dims.y) +func _update_view_states(force_signal : bool) -> void: + var new_is_parchment_view : bool = _camera.position.y >= _zoom_parchment_threshold - _zoom_epsilon + if force_signal or new_is_parchment_view != _is_parchment_view: + _is_parchment_view = new_is_parchment_view + parchment_view_changed.emit(_is_parchment_view) + + var new_is_detailed_view : bool = _camera.position.y <= _zoom_detailed_threshold + _zoom_epsilon + if force_signal or new_is_detailed_view != _is_detailed_view: + _is_detailed_view = new_is_detailed_view + detailed_view_changed.emit(_is_detailed_view) + # REQUIREMENTS # * SS-74 # * UIFUN-123 @@ -237,12 +270,17 @@ func _zoom_process(delta : float) -> void: _zoom_position.y * zoom_delta * int(_mouse_over_viewport) ) # TODO - smooth transition similar to smooth zoom - var parchment_mapmode : bool = GameSingleton.is_parchment_mapmode_allowed() and _camera.position.y > _zoom_parchment_threshold + _update_view_states(false) + var parchment_mapmode : bool = GameSingleton.is_parchment_mapmode_allowed() and _is_parchment_view _map_shader_material.set_shader_parameter(GameLoader.ShaderManager.param_parchment_mix, float(parchment_mapmode)) func _update_orientation() -> void: const up := Vector3(0, 0, -1) - var dir := Vector3(0, -1, -1.25 * exp(-10 * _camera.position.y - _zoom_target_min)) + var dir := Vector3(0, -1, 0) + if _is_detailed_view: + # Zero at the transition point, increases as you zoom further in + var delta : float = (_zoom_detailed_threshold - _camera.position.y) / _zoom_detailed_threshold + dir.z = -(delta ** 4) _camera.look_at(_camera.position + dir, up) func _update_minimap_viewport() -> void: @@ -253,9 +291,9 @@ func _update_minimap_viewport() -> void: map_view_camera_changed.emit(near_left, far_left, far_right, near_right) func _update_mouse_map_position() -> void: - _mouse_pos_map = _viewport_to_map_coords(_mouse_pos_viewport) - var hover_index := GameSingleton.get_province_index_from_uv_coords(_mouse_pos_map) if _mouse_over_viewport: + _mouse_pos_map = _viewport_to_map_coords(_mouse_pos_viewport) + var hover_index := GameSingleton.get_province_index_from_uv_coords(_mouse_pos_map) _map_shader_material.set_shader_parameter(GameLoader.ShaderManager.param_hover_index, hover_index) func _on_mouse_entered_viewport() -> void: diff --git a/game/src/Game/GameSession/MapView.tscn b/game/src/Game/GameSession/MapView.tscn index bf22ef8..dff02a6 100644 --- a/game/src/Game/GameSession/MapView.tscn +++ b/game/src/Game/GameSession/MapView.tscn @@ -1,7 +1,8 @@ -[gd_scene load_steps=7 format=3 uid="uid://dkehmdnuxih2r"] +[gd_scene load_steps=8 format=3 uid="uid://dkehmdnuxih2r"] [ext_resource type="Script" path="res://src/Game/GameSession/MapView.gd" id="1_exccw"] [ext_resource type="Shader" path="res://src/Game/GameSession/TerrainMap.gdshader" id="1_upocn"] +[ext_resource type="Script" path="res://src/Game/GameSession/MapText.gd" id="2_13bgq"] [sub_resource type="ShaderMaterial" id="ShaderMaterial_tayeg"] render_priority = 0 @@ -23,17 +24,22 @@ albedo_color = Color(0, 0, 0, 1) material = SubResource("StandardMaterial3D_irk50") size = Vector2(6, 2) -[node name="MapView" type="Node3D" node_paths=PackedStringArray("_camera", "_map_mesh_instance", "_map_background_instance")] +[node name="MapView" type="Node3D" node_paths=PackedStringArray("_camera", "_map_mesh_instance", "_map_background_instance", "_map_text")] editor_description = "SS-73" script = ExtResource("1_exccw") _camera = NodePath("MapCamera") _map_mesh_instance = NodePath("MapMeshInstance") _map_background_instance = NodePath("MapBackgroundInstance") +_map_text = NodePath("MapText") [node name="MapCamera" type="Camera3D" parent="."] transform = Transform3D(1, 0, 0, 0, 0.707107, 0.707107, 0, -0.707107, 0.707107, 0.25, 1.5, -2.75) near = 0.01 +[node name="MapText" type="Node3D" parent="." node_paths=PackedStringArray("_map_view")] +script = ExtResource("2_13bgq") +_map_view = NodePath("..") + [node name="MapMeshInstance" type="MeshInstance3D" parent="."] editor_description = "FS-343" transform = Transform3D(10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0) @@ -43,3 +49,5 @@ mesh = SubResource("MapMesh_3gtsd") [node name="MapBackgroundInstance" type="MeshInstance3D" parent="."] transform = Transform3D(10, 0, 0, 0, 10, 0, 0, 0, 10, 0, -1, 0) mesh = SubResource("PlaneMesh_fnhgl") + +[connection signal="detailed_view_changed" from="." to="MapText" method="set_visible"] 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: diff --git a/game/src/Game/GameSession/TerrainMap.gdshader b/game/src/Game/GameSession/TerrainMap.gdshader index 98f9efd..467e277 100644 --- a/game/src/Game/GameSession/TerrainMap.gdshader +++ b/game/src/Game/GameSession/TerrainMap.gdshader @@ -34,37 +34,87 @@ uniform sampler2D colormap_water_tex: repeat_enable, filter_linear; // Overlay map tint uniform sampler2D colormap_overlay_tex: repeat_enable, filter_linear; -struct terrain_args_t { - vec2 uv, half_pixel_size; // Components for calculating terrain sampling UV +struct corner_args_t { + vec2 uv, half_pixel_size; // Components for calculating a corner's sampling UV vec2 terrain_uv; // UV coordinates scaled for terrain texture tiling vec3 land_tint_colour, water_tint_colour; // Colours for tinting the terrain + float stripe_mask; // Weight for mixing base and stripe province colours +}; + +struct corner_ret_t { + vec3 terrain_colour; + vec4 province_colour; + float highlight_mix_val; }; // Calculate terrain colour at the specified corner of the current pixel -vec3 get_terrain_colour(const terrain_args_t terrain_args, const vec2 corner) { +corner_ret_t get_corner_colour(const corner_args_t corner_args, const vec2 corner) { + corner_ret_t ret; + + uvec3 province_data = read_uvec3(fma(corner, corner_args.half_pixel_size, corner_args.uv)); + // Find the terrain index at the specified corner of the current pixel - uint terrain_index = read_uvec3(fma(corner, terrain_args.half_pixel_size, terrain_args.uv)).z; + uint terrain_index = province_data.z; // Get the tinted land colour at the current position - vec3 land_colour = texture(terrain_tex, vec3(terrain_args.terrain_uv, float(terrain_index))).rgb; - land_colour = mix(land_colour, terrain_args.land_tint_colour, 0.3); + vec3 land_colour = texture(terrain_tex, vec3(corner_args.terrain_uv, float(terrain_index))).rgb; + land_colour = mix(land_colour, corner_args.land_tint_colour, 0.3); // TODO - proper water texture - vec3 water_colour = terrain_args.water_tint_colour; + vec3 water_colour = corner_args.water_tint_colour; // Select land or water colour based on the terrain index (0 is water, otherwise land) - vec3 terrain_colour = mix(land_colour, water_colour, float(terrain_index == 0u)); + ret.terrain_colour = mix(land_colour, water_colour, float(terrain_index == 0u)); + + uint province_index = uvec2_to_uint(province_data.xy); + + // Get base and stripe colours for province at the current position + province_data.x *= 2u; // Double "x coordinate" as colours come in (base, stripe) pairs + vec4 province_base_colour = texelFetch(province_colour_tex, ivec2(province_data.xy), 0); + + province_data.x += 1u; // Add 1 to "x coordinate" to move from base to strip colour + vec4 province_stripe_colour = texelFetch(province_colour_tex, ivec2(province_data.xy), 0); + + // Blend the base and stripe colours according to the current position's stripe mask value + ret.province_colour = mix(province_base_colour, province_stripe_colour, corner_args.stripe_mask); + + ret.province_colour = mix(ret.province_colour, vec4(corner_args.water_tint_colour, 0.0), float(terrain_index == 0u)); + + ret.highlight_mix_val = 0.4 * ( + float(province_index == hover_index) + float(province_index == selected_index) + ) * float(province_index != 0u); - return terrain_colour; + return ret; } // Blend together terrain colours from the four corners of the current pixel -vec3 mix_terrain_colour(const terrain_args_t terrain_args, const vec2 pixel_offset) { - return mix( - mix(get_terrain_colour(terrain_args, vec2(-1, -1)), get_terrain_colour(terrain_args, vec2(+1, -1)), pixel_offset.x), - mix(get_terrain_colour(terrain_args, vec2(-1, +1)), get_terrain_colour(terrain_args, vec2(+1, +1)), pixel_offset.x), +corner_ret_t mix_terrain_colour(const corner_args_t corner_args, const vec2 pixel_offset) { + corner_ret_t mm = get_corner_colour(corner_args, vec2(-1, -1)); + corner_ret_t pm = get_corner_colour(corner_args, vec2(+1, -1)); + corner_ret_t mp = get_corner_colour(corner_args, vec2(-1, +1)); + corner_ret_t pp = get_corner_colour(corner_args, vec2(+1, +1)); + + corner_ret_t ret; + + ret.terrain_colour = mix( + mix(mm.terrain_colour, pm.terrain_colour, pixel_offset.x), + mix(mp.terrain_colour, pp.terrain_colour, pixel_offset.x), + pixel_offset.y + ); + + ret.province_colour = mix( + mix(mm.province_colour, pm.province_colour, pixel_offset.x), + mix(mp.province_colour, pp.province_colour, pixel_offset.x), + pixel_offset.y + ); + + ret.highlight_mix_val = mix( + mix(mm.highlight_mix_val, pm.highlight_mix_val, pixel_offset.x), + mix(mp.highlight_mix_val, pp.highlight_mix_val, pixel_offset.x), pixel_offset.y ); + + return ret; } // Mix overlay and base colours, used for the parchment map @@ -84,41 +134,33 @@ vec3 get_map_colour(vec2 uv) { // Offset of uv_map_pixels from the top left corner of the current pixel vec2 pixel_offset = fract(uv_map_pixels); - terrain_args_t terrain_args; - terrain_args.uv = uv; - terrain_args.half_pixel_size = 0.49 / map_size; + corner_args_t corner_args; + corner_args.uv = uv; + corner_args.half_pixel_size = 0.49 / map_size; // Terrain texture tiling UV - terrain_args.terrain_uv = 0.5 - uv_map_pixels * terrain_tile_factor; + corner_args.terrain_uv = 0.5 - uv_map_pixels * terrain_tile_factor; + + vec2 stripe_uv = uv_map_pixels * stripe_tile_factor; + // Stripe mask value - between 0 (base) and 1 (stripe) + corner_args.stripe_mask = texture(stripe_tex, stripe_uv).b; vec2 colormap_uv = vec2(uv.x, 1.0 - uv.y); // Terrain tinting colours - terrain_args.land_tint_colour = texture(colormap_land_tex, colormap_uv).rgb; - terrain_args.water_tint_colour = texture(colormap_water_tex, colormap_uv).rgb; + corner_args.land_tint_colour = texture(colormap_land_tex, colormap_uv).rgb; + corner_args.water_tint_colour = texture(colormap_water_tex, colormap_uv).rgb; // Parchment tint colour vec3 overlay_tint_colour = texture(colormap_overlay_tex, colormap_uv).rgb; - // Blended terrain colour (average of four corners of current pixel) - vec3 terrain_colour = mix_terrain_colour(terrain_args, pixel_offset); + corner_ret_t colours = mix_terrain_colour(corner_args, pixel_offset); - vec2 stripe_uv = uv_map_pixels * stripe_tile_factor; - // Stripe mask value - between 0 (base) and 1 (stripe) - float stripe_mask = texture(stripe_tex, stripe_uv).b; + // Blended terrain colour (average of four corners of current pixel) + vec3 terrain_colour = colours.terrain_colour; vec2 overlay_uv = vec2(uv_map_pixels.x, map_size.y - uv_map_pixels.y) * overlay_tile_factor; // Parchment overlay colour vec3 overlay_colour = texture(overlay_tex, overlay_uv).rgb; - // Current province index as a pair of byte coordinates and as a combined 16 bit value - uvec2 province_data = read_uvec3(uv).xy; - uint province_index = uvec2_to_uint(province_data); - - // Get base and stripe colours for province at the current position - province_data.x *= 2u; // Double "x coordinate" as colours come in (base, stripe) pairs - vec4 province_base_colour = texelFetch(province_colour_tex, ivec2(province_data), 0); - province_data.x += 1u; // Add 1 to "x coordinate" to move from base to strip colour - vec4 province_stripe_colour = texelFetch(province_colour_tex, ivec2(province_data), 0); - // Blend the base and stripe colours according to the current position's stripe mask value - vec4 province_colour = mix(province_base_colour, province_stripe_colour, stripe_mask); + vec4 province_colour = colours.province_colour; // Darken the province colour province_colour.rgb -= 0.7; @@ -129,22 +171,21 @@ vec3 get_map_colour(vec2 uv) { // Near colour is either the terrain's luma component tinted with the province colour and brightened, // or the normal terrain colour vec3 near_province_colour = mix(vec3(terrain_luma), province_colour.rgb, 0.3) * 1.5; - vec3 near_colour = mix(near_province_colour, terrain_colour, float(province_colour.a == 0.0)); + vec3 near_colour = mix(terrain_colour, near_province_colour, province_colour.a); // Far colour is either the province colour mixed with the overlay texture, tinted with the overlay colormap and brightened, // or the normal terrain colour mixed with the overlay texture (primarily for water) vec3 far_province_colour = mix_overlay(overlay_colour, province_colour.rgb); far_province_colour = mix(overlay_tint_colour, far_province_colour, 0.3) * 1.5; vec3 far_terrain_colour = mix_overlay(overlay_colour, terrain_colour); - vec3 far_colour = mix(far_province_colour, far_terrain_colour, float(province_colour.a == 0.0)); + vec3 far_colour = mix(far_terrain_colour, far_province_colour, province_colour.a); // Blend the near (detailed terrain) and far (parchment) colours according to the parchment mix factor (0 for near, 1 for far) vec3 final_colour = mix(near_colour, far_colour, parchment_mix); // Significantly brighted the colour if it is hovered over and/or selected, but not if it has province index 0 (all invalid pixels) const vec3 highlight_colour = vec3(1.0); - float highlight_mix_val = 0.4 * (float(province_index == hover_index) + float(province_index == selected_index)) * float(province_index != 0u); - vec3 highlighted_colour = mix(final_colour, highlight_colour, highlight_mix_val); + vec3 highlighted_colour = mix(final_colour, highlight_colour, colours.highlight_mix_val); return highlighted_colour; } |