From 505176d9cabe76cff7cdac6b4d4ef1c77ccb00d9 Mon Sep 17 00:00:00 2001 From: Nemrav <> Date: Fri, 28 Jul 2023 22:07:02 -0300 Subject: add piecharts --- game/src/Game/Theme/PieChart/PieChart.gd | 235 +++++++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 game/src/Game/Theme/PieChart/PieChart.gd (limited to 'game/src/Game/Theme/PieChart/PieChart.gd') diff --git a/game/src/Game/Theme/PieChart/PieChart.gd b/game/src/Game/Theme/PieChart/PieChart.gd new file mode 100644 index 0000000..aba1f81 --- /dev/null +++ b/game/src/Game/Theme/PieChart/PieChart.gd @@ -0,0 +1,235 @@ +@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.6,0.6) +@export var shadow_focus : 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 + +#@onready +@export var RichTooltip : RichTextLabel# = $RichToolTip + +#a data class for the pie chart +class SliceData: + #primary properties, change these to change + #the displayed piechart + var colour:Color = Color(1.0,0.0,0.0): + get: + return colour + set(value): + colour = value + var tooltip:String = "DEFAULT": + get: + return tooltip + set(value): + tooltip = value + var quantity:float = -1: + get: + return quantity + set(value): + quantity = value + #derived properties, don't set from an external script + var final_angle:float = -1: + get: + return final_angle + set(value): + final_angle = value + 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 dictionnary should be an easy to reference constant +#The tooltip label is what the user will actually read +var slices: Dictionary = { + +} +#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 addOrReplaceLabel(labelName:String,quantity:float,tooltip:String,colour:Color=Color(0.0,0.0,0.0)) -> void: + slices[labelName] = SliceData.new(quantity,tooltip,colour) + _recalculate() + +func updateLabelQuantity(labelName:String,quantity:float) -> void: + if slices.has(labelName): + slices[labelName].quantity = quantity + _recalculate() + +func updateLabelColour(labelName:String,colour:Color) -> void: + if slices.has(labelName): + slices[labelName].colour = colour + _recalculate() + +func updateLabelTooltip(labelName:String,tooltip:String) -> void: + if slices.has(labelName): + slices[labelName].tooltip = tooltip + +func RemoveLabel(labelName:String) -> bool: + var out = slices.erase(labelName) + _recalculate() + return out + +#Perhaps in the future, a method to reorder the labels? + + +#In editor only, force the shader parameters to update whenever _draw +#is called so developers can see their changes +#otherwise, for performance, reduce the number of material resets +func _draw(): + if Engine.is_editor_hint(): + if not material: + _reset_material() + _setShaderParams() + _recalculate() + +func _ready(): + _reset_material() + _setShaderParams() + +func _reset_material(): + texture = CanvasTexture.new() + var mat_res = load("res://src/Game/Theme/PieChart/PieChartMat.tres") + material = mat_res.duplicate(true) + custom_minimum_size = Vector2(50.0,50.0) + size_flags_horizontal = Control.SIZE_SHRINK_CENTER + size_flags_vertical = Control.SIZE_SHRINK_CENTER + _recalculate() + +func _setShaderParams(): + material.set_shader_parameter("donut",donut) + material.set_shader_parameter("donut_inner_trim",donut_inner_trim) + material.set_shader_parameter("radius",radius) + material.set_shader_parameter("donut_inner_radius",donut_inner_radius/2.0) + + material.set_shader_parameter("trim_colour",Vector3(trim_colour.r,trim_colour.g,trim_colour.b)) + material.set_shader_parameter("trim_size",trim_size) + material.set_shader_parameter("gradient_falloff",slice_gradient_falloff) + material.set_shader_parameter("gradient_base",slice_gradient_base) + + material.set_shader_parameter("shadow_displacement",shadow_displacement) + material.set_shader_parameter("shadow_tightness",shadow_focus) + material.set_shader_parameter("shadow_radius",shadow_radius) + material.set_shader_parameter("shadow_thickness",shadow_thickness) + + +#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 + var angles: Array = [] + var colours: Array = [] + + var total:float = 0 + for slice in slices.values(): + total += slice.quantity + + var current_arc_start:float = 0 + var current_arc_finish:float = 0 + + for slice in slices.values(): + 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(Vector3(slice.colour.r,slice.colour.g,slice.colour.b) ) + material.set_shader_parameter("stopAngles",angles) + material.set_shader_parameter("colours",colours) + + +#Process mouse to select the appropriate tooltip for the slice +func _gui_input(event:InputEvent): + if event is InputEventMouse: + var pos = event.position + var _handled:bool = _handleTooltip(pos) + +func _on_mouse_exited(): + RichTooltip.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 _handleTooltip(pos:Vector2) -> bool: + #is it within the circle? + var center = Vector2(size.x/2.0, size.x/2.0) + var radius = size.x/2.0 + var distance = center.distance_to(pos) + #print(distance >= donut_inner_radius/2.0) + var real_donut_inner_radius:float = radius * donut_inner_radius + if distance <= radius and (not donut or distance >= real_donut_inner_radius): + var angle = _convertAngle(center.angle_to_point(pos)) + for label in slices.keys(): + var slice = slices.get(label) + if angle <= slice.final_angle: + RichTooltip.visible = true + RichTooltip.text = _createTooltip(label) + RichTooltip.position = pos + Vector2(5,5) + get_global_rect().position #get_global_rect().position + + RichTooltip.reset_size() + + return true + else: + #Technically the corners of the bounding box + #are part of the chart, but we don't want a tooltip there + RichTooltip.visible = false + return false + +#create a list of all the values and percentages +# but with the hovered one on top and highlighted +func _createTooltip(labelHovered:String) -> String: + var tooltip:String = "" + var hoveredSlice = slices.get(labelHovered) + var formatted_percent = _formatpercent(hoveredSlice.percentage) + #TOOD: perhaps this is a bit much, but final feedback should determine this + tooltip += "[font_size=10][i][u][b]>> {name} {percentage}% <<[/b][/u][/i]".format( + {"name":hoveredSlice.tooltip,"percentage":formatted_percent}) + + for label in slices.keys(): + if label == labelHovered: continue + var slice = slices.get(label) + var percent = _formatpercent(slice.percentage) + tooltip += "\n{name} {percentage}%".format( + {"name":slice.tooltip,"percentage":percent}) + tooltip += "[/font_size]" + return tooltip + +#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 _convertAngle(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 + angle + return angle + +func _formatpercent(percentIn:float) -> float: + return snappedf((percentIn * 100),0.1) + + + -- cgit v1.2.3-56-ga3b1 From 4c43951e70aaa2e7265d3b3f3c4964c048b9328d Mon Sep 17 00:00:00 2001 From: Hop311 Date: Thu, 10 Aug 2023 12:00:54 +0100 Subject: PieChart data and image now come from c++ layer --- extension/deps/openvic-simulation | 2 +- extension/src/GameSingleton.cpp | 81 ++++- extension/src/GameSingleton.hpp | 16 +- extension/src/Utilities.cpp | 90 ++++++ extension/src/Utilities.hpp | 10 + game/localisation/en_GB/mapmodes.csv | 1 + game/localisation/en_GB/menus.csv | 3 + .../ProvinceOverviewPanel/ProvinceOverviewPanel.gd | 11 +- .../ProvinceOverviewPanel.tscn | 127 +------- game/src/Game/Theme/PieChart/LayeredChart.gd | 28 -- game/src/Game/Theme/PieChart/LayeredChart.tscn | 249 --------------- game/src/Game/Theme/PieChart/PieChart.gd | 355 ++++++++++----------- game/src/Game/Theme/PieChart/PieChart.gdshader | 87 ----- game/src/Game/Theme/PieChart/PieChart.tscn | 117 +------ game/src/Game/Theme/PieChart/PieChartMat.tres | 21 -- game/src/Game/Theme/PieChart/chart_test.gd | 32 -- 16 files changed, 385 insertions(+), 845 deletions(-) delete mode 100644 game/src/Game/Theme/PieChart/LayeredChart.gd delete mode 100644 game/src/Game/Theme/PieChart/LayeredChart.tscn delete mode 100644 game/src/Game/Theme/PieChart/PieChart.gdshader delete mode 100644 game/src/Game/Theme/PieChart/PieChartMat.tres delete mode 100644 game/src/Game/Theme/PieChart/chart_test.gd (limited to 'game/src/Game/Theme/PieChart/PieChart.gd') diff --git a/extension/deps/openvic-simulation b/extension/deps/openvic-simulation index 8a08be3..538e7dc 160000 --- a/extension/deps/openvic-simulation +++ b/extension/deps/openvic-simulation @@ -1 +1 @@ -Subproject commit 8a08be3e7e8477973e243716d431ad7117acfa43 +Subproject commit 538e7dc4ec44c4d09a6a654f10229e6392653a50 diff --git a/extension/src/GameSingleton.cpp b/extension/src/GameSingleton.cpp index efacb8e..baf4d44 100644 --- a/extension/src/GameSingleton.cpp +++ b/extension/src/GameSingleton.cpp @@ -1,7 +1,5 @@ #include "GameSingleton.hpp" -#include - #include #include "openvic/utility/Logger.hpp" @@ -13,8 +11,7 @@ using namespace OpenVic; TerrainVariant::TerrainVariant(std::string const& new_identfier, colour_t new_colour, Ref const& new_image) - : HasIdentifier { new_identfier }, - HasColour { new_colour, true }, + : HasIdentifierAndColour { new_identfier, new_colour, true }, image { new_image } {} Ref 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_culture_info_size_key"), &GameSingleton::get_piechart_info_size_key); + ClassDB::bind_static_method("GameSingleton", D_METHOD("get_culture_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, + 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 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 const& buildings = province->get_buildings(); if (!buildings.empty()) { diff --git a/extension/src/GameSingleton.hpp b/extension/src/GameSingleton.hpp index 23eb334..c4e90e0 100644 --- a/extension/src/GameSingleton.hpp +++ b/extension/src/GameSingleton.hpp @@ -6,7 +6,7 @@ #include "openvic/GameManager.hpp" namespace OpenVic { - struct TerrainVariant : HasIdentifier, HasColour { + struct TerrainVariant : HasIdentifierAndColour { friend class GameSingleton; private: @@ -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 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/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 + #include #include @@ -14,3 +16,91 @@ Ref 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, + 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(x) / static_cast(size), + static_cast(y) / static_cast(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 load_godot_image(godot::String const& path); + + void draw_pie_chart(godot::Ref 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/GameSession/ProvinceOverviewPanel/ProvinceOverviewPanel.gd b/game/src/Game/GameSession/ProvinceOverviewPanel/ProvinceOverviewPanel.gd index 04a035c..0220be2 100644 --- a/game/src/Game/GameSession/ProvinceOverviewPanel/ProvinceOverviewPanel.gd +++ b/game/src/Game/GameSession/ProvinceOverviewPanel/ProvinceOverviewPanel.gd @@ -7,6 +7,8 @@ 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" @@ -114,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) @@ -122,11 +128,6 @@ func _update_info() -> void: for i in max(buildings.size(), _building_rows.size()): _set_building_row(i, buildings[i] if i < buildings.size() else {}) - #PLACEHOLDER for updating piechart - _pop_culture_chart.addOrReplaceLabel("NORTH_GERMAN",50,"North German Culture",Color.DIM_GRAY) - _pop_culture_chart.addOrReplaceLabel("FRENCH",25,"French Culture",Color.BLUE) - _pop_culture_chart.addOrReplaceLabel("SOUTH_GERMAN",7,"South German Culture",Color.FIREBRICK) - show() else: hide() diff --git a/game/src/Game/GameSession/ProvinceOverviewPanel/ProvinceOverviewPanel.tscn b/game/src/Game/GameSession/ProvinceOverviewPanel/ProvinceOverviewPanel.tscn index 9b4c45b..7e49ac8 100644 --- a/game/src/Game/GameSession/ProvinceOverviewPanel/ProvinceOverviewPanel.tscn +++ b/game/src/Game/GameSession/ProvinceOverviewPanel/ProvinceOverviewPanel.tscn @@ -1,118 +1,9 @@ -[gd_scene load_steps=6 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"] -[sub_resource type="Shader" id="Shader_2k3yf"] -code = "shader_type canvas_item; - -// The center in UV coordinates, which will always -//be 0.5, the actual radius will be controlled by the control node -//const vec2 center = vec2(0.5,0.5); -uniform float radius = 0.4; - -//shadow -uniform vec2 shadow_displacement = vec2(0.75,0.75); -uniform float shadow_tightness = 10; -uniform float shadow_radius = 0.7; -uniform float shadow_thickness = 1.0; - -// Control of the slices -uniform float stopAngles[5]; -uniform vec3 colours[5]; - -// Trim -uniform vec3 trim_colour; -uniform float trim_size = 0.05; - -// The center is spotlighted by the gradient, -//control its size and falloff with these -uniform float gradient_falloff = 3.6; -uniform float gradient_base = 3.1; - -// control whether this is a donut instead of a pie chart -uniform bool donut = false; -uniform bool donut_inner_trim = false; -uniform float donut_inner_radius = 0.15; - -// get the polar coordinates of a pixel relative to the center -vec2 getPolar(vec2 UVin, vec2 center){ - vec2 relcoord = (UVin-center); - float dist = length(relcoord); - float theta = PI/2.0 + atan((relcoord.y)/(relcoord.x)); - if(UVin.x < 0.5){ - theta += PI; - } - return vec2(dist,theta); -} - -// from thebookofshaders, returns a gradient falloff -float parabola( float base, float x, float k ){ - return pow( base*x*(1.0-x), k ); -} - -float parabola_shadow(float base, float x){ - return base*x*x; -} - -void fragment() { - vec2 coords = getPolar(UV,vec2(0.5,0.5)); - float dist = coords.x; - float theta = coords.y; - - vec2 shadow_polar = getPolar(UV,vec2(0.0+shadow_displacement.x,0.0+shadow_displacement.y)); - 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){ - COLOR = vec4(0.1,0.1,0.1,shadow_gradient); - } - // inner trim - else if(donut && donut_inner_trim && dist <= donut_inner_radius + trim_size){ - COLOR = vec4(trim_colour,1.0); - } - // interior - else if(dist <= radius-trim_size){ - for(int i=0;i void: - slices[labelName] = SliceData.new(quantity,tooltip,colour) +# 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 updateLabelQuantity(labelName:String,quantity:float) -> void: - if slices.has(labelName): - slices[labelName].quantity = quantity +func update_label_quantity(labelName : String, quantity : float) -> void: + if _slices.has(labelName): + _slices[labelName].quantity = quantity _recalculate() -func updateLabelColour(labelName:String,colour:Color) -> void: - if slices.has(labelName): - slices[labelName].colour = colour +func update_label_colour(labelName : String, colour : Color) -> void: + if _slices.has(labelName): + _slices[labelName].colour = colour _recalculate() -func updateLabelTooltip(labelName:String,tooltip:String) -> void: - if slices.has(labelName): - slices[labelName].tooltip = tooltip +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 RemoveLabel(labelName:String) -> bool: - var out = slices.erase(labelName) +func clear_slices() -> void: + _slices.clear() + _slice_order.clear() + +# Distribution dictionary of the form: +# { "