aboutsummaryrefslogtreecommitdiff
path: root/game/src/GameSession
diff options
context:
space:
mode:
Diffstat (limited to 'game/src/GameSession')
-rw-r--r--game/src/GameSession/GameSession.gd14
-rw-r--r--game/src/GameSession/GameSession.tscn45
-rw-r--r--game/src/GameSession/GameSessionMenu.gd67
-rw-r--r--game/src/GameSession/GameSessionMenu.tscn81
-rw-r--r--game/src/GameSession/GameSpeedPanel.gd37
-rw-r--r--game/src/GameSession/GameSpeedPanel.tscn38
-rw-r--r--game/src/GameSession/MapControlPanel.gd53
-rw-r--r--game/src/GameSession/MapControlPanel.tscn100
-rw-r--r--game/src/GameSession/MapView.gd274
-rw-r--r--game/src/GameSession/MapView.tscn31
-rw-r--r--game/src/GameSession/Minimap.gd89
-rw-r--r--game/src/GameSession/ProvinceOverviewPanel.gd95
-rw-r--r--game/src/GameSession/ProvinceOverviewPanel.tscn63
-rw-r--r--game/src/GameSession/TerrainMap.gdshader66
14 files changed, 1013 insertions, 40 deletions
diff --git a/game/src/GameSession/GameSession.gd b/game/src/GameSession/GameSession.gd
index 0d69bf2..556b98e 100644
--- a/game/src/GameSession/GameSession.gd
+++ b/game/src/GameSession/GameSession.gd
@@ -3,14 +3,14 @@ extends Control
@export var _game_session_menu : Control
func _ready():
- print("GameSession ready")
+ Events.Options.load_settings_from_file()
+ if GameSingleton.setup() != OK:
+ push_error("Failed to setup game")
+
+func _process(delta : float):
+ GameSingleton.try_tick()
# REQUIREMENTS:
# * SS-42
-func _on_game_session_menu_button_pressed():
+func _on_game_session_menu_button_pressed() -> void:
_game_session_menu.visible = !_game_session_menu.visible
-
-# REQUIREMENTS:
-# * SS-64
-func _on_game_session_menu_close_button_pressed():
- _game_session_menu.hide()
diff --git a/game/src/GameSession/GameSession.tscn b/game/src/GameSession/GameSession.tscn
index f984daf..b993acf 100644
--- a/game/src/GameSession/GameSession.tscn
+++ b/game/src/GameSession/GameSession.tscn
@@ -1,20 +1,28 @@
-[gd_scene load_steps=4 format=3 uid="uid://bgnupcshe1m7r"]
+[gd_scene load_steps=9 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.tscn" id="3_afh6d"]
[ext_resource type="PackedScene" uid="uid://dvdynl6eir40o" path="res://src/GameSession/GameSessionMenu.tscn" id="3_bvmqh"]
+[ext_resource type="PackedScene" uid="uid://dkehmdnuxih2r" path="res://src/GameSession/MapView.tscn" id="4_xkg5j"]
+[ext_resource type="PackedScene" uid="uid://byq323jbel48u" path="res://src/GameSession/ProvinceOverviewPanel.tscn" id="5_osjnn"]
+[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"]
[node name="GameSession" type="Control" node_paths=PackedStringArray("_game_session_menu")]
-editor_description = "SS-102"
+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="GameSessionMenu" parent="." instance=ExtResource("3_bvmqh")]
visible = false
layout_mode = 1
@@ -36,5 +44,36 @@ anchor_bottom = 1.0
grow_horizontal = 0
grow_vertical = 0
-[connection signal="close_button_pressed" from="GameSessionMenu" to="." method="_on_game_session_menu_close_button_pressed"]
+[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="OptionsMenu" parent="." instance=ExtResource("6_p5mnx")]
+visible = false
+layout_mode = 1
+
+[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="province_selected" from="MapView" to="ProvinceOverviewPanel" method="_on_province_selected"]
+[connection signal="options_button_pressed" from="GameSessionMenu" to="OptionsMenu" method="show"]
[connection signal="game_session_menu_button_pressed" from="MapControlPanel" to="." method="_on_game_session_menu_button_pressed"]
+[connection signal="mapmode_changed" from="MapControlPanel" to="MapView" method="_update_colour_texture"]
+[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="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/GameSession/GameSessionMenu.gd b/game/src/GameSession/GameSessionMenu.gd
index 3722b52..6f373d7 100644
--- a/game/src/GameSession/GameSessionMenu.gd
+++ b/game/src/GameSession/GameSessionMenu.gd
@@ -1,19 +1,70 @@
extends PanelContainer
-signal close_button_pressed
-
@export var _main_menu_scene : PackedScene
-func _ready():
- print("GameSessionMenu ready")
+@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 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_to_main_menu_pressed():
+func _on_main_menu_confirmed() -> void:
get_tree().change_scene_to_packed(_main_menu_scene)
# REQUIREMENTS:
-# * UIFUN-69
-func _on_close_button_pressed():
- close_button_pressed.emit()
+# * 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()
diff --git a/game/src/GameSession/GameSessionMenu.tscn b/game/src/GameSession/GameSessionMenu.tscn
index 45fff4b..99f38df 100644
--- a/game/src/GameSession/GameSessionMenu.tscn
+++ b/game/src/GameSession/GameSessionMenu.tscn
@@ -1,28 +1,89 @@
-[gd_scene load_steps=3 format=3 uid="uid://dvdynl6eir40o"]
+[gd_scene load_steps=4 format=3 uid="uid://dvdynl6eir40o"]
+[ext_resource type="Theme" uid="uid://dndova5cw036e" 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 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="VBoxContainer" type="VBoxContainer" parent="."]
+[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="MainMenuButton" type="Button" parent="VBoxContainer"]
+[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
-text = "GAMESESSIONMENU_RESIGN"
+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="HSeparator" type="HSeparator" parent="VBoxContainer"]
+[node name="CloseSeparator" type="HSeparator" parent="ButtonListMargin/ButtonList"]
layout_mode = 2
+theme_type_variation = &"SessionSeparator"
-[node name="CloseButton" type="Button" parent="VBoxContainer"]
-editor_description = "UI-80"
+[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"
-[connection signal="pressed" from="VBoxContainer/MainMenuButton" to="." method="_on_to_main_menu_pressed"]
-[connection signal="pressed" from="VBoxContainer/CloseButton" to="." method="_on_close_button_pressed"]
+[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/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/GameSession/GameSpeedPanel.gd b/game/src/GameSession/GameSpeedPanel.gd
new file mode 100644
index 0000000..80708b1
--- /dev/null
+++ b/game/src/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/GameSession/GameSpeedPanel.tscn b/game/src/GameSession/GameSpeedPanel.tscn
new file mode 100644
index 0000000..8a37565
--- /dev/null
+++ b/game/src/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/GameSession/MapControlPanel.gd b/game/src/GameSession/MapControlPanel.gd
index ad56536..73d7e06 100644
--- a/game/src/GameSession/MapControlPanel.gd
+++ b/game/src/GameSession/MapControlPanel.gd
@@ -1,8 +1,59 @@
extends PanelContainer
signal game_session_menu_button_pressed
+signal mapmode_changed
+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-554
+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():
+func _on_game_session_menu_button_pressed() -> void:
game_session_menu_button_pressed.emit()
+
+# REQUIREMENTS:
+# * SS-76
+# * UIFUN-129, UIFUN-133
+func _mapmode_pressed(button : BaseButton) -> void:
+ GameSingleton.set_mapmode(button.tooltip_text)
+ mapmode_changed.emit()
+
+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/GameSession/MapControlPanel.tscn b/game/src/GameSession/MapControlPanel.tscn
index 71d43e7..18b1c3f 100644
--- a/game/src/GameSession/MapControlPanel.tscn
+++ b/game/src/GameSession/MapControlPanel.tscn
@@ -1,32 +1,110 @@
-[gd_scene load_steps=2 format=3 uid="uid://g524p8lr574w"]
+[gd_scene load_steps=7 format=3 uid="uid://g524p8lr574w"]
[ext_resource type="Script" path="res://src/GameSession/MapControlPanel.gd" id="1_ign64"]
+[ext_resource type="Texture2D" uid="uid://c0sm1jfu4kyv3" path="res://art/ui/minimap.png" id="2_r613r"]
+[ext_resource type="Script" path="res://src/GameSession/Minimap.gd" id="3_s4dml"]
+[ext_resource type="Texture2D" uid="uid://vr1hq2stk8ny" path="res://art/ui/minimap_frame.png" id="4_f1exl"]
-[node name="PanelContainer" type="PanelContainer"]
+[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"
+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="HBoxContainer" type="HBoxContainer" parent="."]
+[node name="Minimap" type="PanelContainer" parent="MapPanelMargin/MapPanelList/MapDisplayList"]
+editor_description = "UI-549"
layout_mode = 2
+mouse_filter = 1
-[node name="VBoxContainer" type="VBoxContainer" parent="HBoxContainer"]
+[node name="MinimapTexture" type="TextureRect" parent="MapPanelMargin/MapPanelList/MapDisplayList/Minimap"]
+editor_description = "UI-751, FS-338"
layout_mode = 2
+texture = ExtResource("2_r613r")
-[node name="MapmodesPlaceholder" type="Label" parent="HBoxContainer/VBoxContainer"]
+[node name="ViewportQuad" type="Control" parent="MapPanelMargin/MapPanelList/MapDisplayList/Minimap"]
layout_mode = 2
-text = "MAPMODES"
+mouse_filter = 2
+script = ExtResource("3_s4dml")
-[node name="MinimapPlaceholder" type="Label" parent="HBoxContainer/VBoxContainer"]
+[node name="Frame" type="NinePatchRect" parent="MapPanelMargin/MapPanelList/MapDisplayList/Minimap"]
layout_mode = 2
-text = "MINIMAP"
+texture = ExtResource("4_f1exl")
+draw_center = false
+patch_margin_left = 10
+patch_margin_top = 10
+patch_margin_right = 10
+patch_margin_bottom = 10
+axis_stretch_horizontal = 1
+axis_stretch_vertical = 1
-[node name="AuxiliaryPanel" type="VBoxContainer" parent="HBoxContainer"]
+[node name="AuxiliaryPanel" type="VBoxContainer" parent="MapPanelMargin/MapPanelList"]
editor_description = "UI-761"
layout_mode = 2
-[node name="GameSessionMenuButton" type="Button" parent="HBoxContainer/AuxiliaryPanel"]
+[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"
-[connection signal="pressed" from="HBoxContainer/AuxiliaryPanel/GameSessionMenuButton" to="." method="_on_game_session_menu_button_pressed"]
+[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/GameSession/MapView.gd b/game/src/GameSession/MapView.gd
new file mode 100644
index 0000000..e74ea59
--- /dev/null
+++ b/game/src/GameSession/MapView.gd
@@ -0,0 +1,274 @@
+extends Node3D
+
+signal province_selected(index : int)
+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"
+
+const _shader_param_province_index : StringName = &"province_index_tex"
+const _shader_param_province_colour : StringName = &"province_colour_tex"
+const _shader_param_hover_index : StringName = &"hover_index"
+const _shader_param_selected_index : StringName = &"selected_index"
+const _shader_param_terrain_tile_factor : StringName = &"terrain_tile_factor"
+
+@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_image_size : Vector2
+var _map_province_colour_image : Image
+var _map_province_colour_texture : ImageTexture
+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 map_material == null:
+ push_error("Map mesh is missing material!")
+ return
+ if not map_material is ShaderMaterial:
+ push_error("Invalid map mesh material class: ", map_material.get_class())
+ return
+ _map_shader_material = map_material
+
+ # Province index textures
+ var map_province_index_images := GameSingleton.get_province_index_images()
+ if map_province_index_images == null or map_province_index_images.is_empty():
+ push_error("Failed to get province index image!")
+ return
+ var province_index_texture := Texture2DArray.new()
+ if province_index_texture.create_from_images(map_province_index_images) != OK:
+ push_error("Failed to generate province index texture array!")
+ return
+ _map_shader_material.set_shader_parameter(_shader_param_province_index, province_index_texture)
+
+ # Province colour texture
+ _map_province_colour_image = GameSingleton.get_province_colour_image()
+ if _map_province_colour_image == null:
+ push_error("Failed to get province colour image!")
+ return
+ _map_province_colour_texture = ImageTexture.create_from_image(_map_province_colour_image)
+ _map_shader_material.set_shader_parameter(_shader_param_province_colour, _map_province_colour_texture)
+
+ if not _map_mesh_instance.mesh is MapMesh:
+ push_error("Invalid map mesh class: ", _map_mesh_instance.mesh.get_class(), "(expected MapMesh)")
+ return
+ _map_mesh = _map_mesh_instance.mesh
+
+ # Set map mesh size and get bounds
+ _map_image_size = Vector2(Vector2i(GameSingleton.get_width(), GameSingleton.get_height()))
+ _map_mesh.aspect_ratio = _map_image_size.x / _map_image_size.y
+ _map_shader_material.set_shader_parameter(_shader_param_terrain_tile_factor, _map_image_size.y / 64.0)
+ 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
+ ))
+
+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 _update_colour_texture() -> void:
+ GameSingleton.update_colour_image()
+ _map_province_colour_texture.update(_map_province_colour_image)
+
+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
+
+# 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):
+ var selected_index := GameSingleton.get_province_index_from_uv_coords(_mouse_pos_map)
+ _map_shader_material.set_shader_parameter(_shader_param_selected_index, selected_index)
+ province_selected.emit(selected_index)
+ 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 / _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(_shader_param_hover_index, hover_index)
+
+func _on_mouse_entered_viewport():
+ _mouse_over_viewport = true
+
+func _on_mouse_exited_viewport():
+ _mouse_over_viewport = false
+
+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/GameSession/MapView.tscn b/game/src/GameSession/MapView.tscn
new file mode 100644
index 0000000..c8934c5
--- /dev/null
+++ b/game/src/GameSession/MapView.tscn
@@ -0,0 +1,31 @@
+[gd_scene load_steps=6 format=3 uid="uid://dkehmdnuxih2r"]
+
+[ext_resource type="Script" path="res://src/GameSession/MapView.gd" id="1_exccw"]
+[ext_resource type="Shader" path="res://src/GameSession/TerrainMap.gdshader" id="1_upocn"]
+[ext_resource type="Texture2D" uid="uid://ckf222w5usrsu" path="res://art/terrain/farmlands.png" id="3_47mq1"]
+
+[sub_resource type="ShaderMaterial" id="ShaderMaterial_tayeg"]
+render_priority = 0
+shader = ExtResource("1_upocn")
+shader_parameter/hover_index = null
+shader_parameter/selected_index = null
+shader_parameter/terrain_tile_factor = null
+shader_parameter/farmlands_tex = ExtResource("3_47mq1")
+
+[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/GameSession/Minimap.gd b/game/src/GameSession/Minimap.gd
new file mode 100644
index 0000000..25c7cac
--- /dev/null
+++ b/game/src/GameSession/Minimap.gd
@@ -0,0 +1,89 @@
+extends Control
+
+signal minimap_clicked(pos_clicked : Vector2)
+
+const _action_click : StringName = &"map_click"
+
+var _viewport_points : PackedVector2Array
+
+# 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/GameSession/ProvinceOverviewPanel.gd b/game/src/GameSession/ProvinceOverviewPanel.gd
new file mode 100644
index 0000000..17da9d0
--- /dev/null
+++ b/game/src/GameSession/ProvinceOverviewPanel.gd
@@ -0,0 +1,95 @@
+extends PanelContainer
+
+@export var _province_name_label : Label
+@export var _region_name_label : Label
+@export var _life_rating_bar : ProgressBar
+@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.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);
+
+# 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(building : Dictionary) -> void:
+ const _building_key : StringName = &"building"
+ const _level_key : StringName = &"level"
+ const _expansion_state_key : StringName = &"expansion_state"
+ const _start_key : StringName = &"start"
+ const _end_key : StringName = &"end"
+ const _expansion_progress_key : StringName = &"expansion_progress"
+
+ const _expand_province_building : String = "EXPAND_PROVINCE_BUILDING"
+
+ var level_label := Label.new()
+ level_label.text = str(building.get(_level_key, 0))
+ _buildings_container.add_child(level_label)
+
+ var building_label := Label.new()
+ building_label.text = building.get(_building_key, _building_key + _missing_suffix)
+ _buildings_container.add_child(building_label)
+
+ var expansion_state : int = building.get(_expansion_state_key, CANNOT_EXPAND)
+ if expansion_state == PREPARING or expansion_state == EXPANDING:
+ var progress_bar := ProgressBar.new()
+ progress_bar.max_value = 1
+ progress_bar.value = building.get(_expansion_progress_key, 0)
+ progress_bar.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ _buildings_container.add_child(progress_bar)
+ else:
+ var expand_button := Button.new()
+ expand_button.text = _expand_province_building
+ expand_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ expand_button.disabled = expansion_state != CAN_EXPAND
+ expand_button.pressed.connect(func(): _expand_building(building_label.text))
+ _buildings_container.add_child(expand_button)
+
+func update_info() -> void:
+ const _province_key : StringName = &"province"
+ const _region_key : StringName = &"region"
+ const _life_rating_key : StringName = &"life_rating"
+ const _buildings_key : StringName = &"buildings"
+
+ const _life_rating_tooltip : String = "LIFE_RATING_TOOLTIP"
+
+ _province_info = GameSingleton.get_province_info_from_index(_selected_index)
+ if _province_info:
+ _province_name_label.text = _province_info.get(_province_key, _province_key + _missing_suffix)
+ _region_name_label.text = _province_info.get(_region_key, _region_key + _missing_suffix)
+
+ _life_rating_bar.value = _province_info.get(_life_rating_key, 0)
+ _life_rating_bar.tooltip_text = tr(_life_rating_tooltip) % _life_rating_bar.value
+
+ for child in _buildings_container.get_children():
+ _buildings_container.remove_child(child)
+ child.queue_free()
+ var buildings : Array = _province_info.get(_buildings_key, [])
+ for building in buildings:
+ _add_building(building)
+
+ show()
+ else:
+ hide()
+
+func _on_province_selected(index : int) -> void:
+ _selected_index = index
+
+func _on_close_button_pressed() -> void:
+ _selected_index = 0
diff --git a/game/src/GameSession/ProvinceOverviewPanel.tscn b/game/src/GameSession/ProvinceOverviewPanel.tscn
new file mode 100644
index 0000000..48e7c25
--- /dev/null
+++ b/game/src/GameSession/ProvinceOverviewPanel.tscn
@@ -0,0 +1,63 @@
+[gd_scene load_steps=2 format=3 uid="uid://byq323jbel48u"]
+
+[ext_resource type="Script" path="res://src/GameSession/ProvinceOverviewPanel.gd" id="1_3n8k5"]
+
+[node name="ProvinceOverviewPanel" type="PanelContainer" node_paths=PackedStringArray("_province_name_label", "_region_name_label", "_life_rating_bar", "_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
+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")
+_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
+
+[node name="CloseButton" type="Button" parent="PanelList/TopBarList"]
+custom_minimum_size = Vector2(30, 30)
+layout_mode = 2
+size_flags_vertical = 0
+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
+
+[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/GameSession/TerrainMap.gdshader b/game/src/GameSession/TerrainMap.gdshader
new file mode 100644
index 0000000..305a34b
--- /dev/null
+++ b/game/src/GameSession/TerrainMap.gdshader
@@ -0,0 +1,66 @@
+shader_type spatial;
+
+render_mode unshaded;
+
+// Cosmetic farmlands terrain texture
+uniform sampler2D farmlands_tex: source_color, repeat_enable, filter_linear;
+// Province index texture
+uniform sampler2DArray province_index_tex : source_color, repeat_enable, filter_nearest;
+// 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;
+// The number of times the terrain textures should tile vertically
+uniform float terrain_tile_factor;
+
+uvec2 vec2_to_uvec2(vec2 v) {
+ return uvec2(v * 255.0);
+}
+uvec2 read_uvec2(vec2 uv) {
+ float width_divisions = float(textureSize(province_index_tex, 0).z);
+ uv.x *= width_divisions;
+ float idx = mod(floor(uv.x), width_divisions);
+ return vec2_to_uvec2(texture(province_index_tex, vec3(uv, idx)).rg);
+}
+uint uvec2_to_uint(uvec2 v) {
+ return (v.y << 8u) | v.x;
+}
+
+const vec3 water_colour = vec3(0, 0, 1);
+
+vec3 get_terrain_colour(vec2 uv, vec2 corner, vec2 half_pixel_size, vec2 terrain_uv) {
+ uvec2 index_split = read_uvec2(fma(corner, half_pixel_size, uv));
+ uint index = uvec2_to_uint(index_split);
+ vec4 province_data = texelFetch(province_colour_tex, ivec2(index_split), 0);
+ vec3 province_colour = province_data.rgb;
+ float is_land = province_data.a;
+ vec3 farmlands_colour = texture(farmlands_tex, terrain_uv).rgb;
+ vec3 terrain_colour = mix(water_colour, farmlands_colour, is_land);
+ float mix_val = 0.4 + float(index == hover_index) * 0.2 + float(index == selected_index) * 0.2;
+ vec3 mixed_colour = mix(terrain_colour, province_colour, mix_val);
+ return mixed_colour;
+}
+
+vec3 mix_terrain_colour(vec2 uv) {
+ vec2 map_size = vec2(textureSize(province_index_tex, 0).xy);
+ 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;
+}