aboutsummaryrefslogtreecommitdiff
path: root/game
diff options
context:
space:
mode:
author Hop311 <hop3114@gmail.com>2023-03-30 23:50:50 +0200
committer Spartan322 <Megacake1234@gmail.com>2023-04-14 17:16:02 +0200
commitc0d76b78d3762e6eec3ed1c62618be84c5b7559b (patch)
treeacfcbeedd5a47136acdf883e791a297200b7d1b8 /game
parent1f04a7827ae377372cb491ff0257a47d0d4c2967 (diff)
Add terrain map
With Directional movement using WASD With Directional movement using arrow keys With Click-Drag movement using middle mouse button With Province identifiers With Province shape loading With Province rendering With Province selection With Province overview panel With Color lookup texture
Diffstat (limited to 'game')
-rw-r--r--game/common/map/provinces.json9
-rw-r--r--game/common/map/provinces.pngbin0 -> 9464 bytes
-rw-r--r--game/common/map/provinces.png.import3
-rw-r--r--game/common/map/terrain/terrain.pngbin0 -> 27542091 bytes
-rw-r--r--game/common/map/terrain/terrain.png.import34
-rw-r--r--game/localisation/en_GB/menus.csv1
-rw-r--r--game/localisation/en_GB/provinces.csv9
-rw-r--r--game/localisation/en_GB/provinces.csv.import3
-rw-r--r--game/project.godot49
-rw-r--r--game/src/Autoload/Events.gd9
-rw-r--r--game/src/GameSession/GameSession.gd2
-rw-r--r--game/src/GameSession/GameSession.tscn24
-rw-r--r--game/src/GameSession/MapControlPanel.tscn9
-rw-r--r--game/src/GameSession/MapView.gd185
-rw-r--r--game/src/GameSession/MapView.tscn27
-rw-r--r--game/src/GameSession/ProvinceOverviewPanel.gd22
-rw-r--r--game/src/GameSession/ProvinceOverviewPanel.tscn40
-rw-r--r--game/src/GameSession/TerrainMap.gdshader48
18 files changed, 462 insertions, 12 deletions
diff --git a/game/common/map/provinces.json b/game/common/map/provinces.json
new file mode 100644
index 0000000..66de29b
--- /dev/null
+++ b/game/common/map/provinces.json
@@ -0,0 +1,9 @@
+{
+ "prov_britain": [150, 0, 0],
+ "prov_ireland": [23, 147, 31],
+ "prov_iceland": "343D91",
+ "prov_cuba": "1E29FF",
+ "prov_madagascar": "790091",
+ "prov_ceylon": "FF6A00",
+ "prov_formosa": "82B1FF"
+}
diff --git a/game/common/map/provinces.png b/game/common/map/provinces.png
new file mode 100644
index 0000000..68bf528
--- /dev/null
+++ b/game/common/map/provinces.png
Binary files differ
diff --git a/game/common/map/provinces.png.import b/game/common/map/provinces.png.import
new file mode 100644
index 0000000..8dd0c09
--- /dev/null
+++ b/game/common/map/provinces.png.import
@@ -0,0 +1,3 @@
+[remap]
+
+importer="keep"
diff --git a/game/common/map/terrain/terrain.png b/game/common/map/terrain/terrain.png
new file mode 100644
index 0000000..6d36dee
--- /dev/null
+++ b/game/common/map/terrain/terrain.png
Binary files differ
diff --git a/game/common/map/terrain/terrain.png.import b/game/common/map/terrain/terrain.png.import
new file mode 100644
index 0000000..56ba5dc
--- /dev/null
+++ b/game/common/map/terrain/terrain.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cmw0pvjthnn8c"
+path="res://.godot/imported/terrain.png-c443b8a709ca7acc4b3bb3df1d3095a9.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://common/map/terrain/terrain.png"
+dest_files=["res://.godot/imported/terrain.png-c443b8a709ca7acc4b3bb3df1d3095a9.ctex"]
+
+[params]
+
+compress/mode=3
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=2
+compress/channel_pack=0
+mipmaps/generate=true
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=0
diff --git a/game/localisation/en_GB/menus.csv b/game/localisation/en_GB/menus.csv
index 27a0858..b483662 100644
--- a/game/localisation/en_GB/menus.csv
+++ b/game/localisation/en_GB/menus.csv
@@ -1,3 +1,4 @@
+
,, Main Menu
MAINMENU_TITLE,OpenVic2
MAINMENU_NEW_GAME,New Game
diff --git a/game/localisation/en_GB/provinces.csv b/game/localisation/en_GB/provinces.csv
new file mode 100644
index 0000000..3b47955
--- /dev/null
+++ b/game/localisation/en_GB/provinces.csv
@@ -0,0 +1,9 @@
+
+,, Test Provinces
+prov_britain_NAME,Britain
+prov_ireland_NAME,Ireland
+prov_iceland_NAME,Iceland
+prov_cuba_NAME,Cuba
+prov_madagascar_NAME,Madagascar
+prov_ceylon_NAME,Ceylon
+prov_formosa_NAME,Formosa
diff --git a/game/localisation/en_GB/provinces.csv.import b/game/localisation/en_GB/provinces.csv.import
new file mode 100644
index 0000000..8dd0c09
--- /dev/null
+++ b/game/localisation/en_GB/provinces.csv.import
@@ -0,0 +1,3 @@
+[remap]
+
+importer="keep"
diff --git a/game/project.godot b/game/project.godot
index 363bbc1..fa2871f 100644
--- a/game/project.godot
+++ b/game/project.godot
@@ -44,6 +44,55 @@ enabled=PackedStringArray("res://addons/keychain/plugin.cfg", "res://addons/open
theme/custom="res://theme/default_theme.tres"
+[input]
+
+map_north={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194320,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":87,"physical_keycode":0,"key_label":0,"unicode":119,"echo":false,"script":null)
+]
+}
+map_east={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194321,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":68,"physical_keycode":0,"key_label":0,"unicode":100,"echo":false,"script":null)
+]
+}
+map_south={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194322,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":83,"physical_keycode":0,"key_label":0,"unicode":115,"echo":false,"script":null)
+]
+}
+map_west={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194319,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":65,"physical_keycode":0,"key_label":0,"unicode":97,"echo":false,"script":null)
+]
+}
+map_zoomin={
+"deadzone": 0.5,
+"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":8,"position":Vector2(174, 17),"global_position":Vector2(180, 80),"factor":1.0,"button_index":4,"pressed":true,"double_click":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":81,"physical_keycode":0,"key_label":0,"unicode":113,"echo":false,"script":null)
+]
+}
+map_zoomout={
+"deadzone": 0.5,
+"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":16,"position":Vector2(325, 24),"global_position":Vector2(331, 87),"factor":1.0,"button_index":5,"pressed":true,"double_click":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":69,"physical_keycode":0,"key_label":0,"unicode":101,"echo":false,"script":null)
+]
+}
+map_drag={
+"deadzone": 0.5,
+"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":3,"pressed":false,"double_click":false,"script":null)
+]
+}
+map_click={
+"deadzone": 0.5,
+"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"pressed":false,"double_click":false,"script":null)
+]
+}
+
[internationalization]
locale/translation_remaps={}
diff --git a/game/src/Autoload/Events.gd b/game/src/Autoload/Events.gd
index 25a185f..040cb06 100644
--- a/game/src/Autoload/Events.gd
+++ b/game/src/Autoload/Events.gd
@@ -2,3 +2,12 @@ extends Node
var Options = preload("Events/Options.gd").new()
var Localisation = preload("Events/Localisation.gd").new()
+
+const _province_identifier_file : String = "res://common/map/provinces.json"
+const _province_shape_file : String = "res://common/map/provinces.png"
+
+func _ready():
+ if MapSingleton.load_province_identifier_file(_province_identifier_file) != OK:
+ push_error("Failed to load province identifiers")
+ if MapSingleton.load_province_shape_file(_province_shape_file) != OK:
+ push_error("Failed to load province shapes")
diff --git a/game/src/GameSession/GameSession.gd b/game/src/GameSession/GameSession.gd
index 0d69bf2..38eaba1 100644
--- a/game/src/GameSession/GameSession.gd
+++ b/game/src/GameSession/GameSession.gd
@@ -1,4 +1,4 @@
-extends Control
+extends Node
@export var _game_session_menu : Control
diff --git a/game/src/GameSession/GameSession.tscn b/game/src/GameSession/GameSession.tscn
index f984daf..390040e 100644
--- a/game/src/GameSession/GameSession.tscn
+++ b/game/src/GameSession/GameSession.tscn
@@ -1,23 +1,20 @@
-[gd_scene load_steps=4 format=3 uid="uid://bgnupcshe1m7r"]
+[gd_scene load_steps=6 format=3 uid="uid://bgnupcshe1m7r"]
[ext_resource type="Script" path="res://src/GameSession/GameSession.gd" id="1_eklvp"]
[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"]
-[node name="GameSession" type="Control" node_paths=PackedStringArray("_game_session_menu")]
+[node name="GameSession" type="Node" node_paths=PackedStringArray("_game_session_menu")]
editor_description = "SS-102"
-layout_mode = 3
-anchors_preset = 15
-anchor_right = 1.0
-anchor_bottom = 1.0
-grow_horizontal = 2
-grow_vertical = 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
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
@@ -27,7 +24,6 @@ grow_horizontal = 2
grow_vertical = 2
[node name="MapControlPanel" parent="." instance=ExtResource("3_afh6d")]
-layout_mode = 1
anchors_preset = 3
anchor_left = 1.0
anchor_top = 1.0
@@ -36,5 +32,13 @@ anchor_bottom = 1.0
grow_horizontal = 0
grow_vertical = 0
+[node name="ProvinceOverviewPanel" parent="." instance=ExtResource("5_osjnn")]
+anchors_preset = -1
+anchor_top = 0.583333
+anchor_right = 0.15625
+offset_top = 0.0
+offset_right = 0.0
+
+[connection signal="province_selected" from="MapView" to="ProvinceOverviewPanel" method="_on_province_selected"]
[connection signal="close_button_pressed" from="GameSessionMenu" to="." method="_on_game_session_menu_close_button_pressed"]
[connection signal="game_session_menu_button_pressed" from="MapControlPanel" to="." method="_on_game_session_menu_button_pressed"]
diff --git a/game/src/GameSession/MapControlPanel.tscn b/game/src/GameSession/MapControlPanel.tscn
index 71d43e7..2a0c971 100644
--- a/game/src/GameSession/MapControlPanel.tscn
+++ b/game/src/GameSession/MapControlPanel.tscn
@@ -1,7 +1,13 @@
-[gd_scene load_steps=2 format=3 uid="uid://g524p8lr574w"]
+[gd_scene load_steps=4 format=3 uid="uid://g524p8lr574w"]
[ext_resource type="Script" path="res://src/GameSession/MapControlPanel.gd" id="1_ign64"]
+[sub_resource type="InputEventAction" id="InputEventAction_5nck3"]
+action = &"ui_cancel"
+
+[sub_resource type="Shortcut" id="Shortcut_fc1tk"]
+events = [SubResource("InputEventAction_5nck3")]
+
[node name="PanelContainer" type="PanelContainer"]
editor_description = "SS-103"
script = ExtResource("1_ign64")
@@ -27,6 +33,7 @@ layout_mode = 2
[node name="GameSessionMenuButton" type="Button" parent="HBoxContainer/AuxiliaryPanel"]
editor_description = "UI-9"
layout_mode = 2
+shortcut = SubResource("Shortcut_fc1tk")
text = "ESC"
[connection signal="pressed" from="HBoxContainer/AuxiliaryPanel/GameSessionMenuButton" to="." method="_on_game_session_menu_button_pressed"]
diff --git a/game/src/GameSession/MapView.gd b/game/src/GameSession/MapView.gd
new file mode 100644
index 0000000..faf90e8
--- /dev/null
+++ b/game/src/GameSession/MapView.gd
@@ -0,0 +1,185 @@
+extends Node3D
+
+signal province_selected(identifier : String)
+
+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_zoomin : StringName = &"map_zoomin"
+const _action_zoomout : StringName = &"map_zoomout"
+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_pos : StringName = &"hover_pos"
+const _shader_param_selected_pos : StringName = &"selected_pos"
+
+@export var _camera : Camera3D
+
+@export var _cardinal_move_speed : float = 1.0
+@export var _edge_move_threshold: float = 0.15
+@export var _edge_move_speed: float = 2.5
+var _drag_anchor : Vector2
+var _drag_active : bool = false
+
+@export var _zoom_target_min : float = 0.2
+@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_index_image : Image
+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)
+
+func _ready():
+ if _camera == null:
+ push_error("MapView's _camera variable hasn't been set!")
+ return
+ if _map_mesh_instance == null:
+ push_error("MapView's _map_mesh variable hasn't been set!")
+ return
+ 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(MapSingleton.get_width(), MapSingleton.get_height()))
+ _map_mesh.aspect_ratio = _map_image_size.x / _map_image_size.y
+ 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
+ ))
+
+ 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 texture
+ _map_province_index_image = MapSingleton.get_province_index_image()
+ if _map_province_index_image == null:
+ push_error("Failed to get province index image!")
+ return
+ var province_index_texture := ImageTexture.create_from_image(_map_province_index_image)
+ _map_shader_material.set_shader_parameter(_shader_param_province_index, province_index_texture)
+ # Province colour texture
+ var province_colour_image = MapSingleton.get_province_colour_image()
+ if province_colour_image == null:
+ push_error("Failed to get province colour image!")
+ return
+ var province_colour_texture := ImageTexture.create_from_image(province_colour_image)
+ _map_shader_material.set_shader_parameter(_shader_param_province_colour, province_colour_texture)
+
+func _unhandled_input(event : InputEvent):
+ if event.is_action_pressed(_action_click):
+ # Check if the mouse is outside of bounds
+ if _map_mesh.is_valid_uv_coord(_mouse_pos_map):
+ _map_shader_material.set_shader_parameter(_shader_param_selected_pos, _mouse_pos_map)
+ var mouse_pixel_pos := Vector2i(_mouse_pos_map * _map_image_size)
+ var province_identifier := MapSingleton.get_province_identifier_from_pixel_coords(mouse_pixel_pos)
+ province_selected.emit(province_identifier)
+ 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_zoomin, true):
+ _zoom_target -= _zoom_target_step
+ elif event.is_action_pressed(_action_zoomout, true):
+ _zoom_target += _zoom_target_step
+
+func _physics_process(delta : float):
+ _mouse_pos_viewport = get_viewport().get_mouse_position()
+ # Process movement
+ _movement_process(delta)
+ # Keep within map bounds
+ _clamp_over_map()
+ # Process zooming
+ _zoom_process(delta)
+ # Orient based on height
+ _update_orientation()
+ # Calculate where the mouse lies on the map
+ _update_mouse_map_position()
+
+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)
+
+func _edge_scrolling_vector() -> Vector2:
+ var viewport_dims := Vector2(Resolution.get_current_resolution())
+ var mouse_vector := _mouse_pos_viewport / viewport_dims - Vector2(0.5, 0.5);
+ if pow(mouse_vector.x, 4) + pow(mouse_vector.y, 4) < pow(0.5 - _edge_move_threshold, 4):
+ mouse_vector *= 0
+ return mouse_vector * _edge_move_speed
+
+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)
+
+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 * 2.0 + 0.5))
+ _camera.look_at(_camera.position + dir)
+
+func _update_mouse_map_position() -> void:
+ var ray_origin := _camera.project_ray_origin(_mouse_pos_viewport)
+ var ray_normal := _camera.project_ray_normal(_mouse_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:
+ var intersection_vec := intersection as Vector3
+ # This loops both horizontally (good) and vertically (bad)
+ _mouse_pos_map = (Vector2(intersection_vec.x, intersection_vec.z) - _map_mesh_corner) / _map_mesh_dims
+ _map_shader_material.set_shader_parameter(_shader_param_hover_pos, _mouse_pos_map)
+ 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)
diff --git a/game/src/GameSession/MapView.tscn b/game/src/GameSession/MapView.tscn
new file mode 100644
index 0000000..4650acb
--- /dev/null
+++ b/game/src/GameSession/MapView.tscn
@@ -0,0 +1,27 @@
+[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://cmw0pvjthnn8c" path="res://common/map/terrain/terrain.png" id="3_l8pnf"]
+
+[sub_resource type="ShaderMaterial" id="ShaderMaterial_tayeg"]
+render_priority = 0
+shader = ExtResource("1_upocn")
+shader_parameter/hover_pos = Vector2(0.5, 0.5)
+shader_parameter/selected_pos = Vector2(0.5, 0.5)
+shader_parameter/terrain_tex = ExtResource("3_l8pnf")
+
+[sub_resource type="MapMesh" id="MapMesh_3gtsd"]
+
+[node name="MapView" type="Node3D" node_paths=PackedStringArray("_camera", "_map_mesh_instance")]
+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, 1, 1)
+
+[node name="MapMeshInstance" type="MeshInstance3D" parent="."]
+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/ProvinceOverviewPanel.gd b/game/src/GameSession/ProvinceOverviewPanel.gd
new file mode 100644
index 0000000..434f6b1
--- /dev/null
+++ b/game/src/GameSession/ProvinceOverviewPanel.gd
@@ -0,0 +1,22 @@
+extends Panel
+
+@export var _province_name_label : Label
+
+@export var province_identifier: String = "":
+ get: return province_identifier
+ set(v):
+ province_identifier = v
+ update_info()
+
+func _ready():
+ update_info()
+
+func update_info() -> void:
+ _province_name_label.text = province_identifier + "_NAME"
+ visible = not province_identifier.is_empty()
+
+func _on_province_selected(identifier : String) -> void:
+ province_identifier = identifier
+
+func _on_button_pressed() -> void:
+ province_identifier = ""
diff --git a/game/src/GameSession/ProvinceOverviewPanel.tscn b/game/src/GameSession/ProvinceOverviewPanel.tscn
new file mode 100644
index 0000000..e21b1c3
--- /dev/null
+++ b/game/src/GameSession/ProvinceOverviewPanel.tscn
@@ -0,0 +1,40 @@
+[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="Panel" node_paths=PackedStringArray("_province_name_label")]
+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("VBoxContainer/ProvinceName")
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+layout_mode = 1
+anchors_preset = -1
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 10.0
+offset_top = 5.0
+offset_right = -10.0
+offset_bottom = -5.0
+
+[node name="ProvinceName" type="Label" parent="VBoxContainer"]
+layout_mode = 2
+text = "PROVINCE_NAME"
+vertical_alignment = 1
+
+[node name="Button" type="Button" parent="."]
+custom_minimum_size = Vector2(30, 30)
+layout_mode = 1
+anchors_preset = -1
+anchor_left = 0.85
+anchor_right = 1.0
+anchor_bottom = 0.103333
+grow_horizontal = 0
+text = "X"
+
+[connection signal="pressed" from="Button" to="." method="_on_button_pressed"]
diff --git a/game/src/GameSession/TerrainMap.gdshader b/game/src/GameSession/TerrainMap.gdshader
new file mode 100644
index 0000000..a6774fd
--- /dev/null
+++ b/game/src/GameSession/TerrainMap.gdshader
@@ -0,0 +1,48 @@
+shader_type spatial;
+
+render_mode unshaded;
+
+// Cosmetic terrain texture
+uniform sampler2D terrain_tex: source_color, repeat_enable, filter_linear;
+// Province index texture
+uniform sampler2D province_index_tex : repeat_enable, filter_nearest;
+// Province colour texture
+uniform sampler2D province_colour_tex: source_color, repeat_enable, filter_nearest;
+// Position of the mouse over the map mesh in UV coords
+uniform vec2 hover_pos;
+// Position in UV coords of a pixel belonging to the currently selected province
+uniform vec2 selected_pos;
+
+uvec2 vec2_to_uvec2(vec2 v) {
+ return uvec2(v * 255.0);
+}
+
+uint uvec2_to_uint(uvec2 v) {
+ return (v.y << 8u) | v.x;
+}
+
+uvec2 read_uvec2(sampler2D tex, vec2 uv) {
+ return vec2_to_uvec2(texture(tex, uv).rg);
+}
+
+uint read_uint16(sampler2D tex, vec2 uv) {
+ return uvec2_to_uint(read_uvec2(tex, uv));
+}
+
+void fragment() {
+ uvec2 prov_idx_split = read_uvec2(province_index_tex, UV);
+
+ uint prov_index = uvec2_to_uint(prov_idx_split);
+ uint hover_index = read_uint16(province_index_tex, hover_pos);
+ uint selected_index = read_uint16(province_index_tex, selected_pos);
+
+ // Boost prov_colour's contribution if it matches hover_colour or selected_colour
+ float mix_val = float(prov_index == hover_index) * 0.3 + float(prov_index == selected_index) * 0.5;
+ // Don't mix if the province index is 0
+ mix_val *= float(prov_index != 0u);
+
+ vec3 terrain_colour = texture(terrain_tex, UV).rgb;
+ vec3 province_colour = texelFetch(province_colour_tex, ivec2(prov_idx_split), 0).rgb;
+
+ ALBEDO = mix(terrain_colour, province_colour, mix_val);
+}