aboutsummaryrefslogtreecommitdiff
path: root/game/src/Game/GameSession
diff options
context:
space:
mode:
Diffstat (limited to 'game/src/Game/GameSession')
-rw-r--r--game/src/Game/GameSession/GameSession.gd16
-rw-r--r--game/src/Game/GameSession/GameSession.tscn95
-rw-r--r--game/src/Game/GameSession/GameSessionMenu.gd80
-rw-r--r--game/src/Game/GameSession/GameSessionMenu.tscn91
-rw-r--r--game/src/Game/GameSession/GameSpeedPanel.gd37
-rw-r--r--game/src/Game/GameSession/GameSpeedPanel.tscn38
-rw-r--r--game/src/Game/GameSession/MapControlPanel/MapControlPanel.gd57
-rw-r--r--game/src/Game/GameSession/MapControlPanel/MapControlPanel.tscn107
-rw-r--r--game/src/Game/GameSession/MapControlPanel/Minimap.gd105
-rw-r--r--game/src/Game/GameSession/MapControlPanel/Minimap.gdshader18
-rw-r--r--game/src/Game/GameSession/MapView.gd242
-rw-r--r--game/src/Game/GameSession/MapView.tscn30
-rw-r--r--game/src/Game/GameSession/ProvinceIndexSampler.gdshaderinc18
-rw-r--r--game/src/Game/GameSession/ProvinceOverviewPanel/ProvinceOverviewPanel.gd122
-rw-r--r--game/src/Game/GameSession/ProvinceOverviewPanel/ProvinceOverviewPanel.tscn86
-rw-r--r--game/src/Game/GameSession/TerrainMap.gdshader50
16 files changed, 1192 insertions, 0 deletions
diff --git a/game/src/Game/GameSession/GameSession.gd b/game/src/Game/GameSession/GameSession.gd
new file mode 100644
index 0000000..5bb1c2f
--- /dev/null
+++ b/game/src/Game/GameSession/GameSession.gd
@@ -0,0 +1,16 @@
+extends Control
+
+@export var _game_session_menu : Control
+
+func _ready():
+ Events.Options.load_settings_from_file()
+ if GameSingleton.setup_game() != OK:
+ push_error("Failed to setup game")
+
+func _process(delta : float):
+ GameSingleton.try_tick()
+
+# REQUIREMENTS:
+# * SS-42
+func _on_game_session_menu_button_pressed() -> void:
+ _game_session_menu.visible = !_game_session_menu.visible
diff --git a/game/src/Game/GameSession/GameSession.tscn b/game/src/Game/GameSession/GameSession.tscn
new file mode 100644
index 0000000..188ccde
--- /dev/null
+++ b/game/src/Game/GameSession/GameSession.tscn
@@ -0,0 +1,95 @@
+[gd_scene load_steps=10 format=3 uid="uid://bgnupcshe1m7r"]
+
+[ext_resource type="Script" path="res://src/GameSession/GameSession.gd" id="1_eklvp"]
+[ext_resource type="PackedScene" uid="uid://cvl76duuym1wq" path="res://src/MusicConductor/MusicPlayer.tscn" id="2_kt6aa"]
+[ext_resource type="PackedScene" uid="uid://g524p8lr574w" path="res://src/GameSession/MapControlPanel/MapControlPanel.tscn" id="3_afh6d"]
+[ext_resource type="PackedScene" uid="uid://dvdynl6eir40o" path="res://src/GameSession/GameSessionMenu.tscn" id="3_bvmqh"]
+[ext_resource type="PackedScene" uid="uid://dkehmdnuxih2r" path="res://src/GameSession/MapView.tscn" id="4_xkg5j"]
+[ext_resource type="PackedScene" uid="uid://byq323jbel48u" path="res://src/GameSession/ProvinceOverviewPanel/ProvinceOverviewPanel.tscn" id="5_osjnn"]
+[ext_resource type="PackedScene" uid="uid://cnbfxjy1m6wja" path="res://src/OptionMenu/OptionsMenu.tscn" id="6_p5mnx"]
+[ext_resource type="PackedScene" uid="uid://dd8k3p7r3huwc" path="res://src/GameSession/GameSpeedPanel.tscn" id="7_myy4q"]
+[ext_resource type="PackedScene" uid="uid://d3g6wbvwflmyk" path="res://src/SaveLoadMenu/SaveLoadMenu.tscn" id="8_4g7ko"]
+
+[node name="GameSession" type="Control" node_paths=PackedStringArray("_game_session_menu")]
+editor_description = "SS-102, UI-546"
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 2
+script = ExtResource("1_eklvp")
+_game_session_menu = NodePath("GameSessionMenu")
+
+[node name="MapView" parent="." instance=ExtResource("4_xkg5j")]
+
+[node name="MapControlPanel" parent="." instance=ExtResource("3_afh6d")]
+layout_mode = 1
+anchors_preset = 3
+anchor_left = 1.0
+anchor_top = 1.0
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 0
+grow_vertical = 0
+
+[node name="ProvinceOverviewPanel" parent="." instance=ExtResource("5_osjnn")]
+layout_mode = 1
+
+[node name="GameSpeedPanel" parent="." instance=ExtResource("7_myy4q")]
+layout_mode = 0
+offset_right = 302.0
+offset_bottom = 31.0
+
+[node name="GameSessionMenu" parent="." instance=ExtResource("3_bvmqh")]
+visible = false
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -159.0
+offset_top = -165.0
+offset_right = 159.0
+offset_bottom = 165.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="OptionsMenu" parent="." instance=ExtResource("6_p5mnx")]
+visible = false
+layout_mode = 1
+
+[node name="SaveLoadMenu" parent="." instance=ExtResource("8_4g7ko")]
+visible = false
+layout_mode = 1
+anchors_preset = -1
+anchor_left = 0.5
+anchor_right = 0.5
+offset_left = -640.0
+offset_right = 640.0
+
+[node name="MusicPlayer" parent="." instance=ExtResource("2_kt6aa")]
+layout_mode = 1
+anchors_preset = 1
+anchor_left = 1.0
+anchor_right = 1.0
+offset_left = -150.0
+offset_right = 0.0
+grow_horizontal = 0
+
+[connection signal="map_view_camera_changed" from="MapView" to="MapControlPanel" method="_on_map_view_camera_changed"]
+[connection signal="game_session_menu_button_pressed" from="MapControlPanel" to="." method="_on_game_session_menu_button_pressed"]
+[connection signal="minimap_clicked" from="MapControlPanel" to="MapView" method="_on_minimap_clicked"]
+[connection signal="mouse_entered" from="MapControlPanel" to="MapView" method="_on_mouse_exited_viewport"]
+[connection signal="mouse_exited" from="MapControlPanel" to="MapView" method="_on_mouse_entered_viewport"]
+[connection signal="zoom_in_button_pressed" from="MapControlPanel" to="MapView" method="zoom_in"]
+[connection signal="zoom_out_button_pressed" from="MapControlPanel" to="MapView" method="zoom_out"]
+[connection signal="mouse_entered" from="ProvinceOverviewPanel" to="MapView" method="_on_mouse_exited_viewport"]
+[connection signal="mouse_exited" from="ProvinceOverviewPanel" to="MapView" method="_on_mouse_entered_viewport"]
+[connection signal="load_button_pressed" from="GameSessionMenu" to="SaveLoadMenu" method="show_for_load"]
+[connection signal="options_button_pressed" from="GameSessionMenu" to="OptionsMenu" method="show"]
+[connection signal="save_button_pressed" from="GameSessionMenu" to="SaveLoadMenu" method="show_for_save"]
+[connection signal="back_button_pressed" from="OptionsMenu" to="MapView" method="enable_processing"]
+[connection signal="back_button_pressed" from="OptionsMenu" to="OptionsMenu" method="hide"]
diff --git a/game/src/Game/GameSession/GameSessionMenu.gd b/game/src/Game/GameSession/GameSessionMenu.gd
new file mode 100644
index 0000000..23ef2ef
--- /dev/null
+++ b/game/src/Game/GameSession/GameSessionMenu.gd
@@ -0,0 +1,80 @@
+extends PanelContainer
+
+@export var _main_menu_scene : PackedScene
+
+@export var _main_menu_dialog : AcceptDialog
+@export var _quit_dialog : AcceptDialog
+
+var _main_menu_save_button : Button
+var _main_menu_save_separator : Control
+var _quit_save_button : Button
+var _quit_save_separator : Control
+
+signal save_button_pressed
+signal load_button_pressed
+signal options_button_pressed
+
+func _ready() -> void:
+ _main_menu_save_button = _main_menu_dialog.add_button("DIALOG_SAVE_AND_RESIGN", true, &"save_and_main_menu")
+ _quit_save_button = _quit_dialog.add_button("DIALOG_SAVE_AND_QUIT", true, &"save_and_quit")
+
+ # Neccessary to center the save buttons and preserve the reference to the separator elements
+ var dialog_hbox : HBoxContainer = _main_menu_dialog.get_child(2, true)
+ var index := _main_menu_save_button.get_index(true)
+ dialog_hbox.move_child(_main_menu_save_button, _main_menu_dialog.get_ok_button().get_index(true))
+ dialog_hbox.move_child(_main_menu_dialog.get_ok_button(), index)
+ _main_menu_save_separator = dialog_hbox.get_child(_main_menu_save_button.get_index(true) - 1)
+
+ dialog_hbox = _quit_dialog.get_child(2, true)
+ index = _quit_save_button.get_index(true)
+ dialog_hbox.move_child(_quit_save_button, _quit_dialog.get_ok_button().get_index(true))
+ dialog_hbox.move_child(_quit_dialog.get_ok_button(), index)
+ _quit_save_separator = dialog_hbox.get_child(_quit_save_button.get_index(true) - 1)
+
+func hide_save_dialog_button() -> void:
+ _main_menu_save_button.hide()
+ _main_menu_save_separator.hide()
+ _quit_save_button.hide()
+ _quit_save_separator.hide()
+
+func show_save_dialog_button() -> void:
+ _main_menu_save_button.show()
+ _main_menu_save_separator.show()
+ _quit_save_button.show()
+ _quit_save_separator.show()
+
+# REQUIREMENTS:
+# * SS-47
+# * UIFUN-69
+func _on_main_menu_confirmed() -> void:
+ SaveManager.current_session_tag = ""
+ SaveManager.current_save = null
+ get_tree().change_scene_to_packed(_main_menu_scene)
+
+# REQUIREMENTS:
+# * SS-48
+# * UIFUN-70
+func _on_quit_confirmed() -> void:
+ get_tree().quit()
+
+# REQUIREMENTS:
+# * SS-7, SS-46
+# * UIFUN-11
+func _on_options_button_pressed() -> void:
+ options_button_pressed.emit()
+
+func _on_main_menu_dialog_custom_action(action) -> void:
+ match action:
+ &"save_and_main_menu":
+ _on_main_menu_confirmed()
+
+func _on_quit_dialog_custom_action(action : StringName) -> void:
+ match action:
+ &"save_and_quit":
+ _on_quit_confirmed()
+
+func _on_save_button_pressed():
+ save_button_pressed.emit()
+
+func _on_load_button_pressed():
+ load_button_pressed.emit()
diff --git a/game/src/Game/GameSession/GameSessionMenu.tscn b/game/src/Game/GameSession/GameSessionMenu.tscn
new file mode 100644
index 0000000..025ef3b
--- /dev/null
+++ b/game/src/Game/GameSession/GameSessionMenu.tscn
@@ -0,0 +1,91 @@
+[gd_scene load_steps=4 format=3 uid="uid://dvdynl6eir40o"]
+
+[ext_resource type="Theme" uid="uid://cqrfmjt5yeti7" path="res://theme/game_session_menu.tres" id="1_2onog"]
+[ext_resource type="Script" path="res://src/GameSession/GameSessionMenu.gd" id="1_usq6o"]
+[ext_resource type="PackedScene" uid="uid://o4u142w4qkln" path="res://src/GameMenu.tscn" id="2_xi6a4"]
+
+[node name="GameSessionMenu" type="PanelContainer" node_paths=PackedStringArray("_main_menu_dialog", "_quit_dialog")]
+process_mode = 3
+editor_description = "UI-68"
+theme = ExtResource("1_2onog")
+theme_type_variation = &"SessionPanel"
+script = ExtResource("1_usq6o")
+_main_menu_scene = ExtResource("2_xi6a4")
+_main_menu_dialog = NodePath("MainMenuDialog")
+_quit_dialog = NodePath("QuitDialog")
+
+[node name="ButtonListMargin" type="MarginContainer" parent="."]
+layout_mode = 2
+theme_override_constants/margin_left = 10
+theme_override_constants/margin_top = 10
+theme_override_constants/margin_right = 10
+theme_override_constants/margin_bottom = 10
+
+[node name="ButtonList" type="VBoxContainer" parent="ButtonListMargin"]
+layout_mode = 2
+
+[node name="SaveButton" type="Button" parent="ButtonListMargin/ButtonList"]
+editor_description = "UI-69"
+layout_mode = 2
+theme_type_variation = &"SessionButton"
+text = "GAMESESSIONMENU_SAVE"
+
+[node name="LoadButton" type="Button" parent="ButtonListMargin/ButtonList"]
+editor_description = "UI-70"
+layout_mode = 2
+theme_type_variation = &"SessionButton"
+text = "GAMESESSIONMENU_LOAD"
+
+[node name="OptionsButton" type="Button" parent="ButtonListMargin/ButtonList"]
+editor_description = "UI-10"
+layout_mode = 2
+theme_type_variation = &"SessionButton"
+text = "GAMESESSIONMENU_OPTIONS"
+
+[node name="MainMenuButton" type="Button" parent="ButtonListMargin/ButtonList"]
+editor_description = "UI-71"
+layout_mode = 2
+theme_type_variation = &"SessionButton"
+text = "GAMESESSIONMENU_MAINMENU"
+
+[node name="QuitButton" type="Button" parent="ButtonListMargin/ButtonList"]
+editor_description = "UI-72"
+layout_mode = 2
+theme_type_variation = &"SessionButton"
+text = "GAMESESSIONMENU_QUIT"
+
+[node name="CloseSeparator" type="HSeparator" parent="ButtonListMargin/ButtonList"]
+layout_mode = 2
+theme_type_variation = &"SessionSeparator"
+
+[node name="CloseButton" type="Button" parent="ButtonListMargin/ButtonList"]
+editor_description = "SS-64, UI-80, UIFUN-79"
+layout_mode = 2
+theme_type_variation = &"SessionButton"
+text = "GAMESESSIONMENU_CLOSE"
+
+[node name="MainMenuDialog" type="ConfirmationDialog" parent="."]
+disable_3d = true
+title = "GAMESESSIONMENU_MAINMENU_DIALOG_TITLE"
+size = Vector2i(384, 100)
+ok_button_text = "DIALOG_OK"
+dialog_text = "GAMESESSIONMENU_MAINMENU_DIALOG_TEXT"
+cancel_button_text = "DIALOG_CANCEL"
+
+[node name="QuitDialog" type="ConfirmationDialog" parent="."]
+disable_3d = true
+title = "GAMESESSIONMENU_QUIT_DIALOG_TITLE"
+ok_button_text = "DIALOG_OK"
+dialog_text = "GAMESESSIONMENU_QUIT_DIALOG_TEXT"
+cancel_button_text = "DIALOG_CANCEL"
+
+[connection signal="pressed" from="ButtonListMargin/ButtonList/SaveButton" to="." method="_on_save_button_pressed"]
+[connection signal="pressed" from="ButtonListMargin/ButtonList/LoadButton" to="." method="_on_load_button_pressed"]
+[connection signal="pressed" from="ButtonListMargin/ButtonList/OptionsButton" to="." method="_on_options_button_pressed"]
+[connection signal="pressed" from="ButtonListMargin/ButtonList/MainMenuButton" to="MainMenuDialog" method="popup_centered"]
+[connection signal="pressed" from="ButtonListMargin/ButtonList/QuitButton" to="QuitDialog" method="popup_centered"]
+[connection signal="pressed" from="ButtonListMargin/ButtonList/CloseButton" to="." method="hide"]
+[connection signal="confirmed" from="MainMenuDialog" to="." method="_on_main_menu_confirmed"]
+[connection signal="custom_action" from="MainMenuDialog" to="." method="_on_main_menu_dialog_custom_action"]
+[connection signal="confirmed" from="QuitDialog" to="." method="_on_quit_confirmed"]
+[connection signal="custom_action" from="QuitDialog" to="." method="_on_quit_dialog_custom_action"]
diff --git a/game/src/Game/GameSession/GameSpeedPanel.gd b/game/src/Game/GameSession/GameSpeedPanel.gd
new file mode 100644
index 0000000..80708b1
--- /dev/null
+++ b/game/src/Game/GameSession/GameSpeedPanel.gd
@@ -0,0 +1,37 @@
+extends PanelContainer
+
+#UI-74 UI-75 UI-76 UI-77
+
+@export var _longform_date_button : Button
+@export var _play_pause_display_button : Button
+@export var _decrease_speed_button : Button
+@export var _increase_speed_button : Button
+
+func _ready():
+ GameSingleton.state_updated.connect(_update_buttons)
+ _update_buttons()
+
+func _update_buttons():
+ _play_pause_display_button.text = "⏸ " if GameSingleton.is_paused() else "▶"
+
+ _increase_speed_button.disabled = not GameSingleton.can_increase_speed()
+ _decrease_speed_button.disabled = not GameSingleton.can_decrease_speed()
+
+ _longform_date_button.text = GameSingleton.get_longform_date()
+
+
+func _on_decrease_speed_button_pressed():
+ GameSingleton.decrease_speed()
+ _update_buttons()
+
+func _on_increase_speed_button_pressed():
+ GameSingleton.increase_speed()
+ _update_buttons()
+
+func _on_play_pause_display_button_pressed():
+ GameSingleton.toggle_paused()
+ _update_buttons()
+
+func _on_longform_date_label_pressed():
+ GameSingleton.toggle_paused()
+ _update_buttons()
diff --git a/game/src/Game/GameSession/GameSpeedPanel.tscn b/game/src/Game/GameSession/GameSpeedPanel.tscn
new file mode 100644
index 0000000..8a37565
--- /dev/null
+++ b/game/src/Game/GameSession/GameSpeedPanel.tscn
@@ -0,0 +1,38 @@
+[gd_scene load_steps=2 format=3 uid="uid://dd8k3p7r3huwc"]
+
+[ext_resource type="Script" path="res://src/GameSession/GameSpeedPanel.gd" id="1_pfs8t"]
+
+[node name="GameSpeedPanel" type="PanelContainer" node_paths=PackedStringArray("_longform_date_button", "_play_pause_display_button", "_decrease_speed_button", "_increase_speed_button")]
+script = ExtResource("1_pfs8t")
+_longform_date_button = NodePath("ButtonList/LongformDateButton")
+_play_pause_display_button = NodePath("ButtonList/PlayPauseDisplayButton")
+_decrease_speed_button = NodePath("ButtonList/DecreaseSpeedButton")
+_increase_speed_button = NodePath("ButtonList/IncreaseSpeedButton")
+
+[node name="ButtonList" type="HBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="LongformDateButton" type="Button" parent="ButtonList"]
+custom_minimum_size = Vector2(200, 0)
+layout_mode = 2
+text = "LONGFORM DATE"
+
+[node name="PlayPauseDisplayButton" type="Button" parent="ButtonList"]
+custom_minimum_size = Vector2(30, 0)
+layout_mode = 2
+text = "⏸ "
+
+[node name="DecreaseSpeedButton" type="Button" parent="ButtonList"]
+custom_minimum_size = Vector2(30, 0)
+layout_mode = 2
+text = "-"
+
+[node name="IncreaseSpeedButton" type="Button" parent="ButtonList"]
+custom_minimum_size = Vector2(30, 0)
+layout_mode = 2
+text = "+"
+
+[connection signal="pressed" from="ButtonList/LongformDateButton" to="." method="_on_longform_date_label_pressed"]
+[connection signal="pressed" from="ButtonList/PlayPauseDisplayButton" to="." method="_on_play_pause_display_button_pressed"]
+[connection signal="pressed" from="ButtonList/DecreaseSpeedButton" to="." method="_on_decrease_speed_button_pressed"]
+[connection signal="pressed" from="ButtonList/IncreaseSpeedButton" to="." method="_on_increase_speed_button_pressed"]
diff --git a/game/src/Game/GameSession/MapControlPanel/MapControlPanel.gd b/game/src/Game/GameSession/MapControlPanel/MapControlPanel.gd
new file mode 100644
index 0000000..0cef057
--- /dev/null
+++ b/game/src/Game/GameSession/MapControlPanel/MapControlPanel.gd
@@ -0,0 +1,57 @@
+extends PanelContainer
+
+signal game_session_menu_button_pressed
+signal map_view_camera_changed(near_left : Vector2, far_left : Vector2, far_right : Vector2, near_right : Vector2)
+signal minimap_clicked(pos_clicked : Vector2)
+signal zoom_in_button_pressed
+signal zoom_out_button_pressed
+
+@export var _mapmodes_grid : GridContainer
+
+var _mapmode_button_group : ButtonGroup
+
+# REQUIREMENTS:
+# * UI-550, UI-552, UI-554, UI-561
+func _add_mapmode_button(identifier : String) -> void:
+ var button := Button.new()
+ button.text = identifier
+ button.tooltip_text = identifier
+ button.toggle_mode = true
+ button.button_group = _mapmode_button_group
+ button.mouse_filter = MOUSE_FILTER_PASS
+ _mapmodes_grid.add_child(button)
+ if _mapmode_button_group.get_pressed_button() == null:
+ button.button_pressed = true
+
+func _ready():
+ _mapmode_button_group = ButtonGroup.new()
+ _mapmode_button_group.pressed.connect(_mapmode_pressed)
+ for index in GameSingleton.get_mapmode_count():
+ _add_mapmode_button(GameSingleton.get_mapmode_identifier(index))
+
+# REQUIREMENTS:
+# * UIFUN-10
+func _on_game_session_menu_button_pressed() -> void:
+ game_session_menu_button_pressed.emit()
+
+# REQUIREMENTS:
+# * SS-76
+# * UIFUN-129, UIFUN-131, UIFUN-133
+func _mapmode_pressed(button : BaseButton) -> void:
+ GameSingleton.set_mapmode(button.tooltip_text)
+
+func _on_map_view_camera_changed(near_left : Vector2, far_left : Vector2, far_right : Vector2, near_right : Vector2) -> void:
+ map_view_camera_changed.emit(near_left, far_left, far_right, near_right)
+
+func _on_minimap_clicked(pos_clicked : Vector2) -> void:
+ minimap_clicked.emit(pos_clicked)
+
+# REQUIREMENTS:
+# * UIFUN-269
+func _on_zoom_in_button_pressed() -> void:
+ zoom_in_button_pressed.emit()
+
+# REQUIREMENTS:
+# * UIFUN-270
+func _on_zoom_out_button_pressed() -> void:
+ zoom_out_button_pressed.emit()
diff --git a/game/src/Game/GameSession/MapControlPanel/MapControlPanel.tscn b/game/src/Game/GameSession/MapControlPanel/MapControlPanel.tscn
new file mode 100644
index 0000000..bd4a010
--- /dev/null
+++ b/game/src/Game/GameSession/MapControlPanel/MapControlPanel.tscn
@@ -0,0 +1,107 @@
+[gd_scene load_steps=7 format=3 uid="uid://g524p8lr574w"]
+
+[ext_resource type="Script" path="res://src/GameSession/MapControlPanel/MapControlPanel.gd" id="1_ign64"]
+[ext_resource type="Shader" path="res://src/GameSession/MapControlPanel/Minimap.gdshader" id="2_rinsg"]
+[ext_resource type="Script" path="res://src/GameSession/MapControlPanel/Minimap.gd" id="3_s4dml"]
+
+[sub_resource type="ShaderMaterial" id="ShaderMaterial_bhuqb"]
+shader = ExtResource("2_rinsg")
+shader_parameter/province_shape_subdivisions = null
+shader_parameter/selected_index = null
+
+[sub_resource type="InputEventAction" id="InputEventAction_5nck3"]
+action = &"ui_cancel"
+
+[sub_resource type="Shortcut" id="Shortcut_fc1tk"]
+events = [SubResource("InputEventAction_5nck3")]
+
+[node name="MapControlPanel" type="PanelContainer" node_paths=PackedStringArray("_mapmodes_grid")]
+editor_description = "SS-103, UI-548"
+mouse_filter = 1
+script = ExtResource("1_ign64")
+_mapmodes_grid = NodePath("MapPanelMargin/MapPanelList/MapDisplayList/MapmodesGrid")
+
+[node name="MapPanelMargin" type="MarginContainer" parent="."]
+layout_mode = 2
+theme_override_constants/margin_left = 5
+theme_override_constants/margin_top = 5
+theme_override_constants/margin_right = 5
+theme_override_constants/margin_bottom = 5
+
+[node name="MapPanelList" type="HBoxContainer" parent="MapPanelMargin"]
+layout_mode = 2
+theme_override_constants/separation = 6
+alignment = 1
+
+[node name="MapDisplayList" type="VBoxContainer" parent="MapPanelMargin/MapPanelList"]
+layout_mode = 2
+alignment = 1
+
+[node name="MapmodesGrid" type="GridContainer" parent="MapPanelMargin/MapPanelList/MapDisplayList"]
+editor_description = "UI-750"
+layout_mode = 2
+columns = 11
+
+[node name="Minimap" type="PanelContainer" parent="MapPanelMargin/MapPanelList/MapDisplayList"]
+editor_description = "UI-549"
+layout_mode = 2
+size_flags_horizontal = 4
+size_flags_vertical = 4
+mouse_filter = 1
+
+[node name="MinimapTexture" type="ColorRect" parent="MapPanelMargin/MapPanelList/MapDisplayList/Minimap"]
+editor_description = "UI-751, FS-338"
+material = SubResource("ShaderMaterial_bhuqb")
+layout_mode = 2
+color = Color(0.921569, 0.835294, 0.701961, 1)
+
+[node name="ViewportQuad" type="Control" parent="MapPanelMargin/MapPanelList/MapDisplayList/Minimap" node_paths=PackedStringArray("_minimap_texture")]
+layout_mode = 2
+mouse_filter = 1
+script = ExtResource("3_s4dml")
+_minimap_texture = NodePath("../MinimapTexture")
+
+[node name="AuxiliaryPanel" type="VBoxContainer" parent="MapPanelMargin/MapPanelList"]
+editor_description = "UI-761"
+layout_mode = 2
+
+[node name="GameSessionMenuButton" type="Button" parent="MapPanelMargin/MapPanelList/AuxiliaryPanel"]
+editor_description = "UI-9"
+layout_mode = 2
+mouse_filter = 1
+shortcut = SubResource("Shortcut_fc1tk")
+text = "ESC"
+
+[node name="LedgerButton" type="Button" parent="MapPanelMargin/MapPanelList/AuxiliaryPanel"]
+editor_description = "UI-860"
+layout_mode = 2
+mouse_filter = 1
+text = "L"
+
+[node name="FindButton" type="Button" parent="MapPanelMargin/MapPanelList/AuxiliaryPanel"]
+editor_description = "UI-861"
+layout_mode = 2
+mouse_filter = 1
+text = "F"
+
+[node name="ZoomButtonsContainer" type="HBoxContainer" parent="MapPanelMargin/MapPanelList/AuxiliaryPanel"]
+layout_mode = 2
+alignment = 1
+
+[node name="ZoomInButton" type="Button" parent="MapPanelMargin/MapPanelList/AuxiliaryPanel/ZoomButtonsContainer"]
+editor_description = "UI-862"
+layout_mode = 2
+mouse_filter = 1
+text = "+"
+
+[node name="ZoomOutButton" type="Button" parent="MapPanelMargin/MapPanelList/AuxiliaryPanel/ZoomButtonsContainer"]
+editor_description = "UI-863"
+layout_mode = 2
+mouse_filter = 1
+text = "-"
+
+[connection signal="map_view_camera_changed" from="." to="MapPanelMargin/MapPanelList/MapDisplayList/Minimap/ViewportQuad" method="_on_map_view_camera_changed"]
+[connection signal="minimap_clicked" from="MapPanelMargin/MapPanelList/MapDisplayList/Minimap/ViewportQuad" to="." method="_on_minimap_clicked"]
+[connection signal="pressed" from="MapPanelMargin/MapPanelList/AuxiliaryPanel/GameSessionMenuButton" to="." method="_on_game_session_menu_button_pressed"]
+[connection signal="pressed" from="MapPanelMargin/MapPanelList/AuxiliaryPanel/ZoomButtonsContainer/ZoomInButton" to="." method="_on_zoom_in_button_pressed"]
+[connection signal="pressed" from="MapPanelMargin/MapPanelList/AuxiliaryPanel/ZoomButtonsContainer/ZoomOutButton" to="." method="_on_zoom_out_button_pressed"]
diff --git a/game/src/Game/GameSession/MapControlPanel/Minimap.gd b/game/src/Game/GameSession/MapControlPanel/Minimap.gd
new file mode 100644
index 0000000..1f9b75e
--- /dev/null
+++ b/game/src/Game/GameSession/MapControlPanel/Minimap.gd
@@ -0,0 +1,105 @@
+extends Control
+
+signal minimap_clicked(pos_clicked : Vector2)
+
+const _action_click : StringName = &"map_click"
+
+@export var _minimap_texture : Control
+var _minimap_shader : ShaderMaterial
+
+var _viewport_points : PackedVector2Array
+
+func _ready():
+ _minimap_texture.custom_minimum_size = Vector2(GameSingleton.get_aspect_ratio(), 1.0) * 150
+ var minimap_material := _minimap_texture.get_material()
+ if Events.ShaderManager.set_up_shader(minimap_material, false) != OK:
+ push_error("Failed to set up minimap shader")
+ else:
+ _minimap_shader = minimap_material
+ GameSingleton.province_selected.connect(_on_province_selected)
+
+func _on_province_selected(index : int) -> void:
+ if _minimap_shader != null:
+ _minimap_shader.set_shader_parameter(Events.ShaderManager.param_selected_index, index)
+
+# REQUIREMENTS
+# * SS-80
+# * UI-752
+func _draw() -> void:
+ if _viewport_points.size() > 1:
+ draw_multiline(_viewport_points, Color.WHITE, -1)
+
+# REQUIREMENTS
+# * SS-81
+# * UIFUN-127
+func _unhandled_input(event : InputEvent):
+ if event is InputEventMouse and Input.is_action_pressed(_action_click):
+ var pos_clicked := get_local_mouse_position() / size - Vector2(0.5, 0.5)
+ if abs(pos_clicked.x) < 0.5 and abs(pos_clicked.y) < 0.5:
+ minimap_clicked.emit(pos_clicked)
+
+# Returns the point on the line going through p and q with the specific x coord
+func _intersect_x(p : Vector2, q : Vector2, x : float) -> Vector2:
+ if p.x == q.x:
+ return Vector2(x, 0.5 * (p.y + q.y))
+ var t := (x - q.x) / (p.x - q.x)
+ return q + t * (p - q)
+
+# Returns the point on the line going through p and q with the specific y coord
+func _intersect_y(p : Vector2, q : Vector2, y : float) -> Vector2:
+ if p.y == q.y:
+ return Vector2(0.5 * (p.x + q.x), y)
+ var t := (y - q.y) / (p.y - q.y)
+ return q + t * (p - q)
+
+const _one_x := Vector2(1, 0)
+
+func _add_line_looped_over_x(left : Vector2, right : Vector2) -> void:
+ if left.x < 0:
+ if right.x < 0:
+ _viewport_points.push_back(left + _one_x)
+ _viewport_points.push_back(right + _one_x)
+ else:
+ var mid_point := _intersect_x(left, right, 0)
+ _viewport_points.push_back(mid_point)
+ _viewport_points.push_back(right)
+ mid_point.x = 1
+ _viewport_points.push_back(left + _one_x)
+ _viewport_points.push_back(mid_point)
+ elif right.x > 1:
+ if left.x > 1:
+ _viewport_points.push_back(left - _one_x)
+ _viewport_points.push_back(right - _one_x)
+ else:
+ var mid_point := _intersect_x(left, right, 1)
+ _viewport_points.push_back(left)
+ _viewport_points.push_back(mid_point)
+ mid_point.x = 0
+ _viewport_points.push_back(mid_point)
+ _viewport_points.push_back(right - _one_x)
+ else:
+ _viewport_points.push_back(left)
+ _viewport_points.push_back(right)
+
+# This can break if the viewport is rotated too far!
+func _on_map_view_camera_changed(near_left : Vector2, far_left : Vector2, far_right : Vector2, near_right : Vector2) -> void:
+ # Bound far y coords
+ if far_left.y < 0:
+ far_left = _intersect_y(near_left, far_left, 0)
+ if far_right.y < 0:
+ far_right = _intersect_y(near_right, far_right, 0)
+ # Bound near y coords
+ if near_left.y > 1:
+ near_left = _intersect_y(near_left, far_left, 1)
+ if near_right.y > 1:
+ near_right = _intersect_y(near_right, far_right, 1)
+
+ _viewport_points.clear()
+ _add_line_looped_over_x(near_left, near_right)
+ _add_line_looped_over_x(far_left, far_right)
+ _add_line_looped_over_x(far_left, near_left)
+ _add_line_looped_over_x(near_right, far_right)
+
+ for i in _viewport_points.size():
+ _viewport_points[i] *= size
+ queue_redraw()
diff --git a/game/src/Game/GameSession/MapControlPanel/Minimap.gdshader b/game/src/Game/GameSession/MapControlPanel/Minimap.gdshader
new file mode 100644
index 0000000..608abe2
--- /dev/null
+++ b/game/src/Game/GameSession/MapControlPanel/Minimap.gdshader
@@ -0,0 +1,18 @@
+shader_type canvas_item;
+
+#include "res://src/GameSession/ProvinceIndexSampler.gdshaderinc"
+
+// Index of the currently selected province
+uniform uint selected_index;
+
+const vec3 land_colour = vec3(0.5);
+const vec3 selected_colour = vec3(1.0, 1.0, 0.0);
+
+void fragment() {
+ uvec3 data = read_uvec3(UV);
+ uint index = uvec2_to_uint(data.rg);
+ float is_land = float(data.b != 0u);
+ float is_selected = float(index == selected_index);
+ COLOR.rgb = mix(COLOR.rgb, land_colour, is_land);
+ COLOR.rgb = mix(COLOR.rgb, selected_colour, is_selected);
+}
diff --git a/game/src/Game/GameSession/MapView.gd b/game/src/Game/GameSession/MapView.gd
new file mode 100644
index 0000000..ced8bb7
--- /dev/null
+++ b/game/src/Game/GameSession/MapView.gd
@@ -0,0 +1,242 @@
+extends Node3D
+
+signal map_view_camera_changed(near_left : Vector2, far_left : Vector2, far_right : Vector2, near_right : Vector2)
+
+const _action_north : StringName = &"map_north"
+const _action_east : StringName = &"map_east"
+const _action_south : StringName = &"map_south"
+const _action_west : StringName = &"map_west"
+const _action_zoom_in : StringName = &"map_zoom_in"
+const _action_zoom_out : StringName = &"map_zoom_out"
+const _action_drag : StringName = &"map_drag"
+const _action_click : StringName = &"map_click"
+
+@export var _camera : Camera3D
+
+@export var _cardinal_move_speed : float = 1.0
+@export var _edge_move_threshold: float = 0.01
+@export var _edge_move_speed: float = 2.5
+var _drag_anchor : Vector2
+var _drag_active : bool = false
+
+var _mouse_over_viewport : bool = true
+
+@export var _zoom_target_min : float = 0.05
+@export var _zoom_target_max : float = 5.0
+@export var _zoom_target_step : float = 0.1
+@export var _zoom_epsilon : float = _zoom_target_step * 0.1
+@export var _zoom_speed : float = 5.0
+@export var _zoom_target : float = 1.0:
+ get: return _zoom_target
+ set(v): _zoom_target = clamp(v, _zoom_target_min, _zoom_target_max)
+
+@export var _map_mesh_instance : MeshInstance3D
+var _map_mesh : MapMesh
+var _map_shader_material : ShaderMaterial
+var _map_mesh_corner : Vector2
+var _map_mesh_dims : Vector2
+
+var _mouse_pos_viewport : Vector2 = Vector2(0.5, 0.5)
+var _mouse_pos_map : Vector2 = Vector2(0.5, 0.5)
+var _viewport_dims : Vector2 = Vector2(1, 1)
+
+# ??? 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
+# is referenced in the script below, then the editor will crash due
+# to a failed HashMap lookup. I'm not sure if this is a bug in the
+# editor, GDExtension, my own extension, or a combination of them.
+# This was an absolute pain to track down. --- hop311
+func _ready():
+ if _camera == null:
+ push_error("MapView's _camera variable hasn't been set!")
+ return
+ _zoom_target = _camera.position.y
+ if _map_mesh_instance == null:
+ push_error("MapView's _map_mesh variable hasn't been set!")
+ return
+
+ # Shader Material
+ var map_material := _map_mesh_instance.get_active_material(0)
+ if Events.ShaderManager.set_up_shader(map_material, true) != OK:
+ push_error("Failed to set up map shader")
+ return
+ _map_shader_material = map_material
+
+ if not _map_mesh_instance.mesh is MapMesh:
+ push_error("Invalid map mesh class: ", _map_mesh_instance.mesh.get_class(), "(expected MapMesh)")
+ return
+ _map_mesh = _map_mesh_instance.mesh
+
+ # Set map mesh size and get bounds
+ const pixels_per_terrain_tile : float = 32.0
+ _map_shader_material.set_shader_parameter(Events.ShaderManager.param_terrain_tile_factor,
+ float(GameSingleton.get_height()) / pixels_per_terrain_tile)
+ var map_mesh_aabb := _map_mesh.get_core_aabb() * _map_mesh_instance.transform
+ _map_mesh_corner = Vector2(
+ min(map_mesh_aabb.position.x, map_mesh_aabb.end.x),
+ min(map_mesh_aabb.position.z, map_mesh_aabb.end.z)
+ )
+ _map_mesh_dims = abs(Vector2(
+ map_mesh_aabb.position.x - map_mesh_aabb.end.x,
+ map_mesh_aabb.position.z - map_mesh_aabb.end.z
+ ))
+
+ GameSingleton.province_selected.connect(_on_province_selected)
+
+func _notification(what : int):
+ match what:
+ NOTIFICATION_WM_MOUSE_ENTER: # Mouse inside window
+ _on_mouse_entered_viewport()
+ NOTIFICATION_WM_MOUSE_EXIT: # Mouse out of window
+ _on_mouse_exited_viewport()
+
+func _world_to_map_coords(pos : Vector3) -> Vector2:
+ return (Vector2(pos.x, pos.z) - _map_mesh_corner) / _map_mesh_dims
+
+func _viewport_to_map_coords(pos_viewport : Vector2) -> Vector2:
+ var ray_origin := _camera.project_ray_origin(pos_viewport)
+ var ray_normal := _camera.project_ray_normal(pos_viewport)
+ # Plane with normal (0,1,0) facing upwards, at a distance 0 from the origin
+ var intersection = Plane(0, 1, 0, 0).intersects_ray(ray_origin, ray_normal)
+ if typeof(intersection) == TYPE_VECTOR3:
+ return _world_to_map_coords(intersection as Vector3)
+ else:
+ # Normals parallel to the xz-plane could cause null intersections,
+ # but the camera's orientation should prevent such normals
+ push_error("Invalid intersection: ", intersection)
+ return Vector2(0.5, 0.5)
+
+func zoom_in() -> void:
+ _zoom_target -= _zoom_target_step
+
+func zoom_out() -> void:
+ _zoom_target += _zoom_target_step
+
+func _on_province_selected(index : int) -> void:
+ _map_shader_material.set_shader_parameter(Events.ShaderManager.param_selected_index, index)
+
+# REQUIREMENTS
+# * SS-31
+func _unhandled_input(event : InputEvent):
+ if _mouse_over_viewport and event.is_action_pressed(_action_click):
+ # 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))
+ else:
+ print("Clicked outside the map!")
+ elif event.is_action_pressed(_action_drag):
+ if _drag_active:
+ push_warning("Drag being activated while already active!")
+ _drag_active = true
+ _drag_anchor = _mouse_pos_map
+ elif event.is_action_released(_action_drag):
+ if not _drag_active:
+ push_warning("Drag being deactivated while already not active!")
+ _drag_active = false
+ elif event.is_action_pressed(_action_zoom_in, true):
+ zoom_in()
+ elif event.is_action_pressed(_action_zoom_out, true):
+ zoom_out()
+
+func _physics_process(delta : float):
+ _mouse_pos_viewport = get_viewport().get_mouse_position()
+ _viewport_dims = Vector2(Resolution.get_current_resolution())
+ # Process movement
+ _movement_process(delta)
+ # Keep within map bounds
+ _clamp_over_map()
+ # Process zooming
+ _zoom_process(delta)
+ # Orient based on height
+ _update_orientation()
+ # Update viewport on minimap
+ _update_minimap_viewport()
+ # Calculate where the mouse lies on the map
+ _update_mouse_map_position()
+
+# REQUIREMENTS
+# * UIFUN-124
+func _movement_process(delta : float) -> void:
+ var direction : Vector2
+ if _drag_active:
+ direction = (_drag_anchor - _mouse_pos_map) * _map_mesh_dims
+ else:
+ direction = _edge_scrolling_vector() + _cardinal_movement_vector()
+ # Scale movement speed with height
+ direction *= _camera.position.y * delta
+ _camera.position += Vector3(direction.x, 0, direction.y)
+
+# REQUIREMENTS
+# * UIFUN-125
+func _edge_scrolling_vector() -> Vector2:
+ if not _mouse_over_viewport:
+ return Vector2()
+ var mouse_vector := _mouse_pos_viewport * GuiScale.get_current_guiscale() / _viewport_dims - Vector2(0.5, 0.5)
+ if abs(mouse_vector.x) < 0.5 - _edge_move_threshold and abs(mouse_vector.y) < 0.5 - _edge_move_threshold:
+ mouse_vector *= 0
+ return mouse_vector * _edge_move_speed
+
+# REQUIREMENTS
+# * SS-75
+func _cardinal_movement_vector() -> Vector2:
+ var move := Vector2(
+ float(Input.is_action_pressed(_action_east)) - float(Input.is_action_pressed(_action_west)),
+ float(Input.is_action_pressed(_action_south)) - float(Input.is_action_pressed(_action_north))
+ )
+ return move * _cardinal_move_speed
+
+func _clamp_over_map() -> void:
+ _camera.position.x = _map_mesh_corner.x + fposmod(_camera.position.x - _map_mesh_corner.x, _map_mesh_dims.x)
+ _camera.position.z = clamp(_camera.position.z, _map_mesh_corner.y, _map_mesh_corner.y + _map_mesh_dims.y)
+
+# REQUIREMENTS
+# * SS-74
+# * UIFUN-123
+func _zoom_process(delta : float) -> void:
+ var height := _camera.position.y
+ var zoom := _zoom_target - height
+ height += zoom * _zoom_speed * delta
+ var new_zoom := _zoom_target - height
+ # Set to target if height is within _zoom_epsilon of it or has overshot past it
+ if abs(new_zoom) < _zoom_epsilon or sign(zoom) != sign(new_zoom):
+ height = _zoom_target
+ _camera.position.y = height
+
+func _update_orientation() -> void:
+ var dir := Vector3(0, -1, -exp(-_camera.position.y - 1))
+ _camera.look_at(_camera.position + dir)
+
+func _update_minimap_viewport() -> void:
+ var near_left := _viewport_to_map_coords(Vector2(0, _viewport_dims.y))
+ var far_left := _viewport_to_map_coords(Vector2(0, 0))
+ var far_right := _viewport_to_map_coords(Vector2(_viewport_dims.x, 0))
+ var near_right := _viewport_to_map_coords(_viewport_dims)
+ map_view_camera_changed.emit(near_left, far_left, far_right, near_right)
+
+func _update_mouse_map_position() -> void:
+ _mouse_pos_map = _viewport_to_map_coords(_mouse_pos_viewport)
+ var hover_index := GameSingleton.get_province_index_from_uv_coords(_mouse_pos_map)
+ if _mouse_over_viewport:
+ _map_shader_material.set_shader_parameter(Events.ShaderManager.param_hover_index, hover_index)
+
+func _on_mouse_entered_viewport():
+ _mouse_over_viewport = true
+
+func _on_mouse_exited_viewport():
+ _mouse_over_viewport = false
+ _map_shader_material.set_shader_parameter(Events.ShaderManager.param_hover_index, 0)
+
+func _on_minimap_clicked(pos_clicked : Vector2):
+ pos_clicked *= _map_mesh_dims
+ _camera.position.x = pos_clicked.x
+ _camera.position.z = pos_clicked.y
+ _clamp_over_map()
+
+func enable_processing() -> void:
+ set_process_unhandled_input(true)
+ set_physics_process(true)
+
+func disable_processing() -> void:
+ set_process_unhandled_input(false)
+ set_physics_process(false)
diff --git a/game/src/Game/GameSession/MapView.tscn b/game/src/Game/GameSession/MapView.tscn
new file mode 100644
index 0000000..fa6ffcd
--- /dev/null
+++ b/game/src/Game/GameSession/MapView.tscn
@@ -0,0 +1,30 @@
+[gd_scene load_steps=5 format=3 uid="uid://dkehmdnuxih2r"]
+
+[ext_resource type="Script" path="res://src/GameSession/MapView.gd" id="1_exccw"]
+[ext_resource type="Shader" path="res://src/GameSession/TerrainMap.gdshader" id="1_upocn"]
+
+[sub_resource type="ShaderMaterial" id="ShaderMaterial_tayeg"]
+render_priority = 0
+shader = ExtResource("1_upocn")
+shader_parameter/province_shape_subdivisions = null
+shader_parameter/hover_index = null
+shader_parameter/selected_index = null
+shader_parameter/terrain_tile_factor = null
+
+[sub_resource type="MapMesh" id="MapMesh_3gtsd"]
+
+[node name="MapView" type="Node3D" node_paths=PackedStringArray("_camera", "_map_mesh_instance")]
+editor_description = "SS-73"
+script = ExtResource("1_exccw")
+_camera = NodePath("MapCamera")
+_map_mesh_instance = NodePath("MapMeshInstance")
+
+[node name="MapCamera" type="Camera3D" parent="."]
+transform = Transform3D(1, 0, 0, 0, 0.707107, 0.707107, 0, -0.707107, 0.707107, 0.25, 1.5, -2.75)
+near = 0.01
+
+[node name="MapMeshInstance" type="MeshInstance3D" parent="."]
+editor_description = "FS-343"
+transform = Transform3D(10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0)
+material_override = SubResource("ShaderMaterial_tayeg")
+mesh = SubResource("MapMesh_3gtsd")
diff --git a/game/src/Game/GameSession/ProvinceIndexSampler.gdshaderinc b/game/src/Game/GameSession/ProvinceIndexSampler.gdshaderinc
new file mode 100644
index 0000000..65f73d8
--- /dev/null
+++ b/game/src/Game/GameSession/ProvinceIndexSampler.gdshaderinc
@@ -0,0 +1,18 @@
+
+// Province shape texture
+uniform sampler2DArray province_shape_tex : repeat_enable, filter_nearest;
+// Province shape subdivisions
+uniform vec2 province_shape_subdivisions;
+
+uvec3 vec3_to_uvec3(vec3 v) {
+ return uvec3(v * 255.0);
+}
+uvec3 read_uvec3(vec2 uv) {
+ uv *= province_shape_subdivisions;
+ vec2 subdivision_coords = mod(floor(uv), province_shape_subdivisions);
+ float idx = subdivision_coords.x + subdivision_coords.y * province_shape_subdivisions.x;
+ return vec3_to_uvec3(texture(province_shape_tex, vec3(uv, idx)).rgb);
+}
+uint uvec2_to_uint(uvec2 v) {
+ return (v.y << 8u) | v.x;
+}
diff --git a/game/src/Game/GameSession/ProvinceOverviewPanel/ProvinceOverviewPanel.gd b/game/src/Game/GameSession/ProvinceOverviewPanel/ProvinceOverviewPanel.gd
new file mode 100644
index 0000000..67060bf
--- /dev/null
+++ b/game/src/Game/GameSession/ProvinceOverviewPanel/ProvinceOverviewPanel.gd
@@ -0,0 +1,122 @@
+extends PanelContainer
+
+@export var _province_name_label : Label
+@export var _region_name_label : Label
+@export var _life_rating_bar : ProgressBar
+@export var _rgo_icon_texture_rect : TextureRect
+@export var _rgo_name_label : Label
+@export var _buildings_container : Container
+
+const _missing_suffix : String = "_MISSING"
+
+var _selected_index : int:
+ get: return _selected_index
+ set(v):
+ _selected_index = v
+ update_info()
+var _province_info : Dictionary
+
+func _ready():
+ GameSingleton.province_selected.connect(_on_province_selected)
+ GameSingleton.state_updated.connect(update_info)
+ update_info()
+
+enum { CANNOT_EXPAND, CAN_EXPAND, PREPARING, EXPANDING }
+
+func _expand_building(building_identifier : String) -> void:
+ if GameSingleton.expand_building(_selected_index, building_identifier) != OK:
+ push_error("Failed to expand ", building_identifier, " in province #", _selected_index);
+
+# Each building row contains:
+# level - Level Label
+# name - Name Label
+# button - Expansion Button
+# progress_bar - Expansion ProgressBar
+var _building_rows : Array[Dictionary]
+
+# REQUIREMENTS:
+# * UI-183, UI-185, UI-186, UI-765, UI-187, UI-188, UI-189
+# * UI-191, UI-193, UI-194, UI-766, UI-195, UI-196, UI-197
+# * UI-199, UI-201, UI-202, UI-767, UI-203, UI-204, UI-205
+func _add_building_row() -> void:
+ var row : Dictionary
+
+ row.level = Label.new()
+ row.level.text = "X"
+ _buildings_container.add_child(row.level)
+
+ row.name = Label.new()
+ row.name.text = GameSingleton.get_building_info_building_key() + _missing_suffix
+ _buildings_container.add_child(row.name)
+
+ row.button = Button.new()
+ row.button.text = "EXPAND_PROVINCE_BUILDING"
+ row.button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ row.button.mouse_filter = Control.MOUSE_FILTER_PASS
+ row.button.pressed.connect(func(): _expand_building(row.name.text))
+ _buildings_container.add_child(row.button)
+
+ row.progress_bar = ProgressBar.new()
+ row.progress_bar.max_value = 1
+ row.progress_bar.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ row.progress_bar.mouse_filter = Control.MOUSE_FILTER_PASS
+ _buildings_container.add_child(row.progress_bar)
+
+ _building_rows.append(row)
+ _set_building_row(_building_rows.size() - 1, {})
+
+func _set_building_row(index : int, building : Dictionary) -> void:
+ if index < 0 or index > _building_rows.size():
+ push_error("Invalid building row index: ", index, " (max ", _building_rows.size(), ")")
+ return
+ if index == _building_rows.size(): _add_building_row()
+ var row := _building_rows[index]
+ if building.is_empty():
+ row.level.visible = false
+ row.name.visible = false
+ row.progress_bar.visible = false
+ row.button.visible = false
+ return
+ row.level.text = str(building.get(GameSingleton.get_building_info_level_key(), 0))
+ row.level.visible = true
+ row.name.text = building.get(GameSingleton.get_building_info_building_key(),
+ GameSingleton.get_building_info_building_key() + _missing_suffix)
+ row.name.visible = true
+
+ var expansion_state : int = building.get(GameSingleton.get_building_info_expansion_state_key(),
+ CANNOT_EXPAND)
+ var show_progress_bar := expansion_state == PREPARING or expansion_state == EXPANDING
+ row.progress_bar.value = building.get(GameSingleton.get_building_info_expansion_progress_key(), 0)
+ row.progress_bar.visible = show_progress_bar
+ row.button.disabled = expansion_state != CAN_EXPAND
+ row.button.visible = not show_progress_bar
+
+func update_info() -> void:
+ _province_info = GameSingleton.get_province_info_from_index(_selected_index)
+ if _province_info:
+ _province_name_label.text = _province_info.get(GameSingleton.get_province_info_province_key(),
+ GameSingleton.get_province_info_province_key() + _missing_suffix)
+ _region_name_label.text = _province_info.get(GameSingleton.get_province_info_region_key(),
+ GameSingleton.get_province_info_region_key() + _missing_suffix)
+
+ _life_rating_bar.value = _province_info.get(GameSingleton.get_province_info_life_rating_key(), 0)
+ _life_rating_bar.tooltip_text = tr("LIFE_RATING_TOOLTIP").format({ "life_rating": _life_rating_bar.value })
+
+ _rgo_name_label.text = _province_info.get(GameSingleton.get_province_info_rgo_key(),
+ GameSingleton.get_province_info_rgo_key() + _missing_suffix)
+ _rgo_icon_texture_rect.texture = GameSingleton.get_good_icon_texture(_rgo_name_label.text)
+
+ var buildings : Array = _province_info.get(GameSingleton.get_province_info_buildings_key(), [])
+ for i in max(buildings.size(), _building_rows.size()):
+ _set_building_row(i, buildings[i] if i < buildings.size() else {})
+
+ show()
+ else:
+ hide()
+ mouse_exited.emit()
+
+func _on_province_selected(index : int) -> void:
+ _selected_index = index
+
+func _on_close_button_pressed() -> void:
+ GameSingleton.set_selected_province(0)
diff --git a/game/src/Game/GameSession/ProvinceOverviewPanel/ProvinceOverviewPanel.tscn b/game/src/Game/GameSession/ProvinceOverviewPanel/ProvinceOverviewPanel.tscn
new file mode 100644
index 0000000..a233db0
--- /dev/null
+++ b/game/src/Game/GameSession/ProvinceOverviewPanel/ProvinceOverviewPanel.tscn
@@ -0,0 +1,86 @@
+[gd_scene load_steps=2 format=3 uid="uid://byq323jbel48u"]
+
+[ext_resource type="Script" path="res://src/GameSession/ProvinceOverviewPanel/ProvinceOverviewPanel.gd" id="1_3n8k5"]
+
+[node name="ProvinceOverviewPanel" type="PanelContainer" node_paths=PackedStringArray("_province_name_label", "_region_name_label", "_life_rating_bar", "_rgo_icon_texture_rect", "_rgo_name_label", "_buildings_container")]
+editor_description = "UI-56"
+anchors_preset = 2
+anchor_top = 1.0
+anchor_bottom = 1.0
+offset_top = -300.0
+offset_right = 200.0
+grow_vertical = 0
+mouse_filter = 1
+script = ExtResource("1_3n8k5")
+_province_name_label = NodePath("PanelList/TopBarList/NameList/ProvinceName")
+_region_name_label = NodePath("PanelList/TopBarList/NameList/RegionName")
+_life_rating_bar = NodePath("PanelList/TopBarList/NameList/LifeRatingBar")
+_rgo_icon_texture_rect = NodePath("PanelList/InteractList/RGOInfo/RGOIcon")
+_rgo_name_label = NodePath("PanelList/InteractList/RGOInfo/RGOName")
+_buildings_container = NodePath("PanelList/InteractList/BuildingsContainer")
+
+[node name="PanelList" type="VBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="TopBarList" type="HBoxContainer" parent="PanelList"]
+layout_mode = 2
+
+[node name="NameList" type="VBoxContainer" parent="PanelList/TopBarList"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 0
+
+[node name="ProvinceName" type="Label" parent="PanelList/TopBarList/NameList"]
+editor_description = "UI-57"
+layout_mode = 2
+text = "province_MISSING"
+vertical_alignment = 1
+
+[node name="RegionName" type="Label" parent="PanelList/TopBarList/NameList"]
+editor_description = "UI-58"
+layout_mode = 2
+text = "region_MISSING"
+vertical_alignment = 1
+
+[node name="LifeRatingBar" type="ProgressBar" parent="PanelList/TopBarList/NameList"]
+editor_description = "UI-62"
+layout_mode = 2
+mouse_filter = 1
+
+[node name="CloseButton" type="Button" parent="PanelList/TopBarList"]
+custom_minimum_size = Vector2(30, 30)
+layout_mode = 2
+size_flags_vertical = 0
+mouse_filter = 1
+text = "X"
+
+[node name="InteractList" type="VBoxContainer" parent="PanelList"]
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="HSeparator" type="HSeparator" parent="PanelList/InteractList"]
+layout_mode = 2
+mouse_filter = 1
+
+[node name="RGOInfo" type="HBoxContainer" parent="PanelList/InteractList"]
+editor_description = "UI-112"
+layout_mode = 2
+
+[node name="RGOIcon" type="TextureRect" parent="PanelList/InteractList/RGOInfo"]
+editor_description = "UI-100"
+layout_mode = 2
+
+[node name="RGOName" type="Label" parent="PanelList/InteractList/RGOInfo"]
+layout_mode = 2
+text = "rgo_MISSING"
+vertical_alignment = 1
+
+[node name="HSeparator2" type="HSeparator" parent="PanelList/InteractList"]
+layout_mode = 2
+mouse_filter = 1
+
+[node name="BuildingsContainer" type="GridContainer" parent="PanelList/InteractList"]
+layout_mode = 2
+columns = 3
+
+[connection signal="pressed" from="PanelList/TopBarList/CloseButton" to="." method="_on_close_button_pressed"]
diff --git a/game/src/Game/GameSession/TerrainMap.gdshader b/game/src/Game/GameSession/TerrainMap.gdshader
new file mode 100644
index 0000000..88e7019
--- /dev/null
+++ b/game/src/Game/GameSession/TerrainMap.gdshader
@@ -0,0 +1,50 @@
+shader_type spatial;
+
+render_mode unshaded;
+
+#include "res://src/GameSession/ProvinceIndexSampler.gdshaderinc"
+
+// Province colour texture
+uniform sampler2D province_colour_tex: source_color, repeat_enable, filter_nearest;
+// Index of the mouse over the map mesh
+uniform uint hover_index;
+// Index of the currently selected province
+uniform uint selected_index;
+// Cosmetic terrain textures
+uniform sampler2DArray terrain_tex: source_color, repeat_enable, filter_linear;
+// The number of times the terrain textures should tile vertically
+uniform float terrain_tile_factor;
+
+const vec3 highlight_colour = vec3(1.0);
+
+vec3 get_terrain_colour(vec2 uv, vec2 corner, vec2 half_pixel_size, vec2 terrain_uv) {
+ uvec3 province_data = read_uvec3(fma(corner, half_pixel_size, uv));
+ vec4 province_colour = texelFetch(province_colour_tex, ivec2(province_data.rg), 0);
+ vec3 terrain_colour = texture(terrain_tex, vec3(terrain_uv, float(province_data.b))).rgb;
+ vec3 mixed_colour = mix(terrain_colour, province_colour.rgb, province_colour.a);
+ uint index = uvec2_to_uint(province_data.rg);
+ float mix_val = 0.1 * (float(index == hover_index) + float(index == selected_index));
+ return mix(mixed_colour, highlight_colour, mix_val);
+}
+
+vec3 mix_terrain_colour(vec2 uv) {
+ vec2 map_size = vec2(textureSize(province_shape_tex, 0).xy) * province_shape_subdivisions;
+ vec2 pixel_offset = fract(fma(uv, map_size, vec2(0.5)));
+ vec2 half_pixel_size = 0.49 / map_size;
+
+ vec2 terrain_uv = uv;
+ terrain_uv.x *= map_size.x / map_size.y;
+ terrain_uv *= terrain_tile_factor;
+
+ return mix(
+ mix(get_terrain_colour(uv, vec2(-1, -1), half_pixel_size, terrain_uv),
+ get_terrain_colour(uv, vec2(+1, -1), half_pixel_size, terrain_uv), pixel_offset.x),
+ mix(get_terrain_colour(uv, vec2(-1, +1), half_pixel_size, terrain_uv),
+ get_terrain_colour(uv, vec2(+1, +1), half_pixel_size, terrain_uv), pixel_offset.x),
+ pixel_offset.y);
+}
+
+void fragment() {
+ vec3 terrain_colour = mix_terrain_colour(UV);
+ ALBEDO = terrain_colour;
+}