From 16b480f55f6c78f0909241ff9dbeada8ad1c3c49 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..442219a0 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 provinces which aren't state capitals, or don't have have a national focus applied, this will be [code]0[/code]. + + + + + + + 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 the province does not have an RGO, 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)