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)