diff options
30 files changed, 1098 insertions, 30 deletions
@@ -70,3 +70,4 @@ bin/* .DS_Store *.translation +!game/common/map/*.obj
\ No newline at end of file diff --git a/extension/src/LoadLocalisation.cpp b/extension/src/LoadLocalisation.cpp index c95c08b..8698bb2 100644 --- a/extension/src/LoadLocalisation.cpp +++ b/extension/src/LoadLocalisation.cpp @@ -8,7 +8,7 @@ using namespace godot; using namespace OpenVic2; -LoadLocalisation *LoadLocalisation::singleton = nullptr; +LoadLocalisation* LoadLocalisation::singleton = nullptr; void LoadLocalisation::_bind_methods() { ClassDB::bind_method(D_METHOD("load_file", "file_path", "locale"), &LoadLocalisation::load_file); @@ -16,7 +16,7 @@ void LoadLocalisation::_bind_methods() { ClassDB::bind_method(D_METHOD("load_localisation_dir", "dir_path"), &LoadLocalisation::load_localisation_dir); } -LoadLocalisation *LoadLocalisation::get_singleton() { +LoadLocalisation* LoadLocalisation::get_singleton() { return singleton; } @@ -54,7 +54,7 @@ Error LoadLocalisation::_load_file_into_translation(String const& file_path, Ref } Ref<Translation> LoadLocalisation::_get_translation(String const& locale) { - TranslationServer *server = TranslationServer::get_singleton(); + TranslationServer* server = TranslationServer::get_singleton(); Ref<Translation> translation = server->get_translation_object(locale); if (translation.is_null() || translation->get_locale() != locale) { translation.instantiate(); @@ -93,7 +93,7 @@ Error LoadLocalisation::load_locale_dir(String const& dir_path, String const& lo */ Error LoadLocalisation::load_localisation_dir(String const& dir_path) { if (DirAccess::dir_exists_absolute(dir_path)) { - TranslationServer *server = TranslationServer::get_singleton(); + TranslationServer* server = TranslationServer::get_singleton(); Error err = OK; for (String const& locale_name : DirAccess::get_directories_at(dir_path)) { if (locale_name == server->standardize_locale(locale_name)) { diff --git a/extension/src/LoadLocalisation.hpp b/extension/src/LoadLocalisation.hpp index 90f3158..f54a025 100644 --- a/extension/src/LoadLocalisation.hpp +++ b/extension/src/LoadLocalisation.hpp @@ -4,11 +4,11 @@ #include <godot_cpp/classes/translation.hpp> namespace OpenVic2 { - class LoadLocalisation : public godot::Object - { + class LoadLocalisation : public godot::Object { + GDCLASS(LoadLocalisation, godot::Object) - static LoadLocalisation *singleton; + static LoadLocalisation* singleton; godot::Error _load_file_into_translation(godot::String const& file_path, godot::Ref<godot::Translation> translation); godot::Ref<godot::Translation> _get_translation(godot::String const& locale); @@ -17,7 +17,7 @@ namespace OpenVic2 { static void _bind_methods(); public: - static LoadLocalisation *get_singleton(); + static LoadLocalisation* get_singleton(); LoadLocalisation(); ~LoadLocalisation(); diff --git a/extension/src/MapMesh.cpp b/extension/src/MapMesh.cpp new file mode 100644 index 0000000..f0fc819 --- /dev/null +++ b/extension/src/MapMesh.cpp @@ -0,0 +1,151 @@ +#include "MapMesh.hpp" + +#include <godot_cpp/templates/vector.hpp> + +using namespace godot; +using namespace OpenVic2; + +void MapMesh::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_aspect_ratio", "ratio"), &MapMesh::set_aspect_ratio); + ClassDB::bind_method(D_METHOD("get_aspect_ratio"), &MapMesh::get_aspect_ratio); + + ClassDB::bind_method(D_METHOD("set_repeat_proportion", "proportion"), &MapMesh::set_repeat_proportion); + ClassDB::bind_method(D_METHOD("get_repeat_proportion"), &MapMesh::get_repeat_proportion); + + ClassDB::bind_method(D_METHOD("set_subdivide_width", "divisions"), &MapMesh::set_subdivide_width); + ClassDB::bind_method(D_METHOD("get_subdivide_width"), &MapMesh::get_subdivide_width); + + ClassDB::bind_method(D_METHOD("set_subdivide_depth", "divisions"), &MapMesh::set_subdivide_depth); + ClassDB::bind_method(D_METHOD("get_subdivide_depth"), &MapMesh::get_subdivide_depth); + + ClassDB::bind_method(D_METHOD("get_core_aabb"), &MapMesh::get_core_aabb); + ClassDB::bind_method(D_METHOD("is_valid_uv_coord"), &MapMesh::is_valid_uv_coord); + + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "aspect_ratio", PROPERTY_HINT_NONE, "suffix:m"), "set_aspect_ratio", "get_aspect_ratio"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "repeat_proportion", PROPERTY_HINT_NONE, "suffix:m"), "set_repeat_proportion", "get_repeat_proportion"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "subdivide_width", PROPERTY_HINT_RANGE, "0,100,1,or_greater"), "set_subdivide_width", "get_subdivide_width"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "subdivide_depth", PROPERTY_HINT_RANGE, "0,100,1,or_greater"), "set_subdivide_depth", "get_subdivide_depth"); +} + +void MapMesh::_request_update() { + // Hack to trigger _update_lightmap_size and _request_update in PrimitiveMesh + set_add_uv2(get_add_uv2()); +} + +void MapMesh::set_aspect_ratio(const float ratio) { + aspect_ratio = ratio; + _request_update(); +} + +float MapMesh::get_aspect_ratio() const { + return aspect_ratio; +} + +void MapMesh::set_repeat_proportion(const float proportion) { + repeat_proportion = proportion; + _request_update(); +} + +float MapMesh::get_repeat_proportion() const { + return repeat_proportion; +} + +void MapMesh::set_subdivide_width(const int divisions) { + subdivide_w = divisions > 0 ? divisions : 0; + _request_update(); +} + +int MapMesh::get_subdivide_width() const { + return subdivide_w; +} + +void MapMesh::set_subdivide_depth(const int divisions) { + subdivide_d = divisions > 0 ? divisions : 0; + _request_update(); +} + +int MapMesh::get_subdivide_depth() const { + return subdivide_d; +} + +AABB MapMesh::get_core_aabb() const { + const Vector3 size{ aspect_ratio, 0.0f, 1.0f }; + return AABB{ size * -0.5f, size }; +} + +bool MapMesh::is_valid_uv_coord(godot::Vector2 const& uv) const { + return 0.0f <= uv.y && uv.y <= 1.0f; +} + +Array MapMesh::_create_mesh_array() const { + Array arr; + arr.resize(Mesh::ARRAY_MAX); + + const int vertex_count = (subdivide_w + 2) * (subdivide_d + 2); + const int indice_count = (subdivide_w + 1) * (subdivide_d + 1) * 6; + + PackedVector3Array points; + PackedVector3Array normals; + PackedFloat32Array tangents; + PackedVector2Array uvs; + PackedInt32Array indices; + + points.resize(vertex_count); + normals.resize(vertex_count); + tangents.resize(vertex_count * 4); + uvs.resize(vertex_count); + indices.resize(indice_count); + + static const Vector3 normal{ 0.0f, 1.0f, 0.0f }; + const Size2 uv_size{ 1.0f + 2.0f * repeat_proportion, 1.0f }; + const Size2 size{ aspect_ratio * uv_size.x, uv_size.y }, start_pos = size * -0.5f; + + int point_index = 0, thisrow = 0, prevrow = 0, indice_index = 0; + Vector2 subdivide_step{ 1.0f / (subdivide_w + 1.0f) , 1.0f / (subdivide_d + 1.0f) }; + Vector3 point{ 0.0f, 0.0f, start_pos.y }; + Vector2 point_step = subdivide_step * size; + Vector2 uv{}, uv_step = subdivide_step * uv_size; + + for (int j = 0; j <= subdivide_d + 1; ++j) { + point.x = start_pos.x; + uv.x = -repeat_proportion; + + for (int i = 0; i <= subdivide_w + 1; ++i) { + + points[point_index] = point; + normals[point_index] = normal; + tangents[point_index * 4 + 0] = 1.0f; + tangents[point_index * 4 + 1] = 0.0f; + tangents[point_index * 4 + 2] = 0.0f; + tangents[point_index * 4 + 3] = 1.0f; + uvs[point_index] = uv; + point_index++; + + if (i > 0 && j > 0) { + indices[indice_index + 0] = prevrow + i - 1; + indices[indice_index + 1] = prevrow + i; + indices[indice_index + 2] = thisrow + i - 1; + indices[indice_index + 3] = prevrow + i; + indices[indice_index + 4] = thisrow + i; + indices[indice_index + 5] = thisrow + i - 1; + indice_index += 6; + } + + point.x += point_step.x; + uv.x += uv_step.x; + } + + point.z += point_step.y; + uv.y += uv_step.y; + prevrow = thisrow; + thisrow = point_index; + } + + arr[Mesh::ARRAY_VERTEX] = points; + arr[Mesh::ARRAY_NORMAL] = normals; + arr[Mesh::ARRAY_TANGENT] = tangents; + arr[Mesh::ARRAY_TEX_UV] = uvs; + arr[Mesh::ARRAY_INDEX] = indices; + + return arr; +} diff --git a/extension/src/MapMesh.hpp b/extension/src/MapMesh.hpp new file mode 100644 index 0000000..d8727cf --- /dev/null +++ b/extension/src/MapMesh.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include <godot_cpp/classes/primitive_mesh.hpp> + +namespace OpenVic2 { + class MapMesh : public godot::PrimitiveMesh { + GDCLASS(MapMesh, godot::PrimitiveMesh) + + float aspect_ratio = 2.0f, repeat_proportion = 0.5f; + int subdivide_w = 0, subdivide_d = 0; + + protected: + static void _bind_methods(); + void _request_update(); + + public: + void set_aspect_ratio(const float ratio); + float get_aspect_ratio() const; + + void set_repeat_proportion(const float proportion); + float get_repeat_proportion() const; + + void set_subdivide_width(const int divisions); + int get_subdivide_width() const; + + void set_subdivide_depth(const int divisions); + int get_subdivide_depth() const; + + godot::AABB get_core_aabb() const; + bool is_valid_uv_coord(godot::Vector2 const& uv) const; + + godot::Array _create_mesh_array() const override; + }; +} diff --git a/extension/src/MapSingleton.cpp b/extension/src/MapSingleton.cpp new file mode 100644 index 0000000..0f5fe7c --- /dev/null +++ b/extension/src/MapSingleton.cpp @@ -0,0 +1,256 @@ +#include "MapSingleton.hpp" + +#include <godot_cpp/variant/utility_functions.hpp> +#include <godot_cpp/classes/file_access.hpp> +#include <godot_cpp/classes/json.hpp> + +using namespace godot; +using namespace OpenVic2; + +MapSingleton* MapSingleton::singleton = nullptr; + +void MapSingleton::_bind_methods() { + ClassDB::bind_method(D_METHOD("load_province_identifier_file", "file_path"), &MapSingleton::load_province_identifier_file); + ClassDB::bind_method(D_METHOD("load_province_shape_file", "file_path"), &MapSingleton::load_province_shape_file); + ClassDB::bind_method(D_METHOD("get_province_identifier_from_pixel_coords", "coords"), &MapSingleton::get_province_identifier_from_pixel_coords); + ClassDB::bind_method(D_METHOD("get_width"), &MapSingleton::get_width); + ClassDB::bind_method(D_METHOD("get_height"), &MapSingleton::get_height); + ClassDB::bind_method(D_METHOD("get_province_index_image"), &MapSingleton::get_province_index_image); + ClassDB::bind_method(D_METHOD("get_province_colour_image"), &MapSingleton::get_province_colour_image); +} + +MapSingleton* MapSingleton::get_singleton() { + return singleton; +} + +MapSingleton::MapSingleton() { + ERR_FAIL_COND(singleton != nullptr); + singleton = this; +} + +MapSingleton::~MapSingleton() { + ERR_FAIL_COND(singleton != this); + singleton = nullptr; +} + +Error MapSingleton::load_province_identifier_file(String const& file_path) { + UtilityFunctions::print("Loading identifier file: ", file_path); + Ref<FileAccess> file = FileAccess::open(file_path, FileAccess::ModeFlags::READ); + Error err = FileAccess::get_open_error(); + if (err != OK || file.is_null()) { + UtilityFunctions::push_error("Failed to load province identifier file: ", file_path); + return err == OK ? FAILED : err; + } + String json_string = file->get_as_text(); + Ref<JSON> json; + json.instantiate(); + err = json->parse(json_string); + if (err) { + UtilityFunctions::push_error("Failed to parse province identifier file as JSON: ", file_path, + "\nError at line ", json->get_error_line(), ": ", json->get_error_message()); + return err; + } + Variant json_var = json->get_data(); + Variant::Type type = json_var.get_type(); + if (type != Variant::DICTIONARY) { + UtilityFunctions::push_error("Invalid province identifier JSON: root has type ", + Variant::get_type_name(type), " (expected Dictionary)"); + return FAILED; + } + Dictionary prov_dict = json_var; + Array prov_identifiers = prov_dict.keys(); + for (int idx = 0; idx < prov_identifiers.size(); ++idx) { + String const& identifier = prov_identifiers[idx]; + Variant const& colour_var = prov_dict[identifier]; + if (identifier.is_empty()) { + UtilityFunctions::push_error("Empty province identifier with colour: ", colour_var); + err = FAILED; + continue; + } + static const String prov_prefix = "prov_"; + if (!identifier.begins_with(prov_prefix)) + UtilityFunctions::push_warning("Province identifier missing prefix: ", identifier); + type = colour_var.get_type(); + Province::colour_t colour = Province::NULL_COLOUR; + if (type == Variant::ARRAY) { + Array colour_array = colour_var; + if (colour_array.size() == 3) { + for (int jdx = 0; jdx < 3; ++jdx) { + Variant var = colour_array[jdx]; + if (var.get_type() != Variant::FLOAT) { + colour = Province::NULL_COLOUR; + break; + } + double colour_double = var; + if (std::trunc(colour_double) != colour_double) { + colour = Province::NULL_COLOUR; + break; + } + int64_t colour_int = static_cast<int64_t>(colour_double); + if (colour_int < 0 || colour_int > 255) { + colour = Province::NULL_COLOUR; + break; + } + colour = (colour << 8) | colour_int; + } + } + } else if (type == Variant::STRING) { + String colour_string = colour_var; + if (colour_string.is_valid_hex_number()) { + int64_t colour_int = colour_string.hex_to_int(); + if (0 <= colour_int && colour_int <= 0xFFFFFF) + colour = colour_int; + } + } + if (colour == Province::NULL_COLOUR) { + UtilityFunctions::push_error("Invalid province identifier colour for ", identifier, ": ", colour_var); + err = FAILED; + continue; + } + std::string error_message; + if (!map.add_province(identifier.utf8().get_data(), colour, error_message)) { + UtilityFunctions::push_error(error_message.c_str()); + err = FAILED; + } + } + map.lock_provinces(); + return err; +} + +static Province::colour_t colour_at(PackedByteArray const& colour_data_array, int32_t idx) { + return (colour_data_array[idx * 3] << 16) | (colour_data_array[idx * 3 + 1] << 8) | colour_data_array[idx * 3 + 2]; +} + +Error MapSingleton::load_province_shape_file(String const& file_path) { + if (province_shape_image.is_valid()) { + UtilityFunctions::push_error("Province shape file has already been loaded, cannot load: ", file_path); + return FAILED; + } + province_shape_image.instantiate(); + Error err = province_shape_image->load(file_path); + if (err != OK) { + UtilityFunctions::push_error("Failed to load province shape file: ", file_path); + province_shape_image.unref(); + return err; + } + width = province_shape_image->get_width(); + height = province_shape_image->get_height(); + if (width < 1 || height < 1) { + UtilityFunctions::push_error("Invalid dimensions (", width, "x", height, ") for province shape file: ", file_path); + err = FAILED; + } + static const Image::Format expected_format = Image::FORMAT_RGB8; + Image::Format format = province_shape_image->get_format(); + if (format != expected_format) { + UtilityFunctions::push_error("Invalid format (", format, ", should be ", expected_format, ") for province shape file: ", file_path); + err = FAILED; + } + if (err) { + province_shape_image.unref(); + return err; + } + + std::vector<bool> province_checklist(map.get_province_count()); + + PackedByteArray shape_data_array = province_shape_image->get_data(), index_data_array; + index_data_array.resize(width * height * sizeof(Province::index_t)); + Province::index_t* index_data = reinterpret_cast<Province::index_t*>(index_data_array.ptrw()); + + for (int32_t y = 0; y < height; ++y) { + for (int32_t x = 0; x < width; ++x) { + const int32_t idx = x + y * width; + const Province::colour_t colour = colour_at(shape_data_array, idx); + if (colour == Province::NULL_COLOUR) { + index_data[idx] = Province::NULL_INDEX; + continue; + } + if (x > 0) { + const int32_t jdx = idx - 1; + if (colour_at(shape_data_array, jdx) == colour) { + index_data[idx] = index_data[jdx]; + continue; + } + } + if (y > 0) { + const int32_t jdx = idx - width; + if (colour_at(shape_data_array, jdx) == colour) { + index_data[idx] = index_data[jdx]; + continue; + } + } + const Province* province = map.get_province_by_colour(colour); + if (province) { + Province::index_t index = province->get_index(); + index_data[idx] = index; + province_checklist[index - 1] = true; + continue; + } + UtilityFunctions::push_error("Unrecognised province colour ", Province::colour_to_hex_string(colour).c_str(), " at (", x, ", ", y, ")"); + err = FAILED; + index_data[idx] = Province::NULL_INDEX; + } + } + + for (size_t idx = 0; idx < province_checklist.size(); ++idx) { + if (!province_checklist[idx]) { + Province* province = map.get_province_by_index(idx + 1); + if (province) UtilityFunctions::push_error("Province missing from shape image: ", province->to_string().c_str()); + else UtilityFunctions::push_error("Province missing for index: ", static_cast<int32_t>(idx + 1)); + err = FAILED; + } + } + + province_index_image = Image::create_from_data(width, height, false, Image::FORMAT_RG8, index_data_array); + if (province_index_image.is_null()) { + UtilityFunctions::push_error("Failed to create province ID image"); + err = FAILED; + } + + PackedByteArray colour_data_array; + colour_data_array.resize((Province::MAX_INDEX + 1) * 3); + for (size_t idx = 1; idx <= map.get_province_count(); ++idx) { + const Province* province = map.get_province_by_index(idx); + if (province) { + const Province::colour_t colour = province->get_colour(); + colour_data_array[3 * idx + 0] = (colour >> 16) & 0xFF; + colour_data_array[3 * idx + 1] = (colour >> 8) & 0xFF; + colour_data_array[3 * idx + 2] = colour & 0xFF; + } else UtilityFunctions::push_error("Missing province at index ", static_cast<int32_t>(idx)); + } + static const int32_t PROVINCE_INDEX_SQRT = 1 << (sizeof(Province::index_t) * 4); + province_colour_image = Image::create_from_data(PROVINCE_INDEX_SQRT, PROVINCE_INDEX_SQRT, false, Image::FORMAT_RGB8, colour_data_array); + if (province_colour_image.is_null()) { + UtilityFunctions::push_error("Failed to create province colour image"); + err = FAILED; + } + + return err; +} + +String MapSingleton::get_province_identifier_from_pixel_coords(Vector2i const& coords) { + if (province_index_image.is_valid()) { + const PackedByteArray index_data_array = province_index_image->get_data(); + const Province::index_t* index_data = reinterpret_cast<const Province::index_t*>(index_data_array.ptr()); + const int32_t x_mod_w = UtilityFunctions::posmod(coords.x, width); + const int32_t y_mod_h = UtilityFunctions::posmod(coords.y, height); + const Province* province = map.get_province_by_index(index_data[x_mod_w + y_mod_h * width]); + if (province) return province->get_identifier().c_str(); + } + return String{}; +} + +int32_t MapSingleton::get_width() const { + return width; +} + +int32_t MapSingleton::get_height() const { + return height; +} + +Ref<Image> MapSingleton::get_province_index_image() const { + return province_index_image; +} + +Ref<Image> MapSingleton::get_province_colour_image() const { + return province_colour_image; +} diff --git a/extension/src/MapSingleton.hpp b/extension/src/MapSingleton.hpp new file mode 100644 index 0000000..71761cd --- /dev/null +++ b/extension/src/MapSingleton.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include <godot_cpp/classes/image.hpp> +#include "openvic2/Map.hpp" + +namespace OpenVic2 { + class MapSingleton : public godot::Object { + + GDCLASS(MapSingleton, godot::Object) + + static MapSingleton* singleton; + + godot::Ref<godot::Image> province_shape_image, province_index_image, province_colour_image; + int32_t width = 0, height = 0; + Map map; + + protected: + static void _bind_methods(); + + public: + static MapSingleton* get_singleton(); + + MapSingleton(); + ~MapSingleton(); + + godot::Error load_province_identifier_file(godot::String const& file_path); + godot::Error load_province_shape_file(godot::String const& file_path); + godot::String get_province_identifier_from_pixel_coords(godot::Vector2i const& coords); + int32_t get_width() const; + int32_t get_height() const; + godot::Ref<godot::Image> get_province_index_image() const; + godot::Ref<godot::Image> get_province_colour_image() const; + }; +} diff --git a/extension/src/TestSingleton.cpp b/extension/src/TestSingleton.cpp index 0855a30..d9e2b03 100644 --- a/extension/src/TestSingleton.cpp +++ b/extension/src/TestSingleton.cpp @@ -6,14 +6,14 @@ using namespace godot; using namespace OpenVic2; -TestSingleton *TestSingleton::singleton = nullptr; +TestSingleton* TestSingleton::singleton = nullptr; void TestSingleton::_bind_methods() { ClassDB::bind_method(D_METHOD("hello_singleton"), &TestSingleton::hello_singleton); } -TestSingleton *TestSingleton::get_singleton() +TestSingleton* TestSingleton::get_singleton() { return singleton; } diff --git a/extension/src/TestSingleton.hpp b/extension/src/TestSingleton.hpp index de27589..d140516 100644 --- a/extension/src/TestSingleton.hpp +++ b/extension/src/TestSingleton.hpp @@ -8,13 +8,13 @@ namespace OpenVic2 { { GDCLASS(TestSingleton, godot::Object) - static TestSingleton *singleton; + static TestSingleton* singleton; protected: static void _bind_methods(); public: - static TestSingleton *get_singleton(); + static TestSingleton* get_singleton(); TestSingleton(); ~TestSingleton(); diff --git a/extension/src/openvic2/Map.cpp b/extension/src/openvic2/Map.cpp new file mode 100644 index 0000000..d980b88 --- /dev/null +++ b/extension/src/openvic2/Map.cpp @@ -0,0 +1,89 @@ +#include "Map.hpp" + +#include <cassert> +#include <sstream> +#include <iomanip> + +using namespace OpenVic2; + +Province::Province(index_t new_index, std::string const& new_identifier, colour_t new_colour) : + index(new_index), identifier(new_identifier), colour(new_colour) { + assert(index != NULL_INDEX); + assert(!identifier.empty()); + assert(colour != NULL_COLOUR); +} + +std::string Province::colour_to_hex_string(colour_t colour) { + std::ostringstream stream; + stream << std::hex << std::setfill('0') << std::setw(6) << colour; + return stream.str(); +} + +Province::index_t Province::get_index() const { + return index; +} + +std::string const& Province::get_identifier() const { + return identifier; +} + +Province::colour_t Province::get_colour() const { + return colour; +} + +std::string Province::to_string() const { + return "(#" + std::to_string(index) + ", " + identifier + ", 0x" + colour_to_hex_string(colour) + ")"; +} + +bool Map::add_province(std::string const& identifier, Province::colour_t colour, std::string& error_message) { + if (provinces_locked) { + error_message = "The map's province list has already been locked!"; + return false; + } + if (provinces.size() >= Province::MAX_INDEX) { + error_message = "The map's province list is full - there can be at most " + std::to_string(Province::MAX_INDEX) + " provinces"; + return false; + } + if (colour == Province::NULL_COLOUR || colour > Province::MAX_COLOUR) { + error_message = "Invalid province colour: " + Province::colour_to_hex_string(colour); + return false; + } + Province new_province{ static_cast<Province::index_t>(provinces.size() + 1), identifier, colour }; + for (Province const& province : provinces) { + if (province.identifier == identifier) { + error_message = "Duplicate province identifiers: " + province.to_string() + " and " + new_province.to_string(); + return false; + } + if (province.colour == colour) { + error_message = "Duplicate province colours: " + province.to_string() + " and " + new_province.to_string(); + return false; + } + } + provinces.push_back(new_province); + error_message = "Added province: " + new_province.to_string(); + return true; +} + +void Map::lock_provinces() { + provinces_locked = true; +} + +size_t Map::get_province_count() const { + return provinces.size(); +} + +Province* Map::get_province_by_index(Province::index_t index) { + return index != Province::NULL_INDEX && index <= provinces.size() ? &provinces[index - 1] : nullptr; +} + +Province* Map::get_province_by_identifier(std::string const& identifier) { + for (Province& province : provinces) + if (province.identifier == identifier) return &province; + return nullptr; +} + +Province* Map::get_province_by_colour(Province::colour_t colour) { + for (Province& province : provinces) + if (province.colour == colour) return &province; + return nullptr; +} diff --git a/extension/src/openvic2/Map.hpp b/extension/src/openvic2/Map.hpp new file mode 100644 index 0000000..365d78b --- /dev/null +++ b/extension/src/openvic2/Map.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include <string> +#include <cstdint> +#include <vector> + +namespace OpenVic2 { + + struct Province { + using colour_t = uint32_t; + using index_t = uint16_t; + friend struct Map; + static const colour_t NULL_COLOUR = 0, MAX_COLOUR = 0xFFFFFF; + static const index_t NULL_INDEX = 0, MAX_INDEX = 0xFFFF; + private: + index_t index; + std::string identifier; + colour_t colour; + + Province(index_t index, std::string const& identifier, colour_t colour); + public: + static std::string colour_to_hex_string(colour_t colour); + + index_t get_index() const; + std::string const& get_identifier() const; + colour_t get_colour() const; + std::string to_string() const; + }; + + struct Map { + private: + std::vector<Province> provinces; + bool provinces_locked = false; + + public: + bool add_province(std::string const& identifier, Province::colour_t colour, std::string& error_message); + void lock_provinces(); + size_t get_province_count() const; + + Province* get_province_by_index(Province::index_t index); + Province* get_province_by_identifier(std::string const& identifier); + Province* get_province_by_colour(Province::colour_t colour); + }; + +} diff --git a/extension/src/register_types.cpp b/extension/src/register_types.cpp index d1613a5..16e59b2 100644 --- a/extension/src/register_types.cpp +++ b/extension/src/register_types.cpp @@ -9,6 +9,8 @@ #include "Simulation.hpp" #include "Checksum.hpp" #include "LoadLocalisation.hpp" +#include "MapSingleton.hpp" +#include "MapMesh.hpp" using namespace godot; using namespace OpenVic2; @@ -17,9 +19,9 @@ static TestSingleton* _test_singleton; static Simulation* _simulation; static Checksum* _checksum; static LoadLocalisation* _load_localisation; +static MapSingleton* _map_singleton; -void initialize_openvic2_types(ModuleInitializationLevel p_level) -{ +void initialize_openvic2_types(ModuleInitializationLevel p_level) { if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { return; } @@ -40,6 +42,11 @@ void initialize_openvic2_types(ModuleInitializationLevel p_level) _load_localisation = memnew(LoadLocalisation); Engine::get_singleton()->register_singleton("LoadLocalisation", LoadLocalisation::get_singleton()); + ClassDB::register_class<MapSingleton>(); + _map_singleton = memnew(MapSingleton); + Engine::get_singleton()->register_singleton("MapSingleton", MapSingleton::get_singleton()); + + ClassDB::register_class<MapMesh>(); } void uninitialize_openvic2_types(ModuleInitializationLevel p_level) { @@ -58,15 +65,16 @@ void uninitialize_openvic2_types(ModuleInitializationLevel p_level) { Engine::get_singleton()->unregister_singleton("LoadLocalisation"); memdelete(_load_localisation); + + Engine::get_singleton()->unregister_singleton("MapSingleton"); + memdelete(_map_singleton); } -extern "C" -{ +extern "C" { // Initialization. - GDExtensionBool GDE_EXPORT openvic2_library_init(const GDExtensionInterface *p_interface, const GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization) - { + GDExtensionBool GDE_EXPORT openvic2_library_init(const GDExtensionInterface* p_interface, const GDExtensionClassLibraryPtr p_library, GDExtensionInitialization* r_initialization) { GDExtensionBinding::InitObject init_obj(p_interface, p_library, r_initialization); init_obj.register_initializer(initialize_openvic2_types); diff --git a/game/common/map/provinces.json b/game/common/map/provinces.json new file mode 100644 index 0000000..66de29b --- /dev/null +++ b/game/common/map/provinces.json @@ -0,0 +1,9 @@ +{ + "prov_britain": [150, 0, 0], + "prov_ireland": [23, 147, 31], + "prov_iceland": "343D91", + "prov_cuba": "1E29FF", + "prov_madagascar": "790091", + "prov_ceylon": "FF6A00", + "prov_formosa": "82B1FF" +} diff --git a/game/common/map/provinces.png b/game/common/map/provinces.png Binary files differnew file mode 100644 index 0000000..68bf528 --- /dev/null +++ b/game/common/map/provinces.png diff --git a/game/common/map/provinces.png.import b/game/common/map/provinces.png.import new file mode 100644 index 0000000..8dd0c09 --- /dev/null +++ b/game/common/map/provinces.png.import @@ -0,0 +1,3 @@ +[remap] + +importer="keep" diff --git a/game/common/map/terrain/terrain.png b/game/common/map/terrain/terrain.png Binary files differnew file mode 100644 index 0000000..6d36dee --- /dev/null +++ b/game/common/map/terrain/terrain.png diff --git a/game/common/map/terrain/terrain.png.import b/game/common/map/terrain/terrain.png.import new file mode 100644 index 0000000..56ba5dc --- /dev/null +++ b/game/common/map/terrain/terrain.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cmw0pvjthnn8c" +path="res://.godot/imported/terrain.png-c443b8a709ca7acc4b3bb3df1d3095a9.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://common/map/terrain/terrain.png" +dest_files=["res://.godot/imported/terrain.png-c443b8a709ca7acc4b3bb3df1d3095a9.ctex"] + +[params] + +compress/mode=3 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=2 +compress/channel_pack=0 +mipmaps/generate=true +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/game/localisation/en_GB/menus.csv b/game/localisation/en_GB/menus.csv index 27a0858..b483662 100644 --- a/game/localisation/en_GB/menus.csv +++ b/game/localisation/en_GB/menus.csv @@ -1,3 +1,4 @@ + ,, Main Menu MAINMENU_TITLE,OpenVic2 MAINMENU_NEW_GAME,New Game diff --git a/game/localisation/en_GB/provinces.csv b/game/localisation/en_GB/provinces.csv new file mode 100644 index 0000000..3b47955 --- /dev/null +++ b/game/localisation/en_GB/provinces.csv @@ -0,0 +1,9 @@ + +,, Test Provinces +prov_britain_NAME,Britain +prov_ireland_NAME,Ireland +prov_iceland_NAME,Iceland +prov_cuba_NAME,Cuba +prov_madagascar_NAME,Madagascar +prov_ceylon_NAME,Ceylon +prov_formosa_NAME,Formosa diff --git a/game/localisation/en_GB/provinces.csv.import b/game/localisation/en_GB/provinces.csv.import new file mode 100644 index 0000000..8dd0c09 --- /dev/null +++ b/game/localisation/en_GB/provinces.csv.import @@ -0,0 +1,3 @@ +[remap] + +importer="keep" diff --git a/game/project.godot b/game/project.godot index 363bbc1..fa2871f 100644 --- a/game/project.godot +++ b/game/project.godot @@ -44,6 +44,55 @@ enabled=PackedStringArray("res://addons/keychain/plugin.cfg", "res://addons/open theme/custom="res://theme/default_theme.tres" +[input] + +map_north={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194320,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":87,"physical_keycode":0,"key_label":0,"unicode":119,"echo":false,"script":null) +] +} +map_east={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194321,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":68,"physical_keycode":0,"key_label":0,"unicode":100,"echo":false,"script":null) +] +} +map_south={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194322,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":83,"physical_keycode":0,"key_label":0,"unicode":115,"echo":false,"script":null) +] +} +map_west={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194319,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":65,"physical_keycode":0,"key_label":0,"unicode":97,"echo":false,"script":null) +] +} +map_zoomin={ +"deadzone": 0.5, +"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":8,"position":Vector2(174, 17),"global_position":Vector2(180, 80),"factor":1.0,"button_index":4,"pressed":true,"double_click":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":81,"physical_keycode":0,"key_label":0,"unicode":113,"echo":false,"script":null) +] +} +map_zoomout={ +"deadzone": 0.5, +"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":16,"position":Vector2(325, 24),"global_position":Vector2(331, 87),"factor":1.0,"button_index":5,"pressed":true,"double_click":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":69,"physical_keycode":0,"key_label":0,"unicode":101,"echo":false,"script":null) +] +} +map_drag={ +"deadzone": 0.5, +"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":3,"pressed":false,"double_click":false,"script":null) +] +} +map_click={ +"deadzone": 0.5, +"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"pressed":false,"double_click":false,"script":null) +] +} + [internationalization] locale/translation_remaps={} diff --git a/game/src/Autoload/Events.gd b/game/src/Autoload/Events.gd index 25a185f..040cb06 100644 --- a/game/src/Autoload/Events.gd +++ b/game/src/Autoload/Events.gd @@ -2,3 +2,12 @@ extends Node var Options = preload("Events/Options.gd").new() var Localisation = preload("Events/Localisation.gd").new() + +const _province_identifier_file : String = "res://common/map/provinces.json" +const _province_shape_file : String = "res://common/map/provinces.png" + +func _ready(): + if MapSingleton.load_province_identifier_file(_province_identifier_file) != OK: + push_error("Failed to load province identifiers") + if MapSingleton.load_province_shape_file(_province_shape_file) != OK: + push_error("Failed to load province shapes") diff --git a/game/src/GameSession/GameSession.gd b/game/src/GameSession/GameSession.gd index 0d69bf2..38eaba1 100644 --- a/game/src/GameSession/GameSession.gd +++ b/game/src/GameSession/GameSession.gd @@ -1,4 +1,4 @@ -extends Control +extends Node @export var _game_session_menu : Control diff --git a/game/src/GameSession/GameSession.tscn b/game/src/GameSession/GameSession.tscn index f984daf..390040e 100644 --- a/game/src/GameSession/GameSession.tscn +++ b/game/src/GameSession/GameSession.tscn @@ -1,23 +1,20 @@ -[gd_scene load_steps=4 format=3 uid="uid://bgnupcshe1m7r"] +[gd_scene load_steps=6 format=3 uid="uid://bgnupcshe1m7r"] [ext_resource type="Script" path="res://src/GameSession/GameSession.gd" id="1_eklvp"] [ext_resource type="PackedScene" uid="uid://g524p8lr574w" path="res://src/GameSession/MapControlPanel.tscn" id="3_afh6d"] [ext_resource type="PackedScene" uid="uid://dvdynl6eir40o" path="res://src/GameSession/GameSessionMenu.tscn" id="3_bvmqh"] +[ext_resource type="PackedScene" uid="uid://dkehmdnuxih2r" path="res://src/GameSession/MapView.tscn" id="4_xkg5j"] +[ext_resource type="PackedScene" uid="uid://byq323jbel48u" path="res://src/GameSession/ProvinceOverviewPanel.tscn" id="5_osjnn"] -[node name="GameSession" type="Control" node_paths=PackedStringArray("_game_session_menu")] +[node name="GameSession" type="Node" node_paths=PackedStringArray("_game_session_menu")] editor_description = "SS-102" -layout_mode = 3 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 script = ExtResource("1_eklvp") _game_session_menu = NodePath("GameSessionMenu") +[node name="MapView" parent="." instance=ExtResource("4_xkg5j")] + [node name="GameSessionMenu" parent="." instance=ExtResource("3_bvmqh")] visible = false -layout_mode = 1 anchors_preset = 8 anchor_left = 0.5 anchor_top = 0.5 @@ -27,7 +24,6 @@ grow_horizontal = 2 grow_vertical = 2 [node name="MapControlPanel" parent="." instance=ExtResource("3_afh6d")] -layout_mode = 1 anchors_preset = 3 anchor_left = 1.0 anchor_top = 1.0 @@ -36,5 +32,13 @@ anchor_bottom = 1.0 grow_horizontal = 0 grow_vertical = 0 +[node name="ProvinceOverviewPanel" parent="." instance=ExtResource("5_osjnn")] +anchors_preset = -1 +anchor_top = 0.583333 +anchor_right = 0.15625 +offset_top = 0.0 +offset_right = 0.0 + +[connection signal="province_selected" from="MapView" to="ProvinceOverviewPanel" method="_on_province_selected"] [connection signal="close_button_pressed" from="GameSessionMenu" to="." method="_on_game_session_menu_close_button_pressed"] [connection signal="game_session_menu_button_pressed" from="MapControlPanel" to="." method="_on_game_session_menu_button_pressed"] diff --git a/game/src/GameSession/MapControlPanel.tscn b/game/src/GameSession/MapControlPanel.tscn index 71d43e7..2a0c971 100644 --- a/game/src/GameSession/MapControlPanel.tscn +++ b/game/src/GameSession/MapControlPanel.tscn @@ -1,7 +1,13 @@ -[gd_scene load_steps=2 format=3 uid="uid://g524p8lr574w"] +[gd_scene load_steps=4 format=3 uid="uid://g524p8lr574w"] [ext_resource type="Script" path="res://src/GameSession/MapControlPanel.gd" id="1_ign64"] +[sub_resource type="InputEventAction" id="InputEventAction_5nck3"] +action = &"ui_cancel" + +[sub_resource type="Shortcut" id="Shortcut_fc1tk"] +events = [SubResource("InputEventAction_5nck3")] + [node name="PanelContainer" type="PanelContainer"] editor_description = "SS-103" script = ExtResource("1_ign64") @@ -27,6 +33,7 @@ layout_mode = 2 [node name="GameSessionMenuButton" type="Button" parent="HBoxContainer/AuxiliaryPanel"] editor_description = "UI-9" layout_mode = 2 +shortcut = SubResource("Shortcut_fc1tk") text = "ESC" [connection signal="pressed" from="HBoxContainer/AuxiliaryPanel/GameSessionMenuButton" to="." method="_on_game_session_menu_button_pressed"] diff --git a/game/src/GameSession/MapView.gd b/game/src/GameSession/MapView.gd new file mode 100644 index 0000000..faf90e8 --- /dev/null +++ b/game/src/GameSession/MapView.gd @@ -0,0 +1,185 @@ +extends Node3D + +signal province_selected(identifier : String) + +const _action_north : StringName = &"map_north" +const _action_east : StringName = &"map_east" +const _action_south : StringName = &"map_south" +const _action_west : StringName = &"map_west" +const _action_zoomin : StringName = &"map_zoomin" +const _action_zoomout : StringName = &"map_zoomout" +const _action_drag : StringName = &"map_drag" +const _action_click : StringName = &"map_click" + +const _shader_param_province_index : StringName = &"province_index_tex" +const _shader_param_province_colour : StringName = &"province_colour_tex" +const _shader_param_hover_pos : StringName = &"hover_pos" +const _shader_param_selected_pos : StringName = &"selected_pos" + +@export var _camera : Camera3D + +@export var _cardinal_move_speed : float = 1.0 +@export var _edge_move_threshold: float = 0.15 +@export var _edge_move_speed: float = 2.5 +var _drag_anchor : Vector2 +var _drag_active : bool = false + +@export var _zoom_target_min : float = 0.2 +@export var _zoom_target_max : float = 5.0 +@export var _zoom_target_step : float = 0.1 +@export var _zoom_epsilon : float = _zoom_target_step * 0.1 +@export var _zoom_speed : float = 5.0 +@export var _zoom_target : float = 1.0: + get: return _zoom_target + set(v): _zoom_target = clamp(v, _zoom_target_min, _zoom_target_max) + +@export var _map_mesh_instance : MeshInstance3D +var _map_mesh : MapMesh +var _map_shader_material : ShaderMaterial +var _map_image_size : Vector2 +var _map_province_index_image : Image +var _map_mesh_corner : Vector2 +var _map_mesh_dims : Vector2 + +var _mouse_pos_viewport : Vector2 = Vector2(0.5, 0.5) +var _mouse_pos_map : Vector2 = Vector2(0.5, 0.5) + +func _ready(): + if _camera == null: + push_error("MapView's _camera variable hasn't been set!") + return + if _map_mesh_instance == null: + push_error("MapView's _map_mesh variable hasn't been set!") + return + if not _map_mesh_instance.mesh is MapMesh: + push_error("Invalid map mesh class: ", _map_mesh_instance.mesh.get_class(), "(expected MapMesh)") + return + _map_mesh = _map_mesh_instance.mesh + + # Set map mesh size and get bounds + _map_image_size = Vector2(Vector2i(MapSingleton.get_width(), MapSingleton.get_height())) + _map_mesh.aspect_ratio = _map_image_size.x / _map_image_size.y + var map_mesh_aabb := _map_mesh.get_core_aabb() * _map_mesh_instance.transform + _map_mesh_corner = Vector2( + min(map_mesh_aabb.position.x, map_mesh_aabb.end.x), + min(map_mesh_aabb.position.z, map_mesh_aabb.end.z) + ) + _map_mesh_dims = abs(Vector2( + map_mesh_aabb.position.x - map_mesh_aabb.end.x, + map_mesh_aabb.position.z - map_mesh_aabb.end.z + )) + + var map_material = _map_mesh_instance.get_active_material(0) + if map_material == null: + push_error("Map mesh is missing material!") + return + if not map_material is ShaderMaterial: + push_error("Invalid map mesh material class: ", map_material.get_class()) + return + _map_shader_material = map_material + # Province index texture + _map_province_index_image = MapSingleton.get_province_index_image() + if _map_province_index_image == null: + push_error("Failed to get province index image!") + return + var province_index_texture := ImageTexture.create_from_image(_map_province_index_image) + _map_shader_material.set_shader_parameter(_shader_param_province_index, province_index_texture) + # Province colour texture + var province_colour_image = MapSingleton.get_province_colour_image() + if province_colour_image == null: + push_error("Failed to get province colour image!") + return + var province_colour_texture := ImageTexture.create_from_image(province_colour_image) + _map_shader_material.set_shader_parameter(_shader_param_province_colour, province_colour_texture) + +func _unhandled_input(event : InputEvent): + if event.is_action_pressed(_action_click): + # Check if the mouse is outside of bounds + if _map_mesh.is_valid_uv_coord(_mouse_pos_map): + _map_shader_material.set_shader_parameter(_shader_param_selected_pos, _mouse_pos_map) + var mouse_pixel_pos := Vector2i(_mouse_pos_map * _map_image_size) + var province_identifier := MapSingleton.get_province_identifier_from_pixel_coords(mouse_pixel_pos) + province_selected.emit(province_identifier) + elif event.is_action_pressed(_action_drag): + if _drag_active: + push_warning("Drag being activated while already active!") + _drag_active = true + _drag_anchor = _mouse_pos_map + elif event.is_action_released(_action_drag): + if not _drag_active: + push_warning("Drag being deactivated while already not active!") + _drag_active = false + elif event.is_action_pressed(_action_zoomin, true): + _zoom_target -= _zoom_target_step + elif event.is_action_pressed(_action_zoomout, true): + _zoom_target += _zoom_target_step + +func _physics_process(delta : float): + _mouse_pos_viewport = get_viewport().get_mouse_position() + # Process movement + _movement_process(delta) + # Keep within map bounds + _clamp_over_map() + # Process zooming + _zoom_process(delta) + # Orient based on height + _update_orientation() + # Calculate where the mouse lies on the map + _update_mouse_map_position() + +func _movement_process(delta : float) -> void: + var direction : Vector2 + if _drag_active: + direction = (_drag_anchor - _mouse_pos_map) * _map_mesh_dims + else: + direction = _edge_scrolling_vector() + _cardinal_movement_vector() + # Scale movement speed with height + direction *= _camera.position.y * delta + _camera.position += Vector3(direction.x, 0, direction.y) + +func _edge_scrolling_vector() -> Vector2: + var viewport_dims := Vector2(Resolution.get_current_resolution()) + var mouse_vector := _mouse_pos_viewport / viewport_dims - Vector2(0.5, 0.5); + if pow(mouse_vector.x, 4) + pow(mouse_vector.y, 4) < pow(0.5 - _edge_move_threshold, 4): + mouse_vector *= 0 + return mouse_vector * _edge_move_speed + +func _cardinal_movement_vector() -> Vector2: + var move := Vector2( + float(Input.is_action_pressed(_action_east)) - float(Input.is_action_pressed(_action_west)), + float(Input.is_action_pressed(_action_south)) - float(Input.is_action_pressed(_action_north)) + ) + return move * _cardinal_move_speed + +func _clamp_over_map() -> void: + _camera.position.x = _map_mesh_corner.x + fposmod(_camera.position.x - _map_mesh_corner.x, _map_mesh_dims.x) + _camera.position.z = clamp(_camera.position.z, _map_mesh_corner.y, _map_mesh_corner.y + _map_mesh_dims.y) + +func _zoom_process(delta : float) -> void: + var height := _camera.position.y + var zoom := _zoom_target - height + height += zoom * _zoom_speed * delta + var new_zoom := _zoom_target - height + # Set to target if height is within _zoom_epsilon of it or has overshot past it + if abs(new_zoom) < _zoom_epsilon or sign(zoom) != sign(new_zoom): + height = _zoom_target + _camera.position.y = height + +func _update_orientation() -> void: + var dir := Vector3(0, -1, -exp(-_camera.position.y * 2.0 + 0.5)) + _camera.look_at(_camera.position + dir) + +func _update_mouse_map_position() -> void: + var ray_origin := _camera.project_ray_origin(_mouse_pos_viewport) + var ray_normal := _camera.project_ray_normal(_mouse_pos_viewport) + # Plane with normal (0,1,0) facing upwards, at a distance 0 from the origin + var intersection = Plane(0, 1, 0, 0).intersects_ray(ray_origin, ray_normal) + if typeof(intersection) == TYPE_VECTOR3: + var intersection_vec := intersection as Vector3 + # This loops both horizontally (good) and vertically (bad) + _mouse_pos_map = (Vector2(intersection_vec.x, intersection_vec.z) - _map_mesh_corner) / _map_mesh_dims + _map_shader_material.set_shader_parameter(_shader_param_hover_pos, _mouse_pos_map) + 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) diff --git a/game/src/GameSession/MapView.tscn b/game/src/GameSession/MapView.tscn new file mode 100644 index 0000000..4650acb --- /dev/null +++ b/game/src/GameSession/MapView.tscn @@ -0,0 +1,27 @@ +[gd_scene load_steps=6 format=3 uid="uid://dkehmdnuxih2r"] + +[ext_resource type="Script" path="res://src/GameSession/MapView.gd" id="1_exccw"] +[ext_resource type="Shader" path="res://src/GameSession/TerrainMap.gdshader" id="1_upocn"] +[ext_resource type="Texture2D" uid="uid://cmw0pvjthnn8c" path="res://common/map/terrain/terrain.png" id="3_l8pnf"] + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_tayeg"] +render_priority = 0 +shader = ExtResource("1_upocn") +shader_parameter/hover_pos = Vector2(0.5, 0.5) +shader_parameter/selected_pos = Vector2(0.5, 0.5) +shader_parameter/terrain_tex = ExtResource("3_l8pnf") + +[sub_resource type="MapMesh" id="MapMesh_3gtsd"] + +[node name="MapView" type="Node3D" node_paths=PackedStringArray("_camera", "_map_mesh_instance")] +script = ExtResource("1_exccw") +_camera = NodePath("MapCamera") +_map_mesh_instance = NodePath("MapMeshInstance") + +[node name="MapCamera" type="Camera3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 0.707107, 0.707107, 0, -0.707107, 0.707107, 0, 1, 1) + +[node name="MapMeshInstance" type="MeshInstance3D" parent="."] +transform = Transform3D(10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0) +material_override = SubResource("ShaderMaterial_tayeg") +mesh = SubResource("MapMesh_3gtsd") diff --git a/game/src/GameSession/ProvinceOverviewPanel.gd b/game/src/GameSession/ProvinceOverviewPanel.gd new file mode 100644 index 0000000..434f6b1 --- /dev/null +++ b/game/src/GameSession/ProvinceOverviewPanel.gd @@ -0,0 +1,22 @@ +extends Panel + +@export var _province_name_label : Label + +@export var province_identifier: String = "": + get: return province_identifier + set(v): + province_identifier = v + update_info() + +func _ready(): + update_info() + +func update_info() -> void: + _province_name_label.text = province_identifier + "_NAME" + visible = not province_identifier.is_empty() + +func _on_province_selected(identifier : String) -> void: + province_identifier = identifier + +func _on_button_pressed() -> void: + province_identifier = "" diff --git a/game/src/GameSession/ProvinceOverviewPanel.tscn b/game/src/GameSession/ProvinceOverviewPanel.tscn new file mode 100644 index 0000000..e21b1c3 --- /dev/null +++ b/game/src/GameSession/ProvinceOverviewPanel.tscn @@ -0,0 +1,40 @@ +[gd_scene load_steps=2 format=3 uid="uid://byq323jbel48u"] + +[ext_resource type="Script" path="res://src/GameSession/ProvinceOverviewPanel.gd" id="1_3n8k5"] + +[node name="ProvinceOverviewPanel" type="Panel" node_paths=PackedStringArray("_province_name_label")] +anchors_preset = 2 +anchor_top = 1.0 +anchor_bottom = 1.0 +offset_top = -300.0 +offset_right = 200.0 +grow_vertical = 0 +script = ExtResource("1_3n8k5") +_province_name_label = NodePath("VBoxContainer/ProvinceName") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = -1 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 10.0 +offset_top = 5.0 +offset_right = -10.0 +offset_bottom = -5.0 + +[node name="ProvinceName" type="Label" parent="VBoxContainer"] +layout_mode = 2 +text = "PROVINCE_NAME" +vertical_alignment = 1 + +[node name="Button" type="Button" parent="."] +custom_minimum_size = Vector2(30, 30) +layout_mode = 1 +anchors_preset = -1 +anchor_left = 0.85 +anchor_right = 1.0 +anchor_bottom = 0.103333 +grow_horizontal = 0 +text = "X" + +[connection signal="pressed" from="Button" to="." method="_on_button_pressed"] diff --git a/game/src/GameSession/TerrainMap.gdshader b/game/src/GameSession/TerrainMap.gdshader new file mode 100644 index 0000000..a6774fd --- /dev/null +++ b/game/src/GameSession/TerrainMap.gdshader @@ -0,0 +1,48 @@ +shader_type spatial; + +render_mode unshaded; + +// Cosmetic terrain texture +uniform sampler2D terrain_tex: source_color, repeat_enable, filter_linear; +// Province index texture +uniform sampler2D province_index_tex : repeat_enable, filter_nearest; +// Province colour texture +uniform sampler2D province_colour_tex: source_color, repeat_enable, filter_nearest; +// Position of the mouse over the map mesh in UV coords +uniform vec2 hover_pos; +// Position in UV coords of a pixel belonging to the currently selected province +uniform vec2 selected_pos; + +uvec2 vec2_to_uvec2(vec2 v) { + return uvec2(v * 255.0); +} + +uint uvec2_to_uint(uvec2 v) { + return (v.y << 8u) | v.x; +} + +uvec2 read_uvec2(sampler2D tex, vec2 uv) { + return vec2_to_uvec2(texture(tex, uv).rg); +} + +uint read_uint16(sampler2D tex, vec2 uv) { + return uvec2_to_uint(read_uvec2(tex, uv)); +} + +void fragment() { + uvec2 prov_idx_split = read_uvec2(province_index_tex, UV); + + uint prov_index = uvec2_to_uint(prov_idx_split); + uint hover_index = read_uint16(province_index_tex, hover_pos); + uint selected_index = read_uint16(province_index_tex, selected_pos); + + // Boost prov_colour's contribution if it matches hover_colour or selected_colour + float mix_val = float(prov_index == hover_index) * 0.3 + float(prov_index == selected_index) * 0.5; + // Don't mix if the province index is 0 + mix_val *= float(prov_index != 0u); + + vec3 terrain_colour = texture(terrain_tex, UV).rgb; + vec3 province_colour = texelFetch(province_colour_tex, ivec2(prov_idx_split), 0).rgb; + + ALBEDO = mix(terrain_colour, province_colour, mix_val); +} |