aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
m---------extension/deps/openvic-simulation0
-rw-r--r--extension/src/openvic-extension/classes/GUIListBox.cpp22
-rw-r--r--extension/src/openvic-extension/classes/GUINode.cpp3
-rw-r--r--extension/src/openvic-extension/classes/GUINode.hpp3
-rw-r--r--extension/src/openvic-extension/singletons/GameSingleton.cpp2
-rw-r--r--extension/src/openvic-extension/singletons/GameSingleton.hpp2
-rw-r--r--extension/src/openvic-extension/singletons/MenuSingleton.cpp186
-rw-r--r--extension/src/openvic-extension/singletons/MenuSingleton.hpp23
-rw-r--r--extension/src/openvic-extension/singletons/ModelSingleton.cpp4
-rw-r--r--extension/src/openvic-extension/singletons/PopulationMenu.cpp2
-rw-r--r--extension/src/openvic-extension/utility/UITools.cpp100
-rw-r--r--game/src/Game/GameSession/GameSession.tscn10
-rw-r--r--game/src/Game/GameSession/MapControlPanel/MapControlPanel.gd4
-rw-r--r--game/src/Game/GameSession/MapControlPanel/MapControlPanel.tscn3
-rw-r--r--game/src/Game/GameSession/MapView.gd15
-rw-r--r--game/src/Game/GameSession/SearchPanel.gd143
16 files changed, 476 insertions, 46 deletions
diff --git a/extension/deps/openvic-simulation b/extension/deps/openvic-simulation
-Subproject 861acff78bd238232ed9e369ea046c2ed4a3119
+Subproject de190028555cb1b70ca9fbbd10af3de4440b8b8
diff --git a/extension/src/openvic-extension/classes/GUIListBox.cpp b/extension/src/openvic-extension/classes/GUIListBox.cpp
index c153870..e3bea86 100644
--- a/extension/src/openvic-extension/classes/GUIListBox.cpp
+++ b/extension/src/openvic-extension/classes/GUIListBox.cpp
@@ -9,6 +9,7 @@
using namespace OpenVic;
using namespace godot;
+using namespace OpenVic::Utilities::literals;
using OpenVic::Utilities::std_view_to_godot_string;
@@ -24,14 +25,14 @@ Error GUIListBox::_calculate_max_scroll_index(bool signal) {
if (fixed_item_count <= 0) {
max_scroll_index = 0;
fixed_visible_items = 0;
- } else if (fixed_item_height <= 0.0f) {
+ } else if (fixed_item_height <= 0.0_real) {
max_scroll_index = fixed_item_count - 1;
fixed_visible_items = max_scroll_index;
} else {
const real_t max_height = get_size().height;
fixed_visible_items = max_height / fixed_item_height;
- max_scroll_index = fixed_item_count - std::max(fixed_visible_items, 1);
+ max_scroll_index = std::max(fixed_item_count - std::max(fixed_visible_items, 1), 0);
}
} else {
const int32_t child_count = get_child_count();
@@ -41,7 +42,7 @@ Error GUIListBox::_calculate_max_scroll_index(bool signal) {
} else {
const real_t max_height = get_size().height;
- real_t height_under_max_scroll_index = 0.0f;
+ real_t height_under_max_scroll_index = 0.0_real;
max_scroll_index = child_count;
@@ -75,8 +76,9 @@ Error GUIListBox::_calculate_max_scroll_index(bool signal) {
Error GUIListBox::_update_child_positions() {
const int32_t child_count = get_child_count();
const real_t max_height = get_size().height;
+ const Vector2 offset = gui_listbox != nullptr ? Utilities::to_godot_fvec2(gui_listbox->get_items_offset()) : Vector2 {};
- real_t height = 0.0f;
+ real_t height = 0.0_real;
const int32_t child_scroll_index = fixed ? 0 : scroll_index;
@@ -87,7 +89,7 @@ Error GUIListBox::_update_child_positions() {
if (index < child_scroll_index) {
child->hide();
} else {
- child->set_position({ 0.0f, height });
+ child->set_position(offset + Vector2 { 0.0_real, height });
height += child->get_size().height; /* Spacing is ignored */
@@ -130,7 +132,7 @@ void GUIListBox::_notification(int what) {
GUIListBox::GUIListBox()
: gui_listbox { nullptr }, scrollbar { nullptr }, scroll_index { 0 }, max_scroll_index { 0 },
- fixed { false }, fixed_item_count { 0 }, fixed_visible_items { 0 }, fixed_item_height { 0.0f } {}
+ fixed { false }, fixed_item_count { 0 }, fixed_visible_items { 0 }, fixed_item_height { 0.0_real } {}
Vector2 GUIListBox::_get_minimum_size() const {
if (gui_listbox != nullptr) {
@@ -177,7 +179,7 @@ void GUIListBox::clear() {
fixed = false;
fixed_item_count = 0;
fixed_visible_items = 0;
- fixed_item_height = 0.0f;
+ fixed_item_height = 0.0_real;
clear_children();
if (scrollbar != nullptr) {
@@ -233,7 +235,7 @@ Error GUIListBox::unset_fixed(bool signal) {
fixed = false;
fixed_item_count = 0;
- fixed_item_height = 0.0f;
+ fixed_item_height = 0.0_real;
return _calculate_max_scroll_index(signal);
}
@@ -273,7 +275,9 @@ Error GUIListBox::set_gui_listbox(GUI::ListBox const* new_gui_listbox) {
add_child(scrollbar, false, INTERNAL_MODE_FRONT);
const Size2 size = Utilities::to_godot_fvec2(gui_listbox->get_size());
- scrollbar->set_position({ size.width, 0.0f });
+ Vector2 position = Utilities::to_godot_fvec2(gui_listbox->get_scrollbar_offset());
+ position.x += size.width;
+ scrollbar->set_position(position);
scrollbar->set_length_override(size.height);
static const StringName set_scroll_index_func_name = "set_scroll_index";
diff --git a/extension/src/openvic-extension/classes/GUINode.cpp b/extension/src/openvic-extension/classes/GUINode.cpp
index d8ab6c1..bd8197b 100644
--- a/extension/src/openvic-extension/classes/GUINode.cpp
+++ b/extension/src/openvic-extension/classes/GUINode.cpp
@@ -46,7 +46,8 @@ using namespace OpenVic;
F(TextureRect, texture_rect) \
F(GUIOverlappingElementsBox, gui_overlapping_elements_box) \
F(GUIScrollbar, gui_scrollbar) \
- F(GUIListBox, gui_listbox)
+ F(GUIListBox, gui_listbox) \
+ F(LineEdit, line_edit)
#define APPLY_TO_TEXTURE_TYPES(F) \
F(GFXSpriteTexture, gfx_sprite_texture) \
diff --git a/extension/src/openvic-extension/classes/GUINode.hpp b/extension/src/openvic-extension/classes/GUINode.hpp
index 27ce780..f8eb62c 100644
--- a/extension/src/openvic-extension/classes/GUINode.hpp
+++ b/extension/src/openvic-extension/classes/GUINode.hpp
@@ -6,6 +6,7 @@
#include <godot_cpp/classes/image.hpp>
#include <godot_cpp/classes/input_event.hpp>
#include <godot_cpp/classes/label.hpp>
+#include <godot_cpp/classes/line_edit.hpp>
#include <godot_cpp/classes/node.hpp>
#include <godot_cpp/classes/panel.hpp>
#include <godot_cpp/classes/ref.hpp>
@@ -58,6 +59,7 @@ namespace OpenVic {
static GUIOverlappingElementsBox* get_gui_overlapping_elements_box_from_node(godot::Node* node);
static GUIScrollbar* get_gui_scrollbar_from_node(godot::Node* node);
static GUIListBox* get_gui_listbox_from_node(godot::Node* node);
+ static godot::LineEdit* get_line_edit_from_node(godot::Node* node);
godot::Button* get_button_from_nodepath(godot::NodePath const& path) const;
godot::Label* get_label_from_nodepath(godot::NodePath const& path) const;
@@ -67,6 +69,7 @@ namespace OpenVic {
GUIOverlappingElementsBox* get_gui_overlapping_elements_box_from_nodepath(godot::NodePath const& path) const;
GUIScrollbar* get_gui_scrollbar_from_nodepath(godot::NodePath const& path) const;
GUIListBox* get_gui_listbox_from_nodepath(godot::NodePath const& path) const;
+ godot::LineEdit* get_line_edit_from_nodepath(godot::NodePath const& path) const;
/* Helper functions to get textures from TextureRects and Buttons. */
static godot::Ref<godot::Texture2D> get_texture_from_node(godot::Node* node);
diff --git a/extension/src/openvic-extension/singletons/GameSingleton.cpp b/extension/src/openvic-extension/singletons/GameSingleton.cpp
index c1a811e..33d70da 100644
--- a/extension/src/openvic-extension/singletons/GameSingleton.cpp
+++ b/extension/src/openvic-extension/singletons/GameSingleton.cpp
@@ -170,7 +170,7 @@ float GameSingleton::get_map_aspect_ratio() const {
return static_cast<float>(get_map_width()) / static_cast<float>(get_map_height());
}
-Vector2 GameSingleton::map_position_to_world_coords(fvec2_t const& position) const {
+Vector2 GameSingleton::normalise_map_position(fvec2_t const& position) const {
return Utilities::to_godot_fvec2(position) / get_map_dims();
}
diff --git a/extension/src/openvic-extension/singletons/GameSingleton.hpp b/extension/src/openvic-extension/singletons/GameSingleton.hpp
index b29e588..e7f12bd 100644
--- a/extension/src/openvic-extension/singletons/GameSingleton.hpp
+++ b/extension/src/openvic-extension/singletons/GameSingleton.hpp
@@ -86,7 +86,7 @@ namespace OpenVic {
int32_t get_map_height() const;
godot::Vector2i get_map_dims() const;
float get_map_aspect_ratio() const;
- godot::Vector2 map_position_to_world_coords(fvec2_t const& position) const;
+ godot::Vector2 normalise_map_position(fvec2_t const& position) const;
/* The cosmetic terrain textures stored in a Texture2DArray. */
godot::Ref<godot::Texture2DArray> get_terrain_texture() const;
diff --git a/extension/src/openvic-extension/singletons/MenuSingleton.cpp b/extension/src/openvic-extension/singletons/MenuSingleton.cpp
index 1c3b6ec..885915c 100644
--- a/extension/src/openvic-extension/singletons/MenuSingleton.cpp
+++ b/extension/src/openvic-extension/singletons/MenuSingleton.cpp
@@ -28,6 +28,10 @@ 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;
}
+StringName const& MenuSingleton::_signal_search_cache_changed() {
+ static const StringName signal_search_cache_changed = "search_cache_changed";
+ return signal_search_cache_changed;
+}
String MenuSingleton::get_state_name(State const& state) const {
StateSet const& state_set = state.get_state_set();
@@ -59,14 +63,47 @@ String MenuSingleton::get_state_name(State const& state) const {
if (owned && split) {
// COUNTRY STATE/CAPITAL
- return tr(std_view_to_godot_string(StringUtils::append_string_views(state.get_owner()->get_identifier(), "_ADJ")))
- + " " + name;
+ return get_country_adjective(*state.get_owner()) + " " + name;
}
// STATE/CAPITAL
return name;
}
+String MenuSingleton::get_country_name(CountryInstance const& country) const {
+ if (country.get_government_type() != nullptr && !country.get_government_type()->get_identifier().empty()) {
+ const String government_name_key = std_to_godot_string(StringUtils::append_string_views(
+ country.get_identifier(), "_", country.get_government_type()->get_identifier()
+ ));
+
+ String government_name = tr(government_name_key);
+
+ if (government_name != government_name_key) {
+ return government_name;
+ }
+ }
+
+ return tr(std_view_to_godot_string(country.get_identifier()));
+}
+
+String MenuSingleton::get_country_adjective(CountryInstance const& country) const {
+ static constexpr std::string_view adjective = "_ADJ";
+
+ if (country.get_government_type() != nullptr && !country.get_government_type()->get_identifier().empty()) {
+ const String government_adjective_key = std_to_godot_string(StringUtils::append_string_views(
+ country.get_identifier(), "_", country.get_government_type()->get_identifier(), adjective
+ ));
+
+ String government_adjective = tr(government_adjective_key);
+
+ if (government_adjective != government_adjective_key) {
+ return government_adjective;
+ }
+ }
+
+ return tr(std_to_godot_string(StringUtils::append_string_views(country.get_identifier(), adjective)));
+}
+
void MenuSingleton::_bind_methods() {
/* PROVINCE OVERVIEW PANEL */
OV_BIND_METHOD(MenuSingleton::get_province_info_from_index, { "index" });
@@ -142,6 +179,15 @@ void MenuSingleton::_bind_methods() {
BIND_ENUM_CONSTANT(SORT_REBEL_FACTION);
BIND_ENUM_CONSTANT(SORT_SIZE_CHANGE);
BIND_ENUM_CONSTANT(SORT_LITERACY);
+
+ /* Find/Search Panel */
+ OV_BIND_METHOD(MenuSingleton::generate_search_cache);
+ OV_BIND_METHOD(MenuSingleton::update_search_results, { "text" });
+ OV_BIND_METHOD(MenuSingleton::get_search_result_rows, { "start", "count" });
+ OV_BIND_METHOD(MenuSingleton::get_search_result_row_count);
+ OV_BIND_METHOD(MenuSingleton::get_search_result_position, { "result_index" });
+
+ ADD_SIGNAL(MethodInfo(_signal_search_cache_changed()));
}
MenuSingleton* MenuSingleton::get_singleton() {
@@ -442,3 +488,139 @@ String MenuSingleton::get_longform_date() const {
return Utilities::date_to_formatted_string(instance_manager->get_today());
}
+
+Error MenuSingleton::generate_search_cache() {
+ GameSingleton const* game_singleton = GameSingleton::get_singleton();
+ ERR_FAIL_NULL_V(game_singleton, FAILED);
+ InstanceManager const* instance_manager = game_singleton->get_instance_manager();
+ ERR_FAIL_NULL_V(instance_manager, FAILED);
+
+ search_panel.entry_cache.clear();
+
+ std::vector<ProvinceInstance> const& provinces = instance_manager->get_map_instance().get_province_instances();
+ std::vector<StateSet> const& state_sets = instance_manager->get_map_instance().get_state_manager().get_state_sets();
+ std::vector<CountryInstance> const& countries = instance_manager->get_country_instance_manager().get_country_instances();
+
+ // TODO - reserve actual state count rather than state set count (maybe use a vector of pointers to all states?)
+ search_panel.entry_cache.reserve(provinces.size() + state_sets.size() + countries.size());
+
+ for (ProvinceInstance const& province : provinces) {
+ String identifier = std_view_to_godot_string(province.get_identifier());
+ String display_name = tr(GUINode::format_province_name(identifier));
+ String search_name = display_name.to_lower();
+
+ search_panel.entry_cache.push_back({
+ &province, std::move(display_name), std::move(search_name), identifier.to_lower()
+ });
+ }
+
+ for (StateSet const& state_set : state_sets) {
+ for (State const& state : state_set.get_states()) {
+ String display_name = get_state_name(state);
+ String search_name = display_name.to_lower();
+
+ search_panel.entry_cache.push_back({
+ // TODO - include state identifier? (region and/or split?)
+ &state, std::move(display_name), std::move(search_name), {}
+ });
+ }
+ }
+
+ for (CountryInstance const& country : countries) {
+ // TODO - replace with a proper "exists" check
+ if (country.get_capital() != nullptr) {
+ String display_name = get_country_name(country);
+ String search_name = display_name.to_lower();
+
+ search_panel.entry_cache.push_back({
+ &country, std::move(display_name), std::move(search_name),
+ std_view_to_godot_string(country.get_identifier()).to_lower()
+ });
+ }
+ }
+
+ std::sort(search_panel.entry_cache.begin(), search_panel.entry_cache.end(), [](auto const& a, auto const& b) -> bool {
+ return a.search_name < b.search_name;
+ });
+
+ emit_signal(_signal_search_cache_changed());
+
+ return OK;
+}
+
+void MenuSingleton::update_search_results(godot::String const& text) {
+ // Sanatise input
+ const String search_text = text.strip_edges().to_lower();
+
+ search_panel.result_indices.clear();
+
+ if (!search_text.is_empty()) {
+ // Search through cache
+ for (size_t idx = 0; idx < search_panel.entry_cache.size(); ++idx) {
+ search_panel_t::entry_t const& entry = search_panel.entry_cache[idx];
+
+ if (entry.search_name.begins_with(search_text) || entry.identifier == search_text) {
+ search_panel.result_indices.push_back(idx);
+ }
+ }
+ }
+}
+
+PackedStringArray MenuSingleton::get_search_result_rows(int32_t start, int32_t count) const {
+ if (search_panel.result_indices.empty()) {
+ return {};
+ }
+
+ ERR_FAIL_INDEX_V_MSG(
+ start, search_panel.result_indices.size(), {},
+ vformat("Invalid start for search panel result rows: %d", start)
+ );
+ ERR_FAIL_COND_V_MSG(count <= 0, {}, vformat("Invalid count for search panel result rows: %d", count));
+
+ if (start + count > search_panel.result_indices.size()) {
+ UtilityFunctions::push_warning(
+ "Requested search result rows beyond the end of the result indices (", start, " + ", count, " > ",
+ static_cast<int64_t>(search_panel.result_indices.size()), "), limiting to ",
+ static_cast<int64_t>(search_panel.result_indices.size() - start), " rows."
+ );
+ count = search_panel.result_indices.size() - start;
+ }
+
+ PackedStringArray results;
+ results.resize(count);
+
+ for (size_t idx = 0; idx < count; ++idx) {
+ results[idx] = search_panel.entry_cache[search_panel.result_indices[start + idx]].display_name;
+ }
+
+ return results;
+}
+
+int32_t MenuSingleton::get_search_result_row_count() const {
+ return search_panel.result_indices.size();
+}
+
+Vector2 MenuSingleton::get_search_result_position(int32_t result_index) const {
+ ERR_FAIL_INDEX_V(result_index, search_panel.result_indices.size(), {});
+
+ GameSingleton const* game_singleton = GameSingleton::get_singleton();
+ ERR_FAIL_NULL_V(game_singleton, {});
+
+ struct entry_visitor_t {
+ fvec2_t operator()(ProvinceInstance const* province) {
+ return province->get_province_definition().get_centre();
+ }
+
+ fvec2_t operator()(State const* state) {
+ return (*this)(state->get_capital());
+ }
+
+ fvec2_t operator()(CountryInstance const* country) {
+ return (*this)(country->get_capital());
+ }
+ } entry_visitor;
+
+ return game_singleton->normalise_map_position(
+ std::visit(entry_visitor, search_panel.entry_cache[search_panel.result_indices[result_index]].target)
+ );
+}
diff --git a/extension/src/openvic-extension/singletons/MenuSingleton.hpp b/extension/src/openvic-extension/singletons/MenuSingleton.hpp
index 54891bc..190e3ea 100644
--- a/extension/src/openvic-extension/singletons/MenuSingleton.hpp
+++ b/extension/src/openvic-extension/singletons/MenuSingleton.hpp
@@ -73,8 +73,18 @@ namespace OpenVic {
std::vector<Pop const*> pops, filtered_pops;
};
+ struct search_panel_t {
+ struct entry_t {
+ std::variant<ProvinceInstance const*, State const*, CountryInstance const*> target;
+ godot::String display_name, search_name, identifier;
+ };
+ std::vector<entry_t> entry_cache;
+ std::vector<size_t> result_indices;
+ };
+
private:
population_menu_t population_menu;
+ search_panel_t search_panel;
/* 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();
@@ -83,8 +93,12 @@ namespace OpenVic {
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();
+ /* Emitted when the collection of possible search results changes. */
+ static godot::StringName const& _signal_search_cache_changed();
godot::String get_state_name(State const& state) const;
+ godot::String get_country_name(CountryInstance const& country) const;
+ godot::String get_country_adjective(CountryInstance const& country) const;
protected:
static void _bind_methods();
@@ -145,6 +159,15 @@ namespace OpenVic {
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;
+
+ /* Find/Search Panel */
+ // TODO - update on country government type change and state creation/destruction
+ // (which automatically includes country creation/destruction)
+ godot::Error generate_search_cache();
+ void update_search_results(godot::String const& text);
+ godot::PackedStringArray get_search_result_rows(int32_t start, int32_t count) const;
+ int32_t get_search_result_row_count() const;
+ godot::Vector2 get_search_result_position(int32_t result_index) const;
};
}
diff --git a/extension/src/openvic-extension/singletons/ModelSingleton.cpp b/extension/src/openvic-extension/singletons/ModelSingleton.cpp
index 7e0e020..ebdc7e8 100644
--- a/extension/src/openvic-extension/singletons/ModelSingleton.cpp
+++ b/extension/src/openvic-extension/singletons/ModelSingleton.cpp
@@ -309,7 +309,7 @@ bool ModelSingleton::add_unit_dict(
}
dict[position_key] =
- game_singleton->map_position_to_world_coords(unit.get_position()->get_province_definition().get_unit_position());
+ game_singleton->normalise_map_position(unit.get_position()->get_province_definition().get_unit_position());
if (display_unit_type->get_unit_category() != UnitType::unit_category_t::INFANTRY) {
dict[rotation_key] = -0.25f * std::numbers::pi_v<float>;
@@ -447,7 +447,7 @@ bool ModelSingleton::add_building_dict(
dict[model_key] = get_model_dict(*actor);
- dict[position_key] = game_singleton->map_position_to_world_coords(
+ dict[position_key] = game_singleton->normalise_map_position(
position_ptr != nullptr ? *position_ptr : province_definition.get_centre()
);
diff --git a/extension/src/openvic-extension/singletons/PopulationMenu.cpp b/extension/src/openvic-extension/singletons/PopulationMenu.cpp
index df5b6b1..65987ad 100644
--- a/extension/src/openvic-extension/singletons/PopulationMenu.cpp
+++ b/extension/src/openvic-extension/singletons/PopulationMenu.cpp
@@ -110,7 +110,7 @@ TypedArray<Dictionary> MenuSingleton::get_population_menu_province_list_rows(int
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[name_key] = menu_singleton.get_country_name(country_entry.country);
country_dict[size_key] = country_entry.country.get_total_population();
country_dict[change_key] = 0;
country_dict[selected_key] = country_entry.selected;
diff --git a/extension/src/openvic-extension/utility/UITools.cpp b/extension/src/openvic-extension/utility/UITools.cpp
index 3c7e04f..93fe9fe 100644
--- a/extension/src/openvic-extension/utility/UITools.cpp
+++ b/extension/src/openvic-extension/utility/UITools.cpp
@@ -3,7 +3,9 @@
#include <godot_cpp/classes/button.hpp>
#include <godot_cpp/classes/color_rect.hpp>
#include <godot_cpp/classes/label.hpp>
+#include <godot_cpp/classes/line_edit.hpp>
#include <godot_cpp/classes/panel.hpp>
+#include <godot_cpp/classes/style_box_empty.hpp>
#include <godot_cpp/classes/style_box_texture.hpp>
#include <godot_cpp/classes/texture_progress_bar.hpp>
#include <godot_cpp/classes/texture_rect.hpp>
@@ -111,7 +113,9 @@ static bool new_control(T*& node, GUI::Element const& element, String const& nam
return ret;
}
-static bool add_theme_stylebox(Control* control, StringName const& theme_name, Ref<Texture2D> const& texture) {
+static bool add_theme_stylebox(
+ Control* control, StringName const& theme_name, Ref<Texture2D> const& texture, Vector2 border = {}
+) {
Ref<StyleBoxTexture> stylebox;
stylebox.instantiate();
ERR_FAIL_NULL_V(stylebox, false);
@@ -121,6 +125,13 @@ static bool add_theme_stylebox(Control* control, StringName const& theme_name, R
static const StringName emit_changed_func = "emit_changed";
texture->connect(changed_signal, Callable { *stylebox, emit_changed_func }, Object::CONNECT_PERSIST);
+ if (border != Vector2 {}) {
+ stylebox->set_texture_margin(SIDE_LEFT, border.x);
+ stylebox->set_texture_margin(SIDE_RIGHT, border.x);
+ stylebox->set_texture_margin(SIDE_TOP, border.y);
+ stylebox->set_texture_margin(SIDE_BOTTOM, border.y);
+ }
+
control->add_theme_stylebox_override(theme_name, stylebox);
return true;
};
@@ -574,37 +585,78 @@ static bool generate_listbox(generate_gui_args_t&& args) {
return ret;
}
-template<std::derived_from<GUI::Element> T>
-requires requires(T const& element) {
- { element.get_size() } -> std::same_as<fvec2_t>;
-}
-static bool generate_placeholder(generate_gui_args_t&& args, Color colour) {
- T const& cast_element = static_cast<T const&>(args.element);
+static bool generate_texteditbox(generate_gui_args_t&& args) {
+ using namespace OpenVic::Utilities::literals;
- static const String type_name = std_view_to_godot_string(T::get_type_static());
- const String placeholder_name = std_view_to_godot_string(cast_element.get_name());
- const Vector2 godot_size = Utilities::to_godot_fvec2(cast_element.get_size());
+ GUI::TextEditBox const& text_edit_box = static_cast<GUI::TextEditBox const&>(args.element);
- UtilityFunctions::push_warning(
- "Generating placeholder ColorRect for GUI ", type_name, " ", placeholder_name, " (size ", godot_size, ")"
- );
+ const String text_edit_box_name = std_view_to_godot_string(text_edit_box.get_name());
- ColorRect* godot_rect = nullptr;
- bool ret = new_control(godot_rect, cast_element, args.name);
+ LineEdit* godot_line_edit = nullptr;
+ bool ret = new_control(godot_line_edit, text_edit_box, args.name);
ERR_FAIL_NULL_V_MSG(
- godot_rect, false, vformat("Failed to create placeholder ColorRect for GUI %s %s", type_name, placeholder_name)
+ godot_line_edit, false, vformat("Failed to create LineEdit for GUI text edit box %s", text_edit_box_name)
);
- godot_rect->set_custom_minimum_size(godot_size);
- godot_rect->set_color(colour);
+ godot_line_edit->set_context_menu_enabled(false);
+ godot_line_edit->set_caret_blink_enabled(true);
+ godot_line_edit->set_focus_mode(Control::FOCUS_CLICK);
- args.result = godot_rect;
- return ret;
-}
+ godot_line_edit->set_text(std_view_to_godot_string(text_edit_box.get_text()));
-static bool generate_texteditbox(generate_gui_args_t&& args) {
- using namespace OpenVic::Utilities::literals;
- return generate_placeholder<GUI::TextEditBox>(std::move(args), { 0.0_real, 1.0_real, 0.0_real, 0.3_real });
+ static const Vector2 default_position_padding { -4.0_real, 1.0_real };
+ static const Vector2 default_size_padding { 2.0_real, 2.0_real };
+ const Vector2 border_size = Utilities::to_godot_fvec2(text_edit_box.get_border_size());
+ const Vector2 max_size = Utilities::to_godot_fvec2(text_edit_box.get_size());
+ godot_line_edit->set_position(godot_line_edit->get_position() + border_size + default_position_padding);
+ godot_line_edit->set_custom_minimum_size(max_size - border_size - default_size_padding);
+
+ static const StringName caret_color_theme = "caret_color";
+ static const Color caret_colour { 1.0_real, 0.0_real, 0.0_real };
+ godot_line_edit->add_theme_color_override(caret_color_theme, caret_colour);
+
+ if (text_edit_box.get_font() != nullptr) {
+ const StringName font_file = std_view_to_godot_string_name(text_edit_box.get_font()->get_fontname());
+ const Ref<Font> font = args.asset_manager.get_font(font_file);
+ if (font.is_valid()) {
+ static const StringName font_theme = "font";
+ godot_line_edit->add_theme_font_override(font_theme, font);
+ } else {
+ UtilityFunctions::push_error("Failed to load font \"", font_file, "\" for GUI text edit box", text_edit_box_name);
+ ret = false;
+ }
+ const Color colour = Utilities::to_godot_color(text_edit_box.get_font()->get_colour());
+ static const StringName font_color_theme = "font_color";
+ godot_line_edit->add_theme_color_override(font_color_theme, colour);
+ }
+
+ const StringName texture_file = std_view_to_godot_string_name(text_edit_box.get_texture_file());
+ if (!texture_file.is_empty()) {
+ Ref<ImageTexture> texture = args.asset_manager.get_texture(texture_file);
+
+ if (texture.is_valid()) {
+ static const StringName normal_theme = "normal";
+ ret &= add_theme_stylebox(godot_line_edit, normal_theme, texture, border_size);
+ } else {
+ UtilityFunctions::push_error(
+ "Failed to load texture \"", texture_file, "\" for text edit box \"", text_edit_box_name, "\""
+ );
+ ret = false;
+ }
+ }
+
+ Ref<StyleBoxEmpty> stylebox_empty;
+ stylebox_empty.instantiate();
+ if (stylebox_empty.is_valid()) {
+ static const StringName focus_theme = "focus";
+ godot_line_edit->add_theme_stylebox_override(focus_theme, stylebox_empty);
+ } else {
+ UtilityFunctions::push_error("Failed to create empty style box for focus of GUI text edit box ", text_edit_box_name);
+ ret = false;
+ }
+
+ args.result = godot_line_edit;
+ return ret;
}
static bool generate_scrollbar(generate_gui_args_t&& args) {
diff --git a/game/src/Game/GameSession/GameSession.tscn b/game/src/Game/GameSession/GameSession.tscn
index d54970f..018aad8 100644
--- a/game/src/Game/GameSession/GameSession.tscn
+++ b/game/src/Game/GameSession/GameSession.tscn
@@ -1,4 +1,4 @@
-[gd_scene load_steps=19 format=3 uid="uid://bgnupcshe1m7r"]
+[gd_scene load_steps=20 format=3 uid="uid://bgnupcshe1m7r"]
[ext_resource type="Script" path="res://src/Game/GameSession/GameSession.gd" id="1_eklvp"]
[ext_resource type="PackedScene" uid="uid://cvl76duuym1wq" path="res://src/Game/MusicConductor/MusicPlayer.tscn" id="2_kt6aa"]
@@ -9,6 +9,7 @@
[ext_resource type="PackedScene" uid="uid://dkehmdnuxih2r" path="res://src/Game/GameSession/MapView.tscn" id="4_xkg5j"]
[ext_resource type="Script" path="res://src/Game/GameSession/NationManagementScreen/ProductionMenu.gd" id="5_16755"]
[ext_resource type="Script" path="res://src/Game/GameSession/ProvinceOverviewPanel.gd" id="5_lfv8l"]
+[ext_resource type="Script" path="res://src/Game/GameSession/SearchPanel.gd" id="5_t260f"]
[ext_resource type="PackedScene" uid="uid://cnbfxjy1m6wja" path="res://src/Game/Menu/OptionMenu/OptionsMenu.tscn" id="6_p5mnx"]
[ext_resource type="Script" path="res://src/Game/GameSession/NationManagementScreen/BudgetMenu.gd" id="6_vninv"]
[ext_resource type="Script" path="res://src/Game/GameSession/NationManagementScreen/TechnologyMenu.gd" id="7_r712c"]
@@ -111,6 +112,12 @@ anchor_bottom = 1.0
grow_horizontal = 0
grow_vertical = 0
+[node name="SearchPanel" type="GUINode" parent="UICanvasLayer/UI" node_paths=PackedStringArray("_map_view")]
+layout_mode = 1
+anchors_preset = 15
+script = ExtResource("5_t260f")
+_map_view = NodePath("../../../MapView")
+
[node name="GameSessionMenu" parent="UICanvasLayer/UI" instance=ExtResource("3_bvmqh")]
visible = false
layout_mode = 1
@@ -153,6 +160,7 @@ grow_horizontal = 0
[connection signal="map_view_camera_changed" from="MapView" to="UICanvasLayer/UI/MapControlPanel" method="_on_map_view_camera_changed"]
[connection signal="game_session_menu_button_pressed" from="UICanvasLayer/UI/MapControlPanel" to="." method="_on_game_session_menu_button_pressed"]
[connection signal="minimap_clicked" from="UICanvasLayer/UI/MapControlPanel" to="MapView" method="_on_minimap_clicked"]
+[connection signal="search_button_pressed" from="UICanvasLayer/UI/MapControlPanel" to="UICanvasLayer/UI/SearchPanel" method="toggle_visibility"]
[connection signal="zoom_in_button_pressed" from="UICanvasLayer/UI/MapControlPanel" to="MapView" method="zoom_in"]
[connection signal="zoom_out_button_pressed" from="UICanvasLayer/UI/MapControlPanel" to="MapView" method="zoom_out"]
[connection signal="load_button_pressed" from="UICanvasLayer/UI/GameSessionMenu" to="UICanvasLayer/UI/SaveLoadMenu" method="show_for_load"]
diff --git a/game/src/Game/GameSession/MapControlPanel/MapControlPanel.gd b/game/src/Game/GameSession/MapControlPanel/MapControlPanel.gd
index eb4dd9f..61de6ae 100644
--- a/game/src/Game/GameSession/MapControlPanel/MapControlPanel.gd
+++ b/game/src/Game/GameSession/MapControlPanel/MapControlPanel.gd
@@ -1,6 +1,7 @@
extends PanelContainer
signal game_session_menu_button_pressed
+signal search_button_pressed
signal map_view_camera_changed(near_left : Vector2, far_left : Vector2, far_right : Vector2, near_right : Vector2)
signal minimap_clicked(pos_clicked : Vector2)
signal zoom_in_button_pressed
@@ -35,6 +36,9 @@ func _ready() -> void:
func _on_game_session_menu_button_pressed() -> void:
game_session_menu_button_pressed.emit()
+func _on_search_button_pressed() -> void:
+ search_button_pressed.emit()
+
# REQUIREMENTS:
# * SS-76
# * UIFUN-129, UIFUN-131, UIFUN-133, UIFUN-140, UIFUN-141, UIFUN-142
diff --git a/game/src/Game/GameSession/MapControlPanel/MapControlPanel.tscn b/game/src/Game/GameSession/MapControlPanel/MapControlPanel.tscn
index 7578c82..d49cf61 100644
--- a/game/src/Game/GameSession/MapControlPanel/MapControlPanel.tscn
+++ b/game/src/Game/GameSession/MapControlPanel/MapControlPanel.tscn
@@ -80,7 +80,7 @@ focus_mode = 0
mouse_filter = 1
text = "L"
-[node name="FindButton" type="Button" parent="MapPanelMargin/MapPanelList/AuxiliaryPanel"]
+[node name="SearchButton" type="Button" parent="MapPanelMargin/MapPanelList/AuxiliaryPanel"]
editor_description = "UI-861"
layout_mode = 2
focus_mode = 0
@@ -108,5 +108,6 @@ text = "-"
[connection signal="map_view_camera_changed" from="." to="MapPanelMargin/MapPanelList/MapDisplayList/Minimap/ViewportQuad" method="_on_map_view_camera_changed"]
[connection signal="minimap_clicked" from="MapPanelMargin/MapPanelList/MapDisplayList/Minimap/ViewportQuad" to="." method="_on_minimap_clicked"]
[connection signal="pressed" from="MapPanelMargin/MapPanelList/AuxiliaryPanel/GameSessionMenuButton" to="." method="_on_game_session_menu_button_pressed"]
+[connection signal="pressed" from="MapPanelMargin/MapPanelList/AuxiliaryPanel/SearchButton" to="." method="_on_search_button_pressed"]
[connection signal="pressed" from="MapPanelMargin/MapPanelList/AuxiliaryPanel/ZoomButtonsContainer/ZoomInButton" to="." method="_on_zoom_in_button_pressed"]
[connection signal="pressed" from="MapPanelMargin/MapPanelList/AuxiliaryPanel/ZoomButtonsContainer/ZoomOutButton" to="." method="_on_zoom_out_button_pressed"]
diff --git a/game/src/Game/GameSession/MapView.gd b/game/src/Game/GameSession/MapView.gd
index bbae02f..171374c 100644
--- a/game/src/Game/GameSession/MapView.gd
+++ b/game/src/Game/GameSession/MapView.gd
@@ -134,18 +134,27 @@ 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:
+func _viewport_to_world_coords(pos_viewport : Vector2) -> Vector3:
var ray_origin := _camera.project_ray_origin(pos_viewport)
var ray_normal := _camera.project_ray_normal(pos_viewport)
# Plane with normal (0,1,0) facing upwards, at a distance 0 from the origin
var intersection : Variant = Plane(0, 1, 0, 0).intersects_ray(ray_origin, ray_normal)
if typeof(intersection) == TYPE_VECTOR3:
- return _world_to_map_coords(intersection as Vector3)
+ return intersection
else:
# Normals parallel to the xz-plane could cause null intersections,
# but the camera's orientation should prevent such normals
push_error("Invalid intersection: ", intersection)
- return Vector2(0.5, 0.5)
+ return _map_to_world_coords(Vector2(0.5, 0.5))
+
+func _viewport_to_map_coords(pos_viewport : Vector2) -> Vector2:
+ return _world_to_map_coords(_viewport_to_world_coords(pos_viewport))
+
+func look_at_map_position(pos : Vector2) -> void:
+ var viewport_centre : Vector2 = Vector2(0.5, 0.5) * _viewport_dims / GuiScale.get_current_guiscale()
+ var pos_delta : Vector3 = _map_to_world_coords(pos) - _viewport_to_world_coords(viewport_centre)
+ _camera.position.x += pos_delta.x
+ _camera.position.z += pos_delta.z
func zoom_in() -> void:
_zoom_target -= _zoom_target_step
diff --git a/game/src/Game/GameSession/SearchPanel.gd b/game/src/Game/GameSession/SearchPanel.gd
new file mode 100644
index 0000000..5554226
--- /dev/null
+++ b/game/src/Game/GameSession/SearchPanel.gd
@@ -0,0 +1,143 @@
+extends GUINode
+
+@export var _map_view : MapView
+
+var _search_panel : Panel
+var _search_line_edit : LineEdit
+var _results_list_box : GUIListBox
+var _result_buttons : Array[Button]
+
+var _drag_active : bool = false
+var _drag_anchor : Vector2
+
+func _ready() -> void:
+ MenuSingleton.search_cache_changed.connect(_update_results_base)
+
+ add_gui_element("goto", "goto_box")
+
+ remove_node(^"./goto_box/goto")
+
+ _search_panel = get_panel_from_nodepath(^"./goto_box")
+
+ var close_button : Button = get_button_from_nodepath(^"./goto_box/cancel")
+ if close_button:
+ close_button.pressed.connect(hide)
+
+ var panel_button : Button = get_button_from_nodepath(^"./goto_box/goto_box")
+ if panel_button:
+ panel_button.button_down.connect(_start_drag)
+ panel_button.button_up.connect(_end_drag)
+ if _search_panel:
+ # Move to back so it's not drawn over the results list
+ _search_panel.move_child(panel_button, 0)
+
+ _search_line_edit = get_line_edit_from_nodepath(^"./goto_box/goto_edit")
+ if _search_line_edit:
+ _search_line_edit.text_changed.connect(_search_string_updated)
+ # Restrict to desired size (by default it's a bit too tall, probably due to font size)
+ _search_line_edit.set_size(_search_line_edit.get_minimum_size())
+
+ _results_list_box = get_gui_listbox_from_nodepath(^"./goto_box/provinces")
+ if _results_list_box:
+ _results_list_box.scroll_index_changed.connect(_update_results_scroll)
+
+ _results_list_box.set_position(_results_list_box.get_position() - Vector2(4, 0))
+
+ hide()
+
+ MenuSingleton.generate_search_cache()
+
+func toggle_visibility() -> void:
+ if is_visible():
+ hide()
+ else:
+ show()
+ if _search_line_edit:
+ _search_line_edit.grab_focus()
+
+func _start_drag() -> void:
+ if _search_panel:
+ _drag_anchor = _search_panel.get_position() - get_window().get_mouse_position()
+ _drag_active = true
+
+func _end_drag() -> void:
+ _drag_active = false
+
+func _input(event : InputEvent) -> void:
+ if _drag_active and event is InputEventMouseMotion:
+ _search_panel.set_position(_drag_anchor + get_window().get_mouse_position())
+
+func _notification(what : int) -> void:
+ match what:
+ NOTIFICATION_TRANSLATION_CHANGED:
+ MenuSingleton.generate_search_cache()
+
+func _search_string_updated(search_string : String) -> void:
+ MenuSingleton.update_search_results(search_string)
+ _update_results_base()
+
+func _update_results_base() -> void:
+ if not _results_list_box:
+ return
+
+ var result_count : int = MenuSingleton.get_search_result_row_count()
+
+ var result_height : float = 0.0
+ if result_count > 0 and (_results_list_box.get_child_count() > 0 or _add_result_button()):
+ result_height = _results_list_box.get_child(0).get_size().y
+
+ _results_list_box.set_fixed(result_count, result_height, false)
+ _update_results_scroll()
+
+func _add_result_button() -> bool:
+ if not _results_list_box:
+ return false
+
+ var child : Panel = GUINode.generate_gui_element("menubar", "save_game_entry")
+ if not child:
+ return false
+
+ var button : Button = GUINode.get_button_from_node(child.get_node(^"./game"))
+ if not button:
+ child.queue_free()
+ return false
+
+ button.pressed.connect(_result_selected.bind(_result_buttons.size()))
+
+ _results_list_box.add_child(child)
+ _result_buttons.push_back(button)
+
+ return true
+
+func _update_results_scroll(scroll_index : int = -1) -> void:
+ if not _results_list_box:
+ return
+
+ if scroll_index >= 0:
+ _results_list_box.set_scroll_index(scroll_index, false)
+
+ scroll_index = _results_list_box.get_scroll_index()
+
+ var results : PackedStringArray = MenuSingleton.get_search_result_rows(scroll_index, _results_list_box.get_fixed_visible_items())
+
+ if results.size() < _result_buttons.size():
+ _result_buttons.resize(results.size())
+ _results_list_box.clear_children(results.size())
+ else:
+ while _result_buttons.size() < results.size() and _add_result_button():
+ pass # Button is added in the loop condition
+
+ for index : int in min(results.size(), _result_buttons.size()):
+ _result_buttons[index].set_text(results[index])
+
+func _result_selected(index : int) -> void:
+ if _map_view:
+ _map_view.look_at_map_position(MenuSingleton.get_search_result_position(index))
+ else:
+ push_error("SearchPanel missing MapView reference!")
+
+ if _search_line_edit:
+ # This triggers a search results update, preventing further get_search_result_position(index) calls
+ _search_line_edit.clear()
+
+ hide()