From f1c71ccd52f776624fcdc68e2a627178d2f7c5bb Mon Sep 17 00:00:00 2001 From: Nemrav <> Date: Mon, 16 Dec 2024 15:22:08 -0400 Subject: [PATCH] Add map projections --- extension/doc_classes/MapItemSingleton.xml | 35 ++++ .../singletons/MapItemSingleton.cpp | 150 ++++++++++++++++-- .../singletons/MapItemSingleton.hpp | 14 +- game/project.godot | 5 + game/src/Game/GameSession/MapView.gd | 34 +++- game/src/Game/GameSession/MapView.tscn | 56 ++++++- game/src/Game/GameSession/MarkerManager.gd | 109 +++++++++++++ game/src/Game/GameSession/Projection.gdshader | 45 ++++++ game/src/Game/GameSession/SelectionMarkers.gd | 112 +++++++++++++ game/src/Game/GameSession/ValidMoveMarkers.gd | 97 +++++++++++ 10 files changed, 635 insertions(+), 22 deletions(-) create mode 100644 game/src/Game/GameSession/MarkerManager.gd create mode 100644 game/src/Game/GameSession/Projection.gdshader create mode 100644 game/src/Game/GameSession/SelectionMarkers.gd create mode 100644 game/src/Game/GameSession/ValidMoveMarkers.gd diff --git a/extension/doc_classes/MapItemSingleton.xml b/extension/doc_classes/MapItemSingleton.xml index 27c55502..28a7b7a1 100644 --- a/extension/doc_classes/MapItemSingleton.xml +++ b/extension/doc_classes/MapItemSingleton.xml @@ -3,6 +3,7 @@ + This singleton provides methods of optaining [code]billboard[/code] and [code]projection[/code] type graphics objects. It also provides methods for getting the proper location of these objects on the map. @@ -10,36 +11,70 @@ + Returns an array of Dictionnaries. Each dictionnary contains the keys [code]name[/code] (String), [code]texture[/code] (String), [code]scale[/code] (float), [code]noOfFrames[/code] (int) used to make a billboard object. [code]texture[/code] is a [code]String[/code] path to the billboard texture. [code]noOfFrames[/code] is a [code]float[/code] scaling factor. [code]noOfFrames[/code] is an [code]int[/code] specifying how many icons are in the texture image. + Returns an array of the positions of country capital billboards for all existing countries. The capital billboard position is the [code]city[/code] property of a province which is a capital in the game defines. + + + + + + + Searches the mouse map coordinate for any port within a radius. Returns the province index of a found port, or [code]0[/code] if no port was found within the radius of the click. + Returns an array of icon indices to use on the crimes texture to get the appropriate crime icons for every land province. An index of [code]0[/code] indicates there is no crime for that province. + Returns the size of the number of defined countries. This is the maximum possible number of capital billboards the game could have to display. + TODO: WIP Function awaiting implementation of national focuses. Returns an array of icon indices used with the national focus icons texture for every province. For state capitals with a national focus applied, this will be a number [code]>= 1[/code], otherwise it will be 0. + + + + + + + Given a province [param]index[/param], returns the position of the province's port. Emits an error and returns [code]0,0[/code] if the province does not have a port. + + + + + + Returns an array of Dictionnaries. Each dictionnary contains the keys [code]name[/code] (String), [code]texture[/code] (String), [code]size[/code] (float), [code]spin[/code] (float), [code]expanding[/code] (float), [code]duration[/code] (float), [code]additative[/code] (bool). + Returns an array of billboard positions for every land province. This corresponds to a province's [code]city[/code] define. + Returns an array of icon indices used with the trade goods (RGO) icons texture for every land province. If the province has an RGO this will be [code]>= 1[/code], otherwise it will be 0. + + + + + + + Given a province [param]index[/param], returns the province's unit position. diff --git a/extension/src/openvic-extension/singletons/MapItemSingleton.cpp b/extension/src/openvic-extension/singletons/MapItemSingleton.cpp index d1145d6e..cb252a67 100644 --- a/extension/src/openvic-extension/singletons/MapItemSingleton.cpp +++ b/extension/src/openvic-extension/singletons/MapItemSingleton.cpp @@ -1,17 +1,18 @@ #include "MapItemSingleton.hpp" #include +#include "godot_cpp/core/error_macros.hpp" #include "godot_cpp/variant/packed_int32_array.hpp" #include "godot_cpp/variant/packed_vector2_array.hpp" #include "godot_cpp/variant/typed_array.hpp" -#include "godot_cpp/variant/utility_functions.hpp" #include "godot_cpp/variant/vector2.hpp" #include "openvic-extension/singletons/GameSingleton.hpp" #include "openvic-extension/utility/ClassBindings.hpp" #include "openvic-extension/utility/Utilities.hpp" -#include "openvic-simulation/DefinitionManager.hpp" #include "openvic-simulation/country/CountryDefinition.hpp" #include "openvic-simulation/country/CountryInstance.hpp" +#include "openvic-simulation/DefinitionManager.hpp" +#include "openvic-simulation/economy/BuildingType.hpp" #include "openvic-simulation/interface/GFXObject.hpp" #include "openvic-simulation/map/ProvinceDefinition.hpp" #include "openvic-simulation/map/ProvinceInstance.hpp" @@ -29,6 +30,11 @@ void MapItemSingleton::_bind_methods() { OV_BIND_METHOD(MapItemSingleton::get_crime_icons); OV_BIND_METHOD(MapItemSingleton::get_rgo_icons); OV_BIND_METHOD(MapItemSingleton::get_national_focus_icons); + OV_BIND_METHOD(MapItemSingleton::get_projections); + OV_BIND_METHOD(MapItemSingleton::get_unit_position_by_province_index,{"index"}); + OV_BIND_METHOD(MapItemSingleton::get_port_position_by_province_index,{"index"}); + OV_BIND_METHOD(MapItemSingleton::get_clicked_port_province_index, {"position"}); + } MapItemSingleton* MapItemSingleton::get_singleton() { @@ -63,27 +69,21 @@ GFX::Billboard const* MapItemSingleton::get_billboard(std::string_view name, boo } // repackage the billboard object into a godot dictionnary for the Billboard manager to work with -bool MapItemSingleton::add_billboard_dict(std::string_view name, TypedArray& billboard_dict_array) const { +void MapItemSingleton::add_billboard_dict(GFX::Billboard const& billboard, TypedArray& billboard_dict_array) const { static const StringName name_key = "name"; static const StringName texture_key = "texture"; static const StringName scale_key = "scale"; static const StringName noOfFrames_key = "noFrames"; - GFX::Billboard const* billboard = get_billboard(name, false); - - ERR_FAIL_NULL_V_MSG(billboard, false, vformat("Failed to find billboard \"%s\"", Utilities::std_to_godot_string(name))); - Dictionary dict; - dict[name_key] = Utilities::std_to_godot_string(billboard->get_name()); - dict[texture_key] = Utilities::std_to_godot_string(billboard->get_texture_file()); - dict[scale_key] = billboard->get_scale().to_float(); - dict[noOfFrames_key] = billboard->get_no_of_frames(); + dict[name_key] = Utilities::std_to_godot_string(billboard.get_name()); + dict[texture_key] = Utilities::std_to_godot_string(billboard.get_texture_file()); + dict[scale_key] = billboard.get_scale().to_float(); + dict[noOfFrames_key] = billboard.get_no_of_frames(); billboard_dict_array.push_back(dict); - - return true; } //get an array of all the billboard dictionnaries @@ -94,8 +94,64 @@ TypedArray MapItemSingleton::get_billboards() const { TypedArray ret; for (std::unique_ptr const& obj : game_singleton->get_definition_manager().get_ui_manager().get_objects()) { - if (obj->is_type()) { - add_billboard_dict(obj->get_name(), ret); + GFX::Billboard const* billboard = obj->cast_to(); + if (billboard != nullptr) { + add_billboard_dict(*billboard, ret); + } + } + + return ret; +} + + +GFX::Projection const* MapItemSingleton::get_projection(std::string_view name, bool error_on_fail) const { + GameSingleton const* game_singleton = GameSingleton::get_singleton(); + ERR_FAIL_NULL_V(game_singleton, nullptr); + + GFX::Projection const* projection = + game_singleton->get_definition_manager().get_ui_manager().get_cast_object_by_identifier(name); + + if (error_on_fail) { + ERR_FAIL_NULL_V_MSG( + projection, nullptr, vformat("Failed to find projection \"%s\"", Utilities::std_to_godot_string(name)) + ); + } + + return projection; +} + +void MapItemSingleton::add_projection_dict(GFX::Projection const& projection, TypedArray& projection_dict_array) const { + static const StringName name_key = "name"; + static const StringName texture_key = "texture"; + static const StringName size_key = "size"; + static const StringName spin_key = "spin"; + static const StringName expanding_key = "expanding"; + static const StringName duration_key = "duration"; + static const StringName additative_key = "additative"; + + Dictionary dict; + + dict[name_key] = Utilities::std_to_godot_string(projection.get_name()); + dict[texture_key] = Utilities::std_to_godot_string(projection.get_texture_file()); + dict[size_key] = projection.get_size().to_float(); + dict[spin_key] = projection.get_spin().to_float(); + dict[expanding_key] = projection.get_expanding().to_float(); + dict[duration_key] = projection.get_duration().to_float(); + dict[additative_key] = projection.get_additative(); + + projection_dict_array.push_back(dict); +} + +TypedArray MapItemSingleton::get_projections() const { + GameSingleton const* game_singleton = GameSingleton::get_singleton(); + ERR_FAIL_NULL_V(game_singleton, {}); + + TypedArray ret; + + for (std::unique_ptr const& obj : game_singleton->get_definition_manager().get_ui_manager().get_objects()) { + GFX::Projection const* projection = obj->cast_to(); + if (projection != nullptr) { + add_projection_dict(*projection, ret); } } @@ -260,3 +316,67 @@ PackedByteArray MapItemSingleton::get_national_focus_icons() const { return icons; } + + +Vector2 MapItemSingleton::get_unit_position_by_province_index(int32_t province_index) const { + GameSingleton const* game_singleton = GameSingleton::get_singleton(); + ERR_FAIL_NULL_V(game_singleton, {}); + + ProvinceDefinition const* province = game_singleton->get_definition_manager().get_map_definition() + .get_province_definition_by_index(province_index); + ERR_FAIL_NULL_V_MSG(province, {}, vformat("Cannot get unit position - invalid province index: %d", province_index)); + + return Utilities::to_godot_fvec2(province->get_unit_position()) + / GameSingleton::get_singleton()->get_map_dims(); +} + +Vector2 MapItemSingleton::get_port_position_by_province_index(int32_t province_index) const { + GameSingleton const* game_singleton = GameSingleton::get_singleton(); + ERR_FAIL_NULL_V(game_singleton, {}); + + ProvinceDefinition const* province = game_singleton->get_definition_manager().get_map_definition() + .get_province_definition_by_index(province_index); + ERR_FAIL_NULL_V_MSG(province, {}, vformat("Cannot get port position - invalid province index: %d", province_index)); + ERR_FAIL_COND_V_MSG(!province->has_port(), {},vformat("Cannot get port position - invalid province index: %d", province_index) ); + + BuildingType const* port_building_type = game_singleton->get_definition_manager().get_economy_manager().get_building_type_manager().get_port_building_type(); + fvec2_t const* port_position = province->get_building_position(port_building_type); + + return Utilities::to_godot_fvec2(*port_position) / GameSingleton::get_singleton()->get_map_dims(); +} + +const static float port_radius = 0.0006; //how close we have to click for a detection + +//Searches provinces near the one clicked and finds +int32_t MapItemSingleton::get_clicked_port_province_index(Vector2 click_position) const { + GameSingleton const* game_singleton = GameSingleton::get_singleton(); + ERR_FAIL_NULL_V(game_singleton, {}); + + int32_t initial_province_index = game_singleton->get_province_index_from_uv_coords(click_position); + + //now get all the provinces around this one + ProvinceDefinition const* province = game_singleton->get_definition_manager().get_map_definition() + .get_province_definition_by_index(initial_province_index); + ERR_FAIL_NULL_V_MSG(province, {}, vformat("Cannot get port position - invalid province index: %d", initial_province_index)); + + BuildingType const* port_building_type = game_singleton->get_definition_manager().get_economy_manager().get_building_type_manager().get_port_building_type(); + + if(province->has_port()){ + Vector2 port_position = Utilities::to_godot_fvec2(*province->get_building_position(port_building_type)) / GameSingleton::get_singleton()->get_map_dims(); + if(click_position.distance_to(port_position) <= port_radius){ + return province->get_index(); + } + } + else if(province->is_water()){ + for(ProvinceDefinition::adjacency_t const& adjacency : province->get_adjacencies()) { + ProvinceDefinition const* adjacent_province = adjacency.get_to(); + if(!adjacent_province->has_port()) continue; // skip provinces without ports (ie. other water provinces) + Vector2 port_position = Utilities::to_godot_fvec2(*adjacent_province->get_building_position(port_building_type)) / GameSingleton::get_singleton()->get_map_dims(); + if(click_position.distance_to(port_position) <= port_radius){ + return adjacent_province->get_index(); + } + } + } + + return 0; +} \ No newline at end of file diff --git a/extension/src/openvic-extension/singletons/MapItemSingleton.hpp b/extension/src/openvic-extension/singletons/MapItemSingleton.hpp index 867ac8d3..8fd326ed 100644 --- a/extension/src/openvic-extension/singletons/MapItemSingleton.hpp +++ b/extension/src/openvic-extension/singletons/MapItemSingleton.hpp @@ -5,8 +5,7 @@ #include #include -//billboards, projections, and progress bar -//for now though, only billboards +//billboards, projections, and progress bar (no progress bar yet) namespace OpenVic { class MapItemSingleton : public godot::Object { @@ -25,8 +24,13 @@ namespace OpenVic { private: GFX::Billboard const* get_billboard(std::string_view name, bool error_on_fail = true) const; - bool add_billboard_dict(std::string_view name, godot::TypedArray& billboard_dict_array) const; + void add_billboard_dict(GFX::Billboard const& billboard, godot::TypedArray& billboard_dict_array) const; godot::TypedArray get_billboards() const; + + GFX::Projection const* get_projection(std::string_view name, bool error_on_fail = true) const; + void add_projection_dict(GFX::Projection const& projection, godot::TypedArray& projection_dict_array) const; + godot::TypedArray get_projections() const; + godot::PackedVector2Array get_province_positions() const; int32_t get_max_capital_count() const; godot::PackedVector2Array get_capital_positions() const; @@ -34,5 +38,9 @@ namespace OpenVic { godot::PackedByteArray get_crime_icons() const; godot::PackedByteArray get_rgo_icons() const; godot::PackedByteArray get_national_focus_icons() const; + + godot::Vector2 get_unit_position_by_province_index(int32_t province_index) const; + godot::Vector2 get_port_position_by_province_index(int32_t province_index) const; + int32_t get_clicked_port_province_index(godot::Vector2 click_position) const; }; } diff --git a/game/project.godot b/game/project.godot index cd3e6641..b20fdda8 100644 --- a/game/project.godot +++ b/game/project.godot @@ -141,6 +141,11 @@ menu_pause={ "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":0,"physical_keycode":4194305,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } +select_add={ +"deadzone": 0.5, +"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":false,"double_click":false,"script":null) +] +} [internationalization] diff --git a/game/src/Game/GameSession/MapView.gd b/game/src/Game/GameSession/MapView.gd index 6bf84f30..3b21dea7 100644 --- a/game/src/Game/GameSession/MapView.gd +++ b/game/src/Game/GameSession/MapView.gd @@ -14,6 +14,7 @@ const _action_zoom_out : StringName = &"map_zoom_out" const _action_drag : StringName = &"map_drag" const _action_click : StringName = &"map_click" const _action_right_click : StringName = &"map_right_click" +const _action_select_add : StringName = &"select_add" @export var _camera : Camera3D @@ -60,6 +61,11 @@ var _viewport_dims : Vector2 = Vector2(1, 1) @export var _map_text : MapText +@export var validMoveMarkers : ValidMoveMarkers +@export var selectionMarkers : SelectionMarkers +var land_units_selected : Array = [] +var naval_units_selected : Array = [] + # ??? Strange Godot/GDExtension Bug ??? # Upon first opening a clone of this repo with the Godot Editor, # if GameSingleton.get_province_index_image is called before MapMesh @@ -201,6 +207,7 @@ func _input(event : InputEvent) -> void: # * SS-31 # * SS-75 var _cardinal_movement_vector := Vector2.ZERO +var temp_id : int = 0 func _unhandled_input(event : InputEvent) -> void: if event is InputEventMouseMotion: _mouse_over_viewport = true @@ -215,19 +222,42 @@ func _unhandled_input(event : InputEvent) -> void: _action_south ) * _cardinal_move_speed + elif event.is_action_pressed(_action_select_add): + if _mouse_over_viewport: + if _map_mesh.is_valid_uv_coord(_mouse_pos_map): + GameSingleton.set_selected_province(GameSingleton.get_province_index_from_uv_coords(_mouse_pos_map)) + var province_index : int = GameSingleton.get_province_index_from_uv_coords(_mouse_pos_map) + + var port_province_index : int = MapItemSingleton.get_clicked_port_province_index(_mouse_pos_map) + if port_province_index != 0: + var port_pos : Vector2 = MapItemSingleton.get_port_position_by_province_index(port_province_index) + selectionMarkers.add_selection_marker(temp_id,_map_to_world_coords(port_pos)) + else: + var unit_position : Vector2 = MapItemSingleton.get_unit_position_by_province_index(province_index) + selectionMarkers.add_selection_marker(temp_id,_map_to_world_coords(unit_position)) + temp_id += 1 + elif event.is_action_pressed(_action_click): if _mouse_over_viewport: # Check if the mouse is outside of bounds if _map_mesh.is_valid_uv_coord(_mouse_pos_map): - GameSingleton.set_selected_province(GameSingleton.get_province_index_from_uv_coords(_mouse_pos_map)) + selectionMarkers.clear_selection_markers() else: print("Clicked outside the map!") elif event.is_action_pressed(_action_right_click): if _mouse_over_viewport: if _map_mesh.is_valid_uv_coord(_mouse_pos_map): + var province_index : int = GameSingleton.get_province_index_from_uv_coords(_mouse_pos_map) + var port_province_index : int = MapItemSingleton.get_clicked_port_province_index(_mouse_pos_map) + if port_province_index != 0: + var port_pos : Vector2 = MapItemSingleton.get_port_position_by_province_index(port_province_index) + validMoveMarkers.add_move_marker(_map_to_world_coords(port_pos), randi_range(0,1)) + else: + var unit_position : Vector2 = MapItemSingleton.get_unit_position_by_province_index(province_index) + validMoveMarkers.add_move_marker(_map_to_world_coords(unit_position), randi_range(0,1)) # TODO - open diplomacy screen on province owner or viewed country if province has no owner #Events.NationManagementScreens.open_nation_management_screen(NationManagement.Screen.DIPLOMACY) - GameSingleton.set_viewed_country_by_province_index(GameSingleton.get_province_index_from_uv_coords(_mouse_pos_map)) + GameSingleton.set_viewed_country_by_province_index(province_index) else: print("Right-clicked outside the map!") elif event.is_action_pressed(_action_drag): diff --git a/game/src/Game/GameSession/MapView.tscn b/game/src/Game/GameSession/MapView.tscn index 385a24df..ae0d0b18 100644 --- a/game/src/Game/GameSession/MapView.tscn +++ b/game/src/Game/GameSession/MapView.tscn @@ -1,8 +1,37 @@ -[gd_scene load_steps=8 format=3 uid="uid://dkehmdnuxih2r"] +[gd_scene load_steps=16 format=3 uid="uid://dkehmdnuxih2r"] [ext_resource type="Script" path="res://src/Game/GameSession/MapView.gd" id="1_exccw"] [ext_resource type="Shader" path="res://src/Game/GameSession/TerrainMap.gdshader" id="1_upocn"] [ext_resource type="Script" path="res://src/Game/GameSession/MapText.gd" id="2_13bgq"] +[ext_resource type="Script" path="res://src/Game/GameSession/MarkerManager.gd" id="2_xnmdg"] +[ext_resource type="Script" path="res://src/Game/GameSession/SelectionMarkers.gd" id="3_fi78k"] +[ext_resource type="Shader" path="res://src/Game/GameSession/Projection.gdshader" id="4_2f4io"] +[ext_resource type="Script" path="res://src/Game/GameSession/ValidMoveMarkers.gd" id="5_h3t3v"] + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_blhuf"] +render_priority = 0 +shader = ExtResource("4_2f4io") +shader_parameter/sizes = null +shader_parameter/spin = null +shader_parameter/expanding = null +shader_parameter/duration = null +shader_parameter/additative = null +shader_parameter/time = 0.0 +shader_parameter/projections = null + +[sub_resource type="QuadMesh" id="QuadMesh_72iss"] +material = SubResource("ShaderMaterial_blhuf") +orientation = 1 + +[sub_resource type="MultiMesh" id="MultiMesh_rytvv"] +transform_format = 1 +use_custom_data = true +mesh = SubResource("QuadMesh_72iss") + +[sub_resource type="MultiMesh" id="MultiMesh_cq0pk"] +transform_format = 1 +use_custom_data = true +mesh = SubResource("QuadMesh_72iss") [sub_resource type="ShaderMaterial" id="ShaderMaterial_tayeg"] render_priority = 0 @@ -24,18 +53,39 @@ albedo_color = Color(0, 0, 0, 1) material = SubResource("StandardMaterial3D_irk50") size = Vector2(6, 2) -[node name="MapView" type="Node3D" node_paths=PackedStringArray("_camera", "_map_mesh_instance", "_map_background_instance", "_map_text")] +[node name="MapView" type="Node3D" node_paths=PackedStringArray("_camera", "_map_mesh_instance", "_map_background_instance", "_map_text", "validMoveMarkers", "selectionMarkers")] editor_description = "SS-73" script = ExtResource("1_exccw") _camera = NodePath("MapCamera") _map_mesh_instance = NodePath("MapMeshInstance") _map_background_instance = NodePath("MapBackgroundInstance") _map_text = NodePath("MapText") +validMoveMarkers = NodePath("ProjectionManager/ValidMoveMarkers") +selectionMarkers = NodePath("ProjectionManager/SelectionMarkers") [node name="MapCamera" type="Camera3D" parent="."] transform = Transform3D(1, 0, 0, 0, 0.707107, 0.707107, 0, -0.707107, 0.707107, 0.25, 1.5, -2.75) near = 0.01 +[node name="ProjectionManager" type="Node3D" parent="." node_paths=PackedStringArray("selectionMarkers", "moveMarkers")] +script = ExtResource("2_xnmdg") +selectionMarkers = NodePath("SelectionMarkers") +moveMarkers = NodePath("ValidMoveMarkers") + +[node name="SelectionMarkers" type="MultiMeshInstance3D" parent="ProjectionManager" node_paths=PackedStringArray("manager")] +cast_shadow = 0 +gi_mode = 0 +multimesh = SubResource("MultiMesh_rytvv") +script = ExtResource("3_fi78k") +manager = NodePath("..") + +[node name="ValidMoveMarkers" type="MultiMeshInstance3D" parent="ProjectionManager" node_paths=PackedStringArray("manager")] +cast_shadow = 0 +gi_mode = 0 +multimesh = SubResource("MultiMesh_cq0pk") +script = ExtResource("5_h3t3v") +manager = NodePath("..") + [node name="MapText" type="Node3D" parent="." node_paths=PackedStringArray("_map_view")] script = ExtResource("2_13bgq") _map_view = NodePath("..") @@ -56,4 +106,6 @@ light_energy = 1.5 light_bake_mode = 0 sky_mode = 1 +[connection signal="detailed_view_changed" from="." to="ProjectionManager/SelectionMarkers" method="set_visible"] +[connection signal="detailed_view_changed" from="." to="ProjectionManager/ValidMoveMarkers" method="set_visible"] [connection signal="detailed_view_changed" from="." to="MapText" method="set_visible"] diff --git a/game/src/Game/GameSession/MarkerManager.gd b/game/src/Game/GameSession/MarkerManager.gd new file mode 100644 index 00000000..d36847ab --- /dev/null +++ b/game/src/Game/GameSession/MarkerManager.gd @@ -0,0 +1,109 @@ +extends Node3D +class_name ProjectionManager + +enum ProjectionType {INVALIDTYPE, SELECTED, LEGAL_MOVE, ILLEGAL_MOVE} +const PROJECTION_NAMES : Dictionary = { + ProjectionType.SELECTED : &"selection_projection", + ProjectionType.LEGAL_MOVE : &"legal_selection_projection", + ProjectionType.ILLEGAL_MOVE : &"illegal_selection_projection" +} + +@export var selectionMarkers : SelectionMarkers +@export var moveMarkers : ValidMoveMarkers + +var Type_to_Index : Dictionary + +var material : ShaderMaterial +var textures : Array[Texture2D] +var sizes : PackedFloat32Array +var spins : PackedFloat32Array +var expansions : PackedFloat32Array +var durations : PackedFloat32Array +var transparency_mode : PackedByteArray + +const SCALE_FACTOR : float = 1.0 / 256.0 +const SPIN_FACTOR : float = 4.0 +const HEIGHT_ADD_FACTOR : Vector3 = Vector3(0,0.002,0) +const GROW_FACTOR : float = 0.25 + +var time : float = 0.0 + +# For the markers (selection compass, legal/illegal move markers) +# this class handles the setup of the projection shader, and the maintenance +# of the time variable for all 3 scripts. +# the two multimeshes handle the individual instances. + + +func _ready() -> void: + const name_key : StringName = &"name"; + const texture_key : StringName = &"texture"; + const size_key : StringName = &"size"; + const spin_key : StringName = &"spin"; + const expanding_key : StringName = &"expanding"; + const duration_key : StringName = &"duration"; + const additative_key : StringName = &"additative"; + + for projection : Dictionary in MapItemSingleton.get_projections(): + var projection_name : StringName = projection[name_key] + + #keep only projections we are currently handling + var projection_type : ProjectionType = ProjectionType.INVALIDTYPE + for key : ProjectionType in PROJECTION_NAMES: + if projection_name == PROJECTION_NAMES[key]: + projection_type = key + break + + if projection_type == ProjectionType.INVALIDTYPE: + continue + + var texture_name : StringName = projection[texture_key] + var size : float = projection[size_key] + var spin : float = projection[spin_key] + var expanding : float = projection[expanding_key] + var duration : float = projection[duration_key] + var additative : bool = projection[additative_key] + + #fix the alpha edges of the projection textures + var texture : ImageTexture = AssetManager.get_texture(texture_name) + if texture == null: + push_error("Texture for projection \"", projection_name, "\" was null!") + continue + var image : Image = texture.get_image() + image.fix_alpha_edges() + texture.set_image(image) + + # We use the texture array size (which will be the same as frames and scales' sizes) + # rather than projection_index as the former only counts projections we're actually using, + # while the latter counts all projections defined in the game's GFX files + Type_to_Index[projection_type] = textures.size() + + textures.push_back(texture) + sizes.push_back(size * SCALE_FACTOR) + spins.push_back(spin * SPIN_FACTOR) + expansions.push_back(expanding * GROW_FACTOR) + durations.push_back(duration) + transparency_mode.push_back(additative) + + material = moveMarkers.multimesh.mesh.surface_get_material(0) + if material == null: + push_error("ShaderMaterial for projections was null") + return + + material.set_shader_parameter(&"projections", textures) + material.set_shader_parameter(&"sizes", sizes) + material.set_shader_parameter(&"spin", spins) + material.set_shader_parameter(&"expanding", expansions) + material.set_shader_parameter(&"additative", transparency_mode) + material.set_shader_parameter(&"duration",durations) + + moveMarkers.multimesh.mesh.surface_set_material(0, material) + selectionMarkers.multimesh.mesh.surface_set_material(0, material) + + moveMarkers.setup() + selectionMarkers.setup() + +func _process(delta : float) -> void: + time += delta + material.set_shader_parameter(&"time", time) + moveMarkers.set_time(time) + selectionMarkers.set_time(time) diff --git a/game/src/Game/GameSession/Projection.gdshader b/game/src/Game/GameSession/Projection.gdshader new file mode 100644 index 00000000..8c53f87f --- /dev/null +++ b/game/src/Game/GameSession/Projection.gdshader @@ -0,0 +1,45 @@ +shader_type spatial; +render_mode unshaded; + +uniform sampler2D projections[3] : source_color; +uniform float sizes[3]; +uniform float spin[3]; +uniform float expanding[3]; +uniform float duration[3]; +uniform bool additative[3]; //if true, black becomes a transparency colour +uniform float time = 0.0; + +void vertex() { + COLOR = INSTANCE_CUSTOM; + uint type = uint(COLOR.x + 0.5); + float start_time = COLOR.y; + + float rot = time*spin[type]; + mat3 rotation_matrix = mat3( + vec3(cos(-rot), 0.0, sin(-rot)), + vec3(0.0, 1.0, 0.0), + vec3(-sin(-rot), 0.0, cos(-rot)) + ); + VERTEX.xyz *= rotation_matrix * + clamp( + expanding[type] * (time-start_time), + 0.0, + sizes[type] + ); +} + +void fragment() { + // Called for every pixel the material is visible on. + uint type = uint(COLOR.x + 0.5); + vec4 sample = texture(projections[type],UV); + ALBEDO.rgb = sample.rgb; + + float start_time = COLOR.y; + //duration == 0 -> 1, duration == 1 -> 0 + float unlimited = float(duration[type] == 0.0); + float is_finished = unlimited + ceil(clamp(duration[type] - time + start_time,0.0,1.0)); + + //if additative, then black = transparent, otherwise use alpha + float add_tr = float(additative[type]); + ALPHA = is_finished*((1.0 - add_tr)*sample.a + add_tr*(sample.r+sample.g+sample.b)); +} \ No newline at end of file diff --git a/game/src/Game/GameSession/SelectionMarkers.gd b/game/src/Game/GameSession/SelectionMarkers.gd new file mode 100644 index 00000000..e48f653f --- /dev/null +++ b/game/src/Game/GameSession/SelectionMarkers.gd @@ -0,0 +1,112 @@ +class_name SelectionMarkers +extends MultiMeshInstance3D + +@export var manager : ProjectionManager + +var time : float = 0.0 +const MIN_INSTANCE_COUNT : int = 32 +const INSTANCE_COUNT_GROW_AMOUNT : int = 4 +const HEIGHT_ADD_FACTOR : Vector3 = Vector3(0,0.002,0) + +var ids : PackedInt32Array +const SELECT_TYPE : ProjectionManager.ProjectionType = ProjectionManager.ProjectionType.SELECTED +var select_index : int = 0 + +# Called when the node enters the scene tree for the first time. +func setup() -> void: + select_index = manager.Type_to_Index[SELECT_TYPE] + # 1) setting instance_count clears and resizes the buffer + # so we want to find the max size once and leave it + # 2) resize must occur after setting the transform format + + ids.resize(MIN_INSTANCE_COUNT) + ids.fill(-1) + #unlike billboards, we don't know how many of these we'll need. Quads are pretty small + # and multimesh means we can have a lot of them, so be generous, and resize later if we have to + multimesh.instance_count = MIN_INSTANCE_COUNT + multimesh.visible_instance_count = 0#MIN_INSTANCE_COUNT + + for i : int in MIN_INSTANCE_COUNT: + multimesh.set_instance_transform( + i,Transform3D(Basis(), Vector3(float(i),0.0,0.0)) + ) + +# for markers with unlimited duration, time will rollover eventually. so we need a way to skip past +# the expansion animation that occurs at start_time every hour. can probably do this by adding or subtracting +# 1/expansion somewhere + +#interface for the instance uniforms is +#INSTANCE_CUSTOM (COLOR).x = type selection, .y = start time + +func add_selection_marker(unit_id : int, unit_position : Vector3) -> void: + var slot_found : bool = false + for i : int in ids.size(): + var id : int = ids[i] + if id < 0: + multimesh.set_instance_transform( + i,Transform3D(Basis(), unit_position + HEIGHT_ADD_FACTOR) + ) + multimesh.set_instance_custom_data(i,Color( + select_index,time,1.0,0.0 + )) + ids[i] = unit_id + slot_found = true + break + + if !slot_found: + _grow_buffer() + + var i : int = ids.size() + ids.resize(i + INSTANCE_COUNT_GROW_AMOUNT) + + multimesh.set_instance_transform( + i,Transform3D(Basis(), unit_position + HEIGHT_ADD_FACTOR) + ) + multimesh.set_instance_custom_data(i,Color( + select_index,time,2.0,0.0 + )) + ids[i] = unit_id + for j : int in ids.size() - i: + ids[i + j] = -1 + multimesh.visible_instance_count += 1 + +func update_selection_marker(unit_id : int, unit_position : Vector3) -> void: + var index : int = ids.find(unit_id) + multimesh.set_instance_transform( + index,Transform3D(Basis(), unit_position + HEIGHT_ADD_FACTOR) + ) + +#TODO: Perhaps there's a more efficient method... +func add_selection_markers(unit_ids : PackedInt32Array, unit_positions : PackedVector3Array) -> void: + assert(unit_ids.size() == unit_positions.size()) + multimesh.visible_instance_count += unit_positions.size() + for index : int in unit_ids.size(): + add_selection_marker(unit_ids[index],unit_positions[index]) + +func update_selection_markers(unit_ids : PackedInt32Array, unit_positions : PackedVector3Array) -> void: + assert(unit_ids.size() == unit_positions.size()) + for index : int in unit_ids.size(): + update_selection_marker(unit_ids[index],unit_positions[index]) + +func clear_selection_markers() -> void: + #no units should display + ids.fill(-1) + multimesh.visible_instance_count = 0 + +func set_time(timeIn : float) -> void: + time = timeIn + +#Grow the multimesh without losing the current data +func _grow_buffer(amount : int = INSTANCE_COUNT_GROW_AMOUNT) -> void: + # we can control the multimesh buffer directly + # 12 floats for transform, 4 floats for colour, 4 floats for custom data + # colour and custom data are optional + const TR_SIZE : int = 12 + const TR_CUSTOM_SIZE : int = 16 + const TR_COLOR_CUSTOM_SIZE : int = 20 + + var temp_buffer : PackedFloat32Array = multimesh.buffer + temp_buffer.resize(temp_buffer.size() + amount*TR_CUSTOM_SIZE) + + multimesh.instance_count += amount + multimesh.set_buffer(temp_buffer) diff --git a/game/src/Game/GameSession/ValidMoveMarkers.gd b/game/src/Game/GameSession/ValidMoveMarkers.gd new file mode 100644 index 00000000..e0fc3443 --- /dev/null +++ b/game/src/Game/GameSession/ValidMoveMarkers.gd @@ -0,0 +1,97 @@ +class_name ValidMoveMarkers +extends MultiMeshInstance3D + +@export var manager : ProjectionManager + +var projection_type_to_index : Dictionary + +var time : float = 0.0 +const MIN_INSTANCE_COUNT : int = 32 +const INSTANCE_COUNT_GROW_AMOUNT : int = 4 +const HEIGHT_ADD_FACTOR : Vector3 = Vector3(0,0.002,0) +var ages : PackedFloat32Array + +const LEGAL_TYPE : ProjectionManager.ProjectionType = ProjectionManager.ProjectionType.LEGAL_MOVE +const ILLEGAL_TYPE : ProjectionManager.ProjectionType = ProjectionManager.ProjectionType.ILLEGAL_MOVE +var legal_index : int = 0 +var illegal_index : int = 0 + + +# Called when the node enters the scene tree for the first time. +func setup() -> void: + legal_index = manager.Type_to_Index[LEGAL_TYPE] + illegal_index = manager.Type_to_Index[ILLEGAL_TYPE] + + ages.resize(MIN_INSTANCE_COUNT) + ages.fill(0.0) + + #unlike billboards, we don't know how many of these we'll need. Quads are pretty small + # and multimesh means we can have a lot of them, so be generous, and resize later if we have to + multimesh.instance_count = MIN_INSTANCE_COUNT + multimesh.visible_instance_count = MIN_INSTANCE_COUNT + + for i : int in MIN_INSTANCE_COUNT: + multimesh.set_instance_transform( + i,Transform3D(Basis(), Vector3(float(i),0.0,0.0)) + ) + +#interface for the instance uniforms is +#INSTANCE_CUSTOM (COLOR).x = type selection, .y = start time + +func add_move_marker(marker_position : Vector3, was_legal_move : bool) -> void: + var type : ProjectionManager.ProjectionType = ILLEGAL_TYPE + if was_legal_move: + type = LEGAL_TYPE + + var duration : float = float(manager.durations[manager.Type_to_Index[type]]) + + var slot_found : bool = false + for i : int in ages.size(): + var age : float = ages[i] + if age <= 0: + multimesh.set_instance_transform( + i,Transform3D(Basis(), marker_position + HEIGHT_ADD_FACTOR) + ) + multimesh.set_instance_custom_data(i,Color( + manager.Type_to_Index[type],time,0.0,0.0 + )) + ages[i] = duration + slot_found = true + break + + if !slot_found: + _grow_buffer() + + var i = ages.size() + ages.resize(i + INSTANCE_COUNT_GROW_AMOUNT) + + multimesh.set_instance_transform( + i,Transform3D(Basis(), marker_position + HEIGHT_ADD_FACTOR) + ) + multimesh.set_instance_custom_data(i,Color( + manager.Type_to_Index[type],time,0.0,0.0 + )) + ages[i] = duration + +func set_time(timeIn : float) -> void: + time = timeIn + +func _process(delta : float) -> void: + for i : int in ages.size(): + ages[i] -= delta + +#Grow the multimesh without losing the current data +func _grow_buffer(amount : int = INSTANCE_COUNT_GROW_AMOUNT) -> void: + # we can control the multimesh buffer directly + # 12 floats for transform, 4 floats for colour, 4 floats for custom data + # colour and custom data are optional + const TR_SIZE : int = 12 + const TR_CUSTOM_SIZE : int = 16 + const TR_COLOR_CUSTOM_SIZE : int = 20 + + var temp_buffer : PackedFloat32Array = multimesh.buffer + temp_buffer.resize(temp_buffer.size() + amount*TR_CUSTOM_SIZE) + + multimesh.instance_count += amount + multimesh.visible_instance_count += amount + multimesh.set_buffer(temp_buffer)