diff options
m--------- | extension/deps/openvic-simulation | 0 | ||||
-rw-r--r-- | extension/src/GameSingleton.cpp | 83 | ||||
-rw-r--r-- | extension/src/GameSingleton.hpp | 18 | ||||
-rw-r--r-- | extension/src/LoadGameCompatibility.cpp | 80 | ||||
-rw-r--r-- | extension/src/LoadGameOpenVic.cpp | 12 | ||||
-rw-r--r-- | extension/src/Utilities.cpp | 90 | ||||
-rw-r--r-- | extension/src/Utilities.hpp | 10 | ||||
-rw-r--r-- | game/localisation/en_GB/mapmodes.csv | 1 | ||||
-rw-r--r-- | game/localisation/en_GB/menus.csv | 3 | ||||
-rw-r--r-- | game/src/Game/Autoload/GuiScale.gd | 10 | ||||
-rw-r--r-- | game/src/Game/GameSession/ProvinceOverviewPanel/ProvinceOverviewPanel.gd | 7 | ||||
-rw-r--r-- | game/src/Game/GameSession/ProvinceOverviewPanel/ProvinceOverviewPanel.tscn | 24 | ||||
-rw-r--r-- | game/src/Game/GameStart.gd | 9 | ||||
-rw-r--r-- | game/src/Game/GameStart.tscn | 3 | ||||
-rw-r--r-- | game/src/Game/LoadingScreen.gd | 10 | ||||
-rw-r--r-- | game/src/Game/Menu/OptionMenu/GuiScaleSelector.gd | 2 | ||||
-rw-r--r-- | game/src/Game/Theme/PieChart/PieChart.gd | 216 | ||||
-rw-r--r-- | game/src/Game/Theme/PieChart/PieChart.tscn | 38 |
18 files changed, 537 insertions, 79 deletions
diff --git a/extension/deps/openvic-simulation b/extension/deps/openvic-simulation -Subproject 8a08be3e7e8477973e243716d431ad7117acfa4 +Subproject 383ef70bea656a77e8b7c6843fd4a79705ecde4 diff --git a/extension/src/GameSingleton.cpp b/extension/src/GameSingleton.cpp index efacb8e..18fd67a 100644 --- a/extension/src/GameSingleton.cpp +++ b/extension/src/GameSingleton.cpp @@ -1,7 +1,5 @@ #include "GameSingleton.hpp" -#include <cassert> - #include <godot_cpp/variant/utility_functions.hpp> #include "openvic/utility/Logger.hpp" @@ -11,10 +9,9 @@ using namespace godot; using namespace OpenVic; -TerrainVariant::TerrainVariant(std::string const& new_identfier, +TerrainVariant::TerrainVariant(const std::string_view new_identfier, colour_t new_colour, Ref<Image> const& new_image) - : HasIdentifier { new_identfier }, - HasColour { new_colour, true }, + : HasIdentifierAndColour { new_identfier, new_colour, true }, image { new_image } {} Ref<Image> TerrainVariant::get_image() const { @@ -75,6 +72,9 @@ void GameSingleton::_bind_methods() { ClassDB::bind_static_method("GameSingleton", D_METHOD("get_province_info_region_key"), &GameSingleton::get_province_info_region_key); ClassDB::bind_static_method("GameSingleton", D_METHOD("get_province_info_life_rating_key"), &GameSingleton::get_province_info_life_rating_key); ClassDB::bind_static_method("GameSingleton", D_METHOD("get_province_info_total_population_key"), &GameSingleton::get_province_info_total_population_key); + ClassDB::bind_static_method("GameSingleton", D_METHOD("get_province_info_pop_types_key"), &GameSingleton::get_province_info_pop_types_key); + ClassDB::bind_static_method("GameSingleton", D_METHOD("get_province_info_pop_ideologies_key"), &GameSingleton::get_province_info_pop_ideologies_key); + ClassDB::bind_static_method("GameSingleton", D_METHOD("get_province_info_pop_cultures_key"), &GameSingleton::get_province_info_pop_cultures_key); ClassDB::bind_static_method("GameSingleton", D_METHOD("get_province_info_rgo_key"), &GameSingleton::get_province_info_rgo_key); ClassDB::bind_static_method("GameSingleton", D_METHOD("get_province_info_buildings_key"), &GameSingleton::get_province_info_buildings_key); @@ -84,6 +84,25 @@ void GameSingleton::_bind_methods() { ClassDB::bind_static_method("GameSingleton", D_METHOD("get_building_info_start_date_key"), &GameSingleton::get_building_info_start_date_key); ClassDB::bind_static_method("GameSingleton", D_METHOD("get_building_info_end_date_key"), &GameSingleton::get_building_info_end_date_key); ClassDB::bind_static_method("GameSingleton", D_METHOD("get_building_info_expansion_progress_key"), &GameSingleton::get_building_info_expansion_progress_key); + + ClassDB::bind_static_method("GameSingleton", D_METHOD("get_piechart_info_size_key"), &GameSingleton::get_piechart_info_size_key); + ClassDB::bind_static_method("GameSingleton", D_METHOD("get_piechart_info_colour_key"), &GameSingleton::get_piechart_info_colour_key); + + ClassDB::bind_static_method("GameSingleton", D_METHOD("draw_pie_chart", "image", "stopAngles", "colours", "radius", + "shadow_displacement", "shadow_tightness", "shadow_radius", "shadow_thickness", + "trim_colour", "trim_size", "gradient_falloff", "gradient_base", + "donut", "donut_inner_trim", "donut_inner_radius"), &GameSingleton::draw_pie_chart); +} + +void GameSingleton::draw_pie_chart(Ref<Image> image, + Array const& stopAngles, Array const& colours, float radius, + Vector2 shadow_displacement, float shadow_tightness, float shadow_radius, float shadow_thickness, + Color trim_colour, float trim_size, float gradient_falloff, float gradient_base, + bool donut, bool donut_inner_trim, float donut_inner_radius) { + + OpenVic::draw_pie_chart(image, stopAngles, colours, radius, shadow_displacement, shadow_tightness, shadow_radius, shadow_thickness, + trim_colour, trim_size, gradient_falloff, gradient_base, + donut, donut_inner_trim, donut_inner_radius); } GameSingleton* GameSingleton::get_singleton() { @@ -153,11 +172,25 @@ Error GameSingleton::_load_hardcoded_defines() { } return HIGH_ALPHA_VALUE | val; } - return HIGH_ALPHA_VALUE; + return NULL_COLOUR; } }, { "mapmode_population", [](Map const& map, Province const& province) -> colour_t { return HIGH_ALPHA_VALUE | (fraction_to_colour_byte(province.get_total_population(), map.get_highest_province_population() + 1, 0.1f, 1.0f) << 8); + } }, + { "mapmode_culture", + [](Map const& map, Province const& province) -> colour_t { + distribution_t const& cultures = province.get_culture_distribution(); + if (!cultures.empty()) { + // This breaks if replaced with distribution_t::value_type, something + // about operator=(volatile const&) being deleted. + std::pair<HasIdentifierAndColour const*, float> culture = *cultures.begin(); + for (distribution_t::value_type const p : cultures) { + if (p.second > culture.second) culture = p; + } + return HIGH_ALPHA_VALUE | culture.first->get_colour(); + } + return NULL_COLOUR; } } }; for (mapmode_t const& mapmode : mapmodes) @@ -208,6 +241,18 @@ StringName const& GameSingleton::get_province_info_total_population_key() { static const StringName key = "total_population"; return key; } +StringName const& GameSingleton::get_province_info_pop_types_key() { + static const StringName key = "pop_types"; + return key; +} +StringName const& GameSingleton::get_province_info_pop_ideologies_key() { + static const StringName key = "pop_ideologies"; + return key; +} +StringName const& GameSingleton::get_province_info_pop_cultures_key() { + static const StringName key = "pop_cultures"; + return key; +} StringName const& GameSingleton::get_province_info_rgo_key() { static const StringName key = "rgo"; return key; @@ -242,6 +287,26 @@ StringName const& GameSingleton::get_building_info_expansion_progress_key() { return key; } +StringName const& GameSingleton::get_piechart_info_size_key() { + static const StringName key = "size"; + return key; +} +StringName const& GameSingleton::get_piechart_info_colour_key() { + static const StringName key = "colour"; + return key; +} + +Dictionary GameSingleton::_distribution_to_dictionary(distribution_t const& dist) const { + Dictionary dict; + for (distribution_t::value_type const& p : dist) { + Dictionary sub_dict; + sub_dict[get_piechart_info_size_key()] = p.second; + sub_dict[get_piechart_info_colour_key()] = to_godot_color(p.first->get_colour()); + dict[std_to_godot_string(p.first->get_identifier())] = sub_dict; + } + return dict; +} + Dictionary GameSingleton::get_province_info_from_index(int32_t index) const { Province const* province = game_manager.map.get_province_by_index(index); if (province == nullptr) return {}; @@ -257,6 +322,12 @@ Dictionary GameSingleton::get_province_info_from_index(int32_t index) const { ret[get_province_info_life_rating_key()] = province->get_life_rating(); ret[get_province_info_total_population_key()] = province->get_total_population(); + distribution_t const& pop_types = province->get_pop_type_distribution(); + if (!pop_types.empty()) ret[get_province_info_pop_types_key()] = _distribution_to_dictionary(pop_types); + //distribution_t const& ideologies = province->get_ideology_distribution(); + //if (!ideologies.empty()) ret[get_province_info_pop_ideologies_key()] = _distribution_to_dictionary(ideologies); + distribution_t const& cultures = province->get_culture_distribution(); + if (!cultures.empty()) ret[get_province_info_pop_cultures_key()] = _distribution_to_dictionary(cultures); std::vector<Building> const& buildings = province->get_buildings(); if (!buildings.empty()) { diff --git a/extension/src/GameSingleton.hpp b/extension/src/GameSingleton.hpp index 23eb334..cd8cdd0 100644 --- a/extension/src/GameSingleton.hpp +++ b/extension/src/GameSingleton.hpp @@ -6,13 +6,13 @@ #include "openvic/GameManager.hpp" namespace OpenVic { - struct TerrainVariant : HasIdentifier, HasColour { + struct TerrainVariant : HasIdentifierAndColour { friend class GameSingleton; private: const godot::Ref<godot::Image> image; - TerrainVariant(std::string const& new_identfier, colour_t new_colour, + TerrainVariant(const std::string_view new_identfier, colour_t new_colour, godot::Ref<godot::Image> const& new_image); public: static constexpr size_t MAX_INDEX = 1 << (8 * sizeof(Map::terrain_t)); @@ -64,10 +64,18 @@ namespace OpenVic { godot::Error _update_colour_image(); void _on_state_updated(); + godot::Dictionary _distribution_to_dictionary(distribution_t const& dist) const; + protected: static void _bind_methods(); public: + static void draw_pie_chart(godot::Ref<godot::Image> image, + godot::Array const& stopAngles, godot::Array const& colours, float radius, + godot::Vector2 shadow_displacement, float shadow_tightness, float shadow_radius, float shadow_thickness, + godot::Color trim_colour, float trim_size, float gradient_falloff, float gradient_base, + bool donut, bool donut_inner_trim, float donut_inner_radius); + static GameSingleton* get_singleton(); GameSingleton(); @@ -106,6 +114,9 @@ namespace OpenVic { static godot::StringName const& get_province_info_region_key(); static godot::StringName const& get_province_info_life_rating_key(); static godot::StringName const& get_province_info_total_population_key(); + static godot::StringName const& get_province_info_pop_types_key(); + static godot::StringName const& get_province_info_pop_ideologies_key(); + static godot::StringName const& get_province_info_pop_cultures_key(); static godot::StringName const& get_province_info_rgo_key(); static godot::StringName const& get_province_info_buildings_key(); @@ -116,6 +127,9 @@ namespace OpenVic { static godot::StringName const& get_building_info_end_date_key(); static godot::StringName const& get_building_info_expansion_progress_key(); + static godot::StringName const& get_piechart_info_size_key(); + static godot::StringName const& get_piechart_info_colour_key(); + /* Get info to display in Province Overview Panel, packaged in * a Dictionary using the StringNames above as keys. */ diff --git a/extension/src/LoadGameCompatibility.cpp b/extension/src/LoadGameCompatibility.cpp index ddde5b8..71435a4 100644 --- a/extension/src/LoadGameCompatibility.cpp +++ b/extension/src/LoadGameCompatibility.cpp @@ -17,57 +17,57 @@ Error GameSingleton::_load_province_identifier_file_compatibility_mode(String co Error err = FileAccess::get_open_error(); if (err != OK || file.is_null()) { UtilityFunctions::push_error("Failed to load compatibility mode province identifier file: ", file_path); - return err == OK ? FAILED : err; - } - - int line_number = 0; - while (!file->eof_reached()) { - const PackedStringArray line = file->get_csv_line(";"); - line_number++; + if (err == OK) err = FAILED; + } else { + int line_number = 0; + while (!file->eof_reached()) { + const PackedStringArray line = file->get_csv_line(";"); + line_number++; - if (line.is_empty() || (line.size() == 1 && line[0].is_empty())) - continue; + if (line.is_empty() || (line.size() == 1 && line[0].is_empty())) + continue; - if (line_number < 2) continue; // skip header line - index_t id = NULL_INDEX; - colour_t colour = NULL_COLOUR; - if (line.size() > 0) { - if (line[0].is_empty()) { - id = game_manager.map.get_province_count() + 1; - } else if (line[0].is_valid_int()) { - const int64_t val = line[0].to_int(); - if (val > NULL_INDEX && val <= MAX_INDEX) id = val; - } - for (int i = 1; i < 4; ++i) { - if (line.size() > i) { - if (line[i].is_valid_int()) { - const int64_t int_val = line[i].to_int(); - if (int_val >= NULL_COLOUR && int_val <= FULL_COLOUR) { - colour = (colour << 8) | int_val; - continue; - } - } else if (line[i].is_valid_float()) { - const double double_val = line[i].to_float(); - if (std::trunc(double_val) == double_val) { - const int64_t int_val = double_val; + if (line_number < 2) continue; // skip header line + index_t id = NULL_INDEX; + colour_t colour = NULL_COLOUR; + if (line.size() > 0) { + if (line[0].is_empty()) { + id = game_manager.map.get_province_count() + 1; + } else if (line[0].is_valid_int()) { + const int64_t val = line[0].to_int(); + if (val > NULL_INDEX && val <= MAX_INDEX) id = val; + } + for (int i = 1; i < 4; ++i) { + if (line.size() > i) { + if (line[i].is_valid_int()) { + const int64_t int_val = line[i].to_int(); if (int_val >= NULL_COLOUR && int_val <= FULL_COLOUR) { colour = (colour << 8) | int_val; continue; } + } else if (line[i].is_valid_float()) { + const double double_val = line[i].to_float(); + if (std::trunc(double_val) == double_val) { + const int64_t int_val = double_val; + if (int_val >= NULL_COLOUR && int_val <= FULL_COLOUR) { + colour = (colour << 8) | int_val; + continue; + } + } } } + colour = NULL_COLOUR; + break; } - colour = NULL_COLOUR; - break; } + if (id == NULL_INDEX || colour == NULL_COLOUR) { + UtilityFunctions::push_error("Invalid province ID-colour entry \"", line, "\" on line ", line_number, " in file: ", file_path); + err = FAILED; + continue; + } + static const std::string province_prefix = "PROV"; + if (game_manager.map.add_province(province_prefix + std::to_string(id), colour) != SUCCESS) err = FAILED; } - if (id == NULL_INDEX || colour == NULL_COLOUR) { - UtilityFunctions::push_error("Invalid province ID-colour entry \"", line, "\" on line ", line_number, " in file: ", file_path); - err = FAILED; - continue; - } - static const std::string province_prefix = "PROV"; - if (game_manager.map.add_province(province_prefix + std::to_string(id), colour) != SUCCESS) err = FAILED; } game_manager.map.lock_provinces(); return err; diff --git a/extension/src/LoadGameOpenVic.cpp b/extension/src/LoadGameOpenVic.cpp index 27fb265..627578f 100644 --- a/extension/src/LoadGameOpenVic.cpp +++ b/extension/src/LoadGameOpenVic.cpp @@ -157,7 +157,11 @@ Error GameSingleton::_parse_region_entry(String const& identifier, Variant const UtilityFunctions::push_error("Invalid province list for region \"", identifier, "\": ", entry); return FAILED; } - return ERR(game_manager.map.add_region(godot_to_std_string(identifier), province_identifiers)); + std::vector<std::string_view> province_identifier_views; + for (std::string const& str : province_identifiers) { + province_identifier_views.push_back(str); + } + return ERR(game_manager.map.add_region(godot_to_std_string(identifier), province_identifier_views)); } Error GameSingleton::_load_region_file(String const& file_path) { @@ -271,7 +275,7 @@ Error GameSingleton::_load_map_images(String const& province_image_path, String // Generate interleaved province and terrain ID image if (game_manager.map.generate_province_shape_image(province_dims.x, province_dims.y, province_image->get_data().ptr(), - terrain_image->get_data().ptr(), terrain_variant_map) != SUCCESS) err = FAILED; + terrain_image->get_data().ptr(), terrain_variant_map, true) != SUCCESS) err = FAILED; static constexpr int32_t GPU_DIM_LIMIT = 0x3FFF; // For each dimension of the image, this finds the small number of equal subdivisions required get the individual texture dims under GPU_DIM_LIMIT @@ -364,8 +368,8 @@ Error GameSingleton::_parse_good_entry(String const& identifier, Variant const& if (var_overseas_maintenance.get_type() == Variant::BOOL) overseas_maintenance = var_overseas_maintenance; else UtilityFunctions::push_error("Invalid good overseas maintenance bool value for ", identifier, ": ", var_overseas_maintenance); - return ERR(game_manager.good_manager.add_good(godot_to_std_string(identifier), godot_to_std_string(category), - colour, base_price, default_available, tradeable, currency, overseas_maintenance)); + return ERR(game_manager.good_manager.add_good(godot_to_std_string(identifier), colour, godot_to_std_string(category), + base_price, default_available, tradeable, currency, overseas_maintenance)); } Error GameSingleton::_load_goods(String const& defines_path, String const& icons_dir_path) { diff --git a/extension/src/Utilities.cpp b/extension/src/Utilities.cpp index a912490..f09eae0 100644 --- a/extension/src/Utilities.cpp +++ b/extension/src/Utilities.cpp @@ -1,5 +1,7 @@ #include "Utilities.hpp" +#include <numbers> + #include <godot_cpp/classes/resource_loader.hpp> #include <godot_cpp/variant/utility_functions.hpp> @@ -14,3 +16,91 @@ Ref<Image> OpenVic::load_godot_image(String const& path) { return Image::load_from_file(path); } } + +// Get the polar coordinates of a pixel relative to the center +static Vector2 getPolar(Vector2 UVin, Vector2 center) { + Vector2 relcoord = (UVin-center); + float dist = relcoord.length(); + float theta = std::numbers::pi / 2 + atan2(relcoord.y, relcoord.x); + if (theta < 0.0f) theta += std::numbers::pi * 2; + return { dist, theta }; +} + +// From thebookofshaders, returns a gradient falloff +static inline float parabola(float base, float x, float k){ + return powf(base * x * (1.0 - x), k); +} + +static inline float parabola_shadow(float base, float x){ + return base * x * x; +} + +static Color pie_chart_fragment(Vector2 UV, float radius, Array const& stopAngles, Array const& colours, + Vector2 shadow_displacement, float shadow_tightness, float shadow_radius, float shadow_thickness, + Color trim_colour, float trim_size, float gradient_falloff, float gradient_base, + bool donut, bool donut_inner_trim, float donut_inner_radius) { + + Vector2 coords = getPolar(UV, { 0.5, 0.5 }); + float dist = coords.x; + float theta = coords.y; + + Vector2 shadow_polar = getPolar(UV, shadow_displacement); + float shadow_peak = radius + (radius - donut_inner_radius) / 2.0; + float shadow_gradient = shadow_thickness + parabola_shadow(shadow_tightness * -10.0, shadow_polar.x + shadow_peak - shadow_radius); + + // Inner hole of the donut => make it transparent + if (donut && dist <= donut_inner_radius) { + return { 0.1, 0.1, 0.1, shadow_gradient }; + } + // Inner trim + else if (donut && donut_inner_trim && dist <= donut_inner_radius + trim_size) { + return { trim_colour, 1.0 }; + } + // Interior + else if (dist <= radius-trim_size) { + Color col { 1.0f, 0.0f, 0.0f }; + for (int i = 0; i < stopAngles.size(); i++){ + if (theta <= float(stopAngles[i])) { + col = colours[i]; + break; + } + } + float gradient = parabola(gradient_base, dist, gradient_falloff); + return { col * (1.0 - gradient), 1.0 }; + } + // Outer trim + else if (dist <= radius) { + return { trim_colour, 1.0 }; + } + // Outside the circle + else{ + return { 0.1, 0.1, 0.1, shadow_gradient }; + } +} + +void OpenVic::draw_pie_chart(Ref<Image> image, + Array const& stopAngles, Array const& colours, float radius, + Vector2 shadow_displacement, float shadow_tightness, float shadow_radius, float shadow_thickness, + Color trim_colour, float trim_size, float gradient_falloff, float gradient_base, + bool donut, bool donut_inner_trim, float donut_inner_radius) { + + ERR_FAIL_NULL_EDMSG(image, "Cannot draw pie chart to null image."); + const int32_t width = image->get_width(); + const int32_t height = image->get_height(); + ERR_FAIL_COND_EDMSG(width <= 0 || height <= 0, "Cannot draw pie chart to empty image."); + if (width != height) { + UtilityFunctions::push_warning("Drawing pie chart to non-square image: ", width, "x", height); + } + const int32_t size = std::min(width, height); + for (int32_t y = 0; y < size; ++y) { + for (int32_t x = 0; x < size; ++x) { + image->set_pixel(x, y, pie_chart_fragment( + { static_cast<float>(x) / static_cast<float>(size), + static_cast<float>(y) / static_cast<float>(size) }, + radius, stopAngles, colours, + shadow_displacement, shadow_tightness, shadow_radius, shadow_thickness, + trim_colour, trim_size, gradient_falloff, gradient_base, + donut, donut_inner_trim, donut_inner_radius)); + } + } +} diff --git a/extension/src/Utilities.hpp b/extension/src/Utilities.hpp index 681b893..e8796e9 100644 --- a/extension/src/Utilities.hpp +++ b/extension/src/Utilities.hpp @@ -16,5 +16,15 @@ namespace OpenVic { return str.c_str(); } + inline godot::Color to_godot_color(colour_t colour) { + return { colour_byte_to_float((colour >> 16) & 0xFF), colour_byte_to_float((colour >> 8) & 0xFF), colour_byte_to_float(colour & 0xFF) }; + } + godot::Ref<godot::Image> load_godot_image(godot::String const& path); + + void draw_pie_chart(godot::Ref<godot::Image> image, + godot::Array const& stopAngles, godot::Array const& colours, float radius, + godot::Vector2 shadow_displacement, float shadow_tightness, float shadow_radius, float shadow_thickness, + godot::Color trim_colour, float trim_size, float gradient_falloff, float gradient_base, + bool donut, bool donut_inner_trim, float donut_inner_radius); } diff --git a/game/localisation/en_GB/mapmodes.csv b/game/localisation/en_GB/mapmodes.csv index 3b7fd52..8fa5798 100644 --- a/game/localisation/en_GB/mapmodes.csv +++ b/game/localisation/en_GB/mapmodes.csv @@ -7,3 +7,4 @@ mapmode_index;Index mapmode_rgo;RGO mapmode_infrastructure;Infrastructure mapmode_population;Population Density +mapmode_culture;Nationality diff --git a/game/localisation/en_GB/menus.csv b/game/localisation/en_GB/menus.csv index 1d16af6..57994ad 100644 --- a/game/localisation/en_GB/menus.csv +++ b/game/localisation/en_GB/menus.csv @@ -131,3 +131,6 @@ building_fort;Fort building_naval_base;Naval Base building_railroad;Railroad EXPAND_PROVINCE_BUILDING;Expand + +;; Pie Chart +PIECHART_TOOLTIP_NO_DATA;No data diff --git a/game/src/Game/Autoload/GuiScale.gd b/game/src/Game/Autoload/GuiScale.gd index afd73df..d364ff5 100644 --- a/game/src/Game/Autoload/GuiScale.gd +++ b/game/src/Game/Autoload/GuiScale.gd @@ -26,7 +26,7 @@ func _ready(): func has_guiscale(guiscale_value : float) -> bool: return guiscale_value in _guiscales - + func add_guiscale(guiscale_value: float, guiscale_name: StringName=&"") -> bool: if has_guiscale(guiscale_value): return true var scale_dict := { value = guiscale_value } @@ -39,24 +39,24 @@ func add_guiscale(guiscale_value: float, guiscale_name: StringName=&"") -> bool: return false _guiscales[guiscale_value] = scale_dict return true - + #returns floats func get_guiscale_value_list() -> Array: var list := _guiscales.keys() list.sort_custom(func(a, b): return a > b) return list - + func get_guiscale_display_name(guiscale_value : float) -> StringName: return _guiscales.get(guiscale_value, {display_name = &"unknown gui scale"}).display_name func get_current_guiscale() -> float: return get_tree().root.content_scale_factor - + func set_guiscale(guiscale:float) -> void: print("New GUI scale: %f" % guiscale) if not has_guiscale(guiscale): push_warning("Setting GUI Scale to non-standard value %sx" % [guiscale]) get_tree().root.content_scale_factor = guiscale - + func reset_guiscale() -> void: set_guiscale(get_current_guiscale()) diff --git a/game/src/Game/GameSession/ProvinceOverviewPanel/ProvinceOverviewPanel.gd b/game/src/Game/GameSession/ProvinceOverviewPanel/ProvinceOverviewPanel.gd index 9a3690e..0220be2 100644 --- a/game/src/Game/GameSession/ProvinceOverviewPanel/ProvinceOverviewPanel.gd +++ b/game/src/Game/GameSession/ProvinceOverviewPanel/ProvinceOverviewPanel.gd @@ -7,6 +7,9 @@ extends PanelContainer @export var _rgo_icon_texture_rect : TextureRect @export var _rgo_name_label : Label @export var _buildings_container : Container +@export var _pop_type_chart : PieChart +@export var _pop_ideology_chart : PieChart +@export var _pop_culture_chart : PieChart const _missing_suffix : String = "_MISSING" @@ -113,6 +116,10 @@ func _update_info() -> void: _total_population_label.text = Localisation.tr_number(_province_info.get(GameSingleton.get_province_info_total_population_key(), 0)) + _pop_type_chart.set_to_distribution(_province_info.get(GameSingleton.get_province_info_pop_types_key(), {})) + _pop_ideology_chart.set_to_distribution(_province_info.get(GameSingleton.get_province_info_pop_ideologies_key(), {})) + _pop_culture_chart.set_to_distribution(_province_info.get(GameSingleton.get_province_info_pop_cultures_key(), {})) + _rgo_name_label.text = _province_info.get(GameSingleton.get_province_info_rgo_key(), GameSingleton.get_province_info_rgo_key() + _missing_suffix) _rgo_icon_texture_rect.texture = GameSingleton.get_good_icon_texture(_rgo_name_label.text) diff --git a/game/src/Game/GameSession/ProvinceOverviewPanel/ProvinceOverviewPanel.tscn b/game/src/Game/GameSession/ProvinceOverviewPanel/ProvinceOverviewPanel.tscn index 7c82f10..7e49ac8 100644 --- a/game/src/Game/GameSession/ProvinceOverviewPanel/ProvinceOverviewPanel.tscn +++ b/game/src/Game/GameSession/ProvinceOverviewPanel/ProvinceOverviewPanel.tscn @@ -1,8 +1,9 @@ -[gd_scene load_steps=2 format=3 uid="uid://byq323jbel48u"] +[gd_scene load_steps=3 format=3 uid="uid://byq323jbel48u"] [ext_resource type="Script" path="res://src/Game/GameSession/ProvinceOverviewPanel/ProvinceOverviewPanel.gd" id="1_3n8k5"] +[ext_resource type="PackedScene" uid="uid://cr7p1k2xm7mum" path="res://src/Game/Theme/PieChart/PieChart.tscn" id="2_3oytt"] -[node name="ProvinceOverviewPanel" type="PanelContainer" node_paths=PackedStringArray("_province_name_label", "_region_name_label", "_life_rating_bar", "_total_population_label", "_rgo_icon_texture_rect", "_rgo_name_label", "_buildings_container")] +[node name="ProvinceOverviewPanel" type="PanelContainer" node_paths=PackedStringArray("_province_name_label", "_region_name_label", "_life_rating_bar", "_total_population_label", "_rgo_icon_texture_rect", "_rgo_name_label", "_buildings_container", "_pop_type_chart", "_pop_ideology_chart", "_pop_culture_chart")] editor_description = "UI-56" anchors_preset = 2 anchor_top = 1.0 @@ -19,6 +20,9 @@ _total_population_label = NodePath("PanelList/InteractList/TotalPopulation") _rgo_icon_texture_rect = NodePath("PanelList/InteractList/RGOInfo/RGOIcon") _rgo_name_label = NodePath("PanelList/InteractList/RGOInfo/RGOName") _buildings_container = NodePath("PanelList/InteractList/BuildingsContainer") +_pop_type_chart = NodePath("PanelList/InteractList/PopStats/PopTypeChart") +_pop_ideology_chart = NodePath("PanelList/InteractList/PopStats/PopIdeologyChart") +_pop_culture_chart = NodePath("PanelList/InteractList/PopStats/PopCultureChart") [node name="PanelList" type="VBoxContainer" parent="."] layout_mode = 2 @@ -88,6 +92,22 @@ vertical_alignment = 1 layout_mode = 2 mouse_filter = 1 +[node name="PopStats" type="HBoxContainer" parent="PanelList/InteractList"] +layout_mode = 2 + +[node name="PopTypeChart" parent="PanelList/InteractList/PopStats" instance=ExtResource("2_3oytt")] +layout_mode = 2 + +[node name="PopIdeologyChart" parent="PanelList/InteractList/PopStats" instance=ExtResource("2_3oytt")] +layout_mode = 2 + +[node name="PopCultureChart" parent="PanelList/InteractList/PopStats" instance=ExtResource("2_3oytt")] +layout_mode = 2 + +[node name="HSeparator3" type="HSeparator" parent="PanelList/InteractList"] +layout_mode = 2 +mouse_filter = 1 + [node name="BuildingsContainer" type="GridContainer" parent="PanelList/InteractList"] layout_mode = 2 columns = 3 diff --git a/game/src/Game/GameStart.gd b/game/src/Game/GameStart.gd index 995541f..bfbbfb1 100644 --- a/game/src/Game/GameStart.gd +++ b/game/src/Game/GameStart.gd @@ -49,12 +49,3 @@ func _initialize_game() -> void: func _on_splash_container_splash_end(): loading_screen.show() - -func _on_loading_screen_load_started(): - Events.Loader.startup_load_begun.emit() - -func _on_loading_screen_load_changed(percentage : float) -> void: - Events.Loader.startup_load_changed.emit(percentage) - -func _on_loading_screen_load_ended(): - Events.Loader.startup_load_ended.emit() diff --git a/game/src/Game/GameStart.tscn b/game/src/Game/GameStart.tscn index f16daa3..189e72f 100644 --- a/game/src/Game/GameStart.tscn +++ b/game/src/Game/GameStart.tscn @@ -51,8 +51,5 @@ stream = ExtResource("5_8euyy") autoplay = true expand = true -[connection signal="load_changed" from="LoadingScreen" to="." method="_on_loading_screen_load_changed"] -[connection signal="load_ended" from="LoadingScreen" to="." method="_on_loading_screen_load_ended"] -[connection signal="load_started" from="LoadingScreen" to="." method="_on_loading_screen_load_started"] [connection signal="splash_end" from="SplashContainer" to="." method="_on_splash_container_splash_end"] [connection signal="finished" from="SplashContainer/SplashVideo" to="SplashContainer" method="_on_splash_startup_finished"] diff --git a/game/src/Game/LoadingScreen.gd b/game/src/Game/LoadingScreen.gd index 3cbf199..d953d20 100644 --- a/game/src/Game/LoadingScreen.gd +++ b/game/src/Game/LoadingScreen.gd @@ -1,9 +1,5 @@ extends Control -signal load_started() -signal load_changed(percentage : float) -signal load_ended() - @export var quote_file_path : String = "res://common/quotes.txt" @export_subgroup("Nodes") @@ -24,7 +20,7 @@ func start_loading_screen(thread_safe_function : Callable) -> void: thread.wait_to_finish() thread.start(thread_safe_function) - load_started.emit() + Events.Loader.startup_load_begun.emit() func try_update_loading_screen(percent_complete: float, quote_should_change = false): # forces the function to behave as if deferred @@ -34,9 +30,9 @@ func try_update_loading_screen(percent_complete: float, quote_should_change = fa quote_label.text = quotes[randi() % quotes.size()] if is_equal_approx(percent_complete, 100): thread.wait_to_finish() - load_ended.emit() + Events.Loader.startup_load_ended.emit() else: - load_changed.emit(percent_complete) + Events.Loader.startup_load_changed.emit(percent_complete) func _ready(): if Engine.is_editor_hint(): return diff --git a/game/src/Game/Menu/OptionMenu/GuiScaleSelector.gd b/game/src/Game/Menu/OptionMenu/GuiScaleSelector.gd index 4dd86e1..42be5e0 100644 --- a/game/src/Game/Menu/OptionMenu/GuiScaleSelector.gd +++ b/game/src/Game/Menu/OptionMenu/GuiScaleSelector.gd @@ -32,7 +32,7 @@ func _sync_guiscales(to_select : float = GuiScale.get_current_guiscale()) -> voi if selected == -1: selected = default_selected - + func _setup_button(): if default_value <= 0: default_value = ProjectSettings.get_setting("display/window/stretch/scale") diff --git a/game/src/Game/Theme/PieChart/PieChart.gd b/game/src/Game/Theme/PieChart/PieChart.gd new file mode 100644 index 0000000..cfd7917 --- /dev/null +++ b/game/src/Game/Theme/PieChart/PieChart.gd @@ -0,0 +1,216 @@ +@tool +extends TextureRect + +class_name PieChart + + +@export var donut : bool = false +@export_range(0.0, 1.0) var donut_inner_radius : float = 0.5 +@export_range(0.0, 0.5) var radius : float = 0.4 +@export var shadow_displacement : Vector2 = Vector2(0.55, 0.6) +@export var shadow_tightness : float = 1.0 +@export var shadow_radius : float = 0.6 +@export var shadow_thickness : float = 1.0 + +@export var trim_colour : Color = Color(0.0, 0.0, 0.0) +@export_range(0.0, 1.0) var trim_size : float = 0.02 +@export var donut_inner_trim : bool = true +@export var slice_gradient_falloff : float = 3.6 +@export var slice_gradient_base : float = 3.1 + +@export var _rich_tooltip : RichTextLabel +var _pie_chart_image : Image + +# A data class for the pie chart +class SliceData: + extends RefCounted + # Primary properties, change these to change + # the displayed piechart + var colour : Color = Color(1.0, 0.0, 0.0) + var tooltip : String = "" + var quantity : float = -1 + # Derived properties, don't set from an external script + var final_angle : float = -1 + var percentage : float = 0: + get: + return percentage + set(value): + percentage = clampf(value, 0, 1) + + func _init(quantityIn : float, tooltipIn : String, colourIn : Color): + colour = colourIn + tooltip = tooltipIn + quantity = quantityIn + +# The key of an entry of this dictionary should be an easy to reference constant +# The tooltip label is what the user will actually read +var _slices : Dictionary = {} + +# Slice keys/labels in the order they should be displayed +var _slice_order : Array = [] + +# Example slices: +""" + "label1": SliceData.new(5, "Conservative", Color(0.0, 0.0, 1.0)), + "label2": SliceData.new(3, "Liberal", Color(1.0, 1.0, 0.0)), + "label3": SliceData.new(2, "Reactionary", Color(0.4, 0.0, 0.6)) +""" + +# These functions are the interface a developer will use to update the piechart +# The piechart will only redraw once one of these has been triggered +func add_or_replace_label(labelName : String, quantity : float, tooltip : String, colour : Color = Color(0.0, 0.0, 0.0)) -> void: + _slices[labelName] = SliceData.new(quantity, tooltip, colour) + if _slice_order.find(labelName) == -1: + _slice_order.push_back(labelName) + _recalculate() + +func update_label_quantity(labelName : String, quantity : float) -> void: + if _slices.has(labelName): + _slices[labelName].quantity = quantity + _recalculate() + +func update_label_colour(labelName : String, colour : Color) -> void: + if _slices.has(labelName): + _slices[labelName].colour = colour + _recalculate() + +func update_label_tooltip(labelName : String, tooltip : String) -> void: + if _slices.has(labelName): + _slices[labelName].tooltip = tooltip + +func remove_label(labelName : String) -> bool: + if _slices.erase(labelName): + var index := _slice_order.find(labelName) + if index == -1: + push_error("Slice in dictionary but not order list: ", labelName) + else: + _slice_order.remove_at(index) + _recalculate() + return true + return false + +func clear_slices() -> void: + _slices.clear() + _slice_order.clear() + +# Distribution dictionary of the form: +# { "<label>": { "size": <quantity>, "colour": <colour> } } +func set_to_distribution(dist : Dictionary) -> void: + clear_slices() + for key in dist: + var entry : Dictionary = dist[key] + _slices[key] = SliceData.new(entry[GameSingleton.get_piechart_info_size_key()], key, entry[GameSingleton.get_piechart_info_colour_key()]) + _slice_order = _slices.keys() + sort_slices() + +# Sorted by quantity, smallest to largest, so that the smallest slice +# is to the left of a radial line straight upwards +func sort_slices() -> void: + _slice_order.sort_custom(func (a, b): return _slices[a].quantity < _slices[b].quantity) + _recalculate() + +func _ready(): + if not Engine.is_editor_hint(): + const image_size : int = 256 + _pie_chart_image = Image.create(image_size, image_size, false, Image.FORMAT_RGBA8) + texture = ImageTexture.create_from_image(_pie_chart_image) + _recalculate() + +# Update the slice angles based on the new slice data +func _recalculate() -> void: + # Where the slices are the public interface, these are the actual paramters + # which will be sent to the shader/draw function + var angles : Array = [] + var colours : Array = [] + + var total : float = 0 + for label in _slice_order: + var quantity : float = _slices[label].quantity + if quantity > 0: + total += quantity + + var current_arc_start : float = 0 + var current_arc_finish : float = 0 + + for label in _slice_order: + var slice : SliceData = _slices[label] + if slice.quantity > 0: + slice.percentage = slice.quantity / total + var rads_to_cover : float = slice.percentage * 2.0 * PI + current_arc_finish = current_arc_start + rads_to_cover + slice.final_angle = current_arc_finish + current_arc_start = current_arc_finish + angles.push_back(current_arc_finish) + colours.push_back(slice.colour) + + GameSingleton.draw_pie_chart(_pie_chart_image, angles, colours, radius, shadow_displacement, shadow_tightness, shadow_radius, shadow_thickness, + trim_colour, trim_size, slice_gradient_falloff, slice_gradient_base, donut, donut_inner_trim, donut_inner_radius / 2) + texture.set_image(_pie_chart_image) + +# Process mouse to select the appropriate tooltip for the slice +func _gui_input(event : InputEvent): + if event is InputEventMouse: + var pos : Vector2 = event.position + var _handled : bool = _handle_tooltip(pos) + +func _on_mouse_exited(): + _rich_tooltip.visible = false + +# Takes a mouse position, and sets an appropriate tooltip for the slice the mouse +# is hovered over. Returns a boolean on whether the tooltip was handled. +func _handle_tooltip(pos : Vector2) -> bool: + # Is it within the circle? + var real_radius := size.x / 2.0 + var center := Vector2(real_radius, real_radius) + var distance := center.distance_to(pos) + var real_donut_inner_radius : float = real_radius * donut_inner_radius + if distance <= real_radius and (not donut or distance >= real_donut_inner_radius): + if _slice_order.is_empty(): + _rich_tooltip.text = "PIECHART_TOOLTIP_NO_DATA" + else: + var angle := _convert_angle(center.angle_to_point(pos)) + var selected_label : String = "" + for label in _slice_order: + if angle <= _slices[label].final_angle: + if not selected_label or _slices[label].final_angle < _slices[selected_label].final_angle: + selected_label = label + if not selected_label: + selected_label = _slice_order[0] + _rich_tooltip.text = _create_tooltip(selected_label) + _rich_tooltip.visible = true + _rich_tooltip.position = pos + Vector2(5, 5) + get_global_rect().position + _rich_tooltip.reset_size() + else: + # Technically the corners of the bounding box + # are part of the chart, but we don't want a tooltip there + _rich_tooltip.visible = false + return _rich_tooltip.visible + +# Create a list of all the values and percentages +# with the hovered one highlighted +func _create_tooltip(labelHovered : String) -> String: + var slice_tooltips : PackedStringArray = [] + for label in _slice_order: + var slice : SliceData = _slices.get(label) + var percent := _format_percent(slice.percentage) + var entry : String = "%s %s%%" % [label, percent] + if label == labelHovered: + entry = "[i][u][b]>>%s<<[/b][/u][/i]" % entry + slice_tooltips.push_back(entry) + # Slices are ordered smallest to largest, but here we want the opposite + slice_tooltips.reverse() + return "[font_size=10]%s[/font_size]" % "\n".join(slice_tooltips) + +# Angle from center.angle_to_point is measured from the +x axis, +# but the chart starts from +y +# The input angle is also -180 to 180, where we want 0 to 360 +func _convert_angle(angleIn : float) -> float: + # Make the angle start from +y, range is now -90 to 270 + var angle := angleIn + PI / 2.0 + # Adjust range to be 0 to 360 + if angle < 0: + angle += 2.0 * PI + return angle + +func _format_percent(percentIn : float) -> float: + return snappedf((percentIn * 100), 0.1) diff --git a/game/src/Game/Theme/PieChart/PieChart.tscn b/game/src/Game/Theme/PieChart/PieChart.tscn new file mode 100644 index 0000000..a0e9992 --- /dev/null +++ b/game/src/Game/Theme/PieChart/PieChart.tscn @@ -0,0 +1,38 @@ +[gd_scene load_steps=2 format=3 uid="uid://cr7p1k2xm7mum"] + +[ext_resource type="Script" path="res://src/Game/Theme/PieChart/PieChart.gd" id="2_ub6u3"] + +[node name="PieChart" type="TextureRect" node_paths=PackedStringArray("_rich_tooltip")] +custom_minimum_size = Vector2(50, 50) +anchors_preset = -1 +anchor_right = 0.039 +anchor_bottom = 0.069 +offset_right = -32.92 +offset_bottom = -34.68 +size_flags_horizontal = 4 +size_flags_vertical = 4 +expand_mode = 3 +script = ExtResource("2_ub6u3") +_rich_tooltip = NodePath("RichToolTip") + +[node name="RichToolTip" type="RichTextLabel" parent="."] +visible = false +top_level = true +layout_mode = 2 +offset_right = 50.0 +offset_bottom = 50.0 +mouse_filter = 2 +bbcode_enabled = true +fit_content = true +autowrap_mode = 0 + +[node name="Panel" type="Panel" parent="RichToolTip"] +show_behind_parent = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[connection signal="mouse_exited" from="." to="." method="_on_mouse_exited"] |